Skip to content

第九章:源码级深入 ​

本章导读 ​

本章围绕“源码级深入”展开,先交代适用场景,再说明核心原理,随后给出典型示例,最后补充小结与延伸阅读。

阅读顺序: 结论 → 原理 → 场景 → 代码 → 小结

Q24:signalKeyAsReady 和 handleClientsBlockedOnKeys 的调用关系是什么?为什么唤醒后要 break? ​

答:

调用关系:

LPUSH 命令执行
  └── lpushCommand()
      ├── listTypePush()           ← 插入元素到列表
      └── signalKeyAsReady()      ← 标记 key 为"有待处理的阻塞客户端"
          └── 把 key 加入 server.ready_keys 队列

LPUSH 命令执行完毕后,主线程事件循环:
  └── handleClientsBlockedOnKeys()
      ├── 从 ready_keys 取出 key
      ├── 查找该 key 的等待链表
      ├── 取链表第一个 client
      ├── 弹出列表元素
      ├── 发给 client
      ├── 从等待链表移除 client
      └── break ← 只唤醒一个

为什么 break:

c
while (clientNode) {
    client *receiver = clientNode->value;
    robj *value = listTypePop(o, LIST_TAIL);
    if (value) {
        addReply(receiver, value);
        listDelNode(clients, clientNode);
        break;  // ← 弹出一个元素,唤醒一个客户端,跳出循环
    }
    clientNode = listNextNode(clientNode);
}

break 保证一次只唤醒一个 Worker。如果不用 break,一个元素可能被发给多个 Worker,导致重复消费。

如果 LPUSH 6 7 8 推入了 3 个元素,主线程会在下一轮事件循环中再次处理 ready_keys,逐个唤醒 3 个 Worker。


Q25:brpop 在 redis-py 内部是如何借出和归还连接的?和 hset 有什么区别? ​

答:

redis-py 的 ConnectionPool 采用"借出-归还"模式:

python
# redis-py 内部简化逻辑
class ConnectionPool:
    def get_connection(self):
        if self._available:                    # 有空闲连接
            conn = self._available.pop()
            self._in_use.add(conn)
            return conn
        elif len(self._in_use) < self._max:    # 还没到上限
            conn = self._make_connection()     # 新建 TCP 连接
            self._in_use.add(conn)
            return conn
        else:
            raise ConnectionError("Too many connections")

    def release(self, conn):
        self._in_use.remove(conn)
        self._available.append(conn)           # 归还到空闲栈

1. brpop 和 hset 各自的借还流程 ​

brpop 借出不归还(阻塞期间):

python
conn = pool.get_connection()      # 借出 Conn-1
conn.send_command("BRPOP", "task:queue", 5)
result = conn.read_response()      # 阻塞!最多等 5 秒
                                   # ← 期间 Conn-1 一直在 _in_use 中
pool.release(conn)                 # 超时或拿到任务后才归还

hset 借出后立即归还:

python
conn = pool.get_connection()       # 借出 Conn-2
conn.send_command("HSET", "task:6:progress", "stage", "IMAGE")
result = conn.read_response()       # 立即返回(纳秒级)
pool.release(conn)                  # 立即归还 Conn-2
命令借出时长归还时机单次占用连接数
brpop(阻塞)最长 5 秒超时或拿到元素后1(独占)
hset~100 纳秒命令返回后立即1(瞬间)

2. ⚠️ 关键澄清:单线程下两者不会"同时"占用连接 ​

单线程顺序执行(本项目主循环):

python
def process_queue():
    while True:
        result = redis_client.brpop("task:queue", timeout=5)
        # ↑ brpop 阻塞期间,hset 根本执行不到
        # ↑ python 线程被 read_response() 挂起
        if result:
            run_task(int(result[1]))  # ← hset 在这里执行
            # ↑ 此时 brpop 已归还 Conn-1,hset 复用 Conn-1

单线程下连接池状态:池里实际只创建 1 个连接(Conn-1 反复借出/归还),max_connections=10 设多大都没意义。

3. 真正需要 2 个连接的场景:多线程 ​

python
# 线程 A:brpop 阻塞中(独占 Conn-1)
def consumer():
    while True:
        result = redis_client.brpop("task:queue", timeout=5)

# 线程 B:hset 并行执行(借 Conn-2)
def progress_writer():
    while True:
        redis_client.hset("task:6:progress", ...)
        # ↑ 借 Conn-2(因为 Conn-1 被线程 A 独占)

多线程下连接池状态:_in_use: {Conn-1, Conn-2},max_connections >= 2 才能跑。

4. 本项目实际的连接占用 ​

场景brpop 期间hset 何时执行连接占用
主循环(单线程)Conn-1 独占,线程挂起brpop 返回后顺序执行1 个(Conn-1 复用)
Seedance 3 线程分镜并行Conn-1 独占3 个分镜线程并行借 Conn-2/3/44 个

详见 Redis连接池详解.md 1.3 节 — 单线程单连接 vs 多线程多连接的完整对比,以及"连接是按需创建的,不是一次性预创建"的源码验证。