更新 2022-10-28

说明:关于使用rabbitmq实现订单超时的部分说明有错误,首先mq是可以实现自定义超时时间的,我们可以在创建队列queue.ordercreate时不设置它的x-message-ttl参数,转而在代码里设置消息过期时间。

public void test() {
        // 消息后处理对象,设置一些消息的参数信息
        MessagePostProcessor messagePostProcessor = message -> {
            //1.设置message的信息
            // 第二个方法:消息的过期时间 ,5秒之后过期,这里可以自由设置消息过期时间
           message.getMessageProperties().setExpiration("5000");
            //2.返回该消息
            return message;
        };

        rabbitTemplate.convertAndSend("exchange.ordertimeout", "*", "hello!", messagePostProcessor);
    }

=========================================================

先描述一下业务场景,用户下单后在规定时间内没有完成支付,那么系统需要把订单终结掉。

但是这个规定时间可能不是定死的,它可能是3小时,2小时,30分钟等等

个人的实现思路

一、轮询数据库

这种方式就是在保存订单的时候把订单的超时时间也一起保存进去,然后用定时任务去轮询数据库获取未支付的订单,再去判断是否超时了。
但是!这种方法太捞了呀,而且也不具备实时性,比如我有个订单号为:123的订单是在10:00:00这个时间超时,但是我的定时任务是每5分钟执行一次,它又恰好是在09:58:00执行了一次任务,那么它这个时候拿到订单号:123的订单做判断,结果是未超时。然后下一次执行是在10:03:00,这个时候再拿到123的订单,肯定是超时了。也就是说我们订单应该在10:00:00就超时的,可直到10:03:00才超时。这就是没有实时性。
这咱都不说数据量太多处理起来还贼慢的情况。

二、mq延迟消息

第二种方式就是借助消息队列,这里我只提供rabbitMQ的实现思路。
使用rabbitMQ实现延迟消息首先要了解两个点:消息的TTL和死信Exchange。通过这两个我们就可以实现延迟消息了。

TTL(Time To Live)

消息的ttl意思就是消息的过期时间。rabbitmq中可以对消息和队列分别设置过期时间。

对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。

30分钟订单过期选用redis还是mq 订单超时方案_java


死信Exchange(Dead Letter Exchanges)

死信交换机不是一个消息队列了,而是一个交换机,交换机可以绑定多个消息队列。

当消息满足以下情况时,消息会进入死信队列:

  • 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。
  • 消息的TTL到期了,消息过期了。
  • 队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信交换机上。

死信交换机并不是一个特殊的交换机,它也是一个很普通的交换机,只是因为它的作用不同,所以称呼也不同。
在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。

30分钟订单过期选用redis还是mq 订单超时方案_java_02


具体实现步骤:1.创建死信交换器 exchange.ordertimeout (fanout)

30分钟订单过期选用redis还是mq 订单超时方案_rabbitmq_03


2.创建队列queue.ordertimeout

30分钟订单过期选用redis还是mq 订单超时方案_30分钟订单过期选用redis还是mq_04


3.建立死信交换器 exchange.ordertimeout 与队列queue.ordertimeout 之间的绑定

30分钟订单过期选用redis还是mq 订单超时方案_队列_05


4.创建队列queue.ordercreate,Arguments添加:

x-message-ttl=10000

x-dead-letter-exchange: exchange.ordertimeout

30分钟订单过期选用redis还是mq 订单超时方案_队列_06


代码层面实现:
我们每次创建订单的时候要往queue.ordercreate这里消息队列里方法消息,可以将订单编号作为消息发送。
在写一个监听类,去监听queue.ordertimeout这个消息队列,当“queue.ordercreate”里的消息过期时会被转发到“queue.ordertimeout”里
这个时候拿到“queue.ordertimeout”里的消息(订单编号),做一个超时处理就行了。

小结:

用rabbitmq作为中间件可以解决很多后端服务的问题,而且是低耦合。
如果你的业务场景要设置所有订单都是固定好的时间内过期,比如都是在2小时内过期,那么很简单,只需要把对应的“x-message-ttl”的值设置成“7200000”。这种情况下用rabbitmq很合适。

回到开头,我们说可能这个时间也不是固定死的,它可能是3小时,2小时,30分钟等等。如果不多,可以多开几个延迟队列,对应不同的过期时间。

如果设置过期时间的自由度很高,用rabbitmq咋实现,以我目前的道行,我实现不了,哈哈哈哈哈哈!

接下的方案就是针对那种自定义过期时间的!

三、redis的过期事件

redis作为中间件有多好用,咱就不多BB里。直接上代码!

第一步、 首先写一个redis的配置类:

@Configuration
public class RedisListenerConfig {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 处理乱码
     */
    @Bean
    public RedisTemplate redisTemplate() {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        return redisTemplate;
    }

    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {

        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);

        return container;
    }

}

第二步、再写一个监听类,这个类需要继承KeyExpirationEventMessageListener类,并且重写onMessage方法。

@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    /**
     * 针对redis数据失效事件处理,进行数据处理
     *
     * @param message
     * @param pattern
     */
    @Override
    public void onMessage(Message message, byte[] pattern) {
        //获取失效的key
        String expirationKey = message.toString();
        //对开头是ordertimeout的键进行处理
        if (expirationKey.startsWith("ordertimeout:")) {
            String[] split = expirationKey.split(":");
            String orderId = split[1];
            //处理订单
            this.endOrder(orderId);
        }
    }
}

第三步、创建订单的时候存入redis里,key的格式:“ordertimeout:订单号”,value:“订单过期时间"

/**
     * 设置订单超时
     *
     * @param time
     * @param orderId
     */
    public void setTimeOut(LocalDateTime time, String orderId) {
        //获取截止时间
        LocalDateTime endTime = time;
        Date endDate = Date.from(endTime.atZone(ZoneId.systemDefault()).toInstant());
        //系统当前时间,计算秒值
        Date nowDate = new Date();
        //相减获取秒
        long expirationTime = (endDate.getTime() - nowDate.getTime()) / 1000;
        redisTemplate.boundValueOps("ordertimeout:" + orderId).set(endTime, expirationTime, TimeUnit.SECONDS);

    }

最后我们就可以在监听类里处理过期的订单了。

四、总结

以上三种方案,第一种个人认为应该没有人会用,就连我自己也觉得是废话。后面两种方案可以参考着来,延迟消息适合那种固定死时间的场景,redis的过期事件适合那种灵活设置过期时间的场景。