局域网文件传输神器LocalSend与KDE Connect实战指南
2026/9/5 12:56:48
在真实业务中,“延时触发”是一类非常常见但又容易被低估的需求,例如:
在单机系统中,这类需求实现并不复杂;
但在分布式、高并发、可扩展系统中,延时消息的设计就变得非常关键。
本文将以「购买机票超时未支付自动取消订单」为例,循序渐进讲清楚:
- 本地延时是如何实现的
- 本地方案的局限在哪里
- 分布式延时消息的几种主流设计方案
- 业界(RocketMQ)是如何解决延时消息问题的
- 一个可落地的分布式延时消息设计思路
如果已支付 → 不处理
如果未支付 → 自动取消订单,释放座位
这本质上是一个:
“现在 + 延迟时间 → 执行一段逻辑”的问题
在进入分布式之前,先看最基础的实现方式。
Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { cancelOrder(orderId); } }, 15 * 60 * 1000);问题:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(4); executor.schedule(() -> { cancelOrder(orderId); }, 15, TimeUnit.MINUTES);优点:
虽然ScheduledThreadPoolExecutor很好用,但只能用于单机,在真实生产环境会遇到:
| 问题 | 说明 |
|---|---|
| 服务重启 | 延时任务直接丢失 |
| 集群部署 | 多实例无法协调 |
| 扩容缩容 | 任务归属混乱 |
| 高并发 | 内存压力大 |
结论:本地延时 ≠ 分布式延时
虽然本地方案不可直接用于分布式,但它给了我们重要启发:
延时任务 = 任务 + 触发时间
换句话说,我们只要解决两个问题:
任务存在哪里
什么时候被取出来执行
将延时消息存储在外部系统中:
(orderId, executeTime, payload, status)然后由后台线程周期性扫描:
SELECT * FROM delay_task WHERE execute_time <= now() AND status = 'NEW' LIMIT 100;下单 → 写延时任务表 → 定时扫描 → 执行业务优点:
缺点:
适合:中小规模系统
利用 ZSet 的score表示时间戳:
key: delay:order score: executeTimestamp value: orderIdZADD delay:order 1700000000 order123ZRANGEBYSCORE delay:order -inf now LIMIT 0 100取到后:
ZREM删除任务优点:
缺点:
业界使用非常广泛
将时间划分为多个“槽位”:
| 0 | 1 | 2 | 3 | 4 | 5 | ... |每个槽代表一个时间区间,任务被放入对应槽位。
Netty、Kafka、RocketMQ 都采用了时间轮思想
RocketMQ不支持任意时间延时,而是采用:
固定等级延时
例如:
| Level | 延时时间 |
|---|---|
| 1 | 1s |
| 2 | 5s |
| 3 | 10s |
| 4 | 30s |
| 5 | 1m |
| ... | ... |
优点:
缺点:
下单 ↓ 发送延时消息(Redis ZSet / RocketMQ) ↓ 延时到期 ↓ 消费者校验订单状态 ↓ 未支付 → 取消订单| 方案 | 适用场景 |
|---|---|
| 本地延时 | 单机、简单系统 |
| DB 扫描 | 小规模、低频 |
| Redis ZSet | 高并发、灵活延时 |
| 时间轮 | 超大规模 |
| RocketMQ | 企业级 |
延时消息的本质不是“等多久”,而是“何时可靠地执行一次”