Redis驱逐和驱逐Key
我们知道del命令会主动删除数据,还有什么情况数据能删除呢?在内存满的时候Redis会如何应对呢?Key到达过期时间会被立刻删除吗?删除大Key会影响性能吗?异步删除是怎么回事呢?如下我们一同探讨。
一. 同步和异步删除
[前赴后继,一呼百应]
DEL 和 UNLINK
Redis服务自身对Key的删除,可以分为「同步删除」和「异步删除」。使用DEL
命令会触发「同步删除」,如果Key是一个有很多元素的复杂类型,这个过程可能会堵塞一下Redis服务自身,从而影响用户的访问。如果使用UNLINK
命令,Redis服务会先计算删除Key的成本,从而更智能地做出「同步删除」或「异步删除」的选择。
成本计算
对于list,hash,set,zset的对象类型,如果长度大于64(由宏LAZYFREE_THRESHOLD定义),才会采用异步删除的手段,从当前db先释放该key,再由另外一个线程做异步删除。对于长度不大于64的复杂类型,异步删除比同步删除还多了一些函数调用与多线程同步的代价,所以同步删除更好。对于string对象,底层的数据结构sds是一份连续的内存,内存分配器回收这块内存的复杂度是O(1),所以采用同步删除也不会堵塞服务。
++总的来说,都能用UNLINK替代DEL。++
二. Key的驱逐
[月满则亏,水满则溢]
定义:
++Redis处理命令前根据内存容量是否触达上限而进行的Key驱逐++。
驱逐策略
Redis通过参数maxmemory
来选择不同的驱逐策略:
- volatile-random 从已设置过期时间的数据集(server.db[i].expires)中任意选择数据驱逐;
- volatile-lru 从数据集(server.db[i].dict)中挑选最近最少使用的数据驱逐(2.8默认);
- volatile-ttl 从已设置过期时间的数据集(server.db[i].expires)中寻找最近即将过期(ttl最小)的key来驱逐;
- allkeys-random 从数据集(server.db[i].dict)中任意选择数据驱逐;
- allkeys-lru 从数据集(server.db[i].dict)中挑选最近最少使用的数据驱逐;
- noeviction 禁止驱逐数据,永远不驱逐,仅对写操作返回一个错误(4.0默认);
在4.0版本后,还增加了以下两种驱逐策略。
- volatile-lfu在过期集合中使用LFU链来驱逐数据;
- allkeys-lfu 从数据集(server.db[i].dict)使用LFU算法来驱逐数据;
行为
Redis在处理命令前,会看看容量是否触达上限。
如果驱逐策略为noeviction,则不会驱逐Key,而是返回写失败。4.0后,在返回写入失败前,还会先检测lazyfree线程是否还有待删除的Key,没有才会给用户返回写入失败。
对于其他策略,都会根据相应定义,进行Key的驱逐,这里不再详述。
在4.0或以上的版本,Key的驱逐会基于参数lazyfree_lazy_eviction
,来决定采用unlink还是del。在2.8版本,则只会用del。lazyfree_lazy_eviction参数默认是no。
这里试问,主从节点都会进行「驱逐」么?
答案是都会的,各自会因应自身的驱逐策略进行驱逐,并且Master节点驱逐的删除命令还会传播到Slave节点。
三. Key的访问淘汰
[雾里看花,视而不见]
定义
++访问一个已过期的Key会触发对其的删除++。
行为
与Key的驱逐一样,Key的访问淘汰同样是基于访问事件来触发的。
主从角色的节点在处理访问淘汰上的逻辑是不同的。
对于Slave节点,访问到了已过期的Key,Slave节点会返回该Key不存在,但不会主动删除该Key。删除的动作,还是会从Master上同步过来。
对于Master节点,在4.0或以上的版本,会根据参数lazyfree-lazy-expire
,来决定用DEL还是UNLINK。对于2.8版本,则只能用DEL了。这些删除的动作,都会同步到Slave与AOF文件中。
默认是不开启异步删除的,即lazyfree-lazy-expire=no,用户最好是开其它。
四. Key的定时淘汰
[花开花落,六度轮回]
定义
++Redis自身的定时调度把已过期Key删除++。
行为
多久会执行一次定时调度呢?
redis服务的参数hz能控制定时淘汰的频率,hz默认是10,即每秒能调度100次。
刚才说「访问淘汰」的逻辑只会在Master角色上发生,那「访问淘汰」也是吗?
一般来说,Slave节点不会进行定时淘汰,它只会等待从Master节点同步过来的删除命令,这样就保持了主从之间的一致性。然而,有些时候,用户会把Slave节点设置成可写,那么Slave上写的带有过期时间的Key,因为Master是不知道的,就一直不会淘汰掉。所以在版本4.0以后,Redis增加了单独的逻辑,在定时淘汰中删除这些在slave节点上写入的过期Key。
对于Master节点,根据宏ACTIVE_EXPIRE_CYCLE_SLOW,能选择两种淘汰模式,分别是“FAST淘汰”和“SLOW淘汰”,前者每次淘汰只能花1毫秒,不能花更多了,后者是默认的选项,这样能在每次调度中淘汰更多的Key,但会花更多的CPU时间在淘汰上,降低了处理的访问吞吐量。 下面我们针对“SLOW淘汰”展开描述。
SLOW淘汰模式,以hz=10为例,每次调度的总时间是100ms,这里调度不会25%的cpu时间,即25ms。
每淘汰多少个key,就检测一次是否超25ms呢?
如果每淘汰1个Key就检测一次,无疑代价太大。从源码上看,定时淘汰会尝试遍历每个db,遍历完了或者时间到了就退出循环。第一层循环是遍历各个db,第二层循环是遍历db里面的一批批key,一批key是20个,如果第三层循环结束后有大于5个key是成功淘汰的(说明这个db很多淘汰key),那么二层就继续循环,如果小于等于5个key,说明这个db没有很多key需要淘汰,则退出二层循环,第三层循环是一批key里面逐个key进行淘汰。即最多320个key进行判断后,就会看看是否已经超过cpu占用时间。
在4.0或以上的版本,会根据参数lazyfree-lazy-expire(默认no)来做DEL还是UNLINK。2.8版本不支持lazyfree-lazy-expire,就只能选择DEL命令。
这里的定时淘汰,也会以命令的形式,传播到Slave节点与记录到AOF文件中。
五. 总结
- 驱逐策略的选择,往往要与业务特点与使用场景紧密相关,不当的选择,可能会让用户丢失不想丢失的数据,或者有较为差的驱逐效率。
- 已过期的Key往往不会立刻被删除,而需要由「访问淘汰」与「定时淘汰」来触发,正因为这个调性,用户在导出快照与建立主从时,会疑惑主从之间的Key数量不一致;
- 驱逐与淘汰都有可能影响服务,在新版本下,最好都开启unlink代替del。
附录
逻辑图