电商平台中订单未支付过期如何实现自动关单

电商平台中订单未支付过期如何实现自动关单

思路:延时任务处理场景

方案一:定时任务

成本低,容易实现。

写一个定义任务,@Scheduled(cron="0 0 23 * * ?"),定期扫描数据库中的订单,若时间过期,更新状态为关闭。

@Scheduled(cron="0 0 23 * * ?") 
是一个在 Spring 框架中使用的注解,用于定义定时任务的执行规则。这个注解中的 cron 属性指定了一个 Cron 表达式,用来描述任务执行的时间。
Cron 表达式的格式通常由六个或七个空格分隔的字段组成,从左到右依次表示秒、分钟、小时、月份中的哪一天、月份、一周中的哪一天、年份(可选)。每个字段可以是数字、星号(*)、问号(?)、逗号(,)分隔的列表、斜杠(/)分隔的范围等。
对于 @Scheduled(cron="0 0 23 * * ?") 这个表达式:
秒:0 (表示每分钟的第 0 秒)
分钟:0 (表示每小时的第 0 分钟)
小时:23 (表示每天的 23 点)
月份中的哪一天:* (表示每个月的每一天)
月份:* (表示每年的每个月)
一周中的哪一天:? (表示不关心星期几,由月份中的哪一天决定)
因此,这个表达式的意思是:“每天晚上 11 点整执行一次”。

优点:容易实现,成本低,不依赖于其他组件。

缺点:

时间可能不够精准。由于定时任务扫描时间间隔固定,会造成一些订单已经过期,但未被扫描,不能及时关闭。

增加数据库的压力。时间推移,订单越来越多,扫描成本增加,执行时间被拉长,导致应该关闭的订单迟迟不能关闭。

总结:此方案适合对时间要求不敏感,并且数据量不太多的业务场景。

方案二:JDK延迟队列 DelayQueue

DelayQueue是JDK提供的一个无界队列,需要实现Delayed接口,该接口提供了一个获取过期时间的方法。

long getDelay(TimeUnit unit);

用户生成订单后,设置过期时间例如30分钟,放入DelayQueue。然后创建一个线程,线程中通过while(true)不断从DelayQueue中获取过期信息。

优点:无需依赖第三方组件,无需查询数据库。

缺点:是一个无界队列,过多订单,如果订单过多,会造成"Out Of Memory"(内存溢出),DelayQueue基于JVM内存,如果JVM重启,数据就会丢失。

JVM 的内存结构主要包括以下几个部分:
程序计数器(Program Counter Register):线程私有,记录当前线程所执行的字节码指令的位置。由于它的大小非常小,并且只存储当前线程执行的字节码指令地址,因此理论上不会发生 OOM 错误。
Java 堆(Heap):所有线程共享,用于存放对象实例,是发生 OOM 错误的主要区域之一。当堆内存不足以分配新的对象时,就会抛出 OutOfMemoryError。
方法区(Method Area):也称为永久代(在 JDK 8 之前),或者元空间(从 JDK 8 开始)。这是用于存储类信息、常量、静态变量、即时编译器编译后的代码缓存等数据的地方。当方法区无法满足新的内存分配需求时,同样会抛出 OutOfMemoryError。
直接内存(Direct Memory):通过 java.nio.ByteBuffer.allocateDirect() 方法分配的内存,这部分内存不在 JVM 堆上,而是在本机内存中。当直接内存不足时,也会抛出 OutOfMemoryError。
虚拟机栈(Virtual Machine Stack) 和 本地方法栈(Native Method Stack):用于存储线程的局部变量、操作数栈、动态链接、方法出口等信息。如果线程请求的栈深度大于 JVM 所允许的最大深度,将会抛出 StackOverflowError;如果 JVM 不允许动态扩展栈大小,且线程申请的栈容量超过了最大限制,将抛出 OutOfMemoryError。
解决 OOM 问题的方法包括但不限于:
调整 JVM 参数:例如增加堆内存大小,调整新生代与老年代的比例。
优化代码:减少不必要的对象创建,避免内存泄漏,及时清理不再使用的对象。
使用工具进行内存分析:如 VisualVM 或 JProfiler 等工具,帮助定位内存消耗较大的对象。
采用更高效的数据结构:比如使用更节省内存的集合类。
使用缓存策略:如 LRU 缓存淘汰策略,减少不必要的内存占用。

总结:DelayQueue适用于数据量较小,丢失不会影响主要业务场景,例如公司内部通知啥的。

方案三:redis过期监听

redis是一个高性能 key value 数据库,除了用户缓存之外,同时还存在过期监听功能。

在redis.conf中,配置notify-keyspace-evnets Ex 即可开启此功能。

在代码中继承 KeyspaceEventMessageListener,实现onMessage就可以监听过期的数据量。

其本质就是注册一个listener,利用redis的发布与订阅,当key过期时,发布过期消息(key)到Channel:可以keyevent@*.expired中,在实际的业务中,我们可以将订单的过期时间设置比如30分钟,放入redis,30分钟后就会消费这个key,后续做业务操作,比如检查用户是否支付。

优点:reidis高性能,设置key和消费key,速度上可以保证。

缺点:由于redis的key过期策略原因,当一个key过期时,redis无法保证立刻将其删除,自然我们监听事件也无法第一时间消费到这个key,所以会存在一定的延迟,另外,在redis5.0之前,订阅发布中的消息并没有被持久化,自然也就没有确认机制。所以一旦消费消息的过程中我们客户端发生宕机,这条信息就彻底丢失了。

总结:redis的过期订阅相比于其他方案没有太大优势,在实际生产环境中,用得相对较少。

方案四:Redission分布式延迟队列

Redission是一个基于redis实现的java驻内存数据网络,它不仅提供了一系列的分布式java常用对象,还提供了许多分布式服务。

Redission除了提供我们常用的分布式锁外,还提供了一个分布式延迟队列RDelayedQueue,他是基于zset结构实现延迟队列,其实现类是RedissionDelayedQueue。

  • 生产者端

1 通过redissonClient的getBlockingDeque方法指定队列名称获得RBlockingDeque对象

2 然后再通过redissonClient的getDelayedQueue方法传入RBlockingDeque对象获得RDelayedQueue对象

3 最后调用RDelayedQueue对象的offer方法就可以将消息指定延迟时间发送到延迟队列了

@Component
public class DelayQueueKit {

    // 注入RedissonClient实例
    @Resource
    private RedissonClient redissonClient;

    /**
     * 添加消息到延迟队列
     *
     * @param queueCode 队列唯一KEY
     * @param msg       消息
     * @param delay     延迟时间
     * @param timeUnit  时间单位
     */
    public <T> void addDelayQueue(String queueCode, T msg, long delay, TimeUnit timeUnit) {
        RBlockingDeque<T> blockingDeque = redissonClient.getBlockingDeque(queueCode);
        RDelayedQueue<T> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
        // 这一步通过offer插入到队列
        delayedQueue.offer(msg, delay, timeUnit);
    }
}
  • 消费者端

1 通过redissonClient获取RBlockingDeque对象

2 通过RBlockingDeque对象获取RDelayedQueue

3 之后RBlockingDeque再通过自旋调用take方法获取到期的消息,没有消息时会阻塞的。

Tip 一般情况下我们在程序刚启动时异步开一个线程去自旋消费队列消息的

@Component
public class DelayQueueKit {
    // 注入RedissonClient实例
    @Resource
    private RedissonClient redissonClient;
    public <T> void consumeQueueMsg(String queueCode) {
        RBlockingDeque<T> delayQueue = redissonClient.getBlockingDeque(queueCode);
        RDelayedQueue<T> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
        log.info("【队列-{}】- 监听队列成功", queueCode);
        while (true) {
            T message = null;
            try {
                message = delayQueue.take();
                // 处理自己的业务
                handleMessage(message);
                log.info("【队列-{}】- 处理元素成功 - ele = {}", queueCode, ele);
            } catch (Exception e) {
                log.error("【队列-{}】- 处理元素失败 - ele = {}", queueCode, ele, e);
            }
        }
    }
}

优点:使用简单,并且其实现类中大量使用lua脚本保证其原子性,不会并发重复问题。

缺点:需要依赖redis

总结:Redission是redis官方推荐java客户端,提供了很多常用的功能,使用简单,高效。

方案五:RocketMQ延迟消息

不会被立刻消费,需要等到指定的时长后才可被消费处理的消息,成为延迟消息。

在创建订单后,把可以把订单作为一条消息投递到rocketmq,并将延迟时间设置为30分钟,这样30分钟后我们定义的consumer就可以消费到这条消息,然后检查用户是否支付了这个订单。

延迟消息,可以将业务解耦,简化代码逻辑。

优点:可以使代码逻辑清晰,系统之间解耦,只需关注生产及消费信息即可,另外吞吐量极高,最多可以支撑万亿级的数据量。

缺点:相对于mq是重量级组件,以内mq之后,随之而来的消息丢失、幂等性问题等都加上呢了系统复杂性。

什么是幂等?
幂等原本是一个数学上的概念,表达的是N次变换与1次变换的结果相同。即公式:f(x)=f(f(x)) 成立。

用在编程领域,表达的是对于同一个系统,使用同样的条件,「一次请求和重复的多次请求对系统资源的影响是一致的」。

一般来说,读操作天然都是幂等的(除非你的读操作有副作用),而写操作是不幂等的。但是有些业务场景我们需要做到写操作幂等,所以需要做一些额外的工作。

比如提交订单请求,有时候可能是同样一个订单,如果不做幂等,重复处理,就有可能会造成用户或者公司的资产损失。

幂等在并发量较高的项目中是一个经常会遇到的问题。

主要有以下两种场景会遇到幂等问题:

请求重复提交/消息重复提交

失败重试

消息幂等

也是经常要考虑的问题。还是之前电商系统中订单提交的例子,比如库存扣减这种操作,为了保证吞吐量,可能会使用消息来实现。

消息幂等的概念可以总结如下:

❝如果消息重试多次,消费者端对该重复消息消费多次与消费一次的结果是相同的,并且多次消费没有对系统产生副作用,那么我们就称这个过程是消息幂等的。❞

对于消息来说,发送端可能产生重复消息,消费端也可能会重复消费同一条消息(大多数都是因为网络问题)。如果靠消息中间件去实现幂等,是一件比较困难的事情,增加幂等的处理会导致消息中间件的「吞吐量」下降。所以绝大多数消息「消息中间件本身不处理幂等问题」,而是交给了业务端自己去处理。

而不论是发送端重复还是消费端重复,我们「只需要保证消费端幂等」就可以了,不需要在发送端做什么事情。而消费端做幂等,其实本质上也是上面提到的“「重复提交下的幂等」”,比较适合在消费端的入口处就做幂等处理。

总结:通过mq进行系统业务解耦,以及对系统性能削峰填谷已经是当前高性能系统的标配。

方案六:RabbitMQ死信队列

相较于RocketMQ延迟队列,RabbitMQ也可以实现延迟功能。

当RabbitMQ中的一条正常消息,因为过了存活时间(TTL过期)、队列长度超限、被消费者拒绝等原因无法被消费时,就会被当成一条死信消息,投递到死信队列。基于这样的机制,我们可以给消息设置一个TTL,然后故意不消费消息,等消息过期就会进入死信队列,我们再消费死信队列,这种方式可以达到同RokectMQ延迟消息一样的效果。

优点:代码逻辑清晰,系统之间解耦,高可用

缺点:死信队列本质还是一个队列,队列都是先进先出,如果队头的消息过期时间比较长,就会导致后面过期的消息无法被及时消费,造成消息阻塞。

总结:除了增加系统复杂性之外,死信队列造成的消息阻塞也得关注。

以上是总结的订单未支付实现自动关单的一些方案,具体流程及代码业务和配置,需要具体情况具体分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值