找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2211

积分

0

好友

293

主题
发表于 3 天前 | 查看: 18| 回复: 0

我们在美团 APP 下单,假如没有立即支付,进入订单详情会显示倒计时,如果超过支付时间,订单就会被自动取消。

网上虽然有很多相关的文章,但不少方案其实不太适合真实的业务场景。今天,我们就来深入剖析一下如何设计订单超时自动取消的功能,希望能带给大家一些更贴近实战的启发。

订单超时自动取消方案思维导图

1 定时任务方案

首先,我们最容易想到的自然是定时任务的方案。

方案流程

  1. 每隔 30 秒查询数据库,取出最近的 N 条未支付的订单。
  2. 遍历查询出来的订单列表,判断当前时间减去订单的创建时间是否超过了支付超时时间,如果超时则对该订单执行取消操作。

这个方案工程实现相对简单,但它会间隔性地对数据库造成一定的 IO 压力。特别是当订单量非常大时,高频次的查询对数据库的性能是个不小的考验。

从功能模块角度来看,定时任务方案通常包含 调度层业务逻辑层 两部分。

定时任务架构分层示意图

关于定时任务的实现策略,我们可以简单划分为 单机版集群版

2 定时任务方案:单机版

我们可以使用 TimerScheduledExecutorServiceQuartz 非常容易地实现单机定时任务。

单机版定时任务实现方案

但笔者并不推荐使用单机版的方案,举个简单的例子:

假设我们应用 A 通过 Quartz 调度三个定时任务 A、B、C,当集群部署时,可能会出现多台不同机器实例同时执行相同任务的风险。

集群部署任务重复执行问题

此时,我们可以通过加锁的方式来规避,如下图所示:

通过Redis锁避免任务重复执行

但这种方式并不优雅,同时定时任务应用内调度层会经常空跑。我们更希望的是三个定时任务 A、B、C 能均匀分布在应用 A 的不同实例中。接下来,笔者会介绍亲身经历的三种集群版定时任务方案。

3 定时任务方案:集群版

集群版定时任务方案

3.1 Quartz + JDBCJobStore

Quartz 本身支持集群模式,但需要在数据库中添加 11 张特定的表,对业务系统有一定的侵入性。

Quartz集群调度架构图

笔者曾经服务的一家彩票公司,订单调度中心就是使用 Quartz 的集群模式,实现了日均百万订单的调度处理。

需要特别注意的是:基于底层数据库悲观锁的机制,Quartz 的集群模式性能并不算高。如果执行频率高的任务数量达到一定规模,可能会出现性能瓶颈。

3.2 Elastic-Job

ElasticJob 定位为轻量级无中心化解决方案,使用 jar 包的形式提供分布式任务的协调服务。

ElasticJob 从本质上来讲,底层的任务调度还是通过 Quartz,它的优势在于可以依赖 Zookeeper 作为协调中心,将任务通过负载均衡算法分配给应用内的 Quartz Scheduler 容器。

举例:应用 A 有五个任务需要执行,分别是 A,B,C,D,E。任务 E 需要分成四个子任务,应用部署在两台机器上。

Elastic-Job任务分配示意图

图中,应用 A 在启动后,5 个任务通过 Zookeeper 协调后被分配到两台机器上,通过各自的 Quartz Scheduler 执行不同的任务。

相比 Quartz 集群模式,ElasticJob 的可扩展性更高,同时因为是本地内存存储 JOB,性能也非常好。但是 ElasticJob 的控制台功能相对粗糙,主要因为它基于 Quartz + Zookeeper 的实现机制。通过控制 Zookeeper 节点来间接操作应用内任务,这种方式不够灵活。所以笔者认为 ElasticJob 更多的还是定位于一个框架,而不是一个完整的调度平台

3.3 任务调度平台

笔者非常认可任务调度平台这种模式。XXL-JOB 是一个使用最广泛的分布式 任务调度平台

XXL-JOB任务管理界面

任务调度平台执行流程

在这种模式下,业务系统和调度平台分开部署。我们在调度中心上配置应用以及其定时任务,当任务需要执行时,调度平台会触发业务系统的任务,业务系统执行完任务之后,再将结果反馈给调度平台。

业务系统和调度平台都可以水平扩展以实现高可用,同时在调度平台可以配置灵活的调度策略(比如 重试机制广播模式 等)。

XXL-JOB 并不完美,因为其底层依然是基于数据库悲观锁的机制,虽然通过时间轮等方式做了一定程度的优化,但依然存在性能瓶颈。很多公司,比如神州专车、美团,都有自己自研的任务调度平台。这种模式非常适合多团队协作,便于大规模调度任务的统一管理。如果你使用 Spring Boot 等框架进行开发,可以很方便地与之集成。

4 延时消息方案

延时消息是一种非常优雅的实现模式。订单服务生成订单后,发送一条延时消息到消息队列。消息队列在消息到达支付过期时间时,将消息投递给消费者,消费者收到消息之后,判断订单状态是否为已支付,假如未支付,则执行取消订单的逻辑。

订单延时消息处理流程图

4.1 消息队列 RocketMQ

RocketMQ 4.X 生产者发送延迟消息的代码如下:

Message msg = new Message();
msg.setTopic("TopicA");
msg.setTags("Tag");
msg.setBody("this is a delay message".getBytes());
//设置延迟level为5,对应延迟1分钟
msg.setDelayTimeLevel(5);
producer.send(msg);

RocketMQ 4.X 版本默认支持 18 个 level 的延迟消息,其延迟等级是通过 broker 端的 messageDelayLevel 配置项确定的。

RocketMQ延迟等级配置

RocketMQ 5.X 版本则支持任意时刻的延迟消息,客户端在构造消息时提供了 3 个 API 来指定延迟时间或定时时间。

RocketMQ 5.X 延迟消息API

如果技术团队的基础架构能力较强,笔者非常推荐使用 RocketMQ 5.X 的延迟消息功能。

4.2 自研延迟服务

基于 RocketMQ 4 内置的延迟消息只能支持几个固定的延迟级别。像快手、滴滴等公司,则开发了单独的 Delay Server 来调度延迟消息。

自研延迟服务架构图

上图这个结构并没有直接将延迟消息发到 Delay Server,而是更换 Topic 以后存入 RocketMQ。这样的好处是可以复用现有的消息发送接口(以及其上的所有扩展能力)。对业务方来说,只需要在构造消息的时候额外指定一个延迟时间字段即可,其它用法都不变。

自研单独的 Delay Server 不仅可以适配 RocketMQ 4.X,也可以适配 Kafka,同时也能获得非常高的性能。说实话,这是一个非常实用且灵活的方案。

4.3 Redis 延迟队列

Redis 延迟队列是一个轻量级的解决方案,开源社区有成熟的实现,比如 Redisson。

Redis延迟队列实现原理

图中,我们定义两个集合:

1、zset 集合
生产者将任务信息发送到 zset 集合,value 是任务编号,score 是任务执行的时间戳。

2、list 集合
守护线程检测 zset 集合中到期的任务,若任务到期,将任务编号转移到 list 集合,消费者从 list 集合弹出任务,并执行任务逻辑。

需要强调的是:Redis 虽然可以实现延迟消息的功能,但它并不是真正意义上的消息队列,在使用过程中还是 有小概率会丢失消息 的风险。

5 最佳实践

5.1 并发口诀:一锁二判三更新

不管我们使用定时任务还是延迟消息,都不可避免会遇到 并发执行任务 的情况(比如消息重复消费、调度重试等)。

当我们执行任务时,可以按照 一锁二判三更新 这个口诀来处理:

  1. 锁定 当前需要处理的订单。
  2. 判断 订单是否已经更新过对应状态了。
  3. 如果订单之前没有更新过状态,更新 状态并完成相关业务逻辑;否则本次不进行更新和业务操作。
  4. 释放 当前订单的锁。

并发处理流程图:一锁二判三更新

并发处理伪代码示例

5.2 兜底意识 + 配置监控

虽然我们提到了很多的实现策略,但在现实实战中依然容易出现问题,比如不合理的操作导致消息丢失。

因此,我们应该具备 兜底意识

假如少量消息丢失,我们可以通过每天凌晨跑一次兜底任务,批量将这些未处理的订单取消。这种兜底行为工程实现简单,同时对系统影响很小。

还有一点至关重要,就是 配置监控

笔者曾经自研过任务调度系统,应用 A 接入后,从控制台发现每隔 2 个小时调度应用 A 的任务时,经常发生超时。通过分析,才发现是应用 A 的线程出现了死锁。这种问题出现的几率并不低,因此 配置监控 特别有必要。

对业务系统来讲,监控分为两个层面:系统监控业务监控

  • 系统监控
    在条件允许的情况下,建议关注性能监控、方法可用性监控、方法调用次数监控这三大类。

    系统性能监控示例图
    上图是性能监控的示例图,展示了不同时间段的性能分布,实时统计 TP99、TP999、AVG、MAX 等维度指标,这也是性能调优的重点关注对象。

  • 业务监控
    业务监控是从业务角度出发,各个应用系统需要从业务层面进行哪些监控。具体就是对业务数据、业务功能进行监控,实时收集业务流程的数据,并根据设置的策略对业务流程中不符合预期的部分进行预警和报警。

    比如订单系统中有一个定时结算的服务,每两分钟执行一次。我们可以在定时任务 JOB 中添加埋点,并配置业务监控,假如十分钟内该定时任务没有执行,则发送邮件或短信给相关负责人。

6 总结

这篇文章,我们总结了订单超时自动取消方案的两种主流思路:定时任务延迟消息

1、定时任务
定时任务的实现策略,我们可以简单划分为 单机版集群版。笔者不推荐在生产环境中使用单机版。集群版有三种典型方式:Quartz + JDBCJobStoreElasticJobXXL-JOB。每种方式各有优缺点,从工程化和易管理角度出发,笔者更倾向于任务调度平台(如 XXL-JOB)这种方式。

2、延迟消息
延时消息是一种非常优雅的模式。本文介绍了三种方式:消息队列 RocketMQ自研延迟服务Redis 延迟队列。如果技术团队基础架构能力很强,推荐使用 RocketMQ 或者自研延迟服务。如果仅想用轻量级的实现,可以选择 Redis 延迟队列。

不管采用定时任务还是延迟消息,要保证架构的稳定性,还需要特别注意以下两点:

  1. 并发口诀:一锁二判三更新
  2. 兜底意识 + 配置监控

希望这些来自实战的经验,能帮助你在设计自己的系统时做出更合适的选择。技术方案没有绝对的好坏,关键在于与团队能力和业务场景的匹配。关于系统设计和架构的更多讨论,也欢迎到 云栈社区 交流分享。




上一篇:深入解析消息队列的7大核心应用场景:从异步解耦到数据枢纽
下一篇:阿里P7面试与能力模型全解析:技术专家需要哪些核心技能?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 10:06 , Processed in 0.479413 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表