过期的key集合
Redis会将每个设置了过期时间的key放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的key。除了定时遍历之外,他还会使用惰性策略来删除过期的key,所谓惰性策略就是在客户端访问这个key的时候,redis对key的过期时间进行检查,如果过期了就立即删除。定时删除是集中处理,惰性删除是零散处理。
定时扫描策略
Redis默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的key,而是采用了一种简单的贪心策略。
- 从过期字典中随机20个key。
- 删除这20个key中已经过期的key。
- 如果过期key的比率超过1/4,那就重复步骤1。
同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上线,默认不会超过25ms。
设想一个大型的Redis实例中所有key在同一时间过期了,会出现怎样的结果。
毫无疑问,Redis会持续扫描过期字典(循环多次),直到过期字典中过期的key变得稀疏,才会停止(循环次数明显下降)。这就会导致线上读写请求出现明显的卡顿现象。导致这种卡顿的另一种原因是内存管理器需要频繁回收内存页,这也会产生一定的CPU消耗。
为什么设置了25ms超时时间,仍然会卡顿
假设有101个客户端同时将请求发过来,每一个请求都需要经过25ms的超时时间,那么第101个指令需要等待2500ms后才能得到执行,这个就是客户端的卡顿时间,是由服务器不间断的小卡顿积少成多导致的。
所以开发人员一定要注意过期时间,如果有大批量的key过期,要给过期时间设置一个随机范围,而不能全部在同一时间过期。
从库的过期策略
从库不会进行过期扫描,从库对过期的处理是被动的。主库在key到期时,会在AOF文件里增加一条del指令,同步到所有的从库,从库通过执行这条del指令来删除过期的key。
因为指令同步是异步进行的,所以从库过期的key的del指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据从库里还存在,比如集群环境分布式锁的算法漏洞就是因为这个同步延迟产生的。
LRU淘汰算法
当Redis内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换,交换会让Redis的性能急剧下降,对于访问量比较频繁的Redis来说,这样龟速的存取效率基本上等于不可用。
在生产环境中是不允许Redis出现交换行为的,为了限制最大使用内存,Redis提供了配置参数maxmemory来限制内存超出期望大小。
当实际内存超出maxmemory时,Redis提供了几种可选策略来让用户决定如何腾出新的空间以继续提供读写服务。
- noeviction: 不会继续服务写请求(del请求除外),读请求可以继续进行,这样可以保证不会丢失数据,但是会让线上的业务不能持续进行,这是默认的淘汰策略。
- volatile-lru: 尝试淘汰了设置过期时间的key,最少使用的key优先被淘汰。没有设置过期时间的key不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
- volatile-ttl: 跟上面一样,除了淘汰策略不是lru,而是key的剩余寿命ttl的值,ttl越小越优先被淘汰。
- volatile-random:跟上面一样,淘汰的key是设置了过期时间的key集合中随机的key。
- allkeys-lru:区别于volatile-lru,这个策略要淘汰的key对象是全体的key集合,而不是只是过期的key集合,这意味着没有设置过期时间的key也会被淘汰。
- allkeys-random:所有的key中随机淘汰。
总结: volatile-xxx:该策略只会针对带过期时间的key进行淘汰,allkeys-xxx策略会针对所有的key进行淘汰。如果只是使用Redis做缓存,应该使用allkeys-xxx,客户端写缓存时不必携带过期时间。如果同时使用Redis的持久化功能,那就使用volatile-xxx策略,这样可以保留没有设置过期时间的key。
Redis的近似LRU算法
Redis使用的是一种近似LRU算法,他跟LRU算法不太一样,之所以不使用LRU算法,是因为需要消耗大量的额外内存,需要对现有的数据结构进行较大的改造。近似LRU算法则很简单,在现有数据结构的基础上采用随机采样法来淘汰元素,能达到和LRU算法非常近似的效果。
Redis为实现近似LRU算法,他给每个key增加了一个额外的小字段,这个字段的长度是24个bit,也就是最后一次被访问的时间戳。
上一节提到处理key过期方式分为集中处理和懒惰处理,LRU淘汰不一样,他的处理方式只有懒惰处理。当Redis执行写操作时,发现内存超出maxmemory,就会执行一次LRU淘汰算法,随机采样出5(可以配置)个key,然后淘汰掉最旧的key,如果淘汰后内存还是超出maxmemory,那就继续随机采样淘汰,直到内存低于maxmemory为止。
如何采样就是看maxmemory-policy的配置,如果是allkeys就是从所有的key字典中随机,如果是volatile就从带过期时间的key字典中随机。每次采样多少个key看的是maxmemory_samples的配置,默认是5.
在Redis3.0中算法增加了淘汰池,进一步提升了近似LRU算法的效果。淘汰池是一个数组,他的大小是maxmemory_samples,在每次淘汰循环中,新随机出来的key列表会和淘汰池中的key列表进行融合,淘汰掉最旧的一个key之后,保留剩余较旧的key列表放入淘汰池中等待下一个循环。
惰性删除
一直以来,我们认为Redis是单线程的,不过Redis内部实际上并不是只有一个主线程,他还有几个异步线程专门用来处理一些耗时操作。
Redis为什么要惰性删除
删除指令del会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延迟。不过如果删除的key是一个非常大的对象,比如一个包含了千万元素的hash,那么删除操作就会导致单线程卡顿。
Redis为了解决这个卡顿问题,在4.0版本中引入了unlink指令,他能对删除操作进行惰性处理,丢给后台线程来异步回收内存。
使用多线程进行内存回收是否存在线程安全问题
不会,当unlink指令发出后,被unlink的key就再也无法被主线程中的其他指令访问到了。
flush
Redis提供了flushdb和flushall指令,用来清空数据库,这也是极其缓慢的操作。Redis4.0 同样给这两个指令也带来了异步化,在指令后面增加async参数就可以让后台线程慢慢处理,同时不会再被主线程访问到其中的key。
异步队列
主线程将对象的引用删除后,会将这个key的内存回收操作包装成一个任务,塞进异步任务队列,后台线程会从这个异步队列中取任务。任务队列被主线程和异步线程同时操作,所以必须是一个线程安全的队列。
不是所有的unlink操作都会延后处理,如果对应key所占用的内存很小,延后处理就没有必要了,这时候Redis会将对应的key内存立即回收,跟del指令一样。
AOF sync也很慢
Redis需要每秒一次同步AOF日志到磁盘,确保消息尽量不丢失,需要调用sync函数,这个操作会比较耗时,会导致主线程的效率下降,所以Redis也将这个操作移到异步线程来完成。执行AOF sync操作的线程是一个独立的异步线程,和前面的惰性删除线程不是一个线程,同样他也有一个属于自己的任务队列,队列里只用来存放AOF Sync任务。