电商平台中订单未支付过期如何实现自动关单?
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
日常开发中,我们经常遇到这种业务场景,如:外卖订单超 30 分钟未支付,则自动取订单;用户注册成功 15 分钟后,发短信息通知用户等等。这就是延时任务处理场景。 在电商,支付等系统中,一设都是先创建订单(支付单),再给用户一定的时间进行支付,如果没有按时支付的话,就需要把之前的订单(支付单)取消掉。这种类以的场景有很多,还有比如到期自动收货,超时自动退款,下单后自动发送短信等等都是类似的业务问题。 定时任务(数据库轮询)通过定时任务关闭订单,是一种成本很低,实现也很容易的方案。通过简单的几行代码,写一个定时任务,定期扫描数据库中的订单,如果时间过期,就将其状态更新为关闭即可。
优点:实现容易,成本低,基本不依赖其他组件。 缺点:
总结:采用定时任务的方案比较适合对时间要求不是很敏感,并且数据量不太多的业务场景。 JDK 延迟队列 DelayQueueDelayQueue是JDK提供的一个无界队列,DelayQueue队列中的元素需要实现Delayed,它只提供了一个方法,就是获取过期时间。 用户的订单生成以后,设置过期时间比如30分钟,放入定义好的DelayQueue,然后创建一个线程,在线程中通过while(true)不断的从DelayQueue中获取过期的数据。 优点:不依赖任何第三方组件,连数据库也不需要了,实现起来也方便。 缺点:
总结:DelayQueue适用于数据量较小,且丢失也不影响主业务的场景,比如内部系统的一些非重要通知,就算丢失,也不会有太大影响。 redis过期监听redis 是一个高性能的KV 数据库,除了用作缓存以外,其实还提供了过期监听的功能。在redis.conf 中,配置notify-keyspace-events Ex 即可开启此功能。然后在代码中继承KeyspaceEventMessageListener,实现onMessage 就可以监听过期的数据量。
通过以上源码,我们可以发现,其本质也是注册一个listener,利用redis 的发布订阅,当key 过期时,发布过期消息(key)到Channel : 在实际的业务中,我们可以将订单的过期时间设置比如30 分钟,然后放入到redis。30 分钟之后,就可以消费这个key,然后做一些业务上的后置动作,比如检查用户是否支付。 优点: 由于redis 的高性能,所以我们在设置key,或者消费key 时,速度上是可以保证的。 缺点:致命缺陷,不宜使用 在 Redis 官方手册的keyspace-notifications: timing-of-expired-events中明确指出:
redis 自动过期的实现方式是:定时任务离线扫描并删除部分过期键;在访问键时惰性检查是否过期并删除过期键。也就是说,由于redis 的key 过期策略原因,当一个key 过期时,redis 从未保证会在设定的过期时间立即删除并发送过期通知,自然我们的监听事件也无法第一时间消费到这个key,所以会存在一定的延迟。实际上,过期通知晚于设定的过期时间数分钟的情况也比较常见。 另外,在redis5.0 之前,订阅发布中的消息并没有被持久化,自然也没有所谓的确认机制。所以一旦消费消息的过程中我们的客户端发生了宕机,这条消息就彻底丢失了。 总结:redis 的过期订阅相比于其他方案没有太大的优势,在实际生产环境中,用得相对较少。 Redisson分布式延迟队列Redisson是一个基于redis实现的Java驻内存数据网格,它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。 Redisson除了提供我们常用的分布式锁外,还提供了一个分布式延迟队列RDelayedQueue,他是一种基于zset结构实现的延迟队列,其实现类是RedissonDelayedQueue。delayqueue 中有一个名为 timeoutSetName 的有序集合,其中元素的 score 为投递时间戳。delayqueue 会定时使用 zrangebyscore 扫描已到投递时间的消息,然后把它们移动到就绪消息列表中。 delayqueue 保证 redis 不崩溃的情况下不会丢失消息,在没有更好的解决方案时不妨一试。 优点:使用简单,并且其实现类中大量使用lua 脚本保证其原子性,不会有并发重复问题。 缺点:需要依赖redis(如果这算一种缺点的话)。 总结:Redisson 是redis 官方推荐的JAVA 客户端,提供了很多常用的功能,使用简单、高效,推荐使用。 RocketMQ 延迟消息延迟消息:当消息写入到Broker 后,不会立刻被消费者消费,需要等待指定的时长后才可被消费处理的消息,称为延时消息。 在订单创建之后,我们就可以把订单作为一条消息投递到rocketmq,并将延迟时间设置为30 分钟,这样,30 分钟后我们定义的consumer 就可以消费到这条消息,然后检查用户是否支付了这个订单。 通过延迟消息,我们就可以将业务解耦,极大地简化我们的代码逻辑。 优点:可以使代码逻辑清晰,系统之间完全解耦,只需关注生产及消费消息即可。另外其吞吐量极高,最多可以支撑万亿级的数据量。 缺点:相对来说,mq 是重量级的组件,引入mq 之后,随之而来的消息丢失、幂等性问题等都加深了系统的复杂度。 总结:通过mq 进行系统业务解耦,以及对系统性能削峰填谷已经是当前高性能系统的标配。 RabbitMQ 死信队列除了RocketMQ 的延迟队列,RabbitMQ 的死信队列也可以实现消息延迟功能。 死信(Dead Letter) 是 rabbitmq 提供的一种机制。当一条消息满足下列条件之一那么它会成为死信:
基于这样的机制,我们可以给消息设置一个ttl,然后故意不消费消息,等消息过期就会进入死信队列,我们再消费死信队列即可。通过这样的方式,就可以达到同RocketMQ 延迟消息一样的效果。 在 rabbitmq 中创建死信队列的操作流程大概是:
死信队列的设计目的是为了存储没有被正常消费的消息,便于排查和重新投递。死信队列同样也没有对投递时间做出保证,在第一条消息成为死信之前,后面的消息即使过期也不会投递为死信。 为了解决这个问题,rabbit 官方推出了延迟投递插件 rabbitmq-delayed-message-exchange ,推荐使用官方插件来做延时消息。 优点:同RocketMQ 一样,RabbitMQ 同样可以使业务解耦,基于其集群的扩展性,也可以实现高可用、高性能的目标。 缺点:死信队列本质还是一个队列,队列都是先进先出,如果队头的消息过期时间比较长,就会导致后面过期的消息无法得到及时消费,造成消息阻塞。 总结:除了增加系统复杂度之外,死信队列的阻塞问题也是需要我们重点关注的。
最佳实践实际上,在数据库索引设计良好的情况下,定时扫描数据库中未完成的订单产生的开销并没有想象中那么大。在使用 redisson delayqueue 等定时任务中间件时可以同时使用扫描数据库的方法作为补偿机制,避免中间件故障造成任务丢失。 为什么不建议用MQ实现订单到期关闭
并发口诀:一锁二判三更新不管我们使用定时任务还是延迟消息时,不可避免的会遇到并发执行任务的情况 (比如重复消费、调度重试等)。 当我们执行任务时,我们可以按照一锁二判三更新这个口诀来处理。
兜底意识 + 配置监控虽然我们提到了很多的实现策略,现实实战时依然容易出现问题,比如不合理的操作导致消息丢失。 因此,我们应该具备兜底意识。 假如少量消息丢失,我们可以通过每天凌晨跑一次任务,批量将这些未处理的订单批量取消。这种兜底行为工程实现简单,同时对系统影响很小。 还有一点,就是配置监控。 笔者曾经自研过任务调度系统,应用 A 接入后,从控制台发现每隔 2 个小时调度应用 A 的任务时,经常发生超时,通过分析,发现应用 A 线程出现了死锁。 这种问题出现的几率非常高,因此配置监控特别要必要。 对业务系统来讲,监控分为两个层面:系统监控和业务监控。
在条件允许的情况下,建议关注性能监控,方法可用性监控,方法调用次数监控这三大类。
上图是性能监控的示例图,性能监控不同时间段性能分布,实时统计 TP99、TP999 、AVG 、MAX 等维度指标,这也是性能调优的重点关注对象。
业务监控功能是从业务角度出发,各个应用系统需要从业务层面进行哪些监控,以及提供怎样的业务层面的监控功能支持业务相关的应用系统。 具体就是对业务数据,业务功能进行监控,实时收集业务流程的数据,并根据设置的策略对业务流程中不符合预期的部分进行预警和报警,并对收集到业务监控数据进行集中统一的存储和各种方式进行展示。 比如订单系统中有一个定时结算的服务,每两分钟执行一次。我们可以在定时任务 JOB 中添加埋点,并配置业务监控,假如十分钟该定时任务没有执行,则发送邮件,短信给相关负责人。 扩展一笔订单,在取消的那一刻用户刚好付款了,怎么办? 这种情况在正常的业务场景中是有可能出现的,因为订单都会有定时取消的逻辑,比如10 分钟或者 15分钟,而用户刚好卡在这个时间点进行付款,此时就会出现两种情况:
可以看到,不论是哪种情况,其实都需要做一定的处理,不然用户肯定会来投诉! 这种场景无非就是支付单支付成功和取消两种状态的“争夺”,正常情况下,订单或者支付单都会有状态机的存在,在当前场景简单来说有以下两条路径:
针对情况1,如果是支付回调取胜,此时的状态应该已从 支付中->支付成功 针对情况2,如果是取消支付单取胜,此时的状态应该已从 支付中->已取消 所以我们在修改支付单状态的时候,基于原始状态的判断,就可以做正常的处理,来看下 SOL应该就很清晰了:
重点就是我们加了 status='paying’这个条件,这就能保证情况只有一个能成功,另一个一定失败。这种其实就是乐观锁的方式
业务优化针对订单超时业务,这里在业务上可以做一个小优化,你想想,用户付款前可能有点挣扎,然后在最后一刻终于下定决心进行付款,这时候却告知被退款了,用户很可能就不会再下单了。因此我们在页面上可以限时订单取消设置计时为 10分钟,但实际后端是延迟 11 分钟取消订单,这样就能避免这种情况的发生啦。 Redis 分布式锁实现最后除了利用数据库处理,还可以使用分布式锁,对一笔订单加锁也能保证这笔订单正常的业务流转。每次进行取消订单或付款操作时,首先尝试获取订单的分布式锁,确保只有一个操作能修改订单状态。在分布式系统中,订单在取消的同时用户付款的竞态问题可以通过分布式锁来解决。以下是一个具体的、落地的方案,确保订单状态的可靠性,避免因并发导致状态冲突 订单取消流程:
订单付款流程:
转自https://www.cnblogs.com/seven97-top/p/18810985 该文章在 2025/4/8 9:10:25 编辑过 |
关键字查询
相关文章
正在查询... |