未支付订单超时处理分析

未支付订单超时处理分析

如购买后未支付订单,需要在十分钟后回滚状态等这类问题。

有什么方法解决

定时任务

实现思路比较简单。启动一个计划任务,每隔一定时间处理一次,不过这种处理方式只是适用比较小而简单的项目。

好处是实现简单、也好做分布式集群。

但是坏处也很明显:

时效性差,会有一定的延迟,这个延迟时间最大就是每隔一定时间的大小,如果你设置每分钟定时轮询一次,那么理论上订单取消时间的最大误差就有一分钟。

效率低。

对数据库的压力比较大。

懒加载

也就是不做处理,只有在用户重新点开的时候在判断。

这种方法简单,不过会造成回库问题。

也可以用定时任务加懒加载。(redis删除策略?

上面两种方法在实际场景用不到的,看看拓展下思路就行了。

map

用map或者队列放每个消息对象(注意map也能做到)。再为每个消息都启动一个timer,时间到了就让timer直接调用回调函数处理业务。
这种方案timer线程不可能多到和消息数量一样大。不实际
【改进】
一个改进是:给每个消息加个剩余时间字段,只用一个timer,将每个消息排序,剩余时间短的在前面,这样每次只需要比较一个时间。
另一个改进是:作为生产者把时间到的消息放到一个队列里,让超时消息处理方成为消费者来消费消息,这样就直接跟rabbitmq结合了起来。

消息队列

此处使用rabbitmq(因为本人只使用过这个- -)

死信队列

它是 通过使用TTLDXL这两个属性间接实现的。

大概是:当消息在一个队列超时指定时间后,将会变成一个死信,然后它会被重新publish到另一个交换机上,这个交换机就是死信交换机。

TTL 顾名思义:指的是消息的存活时间,RabbitMQ可以通过x-message-tt参数来设置指定Queue(队列)和 Message(消息)上消息的存活时间,它的值是一个非负整数,单位为微秒。

DLX:死信交换机,绑定在死信交换机上的即死信队列。RabbitMQ的Queue(队列)可以配置两个参数x-dead-letter-exchange和x-dead-letter-routing-key(可选),一旦队列内出现了Dead Letter(死信),则按照这两个参数可以将消息重新路由到另一个Exchange(交换机),让消息重新被消费。

x-dead-letter-exchange:队列中出现Dead Letter后将Dead Letter重新路由转发到指定 exchange(交换机)。

x-dead-letter-routing-key:指定routing-key发送,一般为要指定转发的队列。

如下所示:

a3d3b4616cde4d65c8894aac258a77c

/**
 * 延时队列
 */
@Bean(name = "order.delay.queue")
public Queue getMessageQueue() {
    return QueueBuilder
            .durable(RabbitConstant.DEAD_LETTER_QUEUE)
            // 配置到期后转发的交换
            .withArgument("x-dead-letter-exchange", "order.close.exchange")
            // 配置到期后转发的路由键
            .withArgument("x-dead-letter-routing-key", "order.close.queue")
            .build();
} 
延迟队列

每个订单创建的时候发延时消息。到点判断是不是要过期。

如果用它实现只需要下载一个插件,然后配置好时间就行了,非常方便。

下面是使用Spring Cloud Stream + Rabbit MQ 的demo

yaml配置:

spring:
  cloud:
    stream:
      bindings:
        testOutput:  
          destination: testBuy
          content-type: application/json
          type: x-delayed-message
        testInput:  
          destination: testBuy
          content-type: application/json
          group: checkOrder
      rabbit:
        bindings:
          testOutput:
            producer:
              delayedExchange: true
          testInput:
            consumer:
              delayedExchange: true
    public void send(TbWechatPayOrder payOrder) {
        log.info("准备发送延迟队列信息:{}", LocalTime.now());
        // 十分钟
        Message<TbWechatPayOrder> build = MessageBuilder.withPayload(payOrder).setHeader("x-delay", 60000 * 10).build();
        testOutput.send(build);
    }

使用它的好处是:

消息队列本就是专门使用收发信息的,专业对口,异步处理,性能较好。

而且还能保证消息可靠性等。

redis

轮询

我们知道 Redis 有一个有序集合的数据结构 ZSet,ZSet 中每个元素都有一个对应 Score,ZSet 中所有元素是按照其 Score 进行排序的。

那么我们可以通过以下这几个操作使用 Redis 的 ZSet 来实现一个延迟队列:

入队操作:ZADD KEY timestamp task, 我们将需要处理的任务,按其需要延迟处理时间作为 Score 加入到 ZSet 中。Redis 的 ZAdd 的时间复杂度是O(logN),N是 ZSet 中元素个数,因此我们能相对比较高效的进行入队操作。
起一个进程定时(比如每隔一秒)通过ZREANGEBYSCORE方法查询 ZSet 中 Score 最小的元素,具体操作为:ZRANGEBYSCORE KEY -inf +inf limit 0 1 WITHSCORES。查询结果有两种情况:
a. 查询出的分数小于等于当前时间戳,说明到这个任务需要执行的时间了,则去异步处理该任务;
b. 查询出的分数大于当前时间戳,由于刚刚的查询操作取出来的是分数最小的元素,所以说明 ZSet 中所有的任务都还没有到需要执行的时间,则休眠一秒后继续查询;
同样的,ZRANGEBYSCORE操作的时间复杂度为O(logN + M),其中N为 ZSet 中元素个数,M为查询的元素个数,因此我们定时查询操作也是比较高效的。

2.png

它有以下这些好处

Redis用来进行实现延时队列是具有这些优势的:

  1. Redis zset支持高性能的 score 排序。
  2. Redis是在内存上进行操作的,速度非常快。
  3. Redis可以搭建集群,当消息很多时候,我们可以用集群来提高消息处理的速度,提高可用性。
  4. Redis具有持久化机制,当出现故障的时候,可以通过AOF和RDB方式来对数据进行恢复,保证了数据的可靠性

另外比rabbitmq简洁 不用定义那么多队列和交换机
缺点是涉及到一个分布式锁的问题
也不可能单独部署一个服务节点专门处理这个订单超时问题的 ,并且redis在主从架构下宕机,容易丢失锁。

过期事件监听

利用Redis的事件监听机制, 还有另外一种方式实现延迟处理.

Redis可以根据需要, 修改redis.conf配置, 实现对一些事件的监听, 其中就包括key过期事件.

redis.conf 配置:

notify-keyspace-events Ex

这个事件监听是通过pubsub机制实现的, 所以业务代码中实现对事件的订阅, 就可以知道哪个key过期了。

有了上述事件监听基础, 将延期事件对应key存入Redis, 并根据延迟时间设置key过期时间, 当key过期时, 便能触发监听事件, 完成延迟处理逻辑。

redisson

百科:

Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。【Redis官方推荐】

Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

demo:

都在代码里,手动泪目:https://cloud.tencent.com/developer/article/1589324

或者找官方文档。

redis主从架构丢锁问题:

问题:
比如某个主redis节点A加锁成功之后宕机了,导致锁丢失,然后另一个redis从节点B发生主从切换,接着节点B又再次加锁成功,这就会产生多个节点加锁成功,出现问题。

解决:
使用Redlock解决。

Redlock思想:
使用N个完全独立,没有主从关系的主redis节点,保证他们大多数情况下不会宕机。
然后对每个主redis节点进行加锁,只有超过半数,也就是(N/2 + 1)的主redis节点加锁成功,才算成功,否则一律算失败。
失败的话,还是会到每个主redis节点进行释放锁,不管之前有没有加锁成功。

为什么需要分布式锁:

如下:

成员变量 A 存在 JVM1、JVM2、JVM3 三个 JVM 内存中

成员变量 A 同时都会在 JVM 分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的

不是同时发过来,三个请求分别操作三个不同 JVM 内存区域的数据,变量 A 之间不存在共享,也不具有可见性,处理的结果也是不对的
注:该成员变量 A 是一个有状态的对象

可参考:

https://blog.csdn.net/qq_30736263/article/details/104009072

https://blog.csdn.net/yue_2018/article/details/89784454

其他

kafka netty也可以

总结

有句话说的好:没有最好的,只有最合适的

以上方法除了定时任务和懒加载不实际外,其他的都是可以被使用到的。

如在一个小而简单的项目里,原本项目就没有redis、消息队列等,贸然加入不仅添加部署成本,还可能出现其他问题,那么就可以用map。

在一个不需要考虑或者已经解决了分布式锁问题的项目里,使用redis做延时队列也是个简单高效的选择。

在因为分布式锁问题而放弃redis时,可以使用消息队列,如rabbitmq的死信、延时等。

资料参考

rabbitmq、redis参考:http://www.dockone.io/article/10139

redis过期事件监听参考:https://blog.csdn.net/weixin_41751625/article/details/107554132

分布式锁参考:https://www.cnblogs.com/gxyandwmm/p/9588383.html

最后

本人只是个菜鸟,这篇分析可能写的有不够完善的地方,如哪里有错误或者不明了的,欢迎大家斧正。

,可以使用消息队列,如rabbitmq的死信、延时等。

资料参考

rabbitmq、redis参考:http://www.dockone.io/article/10139

redis过期事件监听参考:https://blog.csdn.net/weixin_41751625/article/details/107554132

分布式锁参考:https://www.cnblogs.com/gxyandwmm/p/9588383.html

最后

本人只是个菜鸟,这篇分析可能写的有不够完善的地方,如哪里有错误或者不明了的,欢迎大家斧正。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值