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

Redis里头那个阻塞调用到底咋实现的,研究下它的机制和用法

Redis的阻塞调用功能,主要用在那些需要等待特定条件满足才能返回结果的场景,比如一个客户端等着从列表里拿数据,但列表是空的,这时候它不想立刻得到一个空值,而是想等一等,等到有其他客户端往列表里放了新数据,自己立马拿到这个新数据再返回,这个“等一等”的过程,就是阻塞调用,实现这个功能的核心机制,主要依赖于Redis的链表数据结构就绪列表(ready list)发布订阅(Pub/Sub)的变体应用,但又不是完全一样的发布订阅。

核心机制:如何让客户端“睡着”和“唤醒”

Redis里头那个阻塞调用到底咋实现的,研究下它的机制和用法

想象一下,Redis服务器就像一个繁忙的招待所,有很多客户端(客人)来来往往,阻塞调用的实现可以分解为以下几个步骤:

Redis里头那个阻塞调用到底咋实现的,研究下它的机制和用法

  1. 客户端发起阻塞请求: 当一个客户端执行像 BLPOP(阻塞式列表左端弹出)这样的命令时,如果目标列表是空的,Redis不会立刻返回空值。
  2. 记录“等待中”的客户: Redis服务器会在内存里创建一个“等待名单”,这个名单不是杂乱无章的,它是按照客户端正在等待的键(key) 来组织的,Redis内部有一个数据结构,通常是一个字典(也叫哈希表),这个字典的键就是客户端等待的那个列表的键名("task_queue"),而对应的值,则是另一个链表,这个链表里存放了所有正在等待这个键的客户端信息(比如客户端的套接字描述符、超时时间等),根据Antirez(Redis创始人)在博客中的说明,这个结构使得Redis能够高效地管理大量的阻塞客户端。
  3. 客户端进入“睡眠”状态: 登记完信息后,Redis就会让这个客户端的连接暂时“睡着”,从操作系统的角度看,就是服务器不再主动给这个客户端发送数据,服务器会把这个客户端的套接字描述符放在一个单独的集合里进行监控,Redis会开始为这个客户端计算超时时间(如果设置了超时参数的话)。
  4. 触发唤醒的信号: 关键的步骤来了,当另一个客户端执行了向这个列表“添加”数据的命令,LPUSH 向 "task_queue" 里放入一个新任务时,Redis的底层代码在执行完正常的插入操作后,会立刻去检查那个“等待名单”。
  5. 查找并“唤醒”客户端: Redis会用刚刚被操作的键("task_queue")作为钥匙,去“等待名单”的字典里查找,果然,它找到了一个或多个正在等待这个键的客户端,通常情况下,Redis会采用先进先出(FIFO) 的策略,从等待链表中取出第一个客户端(也就是等待时间最长的那个)作为唤醒对象,根据《Redis设计与实现》一书中的描述,这种公平的策略避免了某些客户端被无限期地饿死。
  6. 交付数据并返回结果: 被选中的幸运客户端会被“唤醒”,Redis会直接将刚刚通过 LPUSH 命令插入的那个元素分配给它,并通过网络连接将结果发送过去,这个被唤醒的客户端收到数据后,阻塞调用就圆满结束了,这里有一个精妙之处:数据是直接由执行 LPUSH 的客户端“推送”给等待的客户端,而不是等待的客户端被唤醒后自己再去执行一次 POP,这减少了一次命令往返,效率更高。

关键细节与用法考量

了解了基本流程,还有一些重要的细节和在实际中使用需要注意的地方:

  • 超时机制: 阻塞命令通常允许设置一个超时时间(BLPOP key1 key2 10 表示最多等10秒),如果超过时间还没有数据到来,Redis会自动唤醒这个客户端并返回一个空值(nil),这在服务器内部是通过维护一个有序的超时链表来实现的,Redis会定期检查并处理超时的客户端。
  • 多个键的阻塞:BLPOP 可以同时监听多个键(BLPOP keyA keyB keyC 0),只要其中任意一个键有数据可用,客户端就会立即返回,其内部实现是客户端会按顺序被添加到它所监听的每一个键的等待链表中,当任何一个键被推送数据时,在唤醒客户端之前,Redis会先把它从所有它监听的其他键的等待链表中移除,防止被重复唤醒。
  • 连接安全性: 阻塞期间,这个TCP连接是被占用的,不能用于执行其他命令,要确保网络稳定,否则连接超时断开,阻塞请求也就失败了,Redis是单线程处理命令的,一个客户端在阻塞等待时,并不会消耗CPU资源,它只是被记录在一个待唤醒的列表里,所以即使有大量客户端阻塞,也不会严重影响Redis处理其他命令的性能,除非内存被这些阻塞连接的信息占满。
  • 与Pub/Sub的区别: 它和Redis的发布订阅(Pub/Sub)有点像,但有本质区别,Pub/Sub是“广播”,一条消息会发给所有订阅者,且消息不存储;而阻塞列表操作是“争抢”,一条消息只会递给一个等待者(精确的说是递交给一个等待客户端的一次阻塞弹出操作),并且数据是存储在列表这种数据结构中的,具有持久化能力。
  • 典型用法: 最常见的用法就是实现简单的消息队列或任务队列,生产者用 LPUSH 放入任务,多个消费者用 BRPOP 来争抢任务,这样就实现了负载分配,由于Redis命令执行的原子性,可以确保一个任务只会被一个消费者取走。

Redis的阻塞调用是一个高效且实用的功能,它通过维护内部的等待列表数据结构,巧妙地利用单线程事件循环机制,在数据尚未就绪时让客户端安静等待,在数据到来时精准快速地唤醒并交付数据,这种机制避免了客户端无用的轮询,减少了网络开销,是实现实时消息传递和任务分配的有力工具。