Java面试——缓存
阅读原文时间:2023年07月09日阅读:3

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


【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 达到内存的上线时会冲掉旧的数据。也会将value的数据通过 swap机制同步到磁盘进行存储。但是 key的之不能使用 swap机制。如果key也很多的话,可以通过压缩记性存储。


【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。


【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 类型中会占用更少内存。

1 192.168.2.129:6379> hset myhash name zhangsan
2 (integer) 1
3 192.168.2.129:6379> hset myhash age 20
4 (integer) 1
5 192.168.2.129:6379> hget myhash name
6 "zhangsan"
7 192.168.2.129:6379> hget myhash age
8 "20"
9 192.168.2.129:6379>


使用阶段我们从数据存储和数据获取两个方面来说明开发时的注意事项:
【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
内存优化】:链接
淘汰策略】:链接




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


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