订单超时处理
几种常见的处理办法
在软件开发中,经常会遇到延时任务的需求,如订单生成 15 分钟未支付自动取消、60 秒后给用户发送短信通知等。延时任务和定时任务有所不同,定时任务有明确触发时间和固定执行周期,而延时任务在事件触发后一段时间内执行,无固定周期,通常针对单个任务。
对于判断订单是否超时,有以下几种常见方案及优缺点:
方案一:定时查询数据库
通过线程定时扫描数据库,根据订单时间判断是否超时并执行相应操作。在单体应用中,可使用 Spring Task
或 Quartz
等单机任务调度工具;在集群环境下,可使用支持分布式任务调度的工具,如 XXL-Job
、Elastic Job
等。
优点:简单且支持集群,是小项目的首选。 缺点:对服务器内存消耗大,存在延迟,对数据库性能影响大,部分工具部署和配置有成本。
方案二:使用 JDK 延迟队列
JDK 中有实现无界阻塞队列的 DelayQueue
,可在延迟期满时获取元素,方便实现订单超时检测。put
方法获取元素为空时返回空,take
方法未获取到元素会一直等待。
优点:效率高,任务触发延迟低。 缺点:服务器重启数据丢失,集群拓展麻烦,内存有限制,代码维护复杂度高。
方案三:时间轮算法
模拟现实时钟,有一轮的 TIK
数、每转一次的持续时间和单位时间等关键参数。
优点:效率较高,代码复杂度较低。 缺点:数据存储在内存,存在服务器重启数据丢失和集群拓展麻烦的问题,也有内存限制。
方案四:Redis 缓存
- 定时扫描有序集合:将订单号作为
key
,时间戳作为value
,按时间戳排序,判断是否超时。 - 利用过期监听机制:设置订单号
key
的过期时间,监听过期执行相应操作。 - 客户端缓存监听:本地缓存与
Redis
缓存关联,通过发布订阅机制通知变更。
优点:对数据库压力小,具备主动推送功能,方便集群拓展,时间准确度高,重启可读取数据处理,可指定实例负责取消减少消息数量。
缺点:需要额外维护 Redis
,长连接重连需补偿 ,Redis
集群变化时需处理实例预分配。
方案五:消息队列方案
以 RocketMQ
为例,其支持延迟消息,可指定时间消费,也可利用死信队列实现类似功能。
优点:消息及时投递,集群支持好,代码量小。
缺点:依赖重量级 MQ
组件,需熟悉其机制,处理幂等性等问题。
综上所述,不同的方案各有优缺点,在实际开发中,应根据项目规模、技术架构和需求特点选择合适的方案来实现延时任务。
Redisson解决订单超时
我们可以使用Redis的Zset结构,过期时间作为分数,订单号作为key
创建订单,保存到Redis的Zset中
// 订单信息保存到redis 采用zest数据结构
String orderNo = order.getOrderNo();
String key = RedisKey.getKey(RedisKey.ORDER_LIST);
long score = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(PayConstant.ORDER_EXPIRE_TIME);
RScoredSortedSet<String> scoredSortedSet = redissonClient.getScoredSortedSet(key);
scoredSortedSet.add(score, orderNo);
再开启一个定时任务,每秒去扫描有没有过期的订单:
// 1s执行一次 移除过期订单
@Scheduled(fixedRate = 1000)
public void removeExpiredOrder() {
String key = RedisKey.getKey(RedisKey.ORDER_LIST);
RScoredSortedSet<String> orderSet = redissonClient.getScoredSortedSet(key);
// 获取”当前时间 > score”的延时任务 获取0 到当前时间的所有订单 true表示包含当前时间 [0,当前时间]
Collection<String> values = orderSet.valueRange(0, true, System.currentTimeMillis(), true);
// 转为list
List<String> expiredOrderList = (List<String>) values;
expiredOrderList.forEach(orderNo -> {
log.warn("