当前位置:首页 > 问答 > 正文

Redis循环写入老是失败,折腾了半天终于想出点招解决问题

这事儿说起来真是让人火大,前几天,我接了个活儿,要用Redis存一堆临时数据,就是那种不断产生、过一阵子就没用了的数据,我想,这简单啊,Redis不就是干这个的嘛,速度快,还支持设置过期时间,于是我就吭哧吭哧写了个循环,不停地往Redis里塞数据。

结果你猜怎么着?刚开始还挺顺,数据嗖嗖地往里进,可没过多久,大概也就几百条吧,程序就开始尥蹶子了,老是报错,错误信息有时候是“OOM command not allowed when used memory > 'maxmemory'”,有时候又是连接超时,反正就是写不进去了,我这暴脾气,一下就上来了,心想我这数据量也不大啊,Redis不是号称能处理百万千万级别的数据吗?怎么到我这儿几百条就趴窝了?

一开始我以为是Redis服务器挂了,赶紧连上去看,用info命令一查,好家伙,内存使用率都快顶到100%了,可我明明没存多少东西啊,这内存都去哪儿了?我试着用keys *命令想看看都有啥,结果这个命令直接卡了半天,差点把生产环境给搞崩了,吓得我赶紧掐掉了,后来才知道,keys *这个命令在生产环境是绝对禁用的,因为它会阻塞整个Redis服务,数据量一大就直接死给你看。(来源:Redis官方文档警告,以及无数踩过坑的程序员血泪史)

内存满了,第一反应当然是清理啊,我试着用flushdb把整个数据库清空了,重新跑程序,嘿,果然又能写进去了,但跑了没一会儿,又满了!这明显不对劲,像个无底洞,这说明不是我当前数据占满了内存,而是有“僵尸数据”赖着不走。

问题肯定出在我的代码逻辑上,我仔细检查那个循环写入的代码,我是用Python的redis库写的,大概逻辑是这样的:一个while True循环,里面生成一条数据,然后用set命令把它存到Redis里,同时设置一个过期时间,比如10分钟,看起来天衣无缝啊,数据自己会过期被清理,内存应该会保持稳定才对。

我又去翻Redis的文档,终于在一个不起眼的角落看到了关键信息:Redis的过期数据删除策略。(来源:Redis官方文档关于过期Expire的详细说明)原来,Redis并不是在 key 过期的那一瞬间就立刻把它删除的,那样太消耗CPU了,它用的是两种懒人策略结合:

Redis循环写入老是失败,折腾了半天终于想出点招解决问题

  1. 惰性删除:当客户端尝试访问一个 key 时,Redis 会检查它是否已过期,如果过期了就当场删除,然后返回空,这就像家里打扫卫生,只有你要用某个东西时,才发现它坏了,然后顺手扔掉。
  2. 定期删除:Redis 会每隔一段时间(默认100毫秒)随机抽取一部分设置了过期时间的 key,检查并删除其中已过期的,如果发现过期 key 的比例很高,它会继续重复这个抽样检查的过程,直到比例降下来,这就像定期做个大扫除,但不会把整个房子翻个底朝天,只是抽查几个房间。

问题就出在这里!我的程序是光写不读的,那些数据写进去之后,在过期前再也没有被访问过,惰性删除”机制完全派不上用场,而“定期删除”是随机的,如果我的数据量在短时间内暴增,过期key的数量远远大于定期删除每次能处理的数量,就会导致大量已经逻辑上“死亡”的数据仍然物理上占据着内存,直到下一次被抽中检查为止,我的内存就是这样被这些“幽灵数据”给撑爆的。

光找到原因还不行,得解决问题啊,我琢磨了几个方案:

第一个想到的是调优Redis配置,比如加大内存,或者调整定期删除的频率和力度,但这是运维的活儿,而且治标不治本,万一数据量再大点呢?

Redis循环写入老是失败,折腾了半天终于想出点招解决问题

第二个方案是换用其他数据结构,我存的是一个个独立的key-value,能不能用列表(List)或者流(Stream)结构,把它们攒成一堆再批量处理?但我的业务场景要求每个数据项都得有独立的过期时间,这个方案不太合适。

我想出了一个笨办法,但特别有效,既然Redis不及时清理,那我帮它清理呗,我不能再只写不读了,我得主动去“触发的删除”。

我修改了代码逻辑,在循环写入之前,我先用一个集合(Set)或者有序集合(Sorted Set),把所有的key都记录起来,并且把它们的写入时间戳作为分数存进去,在每次写入新数据之前,我先从这个有序集合里查找那些已经过了期的key(根据当前时间和分数比较),然后用del命令主动删除这些过期的key,同时把它们从有序集合里也移除掉,再写入新数据,并把新key记录到有序集合里。

这样一来,我相当于自己实现了一个简单的、主动的过期清理机制,虽然增加了一些操作,每次写入前都要先干点清理的活儿,但保证了内存不会无限制增长,我把这个改动部署上去之后,程序终于可以7x24小时稳定运行了,再也没爆过内存。

折腾了半天,总结就一句话:用Redis的时候,千万别以为设置了过期时间就万事大吉了,尤其是在那种高并发写入、却几乎不读取的场景下,一定要对它的内存淘汰机制心里有数,不然很容易就掉坑里了。