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

Redis过期键怎么用多线程搞定,简单又不复杂的思路分享

关于Redis过期键的处理,想用多线程来搞,其实有一个非常直接又不会把事情弄复杂的思路,这个思路的核心就是“别动Redis内核,在客户端想办法”,因为去改动Redis服务器本身的代码,那是个超级复杂的事情,涉及到C语言、内存管理、网络IO等等,绝对不是我们通常说的“简单又不复杂”,聪明的方法是把问题放在我们自己的应用层面来解决。

这个思路主要参考了Redis作者本人(Salvatore Sanfilippo)在一些讨论中表达的观点,以及Redis本身的工作机制,Redis内部其实是用了一种叫做“惰性删除”和“定期删除”相结合的方式来处理过期键,惰性删除就是说,当客户端来访问一个键的时候,Redis才会检查它是否过期,如果过期了就当场删除并返回空,定期删除则是Redis会每隔一段时间随机抽查一些键,把过期的清理掉。

但这两种方式都可能有问题,惰性删除是被动触发,如果某个过期键永远没人访问,那它就永远占着内存,成了“垃圾”,定期删除呢,为了不影响主线程处理正常请求,它不敢做得太猛,清理力度是有限制的,如果我们的应用一下子设置了海量的键并且同时过期,或者内存紧张急需回收,单靠Redis自己可能会有点慢,导致内存降不下来。

Redis过期键怎么用多线程搞定,简单又不复杂的思路分享

这时候,多线程的思路就可以派上用场了,我们可以在我们的应用程序里,也就是Redis的客户端这边,启动一个或多个后台线程,专门来帮忙清理过期键,具体可以这么做:

第一步:建立一份“死亡笔记”

Redis过期键怎么用多线程搞定,简单又不复杂的思路分享

我们不能让这些后台线程漫无目的地去扫描Redis里所有的键,那样效率太低,而且会给Redis带来巨大的压力,我们需要一份“清单”,记录下哪些键是我们设置的、并且会过期的。

  • 做法很简单:在我们自己的应用程序里,每当通过Redis客户端(比如Java的Jedis、Lettuce,或者Python的redis-py)设置一个带过期时间(TTL)的键时,除了向Redis发送SETEXEXPIRE命令外,同时把这个键的key和它预期的过期时间戳(当前时间戳 + TTL秒),记录到我们自己的一个地方,这个地方可以是一个共享的内存数据结构(比如一个优先队列,按过期时间排序),也可以是一个简单的数据库表,甚至是一个文件,关键是,这个记录动作要和我们设置Redis键的动作在同一个事务或逻辑单元里,保证一致性。

第二步:多线程后台扫描“死亡笔记”

Redis过期键怎么用多线程搞定,简单又不复杂的思路分享

我们有了这份记录了所有“将死”键的清单,我们就可以启动一个或多个后台线程。

  • 线程的工作流程就是个死循环
    1. 线程去检查我们刚刚创建的那个“死亡笔记”队列或列表。
    2. 它查看队列头部(也就是最早要过期的)那些键。
    3. 将当前时间与记录中的过期时间戳进行比较。
    4. 如果发现某个键已经过期了(当前时间 > 过期时间戳),那么这个线程就执行下一步操作。
    5. 它向Redis服务器发送一个DEL命令,尝试删除这个键。
    6. 无论删除成功与否,它都把这个键从我们的“死亡笔记”中移除。

第三步:处理一些细节,让方案更健壮

上面两步是核心,但要做得更好,需要考虑几点:

  • 线程安全:如果我们的应用是多个节点或者一个节点里多个线程都在写Redis和“死亡笔记”,那么对这个“死亡笔记”的读写操作必须是线程安全的,要加锁或者使用线程安全的并发队列。
  • 避免重复删除:有可能在我们的后台线程去删除之前,Redis自己的惰性删除机制已经把这个键干掉了,这没关系,我们的线程发DEL命令过去,如果键不存在,Redis会忽略,所以是安全的,为了减少不必要的网络请求,我们可以在发送DEL之前,先发一个TTL命令检查一下这个键是否还存在、是否还过期,如果键不存在或者TTL返回值不是-2(表示键不存在),我们就可以跳过删除操作,直接从“死亡笔记”里移除记录。
  • 控制线程数和扫描频率:不需要启动太多线程,一两个就够了,扫描的频率也可以控制一下,比如每100毫秒扫描一次,避免过于频繁空转,这样可以避免这个后台任务对应用本身和Redis造成不必要的负担。
  • 分布式协调:如果我们的应用是部署在多台机器上的,那么每台机器上的应用实例都会有自己的后台清理线程,这会导致多个线程尝试删除同一个键,造成重复操作,为了解决这个问题,我们可以把“死亡笔记”放在一个所有应用实例都能访问的中央存储里,比如一个独立的Redis实例或者ZooKeeper,然后通过分布式锁或者其他协调机制,让只有一个实例的后台线程来执行全局的清理任务,这会增加一些复杂性,但如果你确实需要全局唯一的清理器,这是一个可行的方向。

这个思路的好处在哪里?

  1. 简单:它完全绕开了Redis内核的复杂性,我们只是在使用Redis的普通命令(SETEX, DEL, TTL),就像写普通的业务代码一样。
  2. 有效:它主动、及时地清理过期键,尤其适合那种“批量键同时过期”的场景,能更快地释放内存。
  3. 可控:清理的速度、线程数量都在我们自己的控制之下,不会影响Redis主线程的性能,如果发现这个后台任务有影响,我们可以随时调整参数或者停掉它,整个Redis服务依然正常运行。
  4. 解耦:清理逻辑和业务逻辑是分离的,后台线程默默干活,不会阻塞主要的业务请求。

用多线程搞定Redis过期键的简单思路就是:在自己的应用里维护一个过期键的清单,然后用一两个后台线程定时检查这个清单,发现过期的键就主动发命令去Redis删除。 这个方法利用了多线程的异步处理能力,又避免了去碰Redis服务器那块难啃的骨头,确实是一个既实用又不太复杂的解决方案。