面试难题:怎么不用定时任务实现关闭订单?

e79b682f45e8cffc52997532f9b85427.jpeg

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢?

一般的做法有如下几种:

  • 定时任务关闭订单
  • rocketmq延迟队列
  • rabbitmq死信队列
  • 时间轮算法
  • redis过期监听

一、定时任务关闭订单(最low)

一般情况下,最不推荐的方式就是关单方式就是定时任务方式,原因我们可以看下面的图来说明。

c2d973b99ef352c71d1bbc59a06ec5e5.jpeg

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

二、rocketmq延迟队列方式

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

发送延迟消息(生产者)

/**
 * 推送延迟消息
 * @param topic 
 * @param body 
 * @param producerGroup 
 * @return boolean
 */

public boolean sendMessage(String topic, String body, String producerGroup) {
     try {
        Message recordMsg =  new Message(topic, body.getBytes());
        producer.setProducerGroup(producerGroup);

         //设置消息延迟级别,我这里设置14,对应就是延时10分钟
         // "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
        recordMsg.setDelayTimeLevel( 14);
         // 发送消息到一个Broker
        SendResult sendResult = producer.send(recordMsg);
         // 通过sendResult返回消息是否成功送达
        log.info( "发送延迟消息结果:======sendResult:{}", sendResult);
        DateFormat format = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss");
        log.info( "发送时间:{}", format.format( new Date()));

         return  true;
    }  catch (Exception e) {
        e.printStackTrace();
        log.error( "延迟消息队列推送消息异常:{},推送内容:{}", e.getMessage(), body);
    }
     return  false;
}

消费延迟消息(消费者)

/**
 * 接收延迟消息
 * 
 * @param topic
 * @param consumerGroup
 * @param messageHandler
 */

public void messageListener(String topic, String consumerGroup, MessageListenerConcurrently messageHandler) {
    ThreadPoolUtil.execute(() -> {
         try {
            DefaultMQPushConsumer consumer =  new DefaultMQPushConsumer();
            consumer.setConsumerGroup(consumerGroup);
            consumer.setVipChannelEnabled( false);
            consumer.setNamesrvAddr(address);
             //设置消费者拉取消息的策略,*表示消费该topic下的所有消息,也可以指定tag进行消息过滤
            consumer.subscribe(topic,  "*");
             //消费者端启动消息监听,一旦生产者发送消息被监听到,就打印消息,和rabbitmq中的handlerDelivery类似
            consumer.registerMessageListener(messageHandler);
            consumer.start();
            log.info( "启动延迟消息队列监听成功:" + topic);
        }  catch (MQClientException e) {
            log.error( "启动延迟消息队列监听失败:{}", e.getErrorMessage());
            System.exit( 1);
        }
    });
}

实现监听类,处理具体逻辑

扫描二维码关注公众号,回复: 14377368 查看本文章
/**
 * 延迟消息监听
 * 
 */

@Component
public&nbsp; class&nbsp;CourseOrderTimeoutListener&nbsp;implements&nbsp;ApplicationListener<ApplicationReadyEvent>&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp; @Resource
&nbsp;&nbsp;&nbsp;&nbsp; private&nbsp;MQUtil&nbsp;mqUtil;

&nbsp;&nbsp;&nbsp;&nbsp; @Resource
&nbsp;&nbsp;&nbsp;&nbsp; private&nbsp;CourseOrderTimeoutHandler&nbsp;courseOrderTimeoutHandler;

&nbsp;&nbsp;&nbsp;&nbsp; @Override
&nbsp;&nbsp;&nbsp;&nbsp; public&nbsp;void&nbsp;onApplicationEvent(ApplicationReadyEvent&nbsp;applicationReadyEvent)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //&nbsp;订单超时监听
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mqUtil.messageListener(EnumTopic.ORDER_TIMEOUT,&nbsp;EnumGroup.ORDER_TIMEOUT_GROUP,&nbsp;courseOrderTimeoutHandler);
&nbsp;&nbsp;&nbsp;&nbsp;}
}

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

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

三、rabbitmq死信队列的方式

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

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

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

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

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

byte[]&nbsp;messageBodyBytes&nbsp;=&nbsp; "Hello,&nbsp;world!".getBytes();&nbsp;&nbsp;
AMQP.BasicProperties&nbsp;properties&nbsp;=&nbsp; new&nbsp;AMQP.BasicProperties();&nbsp;&nbsp;
properties.setExpiration( "60000");&nbsp;&nbsp;
channel.basicPublish( "my-exchange",&nbsp; "queue-key",&nbsp;properties,&nbsp;messageBodyBytes);

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

处理流程图

d070d82d8f3bbb556c40f2803f0addd8.jpeg

创建交换机(Exchanges)和队列(Queues)

创建死信交换机

8e118f36c8dd5005286ae147e5b8f7b7.jpeg

如图所示,就是创建一个普通的交换机,这里为了方便区分,把交换机的名字取为:delay。

创建自动过期消息队列&nbsp;这个队列的主要作用是让消息定时过期的,比如我们需要2小时候关闭订单,我们就需要把消息放进这个队列里面,把消息过期时间设置为2小时。

f81b16d65aecee69f14b6aba1edbc6cb.jpeg

创建一个一个名为delay_queue1的自动过期的队列,当然图片上面的参数并不会让消息自动过期,因为我们并没有设置x-message-ttl参数,如果整个队列的消息有消息都是相同的,可以设置,这里为了灵活,所以并没有设置,另外两个参数x-dead-letter-exchange代表消息过期后,消息要进入的交换机,这里配置的是delay,也就是死信交换机,x-dead-letter-routing-key是配置消息过期后,进入死信交换机的routing-key,跟发送消息的routing-key一个道理,根据这个key将消息放入不同的队列。

创建消息处理队列&nbsp;这个队列才是真正处理消息的队列,所有进入这个队列的消息都会被处理。

e1de93118a9d84f89c20dfc8e6c4ae01.jpeg

消息队列的名字为delay_queue2&nbsp;消息队列绑定到交换机 进入交换机详情页面,将创建的2个队列(delayqueue1和delayqueue2)绑定到交换机上面

e16b38da4a6bdf00ab4da2b6eae58911.jpeg

自动过期消息队列的routing key 设置为delay 绑定delayqueue2

acd81c6ff06593be5a115031cb77a664.jpeg

delayqueue2 的key要设置为创建自动过期的队列的x-dead-letter-routing-key参数,这样当消息过期的时候就可以自动把消息放入delay_queue2这个队列中了 绑定后的管理页面如下图:

14592222b3a47d24ebc5cc5b2fc04aa6.jpeg

当然这个绑定也可以使用代码来实现,只是为了直观表现,所以本文使用的管理平台来操作发送消息

String&nbsp;msg&nbsp;=&nbsp; "hello&nbsp;word";&nbsp;&nbsp;
MessageProperties&nbsp;messageProperties&nbsp;=&nbsp;newMessageProperties();&nbsp;&nbsp;
messageProperties.setExpiration( "6000");
messageProperties.setCorrelationId(UUID.randomUUID().toString().getBytes());
Message&nbsp;message&nbsp;=&nbsp;newMessage(msg.getBytes(),&nbsp;messageProperties);
rabbitTemplate.convertAndSend( "delay",&nbsp; "delay",message);

设置了让消息6秒后过期 注意:因为要让消息自动过期,所以一定不能设置delay_queue1的监听,不能让这个队列里面的消息被接受到,否则消息一旦被消费,就不存在过期了。

接收消息接收消息配置好delay_queue2的监听就好了。

package&nbsp;wang.raye.rabbitmq.demo1; import&nbsp;org.springframework.amqp.core.AcknowledgeMode;&nbsp;&nbsp;
import&nbsp;org.springframework.amqp.core.Binding;&nbsp;&nbsp;
import&nbsp;org.springframework.amqp.core.BindingBuilder;&nbsp;&nbsp;
import&nbsp;org.springframework.amqp.core.DirectExchange;&nbsp;&nbsp;
import&nbsp;org.springframework.amqp.core.Message;&nbsp;&nbsp;
import&nbsp;org.springframework.amqp.core.Queue;&nbsp;&nbsp;
import&nbsp;org.springframework.amqp.rabbit.connection.CachingConnectionFactory;&nbsp;&nbsp;
import&nbsp;org.springframework.amqp.rabbit.connection.ConnectionFactory;&nbsp;&nbsp;
import&nbsp;org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;&nbsp;&nbsp;
import&nbsp;org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;&nbsp;&nbsp;
import&nbsp;org.springframework.beans.factory.annotation.Autowired;&nbsp;&nbsp;
import&nbsp;org.springframework.context.annotation.Bean;&nbsp;&nbsp;
import&nbsp;org.springframework.context.annotation.Configuration; @ConfigurationpublicclassDelayQueue{&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; /**&nbsp;消息交换机的名字*/
&nbsp;&nbsp;&nbsp;&nbsp;publicstaticfinalString&nbsp;EXCHANGE&nbsp;=&nbsp; "delay";&nbsp;&nbsp;&nbsp;&nbsp; /**&nbsp;队列key1*/
&nbsp;&nbsp;&nbsp;&nbsp;publicstaticfinalString&nbsp;ROUTINGKEY1&nbsp;=&nbsp; "delay";&nbsp;&nbsp;&nbsp;&nbsp; /**&nbsp;队列key2*/
&nbsp;&nbsp;&nbsp;&nbsp;publicstaticfinalString&nbsp;ROUTINGKEY2&nbsp;=&nbsp; "delay_key";&nbsp;&nbsp;&nbsp;&nbsp; /**
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;配置链接信息
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;@return
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*/

&nbsp;&nbsp;&nbsp;&nbsp; @Bean
&nbsp;&nbsp;&nbsp;&nbsp; publicConnectionFactory&nbsp;connectionFactory()&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CachingConnectionFactory&nbsp;connectionFactory&nbsp;=&nbsp;newCachingConnectionFactory( "120.76.237.8", 5672);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;connectionFactory.setUsername( "kberp");
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;connectionFactory.setPassword( "kberp");
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;connectionFactory.setVirtualHost( "/");
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;connectionFactory.setPublisherConfirms( true);&nbsp; //&nbsp;必须要设置
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return&nbsp;connectionFactory;
&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp; /**&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;配置消息交换机
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;针对消费者配置&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FanoutExchange:&nbsp;将消息分发到所有的绑定队列,无routingkey的概念&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; HeadersExchange :通过添加属性key-value匹配&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DirectExchange:按照routingkey分发到指定队列&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;TopicExchange:多关键字匹配&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*/
&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; @Bean&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; publicDirectExchange&nbsp;defaultExchange()&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;returnnewDirectExchange(EXCHANGE,&nbsp; true,&nbsp; false);
&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp; /**
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;配置消息队列2
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;针对消费者配置&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;@return
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*/

&nbsp;&nbsp;&nbsp;&nbsp; @Bean
&nbsp;&nbsp;&nbsp;&nbsp; publicQueue&nbsp;queue()&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;returnnewQueue( "delay_queue2",&nbsp; true);&nbsp; //队列持久&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp; /**
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;将消息队列2与交换机绑定
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;针对消费者配置&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;@return
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*/

&nbsp;&nbsp;&nbsp;&nbsp; @Bean&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; @Autowired
&nbsp;&nbsp;&nbsp;&nbsp; publicBinding&nbsp;binding()&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;returnBindingBuilder.bind(queue()).to(defaultExchange()).with(DelayQueue.ROUTINGKEY2);&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp; /**
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;接受消息的监听,这个监听会接受消息队列1的消息
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;针对消费者配置&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;@return
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*/

&nbsp;&nbsp;&nbsp;&nbsp; @Bean&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; @Autowired
&nbsp;&nbsp;&nbsp;&nbsp; publicSimpleMessageListenerContainer&nbsp;messageContainer2(ConnectionFactory&nbsp;connectionFactory)&nbsp;{&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SimpleMessageListenerContainer&nbsp;container&nbsp;=&nbsp;newSimpleMessageListenerContainer(connectionFactory());&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;container.setQueues(queue());&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;container.setExposeListenerChannel( true);&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;container.setMaxConcurrentConsumers( 1);&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;container.setConcurrentConsumers( 1);&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;container.setAcknowledgeMode(AcknowledgeMode.MANUAL);&nbsp; //设置确认模式手工确认&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;container.setMessageListener(newChannelAwareMessageListener()&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; publicvoid&nbsp;onMessage(Message&nbsp;message,&nbsp;com.rabbitmq.client.Channel&nbsp;channel)&nbsp;throwsException{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; byte[]&nbsp;body&nbsp;=&nbsp;message.getBody();&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println( "delay_queue2&nbsp;收到消息&nbsp;:&nbsp;"+&nbsp;newString(body));&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;channel.basicAck(message.getMessageProperties().getDeliveryTag(),&nbsp; false);&nbsp; //确认消息成功消费&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;});&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return&nbsp;container;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;
}

这种方式可以自定义进入死信队列的时间;是不是很完美,但是有的小伙伴的情况是消息中间件就是rocketmq,公司也不可能会用商业版,怎么办?那就进入下一节

四、时间轮算法

7ce87a9df4c04edc56fcc4106b0374bd.jpeg

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

五、redis过期监听

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

2. 创建配置类RedisListenerConfig(配置RedisMessageListenerContainer这个Bean)

package&nbsp;com.zjt.shop.config; import&nbsp;org.springframework.beans.factory.annotation.Autowired; import&nbsp;org.springframework.context.annotation.Bean; import&nbsp;org.springframework.context.annotation.Configuration; import&nbsp;org.springframework.data.redis.connection.RedisConnectionFactory; import&nbsp;org.springframework.data.redis.core.RedisTemplate; import&nbsp;org.springframework.data.redis.listener.RedisMessageListenerContainer; import&nbsp;org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import&nbsp;org.springframework.data.redis.serializer.StringRedisSerializer;&nbsp;
&nbsp;
@Configurationpublic&nbsp; class&nbsp;RedisListenerConfig&nbsp;{&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; @Autowired
&nbsp;&nbsp;&nbsp;&nbsp; private&nbsp;RedisTemplate&nbsp;redisTemplate;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; /**
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;@return
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*/

&nbsp;&nbsp;&nbsp;&nbsp; @Bean
&nbsp;&nbsp;&nbsp;&nbsp; public&nbsp;RedisTemplate&nbsp;redisTemplateInit()&nbsp;{&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //&nbsp;key序列化
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;redisTemplate.setKeySerializer( new&nbsp;StringRedisSerializer());&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //val实例化
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;redisTemplate.setValueSerializer( new&nbsp;GenericJackson2JsonRedisSerializer());&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return&nbsp;redisTemplate;
&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; @Bean
&nbsp;&nbsp;&nbsp;&nbsp; RedisMessageListenerContainer&nbsp;container(RedisConnectionFactory&nbsp;connectionFactory)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RedisMessageListenerContainer&nbsp;container&nbsp;=&nbsp; new&nbsp;RedisMessageListenerContainer();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;container.setConnectionFactory(connectionFactory);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return&nbsp;container;
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;
}

3.继承KeyExpirationEventMessageListener创建redis过期事件的监听类

package&nbsp;com.zjt.shop.common.util; import&nbsp;com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import&nbsp;com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import&nbsp;com.zjt.shop.modules.order.service.OrderInfoService; import&nbsp;com.zjt.shop.modules.product.entity.OrderInfoEntity; import&nbsp;com.zjt.shop.modules.product.mapper.OrderInfoMapper; import&nbsp;lombok.extern.slf4j.Slf4j; import&nbsp;org.springframework.beans.factory.annotation.Autowired; import&nbsp;org.springframework.data.redis.connection.Message; import&nbsp;org.springframework.data.redis.listener.KeyExpirationEventMessageListener; import&nbsp;org.springframework.data.redis.listener.RedisMessageListenerContainer; import&nbsp;org.springframework.stereotype.Component;&nbsp;

@Slf4j @Componentpublic&nbsp; class&nbsp;RedisKeyExpirationListener&nbsp;extends&nbsp;KeyExpirationEventMessageListener&nbsp;{&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; public&nbsp;RedisKeyExpirationListener(RedisMessageListenerContainer&nbsp;listenerContainer)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; super(listenerContainer);
&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; @Autowired
&nbsp;&nbsp;&nbsp;&nbsp; private&nbsp;OrderInfoMapper&nbsp;orderInfoMapper;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; /**
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;针对redis数据失效事件,进行数据处理
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;@param&nbsp;message
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp;@param&nbsp;pattern
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*/

&nbsp;&nbsp;&nbsp;&nbsp; @Override
&nbsp;&nbsp;&nbsp;&nbsp; public&nbsp;void&nbsp;onMessage(Message&nbsp;message,&nbsp;byte[]&nbsp;pattern)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; try&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;key&nbsp;=&nbsp;message.toString();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //从失效key中筛选代表订单失效的key
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if&nbsp;(key&nbsp;!=&nbsp; null&nbsp;&&&nbsp;key.startsWith( "order_"))&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //截取订单号,查询订单,如果是未支付状态则为-取消订单
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String&nbsp;orderNo&nbsp;=&nbsp;key.substring( 6);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;QueryWrapper<OrderInfoEntity>&nbsp;queryWrapper&nbsp;=&nbsp; new&nbsp;QueryWrapper<>();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;queryWrapper.eq( "order_no",orderNo);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;OrderInfoEntity&nbsp;orderInfo&nbsp;=&nbsp;orderInfoMapper.selectOne(queryWrapper);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if&nbsp;(orderInfo&nbsp;!=&nbsp; null)&nbsp;{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if&nbsp;(orderInfo.getOrderState()&nbsp;==&nbsp; 0)&nbsp;{&nbsp;&nbsp;&nbsp; //待支付
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;orderInfo.setOrderState( 4);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //已取消
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;orderInfoMapper.updateById(orderInfo);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.info( "订单号为【"&nbsp;+&nbsp;orderNo&nbsp;+&nbsp; "】超时未支付-自动修改为已取消状态");
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp; catch&nbsp;(Exception&nbsp;e)&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;e.printStackTrace();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.error( "【修改支付订单过期状态异常】:"&nbsp;+&nbsp;e.getMessage());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;}
}

4:测试

通过redis客户端存一个有效时间为3s的订单:

1e0906f62860bdd38041b2d03dd656ba.jpeg

结果:

599a2b435a488fb1f34ad0e8c8c592a7.jpeg

总结:&nbsp;以上方法只是个人对于关单的一些想法,可能有些地方有疏漏,请直接留言进行指出,当然如果你有更好的关单方式也可以随时沟通交流。

猜你喜欢

转载自blog.csdn.net/jjc4261/article/details/125806974
今日推荐