2. Redis核心数据结构回顾:String、List、Set、Sorted Set、Hash在任务队列中的应用场景
在构建异步任务编排系统时,Redis的五种核心数据结构各自承担着不同的角色。理解它们的特点与适用场景,是设计高效、可靠任务队列的基础。本章将逐一回顾这些数据结构,并重点分析它们在任务队列中的典型应用。
2.1 数据结构概览与任务队列映射
下表总结了五种数据结构的基本特性及其在任务队列中的常见定位:
| 数据结构 | 底层实现 | 核心特性 | 任务队列典型角色 |
|---|---|---|---|
| String | 动态字符串 | 简单键值、原子计数、过期时间 | 任务锁、计数器、任务结果暂存 |
| List | 双向链表 / 压缩列表 | 有序、阻塞弹出(BLPOP/BRPOP) | 经典FIFO任务队列、延迟队列 |
| Set | 哈希表 / 整数集合 | 无序、去重、集合运算 | 任务去重、白名单/黑名单、任务标签 |
| Sorted Set | 跳表 + 哈希表 | 有序、按分值范围查询、排名 | 延迟队列、优先级队列、任务调度 |
| Hash | 压缩列表 / 哈希表 | 字段级操作、内存紧凑 | 任务元数据存储、任务状态跟踪 |
设计原则: 没有一种数据结构能解决所有问题。实际项目中,通常需要组合使用多种数据结构来构建完整的任务编排链路。
2.2 String:任务锁与计数器
String是Redis最基础的数据结构,在任务队列中主要用于以下两个场景:
- 分布式任务锁: 使用
SET key value NX EX seconds命令实现互斥锁,防止同一任务被多个消费者重复执行。 - 任务计数器: 使用
INCR/DECR原子操作统计任务处理速率、失败次数或重试次数。
// 获取任务锁(Java伪代码)
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent("task:lock:order_123", "worker_1", Duration.ofSeconds(30));
if (Boolean.TRUE.equals(locked)) {
try {
// 执行任务
} finally {
redisTemplate.delete("task:lock:order_123");
}
}
// 任务失败计数
redisTemplate.opsForValue().increment("task:fail:order_123");
2.3 List:经典FIFO任务队列
List是构建任务队列最直观的选择。生产者使用 LPUSH 将任务推入队列头部,消费者使用 BRPOP 从队列尾部阻塞弹出,实现先进先出(FIFO)语义。
关键命令:
BRPOP key timeout —— 阻塞式弹出,当队列为空时等待指定时间,避免空轮询浪费CPU。
// 生产者:推送任务
redisTemplate.opsForList().leftPush("task:queue:email", taskJson);
// 消费者:阻塞获取任务
String taskJson = redisTemplate.opsForList()
.rightPop("task:queue:email", 5, TimeUnit.SECONDS);
if (taskJson != null) {
processTask(taskJson);
}
List的局限性在于:不支持优先级,且无法直接实现延迟执行。这些需求需要借助Sorted Set或第三方插件。
2.4 Set:任务去重与标签管理
Set天然具备去重能力,在任务队列中常用于:
- 任务ID去重: 在任务入队前,先检查Set中是否已存在该任务ID,防止重复提交。
- 任务标签系统: 为任务打上多个标签(如“高优先级”、“需重试”),通过
SINTER/SUNION进行灵活筛选。
// 任务去重检查
Boolean added = redisTemplate.opsForSet()
.add("task:ids:email", "order_123");
if (Boolean.TRUE.equals(added)) {
// 新任务,允许入队
pushToList(taskJson);
} else {
// 重复任务,忽略或更新
log.warn("Duplicate task detected: order_123");
}
2.5 Sorted Set:延迟队列与优先级调度
Sorted Set是构建延迟队列和优先级队列的核心数据结构。它使用分值(score)作为排序依据,天然支持按时间或优先级调度。
- 延迟队列: 将任务执行时间戳作为score,消费者定期使用
ZRANGEBYSCORE拉取到期的任务。 - 优先级队列: 将优先级数值(如1-10)作为score,高优先级任务分值更小,优先被取出。
// 延迟队列:任务在5分钟后执行
long executeAt = System.currentTimeMillis() + 300_000;
redisTemplate.opsForZSet()
.add("task:delay:order", "order_123", executeAt);
// 消费者:每秒拉取到期任务
Set<String> tasks = redisTemplate.opsForZSet()
.rangeByScore("task:delay:order", 0, System.currentTimeMillis());
for (String taskId : tasks) {
// 处理任务
redisTemplate.opsForZSet().remove("task:delay:order", taskId);
}
注意: 使用Sorted Set实现延迟队列时,需要配合轮询或定时任务。对于高精度延迟场景,建议结合Redis Keyspace Notifications或Redisson的RDelayedQueue。
2.6 Hash:任务元数据与状态跟踪
Hash允许以字段(field)为单位存储任务的多维属性,非常适合管理任务的完整生命周期:
- 任务元数据: 存储任务类型、创建时间、重试次数、回调地址等。
- 状态跟踪: 使用字段
status记录任务当前状态(pending / running / success / failed),支持原子更新。
// 存储任务元数据
redisTemplate.opsForHash()
.putAll("task:meta:order_123", Map.of(
"type", "email",
"status", "pending",
"retry", "0",
"created_at", String.valueOf(System.currentTimeMillis())
));
// 原子更新状态
redisTemplate.opsForHash()
.put("task:meta:order_123", "status", "running");
2.7 综合架构:五种数据结构的协作
下图展示了一个典型的异步任务编排系统中,五种数据结构如何协同工作:
2.8 场景选择指南
在实际项目中,可以根据以下条件快速选择合适的数据结构:
| 业务需求 | 推荐数据结构 | 理由 |
|---|---|---|
| 简单FIFO队列 | List | LPUSH + BRPOP 天然支持阻塞消费 |
| 延迟执行任务 | Sorted Set | 以时间戳作为score,范围查询到期任务 |
| 任务去重 | Set | SADD 自动去重,O(1) 判断是否存在 |
| 任务状态跟踪 | Hash | 字段级更新,支持存储多维属性 |
| 分布式锁 | String | SET NX EX 原子操作,简单可靠 |
| 优先级调度 | Sorted Set | 以优先级数值作为score,支持范围获取 |
最佳实践: 在复杂任务编排系统中,建议将List作为主队列,Sorted Set作为延迟/优先级补充,Hash存储元数据,Set用于去重,String用于锁和计数。这种组合既能保证核心流程的高效,又能灵活应对各种边界场景。