引言
在日常开发过程中,我们经常会碰到这类业务场景:
- 订单创建后30分钟未支付需自动取消
- 优惠券到期前24小时要推送提醒
- 消息发送失败后需延迟重试
这些场景的核心特征是延迟执行特定业务操作 ,也就是业内常说的延迟消息处理需求。
传统解决思路存在诸多弊端,而基于时间轮算法的延迟消息实现方案,能更优雅、高效地应对这类问题。
传统延迟任务方案的核心痛点
在深入讲解时间轮算法之前,我们先拆解下传统延迟任务方案的问题:
1. 定时任务扫描数据库方案
最常见的做法是借助 Quartz 等定时任务框架,每隔固定时间扫描数据库,筛选出需要处理的超时订单、过期优惠券等数据。
示例代码如下:
java
/**
* 定时扫描超时订单并取消
* 传统定时任务扫描方案,存在精度低、资源浪费、数据库压力大等问题
*/
public void scanForTimeoutOrders() {
// 查询所有超时未支付的订单
List<Order> timeoutOrders = orderMapper.selectTimeoutOrders();
// 遍历处理每个超时订单
for (Order order : timeoutOrders) {
cancelOrder(order.getId()); // 执行订单取消逻辑
}
}
该方案的问题十分突出:
- 精度不足:若设置5分钟扫描一次,最坏情况下用户要多等4分59秒才能触发订单取消
- 资源浪费:即便没有超时订单,定时任务也会周期性执行扫描操作
- 数据库压力:频繁的全表/条件查询会加重数据库的负载
2. 消息队列延迟消息方案
另一种常见方案是使用支持延迟消息的中间件(如 RocketMQ、RabbitMQ)。
示例代码如下:
java
/**
* 发送延迟消息示例
* 依赖MQ中间件,存在配置复杂、运维成本高的问题
*/
public void sendDelayedMessage() throws UnsupportedEncodingException {
// 构建消息体
Message message = new Message(
"TopicTest", // 消息主题
"TagA", // 消息标签
("Hello").getBytes(RemotingHelper.DEFAULT_CHARSET) // 消息内容
);
// 设置延迟级别(不同MQ的延迟级别定义不同)
message.setDelayTimeLevel(3);
// 发送延迟消息
producer.send(message);
}
这种方式虽优于定时扫描,但仍有明显短板:
- 强依赖中间件:需引入额外的 MQ 组件,增加系统耦合度
- 配置复杂度高:不同 MQ 的延迟消息配置规则、延迟级别定义差异大
- 运维成本高:需维护 MQ 集群的稳定性、可用性,增加运维工作量
时间轮算法的核心原理与优势
时间轮算法(Timing Wheel) 是一种高性能的任务调度算法,最早由 Netty 框架引入,核心思想类比日常生活中的时钟:
将时间划分为多个固定粒度的槽(Slot),每个槽对应一个时间窗口,需要在该窗口执行的任务被放入对应槽中;随着时间推移,"指针"不断移动,指针指向某个槽时,立即执行该槽内的所有任务。
时间轮核心概念
- 刻度(Tick):时间轮的最小时间单位(如100毫秒),决定任务调度的精度
- 槽(Slot):时间轮上的存储单元,每个槽可存放多个延迟任务
- 指针(Pointer):指向当前时间对应的槽,随时间匀速移动
时间轮算法的核心优势
- 时间复杂度接近 O(1),调度效率远高于定时扫描
- 内存占用低,仅需存储待执行的任务
- 实时性高,任务触发延迟仅取决于 Tick 的设置
- 无外部中间件依赖,降低系统复杂度
时间轮工作原理流程图
时间轮算法的工作原理可以通过以下流程图直观展示:
#mermaid-svg-D8Lh4n4dHyFC6pJl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-D8Lh4n4dHyFC6pJl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-D8Lh4n4dHyFC6pJl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-D8Lh4n4dHyFC6pJl .error-icon{fill:#552222;}#mermaid-svg-D8Lh4n4dHyFC6pJl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-D8Lh4n4dHyFC6pJl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-D8Lh4n4dHyFC6pJl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-D8Lh4n4dHyFC6pJl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-D8Lh4n4dHyFC6pJl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-D8Lh4n4dHyFC6pJl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-D8Lh4n4dHyFC6pJl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-D8Lh4n4dHyFC6pJl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-D8Lh4n4dHyFC6pJl .marker.cross{stroke:#333333;}#mermaid-svg-D8Lh4n4dHyFC6pJl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-D8Lh4n4dHyFC6pJl p{margin:0;}#mermaid-svg-D8Lh4n4dHyFC6pJl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-D8Lh4n4dHyFC6pJl .cluster-label text{fill:#333;}#mermaid-svg-D8Lh4n4dHyFC6pJl .cluster-label span{color:#333;}#mermaid-svg-D8Lh4n4dHyFC6pJl .cluster-label span p{background-color:transparent;}#mermaid-svg-D8Lh4n4dHyFC6pJl .label text,#mermaid-svg-D8Lh4n4dHyFC6pJl span{fill:#333;color:#333;}#mermaid-svg-D8Lh4n4dHyFC6pJl .node rect,#mermaid-svg-D8Lh4n4dHyFC6pJl .node circle,#mermaid-svg-D8Lh4n4dHyFC6pJl .node ellipse,#mermaid-svg-D8Lh4n4dHyFC6pJl .node polygon,#mermaid-svg-D8Lh4n4dHyFC6pJl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-D8Lh4n4dHyFC6pJl .rough-node .label text,#mermaid-svg-D8Lh4n4dHyFC6pJl .node .label text,#mermaid-svg-D8Lh4n4dHyFC6pJl .image-shape .label,#mermaid-svg-D8Lh4n4dHyFC6pJl .icon-shape .label{text-anchor:middle;}#mermaid-svg-D8Lh4n4dHyFC6pJl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-D8Lh4n4dHyFC6pJl .rough-node .label,#mermaid-svg-D8Lh4n4dHyFC6pJl .node .label,#mermaid-svg-D8Lh4n4dHyFC6pJl .image-shape .label,#mermaid-svg-D8Lh4n4dHyFC6pJl .icon-shape .label{text-align:center;}#mermaid-svg-D8Lh4n4dHyFC6pJl .node.clickable{cursor:pointer;}#mermaid-svg-D8Lh4n4dHyFC6pJl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-D8Lh4n4dHyFC6pJl .arrowheadPath{fill:#333333;}#mermaid-svg-D8Lh4n4dHyFC6pJl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-D8Lh4n4dHyFC6pJl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-D8Lh4n4dHyFC6pJl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D8Lh4n4dHyFC6pJl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-D8Lh4n4dHyFC6pJl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D8Lh4n4dHyFC6pJl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-D8Lh4n4dHyFC6pJl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-D8Lh4n4dHyFC6pJl .cluster text{fill:#333;}#mermaid-svg-D8Lh4n4dHyFC6pJl .cluster span{color:#333;}#mermaid-svg-D8Lh4n4dHyFC6pJl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-D8Lh4n4dHyFC6pJl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-D8Lh4n4dHyFC6pJl rect.text{fill:none;stroke-width:0;}#mermaid-svg-D8Lh4n4dHyFC6pJl .icon-shape,#mermaid-svg-D8Lh4n4dHyFC6pJl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D8Lh4n4dHyFC6pJl .icon-shape p,#mermaid-svg-D8Lh4n4dHyFC6pJl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-D8Lh4n4dHyFC6pJl .icon-shape .label rect,#mermaid-svg-D8Lh4n4dHyFC6pJl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D8Lh4n4dHyFC6pJl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-D8Lh4n4dHyFC6pJl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-D8Lh4n4dHyFC6pJl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
延迟任务提交
计算目标时间槽
时间槽是否已存在?
将任务添加到对应槽的链表中
创建新的时间槽并添加任务
等待指针移动
时间轮指针按Tick移动
指针到达任务所在槽?
执行该槽所有任务
任务执行完成
从时间轮中移除任务
SpringBoot集成时间轮架构流程图
以下是SpringBoot项目中集成时间轮处理延迟任务的完整架构流程:
#mermaid-svg-YJtjummdkliTjlmL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YJtjummdkliTjlmL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YJtjummdkliTjlmL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YJtjummdkliTjlmL .error-icon{fill:#552222;}#mermaid-svg-YJtjummdkliTjlmL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YJtjummdkliTjlmL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YJtjummdkliTjlmL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YJtjummdkliTjlmL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YJtjummdkliTjlmL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YJtjummdkliTjlmL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YJtjummdkliTjlmL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YJtjummdkliTjlmL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YJtjummdkliTjlmL .marker.cross{stroke:#333333;}#mermaid-svg-YJtjummdkliTjlmL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YJtjummdkliTjlmL p{margin:0;}#mermaid-svg-YJtjummdkliTjlmL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YJtjummdkliTjlmL .cluster-label text{fill:#333;}#mermaid-svg-YJtjummdkliTjlmL .cluster-label span{color:#333;}#mermaid-svg-YJtjummdkliTjlmL .cluster-label span p{background-color:transparent;}#mermaid-svg-YJtjummdkliTjlmL .label text,#mermaid-svg-YJtjummdkliTjlmL span{fill:#333;color:#333;}#mermaid-svg-YJtjummdkliTjlmL .node rect,#mermaid-svg-YJtjummdkliTjlmL .node circle,#mermaid-svg-YJtjummdkliTjlmL .node ellipse,#mermaid-svg-YJtjummdkliTjlmL .node polygon,#mermaid-svg-YJtjummdkliTjlmL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YJtjummdkliTjlmL .rough-node .label text,#mermaid-svg-YJtjummdkliTjlmL .node .label text,#mermaid-svg-YJtjummdkliTjlmL .image-shape .label,#mermaid-svg-YJtjummdkliTjlmL .icon-shape .label{text-anchor:middle;}#mermaid-svg-YJtjummdkliTjlmL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YJtjummdkliTjlmL .rough-node .label,#mermaid-svg-YJtjummdkliTjlmL .node .label,#mermaid-svg-YJtjummdkliTjlmL .image-shape .label,#mermaid-svg-YJtjummdkliTjlmL .icon-shape .label{text-align:center;}#mermaid-svg-YJtjummdkliTjlmL .node.clickable{cursor:pointer;}#mermaid-svg-YJtjummdkliTjlmL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YJtjummdkliTjlmL .arrowheadPath{fill:#333333;}#mermaid-svg-YJtjummdkliTjlmL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YJtjummdkliTjlmL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YJtjummdkliTjlmL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YJtjummdkliTjlmL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YJtjummdkliTjlmL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YJtjummdkliTjlmL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YJtjummdkliTjlmL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YJtjummdkliTjlmL .cluster text{fill:#333;}#mermaid-svg-YJtjummdkliTjlmL .cluster span{color:#333;}#mermaid-svg-YJtjummdkliTjlmL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YJtjummdkliTjlmL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YJtjummdkliTjlmL rect.text{fill:none;stroke-width:0;}#mermaid-svg-YJtjummdkliTjlmL .icon-shape,#mermaid-svg-YJtjummdkliTjlmL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YJtjummdkliTjlmL .icon-shape p,#mermaid-svg-YJtjummdkliTjlmL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YJtjummdkliTjlmL .icon-shape .label rect,#mermaid-svg-YJtjummdkliTjlmL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YJtjummdkliTjlmL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YJtjummdkliTjlmL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YJtjummdkliTjlmL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 异常处理
业务执行层
时间轮调度层
SpringBoot应用层
客户端请求
是
否
是
否
用户创建订单
调用创建延迟任务接口
TimingWheelController
生成唯一任务ID
创建OrderTimeoutHandler任务
提交任务到DelayedTaskManager
DelayedTaskManager
计算延迟时间
将任务放入对应时间槽
时间轮指针移动
到达任务执行时间?
触发任务执行
OrderTimeoutHandler.run
更新订单状态
释放库存
处理退款
发送取消通知
记录执行日志
任务执行异常
记录错误日志
是否重试?
重新提交延迟任务
任务失败处理
时间轮数据结构示意图
#mermaid-svg-Hq4oh1XgHTqSAKjc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Hq4oh1XgHTqSAKjc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Hq4oh1XgHTqSAKjc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Hq4oh1XgHTqSAKjc .error-icon{fill:#552222;}#mermaid-svg-Hq4oh1XgHTqSAKjc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Hq4oh1XgHTqSAKjc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Hq4oh1XgHTqSAKjc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Hq4oh1XgHTqSAKjc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Hq4oh1XgHTqSAKjc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Hq4oh1XgHTqSAKjc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Hq4oh1XgHTqSAKjc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Hq4oh1XgHTqSAKjc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Hq4oh1XgHTqSAKjc .marker.cross{stroke:#333333;}#mermaid-svg-Hq4oh1XgHTqSAKjc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Hq4oh1XgHTqSAKjc p{margin:0;}#mermaid-svg-Hq4oh1XgHTqSAKjc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Hq4oh1XgHTqSAKjc .cluster-label text{fill:#333;}#mermaid-svg-Hq4oh1XgHTqSAKjc .cluster-label span{color:#333;}#mermaid-svg-Hq4oh1XgHTqSAKjc .cluster-label span p{background-color:transparent;}#mermaid-svg-Hq4oh1XgHTqSAKjc .label text,#mermaid-svg-Hq4oh1XgHTqSAKjc span{fill:#333;color:#333;}#mermaid-svg-Hq4oh1XgHTqSAKjc .node rect,#mermaid-svg-Hq4oh1XgHTqSAKjc .node circle,#mermaid-svg-Hq4oh1XgHTqSAKjc .node ellipse,#mermaid-svg-Hq4oh1XgHTqSAKjc .node polygon,#mermaid-svg-Hq4oh1XgHTqSAKjc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Hq4oh1XgHTqSAKjc .rough-node .label text,#mermaid-svg-Hq4oh1XgHTqSAKjc .node .label text,#mermaid-svg-Hq4oh1XgHTqSAKjc .image-shape .label,#mermaid-svg-Hq4oh1XgHTqSAKjc .icon-shape .label{text-anchor:middle;}#mermaid-svg-Hq4oh1XgHTqSAKjc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Hq4oh1XgHTqSAKjc .rough-node .label,#mermaid-svg-Hq4oh1XgHTqSAKjc .node .label,#mermaid-svg-Hq4oh1XgHTqSAKjc .image-shape .label,#mermaid-svg-Hq4oh1XgHTqSAKjc .icon-shape .label{text-align:center;}#mermaid-svg-Hq4oh1XgHTqSAKjc .node.clickable{cursor:pointer;}#mermaid-svg-Hq4oh1XgHTqSAKjc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Hq4oh1XgHTqSAKjc .arrowheadPath{fill:#333333;}#mermaid-svg-Hq4oh1XgHTqSAKjc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Hq4oh1XgHTqSAKjc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Hq4oh1XgHTqSAKjc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Hq4oh1XgHTqSAKjc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Hq4oh1XgHTqSAKjc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Hq4oh1XgHTqSAKjc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Hq4oh1XgHTqSAKjc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Hq4oh1XgHTqSAKjc .cluster text{fill:#333;}#mermaid-svg-Hq4oh1XgHTqSAKjc .cluster span{color:#333;}#mermaid-svg-Hq4oh1XgHTqSAKjc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Hq4oh1XgHTqSAKjc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Hq4oh1XgHTqSAKjc rect.text{fill:none;stroke-width:0;}#mermaid-svg-Hq4oh1XgHTqSAKjc .icon-shape,#mermaid-svg-Hq4oh1XgHTqSAKjc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Hq4oh1XgHTqSAKjc .icon-shape p,#mermaid-svg-Hq4oh1XgHTqSAKjc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Hq4oh1XgHTqSAKjc .icon-shape .label rect,#mermaid-svg-Hq4oh1XgHTqSAKjc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Hq4oh1XgHTqSAKjc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Hq4oh1XgHTqSAKjc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Hq4oh1XgHTqSAKjc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 任务执行
任务存储
时间轮结构
指针移动
时间轮 HashedWheelTimer
槽0: 0-99ms
槽1: 100-199ms
槽2: 200-299ms
...
槽N: N*100ms
指针 Pointer
当前槽
任务1: 订单超时
任务2: 优惠券过期
任务3: 消息重试
OrderTimeoutHandler
CouponExpiryHandler
MessageRetryHandler
执行当前槽所有任务
更新数据库
发送消息
调用API
下一个槽
流程图说明:
-
时间轮工作原理图:展示了任务从提交到执行的完整流程,包括时间槽计算、任务存储、指针移动和执行触发等关键步骤。
-
SpringBoot集成架构图:展示了从客户端请求到任务执行的完整架构流程,包括应用层、调度层、业务执行层和异常处理层。
-
数据结构示意图:直观展示了时间轮的内部数据结构,包括时间槽、指针、任务存储和执行流程。
这些流程图帮助读者更直观地理解时间轮算法的工作原理和SpringBoot集成方案的整体架构,增强文章的可读性和理解性。
SpringBoot + 时间轮实战:落地延迟任务
接下来通过完整案例,演示如何在 SpringBoot 中集成时间轮算法,实现订单超时取消、优惠券过期提醒的核心逻辑。
步骤1:引入依赖
在 pom.xml 中添加 Netty 依赖(时间轮核心实现依赖 Netty):
xml
<!-- Netty依赖:提供时间轮核心实现 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-common</artifactId>
<version>4.1.86.Final</version>
</dependency>
步骤2:配置时间轮核心组件
java
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timer;
import java.util.concurrent.ThreadFactory;
/**
* 时间轮配置类
* 自定义线程工厂和时间轮参数,初始化HashedWheelTimer核心对象
*/
public class TimingWheelConfig {
// 时间轮最小刻度(Tick)时长,单位:毫秒
private long tickDuration = 100;
// 时间轮槽(Slot)数量,建议设置为2的幂次方(如512、1024)
private int ticksPerWheel = 1024;
/**
* 初始化HashedWheelTimer对象
* @return 时间轮核心调度器
*/
public Timer hashedWheelTimer() {
return new HashedWheelTimer(
// 自定义线程工厂:命名线程并设置为非守护线程
new ThreadFactory() {
private int counter = 0; // 线程计数器
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "timing-wheel-thread-" + counter++);
thread.setDaemon(false); // 非守护线程:确保任务能执行完成
return thread;
}
},
tickDuration, // 每个刻度的时长
java.util.concurrent.TimeUnit.MILLISECONDS, // 时间单位
ticksPerWheel // 槽数量
);
}
}
步骤3:封装延迟任务管理类
java
import io.netty.util.Timer;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* 延迟任务管理器
* 封装时间轮的任务调度、取消逻辑,维护活跃任务的生命周期
*/
public class DelayedTaskManager {
// 时间轮核心调度器
private Timer timer;
// 存储活跃的延迟任务:key=任务ID,value=任务超时对象
private final Map<String, Timeout> activeTimeouts = new ConcurrentHashMap<>();
// 构造方法:注入时间轮调度器
public DelayedTaskManager(Timer timer) {
this.timer = timer;
}
/**
* 调度延迟任务
* @param task 待执行的延迟任务
* @param delay 延迟时长
* @param unit 时间单位
* @return 任务超时对象(可用于取消任务)
*/
public Timeout scheduleTask(TimerTask task, String taskId, long delay, TimeUnit unit) {
// 提交任务到时间轮,指定延迟时长
Timeout timeout = timer.newTimeout(task, delay, unit);
// 若任务ID不为空,存入活跃任务Map
if (taskId != null && !taskId.isEmpty()) {
activeTimeouts.put(taskId, timeout);
}
return timeout;
}
/**
* 取消延迟任务
* @param taskId 任务ID
* @return 取消结果:true=取消成功,false=任务不存在/已执行
*/
public boolean cancelTask(String taskId) {
Timeout timeout = activeTimeouts.get(taskId);
if (timeout != null) {
boolean cancelled = timeout.cancel(); // 取消任务
if (cancelled) {
activeTimeouts.remove(taskId); // 从活跃任务Map移除
}
return cancelled;
}
return false;
}
/**
* 获取活跃任务数量
* @return 活跃任务数
*/
public int getActiveTaskCount() {
return activeTimeouts.size();
}
}
步骤4:实现订单超时处理任务
java
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 订单超时处理任务
* 实现TimerTask接口,封装订单超时取消的核心业务逻辑
*/
public class OrderTimeoutHandler implements TimerTask {
private static final Logger logger = LoggerFactory.getLogger(OrderTimeoutHandler.class);
// 任务ID(唯一标识)
private String taskId;
// 订单ID(业务主键)
private String orderId;
// 构造方法:初始化任务参数
public OrderTimeoutHandler(String taskId, String orderId) {
this.taskId = taskId;
this.orderId = orderId;
}
/**
* 任务执行核心方法
* @param timeout 任务超时对象
* @throws Exception 执行异常
*/
@Override
public void run(Timeout timeout) throws Exception {
if (timeout.isCancelled()) { // 检查任务是否已取消
logger.info("订单超时任务已取消,任务ID:{},订单ID:{}", taskId, orderId);
return;
}
logger.info("开始处理订单超时任务,任务ID:{},订单ID:{}", taskId, orderId);
try {
// 1. 更新订单状态为"已取消"
updateOrderStatus(orderId, "CANCELLED");
// 2. 释放订单占用的库存
releaseInventory(orderId);
// 3. 处理订单退款(若有预付款)
processRefund(orderId);
// 4. 发送订单取消通知(短信/站内信)
sendCancellationNotification(orderId);
logger.info("订单超时任务处理完成,订单ID:{}", orderId);
} catch (Exception e) {
logger.error("订单超时任务执行失败,订单ID:{}", orderId, e);
// 可添加任务重试逻辑
}
}
// 更新订单状态
private void updateOrderStatus(String orderId, String status) {
logger.info("更新订单{}状态为{}", orderId, status);
// 实际业务中调用订单DAO/Service更新状态
}
// 释放库存
private void releaseInventory(String orderId) {
logger.info("释放订单{}占用的库存", orderId);
// 实际业务中调用库存服务释放库存
}
// 处理退款
private void processRefund(String orderId) {
logger.info("处理订单{}的退款逻辑", orderId);
// 实际业务中调用支付服务处理退款
}
// 发送取消通知
private void sendCancellationNotification(String orderId) {
logger.info("向用户发送订单{}取消通知", orderId);
// 实际业务中调用消息服务发送通知
}
// Getter方法
public String getTaskId() {
return taskId;
}
public String getOrderId() {
return orderId;
}
}
步骤5:封装接口层调用
java
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* 延迟任务控制层
* 对外提供订单超时任务的创建接口
*/
@RestController
public class TimingWheelController {
// 注入延迟任务管理器
private final DelayedTaskManager taskManager;
// 构造方法注入
public TimingWheelController(DelayedTaskManager taskManager) {
this.taskManager = taskManager;
}
/**
* 创建订单超时延迟任务
* @param orderId 订单ID
* @param minutes 延迟分钟数(如30分钟未支付取消)
* @return 接口响应
*/
@PostMapping("/create/order/timeout/task")
public ResponseEntity<String> createOrderTimeoutTask(
@RequestParam String orderId,
@RequestParam int minutes) {
// 生成唯一任务ID
String taskId = "ORDER_TIMEOUT_" + orderId + "_" + System.currentTimeMillis();
// 构建订单超时处理任务
OrderTimeoutHandler task = new OrderTimeoutHandler(taskId, orderId);
// 提交任务到时间轮:延迟minutes分钟执行
taskManager.scheduleTask(task, taskId, minutes, TimeUnit.MINUTES);
return ResponseEntity.ok(
String.format("订单超时任务创建成功,订单号:%s,超时时间:%d分钟", orderId, minutes)
);
}
}
方案性能对比与核心优势
我们通过表格直观对比三种方案的核心指标:
| 方案 | 时间复杂度 | 内存占用 | 实时性 | 运维复杂度 |
|---|---|---|---|---|
| 定时扫描 | O(n) | 高 | 差 | 低 |
| 消息队列 | O(log n) | 中 | 好 | 高 |
| 时间轮算法 | O(1) | 低 | 优秀 | 低 |
时间轮算法的核心优势总结:
- 性能最优:O(1)的时间复杂度,百万级延迟任务调度仍能保持高效
- 轻量无依赖:无需引入 MQ 等中间件,降低系统耦合和运维成本
- 实时性可控:通过调整 Tick 参数,可灵活控制任务触发的实时性
- 资源占用低:仅需维护时间轮和活跃任务,内存/CPU 消耗远低于定时扫描
最佳实践与注意事项
核心参数配置建议
- tickDuration(刻度时长) :根据业务实时性要求设置,建议100-200毫秒
- 实时性要求高的场景设为100ms
- 非核心场景可设为500ms降低CPU消耗
- ticksPerWheel(槽数量):建议设置为2的幂次方(如512、1024、2048),Netty底层对2的幂次方有优化
- 线程配置:时间轮线程设为非守护线程,避免 JVM 退出导致任务丢失
异常处理最佳实践
任务执行过程中需捕获所有异常,避免单个任务失败导致时间轮线程异常:
java
@Override
public void run(Timeout timeout) throws Exception {
if (timeout.isCancelled()) {
return;
}
try {
// 核心业务逻辑
processOrderTimeout(orderId);
} catch (Exception e) {
logger.error("延迟任务执行失败,订单ID:{}", orderId, e);
// 可选:添加任务重试逻辑(如重新提交到时间轮)
retryTask(orderId);
}
}
// 任务重试逻辑
private void retryTask(String orderId) {
String retryTaskId = "RETRY_ORDER_TIMEOUT_" + orderId + "_" + System.currentTimeMillis();
OrderTimeoutHandler retryTask = new OrderTimeoutHandler(retryTaskId, orderId);
// 延迟5分钟重试
taskManager.scheduleTask(retryTask, retryTaskId, 5, TimeUnit.MINUTES);
}
监控与可观测性
添加核心监控指标,便于排查问题:
java
/**
* 获取时间轮核心监控指标
* @return 监控数据Map
*/
public Map<String, Object> getTimingWheelMetrics() {
Map<String, Object> metrics = new ConcurrentHashMap<>();
metrics.put("activeTaskCount", taskManager.getActiveTaskCount()); // 活跃任务数
metrics.put("currentTime", System.currentTimeMillis()); // 当前时间戳
metrics.put("tickDuration", 100); // 刻度时长(毫秒)
metrics.put("ticksPerWheel", 1024); // 槽数量
return metrics;
}
局限性与补充方案
时间轮算法的核心局限性:系统重启后未执行的任务会丢失。
解决方案:
- 核心业务场景:结合数据库/Redis 持久化延迟任务,系统启动时重新加载未执行的任务
- 非核心场景:接受任务丢失,或通过补偿定时任务兜底
总结
时间轮算法凭借高性能、轻量级、低运维成本的特性,成为处理订单超时、优惠券过期、消息重试等延迟任务场景的最优解之一。
相较于传统的定时扫描方案,它在性能和实时性上有质的提升;相较于消息队列方案,它无需依赖外部中间件,降低了系统复杂度和运维成本。
在实际落地时,只需结合业务场景调整核心参数、完善异常处理和监控,即可稳定支撑百万级延迟任务的调度需求。