Skip to content

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
字段含义
idRedis 内部客户端唯一 ID
addr客户端 IP:端口
fdTCP 文件描述符(每个连接不同)
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μsN 通常 ≤ 10
唤醒等待客户端O(1)~200ns等待链表头部操作

3.3 单实例 Redis 队列吞吐量 ​

场景QPS瓶颈
单 Worker BRPOP + LPUSH10 万+Redis 不是瓶颈,Worker 处理速度是瓶颈
10 Worker BRPOP + LPUSH10 万+Redis 仍不是瓶颈,网络带宽可能是
100 Worker BRPOP + LPUSH10 万+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 线程把结果写回 socket

5.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-2

6.5 连接数计算 ​

实际同时占用的连接数:

阶段占用连接说明
process_queue 中 BRPOP 阻塞1 个只有 BRPOP 在执行
run_task 中写进度1 个BRPOP 已归还,HSET 临时借
run_task 中推审核队列1 个同上
理论峰值1-2 个BRPOP 和其他命令不会同时执行

多 Worker 进程的总连接数:

Worker 进程数每进程连接峰值总 TCP 连接数Redis max_clients
122默认 10000
326足够
10220足够
502100足够
2002400足够

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)333-6完全隔离无
单进程多线程113-10共享池无
多进程 fork继承父进程继承fd 冲突不隔离有
多进程 spawn333-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 8
LPUSH 执行(原子):
  列表变为 [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深度解析.mdBRPOP 语法、阻塞机制、多 Worker 行为、注意事项
Redis 原子性保证Redis原子性保证机制.md单线程模型、等待链表源码级分析、命令原子性
Worker 编排器解析Worker编排器深度解析.mdWorker 启动、消费循环、异常处理、进度缓存
剧本到视频流程剧本到视频生成流程说明.md完整生成流水线、Seedance 2.0 接入、FFmpeg 拼接