层级时间轮

问题

你的延时队列需要处理长延时任务

场景:物流自动确认收货
需求:
- 延时时间:7 天(甚至 30 天)
- 任务数量:每天 10 万个
- 精度:±1 小时

问题:
单层时间轮:
- tick_duration = 100ms
- ticks_per_wheel = 512
- 最大延时 = 51.2 秒

7 天延时怎么办?

解决方案:层级时间轮

就像钟表有秒针、分针、时针,层级时间轮用多层时间轮处理不同时间跨度!🎯


层级时间轮原理

生活类比:钟表

         时针(12 格)
        ┌─────────┐
   分针 │  秒针   │ 分针
  (60格)│ (60格) │ (60格)
        └─────────┘

时间流逝:
秒针每转一圈(60 秒)→ 分针走一格(1 分钟)
分针每转一圈(60 分钟)→ 时针走一格(1 小时)

最大延时:
60 秒 × 60 分 × 12 时 = 12 小时

层级时间轮设计

第一层(秒针轮):
- tick_duration = 1 秒
- ticks_per_wheel = 60
- 最大延时 = 60 秒

第二层(分针轮):
- tick_duration = 1 分钟
- ticks_per_wheel = 60
- 最大延时 = 60 分钟 = 1 小时

第三层(时针轮):
- tick_duration = 1 小时
- ticks_per_wheel = 24
- 最大延时 = 24 小时 = 1 天

第四层(天针轮):
- tick_duration = 1 天
- ticks_per_wheel = 30
- 最大延时 = 30 天

总最大延时:30 天!✅

数据结构设计

核心类


完整方案

增强版层级时间轮


任务降级机制

原理图

7 天延时的任务:

初始:
添加到天针轮(第4层),槽位=7, rounds=0

第 1 天:
天针轮指针到达槽位 7 → 任务到期 → 降级到时针轮

时针轮:
计算剩余延时 = 6 天
槽位 = 6 * 24 = 144 → 槽位 144 % 24 = 0, rounds=6

第 2 天:
时针轮转完一圈 → 槽位 0 的任务 → rounds 减到 5

第 6 天:
时针轮 rounds = 0 → 降级到分针轮

分针轮:
计算剩余延时 = 几小时
...(继续降级)

最后:
降级到秒针轮 → 执行任务

降级代码


性能测试

测试代码

测试结果

1秒 延时 1000 个任务: 0.01s (100000 ops/s)
1分钟 延时 1000 个任务: 0.01s (100000 ops/s)
1小时 延时 1000 个任务: 0.01s (100000 ops/s)
1天 延时 1000 个任务: 0.01s (100000 ops/s)
7天 延时 100 个任务: 0.02s (5000 ops/s)
30天 延时 100 个任务: 0.02s (5000 ops/s)

结论:
- 添加任务性能稳定,不受延时时间影响
- 长延时任务会添加到高层,不占用底层空间

层级时间轮优势

对比单层时间轮

指标单层时间轮层级时间轮
最大延时有限(如51秒)理论无限(可扩展层级)
内存占用高(所有任务都在底层)低(任务分散在各层)
添加性能O(1)O(1)
精度tick 级别第一层 tick 级别
复杂度

内存优化


实际应用示例

Kafka 时间轮实现

应用场景

1. 网络超时控制
   - 连接超时:30秒
   - 请求超时:5分钟
   - 心跳超时:1小时

2. 任务调度系统
   - 定时任务:每天执行
   - 周期任务:每周执行
   - 延时任务:7天后执行

3. 订单系统
   - 订单取消:30分钟
   - 自动确认:7天
   - 自动评价:30天

想一想

思考 1:层级数量如何确定?

在设计层级时间轮时,需要确定几层时间轮才能满足业务需求。如果层级太少,无法支持长延时任务;如果层级太多,又会增加复杂度。应该如何设计层级配置?

参考答案

问题分析

层级数量的确定需要考虑:

  1. 最大延时需求:业务最长的延时任务(如30天)
  2. 时间精度要求:最短的任务间隔(如1秒)
  3. 内存限制:每层都会占用内存
  4. 性能平衡:层级过多会增加降级开销

技术要点

1. 层级计算公式

最大延时 = tick_duration × wheel_size ^ (layer_count - 1)

倒推层级数量:
layer_count = ⌈log(wheel_size, max_delay / tick_duration)⌉ + 1

2. 标准配置方案

3. 设计原则

原则说明示例
覆盖需求最大延时 ≥ 业务最大延时订单确认7天 → 最大延时≥7天
精度优先第一层tick满足精度要求精度±1秒 → tick≤1秒
均匀递增每层跨度递增倍数相近60秒→60分→24小时
控制层级3-5层为宜太多增加复杂度

架构图解

层级设计决策树:

┌─────────────────────────────────────────┐
│  业务需求分析                            │
│  - 最大延时:30天                        │
│  - 最小精度:1秒                         │
│  - 任务分布:70%短延时,30%长延时         │
└─────────────────┬───────────────────────┘


┌─────────────────────────────────────────┐
│  确定第一层                              │
│  tick_duration = 1秒(满足精度要求)      │
│  wheel_size = 60(常见值)               │
│  最大延时:60秒                          │
└─────────────────┬───────────────────────┘


┌─────────────────────────────────────────┐
│  计算层级数量                            │
│  第2层:1分钟 × 60 = 1小时               │
│  第3层:1小时 × 24 = 1天                 │
│  第4层:1天 × 30 = 30天                  │
│  ✅ 满足需求,共4层                      │
└─────────────────┬───────────────────────┘


┌─────────────────────────────────────────┐
│  验证配置                                │
│  ✓ 最大延时:30天(≥7天需求)            │
│  ✓ 精度:±1秒                            │
│  ✓ 层级数:4层(适中)                   │
│  ✓ 内存占用:约10MB(100万任务)          │
└─────────────────────────────────────────┘

性能对比

层级配置最大延时内存占用(100万任务)降级延迟推荐场景
3层18小时15MB短延时为主
4层30天10MB通用场景
5层2年8MB超长延时
6层100年7MB很高极端场景

最佳实践

1. 动态计算层级数量

2. 根据业务特性调整

3. 监控与调优

总结

  • ✅ 通用场景:4层(秒-分-小时-天)
  • ✅ 短延时场景:3层即可
  • ✅ 超长延时:5-6层,但要权衡性能
  • ❌ 避免:超过6层(降级开销大)

思考 2:任务降级是否会丢失精度?

当任务从上层时间轮降级到下层时,需要重新计算剩余延时和槽位。这个过程中是否会损失精度?比如一个7天的任务,从天轮降到小时轮,再降到分钟轮,最终精度如何?

参考答案

问题分析

任务降级过程中,精度是否丢失取决于:

  1. 计算方式:是否基于当前时间重新计算
  2. 时间单位:不同层的tick粒度不同
  3. 累积误差:多次降级是否会叠加误差

技术要点

1. 降级过程不会丢失精度

关键:每次降级都重新计算 remaining_delay = execute_at - current_time,因此不会累积误差。

2. 精度对比

阶段任务层级tick粒度剩余时间计算方式精度
初始天轮(L4)1天7天execute_at - now±0.5天
第1次降级小时轮(L3)1小时6天23小时execute_at - now±0.5小时
第2次降级分钟轮(L2)1分钟6天59分execute_at - now±0.5分钟
第3次降级秒轮(L1)1秒59秒execute_at - now±0.5秒
执行--0秒精确触发0秒

结论:精度逐步提高,最终达到第一层的tick精度。

3. 误差来源分析

架构图解

任务降级精度示意图:

7天任务:execute_at = T + 604800秒

T0: 添加任务
┌──────────────────────────────────────────┐
│ 天轮(L4)                                │
│ 剩余时间:604800秒                        │
│ 槽位:7,rounds:0                        │
│ 精度:±0.5天(12小时)                    │
└──────────────────────────────────────────┘
           │ 1天后

T1: 第一次降级
┌──────────────────────────────────────────┐
│ 小时轮(L3)                              │
│ 剩余时间:518400秒(6天)                  │
│ 精确计算:execute_at - current_time       │
│ 槽位:0,rounds:6                        │
│ 精度:±0.5小时(30分钟)                  │
└──────────────────────────────────────────┘
           │ 6天后

T2: 第二次降级
┌──────────────────────────────────────────┐
│ 分钟轮(L2)                              │
│ 剩余时间:3600秒(1小时)                 │
│ 精确计算:execute_at - current_time       │
│ 槽位:0,rounds:60                       │
│ 精度:±0.5分钟(30秒)                    │
└──────────────────────────────────────────┘
           │ 1小时后

T3: 第三次降级
┌──────────────────────────────────────────┐
│ 秒轮(L1)                                │
│ 剩余时间:60秒                            │
│ 精确计算:execute_at - current_time       │
│ 槽位:60,rounds:0                       │
│ 精度:±0.5秒                              │
└──────────────────────────────────────────┘
           │ 60秒后

T4: 执行任务
实际执行时间:T + 604800秒 ± 0.5秒
相对误差:0.5 / 604800 ≈ 0.00008%

性能对比

降级策略精度计算开销适用场景
重新计算剩余时间高(±0.5 tick)每次O(1)推荐
固定递减低(累积误差)O(1)不推荐
保持原始槽位中(±0.5高层tick)O(1)特殊场景

测试验证

最佳实践

1. 使用高精度时间戳

2. 避免累积误差

3. 处理边界情况

4. 监控精度指标

总结

  • ✅ 任务降级不会丢失精度
  • ✅ 每次重新计算剩余时间,避免累积误差
  • ✅ 最终精度由第一层tick决定
  • ⚠️ 实际执行可能有微小延迟(线程调度)
  • 📊 典型精度:±500ms(tick=1秒时)

思考 3:如何处理进程重启?

层级时间轮是基于内存的数据结构,进程重启后所有任务都会丢失。对于生产环境,这是不可接受的。应该如何设计持久化机制,保证重启后任务不丢失?

参考答案

问题分析

进程重启面临的挑战:

  1. 任务丢失:内存中的任务全部丢失
  2. 状态恢复:需要恢复时间轮的运行状态
  3. 数据一致性:避免重复执行或漏执行
  4. 性能影响:持久化不能严重影响性能

技术要点

1. 持久化方案对比

方案实时性性能影响可靠性复杂度推荐度
定时快照⭐⭐⭐
WAL日志⭐⭐⭐⭐
任务持久化⭐⭐⭐⭐⭐
数据库轮询⭐⭐

2. 推荐方案:任务持久化 + 定期检查

3. 完整持久化架构

┌─────────────────────────────────────────────────────────┐
│                    应用层                                │
│  ┌──────────────────────────────────────────────────┐  │
│  │        LayeredTimingWheel (内存)                 │  │
│  │  - 秒轮  - 分轮  - 时轮  - 天轮                   │  │
│  └────────────┬─────────────────────────────────────┘  │
│               │                                         │
│               │ 添加任务时                              │
│               ▼                                         │
│  ┌──────────────────────────────────────────────────┐  │
│  │           持久化层                                │  │
│  │  ┌────────────┐  ┌────────────┐                 │  │
│  │  │ Redis ZSet │  │   PostgreSQL│                 │  │
│  │  │ - 实时写入  │  │ - 定期归档  │                 │  │
│  │  │ - 快速查询  │  │ - 历史记录  │                 │  │
│  │  └────────────┘  └────────────┘                 │  │
│  └──────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
                    │ 进程重启

┌─────────────────────────────────────────────────────────┐
│                    恢复流程                              │
│                                                         │
│  1. 从Redis加载未执行的任务                              │
│     └──> execute_at > current_time                     │
│                                                         │
│  2. 重新构建时间轮状态                                   │
│     └──> 根据剩余延时分配到各层                          │
│                                                         │
│  3. 启动工作线程                                         │
│     └──> 继续处理任务                                   │
│                                                         │
│  4. 数据一致性检查                                       │
│     └──> 去重、补漏、修复                               │
└─────────────────────────────────────────────────────────┘

架构图解

持久化时机

任务生命周期:

创建任务

    ├─> 内存:添加到时间轮
    ├─> Redis:写入ZSet(score=execute_at)
    └─> DB:写入任务表(异步)


执行前

    ├─> 内存:从时间轮取出
    ├─> Redis:删除(防止重复执行)
    └─> DB:更新状态为"已执行"


进程重启

    ├─> 内存:清空
    ├─> Redis:保持不变
    └─> DB:保持不变


恢复流程

    ├─> 从Redis读取 execute_at > now 的任务
    ├─> 重新添加到时间轮
    └─> 继续运行

最佳实践

1. Redis + PostgreSQL 双写策略

2. 数据一致性保证

3. 优雅重启流程

4. 监控告警

5. 性能优化

性能对比

指标无持久化仅RedisRedis+DB
添加延迟<1ms~2ms~3ms
重启恢复0s~1s~3s
数据可靠性0%99.9%99.99%
额外存储0~100MB~1GB

总结

  • ✅ 推荐:Redis实时 + DB异步双写策略
  • ✅ 关键:execute_at作为score,便于范围查询
  • ✅ 恢复:重启时从Redis加载未执行任务
  • ✅ 一致性:定期检查三层(内存、Redis、DB)一致性
  • ⚠️ 性能:批量写入、Pipeline、Lua脚本优化