reids分为三个过期策略分别是:
- 惰性删除
- 定期删除
- 主动删除
1.惰性删除
当读写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key,这个是被动的
2.定期删除
redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果有过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载
2.1 Redis过期key删除策略之定期删除
因为redis本身的定位为轻量、快速的内存数据库,所以如果为所有key都加上定时器,过期即删除的定时策略显然会消耗大量的性能,这与redis作者的价值观有着巨大差异;由于redis中key的过期删除只会在主库上进行,对于目前redis使用的组合策略来说,单位时间过期的数据量越多,越可能会带来key的过期延迟,对于做了读写分离的业务,很容易导致从库读取到过期的脏数据。
redis源码activeExpireCycle函数的解读结果请看下文(如果你懒得看,可以直接跳过本节):
- 相关参数默认值:
hz 10 :每秒执行10次activeExpireCycle 函数
- activeExpireCycle函数解析:
- 每次循环随机拿出的key的数量
- 正常过期模式最大cpu耗时率
- 过期模式:
1) “正常过期”模式 :执行时间限制:25ms;计算公式为
2) “快速过期”模式 :执行时间限制为1ms,触发条件为上次的执行时间超过了timelimit,之后函数会使timelimit_exit=1 为真,并从上次发生超时的db的下一个db开始继续处理。
-
过期策略:redis会遍历所有db,每次从db中随机拿出20个带有过期时间属性的key做过期判断。
-
循环检测:对随机拿出的20个key进行检测,如果在本次检测中发现有超过25%的key被判定为过期则持续执行过期检测循环,直到这批key中需要过期的key的比例低于25%或某次循环超过timelimit执行时间限制。
上文已经提到,过期删除行为只会在主库中进行。这是因为key的过期删除依赖于expireIfNeeded函数,这个函数在任何访问数据的操作中都会被调用并用来检测客户端访问的数据是否过期。
如果当前数据库实例角色是master,则不进行key过期的删除操作。反之,它会先调用另一个函数propagateExpire发送del key命令到aof和当前redis实例的所有slave,最后将该key从数据库中删除。此时,从库中的该key才真正意义上的过期/消失/你访问不到了!
所以一旦一个redis集群的内存没有触及maxmemory,而它每时每刻都有大量的key需要过期导致定期删除忙不过来,并且这些过期了的key不会再被访问到,那么你就很可能会在从库莫名其妙的读到了本应过期的key了。
3.主动删除
当前内存超过maxmemory限定时,触发主动清理策略,该策略由启动参数的配置决定,可配置参数及说明:
volatile-lru:从已设置过期时间的数据集中根据LRU算法删除数据(redis3.0之前的默认策略)
volatile-ttl:从已设置过期时间的数据集中挑选过期时间最小的数据删除
volatile-random:从已设置过期时间的数据集中随机选择数据删除
allkeys-lru:从所有数据集中根据LRU算法删除数据
allkeys-random:从所有数据集中任意选择删除数据
noenviction:禁止从内存中删除数据(从redis3.0开始的默认策略)
maxmemory-samples:删除数据的抽样样本数,redis3.0之前默认样本数为3,redis3.0之后默认样本数为5,该参数过小会导致主动删除策略不准确,过大会消耗多余的cpu
redis主从出现数据脏读
说明:redis作为读写分离部署,当数据过期了,在从库还能都到key值的情况
原因:
从redis原码级别分析问题
查看redis对于ttl这个命令的源代码,代码如下:
代码中确实出现了TTL = 0 的情况,理论上对于存在过期时间的key,应该返回-2才对,而这个代码中,第一个if语句(应该返回-2)并没有执行,才导致调入了第二个循环里,而理 论上当前的key的过期时间一定小于当前时间戳(且不为-1),所以TTL应该是小于0,而在代码里,作者将TTL<0的情况处理成TTL=0,那 问题就在为什么第一个个if没有生效上了,既该条件的主要判断函数lookupKeyRead并没有返回NULL,再查看该函数的代码:
从这开始终于看出点端倪了,该函数之所以没有返回NULL,也是由于第一个if语句并没有return NULL,从代码的评论中可以看出,当redis作为slave的时候,是可能不返回NULL的。
从 expireIfNeeded函数的注释中可以看到,当当前的Redis为Slave时,为了保证主从数据的一致性,是并不会将当前key删除的,触发这 一句:if (server.masterhost != NULL) return now > when;当前的时间now一定是大于key存储的过期时间的,故该函数还是返回了1,这样又回到lookupKeyRead,函数中。下面的这段函数起 到决定性作用:
以下几个条件满足的时候,该函数才会Return NULL。
-
当前链接存在
-
当前链接不是master
-
当前链接的命令存在
-
当前链接的命令flags于REDIS_CMD_READONLY的与为True
前三个比较在测试过程中,一定是为True的,问题在第四个条件上,这里又引出了Redis Command的flags,在客户端,通过client list,可以查看到当前链接的flags:
可以看到,执行ttl命令的flags为N,而在下面的代码中可以看出flags=N时,表示flags=0,所以在上面的代码中,flags & REDIS_CMD_READONLY = 0 &2(REDIS_CMD_READONLY = 2,redis.h中定义),故这个if语句也没有进入,所以并没有返回NULL,因此导致ttlGenericCommand命令返回了TTL=0的结 果。(至于redis使用这些flags的原理以及上面的if语句的原理,还需要更加深入的分析,这里就不再阐述了)
所以,这种情况下,我们才知道,如果一个redis作为slave,且将slave-read-only设置为off,并写入了一个带有TTL的key时,当key过期后,该key是不会被Redis删除的,且TTL在过期后永远为0。
如何避免从库读取到脏数据
4.1. 通过scan命令扫库
当redis中的key被scan的时候,相当于访问了该key,同样也会做过期检测,充分发挥redis惰性删除的策略。这个方法能大大降低了脏数据读取的概率,但缺点也比较明显,会造成一定的数据库压力,谨慎合理使用,否则有可能影响线上业务的效率。
3.2. 升级redis到新的版本
在redis 3.2-rc1版本中,redis加入了一个新特性来解决主从不一致导致读取到过期数据的问题(好吧,虽然这个新特性我们一直觉得是个bug fix),在源码db.c文件中,作者对lookupKeyRead做了相应的修改,增加了key是否过期以及对主从库的判断(代码如下),如果key已过期,当前访问的是master则返回null;当前访问的是从库,且执行的是只读命令也返回null(老版本从库真实的返回该操作的结果,如果该key过期后主库没有删除),源码片段如下: