Redis 在异步任务中的角色:消息队列、缓存与状态存储

在 DB 异步任务架构中,Redis 并非单一用途组件,而是同时承担 消息队列、缓存 和 状态存储 三种核心角色。理解这三个角色的边界与协同方式,是设计高吞吐、低延迟异步系统的关键。

2.1 角色全景:Redis 的三重定位

下表从数据模型、访问模式、持久化要求三个维度,对比 Redis 在异步任务中的三种核心定位:

角色 核心数据结构 访问模式 持久化要求 典型场景
消息队列 List、Stream、Pub/Sub 生产者-消费者,阻塞弹出 低(可容忍少量丢失) 任务分发、事件通知
缓存 String、Hash、Sorted Set 键值读写,TTL 自动过期 无(纯内存加速) DB 查询结果缓存、热点数据
状态存储 String、Hash、Bitmap 频繁更新,原子操作 中(RDB/AOF 混合) 任务进度、锁、计数器
设计原则: 同一 Redis 实例可以同时承载三种角色,但建议按业务重要性拆分实例。消息队列和状态存储对可用性要求较高,缓存可接受部分数据丢失。

2.2 消息队列:异步任务的传输通道

Redis 作为消息队列,核心优势在于 低延迟 和 轻量级。相比 Kafka 或 RabbitMQ,Redis 队列更适合中小规模、对吞吐要求适中的异步任务。

2.2.1 基于 List 的经典队列

使用 LPUSH + BRPOP 实现生产者-消费者模式:

# 生产者:推送任务
LPUSH task:queue "{'id': 1001, 'type': 'email', 'to': 'user@example.com'}"

# 消费者:阻塞获取任务
BRPOP task:queue 0

2.2.2 基于 Stream 的可靠队列

Redis 5.0 引入的 Stream 提供了消费者组、消息确认、消息回溯等特性,更适合需要 至少一次投递 的场景:

# 生产者:添加消息到流
XADD task:stream * type email payload '{"to":"user@example.com"}'

# 消费者组:创建并读取
XGROUP CREATE task:stream email-group $
XREADGROUP GROUP email-group consumer-1 COUNT 1 BLOCK 5000 STREAMS task:stream >
选型建议: 简单任务用 List,需要消息确认和回溯用 Stream。Pub/Sub 仅适合实时通知,不适用于持久化任务队列。

2.3 缓存:DB 查询的加速层

在异步任务中,缓存主要用于 减少 DB 重复查询 和 存储中间计算结果。典型模式是 旁路缓存(Cache-Aside):

# 任务处理时先查缓存
result = redis.get("task:result:1001")
if result is None:
    # 缓存未命中,查 DB
    result = db.query("SELECT * FROM task_result WHERE id = 1001")
    # 写入缓存,设置 TTL
    redis.setex("task:result:1001", 3600, result)

2.3.1 缓存穿透与空值缓存

当 DB 中不存在某条记录时,大量请求会直接穿透缓存打到 DB。解决方案是缓存空值:

# 查询 DB 返回空时,缓存空值并设置短 TTL
if result is None:
    redis.setex("task:result:1001", 60, "NULL")
    return None

2.4 状态存储:任务生命周期的记录者

异步任务需要跟踪每个任务的 当前状态、重试次数、执行进度。Redis 的原子操作和高速写入使其成为状态存储的理想选择。

2.4.1 任务状态机

使用 Hash 存储任务的多维状态:

# 设置任务初始状态
HSET task:1001 status pending retry_count 0 created_at 1700000000

# 原子更新状态
HSET task:1001 status running started_at 1700000100

# 任务完成
HSET task:1001 status completed finished_at 1700000200

2.4.2 分布式锁与幂等控制

使用 Redis 实现分布式锁,防止同一任务被多个 Worker 重复执行:

# 获取锁,SET NX + EX 原子操作
SET task:lock:1001 worker-1 NX EX 30

# 释放锁(Lua 脚本保证原子性)
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end
注意: 状态存储对数据一致性要求较高,建议开启 Redis 的 AOF 持久化(everysec 策略),并配合 RDB 快照做定期备份。

2.5 三种角色的协同架构

下图展示了一个典型的异步任务系统中,Redis 如何同时扮演三种角色并协同工作:

Redis 在异步任务中的三重角色协同架构 任务生产者 消息队列 (List/Stream) Worker 节点 缓存层 (Cache-Aside) 状态存储 (Hash/Lock) 关系型 DB 角色交互说明: 1. 生产者将任务推入消息队列(List 或 Stream) 2. Worker 从队列拉取任务,同时查询缓存层加速数据获取 3. Worker 将任务进度、状态写入状态存储(Hash) 4. 缓存未命中时回源 DB,并将结果写入缓存(设置 TTL) 5. 状态存储配合分布式锁保证任务不被重复执行

2.6 角色边界与避坑指南

  • 消息队列 vs 状态存储: 不要将任务状态信息(如进度、重试次数)存储在队列消息体中,应单独使用 Hash 存储状态,队列只传递任务 ID。
  • 缓存 vs 状态存储: 缓存数据可以接受 TTL 过期后重新加载,但状态存储(如任务进度)不能随意过期,需设置合理的过期策略或使用持久化。
  • 消息队列 vs 缓存: 不要用缓存(如 String 类型)模拟消息队列,缺少阻塞弹出和消费者组机制,会导致轮询开销和消息丢失。
最佳实践: 在同一个异步任务系统中,建议为三种角色使用不同的 Redis 逻辑库(db0/db1/db2)或不同的实例,避免 key 冲突和性能干扰。