参考资料
- RabbitMQ官方网站
- RabbitMQ官方文档
- 噼咔噼咔-动力节点教程
ttl:time to live ,顾名思义,就是过期消息,在指定时间内没有被接受的消息,就会过期。成为过期消息,或死信。
单条消息的过期时间只决定了 没有任何消费者消费时,消息可以存活多久
具体操作就是使用Message对象中的Properties去设置过期时间。如下
@GetMapping("/{msg}")
public void send(@PathVariable String msg){
MessageProperties messageProperties = new MessageProperties();
// 设置过期时间:单位:毫秒
messageProperties.setExpiration("15000");
Message message = MessageBuilder.withBody(msg.getBytes()).andProperties(messageProperties).build();
rabbitTemplate.convertAndSend(DirectExchangeConfig.exchangeName , "info" , message);
log.info("(ttl)发送消息 :{} , 过期时间 :{}" ,msg , LocalDateTimeUtil.of(System.currentTimeMillis()+15000));
}
发送到之前的直连交换器
访问路径/ttl/一条会过期的消息
到控制台查看
过一段时间再查看就无了。
队列的过期时间决定了 在没有任何消费者的情况下,队列中的消息可以存活多久。
注意事项
两种配置方法
@Bean
// 方式1:使用map将参数传入
public Queue queueTTLA(){
Map<String , Object> map = new HashMap<>();
map.put("x-message-ttl",15000);
return QueueBuilder.durable("xcong.queue.ttl.A").withArguments(map).build();
}
@Bean
// 方式2:利用QueueBuilder
public Queue queueTTLB(){
return QueueBuilder.durable("xcong.queue.ttl.B").ttl(15000).build();
}
@RestController
@Slf4j
@RequestMapping("/ttl")
public class TTLController {
@Resource
private RabbitTemplate rabbitTemplate;
@GetMapping("/q/{msg}")
public void sendQ(@PathVariable String msg){
rabbitTemplate.convertAndSend("xcong.fanout","",msg.getBytes(StandardCharsets.UTF_8));
log.info("(TTL队列)成功发送消息 {} " ,msg);
}
}
访问接口/ttl/q/一条会过期的消息
对比正常的队列,可以发现,ttl队列的消息会自动过期
即DLX(Dead-Letter-Exchange)。也称为:死信交换机、死信邮箱。
如下情况后,消息会进入死信交换机中。并进一步被安排到死信队列里,消费者也可以从死信队列中获取消息。
很常见的买票下订单场景。比如一个用户下了订单买票,需要在30分钟内完成支付。
如果超过30分钟没有完成,就会发送消息(比如短信)通知用户,并修改订单状态(为未支付),还需要是否存票让别人购买(修改库存信息)。
而这后续的一系列操作,就可以设置一个消费者去监听死信队列来专门完成。
根据流程图,我们需要设置2个交换机,一个为带过期时间的正常的交换机(TTL),另一个则作为死信交换机(DLX。
注意:死信交换机也只是一个普通的交换,用法和命名是一样的。
为了便于测试,需要分别声明两个不同的队列,和上面的交换机分别绑定。至此前置设置完毕。
核心关键是:如何让正常队列的信息过期后进入到死信队列。使用到的参数如下:
@Configuration
public class DlxExchangeConfig {
public static String ttlXName = "xcong.dlx.ttl";
public static String ttlKey = "normal";
public static String dlxXName = "xcong.dlx.dlx";
public static String dlxKey = "dead";
@Bean
public DirectExchange ttlExchange(){
return ExchangeBuilder.directExchange(ttlXName).build();
}
@Bean DirectExchange dlxExchange(){
return ExchangeBuilder.directExchange(dlxXName).build();
}
@Bean
// 配置普通的过期队列
public Queue queueTTL(){
// 设置一个15秒过期的队列
// 并且要求指定dlx交换机和dlx的key
Map<String , Object> config = new HashMap<>();
config.put("x-message-ttl" , 15000);
config.put("x-dead-letter-exchange" , dlxXName);
config.put("x-dead-letter-routing-key" , dlxKey);
return QueueBuilder.durable("queue.dlx.ttl").withArguments(config).build();
}
@Bean
public Binding bindingTTL(){
return BindingBuilder.bind(queueTTL()).to(ttlExchange()).with(ttlKey);
}
@Bean
// 配置死信队列
public Queue queueDLX(){
return QueueBuilder.durable("queue.dlx.dead").build();
}
@Bean
public Binding bindingDLX(){
return BindingBuilder.bind(queueDLX()).to(dlxExchange()).with(dlxKey);
}
}
接口代码
@RestController
@RequestMapping("/dlx")
@Slf4j
public class DLXController {
@Resource
private RabbitTemplate rabbitTemplate;
@GetMapping("/{msg}")
public void sentErrorMsg(@PathVariable("msg") String msg ){
log.info("准备发送的信息:{} , 路由键 :{}",msg , ttlKey);
// 发送到普通的延时列表中
rabbitTemplate.convertAndSend(ttlXName , ttlKey , msg.getBytes(StandardCharsets.UTF_8));
log.info("成功发送!");
}
}
发送消息,查看后台
查看死信队列
略
当队列的信息达到最大长度时,先入队的消息会被发送到DLX。
示例思路:
结果:消息123进入死信(先进先出),45678依然在put队列中
从正常的队列接受消息,但是对消息不进行确认,并且不对消息进行重新投递。
此时消息就会进入死信队列。
这里设计到一个新技术点:消费者手动确认消息
核心配置
在yml中添加如下,开启手动确认
spring:
rabbitmq:
listener:
simple:
# 手动acks-用户必须通过通道感知侦听器进行ack/nack。
acknowledge-mode: manual
接收消息的代码就需要利用Channel(信道)。核心的方法如下:
channel.basicAck(long deliveryTag, boolean multiple)
channel.basicNack(long deliveryTag, boolean multiple, boolean requeue)
@RabbitListener(queues = {"xcong.fanout.A", "xcong.fanout.B", "xcong.direct.C", "xcong.direct.D"})
public void reviverMsg(Message message, Channel channel) {
byte[] body = message.getBody();
String result = new String(body);
MessageProperties messageProperties = message.getMessageProperties();
// 获取消息传递的唯一标签
long deliveryTag = messageProperties.getDeliveryTag();
log.info("接收到的消息:{}", result);
try {
// 进行确认
channel.basicAck(deliveryTag , false);
} catch (Exception e) {
try {
// 进行拒绝
channel.basicNack(deliveryTag , false ,true);
log.error("遇到异常,拒绝消息:{}" ,e);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
// 略,沿用之前的直连交换机进行测试
测试前进入控制台确保消息都已经被消费
预计情况,消息会不断的返回队列并再次接受(记得打断点
@Service
@Slf4j
public class ConsumerService {
@RabbitListener(queues = {"xcong.direct.C", "xcong.direct.D"})
public void reviverMsg(Message message, Channel channel) {
byte[] body = message.getBody();
String result = new String(body);
MessageProperties messageProperties = message.getMessageProperties();
// 获取消费者队列名称
String consumerQueue = messageProperties.getConsumerQueue();
// 获取接受者交换机名称
String receivedExchange = messageProperties.getReceivedExchange();
// 获取消息传递的唯一标签
long deliveryTag = messageProperties.getDeliveryTag();
log.info("接收到的消息:{}", result);
log.info("消息队列 :{} , 交换机名称:{}", consumerQueue, receivedExchange);
log.info("唯一标识:{}", deliveryTag);
try {
// 模拟异常
Integer.parseInt(result);
// 进行手动确认,并关闭批量确认
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
try {
// 进行手动拒绝,并关闭批量拒绝,同时返回队列
channel.basicNack(deliveryTag, false, true);
log.error("遇到异常,异常信息:{}", e.getLocalizedMessage());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
}
前面我们使用basicNack方法来“拒绝”消息,还有一种拒绝方法叫reject。
在 RabbitMQ 中,nack 和 reject 消息都可以用于拒绝消息的处理。它们的不同之处在于:
basic.reject
拒绝消息时,消息会被立即丢弃,不会被重新排队。这意味着该消息将永远不会被消费者接收到。basic.nack
拒绝消息时,消息可以被重新排队或者被丢弃。basic.nack
可以接受三个参数:requeue
、multiple
和 delivery_tag
。
requeue
参数控制着消息是否应该重新排队multiple
参数控制着是否确认多个消息delivery_tag
参数则指定了要拒绝的消息。总的来说,如果你希望消息能够重新排队并稍后重新处理,那么应该使用 basic.nack
。如果你希望消息被永久地丢弃,那么应该使用 basic.reject
。
以消费前面案例中的queue.dlx.ttl队列中信息为例
@RabbitListener(queues = "queue.dlx.ttl")
public void rejectMsg(Message message, Channel channel) {
String str = new String(message.getBody());
MessageProperties messageProperties = message.getMessageProperties();
long deliveryTag = messageProperties.getDeliveryTag();
log.info("接到准备拒绝的消息 : {} , tag :{} 。。。。5秒后拒绝", str , deliveryTag);
try {
ThreadUtil.safeSleep(5000L);
channel.basicReject(deliveryTag, false);
log.info("拒绝消息:{}", str);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@RabbitListener(queues = "queue.dlx.dead")
public void receiveDeadMsg(Message message , Channel channel) throws IOException {
String str = new String(message.getBody());
MessageProperties messageProperties = message.getMessageProperties();
long deliveryTag = messageProperties.getDeliveryTag();
log.info("接收到的死信 : {} , tag :{} ", str , deliveryTag);
channel.basicAck(deliveryTag , true);
}
访问端口/dlx/一条新的信息:2023年10月17日11:42:13
,发送一条信息。
控制台日志打印如下