✨这里是第七人格的博客✨小七,欢迎您的到来~✨
🍅系列专栏:无🍅
✈️本篇内容: rabbitmq如何实现延时队列✈️
🍱本篇收录完整代码地址:gitee.com/diqirenge/s…🍱
一、楔子
延时队列在许多业务场景中都有着广泛的运用。但可惜的是在RabbitMQ中并未提供延迟队列功能。这里小七结合工作所用,列出2种实现方式。
(1)使用TTL+死信队列组合实现延迟队列的效果。
(2)使用RabbitMQ官方延迟插件,实现延时队列效果。
二、使用TTL+死信队列组合实现延迟队列的效果
使用这种方式实现延时队列,我们首先要理清楚2个概念。TTL和死信队列。
1、TTL
TTL 全称 Time To Live(存活时间/过期时间)。当消息到达存活时间后,还没有被消费,会被自动清除。RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。可以类比redis的TTL。
2、死信队列
死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机)。那如何让一个普通的队列成为死信队列呢?
消息成为死信的三种情况:
(1) 队列消息长度到达限制;
(2)消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
(3) 原队列存在消息过期设置,消息到达超时时间未被消费;
3、逻辑整理
结合TTL和死信队列,我们可以做以下思考
(1)创建队列b(普通队列)和队列a(死信队列)分别绑定交换机d,并且提供监听队列b(普通队列)的消费者c,队列a(死信队列)不提供消费者。
(2)生产者发送包含TTL的消息到队列a(死信队列)。
(3)队列a(死信队列)过期后,经由交换机d转发到队列b(普通队列)。
(4)消费者c消费队列b(普通队列)的信息。
如图:
4、代码实现
有一个经典的业务场景可以使用延迟队列:下单后,30分钟未支付,取消订单。这里小七基于这个需求给出代码实现。
测试消息体
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageStruct implements Serializable {
private static final long serialVersionUID = 392365881428311040L;
private String message;
}
RabbitMQ常量池
public interface RabbitConsts {
//=====================TTL+死信队列实现======================//
/**
* 声明了队列里的死信转发到的DLX名称
*/
String X_DEAD_LETTER_EXCHANGE = "x-dead-letter-exchange";
/**
* 声明了这些死信在转发时携带的 routing-key 名称
*/
String X_DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
/**
* 消息过期后会转到该队列中,需要消费者监听并消费
*/
String CANCELORDER_CONSUME_QUEUE = "cancelOrderConsumeQueue";
/**
* 声明了生产者在发送消息时携带的 routing-key 名称
*/
String CANCELORDER_SEND_ROUTINGKEY = "cancelOrderSendRoutingKey";
/**
* 声明了生产者在发送消息时携带的 Queue 名称
*/
String CANCELORDER_QUEUE = "cancelOrderSendQueue";
}
配置文件
@Slf4j
@Configuration
public class RabbitMqConfig {
@Bean
public RabbitTemplate rabbitTemplate(CachingConnectionFactory connectionFactory) {
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> log.info("消息发送成功:correlationData({}),ack({}),cause({})", correlationData, ack, cause));
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message));
return rabbitTemplate;
}
@Bean
public TopicExchange synCancelOrderDelayExchange(){
TopicExchange topicExchange = new TopicExchange(RabbitConsts.X_DEAD_LETTER_EXCHANGE,true,false);
return topicExchange;
}
@Bean
public Queue synCancelOrderDelaySendQueue() {
/**
durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列
auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
exclusive 表示该消息队列是否只在当前connection生效,默认是false
*/
Map<String, Object> params = new HashMap<>();
// x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
params.put(RabbitConsts.X_DEAD_LETTER_EXCHANGE, RabbitConsts.X_DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
params.put(RabbitConsts.X_DEAD_LETTER_ROUTING_KEY,RabbitConsts.X_DEAD_LETTER_ROUTING_KEY);
return new Queue(RabbitConsts.CANCELORDER_QUEUE, true, false, false,params);
}
@Bean
public Queue synCancelOrderDelayConsumeQueue() {
/**
durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列
auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
exclusive 表示该消息队列是否只在当前connection生效,默认是false
死信队列里的消息过期后会转到该队列中
*/
return new Queue(RabbitConsts.CANCELORDER_CONSUME_QUEUE, true, false, false);
}
@Bean
public Binding bindingCancelOrderSend() {
return BindingBuilder.bind(synCancelOrderDelaySendQueue()).to(synCancelOrderDelayExchange()).with(RabbitConsts.CANCELORDER_SEND_ROUTINGKEY);
}
@Bean
public Binding bindingCancelOrderConsume() {
return BindingBuilder.bind(synCancelOrderDelayConsumeQueue()).to(synCancelOrderDelayExchange()).with(RabbitConsts.X_DEAD_LETTER_ROUTING_KEY);
}
}
消费者
@Slf4j
@Component
@RabbitListener(queues = RabbitConsts.CANCELORDER_CONSUME_QUEUE)
public class CancelOrderConsumer {
@RabbitHandler
public void directHandlerManualAck(MessageStruct messageStruct, Message message, Channel channel) {
// 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
final long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
log.info("订单自动取消延迟队列,手动ACK,接收消息:{}", JSONUtil.toJsonStr(messageStruct));
// 通知 MQ 消息已被成功消费,可以ACK了
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
try {
// 处理失败,重新压入MQ
channel.basicRecover();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
生产者
@RestController
@Api(tags = "测试SpringBoot整合进行各种工作模式信息的发送", value = "测试SpringBoot整合进行各种工作模式信息的发送")
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 下单完成会发送消息到延迟队列 用于自动取消订单
*/
@ApiOperation(value="测试延迟队列发送接口(订单取消)",notes="测试延迟队列发送(订单取消)")
@GetMapping(value="/synSendCancelOrderMessage")
public void synSendCancelOrderMessage(){
rabbitTemplate.convertAndSend(RabbitConsts.X_DEAD_LETTER_EXCHANGE,
RabbitConsts.CANCELORDER_SEND_ROUTINGKEY, new MessageStruct("自动取消订单"), message -> {
message.getMessageProperties().setExpiration(String.valueOf(10 * 1000));
return message;
});
}
}
完整项目代码地址:gitee.com/diqirenge/s…
三、使用RabbitMQ官方延迟插件,实现延时队列效果。
基于TTL+死信队列的组合虽然可以实现延迟队列,但是对于平常的开发太过繁琐。于是小七在rabbitmq官方,发现了官方推荐的一款延迟队列插件,让延时队列的实现同一般队列那么简单。
测试消息体
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageStruct implements Serializable {
private static final long serialVersionUID = 392365881428311040L;
private String message;
}
RabbitMQ常量池
public interface RabbitConsts {
/**
* 延迟队列
*/
String DELAY_QUEUE = "delay.queue";
/**
* 延迟队列交换器
*/
String DELAY_MODE_QUEUE = "delay.mode";
}
配置文件
@Slf4j
@Configuration
public class RabbitMqConfig {
@Bean
public RabbitTemplate rabbitTemplate(CachingConnectionFactory connectionFactory) {
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> log.info("消息发送成功:correlationData({}),ack({}),cause({})", correlationData, ack, cause));
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message));
return rabbitTemplate;
}
//===================延迟队列插件实现start====================//
/**
* 延迟队列
*/
@Bean
public Queue delayQueue() {
return new Queue(RabbitConsts.DELAY_QUEUE, true);
}
/**
* 延迟队列交换器, x-delayed-type 和 x-delayed-message 固定
*/
@Bean
public CustomExchange delayExchange() {
Map<String, Object> args = new HashMap<>(16);
args.put("x-delayed-type", "direct");
return new CustomExchange(RabbitConsts.DELAY_MODE_QUEUE, "x-delayed-message", true, false, args);
}
/**
* 延迟队列绑定自定义交换器
*
* @param delayQueue 队列
* @param delayExchange 延迟交换器
*/
@Bean
public Binding delayBinding(Queue delayQueue, CustomExchange delayExchange) {
return BindingBuilder.bind(delayQueue).to(delayExchange).with(RabbitConsts.DELAY_QUEUE).noargs();
}
//===================延迟队列插件实现end====================/
}
消费者
@Slf4j
@Component
@RabbitListener(queues = RabbitConsts.DELAY_QUEUE)
public class DelayQueueConsumer {
@RabbitHandler
public void directHandlerManualAck(MessageStruct messageStruct, Message message, Channel channel) {
// 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
final long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
log.info("延迟队列,手动ACK,接收消息:{}", JSONUtil.toJsonStr(messageStruct));
// 通知 MQ 消息已被成功消费,可以ACK了
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
try {
// 处理失败,重新压入MQ
channel.basicRecover();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
生产者
@RestController
@Api(tags = "测试SpringBoot整合进行各种工作模式信息的发送", value = "测试SpringBoot整合进行各种工作模式信息的发送")
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 测试延迟队列发送
*/
@ApiOperation(value="测试延迟队列发送接口",notes="测试延迟队列发送")
@GetMapping(value="/sendDelay")
public void sendDelay() {
rabbitTemplate.convertAndSend(RabbitConsts.DELAY_MODE_QUEUE, RabbitConsts.DELAY_QUEUE, new MessageStruct("delay message, delay 5s, " + DateUtil
.date()), message -> {
message.getMessageProperties().setHeader("x-delay", 5000);
return message;
});
rabbitTemplate.convertAndSend(RabbitConsts.DELAY_MODE_QUEUE, RabbitConsts.DELAY_QUEUE, new MessageStruct("delay message, delay 2s, " + DateUtil
.date()), message -> {
message.getMessageProperties().setHeader("x-delay", 2000);
return message;
});
rabbitTemplate.convertAndSend(RabbitConsts.DELAY_MODE_QUEUE, RabbitConsts.DELAY_QUEUE, new MessageStruct("delay message, delay 8s, " + DateUtil
.date()), message -> {
message.getMessageProperties().setHeader("x-delay", 8000);
return message;
});
}
}
完整项目代码地址:gitee.com/diqirenge/s…