1.死信交换机和死信队列实现延迟消息
1.1死信交换机和死信队列
什么是死信?
当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):
- 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
- 消息是一个过期消息,超时无人消费
- 要投递的队列消息满了,无法投递
如果这个包含死信的队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,检查DLX)。
在失败重试策略中,默认的RejectAndDontRequeueRecoverer会在本地重试次数耗尽后,发送reject给RabbitMQ,消息变成死信,被丢弃。
我们可以给simple.queue添加一个死信交换机,给死信交换机绑定一个队列。这样消息变成死信后也不会丢弃,而是最终投递到死信交换机,路由到与死信交换机绑定的队列。
1.2 TTL实现延迟消息
一个队列中的消息如果超时未消费,则会变为死信,超时分为两种情况:
- 消息所在的队列设置了超时时间
- 消息本身设置了超时时间
死信交换机配置:
@Configuration
public class TTLMessageConfig {
@Bean
public DirectExchange ttlDirectExchange(){
//声明交换机
return new DirectExchange("ttl.direct");
}
@Bean
public Queue ttlQueue(){
return QueueBuilder
.durable("ttl.queue")
.ttl(10000)//队列设置超时时间
.deadLetterExchange("dl.direct")//队列绑定死信交换机 超时会投递到该交换机
.deadLetterRoutingKey("dl")// 设置死信routingkey
.build();
}
@Bean
public Binding ttlBinding(){
//交换机和队列绑定
return BindingBuilder.bind(ttlQueue()).to(ttlDirectExchange()).with("ttl");
}
}
publisher:
/**
* 死信交换机发送延迟消息
* 死信队列设置的ttl 和 消息设置的setExpiration 较小的生效
*/
@Test
public void testTTLMessage() {
Map<String, String> msg = new HashMap<>();
msg.put("msg", "hello, ttl messsage");
// 1.准备消息
Message message = MessageBuilder
.withBody(JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.setExpiration("5000").build();
// 2.发送消息
rabbitTemplate.convertAndSend("ttl.direct", "ttl", message);
// 3.记录日志
log.info("消息已经成功发送!");
}
consumer:
//设置死信交换机绑定死信队列并监听死信消息
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "dl.queue", durable = "true"),
exchange = @Exchange(name = "dl.direct"),
key = "dl"
))
public void listenDlQueue(Message msg) {
log.info("消费者接收到了dl.queue的延迟消息:"+ new String(msg.getBody(), StandardCharsets.UTF_8));
}
2.延迟队列实现
2.1 DelayExchange原理
DelayExchange是RabbitMQ的官方也推出了一个插件,原生支持延迟队列效果。安装DelayExchange步骤不做赘述,直接记录使用过程。
DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时,流程如下:
- 接收消息
- 判断消息是否具备x-delay属性
- 如果有x-delay属性,说明是延迟消息,持久化到硬盘,读取x-delay值,作为延迟时间
- 返回routing not found结果给消息发送者
- x-delay时间到期后,重新投递消息到指定队列
2.2 代码实现
publisher:
/**
* 测试延迟队列发送消息
* @throws InterruptedException
*/
@Test
public void testSendDelayMessage() {
Map<String, String> msg = new HashMap<>();
msg.put("msg", "hello, delay messsage");
// 1.准备消息
Message message = MessageBuilder
.withBody(JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.setHeader("x-delay", 10000) // 设置延迟10秒
.build();
// 2.准备CorrelationData
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 2.2.准备ConfirmCallback
correlationData.getFuture().addCallback(result -> {
// 判断结果
if (result.isAck()) {
// ACK
log.info("消息成功投递到交换机!消息ID: {}", correlationData.getId());
} else {
// NACK
log.error("消息投递到交换机失败!消息ID:{}", correlationData.getId());
// 重发消息
}
}, ex -> {
// 记录日志
log.error("消息发送失败!", ex);
// 重发消息
});
// 3.发送消息
rabbitTemplate.convertAndSend("delay.direct", "delay", message, correlationData);
log.info("发送消息完成");
}
consumer:
//声明并监听延迟队列 delayed = "true"设置延迟交换机
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue", durable = "true"),
exchange = @Exchange(name = "delay.direct", delayed = "true"),
key = "delay"
))
public void listenDelayExchange(Message msg) {
log.info("消费者接收到了delay.queue的延迟消息:"+new String(msg.getBody(),StandardCharsets.UTF_8));
}