主题
第八章:综合场景
本章导读
本章围绕“综合场景”展开,先交代适用场景,再说明核心原理,随后给出典型示例,最后补充小结与延伸阅读。
阅读顺序: 结论 → 原理 → 场景 → 代码 → 小结
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 BRPOP | RabbitMQ | Kafka |
|---|---|---|---|
| 部署复杂度 | 低(已有 Redis) | 中(需独立部署) | 高(需 ZooKeeper + Broker) |
| 原子性保证 | 单线程串行 | Channel 级别锁 | 分区单消费者 |
| 任务确认(ACK) | 无(需 RPOPLPUSH 弥补) | 内置 ACK 机制 | Offset 提交 |
| 延迟 | < 1ms | ~1ms | ~10ms |
| 吞吐量 | 10 万+ QPS | 万级 QPS | 百万级 QPS |
| 适用规模 | 中小规模(< 200 Worker) | 中大规模 | 超大规模 |
选择 Redis 的原因:
- 项目已经依赖 Redis(进度缓存、审核队列),无需额外部署
- MVP 阶段任务量不大(每天几十到几百个任务),Redis 足够
- 部署简单,运维成本低
- BRPOP 延迟极低(< 1ms),满足实时性要求
何时考虑迁移:
- Worker 超过 200 个,Redis 单线程开始成为瓶颈
- 需要复杂的任务路由、重试、死信队列
- 需要消息持久化保证(Redis 重启丢数据风险)