4. Redis 数据结构选型:String、List、Set、Sorted Set、Hash 在异步任务中的适用场景
在异步任务系统中,Redis 不仅仅是缓存层,更是任务队列、去重器、延迟调度器和状态存储的核心引擎。不同的数据结构对应不同的任务语义,选型错误会导致性能瓶颈或功能缺失。本章从异步任务的典型生命周期出发,逐一分析五种数据结构的适用边界。
4.1 选型总览:五种数据结构与任务语义的映射
| 数据结构 | 核心特性 | 异步任务典型场景 | 不适用场景 |
|---|---|---|---|
| String | 简单键值,支持原子计数、过期、SETNX | 任务锁、幂等令牌、计数器、单次调度标记 | 多消费者队列、范围查询、排序 |
| List | 双向链表,LPUSH/RPOP 阻塞操作 | FIFO 任务队列、工作流管道、日志缓冲 | 去重、优先级排序、随机访问 |
| Set | 无序唯一集合,支持交并差 | 任务去重、白名单/黑名单、已消费 ID 集合 | 顺序消费、延迟调度、排行榜 |
| Sorted Set | 有序唯一集合,按 score 排序,范围查询 | 延迟队列、优先级队列、定时任务调度、进度排名 | 高吞吐 FIFO(ZADD 开销高于 LPUSH) |
| Hash | 字段级操作,内存紧凑,支持部分更新 | 任务元数据存储、状态机字段、批量配置 | 队列顺序、集合运算、排序 |
选型第一原则:先定义任务的生命周期行为(入队 → 去重 → 排序 → 消费 → 状态追踪),再匹配数据结构。不要因为“熟悉 List”就把所有场景都塞进 List。
4.2 String:锁、幂等与计数器
String 是 Redis 最基础的数据结构,在异步任务中主要承担三类职责:
- 分布式任务锁:使用
SET key value NX EX timeout实现互斥消费,防止同一个任务被多个 Worker 同时执行。 - 幂等令牌:任务 ID 作为 key,消费前 SETNX,消费后 DEL,确保 at-least-once 语义下的幂等性。
- 任务计数器:INCR/DECR 记录任务重试次数、并发水位、失败阈值。
// 任务锁示例(Lua 脚本保证原子性)
local locked = redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2])
if locked then
-- 执行任务逻辑
redis.call('DEL', KEYS[1])
return 1
else
return 0
end
避坑:String 不适合存储大任务体(如 >10KB 的 JSON 负载),会浪费内存并增加序列化开销。大任务体应存入 DB 或对象存储,Redis 只存 ID 或状态标记。
4.3 List:FIFO 任务队列的基石
List 是构建异步任务队列最经典的数据结构,核心操作组合为 LPUSH + BRPOP 或 RPUSH + BLPOP。其优势在于:
- 阻塞弹出:BRPOP 支持超时阻塞,避免轮询浪费 CPU。
- 天然 FIFO:左进右出或右进左出,严格保证顺序。
- 多消费者安全:每个元素只被一个消费者弹出,天然支持 Worker 水平扩展。
生产级模式:使用
RPUSH + BLPOP 组合,配合 BRPOPLPUSH 或 Redis 7.0+ 的 BLMOVE 实现可靠队列——消费前将任务原子移动到 backup list,消费成功后再删除,防止任务丢失。
# 生产者
RPUSH task:queue "{'id':'task_001','type':'email'}"
# 消费者(可靠模式)
BLMOVE task:queue task:backup LEFT LEFT 30
# 消费 task:backup 中的任务
# 成功后 DEL task:backup 中的对应元素
List 的局限也很明显:不支持去重、不支持优先级、不支持延迟调度。这些场景需要转向 Set 或 Sorted Set。
4.4 Set:去重与集合运算
Set 在异步任务中的核心价值是 去重 和 集合关系判断。
- 任务去重:使用
SADD task:dedup task_id在入队前检查是否已存在,避免重复入队。 - 白名单/黑名单:
SISMEMBER whitelist worker_id控制哪些 Worker 可以消费特定任务。 - 已消费 ID 集合:记录已成功消费的任务 ID,用于幂等校验或补偿回查。
性能注意:Set 的
SISMEMBER 是 O(1) 操作,适合高频去重检查。但如果去重集合超过百万级,建议使用 Bloom Filter(Redis 模块或布隆过滤器插件)替代,以节省内存。
Set 不适合需要排序或延迟的场景,也不适合作为队列直接消费——因为 SPOP 随机弹出,无法保证顺序。
4.5 Sorted Set:延迟队列与优先级调度
Sorted Set 是异步任务系统中 最灵活的数据结构,通过 score 表达时间或优先级,通过 range 查询实现精准调度。
| 场景 | score 含义 | 核心命令 |
|---|---|---|
| 延迟队列 | 任务执行时间戳(毫秒) | ZADD → ZRANGEBYSCORE … WITHSCORES → ZREM |
| 优先级队列 | 优先级数值(越小越优先) | ZADD → ZPOPMIN / ZRANGE 0 0 |
| 定时任务调度 | 下次调度时间戳 | ZRANGEBYSCORE 0 now → 执行 → ZADD 更新下次时间 |
| 任务进度排名 | 已完成百分比或剩余时间 | ZINCRBY → ZREVRANGE 排名 |
// 延迟队列核心逻辑(伪代码)
// 1. 生产者:任务 5 分钟后执行
ZADD delay:queue (now + 300000) task_001
// 2. 消费者轮询:取出所有已到期的任务
tasks = ZRANGEBYSCORE delay:queue 0 now WITHSCORES
for task in tasks:
if ZREM delay:queue task > 0:
process(task)
关键陷阱:ZRANGEBYSCORE + ZREM 不是原子操作,高并发下多个 Worker 可能拿到同一个任务。解决方案:使用 Lua 脚本将查询和删除原子化,或使用 Redis 7.0+ 的
ZMPOP 命令。
下图展示了 Sorted Set 在延迟队列中的核心流程:
4.6 Hash:任务元数据与状态机
Hash 在异步任务中主要用于 存储任务的动态元数据,而不是作为队列本身。典型场景包括:
- 任务状态机:使用
HSET task:meta:{id} status pending,后续更新为processing、success、failed。 - 任务配置/上下文:存储重试次数、超时时间、回调 URL、输入参数等字段,支持部分更新而不需要读写整个 JSON。
- 批量读取:
HGETALL或HMGET一次获取多个字段,减少网络往返。
# 任务入队时写入元数据
HSET task:meta:001 status pending retry_count 0 max_retry 3 callback "https://api.example.com/callback"
# 消费过程中更新状态
HSET task:meta:001 status processing started_at 1699000000
# 消费完成后标记
HSET task:meta:001 status success completed_at 1699000100
内存优化:当 Hash 的字段数较少(<512 个)且每个字段值较短(<64 bytes)时,Redis 使用 ziplist 编码,内存效率极高。适合存储任务元数据,但不适合存储大文本或二进制负载。
4.7 综合选型决策树
在实际项目中,一个异步任务往往需要组合使用多种数据结构。以下决策树可以帮助快速定位:
4.8 实战组合模式
单一数据结构很少能覆盖完整任务生命周期。以下是三种经过生产验证的组合模式:
| 模式名称 | 数据结构组合 | 适用场景 | 关键注意点 |
|---|---|---|---|
| 可靠延迟队列 | Sorted Set + List + Hash | 订单超时取消、定时通知、任务重试 | Sorted Set 只做调度触发,实际消费走 List 的 BRPOPLPUSH |
| 幂等工作流 | String + Set + Hash | 支付回调、消息推送、数据同步 | String 做锁,Set 做去重,Hash 记录状态机 |
| 优先级批处理 | Sorted Set + List | 日志分级处理、紧急任务插队 | Sorted Set 按优先级排序,Worker 批量 ZPOPMIN 后推入 List |
架构建议:不要试图用 Redis 一个数据结构解决所有问题。Redis 的优势在于组合多种数据结构形成管道,配合 Lua 脚本保证原子性。对于超过 10 万级延迟任务,考虑将 Sorted Set 分片(按时间槽或任务类型拆分为多个 key),避免单个 ZSET 过大导致 ZADD 性能下降。