news 2026/7/17 13:05:44

如何设计分布式延时消息?——以机票购买场景为例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
如何设计分布式延时消息?——以机票购买场景为例

前言

在真实业务中,“延时触发”是一类非常常见但又容易被低估的需求,例如:

  • 机票下单后15 分钟未支付自动取消
  • 订单创建后30 分钟关闭
  • 活动开始前定时推送通知
  • 资源锁定一段时间后自动释放

单机系统中,这类需求实现并不复杂;
但在分布式、高并发、可扩展系统中,延时消息的设计就变得非常关键。

本文将以「购买机票超时未支付自动取消订单」为例,循序渐进讲清楚:

  • 本地延时是如何实现的
  • 本地方案的局限在哪里
  • 分布式延时消息的几种主流设计方案
  • 业界(RocketMQ)是如何解决延时消息问题的
  • 一个可落地的分布式延时消息设计思路

业务场景抽象:机票超时未支付

典型业务流程

  1. 用户下单购买机票
  2. 系统创建订单,状态为「待支付
  3. 系统需要在15 分钟后检查订单
  • 如果已支付 → 不处理

  • 如果未支付 → 自动取消订单,释放座位

这本质上是一个:

“现在 + 延迟时间 → 执行一段逻辑”的问题

本地延时任务的实现方式(单机)

在进入分布式之前,先看最基础的实现方式。

Timer(已不推荐)

Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { cancelOrder(orderId); } }, 15 * 60 * 1000);

问题:

  • 单线程执行
  • 任务异常会影响整个 Timer
  • 无法承载高并发

ScheduledThreadPoolExecutor(推荐)

ScheduledExecutorService executor = Executors.newScheduledThreadPool(4); executor.schedule(() -> { cancelOrder(orderId); }, 15, TimeUnit.MINUTES);

优点:

  • 支持线程池
  • API 简单
  • 本地可靠性较好

本地延时方案的问题

虽然ScheduledThreadPoolExecutor很好用,但只能用于单机,在真实生产环境会遇到:

问题说明
服务重启延时任务直接丢失
集群部署多实例无法协调
扩容缩容任务归属混乱
高并发内存压力大

结论:本地延时 ≠ 分布式延时

从本地延时中抽象可复用的思想

虽然本地方案不可直接用于分布式,但它给了我们重要启发:

延时任务 = 任务 + 触发时间

换句话说,我们只要解决两个问题:

  1. 任务存在哪里

  2. 什么时候被取出来执行

分布式延时消息的核心设计思路

核心目标

  • 可靠存储:服务重启不丢任务
  • 可水平扩展
  • 时间精度可控
  • 高吞吐

分布式延时消息方案一:外部存储 + 定时扫描

设计思路

将延时消息存储在外部系统中:

(orderId, executeTime, payload, status)

然后由后台线程周期性扫描

SELECT * FROM delay_task WHERE execute_time <= now() AND status = 'NEW' LIMIT 100;

架构示意

下单 → 写延时任务表 → 定时扫描 → 执行业务

优缺点分析

优点:

  • 实现简单
  • 可控性强
  • 易于理解

缺点:

  • 扫描数据库压力大
  • 时间精度有限(秒级)
  • 高并发下性能瓶颈明显

适合:中小规模系统

分布式延时消息方案二:Redis 实现

Redis ZSet(推荐)

利用 ZSet 的score表示时间戳:

key: delay:order score: executeTimestamp value: orderId

写入延时任务

ZADD delay:order 1700000000 order123

消费逻辑

ZRANGEBYSCORE delay:order -inf now LIMIT 0 100

取到后:

  • 执行业务
  • ZREM删除任务

优缺点

优点:

  • 性能极高
  • 实现相对简单
  • 天然支持排序

缺点:

  • Redis 内存成本
  • 数据持久性依赖 Redis 配置
  • 需要处理重复消费、幂等

业界使用非常广泛

分布式延时消息方案三:时间轮(Time Wheel)

核心思想

将时间划分为多个“槽位”:

| 0 | 1 | 2 | 3 | 4 | 5 | ... |

每个槽代表一个时间区间,任务被放入对应槽位。

特点

  • 插入和触发复杂度接近 O(1)
  • 非常适合大量延时任务

局限

  • 实现复杂
  • 精度有限
  • 通常需要多级时间轮

Netty、Kafka、RocketMQ 都采用了时间轮思想

业界成熟方案:RocketMQ 延时消息

RocketMQ 的做法

RocketMQ不支持任意时间延时,而是采用:

固定等级延时

例如:

Level延时时间
11s
25s
310s
430s
51m
......

实现原理简述

  • 延时消息写入特殊 Topic
  • 使用时间轮 + 定时调度
  • 到期后转发到真实 Topic

优缺点

优点:

  • 高性能
  • 高可靠
  • 生产级方案

缺点:

  • 延时时间不灵活
  • 强依赖 MQ

一个完整的分布式延时消息落地方案(机票)

推荐组合方案

下单 ↓ 发送延时消息(Redis ZSet / RocketMQ) ↓ 延时到期 ↓ 消费者校验订单状态 ↓ 未支付 → 取消订单

关键设计点

  • 业务幂等
  • 状态二次校验
  • 延时消息 ≠ 定时任务
  • 失败重试机制

总结

方案适用场景
本地延时单机、简单系统
DB 扫描小规模、低频
Redis ZSet高并发、灵活延时
时间轮超大规模
RocketMQ企业级

延时消息的本质不是“等多久”,而是“何时可靠地执行一次”

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/15 2:56:12

9 个降AI率工具,自考人必备的降重神器!

9 个降AI率工具&#xff0c;自考人必备的降重神器&#xff01; AI降重工具&#xff1a;自考人论文的“隐形守护者” 随着人工智能技术的迅猛发展&#xff0c;越来越多的自考学生在论文写作过程中开始依赖AI工具来提升效率。然而&#xff0c;AI生成的内容往往存在明显的痕迹&…

作者头像 李华
网站建设 2026/7/15 18:08:51

9 个降AI率工具,自考人必备!

9 个降AI率工具&#xff0c;自考人必备&#xff01; AI降重工具&#xff0c;自考论文的“隐形助手” 在当前的学术环境中&#xff0c;越来越多的高校和考试机构开始使用AIGC检测系统来评估论文的原创性。对于自考学生来说&#xff0c;这无疑增加了论文写作的难度。为了应对这一…

作者头像 李华
网站建设 2026/7/17 3:20:01

旅行记录应用新建旅行 - Cordova OpenHarmony 混合开发实战

欢迎大家加入开源鸿蒙跨平台开发者社区&#xff0c;一起共建开源鸿蒙跨平台生态。 &#x1f4cc; 概述 新建旅行功能允许用户创建新的旅行记录。用户需要填写旅行的基本信息&#xff0c;如目的地、开始日期、结束日期、花费等。新建旅行页面提供了表单验证、自动保存、图片上传…

作者头像 李华
网站建设 2026/7/16 18:05:09

9 个降AI率工具推荐,继续教育学生必备

9 个降AI率工具推荐&#xff0c;继续教育学生必备 AI降重工具&#xff1a;让论文更自然&#xff0c;让学术更纯粹 在当前的学术环境中&#xff0c;随着AI技术的广泛应用&#xff0c;越来越多的学生和研究者开始面临“AIGC率过高”的问题。无论是使用AI生成初稿&#xff0c;还是…

作者头像 李华
网站建设 2026/7/16 3:59:09

Java八股文(Java基础面试题)

为什么重写 equals() 时必须重写 hashCode() 方法&#xff1f; 因为两个相等的对象的hashCode值一定相等&#xff0c;也就是说如果equals()方法判断两个对象是相等的&#xff0c;那这两个对象的hashCode值也要相等。如果重写equals()时没有重写hashCode()方法的话就可能会导致…

作者头像 李华
网站建设 2026/7/16 18:05:07

邦芒忠告:职场中没有好人缘的10种人

在职场中&#xff0c;某些行为模式容易导致人际关系紧张&#xff0c;影响团队合作和个人发展。以下几种类型的人往往难以获得良好的人缘&#xff1a;1‌、说话刻薄直接的人‌&#xff1a;这类人习惯于用犀利、不留余地的语言指出问题&#xff0c;虽然可能自认为是直言不讳&…

作者头像 李华