主题
Redis 单线程与多客户端交互
多进程 Worker 客户端如何与 Redis 单线程服务端交互 日期:2026-08-26
一、核心结论
| 层面 | 关系 | 说明 |
|---|---|---|
| 客户端 | 多进程,每个进程独立连接 | 3 个 Worker 进程 = 3 个 TCP 连接 |
| 服务端 | 单线程,逐条执行命令 | 所有客户端的命令在主线程中串行执行 |
| 阻塞命令 | 不阻塞主线程 | BRPOP 挂起客户端,服务端继续处理其他命令 |
| 性能 | 不会成为瓶颈 | Redis 命令 100 纳秒,Worker 任务 4 分钟,差距 7 个数量级 |
二、多进程客户端 → 单线程服务端
2.1 架构总览
客户端:3 个独立的 Worker 进程
进程 A → TCP Conn-1
进程 B → TCP Conn-2
进程 C → TCP Conn-3
↓ 三条连接接入同一个 Redis 服务端
服务端:主线程逐条执行命令
各连接的命令由 Redis 主线程串行处理;命令执行阶段,同一时刻只执行一条。
每个客户端保持独立连接,服务端通过 I/O 多路复用监听这些连接。这里的“单线程”指命令执行主线程,不代表 Redis 进程内完全没有其他线程。
不管 3 个进程的命令何时到达,Redis 主线程逐条执行:
sh
时间 0ms: 进程 A 发 BRPOP task:queue 5
时间 0.1ms: 进程 B 发 BRPOP task:queue 5
时间 0.2ms: 进程 C 发 BRPOP task:queue 5
Redis 主线程执行顺序:
0.0ms: 执行进程A的 BRPOP → 队列空 → A 加入等待链表
0.1ms: 执行进程B的 BRPOP → 队列空 → B 加入等待链表
0.2ms: 执行进程C的 BRPOP → 队列空 → C 加入等待链表
0.3ms: 执行进程D的 LPUSH → 队列非空 → 唤醒A → A 拿到任务不可能出现两个命令同时执行。即使 3 个进程的命令在"同一毫秒"到达,Redis 的 I/O 多路复用也只是把它们排成队列,主线程逐条处理。
2.2 等待链表中的每个节点 = 一个 TCP 连接
等待链表存的不是"Worker 进程"的抽象概念,而是具体的 redisClient 结构体指针:
c
// Redis 源码 server.h
typedef struct client {
int fd; // ← TCP 连接的文件描述符
connection *conn; // ← 连接对象
// ... 其他字段
} client;
// 阻塞 key 的等待客户端链表
dict *db->blocking_keys;
// key → list of client*每个 client 对应一个 fd(文件描述符),就是一个 TCP 连接。唤醒时 Redis 通过这个 fd 把数据写回给客户端。
2.3 队列数据如何触发客户端唤醒
Redis 不会定时扫描所有 List 来判断是否有新任务。执行 LPUSH、RPUSH 等写命令时,Redis 已经知道被修改的 key,因此会在该命令的处理流程中主动检查这个 key 是否存在阻塞客户端。
以 task:queue 为例:
text
Client A:BRPOP task:queue 0
↓
队列为空,A 加入 task:queue 的等待链表
↓
Client D:LPUSH task:queue task-001
↓
Redis 执行 LPUSH,发现 task:queue 存在等待客户端
↓
从等待链表中选择 A,完成 BRPOP 并把 task-001 返回给 A这个过程可以抽象为:
text
执行 LPUSH(task:queue, task-001)
↓
将 task-001 写入 task:queue
↓
检查 task:queue 对应的阻塞客户端链表
↓
取出一个等待客户端
↓
完成该客户端的 BRPOP
↓
解除客户端阻塞状态并准备返回结果因此,epoll、kqueue 等 I/O 多路复用机制负责发现生产者 TCP 连接上有新的 LPUSH 命令;List 命令的执行逻辑负责发现该命令修改的 key 上存在等待客户端。两者处于不同层面:
| 阶段 | Redis 如何感知 | 处理内容 |
|---|---|---|
| 发现生产者命令 | I/O 多路复用发现 TCP 连接可读 | 读取并解析 LPUSH 或 RPUSH |
| 发现可唤醒客户端 | 执行写命令时检查被修改的 key | 查找该 key 对应的阻塞等待链表 |
| 返回 BRPOP 结果 | 将客户端从阻塞状态转为可返回状态 | 通过客户端连接把结果写回 |
等待客户端不会主动监听等待链表,也不会占用一个专门的 Redis 线程。它只是保持 TCP 连接,并由 Redis 保存其阻塞状态、等待的 key 和输出结果。Redis 找到它之后,会解除阻塞状态,将 BRPOP 结果放入输出缓冲区,并通过该 TCP 连接返回给客户端。
如果 Redis 正在执行另一条命令,唤醒动作必须等当前命令结束,因为命令执行仍然是串行的;但它不需要等待所有普通客户端处理完毕,只要执行到了修改目标 key 的写命令,就会检查对应的等待链表。
2.4 用 CLIENT LIST 验证
bash
# 3 个 Worker 正在 BRPOP 阻塞时,执行:
redis-cli CLIENT LIST输出:
id=1 addr=127.0.0.1:54321 fd=10 cmd=brpop age=5 idle=5
id=2 addr=127.0.0.1:54322 fd=11 cmd=brpop age=5 idle=5
id=3 addr=127.0.0.1:54323 fd=12 cmd=brpop age=5 idle=5| 字段 | 含义 |
|---|---|
id | Redis 内部客户端唯一 ID |
addr | 客户端 IP:端口 |
fd | TCP 文件描述符(每个连接不同) |
cmd=brpop | 最后执行的命令是 BRPOP |
age | 连接建立秒数 |
idle | 空闲秒数(BRPOP 阻塞期间持续增长) |
3 个 Worker = 3 行 = 3 个 TCP 连接 = 3 个等待链表节点。
三、为什么单线程不会成为瓶颈
3.1 命令执行速度对比
sh
进程 A 的 BRPOP ──► Redis 主线程执行 ──► 100 纳秒完成
进程 B 的 BRPOP ──► Redis 主线程执行 ──► 100 纳秒完成
进程 C 的 BRPOP ──► Redis 主线程执行 ──► 100 纳秒完成
进程 D 的 LPUSH ──► Redis 主线程执行 ──► 100 纳秒完成
4 条命令总共耗时: 400 纳秒(0.0004 毫秒)单任务处理耗时 4 分钟(如,Seedance生成视频,护着接口查询也经常是毫秒级的),Redis 命令耗时 100 纳秒。差距 7 个数量级。
3.2 各操作时间复杂度
| 操作 | 时间复杂度 | 实际耗时 | 说明 |
|---|---|---|---|
| BRPOP 弹出 | O(1) | ~100ns | 列表右端弹出,操作尾部指针 |
| LPUSH 推入 | O(1) | ~100ns | 列表左端推入,操作头部指针 |
| LLEN 长度 | O(1) | ~50ns | 直接读取长度字段 |
| LRANGE 读取 | O(N) | ~1μs | N 通常 ≤ 10 |
| 唤醒等待客户端 | O(1) | ~200ns | 等待链表头部操作 |
3.3 单实例 Redis 队列吞吐量
| 场景 | QPS | 瓶颈 |
|---|---|---|
| 单 Worker BRPOP + LPUSH | 10 万+ | Redis 不是瓶颈,Worker 处理速度是瓶颈 |
| 10 Worker BRPOP + LPUSH | 10 万+ | Redis 仍不是瓶颈,网络带宽可能是 |
| 100 Worker BRPOP + LPUSH | 10 万+ | Redis 单线程开始成为瓶颈,需考虑集群 |
| 仅 LPUSH(API 推入) | 10 万+ | 推入比弹出更快,无阻塞开销 |
结论:瓶颈永远在 Worker 处理速度(Seedance API 调用 4 分钟/任务),而非 Redis 队列操作(100 纳秒/操作)。
四、阻塞命令不影响其他命令
4.1 BRPOP 的"阻塞"是客户端层面的
进程 A 的 BRPOP 阻塞 5 秒等待期间,Redis 服务端不会卡住等它:
js
0ms: 主线程执行 A 的 BRPOP → 队列空 → A 挂起(加入等待链表)→ 立即返回
0.1ms: 主线程执行 B 的 BRPOP → 队列空 → B 挂起 → 立即返回
0.2ms: 主线程执行 C 的 BRPOP → 队列空 → C 挂起 → 立即返回
0.3ms: 主线程空闲,可以处理其他客户端命令
↑ 3 个 Worker 都在阻塞,但主线程没被阻塞
3000ms: 进程 D 发 LPUSH → 主线程执行 → 队列非空 → 唤醒 A
3000ms: 主线程继续处理后续命令关键:BRPOP 的"阻塞"是客户端层面的。Redis 服务端只是把客户端加入等待链表后立即去处理下一条命令,不会原地等待。
4.2 阻塞 vs 非阻塞命令的执行对比
| 命令 | 客户端行为 | 服务端行为 |
|---|---|---|
BRPOP(队列空) | 客户端挂起等待 | 立即把客户端加入等待链表,去处理下一条命令 |
BRPOP(队列非空) | 立即收到返回值 | 立即弹出元素返回,去处理下一条命令 |
LPUSH | 立即收到返回值 | 立即推入并检查等待链表,去处理下一条命令 |
GET | 立即收到返回值 | 立即读取返回,去处理下一条命令 |
没有任何命令会让 Redis 主线程阻塞等待。即使是 BRPOP,服务端也只是"标记一下,你等着",然后立刻去服务下一个客户端。
4.3 LPUSH 唤醒等待客户端的流程
sh
时间 3000ms: 进程 D 发 LPUSH task:queue 6
Redis 主线程执行 LPUSH(仍然单线程,仍然串行):
1. 执行 LPUSH → 列表变为 [6]
2. 检查 task:queue 的等待链表 → 有 client(fd=10) 在等
3. 直接把 "6" 发给 client(fd=10)(不从列表弹出)
4. 从列表删除 "6",列表变为 []
5. 从等待链表移除 client(fd=10)
6. LPUSH 命令完成,主线程去处理下一条命令
整个过程耗时: ~200 纳秒数据不经过列表中转,直接从 LPUSH 方传递给 BRPOP 方,O(1) 时间完成。
五、Redis 6.0 多线程 I/O
5.1 多线程只加速网络,命令执行仍单线程
Redis 6.0 引入了多线程 I/O,但只用于读写 socket 数据,命令执行仍然在主线程:
sh
┌──────────────────────────┐
│ Redis 6.0 │
│ │
多线程 I/O │ ┌────┐ ┌────┐ ┌────┐ │
(读取/写入 │ │IO-1│ │IO-2│ │IO-3│ │
socket 数据) │ └─┬──┘ └─┬──┘ └─┬──┘ │
│ │ │ │ │
│ └──────┼──────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ 主线程 │ │
│ │ (命令执行) │ │
│ └───────────┘ │
│ │
└──────────────────────────┘
1. IO 线程从 socket 读取客户端数据
2. 主线程执行命令(单线程,串行)
3. IO 线程把结果写回 socket5.2 为什么不影响 BRPOP 的原子性
py
进程 A ──► IO线程-1 读取 "BRPOP task:queue 5" ──► 主线程执行 ──► A 加入等待链表
进程 B ──► IO线程-2 读取 "BRPOP task:queue 5" ──► 主线程执行 ──► B 加入等待链表
进程 C ──► IO线程-3 读取 "BRPOP task:queue 5" ──► 主线程执行 ──► C 加入等待链表
3 个 IO 线程可以同时读取 socket 数据
但主线程逐条执行 BRPOP,仍然保证原子性| 层面 | 线程数 | 职责 |
|---|---|---|
| I/O 线程 | 1-N(可配置) | 读取 socket 数据、写回 socket 数据 |
| 主线程 | 1 | 解析命令、执行命令、操作数据结构 |
命令执行仍然是单线程的,所以 BRPOP 的原子性保证不受影响。
六、Worker 进程与 Redis 连接池的对应关系
6.1 核心结论
| 层面 | 关系 | 说明 |
|---|---|---|
| 进程级 | 1 个 Worker 进程 = 1 个 redis_client = 1 个 ConnectionPool | 每个 python -m app.worker 是独立进程,各自 import 模块时创建单例 |
| 连接级 | 1 个 ConnectionPool 内部复用多个 TCP 连接 | brpop 阻塞期间独占 1 个连接,其他命令用其他连接 |
| Redis 端 | 1 个 TCP 连接 = 1 个 client = 等待链表中 1 个节点 | Redis 不关心客户端是进程还是线程,只看 TCP 连接 |
6.2 多进程部署模型
本项目通过启动多个 python -m app.worker 进程实现多 Worker:
bash
# 启动 3 个独立 Worker 进程
.venv/bin/python -m app.worker & # 进程 A
.venv/bin/python -m app.worker & # 进程 B
.venv/bin/python -m app.worker & # 进程 C每个进程独立 import redis_client 模块,各自创建 redis_client 单例:
python
# app/redis_client.py — 模块级单例
import redis
from app.config import settings
redis_client = redis.Redis(
host=settings.redis_host,
port=settings.redis_port,
db=0,
decode_responses=True,
max_connections=10,
socket_timeout=30,
socket_connect_timeout=5,
health_check_interval=30,
)python
# app/worker.py — Worker 入口
from app.redis_client import redis_client # ← 每个进程各自 import
def process_queue():
while True:
result = redis_client.brpop("task:queue", timeout=5)
if result:
run_task(int(result[1]))3 个进程的内存结构:
sh
进程 A (PID 1001) 进程 B (PID 1002) 进程 C (PID 1003)
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ redis_client │ │ redis_client │ │ redis_client │
│ └─ ConnectionPool │ │ └─ ConnectionPool │ │ └─ ConnectionPool │
│ max=10 │ │ max=10 │ │ max=10 │
│ ┌─────┬─────┬───┐ │ │ ┌─────┬─────┬───┐ │ │ ┌─────┬─────┬───┐ │
│ │C-1 │C-2 │...│ │ │ │C-1 │C-2 │...│ │ │ │C-1 │C-2 │...│ │
│ │BRPOP│空闲 │ │ │ │ │BRPOP│空闲 │ │ │ │ │BRPOP│空闲 │ │ │
│ └─────┴─────┴───┘ │ │ └─────┴─────┴───┘ │ │ └─────┴─────┴───┘ │
└────────┬────────────┘ └────────┬────────────┘ └────────┬────────────┘
│ TCP │ TCP │ TCP
▼ ▼ ▼
127.0.0.1:54321 127.0.0.1:54322 127.0.0.1:54323
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ Redis 服务端 │
│ client(fd=10, cmd=brpop) ← 等待链表节点 1 │
│ client(fd=11, cmd=brpop) ← 等待链表节点 2 │
│ client(fd=12, cmd=brpop) ← 等待链表节点 3 │
│ │
│ task:queue → waiting_clients: [client(fd=10), client(fd=11), client(fd=12)] │
└─────────────────────────────────────────────────────────────────────────────────────────────┘关键:3 个进程各自独立的连接池,互不共享。Redis 端看到 3 个 TCP 连接,等待链表有 3 个节点。
6.3 单进程内的连接复用
同一个 Worker 进程内,redis_client 是单例,所有 Redis 调用共用一个连接池:
python
# 同一个 Worker 进程内的不同阶段
def run_task(task_id: int):
# 阶段1:写进度(借 Conn-2)
redis_client.hset(f"task:{task_id}:progress", "stage", "SCRIPT")
# 阶段2:写进度(复用 Conn-2,Conn-1 仍被 BRPOP 独占)
redis_client.hset(f"task:{task_id}:progress", "stage", "IMAGE")
# 阶段3:完成后推入审核队列(复用 Conn-2)
redis_client.lpush("review:queue", task_id)Worker 进程内部:
redis_client (单例)
└── ConnectionPool (max=10)
├── Conn-1 ←── brpop() 阻塞中(从 process_queue 开始就独占)
├── Conn-2 ←── hset / lpush 轮流借用(用完归还)
└── Conn-3~10 ←── 空闲brpop 独占 Conn-1 直到超时或拿到任务。期间 hset、lpush 等命令从池中借 Conn-2,用完归还。不互相阻塞。
6.4 连接借还生命周期
redis-py 的 ConnectionPool 采用"借出-归还"模式:
python
# redis-py 内部简化逻辑
class ConnectionPool:
def __init__(self, max_connections):
self._available = [] # 空闲连接栈
self._in_use = set() # 正在使用的连接
self._max = max_connections
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) # 归还到空闲栈brpop 借出不归还(阻塞期间):
python
# redis_client.brpop("task:queue", timeout=5)
# 内部流程:
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
# redis_client.hset("task:6:progress", "stage", "IMAGE")
# 内部流程:
conn = pool.get_connection() # 借出 Conn-2
conn.send_command("HSET", "task:6:progress", "stage", "IMAGE")
result = conn.read_response() # 立即返回
pool.release(conn) # 立即归还 Conn-26.5 连接数计算
实际同时占用的连接数:
| 阶段 | 占用连接 | 说明 |
|---|---|---|
process_queue 中 BRPOP 阻塞 | 1 个 | 只有 BRPOP 在执行 |
run_task 中写进度 | 1 个 | BRPOP 已归还,HSET 临时借 |
run_task 中推审核队列 | 1 个 | 同上 |
| 理论峰值 | 1-2 个 | BRPOP 和其他命令不会同时执行 |
多 Worker 进程的总连接数:
| Worker 进程数 | 每进程连接峰值 | 总 TCP 连接数 | Redis max_clients |
|---|---|---|---|
| 1 | 2 | 2 | 默认 10000 |
| 3 | 2 | 6 | 足够 |
| 10 | 2 | 20 | 足够 |
| 50 | 2 | 100 | 足够 |
| 200 | 2 | 400 | 足够 |
6.6 fork 场景的陷阱
如果用 multiprocessing.Process 的 fork 模式(Unix 默认)启动 Worker:
python
import multiprocessing
from app.redis_client import redis_client # ← 父进程已创建
def worker():
# ← fork 后子进程继承了父进程的 redis_client
# ← 连接池中的 TCP fd 被复制
# ← 两个进程操作同一个 socket → 数据错乱!
redis_client.brpop("task:queue", timeout=5)
p = multiprocessing.Process(target=worker)
p.start() # fork!问题:fork 复制父进程的内存,包括 redis_client 和已建立的 TCP 连接的 fd。两个进程向同一个 socket 写数据会导致 Redis 协议错乱。
正确做法:子进程中重建 redis_client:
python
import multiprocessing
import redis
def worker():
# 子进程中重新创建,不复用父进程的连接池
client = redis.Redis(
host="localhost",
port=6380,
decode_responses=True,
max_connections=10,
)
client.brpop("task:queue", timeout=5)
p = multiprocessing.Process(target=worker)
p.start()本项目不受影响:python -m app.worker 每次都是全新进程(spawn 模式),不走 fork,不存在继承问题。
6.7 对比三种部署模式
python
# 模式 1:多进程(本项目使用)
# 每个 python -m app.worker 是独立进程
import redis
redis_client = redis.Redis(
host="localhost", port=6380,
decode_responses=True, max_connections=10,
)
# 进程 A: redis_client_A, Pool_A, TCP_A
# 进程 B: redis_client_B, Pool_B, TCP_B
# 进程 C: redis_client_C, Pool_C, TCP_C
# → 3 个独立连接池,3 个 TCP 连接
# 模式 2:单进程多线程
import threading
def worker():
# 所有线程共享同一个 redis_client 和连接池
redis_client.brpop("task:queue", timeout=5)
# 线程 1: 借 Conn-1
# 线程 2: 借 Conn-2
# 线程 3: 借 Conn-3
# → 1 个连接池,3 个 TCP 连接
t1 = threading.Thread(target=worker)
t2 = threading.Thread(target=worker)
t3 = threading.Thread(target=worker)
# 模式 3:多进程 spawn(推荐)
import multiprocessing
def worker():
import redis
# 子进程内部创建新连接池
client = redis.Redis(host="localhost", port=6380)
client.brpop("task:queue", timeout=5)
if __name__ == "__main__":
for _ in range(3):
p = multiprocessing.Process(target=worker)
p.start()| 模式 | redis_client 数量 | 连接池数量 | TCP 连接数 | 隔离性 | fork 风险 |
|---|---|---|---|---|---|
多进程(python -m) | 3 | 3 | 3-6 | 完全隔离 | 无 |
| 单进程多线程 | 1 | 1 | 3-10 | 共享池 | 无 |
多进程 fork | 继承父进程 | 继承 | fd 冲突 | 不隔离 | 有 |
多进程 spawn | 3 | 3 | 3-6 | 完全隔离 | 无 |
6.8 本项目的完整连接拓扑
3 个 Worker 进程(python -m app.worker)
进程 A ──import──► redis_client ──► ConnectionPool(max=10)
│ ├── Conn-1 (BRPOP 独占)
│ └── Conn-2~10 (空闲)
│
└──TCP──► Redis fd=10 ──► waiting_clients[0]
进程 B ──import──► redis_client ──► ConnectionPool(max=10)
│ ├── Conn-1 (BRPOP 独占)
│ └── Conn-2~10 (空闲)
│
└──TCP──► Redis fd=11 ──► waiting_clients[1]
进程 C ──import──► redis_client ──► ConnectionPool(max=10)
│ ├── Conn-1 (BRPOP 独占)
│ └── Conn-2~10 (空闲)
│
└──TCP──► Redis fd=12 ──► waiting_clients[2]
Redis 等待链表:
task:queue → [client(fd=10), client(fd=11), client(fd=12)]
↑ 进程A的连接 ↑ 进程B的连接 ↑ 进程C的连接
LPUSH task:queue 6:
→ Redis 弹出 client(fd=10) → 通过 fd=10 发送 "6" → 进程A收到
→ fd=10 从等待链表移除
→ 进程A 的 brpop() 返回 → 开始处理任务
→ 进程B、C 继续等待一句话总结:每个 Worker 进程拥有独立的 redis_client 和连接池,进程内复用连接池中的多个连接,进程间互不共享。Redis 端每个 TCP 连接对应等待链表中的一个节点。
七、单线程如何保证 BRPOP 原子性
7.1 四层保证
| 层 | 保证 | 说明 |
|---|---|---|
| 1. 单线程模型 | 命令执行引擎只有一个线程 | 同一时刻只执行一条命令,不可能两条命令交叉 |
| 2. 命令原子性 | 每条 Redis 命令是原子操作 | BRPOP 的"检查→弹出→返回"不可被中断 |
| 3. 等待链表 FIFO | 先 BRPOP 的排在前面 | LPUSH 时唤醒链表头部第一个,不会同时唤醒两个 |
| 4. 事件循环串行 | LPUSH 触发唤醒也是串行的 | 即使批量 LPUSH 多个元素,也是一个一个唤醒 |
7.2 LPUSH 到唤醒的完整流程
LPUSH task:queue 6 到达 Redis:
┌─ 主线程执行 LPUSH ──────────────────────────────────────────┐
│ │
│ 1. 执行 LPUSH,列表变为 [6] │
│ 2. 调用 signalKeyAsReady("task:queue") │
│ 3. LPUSH 命令返回 │
│ │
└────────────────────────────────────────────────────────────┘
│
▼
┌─ 主线程事件循环处理 ready_keys ──────────────────────────────┐
│ │
│ 4. 检查 task:queue → 有元素 │
│ 5. 检查等待链表 → W1 是第一个 │
│ 6. 弹出 "6"(右端)→ 发给 W1 │
│ 7. 从等待链表移除 W1 │
│ 8. break ← 只唤醒一个,不唤醒 W2、W3 │
│ │
└────────────────────────────────────────────────────────────┘步骤 3→4 的衔接:LPUSH 执行完后,主线程继续事件循环,发现 task:queue 在 ready_keys 中,进入唤醒流程。
步骤 8 的 break:唤醒一个 Worker 后立即 break,不会继续唤醒 W2、W3。如果 LPUSH 了多个元素,主线程会在下一轮事件循环中再次处理。
7.3 批量 LPUSH 多个元素的唤醒流程
bash
LPUSH task:queue 6 7 8LPUSH 执行(原子):
列表变为 [8, 7, 6]
调用 signalKeyAsReady
主线程处理 ready_keys(仍在 LPUSH 命令内):
第1轮循环:
检查 task:queue → 有元素
弹出 "6"(右端)→ 发给 W1
从等待链表移除 W1
列表变为 [8, 7]
break ← 只唤醒一个
主线程继续事件循环,发现 task:queue 还在 ready_keys 里
第2轮循环:
检查 task:queue → 还有元素
弹出 "7" → 发给 W2
从等待链表移除 W2
列表变为 [8]
break
第3轮循环:
检查 task:queue → 还有元素
弹出 "8" → 发给 W3
从等待链表移除 W3
列表变为 []
break
第4轮循环:
检查 task:queue → 空,不弹出
结束处理结果:3 个元素分别发给 W1、W2、W3,W4~W10 继续等待。
八、相关文档索引
| 文档 | 路径 | 内容 |
|---|---|---|
| BRPOP 深度解析 | Redis-BRPOP深度解析.md | BRPOP 语法、阻塞机制、多 Worker 行为、注意事项 |
| Redis 原子性保证 | Redis原子性保证机制.md | 单线程模型、等待链表源码级分析、命令原子性 |
| Worker 编排器解析 | Worker编排器深度解析.md | Worker 启动、消费循环、异常处理、进度缓存 |
| 剧本到视频流程 | 剧本到视频生成流程说明.md | 完整生成流水线、Seedance 2.0 接入、FFmpeg 拼接 |