一、Redis过期策略
Redis删除过期key采用的是定期删除 + 懒惰删除的策略。
1.1 定期删除策略
Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,默认每 100ms 进行一次过期扫描:
- 随机抽取 20 个 key
- 删除这 20 个key中过期的key
- 如果过期的 key 比例超过 1/4,就重复步骤 1,继续删除。
为什不扫描所有的 key?
- Redis 是单线程,全部扫描岂不是卡死了。而且为了防止每次扫描过期的 key 比例都超过 1/4,导致不停循环卡死线程,Redis 为每次扫描添加了上限时间,默认是 25ms。
- 虽然有扫描上限时间,但因为 Redis 是单线程,每个请求处理都需要排队,每个请求最多 25ms,100 个请求就是 2500ms。
- 如果客户端将超时时间设置的比较短,比如 10ms,那么就会出现大量的链接因为超时而关闭,业务端就会出现很多异常。而且这时你还无法从 Redis 的 slowlog 中看到慢查询记录,因为慢查询指的是逻辑处理过程慢,不包含等待时间。
- 如果在同一时间出现大面积 key 过期,Redis 循环多次扫描过期词典,直到过期的 key 比例小于 1/4。这会导致卡顿,而且在高并发的情况下,可能会导致缓存雪崩。
- 如果有大批量的 key 过期,要给过期时间设置一个随机范围,而不宜全部在同一时间过期,分散过期处理的压力。
1.2 从库过期策略
- 从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
- 因为指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在。
1.3 懒惰删除策略
优化场景
删除指令 del 会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延迟。
如果删除的 key 是一个非常大的对象,比如一个包含了千万元素的 hash,又或者在使用 FLUSHDB 和 FLUSHALL 删除包含大量键的数据库时,那么删除操作就会导致单线程卡顿。
指令
- unlink 指令,它能对删除操作进行懒处理,丢给后台线程来异步回收内存。
- flush asyn,清空数据库是极其缓慢的操作。在指令后面增加 async 参数就可以将整棵大树连根拔起,扔给后台线程慢慢焚烧。
1.4 内存淘汰机制
Redis 的内存占用会越来越高。Redis 为了限制最大使用内存,提供了 redis.conf 中的配置参数 maxmemory。
当内存超出 maxmemory,Redis 提供了几种内存淘汰机制让用户选择,配置 maxmemory-policy:
- noeviction:当内存超出 maxmemory,写入请求会报错,但是删除和读请求可以继续。(使用这个策略,疯了吧)
- allkeys-lru:当内存超出 maxmemory,在所有的 key 中,移除最少使用的key。只把 Redis 既当缓存是使用这种策略。(推荐)。
- allkeys-random:当内存超出 maxmemory,在所有的 key 中,随机移除某个 key。(应该没人用吧)
- volatile-lru:当内存超出 maxmemory,在设置了过期时间 key 的字典中,移除最少使用的 key。把 Redis 既当缓存,又做持久化的时候使用这种策略。
- volatile-random:当内存超出 maxmemory,在设置了过期时间 key 的字典中,随机移除某个key。
- volatile-ttl:当内存超出 maxmemory,在设置了过期时间 key 的字典中,优先移除 ttl 小的。
1.5 LRU 算法
- 实现 LRU 算法除了需要 key/value 字典外,还需要附加一个链表,链表中的元素按照一定的顺序进行排列。当空间满的时候,会踢掉链表尾部的元素。当字典的某个元素被访问时,它在链表中的位置会被移动到表头。所以链表的元素排列顺序就是元素最近被访问的时间顺序。
- Redis 使用的并不是完全 LRU 算法。不使用 LRU 算法,是为了节省内存,Redis 采用的是随机LRU算法,Redis 为每一个 key 增加了一个24 bit的字段,用来记录这个 key 最后一次被访问的时间戳。
- Redis 的 LRU 淘汰策略是懒惰处理,也就是不会主动执行淘汰策略,当 Redis 执行写操作时,发现内存超出 maxmemory,就会执行 LRU 淘汰算法。这个算法就是随机采样出5(默认值)个 key,然后移除最旧的 key,如果移除后内存还是超出 maxmemory,那就继续随机采样淘汰,直到内存低于 maxmemory 为止。
- LFU 表示按最近的访问频率进行淘汰,它比 LRU 更加精准地表示了一个 key 被访问的热度。
二、Redis常见问题
2.1 缓存雪崩
定义
通常会使用缓存用于缓冲对 DB 的冲击,如果缓存宕机,所有请求将直接打在 DB,造成 DB 宕机——从而导致整个系统宕机。
解决方案
- 对缓存做高可用,防止缓存宕机
- 使用断路器,如果缓存宕机,为了防止系统全部宕机,限制部分流量进入 DB,保证部分可用,其余的请求返回断路器的默认值。
2.2 缓存穿透
定义1
缓存查询一个没有的 key,同时数据库也没有,如果黑客大量的使用这种方式,那么就会导致 DB 宕机。
解决方案
使用一个默认值来防止。当访问一个不存在的 key,然后再去访问数据库,还是没有,那么就在缓存里放一个占位符,下次来的时候,检查这个占位符,如果发生时占位符,就不去数据库查询了,防止 DB 宕机。
定义2
大量请求查询一个刚刚失效的 key,导致 DB 压力倍增,可能导致宕机,但实际上,查询的都是相同的数据。
解决方案
可以在这些请求代码加上双重检查锁。但是那个阶段的请求会变慢。不过总比 DB 宕机好。
2.3 缓存并发竞争
定义
多个客户端写一个 key,如果顺序错了,数据就不对了。但是顺序我们无法控制。
解决方案
使用分布式锁,例如 zk,同时加入数据的时间戳。同一时刻,只有抢到锁的客户端才能写入,同时,写入时,比较当前数据的时间戳和缓存中数据的时间戳。
2.4 双写不一致
定义
连续写数据库和缓存,但是操作期间,出现并发了,数据不一致了。
解决方案
先更新数据库,再删除缓存。
若更新操作的时候,同时进行查询操作,若hit,则查询得到的数据是旧的数据。但是不会影响后面的查询。(代价较小)
脏数据
一个读操作没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。
该情况出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率非常小。
三、Redis和MongoDB
大家一般称之为Redis缓存、MongoDB数据库。
- Redis主要把数据存储在内存中,其“缓存”的性质远大于其“数据存储“的性质,其中数据的增删改查也只是像变量操作一样简单;
- MongoDB是一个“存储数据”的系统,增删改查可以添加很多条件,就像SQL数据库一样灵活
Q.E.D.
Comments | 0 条评论