Redis过期策略

一、踏坑事件

  • 时间:2017年10月23日 凌晨

  • 背景:个人赛中用户可以发起pk,pk即为两个开播主播进行限时收礼比拼,收礼数多的主播获得3分钟加速1.5倍buf,加速期间不可再发起pk。

  • 事件:年度盛典活动个人赛 - 冲刺赛阶段(下文以‘个人赛’代替)上线,用户再次发起pk操作时,出现提示"加速buf期间,不能发起pk",但实际用户已经结束buf时间(3分钟)。

  • 业务实现:使用Redis进行用户加速buf的判断,并给Redis用expire设置自动失效时间(3分钟)保证用户buf自然失效。

  • 部署结构:Redis使用1主8从的部署方案。

  • 数据表现:用户提示异常后查询主库Redis对应key值,发现key值确实已不存在。在不查询主库的情况下直接查询分库,数据存在且ttl为0。

  • 初步结论:Redis主从同步中不进行自动失效的删除同步。

  • 测试验证

主库操作:

HMSET z:pk:spd:100045:20171023:26586578 aaa 'test'

EXPIRE z:pk:spd:100045:20171023:26586578 30

分库1操作:

watch -n 1 'redis-cli -h ip -p port -a pass hgetall z:pk:spd:100045:20171023:26586578'

分库2操作:

watch -n 1 'redis-cli -h ip -p port -a pass ttl z:pk:spd:100045:20171023:26586578'

30秒后发现:

分库1数据存在、分库2ttl得到值为0

对主库进行一次hgetall 操作后,分库1数据为nil 分库2数据为-2

结论:主库自动失效的key并不对从库进行同步通过ttl检查返回结果为0.

二、Redis key的三种过期策略

  1. 惰性删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key,很明显,这是被动的!
  2. 定期删除:由于惰性删除策略无法保证冷数据被及时删掉,所以redis会定期主动淘汰一批已过期的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

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

三、从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。

  1. 当前链接存在

  2. 当前链接不是master

  3. 当前链接的命令存在

  4. 当前链接的命令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过期后主库没有删除),源码片段如下:

注:那么,不想通过自己写程序解决问题的同学,快快升级redis到新的版本吧。

进一步加深理解推荐地址:
http://www.cppblog.com/richbirdandy/archive/2011/11/29/161184.html


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 一、Redis采用的过期策略 惰性删除+定期删除 惰性删除流程 在进行get或setnx等操作时,先检查key是否...
    晚歌歌阅读 695评论 0 0
  • 分布式缓存技术PK:选择Redis还是Memcached? 经平台同意授权转载 作者:田京昆(腾讯后台研发工程师)...
    meng_philip123阅读 68,915评论 7 60
  • 01 最近咪蒙支持实习生休学一文横空出世,众说纷纭,大家复讨论那个激烈的辩题:读大学真的无用吗?还有一直以来某些人...
    紫微分享小茂阅读 423评论 2 3
  • 有一段时间 我错过了星辰 错过了云皑 错过朝阳和溅漫的鱼白 西下无奈 夕阳卷走尘埃 氤氲盛开 野草独白 同样,我也...
    刘凯锋阅读 329评论 5 6
  • 大学文学系的教授林士奇退休后,便琢磨给儿子林明利买房子的问题。“孟母三迁,君子择室而居,”林士奇心中想到,“不能辱...
    文的我阅读 369评论 0 1