缓存_缓存雪崩 - CSDN
缓存 订阅
缓存(cache),原始意义是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。缓存的设置是所有现代计算机系统发挥高性能的重要因素之一。 展开全文
缓存(cache),原始意义是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。缓存的设置是所有现代计算机系统发挥高性能的重要因素之一。
信息
外文名
cache
类    别
高速缓存、磁盘缓存
原    理
程序局部性原理
中文名
缓存
学    科
计算机
目    的
提高数据存取速度
缓存简介
缓存是指可以进行高速数据交换的存储器,它先于内存与CPU交换数据,因此速率很快。L1 Cache(一级缓存)是CPU第一层高速缓存。内置的L1高速缓存的容量和结构对CPU的性能影响较大,不过高速缓冲存储器均由静态RAM组成,结构较复杂,在CPU管芯面积不能太大的情况下,L1级高速缓存的容量不可能做得太大。一般L1缓存的容量通常在32—256KB。L2 Cache(二级缓存)是CPU的第二层高速缓存,分内部和外部两种芯片。内部的芯片二级缓存运行速率与主频相同,而外部的二级缓存则只有主频的一半。L2高速缓存容量也会影响CPU的性能,原则是越大越好,普通台式机CPU的L2缓存一般为128KB到2MB或者更高,笔记本、服务器和工作站上用CPU的L2高速缓存最高可达1MB-3MB。由于高速缓存的速度越高价格也越贵,故有的计算机系统中设置了两级或多级高速缓存。紧靠内存的一级高速缓存的速度最高,而容量最小,二级高速缓存的容量稍大,速度也稍低 [1]  。缓存只是内存中少部分数据的复制品,所以CPU到缓存中寻找数据时,也会出现找不到的情况(因为这些数据没有从内存复制到缓存中去),这时CPU还是会到内存中去找数据,这样系统的速率就慢下来了,不过CPU会把这些数据复制到缓存中去,以便下一次不要再到内存中去取。随着时间的变化,被访问得最频繁的数据不是一成不变的,也就是说,刚才还不频繁的数据,此时已经需要被频繁的访问,刚才还是最频繁的数据,又不频繁了,所以说缓存中的数据要经常按照一定的算法来更换,这样才能保证缓存中的数据是被访问最频繁的。
收起全文
精华内容
参与话题
  • Java面试——缓存

    万次阅读 多人点赞 2020-10-14 22:18:43
    一、什么是缓存? ☞ 缓存就是数据交换的缓冲区(称作:Cache),当某一硬件要读取数据时,会首先从缓存汇总查询数据,有则直接执行,不存在时从内存中获取。由于缓存的数据比内存快的多,所以缓存的作用就是帮助...

    一、什么是缓存


    【1】缓存就是数据交换的缓冲区(称作:Cache),当某一硬件要读取数据时,会首先从缓存中查询数据,有则直接执行,不存在时从磁盘中获取。由于缓存的数据比磁盘快的多,所以缓存的作用就是帮助硬件更快的运行。
    【2】缓存往往使用的是RAM(断电既掉的非永久存储),所以在用完后还是会把文件送到硬盘等存储器中永久存储。电脑中最大缓存就是内存条,硬盘上也有16M或者32M的缓存。
    【3】高速缓存是用来协调CPU与主存之间存取速度的差异而设置的。一般CPU工作速度高,但内存的工作速度相对较低,为了解决这个问题,通常使用高速缓存,高速缓存的存取速度介于CPU与主存之间。系统将一些CPU在最近几个时间段经常访问的内容存在高速缓存,这样就在一定程度上缓解了由于主存速度低造成的CPU“停工待料”的情况。
    【4】缓存就是把一些外存上的数据保存在内存上而已,为什么保存在内存上,我们运行的所有程序里面的变量都是存放在内存中的,所以如果想将值放入内存上,可以通过变量的方式存储。在JAVA中一些缓存一般都是通过Map集合来实现的。
     ▁▂▃▅▆ 缓存在不同的场景下,作用是不一样的具体举例说明:
             ✔  操作系统磁盘缓存 ——> 减少磁盘机械操作。
             ✔  数据库缓存——>减少文件系统IO。
             ✔  应用程序缓存——>减少对数据库的查询。
             ✔  Web服务器缓存——>减少应用服务器请求。
             ✔  客户端浏览器缓存——>减少对网站的访问。
            

    二、常见的缓存策略有哪些,如何做到缓存(比如redis)与DB里的数据一致性,你们项目中用到了什么缓存系统,如何设计的。


    【1】由于不同系统的数据访问模式不同,同一种缓存策略很难在不同的数据访问模式下取得满意的性能,研究人员提出不同缓存策略以适应不同的需求。缓存策略的分类:
       1)、基于访问的时间:此类算法按各缓存项被访问时间来组织缓存队列,决定替换对象。如 LRU;
       2)、基于访问频率:此类算法用缓存项的被访问频率来组织缓存。如 LFU、LRU2、2Q、LIRS;
       3)、访问时间与频率兼顾:通过兼顾访问时间和频率。使得数据模式在变化时缓存策略仍有较好性能。如 FBR、LRUF、ALRFU。多数此类算法具有一个可调或自适应参数,通过该参数的调节使缓存策略在基于访问时间与频率间取得一个平衡;
       4)、基于访问模式:某些应用有较明确的数据访问特点,进而产生与其相适应的缓存策略。如专用的 VoD 系统设计的A&L缓存策略,同时适应随机、顺序两种访问模式的 SARC策略;
    【2】数据不一致性产生的原因: 1)、先操作缓存,再写数据库成功之前,如果有读请求发生,可能导致旧数据入缓存,引发数据不一致。在分布式环境下,数据的读写都是并发的,一个服务多机器部署,对同一个数据进行读写,在数据库层面并不能保证完成顺序,就有可能后读的操作先完成(读取到的是脏数据),如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
    解决办法①、可采用更新前后双删除缓存策略;②、可以通过“串行化”解决,保证同一个数据的读写落在同一个后端服务上;
      2)、先操作数据库,再清除缓存。如果删缓存失败了,就会出现数据不一致问题
    方案一将删除失败的 key 值存入队列中重复删除,如下图:
     
      (1)更新数据库数据。
      (2)缓存因为种种问题删除失败。
      (3)将需要删除的key发送至消息队列。
      (4)自己消费消息,获得需要删除的key。
      (5)继续重试删除操作,直到成功。
    【缺点】:对业务线代码造成大量的侵入。于是有了方案二。
    方案二通过订阅 binlog 获取需要重新删除的 Key 值数据。在应用程序中,另起一段程序,获得这个订阅程序传来的消息,进行删除缓存操作。
     
      (1)更新数据库数据
      (2)数据库会将操作信息写入binlog日志当中
      (3)订阅程序提取出所需要的数据以及key
      (4)另起一段非业务代码,获得该信息
      (5)尝试删除缓存操作,发现删除失败
      (6)将这些信息发送至消息队列
      (7)重新从消息队列中获得该数据,重试操作

    三、如何防止缓存穿透、缓存击穿、缓存雪崩和缓存刷新。


    1】缓存穿透:缓存穿透是说收到一个请求,但是该请求缓存中不存在,只能去数据库中查询,然后放进缓存。但当有好多请求同时访问同一个数据时,业务系统把这些请求全发到了数据库;或者恶意构造一个逻辑上不存在的数据,然后大量发送这个请求,这样每次都会被发送到数据库,最终导致数据库挂掉。
    解决的办法对于恶意访问,一种思路是先做校验,对恶意数据直接过滤掉,不要发送至数据库层;第二种思路是缓存空结果,就是对查询不存在的数据也记录在缓存中,这样就可以有效的减少查询数据库的次数。非恶意访问,结合缓存击穿说明。
    【2】缓存击穿:上面提到的某个数据没有,然后好多请求查询数据库,可以归为缓存击穿的范畴:对于热点数据,当缓存失效的一瞬间,所有的请求都被下放到数据库去请求更新缓存,数据库被压垮。
    解决的办法防范此类问题,一种思路是加全局锁,就是所有访问某个数据的请求都共享一个锁,获得锁的那个才有资格去访问数据库,其他线程必须等待。但现在大部分系统都是分布式的,本地锁无法控制其他服务器也等待,所以要用到全局锁,比如 Redis的 setnx实现全局锁。另一种思想是对即将过期的数据进行主动刷新,比如新起一个线程轮询数据,或者比如把所有的数据划分为不同的缓存区间,定期分区间刷新数据。第二个思路与缓存雪崩有点关系。
    【3】缓存雪崩:缓存雪崩是指当我们给所有的缓存设置了同样的过期时间,当某一时刻,整个缓存的数据全部过期了,然后瞬间所有的请求都被抛向了数据库,数据库就崩掉了。
    解决的办法解决思路要么是分治,划分更小的缓存区间,按区间过期;要么给每个 key的过期时间加一个随机值,避免同时过期,达到错峰刷新缓存的目的。

    对于 Redis 挂掉了,请求全部走数据库,也属于缓存雪崩,我们可以有以下思路进行解决:
        事发前:实现 Redis 的高可用(主从架构+Sentinel 或者 Redis Cluster),尽可能避免 Redis 挂掉这种情况。
        事发中:万一 Redis 真的挂了,我们可以设置本地缓存(ehcache)+ 限流(hystrix),尽量避免我们的数据库被干掉。
        事发后:Redis 持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。

    【4】缓存刷新:既清空缓存 ,一般在 Insert、Update、Delete 操作后就需要刷新缓存,如果不执行就会出现脏数据。但当缓存请求的系统蹦掉后,返回给缓存的值为null。

    四、Redis内存用完会发生什么


    如果达到设置的上限,Redis 的写命令会返回错误信息(但是读命令还是可以正常返回),或者将 Redis 当缓存使用,配置缓存淘汰机制,当 Redis 达到内存的上线时会冲掉旧的数据。

    五、Redis 的 List 结构相关的操作


    【1】PUSH操作:是从队列头部和尾部增加节点的操作。
       ①、RPUSH KEY VALUE [VALUE ...] :从队列的右端入队一个或者多个数据,如果 key值不存在,会自动创建一个空的列表。如果对应的 key不是一个List,则会返回一个错误。
       ②、LPUSH KEY VALUE [VALUE...] :从队列的左边入队一个或多个元素。复杂度O(1)。
       ③、RPUSHX KEY VALUE:从队列的右边入队一个元素,仅队列存在时有效,当队列不存在时,不进行任何操作。
       ④、LPUSHX KEY VALUE:从队列的左边入队一个元素,仅队列存在时有效。当队列不存在时,不进行任何操作。
    【2】POP操作:获取并删除头尾节点的操作。
       ①、LPOP KEY:从队列左边出队一个元素,复杂度O(1)。如果list为空,则返回nil。
       ②、RPOP KEY:从队列的右边出队一个元素,复杂度O(1)。如果list为空,则返回nil。
       ③、BLPOP KEY[KEY...] TIMEOUT:删除&获取KEY中最左边的第一个元素,当队列为空时,阻塞TIMEOUT时间,单位是秒(这个时间内尝试获取KEY中的数据),超过TIMEOUT后如果仍未数据则返回(nil)。

    1 redis> BLPOP queue 1
    2 (nil)
    3 (1.10s)

       ④、BRPOP KEY[KEY...] TIMEOUT:删除&获取KEY中最后一个元素,或阻塞TIMEOUT。如上↑
    【3】POP and PUSH
       ①、RPOPLPUSH KEY1 KEY2:删除KEY1中最后一个元素,将其追加到KEY2的最左端。
       ②、BRPOPLPUSH KEY1 KEY2 TIMEOUT:弹出KEY1列表的值,将它推到KEY2列表,并返回它;或阻塞TIMEOUT时间,直到有一个可用。
    【4】其他
       ①、LLEN KEY:获取队列(List)的长度。
       ②、LRANG KEY START STOP:从列表中获取指定(START-STOP)长度的元素。负数表示从右向左数。需要注意的是,超出范围的下标不会产生错误:如果start>end,会得到空列表,如果end超过队尾,则Redis会将其当做列表的最后一个元素。

     1 redis> rpush q1 a b c d f e g
     2 (integer) 7
     3 redis> lrange q1 0 -1
     4 1) "a"
     5 2) "b"
     6 3) "c"
     7 4) "d"
     8 5) "f"
     9 6) "e"
    10 7) "g"

     ③、 LINDEX KEY INDEX:获取一个元素,通过其索引列表。我们之前介绍的操作都是对 list的两端进行的,所以算法复杂度都只有O(1)。而这个操作是指定位置来进行的,每次操作,list都得找到对应的位置,因此算法复杂度为O(N)。list的下表是从0开始的,index为负的时候是从右向左数。-1表示最后一个元素。当下标超出的时候,会返回nul。所以不用像操作数组一样担心范围越界的情况。
     ④、LSET KEY INDEX:重置队列中 INDEX位置的值。当 index越界的时候,这里会报异常。
     ⑤、LREM KEY COUNT VALUE:从列表中删除COUNT个VALUE元素。COUNT参数有三种情况:
              ☛ count > 0: 表示从头向尾(左到右)移除值为value的元素。
              ☛ count < 0: 表示从尾向头(右向左)移除值为value的元素。
              ☛ count = 0: 表示移除所有值为value的元素。
      ⑥、LTRIM KEY START STOP:修剪到指定范围内的清单,相当与截取,只保留START-STOP之间的数据。

     1 redis> rpush q a b c d e f g
     2 (integer) 7
     3 redis> lrange q 0 -1
     4 1) "a"
     5 2) "b"
     6 3) "c"
     7 4) "d"
     8 5) "e"
     9 6) "f"
    10 7) "g"
    11 redis> ltrim q 1 4
    12 OK
    13 redis> lrange q 0 -1
    14 1) "b"
    15 2) "c"
    16 3) "d"
    17 4) "e"

      ⑦、LINSERT KEY BEFORE|AFTER 元素 VALUE:在列表中的另一个元素之前或之后插入VAULE。当 key 不存在时,这个List被视为空列表,任何操作都不会发生。当key存在,但保存的不是 List,则会报 error。该命令会返回修改之后的 List的长度,如果找不到元素,则会返回 -1。

    六、Redis的数据结构都有哪些


    【1】String:可以是字符串,整数或者浮点数,对整个字符串或者字符串中的一部分执行操作,对整个整数或者浮点执行自增(increment)或者自减(decrement)操作。
    【2】List:一个链表,链表上的每个节点都包含了一个字符串,链表的两端推入或者弹出元素,根据偏移量对链表进行修剪(trim),读取单个或者多个元素,根据值查找或者移除元素。可参考5
    【3】Set:包含字符串的无序收集器(unordered collection)、并且被包含的每个字符串都是独一无二的。添加,获取,移除单个元素,检查一个元素是否存在于集合中,计算交集(sinter),并集(suion),差集(sdiff),从集合里面随机获取元素。
    【4】SortSet:是一个排好序的 Set,它在 Set 的基础上增加了一个顺序属性 score,这个属性在添加修改元素时可以指定,每次指定后,SortSet 会自动重新按新的值排序。sorted set 的内部使用 HashMap 和跳跃表(SkipList)来保证数据的存储和有序,HashMap 里放的是成员到 score 的映射,而跳跃表里存放的是所有的成员,排序依据是 HashMap 里存的 score。

    192.168.2.129:6379> zadd myzset 1 "one" 2 "two" 3 "three" #添加元素
    (integer) 3
    192.168.2.129:6379> zrange myzset 0 -1
    1) "one"
    2) "two"
    3) "three"
    192.168.2.129:6379> zrange myzset 0 -1 withscores
    1) "one"
    2) "1"
    3) "two"
    4) "2"
    5) "three"
    6) "3"
    192.168.2.129:6379> zrem myzset one   //删除元素
    (integer) 1
    192.168.2.129:6379> zrange myzset 0 -1 withscores
    1) "two"
    2) "2"
    3) "three"
    4) "3"
    192.168.2.129:6379> 

    【 5】hash:Hash 是一个 String 类型的 field 和 value 之间的映射表,即 redis 的 Hash 数据类型的 key(hash表名称)对应的 value 实际的内部存储结构为一个 HashMap,因此 Hash 特别适合存储对象。相对于把一个对象的每个属性存储为 String 类型,将整个对象存储在 Hash 类型中会占用更少内存。

    192.168.2.129:6379> hset myhash name zhangsan
    (integer) 1
    192.168.2.129:6379> hset myhash age 20
    (integer) 1
    192.168.2.129:6379> hget myhash name
    "zhangsan"
    192.168.2.129:6379> hget myhash age
    "20"
    192.168.2.129:6379>

    七、Redis 使用要注意什么,持久化方式,内存设置,集群的应用和优劣势,淘汰策略等。 


    使用阶段我们从数据存储数据获取两个方面来说明开发时的注意事项
    【1】数据存储:因为内存空间的局限性,注定了能存储的数据量有限,如何在有限的空间内存储更多的数据信息是我们应该关注的。Redis内存储的都是键值对,那么如何减小键值对所占据的内存空间就是空间优化的本质。在能清晰表达业务含义的基础上尽可能缩减 Key的字符长度,比如一个键是user:{id}:logintime ,可以使用业务属性的简写来u:{id}:lgt,只要能清晰表达业务意义,使用简写形式是有其必要性的。在不影响使用的情况下,缩减Value的数据大小。如果Value是较大的数据信息,比如图片,大文本等,可以使用压缩工具压缩过后再存入Redis;如果Value是对象序列化或者gson信息,可以考虑去除非必要的业务属性。
    减少键值对的数量,对于大量的String类型的小对象,可以尝试使用Hash的形式组合他们,在Hash对象内Field数量少于1000,且Value的字符长度小于40时,内部使用ziplist的编码形式,能够极大的降低小对象占据的内存空间。
    Redis内维护了一个[0-9999]的整数对象池,类似Java内的运行时常量池,只创建一个常量,使用时都去引用这个常量,所以当存储的value是这个范围内的数字时均是引向一个内存地址,所以能够降低一些内存空间耗费。但是共享对象池和maxmemory+LRU的内存回收策略冲突,因为共享Value对象的lru值也共享,难以通过lru知道哪个Key的最后引用时间,所以永远也不能回收内存。如果多次数据操作要求原子性,可使用Multi来实现Redis的事务。
    【2】数据查询:Redis 是一种数据库,和其他数据库一样,操作时也需要有连接对象,连接对象的创建和销毁也需要耗费资源,复用连接对象很有必要,所以推荐使用连接池来管理连接。Redis数据存储在内存中,查询很快,但不代表连接也很快。一次Redis查询可能IO部分占据了请求时间的绝大部分比例,缩短IO时间是开发过程中很需要注意的一点。对于一个业务内的多次查询,考虑使用Pipeline,将多次查询合并为一次查询,命令会被执行多次,但是只有一个IO传输,能够有效的提高响应速度。
    对于多次String类型的查询,使用mget,将多次请求合并为一次,同时命令和会被合并为一次,能有效提高响应速度,对于Hash内多个Field查询,使用hmget,起到和mget同样的效果。Redis是单线程执行的,也就是说同一时间只能执行一条命令,如果一条命令执行的时间较长,其他线程在此期间均会被阻塞,所以在操作Redis时要注意操作指令的涉及的数据量,尽量降低单次操作的执行时间。
    持久化方式RDB 时间点快照 AOF 记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。可参考14深度解析
    内存设置maxmemory used_memory
    虚拟内存】: vm-enabled yes
    集群的应用和优劣势】:参考9
    内存优化】:链接
    淘汰策略】:链接

    八、Redis2 和 Redis3 的区别,Redis3 内部通讯机制


    集群方式的区别:Redis3 采用Cluster,Redis2 采用客户端分区方案和代理方案;
    通信过程说明:
      1) 集群中的每个节点都会单独开辟一个TCP通道, 用于节点之间彼此通信, 通信端口号在基础端口上加10000。
      2) 每个节点在固定周期内通过特定规则选择几个节点发送 ping 消息。
      3) 接收到 ping 消息的节点用 pong 消息作为响应。

    九、当前 Redis 集群有哪些玩法,各自优缺点,场景


    【1】数据共享:Redis 提供多个节点实例间的数据共享,也就是 Redis A,B,C,D彼此之间的数据是同步的,同样彼此之间也可以通信,而对于客户端操作的 keys 是由 Redis 系统自行分配到各个节点中。
    【2】主从复制:Redis 的多个实例间通信时,一旦其中的一个节点故障,那么 Redis 集群就不能继续正常工作,所以需要一种复制机制(Master-Slave)机制,做到一旦节点A故障了,那么其从节点A1和A2就可以接管并继续提供与A同样的工作服务,当然如果节点A,A1,A2节点都出现问题,那么同样这个集群不会继续保持工作,但是这种情况比较罕见,即使出现了,也会及时发现并修复使用。建议:部署主从复制机制(Master-Slave)。
    【3】哈希槽值:Redis 集群中使用哈希槽来存储客户端的 keys,而在 Redis 中,目前存在16384个哈希槽,它们被全部分配给所有的节点,正如上图所示,所有的哈希槽值被节点A,B,C分配完成了。
    参考链接

    十、Memcache 的原理,哪些数据适合放在缓存中


    首先要说明一点,MemCache 的数据存放在内存中,存放在内存中个人认为意味着几点
    【1】访问数据的速度比传统的关系型数据库要快,因为 Oracle、MySQL 这些传统的关系型数据库为了保持数据的持久性,数据存放在硬盘中,IO操作速度慢;
    【2】MemCache 的数据存放在内存中同时意味着只要 MemCache 重启了,数据就会消失;
    【3】既然 MemCache 的数据存放在内存中,那么势必受到机器位数的限制,这个之前的文章写过很多次了,32位机器最多只能使用2GB的内存空间,64位机器可以认为没有上限。
    MemCache 的原理:MemCache 最重要的莫不是内存分配的内容了,MemCache 采用的内存分配方式是固定空间分配,还是自己画一张图说明:

    这张图片里面涉及了slab_class、slab、page、chunk四个概念,它们之间的关系是:
    【1】MemCache将内存空间分为一组slab;
    【2】每个slab下又有若干个page,每个page默认是1M,如果一个slab占用100M内存的话,那么这个slab下应该有100个page;
    【3】每个page里面包含一组chunk,chunk是真正存放数据的地方,同一个slab里面的chunk的大小是固定的;
    【4】有相同大小chunk的slab被组织在一起,称为slab_class;
    MemCache内存分配的方式称为allocator,slab的数量是有限的,几个、十几个或者几十个,这个和启动参数的配置相关。
    MemCache中的value过来存放的地方是由value的大小决定的,value总是会被存放到与chunk大小最接近的一个slab中,比如slab[1]的chunk大小为80字节、slab[2]的chunk大小为100字节、slab[3]的chunk大小为128字节(相邻slab内的chunk基本以1.25为比例进行增长,MemCache启动时可以用-f指定这个比例),那么过来一个88字节的value,这个value将被放到2号slab中。放slab的时候,首先slab要申请内存,申请内存是以page为单位的,所以在放入第一个数据的时候,无论大小为多少,都会有1M大小的page被分配给该slab。申请到page后,slab会将这个page的内存按chunk的大小进行切分,这样就变成了一个chunk数组,最后从这个chunk数组中选择一个用于存储数据。
    如果这个slab中没有chunk可以分配了怎么办,如果MemCache启动没有追加-M(禁止LRU,这种情况下内存不够会报Out Of Memory错误),那么MemCache会把这个slab中最近最少使用的chunk中的数据清理掉,然后放上最新的数据。针对MemCache的内存分配及回收算法,总结三点:
    【1】MemCache的内存分配chunk里面会有内存浪费,88字节的value分配在128字节(紧接着大的用)的chunk中,就损失了30字节,但是这也避免了管理内存碎片的问题;
    【2】MemCache的LRU算法不是针对全局的,是针对slab的;
    【3】应该可以理解为什么MemCache存放的value大小是限制的,因为一个新数据过来,slab会先以page为单位申请一块内存,申请的内存最多就只有1M,所以value大小自然不能大于1M了;
    再总结 MemCache 的特性和限制:上面已经对于MemCache做了一个比较详细的解读,这里再次总结MemCache的限制和特性:
    1】MemCache中可以保存的item数据量是没有限制的,只要内存足够
    2】MemCache单进程在32位机中最大使用内存为2G,这个之前的文章提了多次了,64位机则没有限制
    3】Key最大为250个字节,超过该长度无法存储
    4】单个item最大数据是1MB,超过1MB的数据不予存储
    5】MemCache服务端是不安全的,比如已知某个MemCache节点,可以直接telnet过去,并通过flush_all让已经存在的键值对立即失效
    6】不能够遍历MemCache中所有的item,因为这个操作的速度相对缓慢且会阻塞其他的操作
    7】MemCache的高性能源自于两阶段哈希结构:第一阶段在客户端,通过Hash算法根据Key值算出一个节点;第二阶段在服务端,通过一个内部的Hash算法,查找真正的item并返回给客户端。从实现的角度看,MemCache是一个非阻塞的、基于事件的服务器程序
    8】MemCache设置添加某一个Key值的时候,传入expiry为0表示这个Key值永久有效,这个Key值也会在30天之后失效。
    MemCache适合存储:变化频繁,具有不稳定性的数据,不需要实时入库, (比如用户在线状态、在线人数..)门户网站的新闻等,觉得页面静态化仍不能满足要求,可以放入到memcache中.(配合jquey的ajax请求)。

    十一、Redis 和 Memcached 的内存管理的区别


    在 Redis中,并不是所有的数据都一直存储在内存中的。这是和 Memcached相比一个最大的区别。当物理内存用完时,Redis可以将一些很久没用到的 value交换到磁盘,Redis只会缓存所有的 key的信息。如果 Redis发现内存的使用量超过了某一个阀值,将触发 swap的操作,Redis根据 “swappability = age*log(size_in_memory)”计算出哪些 key对应的 value需要 swap到磁盘。然后再将这些 key对应的 value持久化到磁盘中,同时在内存中清除。这种特性使得 Redis可以保持超过其机器本身内存大小的数据。当然,机器本身的内存必须要能够保持所有的key,毕竟这些数据是不会进行 swap操作的。同时由于 Redis将内存中的数据swap到磁盘中的时候,提供服务的主线程和进行swap操作的子线程会共享这部分内存,所以如果更新需要 swap的数据,Redis将阻塞这个操作,直到子线程完成 swap操作后才可以进行修改。当从 Redis中读取数据的时候,如果读取的 key对应的 value不在内存中,那么 Redis就需要从 swap文件中加载相应数据,然后再返回给请求方。 这里就存在一个 I/O线程池的问题。在默认的情况下,Redis会出现阻塞,即完成所有的 swap文件加载后才会相应。这种策略在客户端的数量较小,进行批量操作的时候比较合适。但是如果将 Redis应用在一个大型的网站应用程序中,这显然是无法满足大并发的情况的。所以 Redis运行我们设置 I/O线程池的大小,对需要从 swap文件中加载相应数据的读取请求进行并发操作,减少阻塞的时间。

    对于像 Redis和 Memcached这种基于内存的数据库系统来说,内存管理的效率高低是影响系统性能的关键因素。传统C语言中的malloc/free函数是最常用的分配和释放内存的方法,但是这种方法存在着很大的缺陷:首先,对于开发人员来说不匹配的malloc和free容易造成内存泄露;其次频繁调用会造成大量内存碎片无法回收重新利用,降低内存利用率;最后作为系统调用,其系统开销远远大于一般函数调用。所以,为了提高内存的管理效率,高效的内存管理方案都不会直接使用malloc/free调用。Redis和Memcached均使用了自身设计的内存管理机制,但是实现方法存在很大的差异,下面将会对两者的内存管理机制分别进行介绍。

    Memcached默认使用 Slab Allocation机制管理内存,其主要思想是按照预先规定的大小,将分配的内存分割成特定长度的块以存储相应长度的key-value数据记录,以完全解决内存碎片问题。Slab Allocation机制只为存储外部数据而设计,也就是说所有的key-value数据都存储在Slab Allocation系统里,而 Memcached的其它内存请求则通过普通的 malloc/free来申请,因为这些请求的数量和频率决定了它们不会对整个系统的性能造成影响 Slab Allocation的原理相当简单。 如图所示,它首先从操作系统申请一大块内存,并将其分割成各种尺寸的块Chunk,并把尺寸相同的块分成组Slab Class。其中,Chunk就是用来存储key-value数据的最小单位。每个Slab Class的大小,可以在 Memcached启动的时候通过制定 Growth Factor来控制。假定图中 Growth Factor的取值为1.25,如果第一组 Chunk的大小为88个字节,第二组 Chunk的大小就为112个字节,依此类推。

    当Memcached接收到客户端发送过来的数据时首先会根据收到数据的大小选择一个最合适的Slab Class,然后通过查询Memcached保存着的该Slab Class内空闲Chunk的列表就可以找到一个可用于存储数据的Chunk。当一条数据库过期或者丢弃时,该记录所占用的Chunk就可以回收,重新添加到空闲列表中。从以上过程我们可以看出Memcached的内存管理制效率高,而且不会造成内存碎片,但是它最大的缺点就是会导致空间浪费。因为每个Chunk都分配了特定长度的内存空间,所以变长数据无法充分利用这些空间。如图 所示,将100个字节的数据缓存到128个字节的Chunk中,剩余的28个字节就浪费掉了。

    Redis的内存管理主要通过源码中 zmalloc.hzmalloc.c两个文件来实现的。Redis为了方便内存的管理,在分配一块内存之后,会将这块内存的大小存入内存块的头部。如图所示,real_ptr是 redis调用 malloc后返回的指针。redis将内存块的大小 size存入头部,size所占据的内存大小是已知的,为 size_t类型的长度,然后返回 ret_ptr。当需要释放内存的时候,ret_ptr被传给内存管理程序。通过 ret_ptr,程序可以很容易的算出 real_ptr的值,然后将 real_ptr传给 free释放内存。

    Redis通过定义一个数组来记录所有的内存分配情况,这个数组的长度为 ZMALLOC_MAX_ALLOC_STAT。数组的每一个元素代表当前程序所分配的内存块的个数,且内存块的大小为该元素的下标。在源码中,这个数组为 zmalloc_allocations。zmalloc_allocations[16]代表已经分配的长度为16bytes的内存块的个数。zmalloc.c中有一个静态变量 used_memory用来记录当前分配的内存总大小。所以,总的来看,Redis采用的是包装的 malloc/free,相较于 Memcached的内存管理方法来说,要简单很多。

    十二、Redis 的并发竞争问题如何解决,了解 Redis 事务的 CAS 操作吗


    Redis 为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis 本身没有锁的概念,Redis 对于多个客户端连接并不存在竞争,但是在 Jedis 客户端对 Redis 进行并发访问时会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。对此有2种解决方法:
    【1】客户端角度,为保证每个客户端间正常有序与 Redis 进行通信,对连接进行池化,同时对客户端读写 Redis 操作采用内部锁 synchronized。
    【2】服务器角度,利用 setnx 实现锁。MULTI,EXEC,DISCARD,WATCH 四个命令是 Redis 事务的四个基础命令。其中:
       ☆ MULTI,告诉 Redis 服务器开启一个事务。注意,只是开启,而不是执行
       ☆ EXEC,告诉 Redis 开始执行事务
       ☆ DISCARD,告诉 Redis 取消事务
       ☆ WATCH,监视某一个键值对,它的作用是在事务执行之前如果监视的键值被修改,事务会被取消。
    Redis 事务机制链接
    CAS 操作链接

    十三、Redis 的选举算法和流程是怎样的


    Raft 采用心跳机制触发 Leader 选举。系统启动后,全部节点初始化为 Follower,term 为0。节点如果收到了 RequestVote 或者AppendEntries,就会保持自己的 Follower 身份。如果一段时间内没收到 AppendEntries 消息直到选举超时,说明在该节点的超时时间内还没发现 Leader,Follower 就会转换成 Candidate,自己开始竞选 Leader。一旦转化为 Candidate,该节点立即开始下面几件事情:
     1)、增加自己的term。
     2)、启动一个新的定时器。
     3)、给自己投一票。
     4)、向所有其他节点发送RequestVote,并等待其他节点的回复。
    ✔ 如果在这过程中收到了其他节点发送的AppendEntries,就说明已经有Leader产生,自己就转换成Follower,选举结束。
    ✔ 如果在计时器超时前,节点收到多数节点的同意投票,就转换成Leader。同时向所有其他节点发送AppendEntries,告知自己成为了Leader。
    ✔ 每个节点在一个term内只能投一票,采取先到先得的策略,Candidate前面说到已经投给了自己,Follower会投给第一个收到RequestVote的节点。每个Follower有一个计时器,在计时器超时时仍然没有接受到来自Leader的心跳RPC, 则自己转换为Candidate, 开始请求投票,就是上面的的竞选Leader步骤。
    ✔ 如果多个Candidate发起投票,每个Candidate都没拿到多数的投票(Split Vote),那么就会等到计时器超时后重新成为Candidate,重复前面竞选Leader步骤。
    ✔ Raft协议的定时器采取随机超时时间,这是选举Leader的关键。每个节点定时器的超时时间随机设置,随机选取配置时间的1倍到2倍之间。由于随机配置,所以各个Follower同时转成Candidate的时间一般不一样,在同一个term内,先转为Candidate的节点会先发起投票,从而获得多数票。多个节点同时转换为Candidate的可能性很小。即使几个Candidate同时发起投票,在该term内有几个节点获得一样高的票数,只是这个term无法选出Leader。由于各个节点定时器的超时时间随机生成,那么最先进入下一个term的节点,将更有机会成为Leader。连续多次发生在一个term内节点获得一样高票数在理论上几率很小,实际上可以认为完全不可能发生。一般1-2个term类,Leader就会被选出来。
    Sentinel 的选举流程Sentinel 集群正常运行的时候每个节点 epoch 相同,当需要故障转移的时候会在集群中选出 Leader执行故障转移操作。Sentinel采 用了Raft 协议实现了 Sentinel 间选举 Leader 的算法,不过也不完全跟论文描述的步骤一致。Sentinel 集群运行过程中故障转移完成,所有 Sentinel 又会恢复平等。Leader 仅仅是故障转移操作出现的角色。
    选举流程1)、某个 Sentinel 认定 master 客观下线的节点后,该 Sentinel 会先看看自己有没有投过票,如果自己已经投过票给其他 Sentinel 了,在2倍故障转移的超时时间自己就不会成为 Leader。相当于它是一个 Follower。
      2)、如果该 Sentinel 还没投过票,那么它就成为 Candidate。
      3)、和 Raft 协议描述的一样,成为 Candidate,Sentinel 需要完成几件事情。
       【1】更新故障转移状态为start
       【2】当前epoch加1,相当于进入一个新term,在Sentinel中epoch就是Raft协议中的term。
       【3】更新自己的超时时间为当前时间随机加上一段时间,随机时间为1s内的随机毫秒数。
       【4】向其他节点发送is-master-down-by-addr命令请求投票。命令会带上自己的epoch。
       【5】给自己投一票,在 Sentinel 中,投票的方式是把自己 master 结构体里的 leader 和 leader_epoch 改成投给的 Sentinel 和它的 epoch。
       4)、其他Sentinel会收到Candidate的is-master-down-by-addr命令。如果Sentinel当前epoch和Candidate传给他的epoch一样,说明他已经把自己master结构体里的leader和leader_epoch改成其他Candidate,相当于把票投给了其他Candidate。投过票给别的Sentinel后,在当前epoch内自己就只能成为Follower。
       5)、Candidate会不断的统计自己的票数,直到他发现认同他成为Leader的票数超过一半而且超过它配置的quorum(quorum可以参考《redis sentinel设计与实现》)。Sentinel比Raft协议增加了quorum,这样一个Sentinel能否当选Leader还取决于它配置的quorum。
       6)、如果在一个选举时间内,Candidate没有获得超过一半且超过它配置的quorum的票数,自己的这次选举就失败了。
       7)、如果在一个epoch内,没有一个Candidate获得更多的票数。那么等待超过2倍故障转移的超时时间后,Candidate增加epoch重新投票。
       8)、如果某个Candidate获得超过一半且超过它配置的quorum的票数,那么它就成为了Leader。
       9)、与Raft协议不同,Leader并不会把自己成为Leader的消息发给其他Sentinel。其他Sentinel等待Leader从slave选出master后,检测到新的master正常工作后,就会去掉客观下线的标识,从而不需要进入故障转移流程。

    十四、Redis 的持久化的机制,AOF和RDB的区别


    Redis的持久化机制:Redis 提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化),另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。
    AOF和RDB的区别:RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

    AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

    二者优缺点RDB存在哪些优势:
    【1】一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
    【2】对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
    【3】性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
    【4】相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
    RDB又存在哪些劣势
    【1】如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
    【2】由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
    AOF 的优势有哪些
    【1】该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3种同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。
    【2】由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
    【3】如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
    【4】AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
    AOF 的劣势有哪些
    【1】对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
    【2】根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。

    二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。

    十五、缓存预热


    新的缓存系统没有任何数据,在缓存重建数据的过程中,系统性能和数据负载都不太好,所以最好在系统上线之前就把缓存的热点数据加载到缓存中,这种缓存预加载手段就是缓存预热。

    十六、缓存热备


    缓存热备既当一个缓存服务器不可用时能实时切换到备用缓存服务器,不影响缓存使用。集群模式下,每个主节点都会有一个或多个从节点备用,一旦主节点挂掉,从节点会被哨兵提升为主节点使用。

    十七、Redis 的集群怎么同步的数据的


    参考博客链接

    十八、知道哪些 Redis 的优化操作


    参考博客链接

    十九、Reids 的主从复制机制原理


    参考博客链接

    二十、Redis 的线程模型是什么


    参考博客链接

    二十、本地缓存与分布式缓存区别


    本地缓存的优势是没有网络开销,在大并发量时用好本地缓存很重要;分布式缓存比如Redis 优势是能够无限扩容量多个系统公用缓存数据,结合这个去在业务中使用缓存是很重要的​。
    本地缓存的缺点是会占用堆内存,影响垃圾回收、影响系统性能。分布式缓存两大开销(网络延迟对象序列化)会导致其慢于本地缓存,同时也需要搭建分布式缓存系统。
    建议:​进程内缓存适用于较小且频率可见的访问场景,尤其适用于不变对象,对于较大且不可预见的访问,最好采用分布式缓存。


     ----架构师成长路线,关注公众号获取----

    展开全文
  • 缓存的基本概念和常用的缓存技术

    万次阅读 2018-04-04 11:15:22
    摘要: 介绍缓存的基本概念和常用的缓存技术,给出了各种技术的实现机制的简单介绍和适用范围说明,以及设计缓存方案应该考虑的问题(共17页)1 概念1.1 缓存能解决的问题· 性能——将相应数据存储起来以避免数据...

    摘要: 介绍缓存的基本概念和常用的缓存技术,给出了各种技术的实现机制的简单介绍和适用范围说明,以及设计缓存方案应该考虑的问题(共17页)

    1         概念

    1.1   缓存能解决的问题

    · 性能——将相应数据存储起来以避免数据的重复创建、处理和传输,可有效提高性能。比如将不改变的数据缓存起来,例如国家列表等,这样能明显提高web程序的反应速度;

    · 稳定性——同一个应用中,对同一数据、逻辑功能和用户界面的多次请求时经常发生的。当用户基数很大时,如果每次请求都进行处理,消耗的资源是很大的浪费,也同时造成系统的不稳定。例如,web应用中,对一些静态页面的呈现内容进行缓存能有效的节省资源,提高稳定性。而缓存数据也能降低对数据库的访问次数,降低数据库的负担和提高数据库的服务能力;

    · 可用性——有时,提供数据信息的服务可能会意外停止,如果使用了缓存技术,可以在一定时间内仍正常提供对最终用户的支持,提高了系统的可用性。

    1.2   理解状态

    在深入介绍缓存技术之前,需要对状态有一个认识,因为缓存可以说是状态管理的框架。理解状态的含义和它的一些特性——比如生存期和生存范围——对决定是否缓存和选择合适的缓存技术有很大帮助。状态是指一些数据,在应用系统某个时间点上,数据的状态和条件。这些数据可能是永久性的存储在数据库中,可能是只在内存里停留一会,也可能是按照某个逻辑存活(比如多长时间后释放),它的应用范围可能是所有用户可访问,可能是单个用户有权限;

    1.2.1  状态的生存期

    生存期是指数据保持有效性的时间区间,也就是从创建到移除的时间间隔。通常的生存期有以下几种:

    ·永久状态Permanent State——应用程序使用的永久数据;

    ·进程状态Process State——只在进程周期内有效;

    ·会话状态Session State——和特定的用户会话有关;

    ·消息状态Message State——处理某个消息的时间内有效;

    1.2.2  状态的范围

    状态的范围指对该状态有访问权限的物理或逻辑范围。

    ·物理范围指可以被访问到的状态数据存放的物理位置,通常包括:

    1、  组织Organization——在一个组织内的所有应用程序可以访问状态数据;

    2、  场Farm——在应用场范围内的任何机器上都可以访问;

    3、  机器Machine——单个机器范围内可以访问;

    4、  进程Process——进程内的访问许可;

    5、  应用域AppDomain——应用程序域内的访问许可。

    ·逻辑范围指可访问状态数据的逻辑范围,常见的有:

    1、  应用程序Application;

    2、  业务流程Business Process;

    3、  角色Role;

    4、  用户User;

    1.2.3  状态数据的陈旧

    缓存的状态数据只是主数据(Master State Data)的快照,由于数据源可能被修改,所以状态数据就有会陈旧的特性。合理利用此特性和将数据陈旧的负面影响最小化是缓存状态数据的一个重要任务。你可以以一下方式定义数据的陈旧依据:

    ·主数据更改的可能性——随着时间的推进,主数据更改的可能是否大大增加?安照这一点来决定缓存状态数据的陈旧;

    ·更改的相关性——主数据更新时,缓存的状态数据不相应更新是不是造成影响系统的使用?比如,更改系统的外观风格并不会对业务造成很大影响。

    1.2.4  状态数据陈旧的容忍度

    缓存状态数据的陈旧对业务流程的影响称为容忍度,应用系统的可以为不能容忍(No Tolerance)和一定程度的容忍(some Tolerance),前者必须和主数据同步更新,后者允许一定时间或一定范围的陈旧,判断标准就是对业务流程的影响度。

    1.2.5  理解状态数据的转换过程

    状态的另一个属性是在不同阶段的表现形式。在数据库中存储的是原始格式的数据,业务流程中的是处理过的数据,给最终用户呈现的则是另外的形式。如下表所示:

    表现形式

    描述

    举例

    原始数据

    数据的原始形式

    如数据库中的数据

    处理过的数据

    业务流程中对原始数据加工后的数据

    业务过程中的数据形式

    呈现形式

    可呈现给最终用户的形式

    HTML或可理解的文字说明

    当决定缓存数据时,应该考虑缓存哪个阶段(哪种形式)的状态数据。以下方针有助于你做决定:

    · 当业务逻辑可以容忍缓存数据的陈旧时就缓存原始数据;原始数据可以缓存在数据库访问组件和服务代理中;

    ·缓存处理过的数据以减少处理时间和资源;处理过的数据可以缓存在业务逻辑组件和服务接口中。

    ·当需要呈现的数据量很大并且控件的呈现时间很长时,缓存呈现数据(比如包含大数据量的Treeview控件)。这种数据应该被缓存在UI控件中。

    1.3   为什么要缓存数据

    在应用程序中缓存数据有以下好处:

    ·减少交互的通讯量——缓存数据能有效减少在进程和机器间的传输量;

    ·降低系统中的处理量——减少处理次数;

    ·降低需要做的磁盘访问次数——比如缓存在内存中的数据。

    1.4   数据应该被缓存在哪里

    缓存数据只是一份主数据的拷贝,它可能在内存中或以不同的表现形式保存在硬盘上,也就是说,离说句的使用者越近越好。所以,除了考虑要缓存哪些数据以外,数据缓存在哪里也是一个主要的考量点。这个问题分为以下两个范围:

    1、  存储类型Storage Type——数据可用的物理存储位置;

    2、  层间的架构元素(Layered architecture elements)——数据可用的逻辑存储位置。

    1.4.1  存储类型

    缓存有很多实现方法,所有这些可以被分为两类,基于内存的缓存和基于磁盘的缓存:

    1、  内存驻留缓存——包含在内存中临时存储数据的所有实现方法,通常在以下情况下使用:

    a)       应用程序频繁使用同样的数据;

    b)       应用程序需要经常获取数据;

    通过将数据保留在内存中,你可以有效降低昂贵的磁盘访问操作,也可以通过将数据保留在使用者进程中来最大程度的减少跨进程的数据传输。

    2、  磁盘驻留缓存——这种技术包含所有使用磁盘作为存储介质的缓存技术,如文件和数据库。在以下情况下基于磁盘的缓存是很有效的:

    a)       处理大数据量时;

    b)       应用服务提供的数据可能并不是总能使用(比如离线的情况);

    c)       缓存的数据必须能在进程回收和机器重启的情况下保持有效;

    通过缓存处理过的数据,你可以有效降低数据处理的负担,同时可减少数据交互的代价。

    1.4.2  架构间元素

    应用程序中的每个逻辑层的组件都会处理数据,下图标识了一些通用组件:


    当使用这些组件进行工作时,你需要考虑哪些数据可以被缓存起来,还有以哪种方式进行缓存会对程序的整体性能和可用性有帮助,以上的这些元素都可以缓存相应的数据。当然,要考虑的远不止这些。

    1.5   实施缓存时的考虑

    当设计一个缓存方案时,不但要考虑缓存哪些数据、数据缓存到哪里,还有其它的因素需要考虑。

    1.5.1  格式和访问模式

    当决定是否缓存一个对象时,关于数据的格式和访问机制,你需要考虑三个主要问题:

    1、  线程安全——当缓存的内容可以被多个线程访问时,使用某种锁定机制来保证数据不会被两个线程同时操作;

    2、  序列化——将一个对象缓存时,需要将它序列化以便保存,所以包缓存的对象必须支持序列化;

    3、  规格化缓存数据——缓存数据时,相对于要使用的数据格式而言,要保证数据的格式是优化过的。

    1.5.2  内容加载

    在使用缓存数据前,必须将数据加载到缓存中,有两种机制来加载数据:

    ·提前加载Proactive Load——使用这种方式时,你提前将所有的状态数据加载到缓存中,可能在应用程序或线程启动时进行,然后在应用程序或线程的生存期内一直缓存;

    ·动态加载Reactive Load——或称反应式加载,当使用这种方法时,在应用程序请求数据时取到数据,并且将它缓存起来以备后续使用。

    1.5.3  过期策略

    另外一个关键因素是如何保持缓存数据和主数据(文件或数据库或其他的应用程序资源)的一致性,你可以定义过期策略来决定缓存中的内容,如已经缓存的时间或者收到其他资源的通知。

    1.5.4  安全性

    当缓存数据时,需要非常清楚缓存中数据的潜在安全威胁。缓存中的数据可能会被别的进程访问或修改,而此进程对主数据是没有权限的。原因是当数据存储在原始位置时,有相应的安全机制来保护它,当数据被带出传统的安全边界时,需要有同等的安全机制。

    1.5.5  管理

    当你缓存数据时,应用系统需要的维护工作加大了。在发布应用程序时,需要配置相应的属性,比如缓存的大小限制和清除策略。同时要使用某种机制来监控缓存的效率(比如事件日志和性能计数器)

    1.6   小结

    第一节内容简单介绍了缓存技术中的概念、缓存数据的原因和方案、优势、实施缓存方案时的考虑等基本内容。现在你对缓存中涉及的内容有了一个大致了解,下面着重介绍可用的缓存技术。

    2         缓存技术

    本节将介绍以下技术:

    使用Asp.Net缓存;

    使用Remoting Singleton缓存;

    使用内存映射文件;

    使用SQL Server缓存;

    使用静态变量缓存;

    使用Asp.net 会话状态(Session State);

    使用Asp.net客户端缓存和状态;

    使用Internet Explorer缓存。

    2.1             Asp.net缓存

    将常用的数据保存在内存中对asp的开发人员来说并不陌生,Session对象和Application对象提供键值对来缓存数据,Session对象保存和单个用户有关的数据,Application对象可保留和应用程序有关的数据,每个用户都可以访问。

    在Asp.net中,提供了专门用于缓存数据的Cache对象,它的应用范围是应用程序域。生存期是和应用程序紧密相关的,每当应用程序启动的时候就重新创建Cache对象。它域Application对象的主要区别就是提供了专门用于缓存管理的特性,比如依赖和过期策略。

    你可以使用Cache对象和它的属性来实现高级的缓存功能,同时可以利用Asp.net Cache来对客户端输出的响应内容进行缓存。关于Asp.net中的缓存技术,有以下内容要介绍:

    2.1.1  编程缓存Programmatic Caching

    Cache对象定义在System.Web.Caching命名空间,可以使用HttpContext类的Cache属性或Page对象的Cache属性来得到Cache的引用,Cache对象除了存储键值对以外,还可以存储.net框架的对象。下面介绍相应的依赖和过期策略。

    2.1.1.1 依赖和过期策略

    当向缓存中加数据时,可以指定它的依赖关系来实现在某些情况下强制移除它。可用的方案包括以下几种:

    ·文件依赖(File Dependency)——当硬盘上的某个(某些)文件更改时,强制移除缓存数据;如:

    CacheDependency cDependency = new

    CacheDependency(Server.MapPath("authors.xml"));

    Cache.Insert("CachedItem", item, cDependency);

    ·键值依赖(Key Dependency)——指定缓存中的某个数据项更改时移除。如:

    // Create a cache entry.

    Cache["key1"] = "Value 1";

    // Make key2 dependent on key1.

    String[] dependencyKey = new String[1];

    dependencyKey[0] = "key1";

    CacheDependency dependency = new CacheDependency(null, dependencyKey);

    Cache.Insert("key2", "Value 2", dependency);

    ·基于时间的过期策略——按照预先定义的时间策略来使数据失效,可以是绝对时间(如某个日期的18:00)也可以是相对现在的相对时间。如:

    /// Absolute expiration

    Cache.Insert("CachedItem", item, null, DateTime.Now.AddSeconds(5),Cache.NoSlidingExpiration);

    /// Sliding expiration

    Cache.Insert("CachedItem", item, null, Cache.NoAbsoluteExpiration,

    TimeSpan.FromSeconds(5));

    使用太短和太长的过期时间都不行,不是造成用不上的缓存数据,就是缓存了陈旧的数据并加重了缓存负担,所以可以使用高并发的测试来决定过期时间的最佳值。

    ·另外有个问题就是如何实现对数据库的依赖,这就要求实现自己的通知机制,当数据库数据改变时能够通知你的缓存数据改变。可参考http://www.gotdotnet.com/team/rhoward的示例。

    由于数据会过期,所以当使用缓存中的数据时,必须检查数据的有效性。如以下代码:

    string data = (string)Cache["MyItem"];

    if (data == null)

    {

    data = GetData();

    Cache.Insert("MyItem", data);

    }

    DoSomeThingWithData(data);

    依赖和过期策略指定了缓存中数据的移除方式,有时候你可能需要在移除发生时做一些工作,这能靠写代码来实现这一点,这就是我们要讲到的。

    2.1.1.2 使用缓存回调(Cache Callback)

    你可以定义回调,这样当移除自动发生时, 你可以不移除它或者使用新的数据来替换它。如:

    CacheItemRemovedCallback onRemove = new CacheItemRemovedCallback(this.RemovedCallback);

    Cache.Insert("CachedItem",

    item,

    null,

    Cache.NoAbsoluteExpiration,

    Cache.NoSlidingExpiration,

    CacheItemPriority.Default,

    onRemove);

    // Implement the function to handle the expiration of the cache.

    public void RemovedCallback(string key, object value, CacheItemRemovedReason r)

    {

    // Test whether the item is expired, and reinsert it into the cache.

    if (r == CacheItemRemovedReason.Expired)

    {

    // Reinsert it into the cache again.

    CacheItemRemovedCallback onRemove = null;

    onRemove = new CacheItemRemovedCallback(this.RemovedCallback);

    Cache.Insert(key,

    value,

    null,

    Cache.NoAbsoluteExpiration,

    Cache.NoSlidingExpiration,

    CacheItemPriority.Default,

    onRemove);

    }

    }

    2.1.1.3 对缓存项使用优先级

    当运行应用程序的服务器内存不足时,会自动清除缓存中的数据,称为“清除scavenging”。此时,Cache对象根据缓存项的优先级来决定先移除哪些缓存数据,你可以在代码中指定缓存项的优先级。参看MSDN中“CacheItemPriority 枚举”,如:

    Cache.Insert("DSN", connectionString, null, d, t, CacheItemPriority.High, onRemove);

    2.1.1.4 刷新数据(清除缓存)

    没有直接的方法来刷新Asp.net的输出缓存,但是有替代方法(设置所有数据失效),比如:

    Response.Cache.SetExpires(DateTime.Now)

    这可以清除缓存,但页面上并不立刻体现出来,直到最初的缓存期结束,比如:

    <%@ OutputCache Duration="10" VaryByParam="none" %>指令指定的缓存只会在10秒后才清除。通常并不需要清除所有缓存项,你只要重新加载数据更新缓存就够了。

    2.1.2  输出缓存(Output Cache)

    你可以使用两种方式的输出缓存来缓存需要传输和显示到客户端浏览器上的数据——页面输出缓存(Page Output Cache)和页面片断缓存(Page Fragment Cache)。当整个页面相对变化较少时,可以缓存整个页面;如果只是页面的一部分经常变化,可以使用片断缓存。

    2.1.2.1 页面输出缓存

    Page Output Caching将对页面请求的响应放入缓存中,后续对此页面的请求将直接从缓存中得到信息而不是重建此页面。可以通过添加Page指令(高级别,声明实现)来实现,也可以使用HTTPCachePolicy类来实现(低级别,程序实现)。本指南不打算介绍技术细节,只给出如何更好使用的指南和最佳实践。有四方面的内容:

    1、 决定缓存的内容

    页面输出缓存可缓存各种信息,缓存这些信息意味着你不需要经常处理同样的数据和结果,包括:

    ·经常被请求但不不改变的静态页面;

    ·更新频率和时间已知的页面(如显示股票价格的页面);

    ·根据HTTP参数,有几个可能输出的页面(如根据城市的代号显示该城市天气情况的页面);

    ·从Web Service返回的结果;如:

    [WebMethod(CacheDuration=60)]

    public string HelloWorld()

    {

    return "Hello World";

    }

    2、 缓存动态页面

    基于输入参数、语言和浏览器类型改变的动态网页经常用到。你可以使用OutputCache的以下属性来实现对动态页面的缓存:

    VaryByParam——基于输入参数不同缓存同一页面的多个版本;

    VaryByHeader——基于Page Header的内容不同缓存页面的多个版本;

    VaryByCustom——通过声明属性和重载GetVaryByCustomString方法来定制缓存处理页面的多个版本;

    VaryByControl——基于控件中asp对象属性的不同来缓存控件。

    对多个版本页面的缓存会降低可用内存,所以要仔细衡量缓存策略。s

    3、 控制缓存的位置

    你可以使用@OutputCache指令的OutputCacheLocation属性的枚举值来指定缓存的位置,如:

    <%@ outputcache duration="10" varybyparam="none" Location="Server" %>

    4、 配置页面输出缓存

    有两种方式控制,你可以使用Page指令,也可以使用Cache API编程实现。参考以下两段代码:

    //代码1,使用指令

    <%@ OutputCache Duration="20" Location="Server" VaryByParam="state" VaryByCustom="minorversion" VaryByHeader="Accept-Language"%>

    //代码2,编程实现

    private void Page_Load(object sender, System.EventArgs e)

    {

    // Enable page output caching.

    Response.Cache.SetCacheability(HttpCacheability.Server);

    // Set the Duration parameter to 20 seconds.

    Response.Cache.SetExpires(System.DateTime.Now.AddSeconds(20));

    // Set the Header parameter.

    Response.Cache.VaryByHeaders["Accept-Language"] = true;

    // Set the cached parameter to 'state'.

    Response.Cache.VaryByParams["state"] = true;

    // Set the custom parameter to 'minorversion'.

    Response.Cache.SetVaryByCustom("minorversion");

    }

    2.1.2.2 页面片断缓存

    有时候缓存整个页面并不灵活,同时内存的发但也比较大,这时候应考虑片断缓存。页面片断缓存适合以下类型的数据:

    ·创建开销很大的页面片断(控件);

    ·包含静态数据的页面片断;

    ·可被多个用户使用的页面片断;

    ·多个页面共享的页面片断(如公用菜单条)

    以下是缓存部分页面的例子:

    // Partial caching for 120 seconds

    [System.Web.UI.PartialCaching(120)]

    public class WebUserControl : System.Web.UI.UserControl

    {

    // Your Web control code

    }

    2.1.3  在非Web项目中使用Asp.net缓存

    Asp.net Cache位于System.Web命名空间,但由于它是一个通用的方案,所以仍然可以在引用此命名空间的任何非Web项目中使用它。

    System.Web.Caching.Cache 类是对象的缓存,它可以通过System.Web.HttpRuntime.Cache 的静态属性或System.Web.UI.Page 和System.Web.HttpContext.Cache来访问。因此在请求上下文之外也可以存在,在每个应用程序域中只有一个实例,所以HttpRuntime.Cache对象可以在Aspnet_wp.exe之外的每个应用程序域中存在。以下代码演示了在普通应用里访问Cache对象:

    HttpRuntime httpRT = new HttpRuntime();

    Cache cache = HttpRuntime.Cache;

     

    2.2             使用Remoting Singleton缓存

    .Net Remoting提供了跨应用程序域、跨进程、跨计算机的程序运行框架。服务器激活的对象有两种激活模式,其中Singleton 类型任何时候都不会同时具有多个实例。如果存在实例,所有客户端请求都由该实例提供服务。如果不存在实例,服务器将创建一个实例,而所有后继的客户端请求都将由该实例来提供服务。由于 Singleton类型具有关联的默认生存期,即使任何时候都不会有一个以上的可用实例,客户端也不会总接收到对可远程处理的类的同一实例的引用。所以将数据缓存起来可以在多个客户端之间共享状态信息。

    为了使用.Net Remoting实现缓存方案,要保证远程对象的租约不过期,并且远程对象没有被垃圾回收器销毁(对象租约是指在系统删除该对象前它在内存中的生存期)。当实现缓存时,重载MarshalByRefObject的InitializeLifetimeService方法并且返回null,这样就能保证租约永远不过期并且相关的对象生存期是无限的。以下代码是一个示例:

    public class DatasetStore : MarshalByRefObject

    {

    // A hash table-based data store

    private Hashtable htStore = new Hashtable();

    //Returns a null lifetime manager so that GC won't collect the object

    public override object InitializeLifetimeService() { return null; }

    // Your custom cache interface

    }

    注意:由于这种方案的成本较高、性能上的限制并且可能造成系统不稳定,通常采用基于Sql Server的方案来替代。

    2.3             使用内存映射文件(Memory-Mapped File)

    内存映射文件提供独一无二的特性,允许应用程序通过指针来访问磁盘上的文件——与访问动态内存趣的方式一样。所以你可以将应用程序进程中的某个地址段的数据映射到文件中,供多个跨应用程序域或跨进程访问。

    在windows中,代码和数据是以以种方式处理的,表现形式都是内存页,而在内存页背后都是磁盘上的文件。唯一的不同磁盘上的文件类型不同。代码后面是可执行的镜像,而数据后面则是系统的页面文件。当多个应用程序共享内存时,系统的性能会有明显提升。

    你可以使用内存映射文件的这种特性来实现同一台机器上的跨进程和跨应用程序域的缓存解决方案。基于内存映射文件的缓存方案包含以下组件:

    ·windows NT服务——启动时创建内存映射文件,停止时删除它。功能是向使用缓存的进程提供句柄。当然,也可以使用命名的内存映射文件来提供操作接口;

    ·缓存托管组件(Cache Management Dll)——实现特定的缓存功能,比如:

    a.     插入和删除数据项到缓存中;

    b.     使用算法清除缓存,比如最后使用算法(Least Recently Used);

    c.     保证数据不被篡改;

    基于内存映射文件的缓存方案可以用在应用程序的每个层中,但由于使用win32 API调用,所以并不容易实现。.Net 框架不支持内存映射文件,所以只能以非托管代码的方式运行,当然也不能利用.Net框架的有力特性,比如垃圾回收等。同时缓存数据项的管理功能需要定制开发,还要开发性能计数器来监控缓存的效果。

    2.4             使用SQL Server缓存

    如果需要在进程回收(重启)、机器重启或电源故障的过程中保持缓存数据的有效,基于内存的方案并不能满足要求。你可以使用基于永久数据存储的方案,如SQL server数据库或NTFS文件系统。

    SQL Server在使用sql语句或存储过程得到数据时,对varchar和varBinary类型的数据有8k的大小限制,你必须使用.Net 框架提供的Ado.Net SQLDataAdapter对象来访问datatable或dataset。

    使用SQL Server缓存数据的优点:

    ·易于实现——使用.Net 框架和Ado.Net访问数据库相当方便;

    ·完善的安全模型和很高的健壮性;

    ·数据非常方便的共享;

    ·数据的持久保留。

    ·支持很大的数据量。

    ·方便的管理工具

    当然,也有缺点:

    ·需要安装SQL Server,对小型应用来说不合适;

    ·重新构造数据的性能和读取数据库的性能比较;

    ·网络负担。

    2.5             使用静态变量缓存

    静态变量常用来记录类的状态,你可以用它来创建定制的缓存对象。在定制的缓存类中将你的数据存储器声明为静态变量,并且提供维护接口(插入、删除和访问等)。如果没有特殊的缓存需求(比如依赖、失效策略等),使用静态变量缓存数据是很方便的。由于是在内存中,这种方案可提供对缓存数据的直接、高速的访问,当没有替代方案解决键值对的存储且对速度要求很高时,可以使用静态变量。当然,在asp.net中,应该使用Cache对象。

    你可以使用这种方案保存大数据的对象,前提是它不经常更改。由于没有清除机制,大数据的内存消耗会影响性能。

    你需要保证定制线程安全机制,或者使用.Net框架提供的同步对象,比如Hashtable。以下代码是使用Hashtable实现的例子:

    static Hashtable mCacheData = new Hashtable();

    应用范围:本方案的应用范围可以限制到类、模块或整个项目。如果变量定义为public,整个项目中的代码都能访问它,范围是整个应用程序域,实现了高效的共享。而它的生存期是和范围紧密相关的。

    2.6             使用asp.net session state

    你可以使用基于HttpSessionState对象的asp.net session state来缓存单个用户的会话状态信息。它解决了asp中会话状态的很多限制,包括:

    ·asp session要求客户端接受cookies,否则就不能使用session;而asp.net可以配置为不使用cookie;

    ·对web server场的情况,asp的session不能支持;当稳定性和可用性要求很高时,asp.net session state虽然效果不好,但对比较小的单个值scalar Value(比如登录信息),还是很有效。

    Asp.net session有很大改进,下面描述使用范围和使用方式。

    Asp.net session state有三种操作模式:

    1、  进程内模式InProc——Session State信息在asp.net工作进程aspnet_wp.exe的进程的内存中存储。这是默认选项,这种情况下,如果进程或应用程序域被回收,则Session 状态信息也被回收;

    2、   进程外模式State Server——状态信息序列化后保存在独立的状态进程中(AspNet_State.exe),所以状态信息可以保存在专门的服务器上(一个状态服务器State Server);

    3、   Sql server模式——状态信息序列化后保存在SQL Server数据库中。

    你可以通过调整配置文件中<sessionState>标签的mode属性来设置要使用的状态模式,比如使用SQL Server模式来在Web server场中共享状态信息。当然,这个优势也有缺点,就是状态信息需要序列化和反序列化,同时多了对数据库的写入和读取,所以性能上有开销,这是要仔细评估的。

    2.6.1  选择使用模式

    2.6.1.1 使用InProc模式

    当使用进程内模式时,状态信息保存在aspnet_wp.exe的进程中。由于在web场的情况下aspnet_wp.exe的多个实例在同一台服务器上运行,所以进程内模式不适用与web场的情况。

    进程内模式是唯一支持Session_End事件的session模式,当用户会话超时或中止时,可以运行Session_End中的事件处理代码来清除资源。

    2.6.1.2 使用StateServer模式

    StateServer模式使用指定的进程储存状态信息。因为它也是一种进程外模式,所以要保证你存储的对象是可序列化的,以支持跨进程传输。

    当使用Session对象在web场的情况下使用时,必须保证web.config文件中的<MachineKey>元素在所有服务器上是唯一的。这样所有的服务器使用同样的加密方式,才能访问缓存中的数据。参考msdn中的“MachineKey元素”。

    2.6.1.3 使用SQL Server模式

    SQL Server模式下,当你使用信任连接(trusted_connection=true 或 integrated security=sspi)访问Session state信息时,不能在asp.net中使用身份用户模拟。

    默认情况下,SQL Server将状态信息存储在TempDb数据库中,它在每次Sql server服务启动时会自动重新创建,当然,你可以指定自己的数据库以便在数据库重启的过程中也能保持数据。

    2.6.2  决定使用Session对象要存储的内容

    你可以使用Session对象缓存任何类型的.net框架数据,但是要了解对某种类型来说最好的方式是什么。有以下几点需要说明:

    1、  对基本类型(比如Int,Byte,String)来说,可以使用任何方式。因为在选用进程外方式时,asp.net使用一个优化的内部方法来序列化和反序列化基本类型的数据;

    2、  对复杂类型(如ArrayList)来说,只选用进程内方式。因为asp.net使用BinaryFormatter来序列化和反序列化这类数据,而这会影响性能的。当然,只有在State Server和SQL Server的方式下,才会进行序列化操作;

    3、  缓存的安全问题,当在缓存中存储敏感数据时,需要考虑安全性,其它页面可以访问到缓存中的数据;

    4、  避免缓存大数据,那会降低性能;

    5、  这种缓存方式不支持过期策略、清除和依赖。

    2.6.3  实现Session State

    Asp.net提供了简单接口来操作Session State,并可使用Web.Config进行简单设置,当配置文件中的设置改变时,能够在页面上立刻体现出来,而不需要重新启动asp.net进程。

    以下代码演示了使用SQL Server来实现Session数据的存储和使用。

    <sessionState

    mode="SQLServer"

    stateConnectionString="tcpip=127.0.0.1:42424"

    sqlConnectionString="data source=127.0.0.1; Integrated Security=SSPI"

    cookieless="false"

    timeout="20"

    />

    private void SaveSession(string CartID)

    {

    Session["ShoppingCartID"] = CartID;

    }

    private void CheckOut()

    {

    string CartID = (string)Session["ShoppingCartID"];

    if(CartID != null)

    {

    // Transfer execution to payment page.

    Server.Transfer("Payment.aspx");

    }

    else

    {

    // Display error message.

    }

    }

    2.7             使用Asp.net客户端缓存和状态

    你还可以使用客户端存储页面信息的方式来降低服务器的负担,这种方法提供最低的安全保障,但却有最快的性能表现。由于需要将数据发送到客户端存储,所以数据量有限。

    实现客户端缓存的机制有以下五种,接下来将依次介绍:

    ·隐藏栏位(Hidden Field)

    ·View State

    ·隐藏帧(Hidden Frame)

    ·Cookies

    ·Query String

    这五种方式分别适合于存储不同类型的数据。

    2.7.1  使用Hidden Field

    你可以将经常改变的少量数据保存在HtmlInputHidden中来维护页面的状态。当每次页面回送的过程中,这些数据都会包含在表单中大送到服务器,所以你要使用HTTP POST方式来提交页面。

    使用这种方式的优点如下:

    1. 不需要服务器资源,直接从页面中读取;
    2. 几乎所有的浏览器都支持;
    3. 实现简单;
    4. 由于数据在页面中,所以在web Farm的情况下也可使用。

    缺点:

    1. 由于可以通过查看源码看到,所以可能会被篡改;
    2. 不支持复杂格式的数据,复杂数据必须使用解析字符串的方式来间接得到;
    3. 当存储大数据的时候会影响性能。

    示例:

    <input id="HiddenValue" type="hidden" value="Initial Value" runat="server" NAME="HiddenValue">

    2.7.2  使用View State

    所有的Web Form页面和控件都包含有一个ViewState属性,在对同一页面多次请求时可以保持页面内的值。它的内部实现是维护相应的hidden field,只不过是加密了的,所以比hidden field的安全性要好。

    使用View State的性能表现很大程度上依赖于服务器控件的类型。一般来说,Label,TextBox,CheckBox,RadioButton,HyperLink的性能要好一些,而DropdownList,ListBox,DataGrid和DataList就要差很多,因为包含的数据量太大,所以每次页面回送都很耗时间。

    有些情况下不推荐使用ViewState,比如:

    1、  不需要回送的页面避免使用;

    2、  避免使用ViewState保存大数据量;

    3、  在需要使用会话超时的情况下避免使用它,因为它没有超时操作。

    ViewState的性能表现和Hidden Field的是类似的,但是具有更高的安全性。

    优点:

    1. 数据在页面中自动维护,不需要服务器资源;
    2. 实现简单;
    3. 数据是经过加密和压缩的,比hidden field有更高的安全性;
    4. 数据存在客户端,可以在Web Farm情况下使用。

    缺点:

    1. 存储大数据量时会降低性能;
    2. 和hidden field类似,在客户端数据仍然有潜在的安全威胁。

    示例代码如下:

    public class ViewStateSample : System.Web.UI.Page

    {

    private void Page_Load(object sender, System.EventArgs e)

    {

    if (!Page.IsPostBack)

    {

    // Save some data in the ViewState property.

    this.ViewState["EnterTime"] = DateTime.Now.ToString();

    this.ViewState["UserName"] = "John Smith";

    this.ViewState["Country"] = "USA";

    }

    }

    private void btnRefresh_Click(object sender, System.EventArgs e)

    {

    // Get the saved data in the view state and display it.

    this.lblTime.Text = this.ViewState["EnterTime"].ToString();

    this.lblUserName.Text = this.ViewState["UserName"].ToString();

    this.lblCountry.Text = this.ViewState["Country"].ToString();

    }

    }

    2.7.3  使用Hidden Frame

    你可以使用Hidden Frame在客户端缓存数据,这就避免了使用hidden field和使用view state时每次页面回送时的缓存数据往返。比如你可以秘密的加载多个页面所需要的图片,这并不会消耗服务器资源。

    优点:

    a.     可以加载较多数据而不只是单个栏位的值;

    b.     避免了不必要的多次回送中的数据往来;

    c.     可以缓存和读取在不同表单中存储的数据项(可以同时缓存多个页面的数据);

    d.     可以访问同一站点不同frame中的客户端脚本数据。

    缺点:

    a.     有些浏览器不支持frame;

    b.     源代码可以在客户端看到,有潜在的安全威胁;

    c.     隐藏frame的数量没有限制,如果框架页面包含较多hidden frame的话,在首次加载时速度会有限制。

    示例代码如下:

    <FRAMESET cols="100%,*">

    <FRAMESET rows="100%,*">

    <FRAME src="contents_of_frame1.html">

    </FRAMESET>

    <FRAME src="contents_of_hidden_frame.html">

    <FRAME src="contents_of_hidden_frame.html" frameborder="0" noresize scrolling="yes">

    <NOFRAMES>

    <P>This frameset document contains:

    <A href="contents_of_frame1.html" TARGET="_top">Some neat contents</A>

    </NOFRAMES>

    </FRAMESET>

    2.7.4  使用Cookies

    Cookie是可以在客户端存储数据另一种方案,这里不过多介绍。

    优点:

    1. 不需要服务器资源;数据保存在客户端,在用户请求时发送到服务器上。
    2. 使用简单。Cookie包含简单的键值对,主要保存轻量级的文本数据。
    3. 支持过期策略;可以指定当会话结束时过期,也可指定一个时间策略。

    缺点:

    1. 数据量的限制;
    2. 用户可能设置为拒绝Cookie;
    3. 安全问题;用户可能更改机器上的cookie信息,造成基于cookie的系统运行失败;
    4. 可能过期或被用户删除,造成一定程度的不可用。

    参看示例代码:

    public class CookiesSample : System.Web.UI.Page

    {

    private void Page_Load(object sender, System.EventArgs e)

    {

    if (this.Request.Cookies["preferences1"] == null)

    {

    HttpCookie cookie = new HttpCookie("preferences1");

    cookie.Values.Add("ForeColor","black");

    cookie.Values.Add("BackColor","beige");

    cookie.Values.Add("FontSize","8pt");

    cookie.Values.Add("FontName","Verdana");

    this.Response.AppendCookie(cookie);

    }

    }

    private string getStyle(string key)

    {

    string val = null;

    HttpCookie cookie= this.Request.Cookies["preferences1"];

    if (cookie != null)

    {

    val = cookie.Values[key];

    }

    return val;

    }

    }

    2.7.5  使用Query String

    Query String是在用户请求的URL后加上相应的参数来使用的,只能在使用HTTP GET方式调用URL时可用。

    优点:

    d.     不需要服务器资源,参数附在URL里面;

    e.     应用面广,几乎所有浏览器都支持;

    f.     实现简单,服务端使用Request对象可直接读取。

    缺点:

    a.     参数直接对用户可见,不安全;

    b.     URL长度的限制,多数浏览器不支持超过255字符的URL。

    示例代码:

    http://www.cache.com/login.asp?user=ronen

    string user = Request.QueryString["User"];

    2.7.6  小结

    下表是使用客户端缓存的建议:

    缓存机制

    适用情况

    Hidden Field

    当安全性要求不高时,在页面中存储少量数据以提交到服务器上的本页面或其它页面。

    ViewState

    在单个页面中存储少量信息满足页面多次回传的要求。提供基本的安全机制。

    Hidden Frame

    在客户端存储数据,避免了数据到服务器的回传。

    Cookie

    当安全性要求不高时,存储少量数据在客户端。

    Query String

    当使用页面地址连接页面时传输少量参数。

     

    2.8             使用Internet Explorer缓存

    IE提供了缓存机制,可以实现对页面的数据进行缓存,同时可以指定过期时间。用户在IE中请求此页面,如果当过期时间没有到,则自动从缓存中提取并呈现;否则,就到服务器上获取新版本。IE对页面的缓存可以在IIS中设置。

    适合在Internet Explorer中缓存的内容

    1. 页面中的图像文件;
    2. 静态的文本内容;
    3. 页面的标题栏和页脚内容——改变频率很低,可以给用户一个迅速相应;
    4. 网站的首页——更改次数页时相对较少的;
    5. 使用动态HTML在客户端保存的特定数据。比如客户自定义的颜色和布局设置信息。

    优点:

    1. 减少对服务器的请求和网络负担;
    2. 支持离线浏览;
    3. 可以实现基于XML的客户端复杂应用。

    缺点:

    1. 客户端的过期时间必须预先指定而不能依赖于服务器更新;IE采用的是Lazy更新机制,优先从缓存中提取数据;
    2. 对其它客户端浏览器没有作用;
    3. 存储的数据没有加密,不能保证客户端数据安全。

    示例代码:

    <META HTTP-EQUIV="expires" CONTENT="Tue, 23 Jun 2002 01:46:05 GMT">

    3         总结

    本文档介绍了缓存和状态数据存储的相关概念,以及可供使用的缓存技术,介绍了各种技术的适用范围,并对其优缺点进行了说明,另外有简单的性能比较和简单的示例代码。更多内容请参看相应的参考资料。

    展开全文
  • 缓存穿透,缓存击穿,缓存雪崩解决方案分析

    万次阅读 多人点赞 2017-09-09 12:07:12
    设计一个缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效应。 缓存穿透 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则...

    前言

    设计一个缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效应。

    缓存穿透

    缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

    解决方案

    有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

    缓存雪崩

    缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

    解决方案

    缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

    缓存击穿

    对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

    缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

    解决方案

    1.使用互斥锁(mutex key)

    业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

    SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。在redis2.6.1之前版本未实现setnx的过期时间,所以这里给出两种版本代码参考:

    //2.6.1前单机版本锁
    String get(String key) {  
       String value = redis.get(key);  
       if (value  == null) {  
        if (redis.setnx(key_mutex, "1")) {  
            // 3 min timeout to avoid mutex holder crash  
            redis.expire(key_mutex, 3 * 60)  
            value = db.get(key);  
            redis.set(key, value);  
            redis.delete(key_mutex);  
        } else {  
            //其他线程休息50毫秒后重试  
            Thread.sleep(50);  
            get(key);  
        }  
      }  
    }
    最新版本代码:

    public String get(key) {
          String value = redis.get(key);
          if (value == null) { //代表缓存值过期
              //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
    		  if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
                   value = db.get(key);
                          redis.set(key, value, expire_secs);
                          redis.del(key_mutex);
                  } else {  //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
                          sleep(50);
                          get(key);  //重试
                  }
              } else {
                  return value;      
              }
     }
    memcache代码:

    if (memcache.get(key) == null) {  
        // 3 min timeout to avoid mutex holder crash  
        if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
            value = db.get(key);  
            memcache.set(key, value);  
            memcache.delete(key_mutex);  
        } else {  
            sleep(50);  
            retry();  
        }  
    } 

    2. "提前"使用互斥锁(mutex key):

    在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。伪代码如下:

    v = memcache.get(key);  
    if (v == null) {  
        if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
            value = db.get(key);  
            memcache.set(key, value);  
            memcache.delete(key_mutex);  
        } else {  
            sleep(50);  
            retry();  
        }  
    } else {  
        if (v.timeout <= now()) {  
            if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
                // extend the timeout for other threads  
                v.timeout += 3 * 60 * 1000;  
                memcache.set(key, v, KEY_TIMEOUT * 2);  
      
                // load the latest value from db  
                v = db.get(key);  
                v.timeout = KEY_TIMEOUT;  
                memcache.set(key, value, KEY_TIMEOUT * 2);  
                memcache.delete(key_mutex);  
            } else {  
                sleep(50);  
                retry();  
            }  
        }  
    } 

    3. "永远不过期":  

    这里的“永远不过期”包含两层意思:

    (1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。

    (2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期

            从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

    String get(final String key) {  
            V v = redis.get(key);  
            String value = v.getValue();  
            long timeout = v.getTimeout();  
            if (v.timeout <= System.currentTimeMillis()) {  
                // 异步更新后台异常执行  
                threadPool.execute(new Runnable() {  
                    public void run() {  
                        String keyMutex = "mutex:" + key;  
                        if (redis.setnx(keyMutex, "1")) {  
                            // 3 min timeout to avoid mutex holder crash  
                            redis.expire(keyMutex, 3 * 60);  
                            String dbValue = db.get(key);  
                            redis.set(key, dbValue);  
                            redis.delete(keyMutex);  
                        }  
                    }  
                });  
            }  
            return value;  
    }

    4. 资源保护:

    采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。

    四种解决方案:没有最佳只有最合适

    解决方案 优点 缺点
    简单分布式互斥锁(mutex key)

     1. 思路简单

    2. 保证一致性

    1. 代码复杂度增大

    2. 存在死锁的风险

    3. 存在线程池阻塞的风险

    “提前”使用互斥锁  1. 保证一致性 同上 
    不过期(本文)

    1. 异步构建缓存,不会阻塞线程池

    1. 不保证一致性。

    2. 代码复杂度增大(每个value都要维护一个timekey)。

    3. 占用一定的内存空间(每个value都要维护一个timekey)。

    资源隔离组件hystrix(本文)

    1. hystrix技术成熟,有效保证后端。

    2. hystrix监控强大。

     

     

    1. 部分访问存在降级策略。



    四种方案来源网络,详文请链接:http://carlosfu.iteye.com/blog/2269687?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

    总结

    针对业务系统,永远都是具体情况具体分析,没有最好,只有最合适。

    最后,对于缓存系统常见的缓存满了和数据丢失问题,需要根据具体业务分析,通常我们采用LRU策略处理溢出,Redis的RDB和AOF持久化策略来保证一定情况下的数据安全。

    展开全文
  • 什么是缓存

    千次阅读 2019-05-26 15:08:23
    硬件的缓存? cpu缓存:位于cpu和内存之间的临时存储器 软件缓存? 软件缓存分为三级 内存缓存(预先将数据写到容器(list,map,set)等数据存储单元中,就是软件内存缓存) 数据库缓存 网络缓存 内存缓存淘汰机制...
    1. 硬件的缓存?
      cpu缓存:位于cpu和内存之间的临时存储器
    2. 软件缓存?
      软件缓存分为三级
      1. 内存缓存(预先将数据写到容器(list,map,set)等数据存储单元中,就是软件内存缓存)
      2. 数据库缓存
      3. 网络缓存
    3. 内存缓存淘汰机制分为三种
      1. FIFO(First In,First Out)先进先出
        优点:是先进先出的数据缓存器,他与普通存储器的区别是没有外部读写地址线,这样使用起来非常简单。
        缺点:只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加1完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址

      2. LFU(Least Freauently Used)
        最不经常使用页置换算法,清理掉留给经常使用的使用

      3. LRU(Least Recently Used)喜新厌旧
        内存管理的一种页面置换算法,新加入的数据放到链表的头部,当缓存命中(被访问)数据移到链表的头部,当链表满的时候,将链表尾部的数据丢弃。

            //单链表
          public class LinkedList<T> {
          
              public LinkedList() {
                  size = 0;
              }
          
              Node list;
              //链表有多少个节点
              int size;
          
              //添加节点
              //在头部添加节点
              public void add(T data) {
                  Node head = list;
                  Node curNode = new Node(data, list);
                  list = curNode;
                  size++;
          
              }
          
              //在链表的index位置插入一个新数据
              public void add(int index, T data) {
                  checkPositionIndex(index);
                  Node cur = list;
                  Node hrad = list;
                  for (int i = 0; i < index; i++) {
                      hrad = cur;
                      cur = cur.next;
                  }
                  Node node = new Node(data, cur);
                  hrad.next = node;
                  size++;
          
              }
          
              // 检查index 是否在链表范围结构
              public void checkPositionIndex(int index) {
                  if (!(index >= 0 && index <= size)) {
                      throw new IndexOutOfBoundsException("index:" + index + ",size" + size);
                  }
              }
          
              //删除节点
              public T remove() {
                  if (list != null) {
                      Node node = list;
                      list = list.next;
                      //gc
                      node.next = null;
                      size--;
                      return node.data;
                  }
          
                  return null;
              }
          
              public T remove(int index) {
                  checkPositionIndex(index);
                  Node cur = list;
                  Node hrad = list;
                  for (int i = 0; i < index; i++) {
                      hrad = cur;
                      cur = cur.next;
                  }
                  hrad.next = cur.next;
                  //gc
                  cur.next = null;
                  size--;
                  return null;
          
              }
          
              public T removeLast() {
                  if (list != null) {
                      Node node = list;
                      Node cur = list;
                      while (cur.next != null) {
                          node = cur;
                          cur = cur.next;
                      }
                      //gc
                      node.next = null;
                      size--;
                      return cur.data;
                  }
                  return null;
              }
          
              //修改节点
          
              public void updata(int index, T newData) {
                  checkPositionIndex(index);
                  Node head = list;
                  for (int i = 0; i < index; i++) {
                      head = head.next;
                  }
                  head.data = newData;
              }
          
              //查询节点
              public T get() {
                  Node node = list;
                  if (node != null) {
                      return node.data;
          
                  } else {
                      return null;
                  }
              }
          
              public T get(int index) {
                  checkPositionIndex(index);
                  Node node = list;
                  for (int i = 0; i < index; i++) {
                      node = node.next;
                  }
                  return node.data;
              }
          
          
              class Node {
                  T data;
                  Node next;
          
                  public Node(T data,
                              Node next) {
                      this.data = data;
                      this.next = next;
                  }
              }
          
              @Override
              public String toString() {
                  Node node = list;
                  for (int i = 0; i < size; i++) {
                      System.out.println("" + node.data);
          
                      node = node.next;
                  }
                  return super.toString();
              }
          }
        
        public class LruLinhedList<T> extends LinkedList<T> {
        
            // 用于限定内存空间的大小
            int memorySize;
            static final int DEFAULT_CAP = 6;
        
            public LruLinhedList() {
                this(DEFAULT_CAP);
            }
        
            public LruLinhedList(int memorySize) {
                this.memorySize = memorySize;
            }
        
            //    LRU 添加节点
            public void lruAdd(T data) {
                if (size >= memorySize) {
                    removeLast();
                    add(data);
                } else {
                    add(data);
                }
            }
        
            // 删除节点
            public T lruRmove() {
                return removeLast();
            }
        
            public T lruGet(int index) {
                checkPositionIndex(index);
                Node node = list;
                Node pro = list;
                for (int i = 0; i < index; i++) {
                    pro = node;
                    node = node.next;
                }
                T reData = node.data;
                //访问节点移开头
                pro.next = node.next;
                Node head = list;
                node.next = head;
                list = node;
                return reData;
        
            }
        
            public static void main(String[] args) {
                LruLinhedList<String> LinkedList = new LruLinhedList<>(6);
                for (int i = 0; i < 6; i++) {
                    LinkedList.lruAdd("" + i);
                }
                LinkedList.toString();
                System.out.println("" + LinkedList.lruGet(4));
        
                LinkedList.toString();
                LinkedList.lruAdd(90+"");
                LinkedList.toString();
        //        LinkedList.lruGet(23);
        //        LinkedList.toString();
            }
        
        }
        
        
    展开全文
  • 各种缓存介绍

    万次阅读 2017-07-27 10:32:02
    说起缓存相关技术,老多了, memcache、redis、squid、varnish、web cache、 CDN等等。缓存技术五花八门,但这些技术间有什么共性的地方,又有什么不同的地方呢?答案肯定是有的,这次为大家分享及整理一下缓存方面...
  • 页面缓存、URL缓存、对象缓存

    千次阅读 2018-07-15 17:29:17
    页面缓存优化:这种缓存技术一般用于不会经常变动信息,并且访问次数较多的页面,这样就不用每次都动态加载。商品列表页 页面缓存:1.取缓存 ()2.手动渲染 3.结果输出Thymeleaf 的页面渲染技术。 @ResponseBody @...
  • spark cache (几种缓存方法)

    万次阅读 2018-02-08 17:19:33
    例如有一张hive表叫做activity。cache表,数据放内存,数据被广播到Executor,broadcast,将数据由reduce side join 变map side join。效果都是查不多的,基本表达的都是一个意思。具体效果体现:读数据次数变小;...
  • Java缓存机制

    万次阅读 2018-09-04 11:14:03
    1 Java缓存 1.1 jvm内置缓存 Java中实现缓存的方式有很多,比如用static hashMap基于内存缓存的jvm内置缓存,简单不实用,保对象的有效性和周期无法控制,容易造成内存急剧上升。常用的有Oscache(主要针对jsp页面...
  • 什么是缓存缓存策略有哪些?

    千次阅读 2019-06-30 15:52:17
    1、什么是缓存? ☞ 缓存就是数据交换的缓冲区(称作:Cache),当某一硬件要读取数据时,会首先从缓存汇总查询数据,有则直接执行,不存在时从内存中获取。由于缓存的数据比内存快的多,所以缓存的作用就是帮助...
  • 缓存穿透、缓存击穿、缓存雪崩区别和解决方案

    万次阅读 多人点赞 2018-09-19 14:37:57
    一、缓存处理流程  前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。     二、缓存穿透  描述:  缓存...
  • Mybatis的一级缓存和二级缓存的理解和区别

    万次阅读 多人点赞 2018-03-01 15:31:59
    一级缓存基于sqlSession默认开启,在操作数据库时需要构造SqlSession对象,在对象中有一个HashMap用于存储缓存数据。不同的SqlSession之间的缓存数据区域是互相不影响的。一级缓存的作用域是SqlSession范围的,当在同...
  • maven清除仓库缓存

    万次阅读 2017-06-09 14:14:34
    cd %userprofile%.m2\repository for /r %i in (*.lastUpdated) do del %i
  • 各大浏览器清除缓存的快捷键

    万次阅读 2017-10-25 20:13:22
    CTRL+SHIFT+DEL:直接进入“清除浏览数据”页面,包括清除浏览历史记录、清空缓存、删除Cookie等。 注意  delete键 不是键盘右下角数字0右边那个 ". Del" 而是 一个名字为 “delete”的键
  • eclipse 中清除项目中的缓存

    万次阅读 2012-07-27 16:41:59
    在菜单里面选择project--clean server也可以clean
  • Google浏览器清除缓存快捷键

    万次阅读 2015-08-21 20:45:36
    CTRL+SHIFT+DEL:直接进入“清除浏览数据”页面,包括清除浏览历史记录、清空缓存、删除Cookie等。
  • hibernate一级缓存和二级缓存的区别

    万次阅读 多人点赞 2014-07-24 09:04:56
    缓存是介于应用程序和物理数据源之间,其作用是为了降低应用程序对物理数据源访问的频次,从而提高了应用的运行性能。缓存内的数据是对物理数据源中的数据的复制,应用程序在运行时从缓存读写数据,在特定的时刻或...
  • Nginx 反向代理并缓存缓存清除

    万次阅读 2020-01-19 12:59:32
    Nginx 反向代理并缓存缓存清除 参考地址 :Nginx 反向代理并缓存缓存清除
  • 作为新手,尝试读取数据的时候缓存数据在redis中,已经放成功了, 去redis中找没找到,要怎么查看我redis中放了的缓存数据。 谢谢大神指导。
  • Redis 本地缓存+远程缓存方案 使用纯java的ehcache作为本地缓存 Reids 作为远程分布式缓存 解决redis缓存压力过大,提高缓存速度,以及缓存性能。 Redis和ehcache缓存的区别 如果是单个应用或者对缓存访问要求很高的...
  • Oracle清除缓存的命令

    万次阅读 2017-05-24 11:23:05
    应该是Oracle缓存的作用,第一次读完以后放入缓存,以后读取就很快了。 但是测试的时候这样可不行。 解决方法:清除缓存 ALTER SYSTEM FLUSH SHARED_POOL ALTER SYSTEM FLUSH BUFFER_CACHE ALTER SYSTEM FLUSH ...
1 2 3 4 5 ... 20
收藏数 1,859,715
精华内容 743,886
关键字:

缓存