Skip to content

Redis 技术问答:队列、连接与并发专题总集 ​

整合自 Redis-BRPOP深度解析、Redis单线程与多客户端交互、Redis原子性保证机制、Redis连接池详解、Redis推拉模型与源码解析、Worker编排器深度解析 日期:2026-08-26

合并来源 ​

本文把 redis/ 目录下围绕队列、连接、原子性、推拉模型和 Worker 编排的专题文档,按“基础语法 → 服务端执行模型 → 连接管理 → 原子性 → 源码与编排 → 数据结构与应用”的顺序重新编排到一篇主文档中,便于统一阅读。

术语说明:文档里的 Worker 默认代表独立执行单元 ​

本文中提到的“多个 Worker”,默认隐含的意思是:多个彼此独立的执行单元。这个执行单元可以是多个进程,也可以是同一个进程里的多个线程。

在 Redis 看来,它并不直接关心 Worker 是线程还是进程。Redis 服务端真正看到的是客户端发起的 TCP 连接,也就是 Redis 内部的 client 结构体。因此:

文档说法应用侧含义Redis 服务端看到的对象
3 个 Worker 进程3 个独立 Python 进程,各自有自己的 redis_client 和连接池通常至少 3 条 TCP 连接,CLIENT LIST 至少 3 行
1 个进程里的 3 个 Worker 线程3 个线程共享同一个 redis_client,但并发命令会从连接池借出不同连接并发时可能看到 3 条 TCP 连接,等待链表也可能有 3 个节点
3 个 Worker 同时 BRPOP3 个独立执行流同时阻塞等待队列Redis 等待链表里是 3 个正在阻塞的 TCP 连接

所以后文中如果写“Worker A / Worker B / Worker C”,除非特别说明,重点不是它们到底是线程还是进程,而是它们代表三个独立发起 Redis 命令的执行流。


第一章:BRPOP 基础语法 ​

本章导读 ​

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

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

Q1:BRPOP 和 RPOP 的核心区别是什么?为什么本项目用 BRPOP? ​

答:

核心区别在于列表为空时的行为:

命令列表为空时CPU 占用延迟
RPOP立即返回 nil轮询模式持续消耗 CPU取决于 sleep 间隔
BRPOP阻塞等待,直到有元素或超时等待期间零 CPU 占用< 1 毫秒

本项目使用 BRPOP 的主要原因是:Worker 没有任务时不需要反复轮询 Redis,而是挂起等待;一旦有任务进入队列,Redis 会立即唤醒等待中的 Worker,让它继续执行后续任务处理逻辑。

需要注意的是,BRPOP 的“阻塞”要区分客户端侧和 Redis 服务端侧:

  • 对当前调用 BRPOP 的 Worker 线程来说,会阻塞在 brpop() 这一行,后面的代码暂时不会继续执行;
  • 对 Redis 服务端来说,不会阻塞 Redis 主线程,Redis 会把这个客户端连接挂到等待链表中,然后继续处理其他客户端命令;
  • 对其他 Worker 或其他 Redis 客户端来说,不受这个阻塞影响,仍然可以继续执行 HSET、LPUSH、GET 等命令。

也就是说,BRPOP 阻塞的是当前客户端线程的后续流程,不是阻塞整个 Redis 服务。

RPOP 轮询方式 ​

python
while True:
    result = redis_client.rpop("task:queue")
    if result:
        process(result)
    else:
        time.sleep(0.1)

这种写法的问题是:队列为空时,Worker 会不断主动查询 Redis。即使每次查询后都 sleep(0.1),本质上仍然是轮询模式,会产生无效请求,并且任务到达后最多可能等待 0.1 秒才被下一轮轮询发现。

BRPOP 阻塞等待方式 ​

python
def worker_loop():
    while True:
        result = redis_client.brpop("task:queue", timeout=5)

        print("只有 brpop 返回后,才会执行到这里")

        if result is None:
            continue

        queue_name, task_id = result
        process_task(task_id)

上面这段代码中,如果 task:queue 队列为空,程序会停在:

python
result = redis_client.brpop("task:queue", timeout=5)

只有出现以下两种情况之一,代码才会继续往下执行:

  1. 队列中有新任务进入,Redis 唤醒当前客户端并返回任务数据;
  2. 等待时间超过 timeout=5,Redis 返回 None。

所以,在同一个 Worker 线程里,brpop() 后面的 print()、process_task() 等代码,必须等 brpop() 返回后才会执行。

BRPOP 不会阻塞 Redis 服务端主线程 ​

假设有三个独立执行流:Worker A、Worker B、Worker C。它们可以是三个不同进程,也可以是同一进程里的三个不同线程;关键是它们不是同一个顺序执行流,否则 Worker A 阻塞在 brpop() 时,后面的 hset 和 lpush 不会继续执行。

python
# Worker A:独立线程或独立进程
result = redis_client.brpop("task:queue", timeout=5)

# Worker B:另一个独立线程或独立进程
redis_client.hset("task:6:progress", "stage", "IMAGE")

# Worker C:另一个独立线程或独立进程
redis_client.lpush("task:queue", 6)

当 Worker A 执行 BRPOP 且队列为空时:

对象是否被阻塞说明
Worker A 当前线程是卡在 brpop(),后续代码暂时不执行
Worker B否仍然可以正常执行 hset
Worker C否仍然可以正常执行 lpush
Redis 服务端主线程否继续处理其他客户端命令
task:queue 等待链表有变化Worker A 对应的客户端连接被挂入等待链表

当 Worker C 执行:

python
redis_client.lpush("task:queue", 6)

Redis 会在执行 LPUSH 后检查是否有客户端正在等待 task:queue。如果发现 Worker A 正在 BRPOP 等待,就会把任务 6 返回给 Worker A。

此时 Worker A 的代码才会继续执行:

python
queue_name, task_id = result
process_task(task_id)

本章小结 ​

BRPOP 的阻塞是客户端侧的阻塞。它会让当前调用 BRPOP 的线程暂停在这一行,直到有任务或超时返回;但它不会阻塞 Redis 服务端主线程,也不会影响其他客户端继续执行命令。

在任务队列场景中,这正是 BRPOP 的优势:

方式没有任务时CPU 消耗后续代码是否执行适用场景
RPOP 轮询不断主动查询高每次查询后继续循环简单但低效
BRPOP 阻塞等待客户端挂起等待低,接近 0当前线程暂停,返回后继续Worker 消费队列

所以,“没有任务时挂起,零 CPU;有任务时 < 1ms 唤醒”的准确含义是:

没有任务时,Worker 当前线程会挂起等待,不会反复空转轮询,因此 CPU 消耗接近 0;一旦有任务入队,Redis 会通过已建立的 TCP 连接把结果返回给等待中的客户端,Worker 被唤醒后继续执行后面的任务处理流程。


Q2:brpop(["task:queue:vip", "task:queue:normal"], timeout=5) 的优先级规则是什么?如果 VIP 队列持续有任务,普通队列会怎样? ​

答:

优先级规则:按 key 的声明顺序从左到右检查。只要左边的队列有元素,就不会弹右边的。

BRPOP [vip, normal] 5

VIP: [101, 102]     Normal: [201, 202]
第1次 BRPOP → 取 102(VIP)
第2次 BRPOP → 取 101(VIP)
第3次 BRPOP → 取 201(Normal)   ← VIP 空了才取 Normal
第4次 BRPOP → 取 202(Normal)

VIP 饥饿问题:如果 VIP 队列持续有任务推入,普通队列的任务可能永远不被消费。

解决方案:计数器策略,每处理 3 个 VIP 任务后强制消费 1 个普通任务:

python
normal_count = 0
while True:
    if normal_count >= 3:
        result = redis_client.brpop(["task:queue:normal"], timeout=1)
        normal_count = 0
    else:
        result = redis_client.brpop(
            ["task:queue:vip", "task:queue:normal"], timeout=5
        )
    if result:
        queue_name, task_id = result
        if queue_name == "task:queue:vip":
            normal_count += 1
        run_task(int(task_id))

Q3:redis_client.lpush("task:queue", 101) 的返回值是什么?为什么不是 101? ​

答:

返回值是 推入后的列表长度(int 类型),不是你推入的值。

python
result = redis_client.lpush("task:queue", 101)
# 列表内容: [101]     ← 101 被存入列表
# result = 1           ← 返回的是列表长度

result = redis_client.lpush("task:queue", 102)
# 列表内容: [102, 101]
# result = 2           ← 列表现在有 2 个元素

为什么这么设计:你已经知道推入了什么(就是你传的参数),Redis 选择返回更有用的信息——列表当前有多少个元素。

对比其他命令的返回值:

命令返回值含义示例
lpush推入后的列表长度1
brpop弹出的元素值 + 队列名("task:queue", "101")
rpop弹出的元素值"101"
llen列表长度1

Q4:为什么 Worker 取出 task_id 后需要 int(task_id) 转换? ​

答:

因为 Redis 内部统一以字符串存储所有值,不管你推入时传的是 int 还是 str。

python
# API 端存入时是 int
redis_client.lpush("task:queue", task.id)  # task.id = 6 (int)

# Redis 内部存储为字符串 "6"

# Worker 端取出时是字符串
_, task_id = redis_client.brpop("task:queue", timeout=5)
# task_id = "6" (str)

# SQLAlchemy 查询需要 int 主键
task = db.get(Task, int(task_id))  # 必须转回 int

如果不转换,直接用字符串 "6" 查数据库主键,SQLAlchemy 会报类型错误。

decode_responses 参数不影响这个行为——不管返回 b"6" 还是 "6",int() 都能正确转换:

python
int(b"6")   # → 6(bytes 自动解码)
int("6")    # → 6

第二章:阻塞机制与连接管理 ​

本章导读 ​

本章围绕“阻塞机制与连接管理”展开,先交代适用场景,再说明核心原理,随后给出典型示例,最后补充小结与延伸阅读。

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

Q5:BRPOP 阻塞期间,Redis 服务端的主线程会卡住吗?其他客户端的命令能执行吗? ​

答:

不会卡住。BRPOP 的"阻塞"是客户端层面的,服务端只是把客户端加入等待链表后立即去处理下一条命令。

0ms:   主线程执行 A 的 BRPOP → 队列空 → A 挂起(加入等待链表)→ 立即返回
0.1ms: 主线程执行 B 的 BRPOP → 队列空 → B 挂起 → 立即返回
0.2ms: 主线程执行 C 的 BRPOP → 队列空 → C 挂起 → 立即返回
0.3ms: 主线程空闲,可以处理其他客户端命令
       ↑ 3 个 Worker 都在阻塞,但主线程没被阻塞
3000ms: 进程 D 发 LPUSH → 主线程执行 → 队列非空 → 唤醒 A

对比:

命令客户端行为服务端行为
BRPOP(队列空)客户端挂起等待立即把客户端加入等待链表,去处理下一条命令
BRPOP(队列非空)立即收到返回值立即弹出元素返回,去处理下一条命令
LPUSH立即收到返回值立即推入并检查等待链表,去处理下一条命令

没有任何命令会让 Redis 主线程阻塞等待。


Q6:一个 Worker 进程中,BRPOP 阻塞期间还能执行 hset 写进度吗?如果能,用的是同一个 TCP 连接吗? ​

答:

分两种情况:

  • 单线程:❌ 不可能。brpop() 的 read_response() 会阻塞整个 Python 线程,hset 根本执行不到。
  • 多线程:✅ 可以。brpop 在线程 A 阻塞,hset 在线程 B 执行,用的是不同的 TCP 连接。

1. 单线程下不可能并行(常见误区) ​

python
# ❌ 单线程下不可能在 brpop 阻塞期间执行 hset
def worker_single_thread():
    while True:
        result = redis_client.brpop("task:queue", timeout=5)
        # ↑ 阻塞在 read_response(),独占 Conn-1
        # ↑ python 主线程被卡住,下面这行根本执行不到!
        redis_client.hset("task:6:progress", "stage", "IMAGE")

为什么执行不到:brpop() 内部调用 conn.read_response(),这是一个阻塞 I/O 调用——整个 Python 线程会被挂起,直到 socket 有数据可读或超时。期间无法执行任何其他代码。

2. 本项目实际是顺序执行(不是并行) ​

python
# ✅ 本项目实际代码:brpop 返回后才执行 hset
def process_queue():
    while True:
        result = redis_client.brpop("task:queue", timeout=5)
        # ↑ brpop 阻塞 → 拿到任务或超时后才返回
        # ↑ 返回时 Conn-1 已经归还到池中
        if result:
            run_task(int(result[1]))  # ← hset 在这里执行
            # ↑ 此时 brpop 已结束,Conn-1 已归还
            # ↑ hset 借出 Conn-1(复用),立即完成

def run_task(task_id):
    redis_client.hset(f"task:{task_id}:progress", "stage", "SCRIPT")
    # ↑ 借 Conn-1(brpop 已归还的)→ 执行 → 归还
时间线(本项目单线程顺序执行):

brpop 阻塞中(5 秒)          brpop 返回
  ├─ 借出 Conn-1                 ├─ 归还 Conn-1
  ├─ read_response() 阻塞         │
  │  _in_use: {Conn-1}            │  _available: [Conn-1]
  │  _available: []                │
  │  线程被挂起                    │
  │  无法执行 hset                 ├─ hset 借出 Conn-1(复用)
  │                                ├─ 立即执行完成
  │                                └─ 归还 Conn-1
  └─ Redis 回复或超时

关键:本项目 brpop 和 hset 是顺序执行,不是并行。每次只有一个命令在执行,一个连接就够了。

3. 真正的并行需要多线程 ​

python
import threading

# ✅ 多线程:brpop 阻塞期间,另一个线程可以执行 hset
def consumer_thread():
    """线程 A:消费队列"""
    while True:
        result = redis_client.brpop("task:queue", timeout=5)
        # ↑ 借出 Conn-1,阻塞在 read_response()
        # ↑ 但只阻塞线程 A,不影响线程 B

def progress_writer_thread():
    """线程 B:写进度"""
    while True:
        time.sleep(1)
        redis_client.hset("task:6:progress", "stage", "PROCESSING")
        # ↑ 借出 Conn-2(因为 Conn-1 被线程 A 独占)
        # ↑ 立即执行完成
        # ↑ 不受 brpop 阻塞影响

threading.Thread(target=consumer_thread).start()
threading.Thread(target=progress_writer_thread).start()
多线程时间线:

线程 A (consumer)               线程 B (progress_writer)
─────────────────              ─────────────────────────
brpop("task:queue", 5)
  借出 Conn-1
  send_command("BRPOP")
  read_response() 阻塞...
  _in_use: {Conn-1}
  _available: [Conn-2]
                                hset("task:6:progress", ...)
                                  get_connection()
                                  ├── _available.pop() → Conn-2  ← 借出 Conn-2
                                  ├── send HSET
                                  ├── read_response() → 立即返回
                                  └── release(Conn-2)
                                  ✓ 不需要等 brpop!

  Redis 回复...
  brpop() 返回
  release(Conn-1)

这才是 max_connections >= 2 真正发挥价值的场景:线程 B 从池中借出 Conn-2,完全不受 Conn-1 被 BRPOP 独占的影响。

4. 连接池状态对比 ​

场景brpop 期间连接池状态hset 能否执行用哪个连接
单线程(本项目)_in_use: {Conn-1} 但线程被阻塞❌ 执行不到-
单线程 brpop 返回后_available: [Conn-1]✅ 顺序执行Conn-1(复用)
多线程_in_use: {Conn-1} + _available: [Conn-2]✅ 并行执行Conn-2(新借出)

5. 如果 max_connections=1 会怎样 ​

场景max_connections=1结果
单线程brpop 独占 Conn-1无影响(hset 本来就执行不到)
单线程 brpop 返回后Conn-1 已归还hset 复用 Conn-1,正常
多线程brpop 独占 Conn-1,线程 B 借不到ConnectionError: Too many connections

所以:单线程下 max_connections=1 也能跑;多线程才需要 max_connections >= 2。本项目用 max_connections=10 是为 Seedance 分镜并行(3 线程)留余量。


Q7:timeout=0(永久阻塞)和 timeout=5 有什么实际区别?为什么不推荐 timeout=0? ​

答:

timeout行为能否响应 Ctrl+C风险
0永久阻塞,直到有元素❌ 不能Redis 重启后连接可能假死
5最多等 5 秒,超时回到循环✅ 每 5 秒检查一次无
python
# ❌ timeout=0:Ctrl+C 无法中断
while True:
    result = redis_client.brpop("task:queue", timeout=0)
    # 如果队列一直没元素,永远卡在这里
    # Ctrl+C 信号无法被 python 处理

# ✅ timeout=5:每 5 秒回到循环顶部
while True:
    try:
        result = redis_client.brpop("task:queue", timeout=5)
        if result:
            run_task(int(result[1]))
    except KeyboardInterrupt:
        logger.info("Worker 收到退出信号,停止")
        break

timeout=5 时,每 5 秒 BRPOP 超时返回 None,while True 循环回到顶部,Python 有机会检查 KeyboardInterrupt 信号。5 秒是经验值——太短浪费 CPU(频繁循环),太长退出延迟。


第三章:单线程模型与原子性 ​

本章导读 ​

本章围绕“单线程模型与原子性”展开,先交代适用场景,再说明核心原理,随后给出典型示例,最后补充小结与延伸阅读。

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

Q8:Redis 是单线程的,为什么还能支撑 10 万 QPS?单线程不会成为瓶颈吗? ​

答:

单线程不会成为瓶颈,原因有四个:

因素说明
纯内存操作内存读写纳秒级,不需要磁盘 I/O
无锁竞争单线程不需要加锁、解锁、等待锁
无上下文切换线程切换成本约 5-10 微秒,Redis 完全避免
I/O 多路复用epoll 让单线程管理万级连接,不阻塞

单命令耗时通常在 100 纳秒以内:

BRPOP 弹出: ~100ns
LPUSH 推入: ~100ns
LLEN 长度: ~50ns
唤醒等待客户端: ~200ns

即使 10 万 QPS,CPU 利用率也不到 50%。

在本项目中:瓶颈永远在 Worker 处理速度(Seedance API 调用 4 分钟/任务),而非 Redis 队列操作(100 纳秒/操作),差距 7 个数量级。


Q9:10 个 Worker 同时 BRPOP task:queue 5,这时有人 LPUSH task:queue 6,为什么不会有两个 Worker 同时拿到 "6"? ​

答:

Redis 通过四层保证确保只有一个 Worker 能拿到元素:

第一层:单线程命令执行

Redis 命令执行引擎只有一个线程。不管多少个客户端同时发命令,它们在主线程中排队执行,不存在两条命令"同时执行"的窗口。

第二层:命令原子性

每条 Redis 命令是原子操作。LPUSH 的"插入元素 → 检查等待链表 → 唤醒客户端"是一个不可被打断的原子步骤。

第三层:等待链表 FIFO

多个 Worker 等待时,Redis 会按 BRPOP 真正到达服务端的先后顺序,把对应客户端连接挂到 task:queue 的等待链表中。这里的多个 Worker 隐含的是多个独立线程或多个独立进程,它们各自通过一条 TCP 连接向 Redis 发起 BRPOP。

假设 10 个 Worker 依次进入阻塞等待,等待链表不是一个无序集合,而是一条有队头和队尾的 FIFO 队列。下面这张图可以更直观地看出等待链和唤醒顺序:

注:原「Redis List 阻塞等待链表 FIFO 结构」手绘图缺失,见下方「图解说明」表格。

图解说明:

图中元素含义
list 头部(左)队头方向,LPUSH 从这里推入元素,LPOP 从这里弹出;也是最早等待的 Worker 所在位置
list 尾部(右)队尾方向,RPUSH 从这里推入元素,RPOP 从这里弹出;最后到达的 Worker 排在这一端
W1(黄色框)最早进入 BRPOP 等待的 Worker,位于队头,LPUSH 到达时第一个被唤醒
W2 / W3(米色框)随后依次进入等待的 Worker,排在 W1 后面,只有 W1 出队后才轮到 W2
W4(绿色框)最后进入等待的 Worker,位于队尾,需要等前面所有 Worker 都被唤醒后才会轮到它
实线箭头 →等待链的方向,从队头到队尾依次排列
虚线弧形箭头一次完整的唤醒流程:LPUSH 到达 → 只唤醒队头 W1 → W1 出队 → 队头变为 W2
LPUSH / LPOP左侧操作,对应队头推入和弹出
RPUSH / RPOP右侧操作,对应队尾推入和弹出

简单来说,这张图要表达三件事:

  1. 等待链是有序的:W1 最先到,排在队头;W4 最后到,排在队尾。
  2. 唤醒只取队头:LPUSH 推入元素后,Redis 只会唤醒队头的 W1,不会同时通知 W2、W3、W4。
  3. 出队后队列前移:W1 拿到元素离开等待链后,W2 自动成为新的队头,等待下一次 LPUSH 唤醒。

当有人执行 LPUSH task:queue 6 时,Redis 只会从队头取出最早等待的 W1,并把元素 6 交给 W1。

因此,6 只会交给一个等待者;后面的 W2、W3 不会同时拿到同一个元素,它们必须等待后续新的元素进入队列。

LPUSH 唤醒时只处理队头第一个等待客户端(break),不批量唤醒:

c
// Redis 源码 handleClientsBlockedOnKeys(简化)
while (clientNode) {
    client *receiver = clientNode->value;
    robj *value = listTypePop(o, LIST_TAIL);
    if (value) {
        addReply(receiver, value);      // 发给这个客户端
        listDelNode(clients, clientNode); // 从等待链表移除
        break;                            // ← 只唤醒一个!
    }
    clientNode = listNextNode(clientNode);
}

第四层:事件循环串行

即使 LPUSH 6 7 8 一次推入 3 个元素,也是一个一个唤醒:

第1轮: 弹出 "6" → 发给 W1 → break
第2轮: 弹出 "7" → 发给 W2 → break
第3轮: 弹出 "8" → 发给 W3 → break

Q10:LPUSH 唤醒 BRPOP 时,数据是先存入列表再弹出,还是直接传递? ​

答:

直接传递,数据不经过列表中转。

LPUSH task:queue 6 到达 Redis:

Redis 主线程执行 LPUSH:
  1. 在列表头部插入 "6" → 列表变为 [6]
  2. 检查等待链表 → 发现 W1 在等
  3. 直接把 "6" 发给 W1(通过 fd 写回)
  4. 从列表删除 "6" → 列表变为 []
  5. 从等待链表移除 W1
  6. LPUSH 命令完成

整个过程耗时: ~200 纳秒

步骤 2-5 全在 LPUSH 的一条命令执行中完成,是原子的。数据从 LPUSH 方直接传递给 BRPOP 方,O(1) 时间完成。

注意:如果等待链表为空(没有 Worker 在等),"6" 会留在列表中,等下一个 BRPOP 来弹出。


Q10.1:BRPOP 的"推送"本质是什么?是延迟响应还是真正的推送? ​

答:

BRPOP 的唤醒机制本质是延迟响应(Delayed Response),不是 TCP 层面的全双工推送,也不是客户端主动拉取。

1. 三种模型的严格区分 ​

模型是否符合 BRPOP说明
纯拉(客户端轮询)❌ 否客户端没有反复发请求查询队列状态
纯推(服务端主动连客户端)❌ 否客户端必须先发 BRPOP 请求,服务端才会响应
延迟响应✅ 是客户端发一次请求,服务端延迟到条件满足才响应

2. 延迟响应的完整流程 ​

普通命令(HSET)—— 立即响应:
  Client ──HSET──► Redis
  Client ◄──OK──── Redis    ← 立即响应(~0.1ms)

BRPOP(延迟响应):
  Client ──BRPOP─► Redis
                  Redis: 队列空 → 加入等待链表 → 不回复(命令挂起)
                  Redis: 主线程继续处理其他客户端命令
                  (等待中... 可能几秒)
                  Redis: 收到 LPUSH → 弹出元素 → addReply(Client, 元素)
  Client ◄──元素── Redis    ← 延迟响应(可能几秒后才回复)

3. 延迟响应的三个关键特征 ​

特征说明
命令发送客户端主动发送 BRPOP(一次请求)
响应时机由服务端决定(队列有元素或超时时才响应)
等待期间客户端 socket 阻塞在 read_response(),服务端主线程不阻塞

4. "推送"说法的精确含义 ​

LPUSH 唤醒 BRPOP 时,Redis 服务端通过 addReply() 把结果写入等待客户端的输出缓冲区——这一步看起来像"推送",但本质是延迟回复之前挂起的 BRPOP 请求:

c
// Redis 源码 t_list.c(简化)

void handleClientsBlockedOnKeys(void) {
    // LPUSH 执行后,主线程事件循环处理 ready_keys
    while (listLength(server.ready_keys) > 0) {
        // 查找等待这个 key 的客户端链表
        list *clients = dictGetVal(de);

        // 遍历等待的客户端
        client *receiver = listNode->value;

        // 弹出列表元素
        robj *value = listTypePop(o, LIST_TAIL);

        if (value) {
            // ★ 关键:把结果写入等待客户端的输出缓冲区
            addReplyArrayLen(receiver, 2);
            addReplyBulk(receiver, key);     // 队列名
            addReplyBulk(receiver, value);    // 弹出的元素
            // ↑ 这就是"延迟响应"——回复之前挂起的 BRPOP 请求
        }
    }
}

5. 与全双工推送的对比 ​

对比项BRPOP 延迟响应TCP 全双工推送
谁先发起客户端先发 BRPOP服务端可主动发
响应触发服务端条件满足时回复服务端任意时刻推送
TCP 模型单工请求-响应(延迟版本)双工独立通道
一次请求响应次数一次请求对应一次响应一次请求可触发多次推送

6. 为什么说是"服务端主动推送" ​

虽然本质是延迟响应,但从客户端视角看:

  • 客户端发完 BRPOP 后就阻塞在 read_response()
  • 没有再发任何请求
  • 数据"自动"到达了 socket

所以从客户端视角称为"推送"也没错——但严格来说这是延迟响应,因为客户端必须先发 BRPOP 请求,服务端才能"推"。

7. 总结 ​

说法是否准确
"BRPOP 是客户端轮询拉取"❌ 错误(客户端没有反复发请求)
"BRPOP 是服务端主动推送"⚠️ 不严谨(本质是延迟响应,但客户端视角像推送)
"BRPOP 是延迟响应"✅ 准确(一次请求,服务端延迟回复)
"LPUSH 触发 BRPOP 的回复"✅ 准确(LPUSH 执行时调用 addReply 写入客户端缓冲区)
"BRPOP 期间 TCP 是全双工"❌ 错误(TCP 是全双工,但 BRPOP 是单工请求-响应)

Q11:假设 Worker-1 和 Worker-2 几乎同时发送 BRPOP,API 也几乎同时发送 LPUSH,三种命令到达 Redis 的顺序不同,结果会怎样? ​

答:

不管命令到达顺序如何,只有一个 Worker 能拿到元素。推演三种情况:

情况 A:BRPOP(W1) → BRPOP(W2) → LPUSH

BRPOP(W1) 执行:列表空 → W1 加入等待链表
BRPOP(W2) 执行:列表空 → W2 加入等待链表(排在 W1 后面)
LPUSH 执行:
  1. 插入 "6"
  2. 检查等待链表 → W1 是第一个
  3. "6" 发给 W1
  4. 删除 "6"
  5. 移除 W1

结果:W1 拿到 "6",W2 继续等待 ✅

情况 B:LPUSH → BRPOP(W1) → BRPOP(W2)

LPUSH 执行:
  1. 插入 "6"
  2. 等待链表为空,没人等待
  3. "6" 留在列表中

BRPOP(W1) 执行:列表非空 → 直接弹出 "6",返回给 W1
BRPOP(W2) 执行:列表空 → W2 加入等待链表

结果:W1 拿到 "6",W2 继续等待 ✅

情况 C:BRPOP(W1) → LPUSH → BRPOP(W2)

BRPOP(W1) 执行:列表空 → W1 加入等待链表
LPUSH 执行:
  1. 插入 "6"
  2. 检查等待链表 → W1 在等
  3. "6" 发给 W1
  4. 删除 "6"
  5. 移除 W1

BRPOP(W2) 执行:列表空 → W2 加入等待链表

结果:W1 拿到 "6",W2 继续等待 ✅

根本原因:Redis 单线程串行执行,LPUSH 和 BRPOP 不可能"同时"执行,两条命令之间不存在"间隙"让数据不一致。


第四章:多 Worker 与连接池 ​

本章导读 ​

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

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

Q12:3 个 Worker 进程启动后,各自有独立的 redis_client 吗?Redis 服务端看到的是 3 个连接还是 1 个? ​

答:

3 个独立的 redis_client,Redis 端看到 3 个 TCP 连接。

这里的“3 个 Worker 进程”指的是用 3 个独立 OS 进程启动 3 个 Worker。每个 python -m app.worker 是独立进程,各自 import 模块时创建 redis_client 单例。每个进程启动时连接池都是空的(按需建连),brpop 第一次执行时才建立第 1 条 TCP:

进程 A (PID 1001)           进程 B (PID 1002)           进程 C (PID 1003)
┌──────────────────┐       ┌──────────────────┐       ┌──────────────────┐
│ redis_client     │       │ redis_client     │       │ redis_client     │
│  └─ Pool (max=10)│       │  └─ Pool (max=10)│       │  └─ Pool (max=10)│
│  ┌─────┬─────┐   │       │  ┌─────┬─────┐   │       │  ┌─────┬─────┐   │
│  │C-1  │     │   │       │  │C-1  │     │   │       │  │C-1  │     │   │
│  │BRPOP│空闲 │   │       │  │BRPOP│空闲 │   │       │  │BRPOP│空闲 │   │
│  └─────┴─────┘   │       │  └─────┴─────┘   │       │  └─────┴─────┘   │
│  _in_use: {C-1}  │       │  _in_use: {C-1}  │       │  _in_use: {C-1}  │
│  _available: []  │       │  _available: []  │       │  _available: []  │
│  实际连接: 1 个  │       │  实际连接: 1 个  │       │  实际连接: 1 个  │
└───────┬──────────┘       └───────┬──────────┘       └───────┬──────────┘
        │ TCP:54321                │ TCP:54322                │ TCP:54323
        ▼                          ▼                          ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Redis 服务端(端口 6380)                                                │
│  client(fd=10, addr=:54321, cmd=brpop)  ← 等待链表节点 1  ← 进程 A
│  client(fd=11, addr=:54322, cmd=brpop)  ← 等待链表节点 2  ← 进程 B
│  client(fd=12, addr=:54323, cmd=brpop)  ← 等待链表节点 3  ← 进程 C
│                                                                          │
│  task:queue → [client(fd=10), client(fd=11), client(fd=12)]             │
│                ↑ 第一个被 LPUSH 唤醒                                    │
└──────────────────────────────────────────────────────────────────────────┘

关键点:

  • 每个进程的连接池 max=10,但实际只创建 1 个连接(brpop 独占 Conn-1)
  • 每条 TCP 连接源端口不同(54321/54322/54323),目的端口都是 6380
  • Redis 端 CLIENT LIST 看到 3 行,无法判断它们属于几个进程

用 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

3 个 Worker = 3 行 = 3 个 TCP 连接 = 3 个等待链表节点。


Q13:Redis 等待链表里存的是什么?是"Worker 进程"吗? ​

答:

不是 Worker 进程或 Worker 线程这种应用侧执行单元的抽象概念,而是具体的 redisClient 结构体指针,每个对应一个 TCP 连接的文件描述符(fd)。

c
// Redis 源码 server.h
typedef struct client {
    int fd;              // ← TCP 连接的文件描述符
    connection *conn;    // ← 连接对象
    // ... 其他字段
} client;

// 阻塞 key 的等待客户端链表
dict *db->blocking_keys;
//  key → list of client*

唤醒时 Redis 通过 fd 把数据写回给客户端。一个 client = 一个 fd = 一个 TCP 连接。

这里要特别区分两个概念:redis_client 是应用侧创建的 Redis 客户端对象,而等待链表中的 client 是 Redis 服务端维护的、正在阻塞等待某个 key 的 TCP 连接。两者不是同一个层面的概念,但它们通常通过同一条连接建立对应关系。

即使一个进程内有 3 个线程同时 BRPOP,redis-py 连接池也会借出 3 个不同的连接,Redis 端看到 3 个 client,等待链表有 3 个节点。Redis 不关心客户端是进程还是线程,只看 TCP 连接。

⚠️ 关键澄清:等待链表节点数 ≠ 连接总数 ​

一个 Python 进程借出 3 个 TCP 连接 → Redis 端有 3 个 client 结构体。但等待链表里存几个,取决于有几个连接正在 BRPOP 等待。

核心规则:

等待链表节点数 = 当前正在执行 BRPOP 且未返回的 TCP 连接数
              ≠ 客户端进程数
              ≠ redis_client 对象数
              ≠ 连接池里的连接总数
              ≠ Redis 端的 client 结构体总数

场景对比:一个进程 + 3 个连接,等待链表节点数不同 ​

场景 A:3 个连接都在 BRPOP(等待链表 3 个节点)

python
# 同一个进程,3 个线程同时 BRPOP 同一个 key
import threading

def worker():
    # 每个线程独立调用一次 brpop,借出各自的连接
    redis_client.brpop("task:queue", timeout=5)
    # ↑ 线程1 借 Conn-1,线程2 借 Conn-2,线程3 借 Conn-3
    # ↑ 3 个线程并发执行,3 个连接同时阻塞在 read_response()

# 启动 3 个线程
threads = [threading.Thread(target=worker) for _ in range(3)]
for t in threads:
    t.start()
for t in threads:
    t.join()

# 3 个线程 → 连接池借出 3 个 TCP 连接 → Redis 端 3 个 client → 都在等待
3 个线程并发执行 brpop 时的连接池状态:
  _in_use: {Conn-1, Conn-2, Conn-3}      ← 3 个连接都被借出
  _available: []                          ← 池空(还有 7 个余量未创建)

Redis 端:
  client(fd=10, cmd=brpop)  ← Conn-1(线程1)
  client(fd=11, cmd=brpop)  ← Conn-2(线程2)
  client(fd=12, cmd=brpop)  ← Conn-3(线程3)

等待链表:
  task:queue → [client(fd=10), client(fd=11), client(fd=12)]
                 ↑ 3 个节点,虽然来自同一个进程

LPUSH 时只唤醒 fd=10(FIFO 第一个),fd=11、fd=12 继续等。

场景 B:1 个 BRPOP + 2 个 hset(等待链表 1 个节点)

python
# 同一个进程,1 个线程 BRPOP 阻塞,2 个线程并行写进度
import threading

# 线程 1:BRPOP 阻塞等待任务
def consumer():
    result = redis_client.brpop("task:queue", timeout=5)
    # ↑ 借出 Conn-1,阻塞在 read_response()
    # ↑ 加入等待链表,未归还 Conn-1

# 线程 2:写任务进度(立即归还)
def writer_progress_image():
    redis_client.hset("task:6:progress", "stage", "IMAGE")
    # ↑ 借出 Conn-2 → 发送 HSET → 收到回复 → 立即归还 Conn-2

# 线程 3:写任务结果(立即归还)
def writer_result_done():
    redis_client.hset("task:6:progress", "stage", "DONE")
    # ↑ 借出 Conn-3 → 发送 HSET → 收到回复 → 立即归还 Conn-3

# 启动 3 个线程(consumer 先启动确保先进入 BRPOP)
t1 = threading.Thread(target=consumer)
t2 = threading.Thread(target=writer_progress_image)
t3 = threading.Thread(target=writer_result_done)

t1.start()
import time; time.sleep(0.1)   # 确保 consumer 先进入 BRPOP
t2.start()
t3.start()

t1.join(); t2.join(); t3.join()
线程并发期间连接池状态:
  _in_use: {Conn-1}                       ← 只有 Conn-1 还在阻塞(BRPOP 没返回)
  _available: [Conn-2, Conn-3]             ← Conn-2、Conn-3 已归还
  ↑ 注意:Conn-2、Conn-3 已归还但连接对象仍在池中(TCP 没断)

Redis 端:
  client(fd=10, cmd=brpop)    ← Conn-1 在等待链表里
  client(fd=11, cmd=hset)     ← Conn-2 已归还,但 client 结构体还在(TCP 仍存活)
  client(fd=12, cmd=hset)     ← Conn-3 已归还,但 client 结构体还在(TCP 仍存活)

等待链表:
  task:queue → [client(fd=10)]
                 ↑ 只有 1 个节点(只有 Conn-1 在 BRPOP)

3 个 client 都存在(TCP 连接还活着),但只有 1 个在等待链表里。

关键澄清:

  • 归还 ≠ 关闭 TCP:连接归还到池中只是从 _in_use 移到 _available,TCP 连接保持打开状态,Redis 端 client 结构体仍然存在
  • client 结构体存活到 TCP 关闭:只有显式 disconnect() 或连接池销毁时,TCP 才关闭,Redis 端 client 才会从 CLIENT LIST 消失

场景 C:本项目实际(等待链表 1 个节点)

python
# 主循环(单线程)
def process_queue():
    while True:
        result = redis_client.brpop("task:queue", timeout=5)
        # ↑ 主线程借 Conn-1 BRPOP 阻塞(等待链表 1 个节点)
        if result:
            task_id = int(result[1])
            # ↑ brpop 已返回,Conn-1 已归还到池中
            run_task(task_id)
            # ↑ 此时主线程在执行 run_task,没有线程在 BRPOP

# run_task 内部启动 3 个分镜线程(ThreadPoolExecutor)
def run_task(task_id):
    redis_client.hset(f"task:{task_id}:progress", "stage", "SCRIPT")
    # ↑ 主线程借 Conn-1(复用)→ 立即归还

    # 3 线程并行调 Seedance API + 写进度
    from concurrent.futures import ThreadPoolExecutor
    def generate_frame_video(frame):
        # 调 Seedance API(不在 Redis 上阻塞)
        # ...
        # 写进度(借连接)
        redis_client.hset(f"task:{task_id}:progress",
                          f"frame_{frame.seq}", "done")
        # ↑ 各线程借 Conn-2/3/4 → 立即归还

    with ThreadPoolExecutor(max_workers=3) as executor:
        futures = [executor.submit(generate_frame_video, f) for f in frames]
        for future in futures:
            future.result()
阶段 1:BRPOP 阻塞期间(主线程在 brpop 中)
  连接池: _in_use: {Conn-1}   _available: []
  Redis 端: client(fd=10, cmd=brpop)
  等待链表: task:queue → [client(fd=10)]    ← 1 个节点

阶段 2:BRPOP 返回后,run_task 执行中(分镜并行)
  连接池: _in_use: {}   _available: [Conn-1, Conn-2, Conn-3, Conn-4]
           ↑ 主线程 hset 借 Conn-1 → 归还
           ↑ 3 个分镜线程 hset 借 Conn-2/3/4 → 归还
  Redis 端: client(fd=10,11,12,13, cmd=hset)
  等待链表: task:queue → []    ← 空!没有线程在 BRPOP

阶段 3:run_task 返回,主线程回到 while 循环顶部再次 brpop
  连接池: _in_use: {Conn-1}   _available: [Conn-2, Conn-3, Conn-4]
  Redis 端: client(fd=10, cmd=brpop)
  等待链表: task:queue → [client(fd=10)]    ← 1 个节点

关键:等待链表节点数随主线程状态变化——brpop 阻塞时 1 个节点,run_task 执行时 0 个节点(brpop 已返回,没有人在 BRPOP)。

速查表 ​

概念场景 A(3 线程 BRPOP)场景 B(1 BRPOP + 2 hset)场景 C(本项目)
Python 进程数111
同时工作的线程数331(主循环)/ 4(分镜并行时)
连接池 _in_use 大小31(另外 2 个已归还)1(brpop 期间)/ 0(run_task 期间)
连接池 TCP 连接总数33(一旦建立不关闭)1-4(按需创建)
Redis 端 client 结构体数33(TCP 仍存活)1-4(动态)
等待链表节点数311(brpop 期间)/ 0(run_task 期间)

核心规律:

关系说明
等待链表节点数 = ?当前正在执行 BRPOP 且未返回的 TCP 连接数
已归还连接 → Redis 端 client 是否消失?❌ 不消失,TCP 仍存活,client 结构体保留
已归还连接 → 等待链表是否还有?❌ 没有,已归还说明 BRPOP 已返回
连接池 TCP 连接总数 = ?历史最大借出数(一旦建立不主动关闭)

用 CLIENT LIST 验证 ​

场景 A(3 个线程都在 BRPOP):

bash
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   ← 在等待链表

3 行 = 3 个 client = 3 个 TCP 连接 = 等待链表 3 个节点(3 个都在等)。

场景 B(1 个 BRPOP + 2 个 hset):

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=hset   age=5  idle=0   ← 不在等待链表
id=3  addr=127.0.0.1:54323  fd=12  cmd=hset   age=5  idle=0   ← 不在等待链表

3 行 = 3 个 client = 3 个 TCP 连接,但等待链表只有 1 个节点(只有 cmd=brpop 且 idle 持续增长的在等)。

本章小结 ​

等待链表里存的是"正在 BRPOP 等待的 TCP 连接",不是"客户端进程"。一个进程借出 3 个连接,如果 3 个都在 BRPOP,等待链表就有 3 个节点;如果只有 1 个在 BRPOP,等待链表就只有 1 个节点。Redis 只看 TCP 连接,不看连接属于哪个进程。

补充:每条 TCP 连接都有独立的源端口 ​

关键概念:一条 TCP 连接由四元组唯一标识:

TCP 连接 = (源 IP, 源端口, 目的 IP, 目的端口)

本项目场景:

目的 IP:目的端口 = 127.0.0.1:6380   ← Redis 服务端固定
源 IP            = 127.0.0.1        ← 客户端机器固定
源端口           = ???              ← 每条连接不同!

源端口是操作系统分配的,每建立一条 TCP 连接,内核就分配一个空闲的源端口:

Conn-1: 127.0.0.1:54321 ←► 127.0.0.1:6380
Conn-2: 127.0.0.1:54322 ←► 127.0.0.1:6380
Conn-3: 127.0.0.1:54323 ←► 127.0.0.1:6380
         ↑ 源端口不同       ↑ 目的端口相同(都是 Redis)

同一进程的 3 个连接也是不同端口(代码示例见上文 Q13 场景 A):

python
# 简化示例:一个进程,主线程 + 2 个子线程,共 3 个 TCP 连接
import threading, redis

redis_client = redis.Redis(host="localhost", port=6380, max_connections=10)

# 主线程借 Conn-1
redis_client.hset("k", "main", "v1")

# 子线程各自借 Conn-2、Conn-3
def worker(tag):
    redis_client.hset("k", tag, f"v-{tag}")

t1 = threading.Thread(target=worker, args=("t1",))
t2 = threading.Thread(target=worker, args=("t2",))
t1.start(); t2.start(); t1.join(); t2.join()

此时 CLIENT LIST 输出(3 条 TCP 连接,3 个源端口):

id=1  addr=127.0.0.1:54321  fd=10  cmd=hset   age=1  idle=0   ← Conn-1(主线程)
id=2  addr=127.0.0.1:54322  fd=11  cmd=hset   age=1  idle=0   ← Conn-2(线程 t1)
id=3  addr=127.0.0.1:54323  fd=12  cmd=hset   age=1  idle=0   ← Conn-3(线程 t2)
              ↑                ↑
              3 个源端口不同     3 个 fd 不同
              但是同一个进程!

Redis 端无法从 CLIENT LIST 判断这 3 个连接属于同一个进程还是不同进程——它只看到 3 条独立的 TCP 连接。

为什么每条 TCP 连接需要独立端口 ​

TCP 协议规定,一个端口同一时刻只能被一条 TCP 连接的某一端使用:

端口 54321  → 只能被 Conn-1 的客户端侧占用
端口 54322  → 只能被 Conn-2 的客户端侧占用
端口 54323  → 只能被 Conn-3 的客户端侧占用
端口 6380   → Redis 服务端共用一个端口(但能区分连接)

Redis 服务端怎么用同一个端口 6380 服务多条连接:靠 fd(文件描述符)区分。每个 TCP 连接到达服务端后,Redis 通过 accept() 创建一个新的 fd,然后把它和 client 结构体绑定。

c
// Redis 服务端源码(简化)
while (1) {
    int client_fd = accept(server_fd, ...);  // ← 每个连接 accept 一次
    // ↑ server_fd 绑定 6380 端口,但 client_fd 是新的
    client *c = createClient(client_fd);
    // ↑ 每个 client_fd 对应一个 client 结构体
}
层面端口fd
客户端侧每条 TCP 连接独立源端口每条连接独立 fd
服务端侧共用 6380 监听端口每条连接独立 fd

用 lsof / netstat 验证 ​

bash
# 查看 Redis 服务端(6380)的连接
lsof -i :6380
COMMAND   PID  USER   FD  TYPE  DEVICE  SIZE/OFF  NODE  NAME
redis-ser 1234  user   6u  IPv4  0x1234    0t0   TCP  *:6380 (LISTEN)        ← 监听 socket
redis-ser 1234  user  10u  IPv4  0x5678    0t0   TCP  localhost:6380→localhost:54321
redis-ser 1234  user  11u  IPv4  0x5679    0t0   TCP  localhost:6380→localhost:54322
redis-ser 1234  user  12u  IPv4  0x567a    0t0   TCP  localhost:6380→localhost:54323
                              ↑                             ↑
                              服务端 fd(每条不同)          客户端源端口(每条不同)

3 条连接 = 3 个服务端 fd(10/11/12)= 3 个客户端源端口(54321/54322/54323)。

端口范围 ​

操作系统给客户端分配源端口时,从一个固定范围内选:

bash
# Linux 查看端口范围
$ cat /proc/sys/net/ipv4/ip_local_port_range
32768   60999

# macOS 查看
$ sysctl net.inet.ip.portrange.first net.inet.ip.portrange.last
net.inet.ip.portrange.first: 1024
net.inet.ip.portrange.last: 65535

理论上一个客户端机器最多能连 28000 多条 TCP 到同一个 Redis 端口(Linux),但实际 Worker 进程不会这么多。

端口总结表 ​

问题答案
同一客户端的 3 个连接,CLIENT LIST 显示 3 行吗?✅ 是
这 3 行的端口相同吗?❌ 不同,每条 TCP 连接独立源端口
每条 TCP 连接都有独立端口吗?✅ 是(客户端侧源端口不同)
Redis 服务端端口相同吗?✅ 相同(都是 6380,靠 fd 区分)
Redis 能从 CLIENT LIST 判断连接属于哪个进程吗?❌ 不能,只看 TCP 连接

核心:一条 TCP 连接 = (源 IP, 源端口, 目的 IP, 目的端口) 四元组。同一个进程的 3 条连接,源端口必然不同,所以 CLIENT LIST 看到 3 行端口不同的记录。

端口与连接速查表 ​

维度客户端侧服务端(Redis)侧
IP127.0.0.1(客户端机器)127.0.0.1(Redis 机器)
端口每条 TCP 连接独立源端口(54321/54322/54323)共用 6380 监听端口
fd每条连接独立 fd每条连接独立 fd(10/11/12)
区分连接靠什么源端口fd(accept 返回的新 fd)
一个端口能被多少连接使用同一时刻只能被一条 TCP 连接的客户端侧占用同一端口可服务多条连接(靠 fd 区分)
场景Python 进程数TCP 连接数源端口数Redis 端 fd 数CLIENT LIST 行数
3 进程 × 1 连接333(每进程 1 个)33
1 进程 × 3 连接(3 线程 BRPOP)133(同一进程)33
1 进程 × 1 连接(本项目主循环)11111
1 进程 × 4 连接(分镜并行)14444
问题答案
同一客户端的 3 个连接,CLIENT LIST 显示 3 行吗?✅ 是
这 3 行的端口相同吗?❌ 不同,每条 TCP 连接独立源端口
每条 TCP 连接都有独立端口吗?✅ 是(客户端侧源端口不同)
Redis 服务端端口相同吗?✅ 相同(都是 6380,靠 fd 区分)
Redis 能从 CLIENT LIST 判断连接属于哪个进程吗?❌ 不能,只看 TCP 连接
同一进程的 3 条连接源端口能相同吗?❌ 不能,TCP 协议规定一个端口同一时刻只能被一条连接的某一端使用
Redis 服务端怎么用 6380 服务多条连接?监听 socket 绑定 6380,accept 时为每条连接创建独立 fd
Linux 客户端源端口范围32768-60999(约 28000 个)
macOS 客户端源端口范围1024-65535
验证命令作用
redis-cli CLIENT LIST查看 Redis 端所有 client(含 addr、fd、cmd、idle)
lsof -i :6380查看 6380 端口的所有 TCP 连接(含客户端源端口)
netstat -an | grep 6380查看与 6380 相关的连接状态
cat /proc/sys/net/ipv4/ip_local_port_rangeLinux 查看客户端源端口范围
sysctl net.inet.ip.portrange.first net.inet.ip.portrange.lastmacOS 查看客户端源端口范围

Q14:如果用 multiprocessing.Process 的 fork 模式启动 Worker,Redis 连接会出什么问题? ​

答:

fork 会复制父进程的内存,包括 redis_client 和已建立的 TCP 连接的 fd。两个进程向同一个 socket 写数据会导致 Redis 协议错乱。

python
# ❌ fork 模式的问题
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!

正确做法:子进程中重新创建 redis_client:

python
def worker():
    import redis
    # 子进程中重新创建,不复用父进程的连接池
    client = redis.Redis(
        host="localhost",
        port=6380,
        decode_responses=True,
        max_connections=10,
    )
    client.brpop("task:queue", timeout=5)

本项目不受影响:python -m app.worker 每次都是全新进程,不走 fork,不存在继承问题。


Q15:一个 Worker 进程实际同时占用的连接数是多少?连接池 max_connections=10 是不是浪费? ​

答:

实际同时占用的连接数取决于是否启用 Seedance 分镜并行:

模式占用连接数说明
单线程顺序执行(无分镜并行)1 个brpop 返回后才执行 hset,全程只用 1 个连接
3 线程分镜并行(本项目实际启用)3-4 个3 个分镜线程 + 主线程 BRPOP

1. 单线程顺序执行模式(1 个连接) ​

阶段占用连接说明
process_queue 中 BRPOP 阻塞1 个只有 BRPOP 在执行,线程被挂起
run_task 中写进度1 个BRPOP 已归还,HSET 复用 Conn-1
run_task 中推审核队列1 个同上
峰值1 个brpop 和 hset 顺序执行,不会同时

2. 3 线程分镜并行模式(3-4 个连接,本项目实际配置) ​

python
# orchestrator.py 实际代码
with ThreadPoolExecutor(max_workers=3) as executor:
    futures = [executor.submit(generate_frame_video, frame) for frame in frames]
    # ↑ 3 个线程并行调用 Redis 写进度
    # ↑ 同时主线程可能还在 brpop 阻塞(Conn-1 被独占)

# 连接池状态:
#   _in_use: {Conn-1(BRPOP), Conn-2, Conn-3, Conn-4}
#   _available: [Conn-5, ..., Conn-10]
#   ↑ max_connections=10 留 6 个余量,不浪费
阶段占用连接说明
主线程 BRPOP 阻塞Conn-1独占
分镜线程 1 写进度Conn-2并行借出
分镜线程 2 写进度Conn-3并行借出
分镜线程 3 写进度Conn-4并行借出
峰值4 个1 BRPOP + 3 分镜并行

3. 为什么 max_connections=10 不浪费 ​

  • 连接是惰性创建的:max_connections=10 只是上限,实际用几个建几个,不用的不建
  • 分镜并行需要 3-4 个:3 线程并行 + 主线程 BRPOP,至少需要 4 个连接
  • 留 6 个余量:未来扩展到 5-6 线程并行也不用改配置

4. 多 Worker 进程的总连接数 ​

Worker 进程数单进程连接峰值总 TCP 连接Redis max_clients
144默认 10000
3412足够
10440足够
504200足够
2004800足够

详见 Redis连接池详解.md 1.3 节 — 单线程单连接 vs 多线程多连接的完整对比。


第五章:Redis 6.0 多线程 I/O ​

本章导读 ​

本章围绕“Redis 6.0 多线程 I/O”展开,先交代适用场景,再说明核心原理,随后给出典型示例,最后补充小结与延伸阅读。

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

Q16:Redis 6.0 引入了多线程 I/O,这会影响 BRPOP 的原子性保证吗? ​

答:

不会影响。多线程 I/O 只用于读写 socket 数据,命令执行仍然在主线程串行进行。

                  ┌──────────────────────────┐
                  │       Redis 6.0          │
                  │                          │
  多线程 I/O      │  ┌────┐ ┌────┐ ┌────┐    │
  (读取/写入     │  │IO-1│ │IO-2│ │IO-3│    │
   socket 数据)  │  └─┬──┘ └─┬──┘ └─┬──┘    │
                  │    │      │      │       │
                  │    └──────┼──────┘       │
                  │           │               │
                  │     ┌─────┴─────┐         │
                  │     │  主线程    │         │
                  │     │ (命令执行) │         │
                  │     └───────────┘         │
                  │                           │
                  └──────────────────────────┘

  1. IO 线程从 socket 读取客户端数据
  2. 主线程执行命令(单线程,串行)  ← 原子性在这里保证
  3. IO 线程把结果写回 socket

3 个 IO 线程可以同时读取 socket 数据,但主线程逐条执行 BRPOP,仍然保证原子性。

层面线程数职责
I/O 线程1-N(可配置)读取 socket 数据、写回 socket 数据
主线程1解析命令、执行命令、操作数据结构

第六章:崩溃恢复与任务丢失 ​

本章导读 ​

本章围绕“崩溃恢复与任务丢失”展开,先交代适用场景,再说明核心原理,随后给出典型示例,最后补充小结与延伸阅读。

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

Q17:Worker 从 BRPOP 取出任务后、执行前崩溃了,这个任务会怎样? ​

答:

任务会永久丢失。因为 BRPOP 是"弹出即删除",取出后任务已从 Redis 队列消失。

python
result = redis_client.brpop("task:queue", timeout=5)
_, task_id = result
# ← 如果 Worker 在这里崩溃,task_id 已从队列消失但未执行
run_task(int(task_id))

解决方案:RPOPLPUSH 模式,弹出时同时放入"处理中"队列:

python
# 弹出并转入"处理中"队列
task_id = redis_client.rpoplpush("task:queue", "task:processing")

# 处理完成后从"处理中"队列删除
redis_client.lrem("task:processing", 1, task_id)

# Worker 启动时扫描"处理中"队列,重新推回 task:queue
def recover_stuck_tasks():
    stuck = redis_client.lrange("task:processing", 0, -1)
    for task_id in stuck:
        redis_client.lpush("task:queue", task_id)
        redis_client.lrem("task:processing", 1, task_id)
参数类型说明
srcstr源队列名,从右端弹出
dststr目标队列名,推入左端
参数类型说明
keystr要操作的列表名
countint正数:从头部开始删 count 个;负数:从尾部开始删;0:删除全部匹配项
valuestr要删除的元素值

Q18:本项目如何监控队列积压?多少个任务算积压? ​

答:

用 LLEN 命令检查队列长度:

python
queue_len = redis_client.llen("task:queue")
if queue_len > 50:
    logger.warning("任务队列积压: %s 个待处理", queue_len)
积压数量严重程度说明
0-5正常Worker 能及时消费
5-20注意可能需要增加 Worker
20-50警告用户等待时间明显增加
50+严重需要立即增加 Worker 或检查是否有任务卡住

LLEN 的时间复杂度是 O(1),Redis 直接读取列表的长度字段,不遍历列表。


第七章:超时与竞争 ​

本章导读 ​

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

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

Q19:BRPOP 超时和 LPUSH 同时发生会怎样?会不会出现"既收到 nil 又拿到元素"的情况? ​

答:

不会。超时检查和 LPUSH 都在主线程的事件循环中串行执行,不可能"同时"发生。

情况 A:LPUSH 先执行

1. LPUSH 执行,发现 W1 在等待链表
2. 把元素发给 W1
3. 移除 W1
4. W1 超时定时器还在,但 W1 已不在等待链表
5. 超时触发时发现 W1 不在链表 → 跳过

结果:W1 拿到元素 ✅

情况 B:超时先执行

1. 超时触发,W1 从等待链表移除
2. 返回 nil 给 W1
3. LPUSH 执行,发现等待链表空(或只有 W2)
4. 元素留在列表中

结果:W1 收到 nil,元素留给下一个 BRPOP ✅

两种情况都不会出现"W1 既收到 nil 又拿到元素"或"元素丢失"。超时处理不会被 LPUSH 打断,LPUSH 也不会被超时处理打断。


Q20:LPUSH + BRPOP 实现 FIFO 队列,那 LPUSH + LPOP 是什么模型?为什么本项目不用 LPUSH + LPOP? ​

答:

推入弹出组合效果
LPUSHBRPOP/RPOP左推右弹FIFO(先进先出队列)
LPUSHLPOP/BLPOP左推左弹LIFO(后进先出栈)
RPUSHBLPOP/LPOP右推左弹FIFO(先进先出队列)
RPUSHBRPOP/RPOP右推右弹LIFO(后进先出栈)

本项目用 LPUSH + BRPOP = FIFO 队列:

LPUSH task:queue 6     ──►    [6]
LPUSH task:queue 7     ──►    [7, 6]
LPUSH task:queue 8     ──►    [8, 7, 6]
                              BRPOP ──►  取出 6(最早推入)
                             [8, 7]
                              BRPOP ──►  取出 7
                             [8]
                              BRPOP ──►  取出 8
                             []

如果用 LPUSH + LPOP(LIFO 栈),后提交的任务会先被消费,先提交的任务可能长时间不被处理,不公平。


第八章:综合场景 ​

本章导读 ​

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

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

Q21:100 个用户同时提交任务,从 BRPOP 到任务完成,完整经历了哪些步骤? ​

答:

时间 0s: 100 个 POST /creative/submit 同时到达
         100 个 task_id 被 LPUSH 到 task:queue
         Redis 列表: [100, 99, 98, ..., 2, 1]

时间 0s: Worker-1 BRPOP → 取出 1(FIFO 最早推入)
         Redis 等待链表: [W2, W3, ..., W10](9 个 Worker 继续等)
         列表: [100, 99, ..., 2]

         → 主线程执行 LPUSH 后,第1轮唤醒 W1
         → 第2轮唤醒 W2(取出 2)
         → 第3轮唤醒 W3(取出 3)
         → ...
         → 第10轮唤醒 W10(取出 10)
         → 列表: [100, 99, ..., 11](90 个任务积压)

时间 0s: W1~W10 各自处理任务 1~10
         W11~W100 在列表中等待

时间 4m: W1 完成任务 1,回到 process_queue,BRPOP → 取出 11
         W2 完成任务 2,BRPOP → 取出 12
         ...

时间 40m: 第 100 个任务被处理完成

关键:Redis 单线程在 LPUSH 后逐个唤醒 Worker,10 个 Worker 在几毫秒内全部被唤醒并拿到不同任务。瓶颈不在 Redis,而在 Worker 处理速度(每个任务 4 分钟)。


Q22:如果 Redis 服务重启了,等待链表和队列中的任务会怎样? ​

答:

全部丢失。Redis 重启后内存数据清空:

重启前:
  task:queue: [50, 49, 48, ..., 1]    ← 50 个待处理任务
  等待链表: [W1, W2, W3]               ← 3 个 Worker 在等

重启后:
  task:queue: []                       ← 空
  等待链表: []                          ← 空
  3 个 Worker 的 BRPOP 超时返回 nil

应对方案:

方案说明
AOF 持久化Redis 重启后从 AOF 文件恢复队列数据
RDB 快照定期快照,但可能丢失最近的任务
数据库兜底任务同时写入 PostgreSQL,Worker 启动时扫描数据库中 status=QUEUED 的任务重新入队

本项目的方案:任务同时存在于 PostgreSQL 和 Redis。如果 Redis 重启,可以从 PostgreSQL 恢复:

python
# Worker 启动时检查数据库中未完成的任务
def recover_tasks():
    db = SessionLocal()
    stuck_tasks = db.scalars(
        select(Task).where(Task.status.in_([
            TaskStatus.QUEUED,
            TaskStatus.SCRIPT,
            TaskStatus.IMAGE,
        ]))
    ).all()
    for task in stuck_tasks:
        redis_client.lpush("task:queue", task.id)
    db.close()

Q23:为什么本项目不用 RabbitMQ 或 Kafka,而是用 Redis BRPOP? ​

答:

维度Redis BRPOPRabbitMQKafka
部署复杂度低(已有 Redis)中(需独立部署)高(需 ZooKeeper + Broker)
原子性保证单线程串行Channel 级别锁分区单消费者
任务确认(ACK)无(需 RPOPLPUSH 弥补)内置 ACK 机制Offset 提交
延迟< 1ms~1ms~10ms
吞吐量10 万+ QPS万级 QPS百万级 QPS
适用规模中小规模(< 200 Worker)中大规模超大规模

选择 Redis 的原因:

  1. 项目已经依赖 Redis(进度缓存、审核队列),无需额外部署
  2. MVP 阶段任务量不大(每天几十到几百个任务),Redis 足够
  3. 部署简单,运维成本低
  4. BRPOP 延迟极低(< 1ms),满足实时性要求

何时考虑迁移:

  • Worker 超过 200 个,Redis 单线程开始成为瓶颈
  • 需要复杂的任务路由、重试、死信队列
  • 需要消息持久化保证(Redis 重启丢数据风险)

第九章:源码级深入 ​

本章导读 ​

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

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

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 多线程多连接的完整对比,以及"连接是按需创建的,不是一次性预创建"的源码验证。


第十章:Redis 常用数据结构与典型使用场景 ​

Q26:Redis 常用的数据结构有哪些?分别适合什么场景? ​

答:

Redis 不是简单的 key-value 缓存。它提供了多种数据结构,不同结构解决的问题不同。选型时不要只问“能不能存”,而要问:数据是否需要按字段更新、是否需要排序、是否需要去重、是否需要阻塞消费、是否需要可靠确认、是否允许近似统计、是否需要按地理位置查询。

Redis 常用数据结构总览图 ​

教科书式可编辑画板版总图: [redis-ch10-data-structures-overview.html](file:///Users/binny/workspace-sdd/aiNotes/redis/redis-ch10-data-structures-overview.html)

Excalidraw 源文件: [redis-ch10-data-structures-overview.excalidraw](file:///Users/binny/workspace-sdd/aiNotes/redis/redis-ch10-data-structures-overview.excalidraw)

下面保留一份纯文本版总览图,便于在不支持画板预览的 Markdown 阅读器中快速查看结构:

text
                         Redis
                           │
   ┌───────────────┬───────┼───────────────┬───────────────┐
   │               │       │               │               │
 String         Hash     List / Stream    Set / ZSet      其他模块
   │               │       │               │               │
单值缓存、计数   对象字段   队列/消息流   去重/排序/排名   JSON、搜索、图形化、统计
                              │               │               │
                           阻塞消费         会员关系      向量、过滤器、指标

可以先用下面这张表建立整体印象:

数据结构 / 能力核心特点最典型场景常用命令
String单 key 单 value,可做字符串、数字、JSON、二进制缓存、计数器、验证码、分布式锁GET、SET、INCR、SET NX EX
Hash一个 key 下多个 field用户资料、任务进度、商品详情HSET、HGET、HGETALL、HINCRBY
List有序列表,两端插入/弹出任务队列、最新列表、栈LPUSH、BRPOP、LRANGE、LTRIM
Set无序去重集合点赞用户、标签、共同关注SADD、SISMEMBER、SINTER
ZSet带 score 的有序集合排行榜、延迟队列、时间线排序ZADD、ZREVRANGE、ZRANGEBYSCORE
Stream带消息 ID 的日志型消息流可靠队列、消费组、事件日志XADD、XREADGROUP、XACK
Bitmapbit 位状态存储签到、活跃用户、灰度标记SETBIT、GETBIT、BITCOUNT
BitField在字符串中按位段读写整数多个小整数状态压缩存储BITFIELD
HyperLogLog大规模去重数量估算UV、访问人数估算PFADD、PFCOUNT
GEO经纬度位置索引附近门店、附近的人GEOADD、GEOSEARCH、GEODIST
Pub/Sub实时发布订阅实时通知、配置变更广播PUBLISH、SUBSCRIBE
RedisJSONJSON 文档读写,需模块支持复杂对象缓存、局部 JSON 更新JSON.SET、JSON.GET
Bloom Filter概率型存在性判断,需模块支持防缓存穿透、黑名单预判BF.ADD、BF.EXISTS
TimeSeries时间序列数据,需模块支持监控指标、埋点趋势TS.ADD、TS.RANGE
RedisSearch全文检索和二级索引,需模块支持商品搜索、文档搜索、任务检索FT.CREATE、FT.SEARCH
Vector Set / Vector Search向量相似度检索,需 Redis Stack / 新版本能力语义搜索、相似视频、RAG 检索FT.SEARCH、VSIM
Cuckoo Filter可删除的概率型存在性判断,需模块支持可删除黑名单、临时去重CF.ADD、CF.EXISTS、CF.DEL
Count-Min Sketch频次估算,需模块支持热点 key、热门搜索词、点击频次估算CMS.INCRBY、CMS.QUERY
TopK高频元素近似统计,需模块支持热门视频、热门标签、热门搜索词TOPK.ADD、TOPK.LIST
TDigest分位数统计,需模块支持P95/P99 延迟、耗时分布TDIGEST.ADD、TDIGEST.QUANTILE

Redis 常用数据结构的图形化、CLI 与案例一体化展示 ​

下面把每种数据结构的场景概述、图形化展示、redis-cli 命令和 Python 示例放在同一个小节里,便于横向对照阅读。

1. String:字符串 / 数值 / JSON 缓存 / 分布式锁 ​

场景概述: String 适合存放单个值、计数器、缓存和锁标记。

图形化展示:

text
user:1001  ──►  "{name: 'Tom', level: 3}"
video:6:view_count  ──►  "1024"
lock:task:6  ──►  "random-token"

redis-cli 命令:

bash
redis-cli SET task:6:status RUNNING EX 900
redis-cli GET task:6:status
redis-cli INCR video:6:view_count
redis-cli SET lock:task:6 abc123 NX EX 300

Python 示例:

python
redis_client.set("task:6:status", "RUNNING", ex=900)
redis_client.incr("video:6:view_count")

适合整体读写单个值、计数、验证码、登录态、分布式锁。

2. Hash:对象字段缓存 / 任务进度 / 用户信息 ​

场景概述: Hash 适合保存对象型数据并进行字段级更新。

图形化展示:

text
task:6:progress
  ├── stage   ──► IMAGE
  ├── percent ──► 45
  └── message ──► 正在生成分镜图

redis-cli 命令:

bash
redis-cli HSET task:6:progress stage IMAGE percent 45 message "正在生成分镜图"
redis-cli HGET task:6:progress stage
redis-cli HGETALL task:6:progress

Python 示例:

python
redis_client.hset("task:6:progress", mapping={"stage": "IMAGE", "percent": 45})
progress = redis_client.hgetall("task:6:progress")

适合一个对象有多个字段、并且经常局部更新。

3. List:队列 / 栈 / 最新列表 / 简单任务流 ​

场景概述: List 适合实现队列、栈和简单的消息流。

图形化展示:

text
task:queue
LPUSH 入队 -> [ task:9 ] -> [ task:8 ] -> [ task:7 ] -> [ task:6 ] <- BRPOP 出队

redis-cli 命令:

bash
redis-cli LPUSH task:queue 6
redis-cli BRPOP task:queue 5
redis-cli LRANGE user:1001:feeds 0 19

Python 示例:

python
redis_client.lpush("task:queue", 6)
result = redis_client.brpop("task:queue", timeout=5)

适合简单队列、最新列表、撤销栈。

4. Set:去重集合 / 标签 / 点赞 / 关系运算 ​

场景概述: Set 适合去重、标签关系和集合运算。

与 ZSet 的区别:

维度SetZSet
底层数据结构哈希表(dict)跳表(skiplist)+ 字典(dict)
是否去重是是
是否有序否是,按 score 排序
元素结构只有 membermember + score
典型用途去重、标签、共同关注排行榜、延迟队列、时间线
适合“只关心是否存在”是也可以,但通常没必要
适合“要按顺序取前 N 个”不适合很适合

Set 更像“成员名单”,ZSet 更像“带分数的排名名单”。

底层结构说明:

Set 内部通常使用哈希表存储成员,查找、插入、删除平均都是 O(1)。 ZSet 同时维护字典和跳表:字典负责快速按 member 查找,跳表负责按 score 排序和范围查询。

图形化展示:

text
video:6:liked_users
{ 1001, 1002, 1003, 1008 }

redis-cli 命令:

bash
redis-cli SADD video:6:liked_users 1001 1002 1003
redis-cli SISMEMBER video:6:liked_users 1001
redis-cli SINTER user:1:follows user:2:follows

Python 示例:

python
redis_client.sadd("video:6:liked_users", 1001, 1002, 1003)
liked = redis_client.sismember("video:6:liked_users", 1001)

适合去重、共同关注、抽奖用户池。

5. ZSet:排行榜 / 权重排序 / 延迟队列 / 时间线 ​

场景概述: ZSet 适合排行榜、延迟任务和按分值排序。

与 Set 的区别:

维度SetZSet
是否去重是是
是否有序否是,按 score 排序
元素结构只有 membermember + score
典型用途去重、标签、共同关注排行榜、延迟队列、时间线
取前 N 个元素只能随机或集合运算可以按分值直接取前 N 名

Set 更像“无序去重名单”,ZSet 更像“带分数的排序名单”。

字母 z 的来源:

ZSet 全名是 Sorted Set。Redis 早期命名时为了和 Set 区分,就在前面加了一个 z,表示 sorted,所以 zset 可以理解为“有序集合”。

图形化展示:

text
rank:video:hot
98.5  ──► video:8
91.0  ──► video:3
76.5  ──► video:6

redis-cli 命令:

bash
redis-cli ZADD rank:video:hot 98.5 video:8 91.0 video:3 76.5 video:6
redis-cli ZREVRANGE rank:video:hot 0 9 WITHSCORES
redis-cli ZADD delay:order:cancel 1787797000 order:1001
redis-cli ZRANGEBYSCORE delay:order:cancel -inf 1787797060

Python 示例:

python
redis_client.zadd("delay:order:cancel", {"order:1001": 1787797000})
expired = redis_client.zrangebyscore("delay:order:cancel", "-inf", 1787797060)

适合排行榜、延迟任务、订单超时取消。

6. Stream:可靠消息流 / 消费组 / 事件日志 ​

场景概述: Stream 适合可靠消息流、消费组和事件追踪。

图形化展示:

text
stream:task
1690000000000-0  ──► task_id=6, stage=SCRIPT
1690000001000-0  ──► task_id=6, stage=IMAGE

redis-cli 命令:

bash
redis-cli XADD stream:task * task_id 6 stage SCRIPT
redis-cli XGROUP CREATE stream:task video-workers 0 MKSTREAM
redis-cli XREADGROUP GROUP video-workers worker-1 COUNT 10 BLOCK 5000 STREAMS stream:task >
redis-cli XACK stream:task video-workers 1690000000000-0

Python 示例:

python
redis_client.xadd("stream:task", {"task_id": "6", "stage": "SCRIPT"})

适合可靠消息流、事件日志、异步任务消费。

7. Bitmap:签到 / 活跃用户 / 布尔状态压缩存储 ​

场景概述: Bitmap 适合高密度布尔状态和签到统计。

图形化展示:

text
active:2026-08-27
用户偏移: 1001 1002 1003 1004
bit 值:     1    0    1    1

redis-cli 命令:

bash
redis-cli SETBIT active:2026-08-27 1001 1
redis-cli GETBIT active:2026-08-27 1001
redis-cli BITCOUNT active:2026-08-27

Python 示例:

python
redis_client.setbit("active:2026-08-27", 1001, 1)
count = redis_client.bitcount("active:2026-08-27")

适合签到、活跃用户、开关状态。

8. BitField:多个小整数状态压缩存储 ​

场景概述: BitField 适合压缩存储多个小整数状态。

图形化展示:

text
score:user:1001:2026-08
| day1 4bit | day2 4bit | day3 4bit |
|     8     |     6     |     9     |

redis-cli 命令:

bash
redis-cli BITFIELD score:user:1001:2026-08 SET u4 0 8 SET u4 4 6 GET u4 0 GET u4 4

Python 示例:

python
redis_client.execute_command("BITFIELD", "score:user:1001:2026-08", "SET", "u4", 0, 8)

适合评分、等级、小整数压缩。

9. HyperLogLog:大规模 UV 去重估算 ​

场景概述: HyperLogLog 适合大规模去重计数和 UV 估算。

图形化展示:

text
uv:video:6
1001, 1002, 1003, 1001, 1008, 1002
        └── HLL 估算唯一访问数

redis-cli 命令:

bash
redis-cli PFADD uv:video:6 1001 1002 1003
redis-cli PFCOUNT uv:video:6
redis-cli PFMERGE uv:site:range uv:day1 uv:day2

Python 示例:

python
redis_client.pfadd("uv:video:6", 1001, 1002, 1003)
uv = redis_client.pfcount("uv:video:6")

适合 UV 估算、访问人数统计。

10. GEO:附近的人 / 门店距离 / 地理位置查询 ​

场景概述: GEO 适合附近位置查询和距离计算。

图形化展示:

text
geo:stores
store:1 ──► (116.397128, 39.916527)
store:2 ──► (116.410000, 39.920000)

redis-cli 命令:

bash
redis-cli GEOADD geo:stores 116.397128 39.916527 store:1
redis-cli GEOSEARCH geo:stores FROMLONLAT 116.4 39.9 BYRADIUS 5 km WITHDIST
redis-cli GEODIST geo:stores store:1 store:2 km

Python 示例:

python
redis_client.geoadd("geo:stores", {"store:1": (116.397128, 39.916527)})

适合附近门店、同城服务、地理围栏。

11. Pub/Sub:实时发布订阅 / 通知广播 ​

场景概述: Pub/Sub 适合实时广播和轻量通知。

图形化展示:

text
PUBLISH config:changed
        ├── 订阅者 A
        ├── 订阅者 B
        └── 订阅者 C

redis-cli 命令:

bash
redis-cli SUBSCRIBE config:changed
redis-cli PUBLISH config:changed "video:max_workers"

Python 示例:

python
redis_client.publish("config:changed", "video:max_workers")

适合实时广播、配置变更通知。

12. RedisJSON:复杂 JSON 文档缓存 ​

场景概述: RedisJSON 适合嵌套 JSON 文档和局部更新。

图形化展示:

text
json:task:6:config
{ model, ratio, frames[] }

redis-cli 命令:

bash
redis-cli JSON.SET json:task:6:config $ '{"model":"seedance-2.0","ratio":"16:9"}'
redis-cli JSON.GET json:task:6:config $

Python 示例:

python
redis_client.execute_command("JSON.SET", "json:task:6:config", "$", '{"model":"seedance-2.0"}')

适合嵌套 JSON 配置、局部更新。

13. Bloom Filter:存在性预判 / 防缓存穿透 ​

场景概述: Bloom Filter 适合做存在性预判和缓存穿透防护。

图形化展示:

text
bf:goods:ids
已加入: goods:1001, goods:1002, goods:1003

redis-cli 命令:

bash
redis-cli BF.ADD bf:goods:ids goods:1001
redis-cli BF.EXISTS bf:goods:ids goods:1002

Python 示例:

python
redis_client.execute_command("BF.ADD", "bf:goods:ids", "goods:1001")
exists = redis_client.execute_command("BF.EXISTS", "bf:goods:ids", "goods:1002")

适合防缓存穿透、黑名单预判。

14. TimeSeries:监控指标 / 趋势数据 ​

场景概述: TimeSeries 适合记录时间序列指标和趋势。

图形化展示:

text
ts:queue:task:length
1787796000000 -> 12
1787796060000 -> 18

redis-cli 命令:

bash
redis-cli TS.ADD ts:queue:task:length * 12
redis-cli TS.RANGE ts:queue:task:length - +

Python 示例:

python
redis_client.execute_command("TS.ADD", "ts:queue:task:length", int(time.time() * 1000), 12)

适合队列长度、QPS、耗时趋势。

15. RedisSearch:全文检索 / 二级索引 / 多条件筛选 ​

场景概述: RedisSearch 适合全文检索、索引和组合筛选。

图形化展示:

text
idx:tasks
  ├── title 可搜
  ├── stage 可筛选
  └── user_id 可过滤

redis-cli 命令:

bash
redis-cli FT.CREATE idx:tasks ON HASH PREFIX 1 task:search: SCHEMA title TEXT stage TAG user_id NUMERIC
redis-cli FT.SEARCH idx:tasks "@stage:{IMAGE} 海边"

Python 示例:

python
redis_client.execute_command("FT.SEARCH", "idx:tasks", "@stage:{IMAGE} 海边")

适合全文检索和组合筛选。

16. Vector Search:向量相似度检索 ​

场景概述: Vector Search 适合相似检索和语义搜索。

图形化展示:

text
video:6 -> [0.12, 0.88, 0.31]
video:7 -> [0.10, 0.86, 0.35]
查询向量 q -> 找最相似的视频

redis-cli 命令:

bash
redis-cli FT.CREATE idx:vec ON HASH PREFIX 1 video: SCHEMA embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 3 DISTANCE_METRIC COSINE
redis-cli FT.SEARCH idx:vec '*=>[KNN 3 @embedding $vec]' PARAMS 2 vec "..." SORTBY __vector_score DIALECT 2

Python 示例:

python
redis_client.execute_command("FT.SEARCH", "idx:vec", "*=>[KNN 3 @embedding $vec]")

适合相似视频推荐、语义搜索、RAG 检索。

17. Cuckoo Filter:可删除的存在性判断 ​

场景概述: Cuckoo Filter 适合可删除的去重和存在性判断。

图形化展示:

text
cf:task:ids
CF.ADD task:6
CF.EXISTS task:6
CF.DEL task:6

redis-cli 命令:

bash
redis-cli CF.ADD cf:task:ids task:6
redis-cli CF.EXISTS cf:task:ids task:6
redis-cli CF.DEL cf:task:ids task:6

Python 示例:

python
redis_client.execute_command("CF.ADD", "cf:task:ids", "task:6")

适合可删除去重、临时黑名单。

18. Count-Min Sketch:大规模频次估算 ​

场景概述: Count-Min Sketch 适合大规模频次估算。

图形化展示:

text
cms:keywords
redis -> 12000
video -> 8500
worker -> 4300

redis-cli 命令:

bash
redis-cli CMS.INCRBY cms:keywords redis 1 video 1 worker 1
redis-cli CMS.QUERY cms:keywords redis video worker

Python 示例:

python
redis_client.execute_command("CMS.INCRBY", "cms:keywords", "redis", 1)

适合热点词、频次趋势。

19. TopK:高频元素近似统计 ​

场景概述: TopK 适合近似统计高频元素。

图形化展示:

text
topk:videos
video:8, video:6, video:3, video:12, video:1

redis-cli 命令:

bash
redis-cli TOPK.RESERVE topk:videos 5
redis-cli TOPK.ADD topk:videos video:8 video:6 video:3
redis-cli TOPK.LIST topk:videos

Python 示例:

python
redis_client.execute_command("TOPK.ADD", "topk:videos", "video:8", "video:6")

适合热门视频、热门词榜单。

20. TDigest:分位数统计 / P95 / P99 ​

场景概述: TDigest 适合分位数和延迟分布统计。

图形化展示:

text
td:video:duration
样本 -> P50 / P95 / P99

redis-cli 命令:

bash
redis-cli TDIGEST.CREATE td:video:duration
redis-cli TDIGEST.ADD td:video:duration 2.1 2.4 3.0 8.2 15.6 30.1
redis-cli TDIGEST.QUANTILE td:video:duration 0.95

Python 示例:

python
redis_client.execute_command("TDIGEST.ADD", "td:video:duration", 2.1, 2.4, 3.0)

适合延迟分布、耗时分位数。

15. 使用场景中函数参数含义速查 ​

第十章中的示例代码涉及较多 Redis 命令和 Python 辅助函数。阅读这些案例时,重点不是只记住函数名,而是理解每个参数对应 Redis 中的 key、value、field、score、member、offset、过期时间或消费组语义。

String / 通用 key 操作

函数示例参数含义
redis_client.set(name, value, ex=None, nx=False)set("task:6:status", "RUNNING", ex=900)name 是 Redis key;value 是要保存的值;ex 是秒级过期时间;nx=True 表示 key 不存在时才写入,常用于分布式锁
redis_client.get(name)get("task:6:status")name 是要读取的 Redis key,返回该 key 当前保存的 value
redis_client.incr(name, amount=1)incr("video:6:view_count")name 是计数器 key;amount 是递增步长,默认加 1
redis_client.expire(name, time)expire(key, 60)name 是要设置过期时间的 key;time 是过期秒数
redis_client.delete(*names)delete(lock_key)names 是一个或多个要删除的 key。分布式锁中不建议直接删除,容易误删别人的锁
redis_client.eval(script, numkeys, *keys_and_args)eval(UNLOCK_SCRIPT, 1, lock_key, lock_token)script 是 Lua 脚本;numkeys 表示后面有几个参数属于 Redis key;前 numkeys 个参数进入 Lua 的 KEYS;剩余参数进入 Lua 的 ARGV

Hash 操作

函数示例参数含义
redis_client.hset(name, key=None, value=None, mapping=None)hset("task:6:progress", "stage", "IMAGE")name 是 Hash key;key 是字段名 field;value 是字段值;mapping 可一次写入多个 field-value
redis_client.hget(name, key)hget(f"order:{oid}", "status")name 是 Hash key;key 是要读取的字段名
redis_client.hgetall(name)hgetall("task:6:progress")name 是 Hash key,返回这个 Hash 下所有 field-value
redis_client.hincrby(name, key, amount=1)hincrby("cart:user:1001", "goods:2001", 1)name 是 Hash key;key 是字段名;amount 是该字段的整数增量

List 操作

函数示例参数含义
redis_client.lpush(name, *values)lpush("task:queue", 6)name 是 List key;values 是要从左侧压入的一个或多个元素
redis_client.brpop(keys, timeout=0)brpop("task:queue", timeout=5)keys 是一个 key 或多个 key;timeout 是最大阻塞等待秒数,0 表示一直等待
redis_client.lrange(name, start, end)lrange("user:1001:feeds", 0, 19)name 是 List key;start 是起始下标;end 是结束下标,包含该位置,-1 表示最后一个元素
redis_client.ltrim(name, start, end)ltrim("user:1001:feeds", 0, 19)name 是 List key;只保留 start 到 end 范围内的元素,常用于限制最新列表长度
redis_client.lpop(name)lpop("undo:user:1001")name 是 List key,从左侧弹出一个元素
redis_client.llen(name)llen("task:queue")name 是 List key,返回列表长度

Set 操作

函数示例参数含义
redis_client.sadd(name, *values)sadd("video:6:liked_users", 1001)name 是 Set key;values 是要加入集合的成员,重复成员会自动去重
redis_client.sismember(name, value)sismember("video:6:liked_users", 1001)name 是 Set key;value 是要判断是否存在的成员
redis_client.scard(name)scard("video:6:liked_users")name 是 Set key,返回集合成员数量
redis_client.sinter(keys, *args)sinter("user:1:follows", "user:2:follows")参数是多个 Set key,返回这些集合的交集
redis_client.srandmember(name, number=None)srandmember("lottery:202608:users", 2)name 是 Set key;number 是随机返回的成员数量

ZSet 操作

函数示例参数含义
redis_client.zadd(name, mapping)zadd("delay:order:cancel", {order_id: expire_at})name 是 ZSet key;mapping 是 {member: score} 字典,member 是成员,score 是排序分数
redis_client.zincrby(name, amount, value)zincrby("rank:video:hot", 1, "video:6")name 是 ZSet key;amount 是分数增量;value 是要增加分数的成员
redis_client.zrevrange(name, start, end, withscores=False)zrevrange("rank:video:hot", 0, 9, withscores=True)name 是 ZSet key;start/end 是排名范围;withscores=True 表示同时返回 score
redis_client.zrevrank(name, value)zrevrank("rank:video:hot", "video:6")name 是 ZSet key;value 是成员,返回从高到低排序时的名次
redis_client.zrangebyscore(name, min, max, start=None, num=None)zrangebyscore("delay:tasks", 0, now, start=0, num=10)name 是 ZSet key;min/max 是 score 范围;start 是偏移量;num 是最多返回数量
redis_client.zrem(name, *values)zrem("delay:order:cancel", oid)name 是 ZSet key;values 是要删除的成员。多 Worker 场景中,谁 ZREM 成功谁获得处理权

Stream 操作

函数示例参数含义
redis_client.xadd(name, fields, id="*")xadd("stream:task", {"task_id": "6"})name 是 Stream key;fields 是消息字段字典;id="*" 表示 Redis 自动生成消息 ID
redis_client.xgroup_create(name, groupname, id="0", mkstream=False)xgroup_create("stream:task", "video-workers", id="0", mkstream=True)name 是 Stream key;groupname 是消费组名;id 表示从哪个消息 ID 开始消费;mkstream=True 表示 Stream 不存在时自动创建
redis_client.xreadgroup(groupname, consumername, streams, count=None, block=None)xreadgroup("video-workers", "worker-1", {"stream:task": ">"}, count=10, block=5000)groupname 是消费组;consumername 是消费者名;streams 指定要读的 Stream 和起始 ID;count 是最多读取条数;block 是阻塞等待毫秒数
redis_client.xack(name, groupname, *ids)xack("stream:task", "video-workers", message_id)name 是 Stream key;groupname 是消费组;ids 是要确认完成的消息 ID
redis_client.xrange(name, min="-", max="+")xrange("stream:video:event", min="-", max="+")name 是 Stream key;min/max 是消息 ID 范围,- 表示最早,+ 表示最新

Bitmap / BitField 操作

函数示例参数含义
redis_client.setbit(name, offset, value)setbit("active:2026-08-27", 1001, 1)name 是 Bitmap key;offset 是 bit 下标;value 只能是 0 或 1
redis_client.getbit(name, offset)getbit("signin:2026-08:user:1001", day_index)name 是 Bitmap key;offset 是要读取的 bit 下标
redis_client.bitcount(key)bitcount("active:2026-08-27")key 是 Bitmap key,返回值为 1 的 bit 数量
redis_client.bitop(operation, dest, *keys)bitop("AND", "active:and", "active:day1", "active:day2")operation 是位运算类型,如 AND/OR/XOR/NOT;dest 是结果 key;keys 是参与运算的源 key
redis_client.bitfield(key, *args)bitfield("score:user:1001:2026-08", "SET", "u4", 26 * 4, 9)key 是底层字符串 key;args 是 BitField 子命令序列,例如 SET/GET、整数类型 u4、bit 偏移量和写入值

HyperLogLog / GEO / Pub/Sub 操作

函数示例参数含义
redis_client.pfadd(name, *values)pfadd("uv:video:6", 1001, 1002, 1003)name 是 HyperLogLog key;values 是要参与去重估算的元素
redis_client.pfcount(*sources)pfcount("uv:video:6")sources 是一个或多个 HyperLogLog key,返回不重复数量估算值
redis_client.pfmerge(dest, *sources)pfmerge("uv:site:range", "uv:day1", "uv:day2")dest 是合并后的目标 key;sources 是要合并的多个 HyperLogLog key
redis_client.geoadd(name, values)geoadd("geo:stores", [116.397128, 39.916527, "store:1"])name 是 GEO key;values 按经度、纬度、成员名传入
redis_client.geosearch(name, longitude=None, latitude=None, member=None, radius=None, unit="m")geosearch("geo:stores", longitude=116.4, latitude=39.9, radius=5, unit="km")name 是 GEO key;可用经纬度或已有成员作为中心点;radius 是半径;unit 是距离单位
redis_client.geodist(name, place1, place2, unit=None)geodist("geo:stores", "store:1", "store:2", unit="km")name 是 GEO key;place1/place2 是两个成员;unit 是返回距离单位
redis_client.publish(channel, message)publish("config:changed", "video:max_workers")channel 是频道名;message 是要发布的消息内容
redis_client.pubsub()pubsub()创建一个 Pub/Sub 订阅对象,后续可通过该对象订阅频道并读取消息

Redis Stack / 模块命令

函数示例参数含义
redis_client.execute_command("JSON.SET", key, path, value)execute_command("JSON.SET", "json:task:6:config", ".", json_text)第 1 个参数是模块命令名;key 是 JSON key;path 是 JSON 路径;value 是要写入的 JSON 字符串
redis_client.execute_command("JSON.GET", key, path)execute_command("JSON.GET", "json:task:6:config", ".")key 是 JSON key;path 是要读取的 JSON 路径
redis_client.execute_command("BF.ADD", key, item)execute_command("BF.ADD", "bf:goods:ids", "goods:2001")key 是 Bloom Filter key;item 是要加入过滤器的元素
redis_client.execute_command("BF.EXISTS", key, item)execute_command("BF.EXISTS", "bf:goods:ids", "goods:9999")key 是 Bloom Filter key;item 是要判断是否可能存在的元素
redis_client.execute_command("TS.ADD", key, timestamp, value)execute_command("TS.ADD", "ts:queue:task:length", ts, queue_length)key 是时间序列 key;timestamp 是毫秒时间戳;value 是指标值
redis_client.execute_command("TS.RANGE", key, from_ts, to_ts, ...)execute_command("TS.RANGE", "ts:queue:task:length", "-", "+")key 是时间序列 key;from_ts/to_ts 是时间范围,- 到 + 表示全量范围
redis_client.execute_command("FT.CREATE", index, ...)execute_command("FT.CREATE", "idx:task", "ON", "HASH", ...)index 是索引名;后续参数定义索引数据来源、前缀、字段类型和排序规则
redis_client.execute_command("FT.SEARCH", index, query, ...)execute_command("FT.SEARCH", "idx:task", "@stage:{IMAGE}")index 是索引名;query 是查询表达式;后续参数可指定排序、分页、返回字段等
redis_client.execute_command("CF.ADD", key, item)execute_command("CF.ADD", "cf:task:ids", "task:6")key 是 Cuckoo Filter key;item 是要加入的元素
redis_client.execute_command("CF.EXISTS", key, item)execute_command("CF.EXISTS", "cf:task:ids", "task:6")key 是 Cuckoo Filter key;item 是要判断是否可能存在的元素
redis_client.execute_command("CF.DEL", key, item)execute_command("CF.DEL", "cf:task:ids", "task:6")key 是 Cuckoo Filter key;item 是要从过滤器中删除的元素
redis_client.execute_command("CMS.INCRBY", key, item, increment)execute_command("CMS.INCRBY", "cms:keywords", "redis", 3)key 是 Count-Min Sketch key;item 是统计对象;increment 是增加次数
redis_client.execute_command("CMS.QUERY", key, *items)execute_command("CMS.QUERY", "cms:keywords", "video", "redis")key 是 Count-Min Sketch key;items 是要查询频次的多个元素
redis_client.execute_command("TOPK.RESERVE", key, k, width, depth, decay)execute_command("TOPK.RESERVE", "topk:videos", 10, 2000, 7, 0.9)key 是 TopK key;k 是保留前多少名;width/depth/decay 控制近似统计的空间、精度和衰减策略
redis_client.execute_command("TOPK.ADD", key, *items)execute_command("TOPK.ADD", "topk:videos", "video:1")key 是 TopK key;items 是要计入高频统计的元素
redis_client.execute_command("TOPK.LIST", key)execute_command("TOPK.LIST", "topk:videos")key 是 TopK key,返回当前近似 TopK 列表
redis_client.execute_command("TDIGEST.CREATE", key)execute_command("TDIGEST.CREATE", "td:video:duration")key 是 TDigest key,用于保存分位数统计结构
redis_client.execute_command("TDIGEST.ADD", key, value)execute_command("TDIGEST.ADD", "td:video:duration", 12.5)key 是 TDigest key;value 是要加入统计的数值样本
redis_client.execute_command("TDIGEST.QUANTILE", key, quantile)execute_command("TDIGEST.QUANTILE", "td:video:duration", 0.95)key 是 TDigest key;quantile 是分位点,例如 0.95 表示 P95

Python 辅助函数

函数示例参数含义
time.time()int(time.time()) + 300不需要参数,返回当前 Unix 时间戳,单位是秒,常用于计算延迟任务执行时间
int(value)int(time.time())value 是要转换成整数的值,常用于把时间戳或 Redis 返回的字符串任务 ID 转成整数
str(value)str(uuid.uuid4())value 是要转换成字符串的对象,分布式锁中用于把 UUID 转成可保存的 token 字符串
uuid.uuid4()uuid.uuid4()不需要参数,生成随机 UUID,适合作为分布式锁的唯一 lock_token

16. 本项目中的推荐使用方式 ​

结合当前视频任务系统,可以这样选择:

业务数据推荐结构原因
待处理任务队列ListLPUSH + BRPOP 简单、高效,适合 Worker 阻塞消费
任务执行进度Hash可以单独更新 stage、percent、message 等字段
任务状态缓存String / Hash简单状态用 String,多字段状态用 Hash
用户提交频率限制String使用 INCR + EXPIRE 实现限流计数
视频播放量String使用 INCR 做计数器
视频点赞用户Set自动去重,方便判断用户是否点过赞
视频热度排行ZSet用热度分或播放量作为 score 排序
延迟重试任务ZSet / Stream简单延迟用 ZSet,需要 ACK 和恢复用 Stream
可靠事件日志Stream如果后续需要 ACK、重试、消费组,可从 List 升级到 Stream
在线配置广播Pub/Sub通知在线服务刷新配置,消息不需要持久化
队列积压趋势TimeSeries / 普通指标系统如果需要趋势图和聚合,可以记录时间序列
防止无效 ID 穿透数据库Bloom Filter大规模 ID 存在性预判,减少无效 DB 查询

17. 选型口诀表 ​

需求关键词优先选择典型命令示例场景注意点
一个 key 存一个完整值StringSET、GET、EXPIRE验证码、登录 token、任务状态缓存适合整体读写,不适合频繁局部更新对象字段
原子计数、自增自减StringINCR、DECR、INCRBY播放量、点赞数、接口调用次数计数器可以配合 EXPIRE 做限流窗口
一个对象有多个字段HashHSET、HGET、HGETALL用户资料、任务进度、视频元信息适合字段级更新,不适合按字段值排序
简单队列、阻塞消费ListLPUSH、BRPOP视频任务队列、异步任务分发适合简单队列,不支持 ACK 和消费组
最新列表、操作历史ListLPUSH、LRANGE、LTRIM最新动态、最近操作、撤销栈要配合 LTRIM 控制列表长度
去重、判断是否存在SetSADD、SISMEMBER、SCARD点赞用户、抽奖参与用户、黑名单无序集合,不适合按时间或分数排序
集合关系运算SetSINTER、SUNION、SDIFF共同关注、共同标签、权限对比数据量很大时要注意集合运算成本
排名、权重、分数排序ZSetZADD、ZREVRANGE、ZREVRANK视频热榜、用户积分榜、优先级队列score 是排序核心,更新频繁时要评估写入成本
时间戳排序、延迟执行ZSetZADD、ZRANGEBYSCORE、ZREM延迟任务、最近访问、定时重试多 Worker 取任务时要用 ZREM 或 Lua 保证不重复执行
可靠消息、消费组、ACKStreamXADD、XREADGROUP、XACK可靠任务队列、订单事件、视频生成事件流比 List 复杂,但支持 pending、ACK 和故障恢复
大量是/否状态BitmapSETBIT、GETBIT、BITCOUNT用户签到、日活统计、功能灰度offset 要可控,稀疏大 ID 可能浪费空间
多个小整数状态压缩BitFieldBITFIELD每日评分、设备状态档位、小型状态机可读性较差,适合内存极度敏感场景
只统计不重复数量,不关心成员HyperLogLogPFADD、PFCOUNT、PFMERGE网站 UV、视频 UV、活动访问人数结果是估算值,不能列出具体成员
经纬度附近查询GEOGEOADD、GEOSEARCH、GEODIST附近门店、附近设备、同城资源适合基础附近查询,不替代专业 GIS
在线实时广播Pub/SubPUBLISH、SUBSCRIBE配置变更通知、WebSocket 网关广播消息不持久化,订阅者离线会丢消息
复杂 JSON 局部更新RedisJSONJSON.SET、JSON.GET复杂配置、用户画像、商品详情缓存需要 RedisJSON 模块支持,标准 Redis 默认没有
大规模存在性预判Bloom FilterBF.ADD、BF.EXISTS防缓存穿透、URL 去重、风险名单预判有误判率,不能替代精确集合
指标趋势和时间范围查询TimeSeriesTS.ADD、TS.RANGE队列长度趋势、接口 QPS、任务耗时监控需要 RedisTimeSeries 模块支持,也可交给专业监控系统

第十一章:Python 可运行案例 ​

Q28:如何让文档里的 Redis 示例连接到本机 6379? ​

答:

当前示例统一把 Redis 连接信息写到 redis/python示例/.env,并通过 python-dotenv 自动加载。默认连接本机 127.0.0.1:6379,密码从 .env 中的 REDIS_PASSWORD 读取。

运行前先安装依赖:

bash
python3 -m pip install -r /Users/binny/workspace-sdd/aiNotes/g; srequirements.txt

.env 文件内容如下:

bash
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DB=0
REDIS_PASSWORD='你的 Redis 密码'

所有示例共享同一个连接入口:

python
import os
from pathlib import Path

from dotenv import load_dotenv
from redis import Redis

ENV_PATH = Path(__file__).resolve().parent / '.env'
load_dotenv(ENV_PATH)


def get_client(decode_responses: bool = True) -> Redis:
    password = os.environ.get('REDIS_PASSWORD')
    if not password:
        raise RuntimeError(f'请先在 {ENV_PATH} 中配置 REDIS_PASSWORD')
    return Redis(
        host=os.environ.get('REDIS_HOST', '127.0.0.1'),
        port=int(os.environ.get('REDIS_PORT', '6379')),
        password=password,
        db=int(os.environ.get('REDIS_DB', '0')),
        decode_responses=decode_responses,
        socket_connect_timeout=3,
        socket_timeout=5,
        health_check_interval=30,
    )

对应文件:[redis_connection.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/redis_connection.py)

这个写法符合 Redis 生产实践中的几条基本原则:

  • .env 由 python-dotenv 加载,运行脚本时无需再手动 export;
  • .gitignore 已忽略 .env,避免把真实密码提交到仓库;
  • 给连接设置 socket_connect_timeout 和 socket_timeout,避免网络异常时无限等待;
  • 使用 decode_responses=True,让命令返回字符串,便于教学演示;
  • 所有示例使用 demo:* 前缀,方便清理,避免误删真实业务 key。

如果运行时出现 invalid password,说明 Python 已经连到 Redis 服务,但 .env 中的 REDIS_PASSWORD 与当前 REDIS_HOST:REDIS_PORT 指向的 Redis 实例不匹配,需要确认本机 6379 是否就是目标 Redis,以及密码是否为该实例的实际认证密码。

Q29:String 如何演示缓存、计数器和分布式锁? ​

答:

String 适合存简单状态、缓存值、计数器和轻量锁。示例会真实执行 SET EX、GET、INCR、SET NX EX。

运行方式:

bash
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/string_cache_and_lock.py

核心代码:

python
from redis_connection import flush_demo_keys, run


def demo(client):
    flush_demo_keys(client, 'demo:string:*', 'demo:lock:*')
    client.set('demo:string:task:6:status', 'RUNNING', ex=900)
    print(client.get('demo:string:task:6:status'))
    print(client.incr('demo:string:video:6:view_count'))
    locked = client.set('demo:lock:task:6', 'abc123', nx=True, ex=300)
    print(locked)
    print(client.get('demo:lock:task:6'))

对应文件:[string_cache_and_lock.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/string_cache_and_lock.py)

Q30:Hash 如何演示任务进度? ​

答:

Hash 适合同一个实体有多个字段的场景,比如任务进度、用户资料、订单状态。它比把整个对象序列化成 String 更适合局部字段更新。

运行方式:

bash
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/hash_progress.py

核心代码:

python
from redis_connection import flush_demo_keys, run


def demo(client):
    key = 'demo:hash:task:6:progress'
    flush_demo_keys(client, key)
    client.hset(key, mapping={'stage': 'IMAGE', 'percent': '45', 'message': '正在生成分镜图'})
    print(client.hget(key, 'stage'))
    print(client.hgetall(key))
    client.hincrby(key, 'percent', 10)
    print(client.hgetall(key))

对应文件:[hash_progress.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/hash_progress.py)

Q31:List 与 BRPOP 如何演示任务队列? ​

答:

List 的典型用法是队列。LPUSH 负责投递任务,BRPOP 负责阻塞等待任务,适合文档前面讲到的 Worker 模型。

运行方式:

bash
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/list_queue_and_latest.py
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/brpop_worker_demo.py

核心代码:

python
import threading
import time

from redis_connection import flush_demo_keys, get_client, run


def demo(client):
    queue_key = 'demo:brpop:task:queue'
    flush_demo_keys(client, queue_key)
    result_holder = []

    def worker_loop():
        worker_client = get_client()
        result_holder.append(worker_client.brpop(queue_key, timeout=5))

    thread = threading.Thread(target=worker_loop)
    thread.start()
    time.sleep(0.5)
    client.lpush(queue_key, 'task-6')
    thread.join(timeout=6)
    print(result_holder)

对应文件:

  • [list_queue_and_latest.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/list_queue_and_latest.py)
  • [brpop_worker_demo.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/brpop_worker_demo.py)

Q32:Set、ZSet、Bitmap、HyperLogLog、Geo 分别适合演示什么? ​

答:

这些结构适合业务侧高频场景:

数据结构适合场景示例文件
Set去重、点赞、共同关注[set_dedup_and_relation.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/set_dedup_and_relation.py)
ZSet排行榜、延迟任务[zset_ranking_and_delay.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/zset_ranking_and_delay.py)
Bitmap签到、布尔状态统计[bitmap_signin.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/bitmap_signin.py)
HyperLogLogUV 去重估算[hll_uv.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/hll_uv.py)
Geo附近门店、地理位置检索[geo_nearby_search.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/geo_nearby_search.py)

运行方式示例:

bash
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/set_dedup_and_relation.py
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/zset_ranking_and_delay.py
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/bitmap_signin.py
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/hll_uv.py
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/geo_nearby_search.py

Q33:Stream 和 Pub/Sub 的可运行案例有什么区别? ​

答:

Stream 适合可靠消息队列,消息会保存在流里,可以配合消费者组、ACK、Pending List 使用;Pub/Sub 适合在线广播,不保存历史消息,订阅者离线时会错过消息。

运行方式:

bash
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/stream_consumer_group.py
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/pubsub_broadcast.py

对应文件:

  • [stream_consumer_group.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/stream_consumer_group.py)
  • [pubsub_broadcast.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/pubsub_broadcast.py)

Q34:连接池如何演示? ​

答:

Redis 客户端通常不会每次命令都新建 TCP 连接,而是复用连接池。示例中使用 BlockingConnectionPool,限制最大连接数,并设置获取连接的等待超时时间。

运行方式:

bash
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/connection_pool_demo.py

核心代码:

python
from redis import BlockingConnectionPool, Redis

from redis_connection import run


def demo(client):
    source_pool = client.connection_pool
    kwargs = source_pool.connection_kwargs.copy()
    pool = BlockingConnectionPool(max_connections=10, timeout=3, **kwargs)
    pooled_client = Redis(connection_pool=pool, decode_responses=True)
    print(pooled_client.ping())

对应文件:[connection_pool_demo.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/connection_pool_demo.py)

Q35:哪些案例依赖 Redis Stack 或额外模块? ​

答:

以下案例已经写成“可运行但可降级”的形式:如果你的 Redis 只安装了基础服务,它们会提示模块不可用;如果安装了 Redis Stack 或对应模块,就会真实执行命令。

示例依赖能力示例文件
RedisJSONRedisJSON 模块[redisjson_document.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/redisjson_document.py)
Bloom / Cuckoo / CMS / TopK / TDigestRedisBloom 模块[probabilistic_filters.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/probabilistic_filters.py)
FT.SEARCH / TimeSeriesRediSearch、RedisTimeSeries[search_vector_timeseries.py](file:///Users/binny/workspace-sdd/aiNotes/redis/python示例/search_vector_timeseries.py)

运行方式:

bash
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/redisjson_document.py
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/probabilistic_filters.py
python3 /Users/binny/workspace-sdd/aiNotes/redis/python示例/search_vector_timeseries.py

这里要注意:Redis Query Engine 查询建议显式使用 DIALECT 2,索引只建实际会查询的字段;向量检索和 RAG 场景要根据数据规模选择 HNSW 或 FLAT,不要盲目套用同一种索引算法。

Q36:如何一次性运行所有基础示例? ​

答:

可以用下面的命令逐个运行。它会自动读取 redis/python示例/.env 中的连接配置:

bash
for file in \
  string_cache_and_lock.py \
  hash_progress.py \
  list_queue_and_latest.py \
  set_dedup_and_relation.py \
  zset_ranking_and_delay.py \
  stream_consumer_group.py \
  bitmap_signin.py \
  hll_uv.py \
  geo_nearby_search.py \
  connection_pool_demo.py \
  brpop_worker_demo.py \
  atomicity_demo.py
  do python3 "/Users/binny/workspace-sdd/aiNotes/redis/python示例/$file"
done

所有示例都使用 demo:* 前缀;如果需要清理示例数据,可以在 redis-cli 中执行:

bash
redis-cli -h 127.0.0.1 -p 6379 -a '你的 Redis 密码' --scan --pattern 'demo:*' | xargs redis-cli -h 127.0.0.1 -p 6379 -a '你的 Redis 密码' DEL

速查表:六篇文档核心知识点 ​

序号文档知识点一句话总结
1BRPOP 深度解析BRPOP vs RPOP空队列时 BRPOP 阻塞等待,RPOP 立即返回 nil
2BRPOP 深度解析多 key 优先级按声明顺序从左到右检查,左边有就不查右边
3BRPOP 深度解析timeout=5每 5 秒回到循环,能响应 Ctrl+C
4BRPOP 深度解析返回值tuple(队列名, 元素值) 或 None
5BRPOP 深度解析LPUSH 返回值列表长度,不是推入的值
6BRPOP 深度解析FIFO 队列LPUSH + BRPOP = 左推右弹 = 先进先出
7单线程与多客户端交互多进程 → 单线程N 个进程的命令在 Redis 主线程串行执行
8单线程与多客户端交互等待链表节点每个节点 = 一个 TCP 连接 = 一个 client 结构体
9单线程与多客户端交互BRPOP 不阻塞主线程服务端加入等待链表后立即处理下一条命令
10单线程与多客户端交互连接池每个进程独立,进程内复用,进程间不共享
11单线程与多客户端交互fork 陷阱fork 复制 fd,两个进程操作同一个 socket 会错乱
12单线程与多客户端交互Redis 6.0 多线程只加速网络 I/O,命令执行仍单线程
13原子性保证机制四层保证单线程 → 命令原子 → 等待链表 FIFO → 事件循环串行
14原子性保证机制LPUSH 唤醒插入元素 → 检查等待链表 → 发给第一个等待者 → break
15原子性保证机制批量 LPUSH3 个元素会逐轮唤醒 3 个 Worker
16原子性保证机制超时原子性超时和 LPUSH 不可能同时执行,Redis 会按事件循环顺序串行处理
17原子性保证机制数据直达LPUSH 的元素不一定先进列表再弹出,可直接发给正在 BRPOP 的客户端
18原子性保证机制break 语句保证一次只唤醒一个 Worker,不会重复消费同一个元素
19连接池详解没有连接池时每次操作新建 TCP,包含建连、命令执行、关闭连接成本
20连接池详解连接池核心借出-归还模式,不 close,复用已有 TCP 连接
21连接池详解单线程 vs 多线程单线程通常只用 1 个连接,多线程会按并发数借出多个连接
22连接池详解max_connections连接数量上限保护,不是预创建连接数
23连接池详解health_check_interval连接健康检查配置,和连接数量没有必然关系
24连接池详解socket_timeout单命令读响应超时兜底,使用 BRPOP 时必须大于 BRPOP timeout
25连接池详解三种复用 TCP 的方式纯 socket、单 Connection、连接池都能复用 TCP,只是封装层级不同
26连接池详解连接池真正附加价值自动重连、线程安全、统一配置应用、并发连接上限保护
27推拉模型与源码解析BRPOP 本质延迟响应(Delayed Response),不是纯推也不是纯拉
28推拉模型与源码解析LPUSH 唤醒服务端主动 addReply 写入等待客户端输出缓冲区
29推拉模型与源码解析零额外往返LPUSH 执行中完成唤醒,不需要客户端再次发送请求
30推拉模型与源码解析TCP 模型BRPOP 是请求-响应模型的延迟返回版本,不是服务端随意推送
31推拉模型与源码解析唤醒触发signalKeyAsReady → handleClientsBlockedOnKeys
32推拉模型与源码解析关键源码addReplyArrayLen + addReplyBulk 写入 client 缓冲区
33Worker 编排器深度解析任务状态机QUEUED → SCRIPT → IMAGE → TTS → VIDEO → PENDING_REVIEW → APPROVED / REJECTED
34Worker 编排器深度解析BRPOP 消费循环5 秒超时 + Ctrl+C 响应 + 异常不中断循环
35Worker 编排器深度解析Seedance 分镜并行ThreadPoolExecutor(max_workers=3) 并行调用 Seedance API
36Worker 编排器深度解析进度写入hset 更新 Redis Hash 字段:task:{id}:progress
37Worker 编排器深度解析超时控制ARK_VIDEO_MAX_CONCURRENCY 全局信号量限制外部 API 并发
38Worker 编排器深度解析任务超时900 秒任务级超时 + 10 秒轮询间隔

参考文献与扩展阅读 ​

文档路径主要内容
BRPOP 深度解析Redis-BRPOP深度解析.mdBRPOP 语法、阻塞机制、多 Worker 行为、注意事项
单线程与多客户端交互Redis单线程与多客户端交互.md多进程客户端如何与单线程服务端交互
原子性保证机制Redis原子性保证机制.md单线程模型、等待链表源码级分析、命令原子性
连接池详解Redis连接池详解.md连接池内部机制、单/多线程对比、超时参数配置建议
推拉模型与源码解析Redis推拉模型与源码解析.mdBRPOP 唤醒机制、延迟响应、源码级流程
Worker 编排器深度解析Worker编排器深度解析.mdWorker 启动、消费循环、Seedance 分镜并行、异常处理