主题
第九章:源码级深入
本章导读
本章围绕“源码级深入”展开,先交代适用场景,再说明核心原理,随后给出典型示例,最后补充小结与延伸阅读。
阅读顺序: 结论 → 原理 → 场景 → 代码 → 小结
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/4 | 4 个 |
详见 Redis连接池详解.md 1.3 节 — 单线程单连接 vs 多线程多连接的完整对比,以及"连接是按需创建的,不是一次性预创建"的源码验证。