Rabbit MQ并没有直接支持消息延时的功能,但是可以通过设置队列(消息)的过期时间(TTL)和死信交换机(DLX)来实现消息这一个功能,估计没听过TTL和DLX的童鞋是不是一脸懵?这是个啥东西?哈哈…那现在就来看一下TTL和DXL的定义吧。
TTL是Time to Live 的简称,就是过期时间的意思,RabbitMQ 可以对消息和队列设置TTL。
延时交换机和延时队列设置如下,SpringBoot + RabbitMQ的代码如下:
@Configuration
public class RabbitMqProducerConfig {
//延时交换机
public static final String TTL_EXCHANGE = "ttl.exchange";
//延时队列
public static final String TTL_QUEUE = "ttl_queue";
//延时交换机的routing_key
public static final String TTL_ROUTING_KEY = "ttl.routingkey";
/**
* 设置一个延时队列
*
* @return
*/
@Bean
public Queue ttlQueue() {
Map params = new HashMap<>();
//设置消息的过期时间,单位:毫秒
params.put("x-message-ttl", 10000);
//设置延时交换机
params.put("x-ttl.exchange", TTL_EXCHANGE);
//设置延时交换机的路由键
params.put("x-ttl.routingkey", TTL_ROUTING_KEY);
return QueueBuilder.durable(TTL_QUEUE).withArguments(params).build();
}
/**
* 设置一个延时交换机
*/
@Bean
public TopicExchange ttlExchange() {
return new TopicExchange(TTL_EXCHANGE);
}
/**
* 死信队列绑定死信交换机
*/
@Bean
public Binding ttlqueueBindExchange() {
return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with(TTL_ROUTING_KEY);
}
}
那么队列(消息)过期后,会怎么样? 当然是Rabbit MQ回收啦,不过这样不就达不到消息延时消费的这个目的了吗?消息都被回收了,消费者怎么消费呢?所以Rabbit MQ出个东西,叫做死信交换机(DLX),那么DLX是怎么定义的呢?
DLX是Dead-Letter-Exchange的简称,就是死信交换机的意思。死信交换机和普通的交换机没有什么区别,只是当
消息在一个队列中变成死信之后,它能被重新被发送到另一个交换器中,而这个交换机就是死信交换机。那么和死信交换机绑定的队列叫啥呢?不用猜肯定就叫死信队列了。
那么什么样的消息才会变成死信呢?如下三种情况出现,消息就会变成死信:
死信交换机和死信队列设置如下,SpringBoot + RabbitMQ的代码:
@Configuration
public class RabbitMqProducerConfig {
//死信交换机
public static final String TTL_DLK_EXCHANGE = "ttl.dlk.exchange";
//死信交换机的routing_key
public static final String TTL_DLK_ROUTING_KEY = "ttl.dlk.routingkey";
//死信队列
public static final String TTL_DLK_QUEUE = "ttl_dlk_queue";
/**
* 申明一个死信队列
*
* @return
*/
@Bean
public Queue dlkQueue() {
Map params = new HashMap<>();
//设置死信交换机
params.put("x-dead-letter-exchange", TTL_DLK_EXCHANGE);
//设置死信交换机的路由键
params.put("x-dead-letter-routing-key", TTL_DLK_ROUTING_KEY);
//durable 表示持久化
return QueueBuilder.durable(TTL_DLK_QUEUE).withArguments(params).build();
}
/**
* 申明一个topic类型的死信交换机
*
* @return
*/
@Bean
public TopicExchange dlkExchange() {
return new TopicExchange(TTL_DLK_EXCHANGE);
}
/**
* 死信队列绑定死信交换机
*/
@Bean
public Binding dlkqueueBindExchange() {
return BindingBuilder.bind(dlkQueue()).to(dlkExchange()).with(TTL_DLK_ROUTING_KEY);
}
}
通过上面的内容,相信童鞋们已经了解了什么是TTL和DLX了,那么就很容易通过TTL和DLX来设置消息的延时消费了。
其实延时消费就是指消费者在指定等待多长时间后才能拿到消息进行消费,那么我们就可以将死信队列看做是一个延时队列,因为我们可以设置消息的过期时间,比如我们将消息的过期时间设置为10s,且不让它匹配任何消费者,那么10s后,这个消息就会变成死信,通过DLX的定义可以知道,死信会先重发到死信交换机,然后死信交换机会将消息发送到和它绑定的死信队列当中,死信队列会将这个消息发送给和它匹配的消费者进行消费,那么这样就相当于消费者延迟了10s才把这个消息进行消费了。
至此,TTL和DLX就完成了整个消息的延时设置了。
其实思路很简单,现在就是要解决消息的重复消费问题,那么在我们进行消费之前,查询一下这条消息是否已经消费过,如果已经消费过了,那么这次就不消费了,如果没有消费,那么就继续消费,这样不就解决重复消费的问题了吗?那么专业术语怎么回答呢?
专业术语总结如下:要解决消息的重复消费问题,首先要保证消息的唯一性,其次要保证消息的幂等性!
幂等性定义:一次和多次请求某一个资源对于资源本身应该具有同样的结果,也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。在编程中,就是一个请求,不管其请求多少次,其结果都不会改变
这个需要根据实际情况来定,如果是订单系统的话,那么订单号是可以作为标志消息的唯一性标志。或者组装一个消息ID作为订单的唯一标志都是可以的。订单的唯一标志加订单的状态足可以保证消息的幂等性。
下面列举两个例子,帮助大家理解:
<1>假如在一个系统中,你消费了一条消息之后,就会往数据库插入一条数据,那么在这个消息第二次到来时,你可以根据这个消息的唯一标志查询数据库,该消息是否已经存在,如果不存在,则进行消费插入数据即可。如果存在数据,则证明已经消费,那么直接丢弃这条消息即可。
当然你也可以不查询数据库,直接往数据表插入数据,但是如果你需要保证消息不被重复,就是数据库不能存在两条一样的消息。因此你需要将消息的唯一标志做为数据表的唯一键。
<2>假如在一个订单系统中,已经存在了一条订单在数据表中,订单状态为未支付,此时来了一个消息,要更改这条订单的状态,将未支付的订单该成已支付,显然需要根据订单号和订单状态去更改这条订单数据,SQL语句如下:
update order_table
set status = '已支付'
where order_id = #{order_id}
and status = ‘未支付’
如上面语句所示,当第二条重复消息来的时候,由于此时订单已经被修改成已支付了,显然条件不匹配,消息不会被执行。所以第二条消息就不会被重复消费了。
全篇终,致敬!
欢迎各位关注我的JAVAERS公众号,陪你一起学习,一起成长,一起分享JAVA路上的诗和远方。在公众号里面都是JAVA这个世界的朋友,公众号每天会有技术类文章,面经干货,也有进阶架构的电子书籍,如Spring实战、SpringBoot实战、高性能MySQL、深入理解JVM、RabbitMQ实战、Redis设计与实现等等一些高质量书籍,关注公众号即可领取哦。