RabbitMQ延时队列及消息可靠性
延时队列
场景
在电商项目中订单环节,未支付15分钟关闭订单、订单成功支付需要调用许多服务(商品扣减、日志记录...)等保证最终一致性、发起退款一段时间没操作通知相关人员等,需要在一定的时间倒计时后消费。
这样场景可以设置定时器轮询数据库状态、修改状态 这种方法是可行但是效率太低,反复操作数据库增加IO。
MQ延迟消费场景则可以应对
介绍
MQ高级特性TTL,它可以是一个队列或者一个消息的属性,指在这个队列中所有消息最大存活时间或者单个消息的最大存活时间,单位毫秒,在最大存活时间范围内,如果消费没被消费就会路由到死信队列。
死信对列-当消息到底,代码中就可以设置监听队列,有消息立即执行逻辑。当普通队列消息在最大时间内未被消费,路由到对应死信队列,被监听者处理,那么这个过程就称延时方式。联想场景:未支付15分钟关闭订单 就可以不用定时器实现。
实现方案
1.声明订单队列、订单交换机、绑定订单交换机关系,死信订单交换机、死信交换机,订单队列绑定死信交换机及队列
//声明MQ队列、交换机
@Configuration
public class RabbitMqConfig {
//订单队列交换机
private static final String ORDER_EXCHANGE="order_exchange";
//订单队列
private static final String ORDER_QUEUE="order_queue";
//路由key 带通配符 以order. 开头命中
private static final String ORDER_ROUTINGKEY="order.#";
//订单死信交换机
private static final String DLX_ORDER_EXCHANGE="dlx_order_exchange";
//订单死信对列
private static final String DLX_ORDER_QUEUE="dlx_order_queue";
//路由Key
private static final String DLX_ROUTINGKEY="dlxOrder";
//订单异常交换机
private static final String EXCEPTION_ORDER_EXCHANGE="exception_order_exchange";
//订单异常队列
private static final String EXCEPTION_ORDER_QUEUE="exception_order_queue";
//订单异常路由Key
private static final String EXCEPTION_ROUTINGKEY="exceptionOrder";
//订单交换机
@Bean(RabbitMqConfig.ORDER_EXCHANGE)
public TopicExchange orderExchange(){
//durable持久化交换机 topic模式
return ExchangeBuilder.topicExchange(RabbitMqConfig.ORDER_EXCHANGE).durable(true).build();
}
//订单队列
@Bean(RabbitMqConfig.ORDER_QUEUE)
public Queue orderQueue(){
//持久化队列 绑定死信交换机和死信路由Key
return QueueBuilder.durable(RabbitMqConfig.ORDER_QUEUE)
.withArgument("x-dead-letter-exchange",RabbitMqConfig.DLX_ORDER_EXCHANGE)
.withArgument("x-dead-letter-routing-key",RabbitMqConfig.DLX_ROUTINGKEY).build();
}
//订单与交换绑定
@Bean
public Binding bindingOrderAndExchange(@Qualifier(RabbitMqConfig.ORDER_QUEUE)Queue orderQueue,
@Qualifier(RabbitMqConfig.ORDER_EXCHANGE)TopicExchange orderExchange ){
//队列 与 交换机 绑定 topic key order.开头的命中
return BindingBuilder.bind(orderQueue).to(orderExchange).with(RabbitMqConfig.ORDER_ROUTINGKEY);
}
//死信订单交换机
@Bean(RabbitMqConfig.DLX_ORDER_EXCHANGE)
public DirectExchange dlxOrderExchange(){
//durable持久化交换机 direct模式
return ExchangeBuilder.directExchange(RabbitMqConfig.DLX_ORDER_EXCHANGE).durable(true).build();
}
//死信订单队列
@Bean(RabbitMqConfig.DLX_ORDER_QUEUE)
public Queue dlxOrderQueue(){
//持久化队列
return QueueBuilder.durable(RabbitMqConfig.DLX_ORDER_QUEUE).build();
}
//死信订单队列与交换绑定
@Bean
public Binding bindingDlxOrderAndExchange(@Qualifier(RabbitMqConfig.DLX_ORDER_QUEUE)Queue dlxOrderQueue,
@Qualifier(RabbitMqConfig.DLX_ORDER_EXCHANGE)DirectExchange dlxOrderExchange ){
//队列 与 交换机 绑定 topic key order.开头的命中
return BindingBuilder.bind(dlxOrderQueue).to(dlxOrderExchange).with(RabbitMqConfig.DLX_ROUTINGKEY);
}
//异常订单交换机
@Bean(RabbitMqConfig.EXCEPTION_ORDER_EXCHANGE)
public DirectExchange exceptionOrderExchange(){
//durable持久化交换机 direct模式
return ExchangeBuilder.directExchange(RabbitMqConfig.EXCEPTION_ORDER_EXCHANGE).durable(true).build();
}
//异常订单队列
@Bean(RabbitMqConfig.EXCEPTION_ORDER_QUEUE)
public Queue exceptionOrderQueue(){
//持久化队列
return QueueBuilder.durable(RabbitMqConfig.EXCEPTION_ORDER_QUEUE).build();
}
//异常订单队列与交换绑定
@Bean
public Binding bindingExceptionOrderAndExchange(@Qualifier(RabbitMqConfig.EXCEPTION_ORDER_QUEUE)Queue exceptionOrderQueue,
@Qualifier(RabbitMqConfig.EXCEPTION_ORDER_EXCHANGE)DirectExchange exceptionOrderExchange ){
//队列 与 交换机 绑定 topic key order.开头的命中
return BindingBuilder.bind(exceptionOrderQueue).to(exceptionOrderExchange).with(RabbitMqConfig.EXCEPTION_ROUTINGKEY);
}
}
2.创建订单生产者
/**
* 生产者
* */
@RestController
public class OrderProducer {
/*注入RabbitTemplate*/
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/addOrder")
public String addOrder(){
rabbitTemplate.convertAndSend("order_exchange","order.1","我是1分钟~~~",message -> {
//60S未消费
message.getMessageProperties().setExpiration("60000");
message.getMessageProperties().setMessageId("1");
return message;
});
rabbitTemplate.convertAndSend("order_exchange","order.1","我是1分钟10秒~~~",message -> {
//70S未消费
message.getMessageProperties().setExpiration("70000");
message.getMessageProperties().setMessageId("2");
return message;
});
rabbitTemplate.convertAndSend("order_exchange","order.1","我是10秒钟~~~",message -> {
//10S未消费
message.getMessageProperties().setExpiration("10000");
message.getMessageProperties().setMessageId("3");
return message;
});
return "成功";
}
}
3.创建死信订单消费者监听队列
/**
* 死信订单消费者
* */
@Component
public class DlxOrderConsumer {
@RabbitListener(queues = {"dlx_order_queue"})
@RabbitHandler
public void orderConsumerHandle(String str, Channel channel, Message message) throws IOException {
try {
//获取消息Message对象 MessageBuilder.withBody(str.getBytes(StandardCharsets.UTF_8)).build();
System.out.println("消息ID:"+message.getMessageProperties().getMessageId()+",消息内容:"+str);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
} catch (Exception e) {
e.printStackTrace();
//失败 true 重回队列 false 丢弃
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
//channel.basicNack(msg.getMessageProperties().getDeliveryTag(),false,false);
//nack表示拒绝消息。multiple表示拒绝指定了delivery_tag的所有未确认的消息,requeue表示不是重回队列 如果队列绑定死信队列 会路由到死信队列
}
}
}
4.YAML配置 连接、消费者手动确认
server:
port: 8080
spring:
rabbitmq:
username:
password:
host: 127.0.0.1
port: 5672
virtual-host: /
listener:
simple:
acknowledge-mode: manual # 消费者手动回执
5.运行结果
补充
1. 上诉TTL是设置Message,如果设置队列,在队列中消息超过时间被路由到死信,如果Message和Queue队列都设置TTL,默认取最小值。
2. 当顺序发送2条数据,第一条A 70S,第二条B 10S,那么A、B哪条会先过期?如果选择B就错了!!!!
消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间
优化
1. 上诉提到,如果使用在消息属性上设置TTL的方式,消息可能并不会按时“死亡“,因为RabbitMQ只会检查第一个消息是否过期,如果过期则丢到死信队列,索引如果第一个消息的延时时长很长,而第二个消息的延时时长很短,则第二个消息并不会优先得到执行
2. A 70秒 B 10秒,那么必须等到第一个消息A70秒过期 第二条B才会进入死信
3. 解决方案
要求消息时间细粒度 安装RabbitMQ插件
将ez插件放入RabbitMq安装目录/plugins根目录
rabbitmq-plugins directories -s
启动插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
查询安装插件
rabbitmq-plugins list
重启Rabbit服务
4.代码需要变更
4.1:死信交换机
//死信订单交换机
@Bean(RabbitMqConfig.DLX_ORDER_EXCHANGE)
public DirectExchange dlxOrderExchange(){
//带插件延时交换机 delayed() ps:已创建好的交换机代码上修改,MQ管理页面记得删除之前交换机,让新代码重创建
return ExchangeBuilder.directExchange(RabbitMqConfig.DLX_ORDER_EXCHANGE).delayed().durable(true).build();
}
4.2:消息生产者 Message.setDelay(time)
/**
* 带插件的延时请求
* */
@GetMapping("/addDLXOrder")
public String addDLXOrder(){
rabbitTemplate.convertAndSend("dlx_order_exchange","dlxOrder","带插件延时,我是30秒~~~",message -> {
//30S未消费
message.getMessageProperties().setDelay(30000);
message.getMessageProperties().setMessageId("1");
return message;
});
rabbitTemplate.convertAndSend("dlx_order_exchange","dlxOrder","带插件延时,我是20秒~~~",message -> {
//20S未消费
message.getMessageProperties().setDelay(20000);
message.getMessageProperties().setMessageId("2");
return message;
});
rabbitTemplate.convertAndSend("dlx_order_exchange","dlxOrder","带插件延时,我是10秒钟~~~",message -> {
//10S未消费
message.getMessageProperties().setDelay(10000);
message.getMessageProperties().setMessageId("3");
return message;
});
return "成功";
}
4.3 结果 完成消息细粒度
消息ID:3,消息内容:带插件延时,我是10秒钟~~~
消息ID:2,消息内容:带插件延时,我是20秒~~~
消息ID:1,消息内容:带插件延时,我是30秒~~~
区别
问:为什么把在死信交换机上创建带插件属性?
答:普通TTL是投递到普通队列,超时后在路由到绑定的死信队列中,普通队列中超时时间不一致Message不会立即到死信,会依赖队列前面的消息TTL,无法完成细粒度。
而插件延迟队列需要判断是否到达延迟时间,不到延迟时间的需要保存在表中,时间到了再推送,这些判断和操作导致效率不如普通的Exchange,所以如果不需要的话,就不要用延迟队列。
故插件延迟投递的消息如果没有超过时间不会投递,所以我们修改创建在死信交换机上,当订单超过15分钟未支付,直接给死信交换机 -> 死信队列,跟普通TTL不一样。
可靠性
消息发送确认
消息发送确认分为2部分,其一:发送给交换机成功回调,其二:交换机传递给Queue队列成功回调。
1. 交换机成功确认
方式一:ConfirmCallback 接口实现,重写confirm方法
方式二: Send()发送前注入rabbitTemplate.setConfirmCallback(this);
方式一主要参数confirm(CorrelationData correlationData, boolean ack, String exception) CorrelationData:分配消息ID 可自定义ID,ack:true成功 false失败,exception:失败原因
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void initRabbitTemplate() {
//注册回调 this 实现ConfirmCallback
rabbitTemplate.setConfirmCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String exception) {
System.out.println(correlationData);
if(ack){
System.out.println("消息投递成功");
}else {
System.out.println("消息投递失败原因:"+exception);
}
}
这里CorrelationData类型在发送时未绑定,则获取是null,发送交换机失败,如何重新发送?
我们可以在发送指定CorrelationDataID 和MessageID关联,存放Redis或者DB,发送失败重新读取消息载体,发送。
rabbitTemplate.convertAndSend("order_exchange","order.1","我是1分钟~~~",message -> {
//60S未消费
message.getMessageProperties().setExpiration("60000");
message.getMessageProperties().setCorrelationId("1");
message.getMessageProperties().setMessageId("1");
return message;
},new CorrelationData("1"));
CorrelationData [id=1]
消息投递成功
CorrelationData [id=2]
消息投递成功
CorrelationData [id=3]
消息投递成功
2. 队列成功确认
一般情况,只要代码创建队列、交换机基本都是可以路由到的,除非写错了。
方式一:ReturnCallback接口实现,重写returnedMessage方法
方式二:rabbitTemplate.setReturnsCallback(returnedMessage->{})
测试可以在发送时候删除队列
@PostConstruct
public void initRabbitTemplate() {
//注册回调 this 实现ConfirmCallback
rabbitTemplate.setConfirmCallback(this);
//队列投递失败退回信息
rabbitTemplate.setReturnsCallback(returnedMessage -> {
Message message = returnedMessage.getMessage();
int replyCode = returnedMessage.getReplyCode();
String replyText = returnedMessage.getReplyText();
String exchange = returnedMessage.getExchange();
String routingKey = returnedMessage.getRoutingKey();
//重新发送
rabbitTemplate.convertAndSend(exchange,routingKey,message);
});
//失败退回,可以在yml配置
//rabbitTemplate.setMandatory(true);
}
server:
port: 8080
spring:
rabbitmq:
username:
password:
host: 127.0.0.1
port: 5672
virtual-host: /
listener:
simple:
acknowledge-mode: manual # 消费者手动回执
publisher-returns: true #队列确认消息 默认false
publisher-confirm-type: correlated #交换机确认消息 默认NONE 禁用发布模式 CORRELATED:发布消息成功到交换器后会触发回调方法
消息接收确认
手动确认
spring.rabbitmq.listener.direct.acknowledge-mode=MANUAL
1.手执ACK
channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
2.手执NACK
channel.basicNack(msg.getMessageProperties().getDeliveryTag(),false,false);
/nack表示拒绝消息。multiple表示拒绝指定了delivery_tag的所有未确认的消息,requeue表示不是重回队列 如果队列绑定死信队列 会路由到死信队列
3.手执Reject
//失败 true 重回队列 false 丢弃
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
补充
1. basicAck传递2个参数
deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel(信道) ,它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel
multiple:手动确认可以被批处理,为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
2.basicNack 传递3个参数 上诉deliveryTag、multiple已讲述
requeue: true :重回队列,false :丢弃 丢弃如果绑定死信会路由到死信
3.basicReject 传递2个参数 上诉deliveryTag已讲述,requeue: true :重回队列 必须等于true
4.重入队列NACK 默认回到队头,如果重回队尾
/**
* 死信订单消费者
* */
@Component
public class DlxOrderConsumer {
@RabbitListener(queues = {"dlx_order_queue"})
@RabbitHandler
public void orderConsumerHandle(String str, Channel channel, Message message) throws IOException {
try {
//获取消息Message对象 MessageBuilder.withBody(str.getBytes(StandardCharsets.UTF_8)).build();
System.out.println("消息ID:"+message.getMessageProperties().getMessageId()+",消息内容:"+str);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
} catch (Exception e) {
e.printStackTrace();
//失败 true 重回队列 false 丢弃
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
//channel.basicNack(msg.getMessageProperties().getDeliveryTag(),false,false);
//nack表示拒绝消息。multiple表示拒绝指定了delivery_tag的所有未确认的消息,requeue表示不是重回队列 如果队列绑定死信队列 会路由到死信队列
//重回队尾 默认NAck、Reject会回到队首
channel.basicPublish(message.getMessageProperties().getReceivedExchange(),
message.getMessageProperties().getReceivedRoutingKey(), MessageProperties.PERSISTENT_TEXT_PLAIN,
str.getBytes(StandardCharsets.UTF_8));
}
}
}
详细可参考源码:点击