SpringBoot 时间轮实现延时任务

作者:风象南日期:2025/10/30

传统方案的困境

在日常开发中,我们经常需要处理各种定时任务:用户注册后的欢迎邮件、订单超时自动取消、缓存定期刷新等。传统的定时器方案在面对大规模定时任务时往往力不从心:

性能瓶颈日益凸显

  • ScheduledExecutor在处理上千个任务时性能急剧下降
  • Timer类不仅线程不安全,还存在单点故障风险
  • 每次调度都要在堆中查找最小元素,时间复杂度O(log n)
  • 频繁的GC压力导致系统吞吐量受限

业务需求日益复杂

  • 消息重试需要指数退避策略
  • 分布式系统需要精确的延迟调度
  • 会话管理需要动态添加删除任务
  • 限流器需要高效的时间窗口控制

时间轮的诞生

时间轮(Timing Wheel) 灵感来源于我们日常使用的时钟。想象一下老式机械表的工作原理

  • 表盘被分成12个小时刻度,时针每12小时转一圈
  • 分针每分钟移动一格,60分钟转一圈
  • 秒针每秒移动一格,60秒转一圈

时间轮借鉴了这个思想,将时间划分成固定的槽位,通过指针的移动来调度任务。这种巧妙的设计将时间维度空间化,用简单的指针移动代替了复杂的堆操作。

核心设计哲学

时间离散化:将连续时间分割成等长的tick间隔

空间映射:每个时间间隔对应一个槽位

批量触发:同一槽位的任务一起执行

轮次计数:多圈任务通过轮数计算

为什么时间轮如此高效?

时间轮的巧妙之处在于它彻底改变了任务调度的思路:

时间复杂度的革命 从O(log n)到O(1)

  • 传统方案:需要在堆中查找最小元素
  • 时间轮:直接计算目标槽位,一次到位

内存访问的优化

  • 顺序访问数组元素,CPU缓存命中率高
  • 相比堆结构的随机访问,性能提升明显

批量处理的威力

  • 同一槽位的多个任务批量触发
  • 减少线程切换和调度开销
  • 提高系统的整体吞吐量

算法设计:从时钟模型到数据结构

时间轮的工作原理

想象一个真实的时钟

  • 时钟有12个小时刻度,每个刻度代表一个小时
  • 秒针每秒移动一格,分针每分钟移动一格,时针每小时移动一格
  • 我们可以通过指针的位置确定当前时间

时间轮采用了类似的概念

  • 将时间划分为N个槽位(通常是2的幂次,如512个)
  • 每个槽位代表一个固定的时间间隔(如100ms)
  • 一个指针周期性地移动到下一个槽位
  • 当指针到达某个槽位时,执行该槽位中的所有任务

核心数据结构设计

时间轮主体结构

1时间轮数组:
2[Slot0][Slot1][Slot2]...[Slot511]
3  ↑       ↑       ↑         ↑
4 指针    100ms200ms51.1秒后
5

槽位(Slot)设计

  • 使用ConcurrentLinkedQueue存储任务列表,保证线程安全
  • 维护任务计数器,便于快速统计
  • 记录最后访问时间,用于监控和清理

任务包装器(TimerTaskWrapper)

  • 包装原始任务和相关信息
  • 记录任务的到期时间和需要经过的轮数
  • 维护任务状态(等待、运行、完成、失败、取消)
  • 提供任务进度和剩余时间计算

算法关键步骤

任务调度算法

  1. 计算任务需要经过的tick数量:ticks = delayMs / tickDuration
  2. 计算目标槽位:targetSlot = (currentSlot + ticks) % slotSize
  3. 计算需要经过的轮数:rounds = ticks / slotSize
  4. 将任务包装后放入目标槽位

指针移动算法

  1. 周期性地将指针移动到下一个槽位
  2. 处理当前槽位中的所有任务
  3. 对于未到期的任务,轮数减1后重新入槽
  4. 对于到期的任务,提交到工作线程池执行

多轮任务处理

  • 当任务延迟时间超过一圈时,需要记录剩余轮数
  • 每次指针经过时,如果轮数>0,则轮数减1
  • 只有轮数为0的任务才会被执行

核心实现:高性能调度引擎

线程模型设计

双线程池架构是时间轮高性能的关键

1调度线程池(单线程):
23周期性移动指针
45处理槽位任务
67提交到工作线程池
8
9工作线程池(多线程):
1011并发执行任务
1213处理业务逻辑
1415更新任务状态
16

调度线程池

  • 使用单线程避免并发问题
  • 负责指针移动和槽位处理
  • 通过scheduleAtFixedRate实现周期性调度
  • 设置为守护线程,不阻止JVM退出

SpringBoot的完整实现

服务层

TimingWheelService设计

1@Service
2public class TimingWheelService {
3    // 任务管理
4    public String scheduleTask(TimerTask task, long delayMs);
5    public boolean cancelTask(String taskId);
6    public List<TimerTaskWrapper> getActiveTasks();
7
8    // 统计信息
9    public TimingWheelStats getStats();
10    public TaskExecutionStats getExecutionStats();
11
12    // 任务清理
13    public int cleanupCompletedTasks();
14
15    // 示例任务
16    public String createSampleTask(String type, long delayMs);
17    public List<String> createBatchTasks(int count, long minDelay, long maxDelay);
18}
19

核心配置类

1/**
2 * 时间轮配置类
3 */
4@Configuration
5@EnableConfigurationProperties(TimingWheelProperties.class)
6public class TimingWheelConfig {
7
8    @Bean
9    public TimingWheel timingWheel(TimingWheelProperties properties, MeterRegistry meterRegistry) {
10        log.info("Creating timing wheel with properties: {}", properties);
11        return new TimingWheel(properties, meterRegistry);
12    }
13
14    @Bean
15    public MetricsConfig metricsConfig(MeterRegistry meterRegistry) {
16        return new MetricsConfig(meterRegistry);
17    }
18
19    @Bean
20    public WebConfig webConfig() {
21        return new WebConfig();
22    }
23}
24

REST API接口

1/**
2 * 时间轮控制器
3 */
4@RestController
5@RequestMapping("/api/timingwheel")
6@CrossOrigin(origins = "*")
7public class TimingWheelController {
8
9    @Autowired
10    private TimingWheelService timingWheelService;
11
12    /**
13     * 获取时间轮统计信息
14     */
15    @GetMapping("/stats")
16    public ResponseEntity<TimingWheel.TimingWheelStats> getStats() {
17        TimingWheel.TimingWheelStats stats = timingWheelService.getStats();
18        return ResponseEntity.ok(stats);
19    }
20
21    /**
22     * 创建示例任务
23     */
24    @PostMapping("/tasks/sample")
25    public ResponseEntity<Map<String, Object>> createSampleTask(@RequestBody Map<String, Object> request) {
26        String type = (String) request.getOrDefault("type", "simple");
27        long delay = ((Number) request.getOrDefault("delay", 1000)).longValue();
28
29        String taskId = timingWheelService.createSampleTask(type, delay);
30
31        Map<String, Object> response = new HashMap<>();
32        response.put("taskId", taskId);
33        response.put("type", type);
34        response.put("delay", delay);
35        response.put("message", "Task created successfully");
36
37        return ResponseEntity.ok(response);
38    }
39
40    /**
41     * 批量创建任务
42     */
43    @PostMapping("/tasks/batch")
44    public ResponseEntity<Map<String, Object>> createBatchTasks(@RequestBody Map<String, Object> request) {
45        int count = (Integer) request.getOrDefault("count", 10);
46        long minDelay = ((Number) request.getOrDefault("minDelay", 1000)).longValue();
47        long maxDelay = ((Number) request.getOrDefault("maxDelay", 10000)).longValue();
48
49        List<String> taskIds = timingWheelService.createBatchTasks(count, minDelay, maxDelay);
50
51        Map<String, Object> response = new HashMap<>();
52        response.put("taskIds", taskIds);
53        response.put("count", taskIds.size());
54        response.put("message", "Batch tasks created successfully");
55
56        return ResponseEntity.ok(response);
57    }
58
59    /**
60     * 压力测试
61     */
62    @PostMapping("/stress-test")
63    public ResponseEntity<Map<String, Object>> stressTest(@RequestBody Map<String, Object> request) {
64        int taskCount = (Integer) request.getOrDefault("taskCount", 1000);
65        long minDelay = ((Number) request.getOrDefault("minDelay", 100)).longValue();
66        long maxDelay = ((Number) request.getOrDefault("maxDelay", 5000)).longValue();
67
68        long startTime = System.currentTimeMillis();
69        List<String> taskIds = timingWheelService.createBatchTasks(taskCount, minDelay, maxDelay);
70        long endTime = System.currentTimeMillis();
71
72        Map<String, Object> response = new HashMap<>();
73        response.put("taskCount", taskIds.size());
74        response.put("creationTime", endTime - startTime);
75        response.put("throughput", taskIds.size() * 1000.0 / (endTime - startTime));
76        response.put("message", "Stress test completed successfully");
77
78        return ResponseEntity.ok(response);
79    }
80}
81

应用配置

1# application.yml
2server:
3  port: 8081
4  servlet:
5    context-path: /
6
7spring:
8  application:
9    name: springboot-timingwheel
10
11# Timing Wheel Configuration
12timingwheel:
13  config:
14    slot-size: 512
15    tick-duration: 100
16    worker-threads: 4
17    enable-multi-wheel: true
18    enable-metrics: true
19    task-timeout: 30000
20
21logging:
22  level:
23    com.example.timingwheel: DEBUG
24    org.springframework.web: INFO
25  pattern:
26    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
27

总结

本文展示了时间轮的设计与实现,从算法原理到可视化监控。时间轮通过巧妙的时间维度空间化思想,用简单的指针移动实现了高效的定时任务调度,在高并发场景下展现出卓越的性能优势。

github.com/yuboon/java…


SpringBoot 时间轮实现延时任务》 是转载文章,点击查看原文


相关推荐


BSON vs JSON:不只是"二进制"这么简单
风象南2025/10/27

前言 当今项目开发,大多以JSON作为各个场景的标准数据格式。从 REST API 到配置文件,从 NoSQL 数据库到日志记录,JSON 几乎无处不在。然而,在 MongoDB 等 NoSQL 数据库的生态系统中,我们经常听到另一个名词:BSON。 很多人对 BSON 的理解停留在"二进制的 JSON"这个层面,认为它只是 JSON 的二进制编码版本。但实际上,BSON 的设计理念和实现细节远比这个简单的描述要丰富和深刻得多。 JSON 的优势与局限 JSON 的优势 JSON 之所以能够成为


ES6+革命:8大特性让你的JavaScript代码质量翻倍
良山有风来2025/10/24

最近review代码的时候,看到一些还在用var声明变量、用function写满屏回调的代码,我真的有点头疼。 你是不是也遇到过这样的困扰:代码写着写着就乱了,变量莫名其妙被修改,回调嵌套到怀疑人生?其实这些问题,ES6+早就给出了优雅的解决方案。 今天我就带你彻底告别老旧的JS写法,用8个核心特性让你的代码质量直接翻倍!每个特性我都会配上详细注释的代码示例,保证你能立刻上手。 let和const:告别变量提升的噩梦 还记得用var时那些诡异的现象吗?变量莫名其妙被提升,循环计数器失效... l


STM32学习(MCU控制)(GPIO)
D.....l2025/10/22

文章目录 MCU 和 GPIO1. 单片机 MCU1.1 单片机和嵌入式系统1.2 MCU1.3 ARM 公司1.4 市场主流 32 芯片1.5 STM32 开发版概述 2. GPIO2.1 GPIO 概述2.2 STM32F103ZET6 GPIO 相关内容2.3 GPIO 开发流程2.4 GPIO 控制 LED 灯2.5 GPIO 端口内部基本电路情况**2.5.1. 浮空输入模式(Floating Input)****2.5.2. 上拉输入模式(Pull - up Inpu


【Node】认识multer库
你的人类朋友2025/10/21

前言 在 Node.js 中处理文件上传是一个常见的需求,比如用户上传头像或文档。 那么,如何简单、安全地接收并保存这些文件呢? 本文说说 multer 的库,它可以帮助我们快速实现这一功能。 小问题:为什么使用 multer 而不是 Node.js 原生模块来处理文件上传。 🤔 补充知识:原生模块如何实现文件上传? 使用 Node.js 原生模块实现文件上传需要手动解析 multipart/form-data 格式的请求体,处理数据流并识别边界符。 然后逐个字段提取文件内容和元数据,最后通


ip rule 策略路由
疯狂吧小飞牛2025/10/20

原文地址:ip rule 策略路由 欢迎参观我的网站:无敌牛 – 技术/著作/典籍/分享等 ip rule 是 Linux 策略路由(Policy-based Routing, PBR) 的核心命令,用于控制内核在查找路由时使用哪张路由表。 一、ip rule 基本语法 ip rule [list|add|del] [selectors] [action] selectors:匹配条件(如源 IP、入接口等)action:匹配后执行的动作(最常见的是 table N,即使用路由表


【汽车篇】AI深度学习在汽车轮胎X-ray缺陷检测应用方案
Dongsheng_20192025/10/19

一、行业痛点 轮胎安全=生命安全 起鼓、胎侧凹陷、开裂、微孔、气泡等内部缺陷肉眼不可见,传统人工敲听+抽检: 漏检率 2%–5%,一旦流出即面临召回及高额索赔; 节拍慢,单胎平均 45 s,无法满足 70 JPH 下线节奏; 结果无数据留存,难以追溯工艺根因。 二、技术方案 东声智能“AI+X-ray”在线检测系统,将穿透成像与深度学习合二为一,为轮胎内部质量打造零缺陷防线。 1.缺陷定位算法: 3D 模块精准标定起鼓、凹陷、气泡等 12 类缺陷中心坐标。 2.双擎决策:传统算法分


苦练Python第67天:光速读取任意行,linecache模块解锁文件处理新姿势
倔强青铜三 VIP.1 初学乍练2025/10/17

前言 大家好,我是 倔强青铜三。欢迎关注我,微信公众号: 倔强青铜三。点赞、收藏、关注,一键三连! 今天咱们把 Python 自带的“光速行读取器”—— linecache 模块,从开箱到实战一次性讲透。 一、为什么需要 linecache? 秒级随意读任意行:再也不用 for i, line in enumerate(f) 数行号。 内存缓存机制:同一个文件多次读取,只加载一次。 源码级调试神器:traceback、pdb 都在用它。 零依赖:官方出品,随 Python 一起安装。 li


Redis(65)如何优化Redis的AOF持久化?
Victor3562025/10/16

优化 Redis 的 AOF(Append-Only File)持久化机制可以提高数据持久化的效率和性能,并且能够确保数据的安全性和一致性。以下是关于如何优化 Redis AOF 持久化的详细指南,包括操作配置、代码示例及具体步骤。 1. AOF 持久化配置 Redis 提供了多个配置参数来调整 AOF 的行为,主要包括: appendonly appendfsync auto-aof-rewrite-percentage auto-aof-rewrite-min-size aof-load-


C#/.NET/.NET Core技术前沿周刊 | 第 57 期(2025年10.1-10.12)
追逐时光者2025/10/14

前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录、追踪C#/.NET/.NET Core领域、生态的每周最新、最实用、最有价值的技术文章、社区动态、优质项目和学习资源等。让你时刻站在技术前沿,助力技术成长与视野拓宽。 欢迎投稿、推荐或自荐优质文章、项目、学习资源等。 🏆技术前沿周刊Gitee开源地址: gitee.com/ysgdaydayup… 📰技术前沿周刊GitHub开源地址: github.com/YSGStudyHar… 👪DotNetGuid


优选算法-二分:19.搜索插入位置
CoderYanger2025/10/13

题目链接:35.搜索插入位置(简单) 算法思路: 二分:二分查找该元素 没找到就返回最后一次查找的左边界下标(即为插入位置) Java代码: /** * Created with IntelliJ IDEA. * Description: * User: 王洋 * Date: 2025-08-28 * Time: 09:24 */ class Solution { //35. 搜索插入位置 /*给定一个排序数组和一个目标值,在数组中找到目标值,

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0