实现电商系统定时自动关闭订单

一、需求描述

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,而且时间很准确,误差在1s内。

二、实现方案

  • 定时任务关闭订单(不推荐)
  • RocketMQ延迟队列(不够灵活)
  • RabbitMQ死信队列(不推荐)
  • RabbitMQ的delay插件rabbitmq_delayed_message_exchange实现延时消息(推荐)
  • 时间轮算法
  • Redis过期监听

三、方案详解

3.1 定时任务关闭订单

一般情况下,最不推荐的方式就是关单方式就是定时任务方式。我们假设,关单时间为下单后10分钟,定时任务间隔也是10分钟,如果在第1分钟下单,在第20分钟的时候才能被扫描到执行关单操作,这样误差达到10分钟,这在很多场景下是不可接受的,另外需要频繁扫描主订单号造成网络IO和磁盘IO的消耗,对实时交易造成一定的冲击,所以PASS。

3.2 RocketMQ延迟队列方式

延迟消息 生产者把消息发送到消息服务器后,并不希望被立即消费,而是等待指定时间后才可以被消费者消费,这类消息通常被称为延迟消息。 在RocketMQ开源版本中,支持延迟消息,但是不支持任意时间精度的延迟消息,只支持特定级别的延迟消息。 消息延迟级别分别为1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,共18个级别。

这种方式相比定时任务好了很多,但是有一个致命的缺点,就是延迟等级只有18种(商业版本支持自定义时间),如果我们想把关闭订单时间设置在15分钟该如何处理呢?显然不够灵活。

3.3 RabbitMQ死信队列

Rabbitmq本身是没有延迟队列的,只能通过Rabbitmq本身队列的特性来实现,想要Rabbitmq实现延迟队列,需要使用Rabbitmq的死信交换机(Exchange)和消息的存活时间TTL(Time To Live)。

一个消息在满足如下条件下,会进死信交换机,记住这里是交换机而不是队列,一个交换机可以对应很多队列。

一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。 上面的消息的TTL到了,消息过期了。

队列的长度限制满了,排在前面的消息会被丢弃或者扔到死信路由上。 死信交换机就是普通的交换机,只是因为我们把过期的消息扔进去,所以叫死信交换机,并不是说死信交换机是某种特定的交换机。

消息TTL(消息存活时间) 消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。如果队列设置了,消息也设置了,那么会取值较小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。

可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。只是expiration字段是字符串参数,所以要写个int类型的字符串,当上面的消息扔到队列中后,过了60秒,如果没有被消费,它就死了。不会被消费者消费到。这个消息后面的,没有“死掉”的消息对顶上来,被消费者消费。死信在队列中并不会被删除和释放,它会被统计到队列的消息数中去。

3.4 RabbitMQ的delay插件rabbitmq_delayed_message_exchange实现延时消息
3.4.1 安装插件

去RabbitMQ的官网下载插件,插件地址:https://www.rabbitmq.com/community-plugins.html,直接搜索rabbitmq_delayed_message_exchange即可找到我们需要下载的插件,下载和RabbitMQ配套的版本。

3.4.2 与RabbitMQ死信队列实现方式对比
  • 死信队列:死信队列是这样一个队列,如果消息发送到该队列并超过了设置的时间,就会被转发到设置好的处理超时消息的队列当中去,利用该特性可以实现延迟消息。
  • 延迟插件:通过安装插件,自定义交换机,让交换机拥有延迟发送消息的能力,从而实现延迟消息。
  • 总结:由于死信队列方式需要创建两个交换机(死信队列交换机+处理队列交换机)、两个队列(死信队列+处理队列),而延迟插件方式只需创建一个交换机和一个队列,所以后者使用起来更简单。
3.5 时间轮算法
1

1、创建环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)

2、任务集合,环上每一个slot是一个Set,同时启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot。

Task结构中有两个很重要的属性:
1、Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务。
2、订单号:要关闭的订单号(也可以是其他信息,比如:是一个基于某个订单号的任务)

假设当前Current Index指向第0格,例如在3610秒之后,有一个订单需要关闭,只需:
1、计算这个订单应该放在哪一个slot,当我们计算的时候现在指向1,3610秒之后,应该是第10格,所以这个Task应该放在第10个slot的Set中。
2、计算这个Task的Cycle-Num,由于环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,所以应该绕3610/3600=1圈之后再执行,于是Cycle-Num=1

Current Index不停的移动,每秒移动到一个新slot,这个slot中对应的Set,每个Task看Cycle-Num是不是0。如果不是0,说明还需要多移动几圈,将Cycle-Num减1。如果是0,说明马上要执行这个关单Task了,取出订单号执行关单(可以用单独的线程来执行Task),并把这个订单信息从Set中删除即可。
1、无需再轮询全部订单,效率高
2、一个订单,任务只执行一次
3、时效性好,精确到秒(控制timer移动频率可以控制精度)。

3.6 Redis过期监听

1、修改redis.windows.conf配置文件中notify-keyspace-events的值,默认配置notify-keyspace-events的值为 "" 修改为 notify-keyspace-events Ex 这样便开启了过期事件

2、 创建配置类RedisListenerConfig(配置RedisMessageListenerContainer这个bean)。

3、继承KeyExpirationEventMessageListener创建Redis过期事件的监听类。

你可能感兴趣的:(实现电商系统定时自动关闭订单)