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 在延迟队列中的核心流程:

Sorted Set 延迟队列核心流程 生产者 ZADD score=未来时间戳 Sorted Set 延迟桶 定时轮询 消费者 Worker ZRANGEBYSCORE 0 now 到期任务集合 ZREM 原子移除 ⚠ 原子性要求: ZRANGEBYSCORE 与 ZREM 必须放在 Lua 脚本或事务中执行,防止多个 Worker 并发消费同一任务。

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 综合选型决策树

在实际项目中,一个异步任务往往需要组合使用多种数据结构。以下决策树可以帮助快速定位:

异步任务 Redis 数据结构选型决策树 任务需要什么能力? 需要队列顺序 需要延迟/优先级? 否 List 是 Sorted Set 需要去重/集合 需要排序? 否 Set 是 Sorted Set 需要存储状态/字段 需要部分更新? 否 String 是 Hash 📌 组合使用提示: • 延迟队列 = Sorted Set(调度)+ List(实际消费)+ Hash(元数据) • 可靠任务 = List(主队列)+ Set(去重)+ Hash(状态机)+ String(锁)

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 性能下降。