更新 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中可以对消息和队列分别设置过期时间。
对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
死信Exchange(Dead Letter Exchanges)
死信交换机不是一个消息队列了,而是一个交换机,交换机可以绑定多个消息队列。
当消息满足以下情况时,消息会进入死信队列:
- 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。
- 消息的TTL到期了,消息过期了。
- 队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信交换机上。
死信交换机并不是一个特殊的交换机,它也是一个很普通的交换机,只是因为它的作用不同,所以称呼也不同。
在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。
具体实现步骤:1.创建死信交换器 exchange.ordertimeout (fanout)
2.创建队列queue.ordertimeout
3.建立死信交换器 exchange.ordertimeout 与队列queue.ordertimeout 之间的绑定
4.创建队列queue.ordercreate,Arguments添加:
x-message-ttl=10000
x-dead-letter-exchange: exchange.ordertimeout
代码层面实现:
我们每次创建订单的时候要往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的过期事件适合那种灵活设置过期时间的场景。