死信队列
死信队列概述
延迟、延迟指定业务的逻辑,在实际生活中比较常见,比如超时未处理订单关闭,本章以商城平台订单为支付超时,掌握如何在死信队列模型的消息延迟发送。
在之前的消息模型中,都有一个共同的特点,就是消息一到队列,就立马被消费者消费了,所以死信队列的初衷就是为了解决这个问题,希望能延迟一定的时间给消费者处理。
死信队列间接和作用
传统的业务处理是使用定时器轮训的方式处理满足的业务条件,比如超时处理,会没10秒钟轮训一次数据库,拿到未支付订单的时间跟当前时间对比,如果小于30分钟就取消订单。
这样就带来一个问题,如果未支付的订单太多,就会给数据库带来压力,甚至会压垮数据库,而死信队列的引入,将极大的改善了原有的处理流程。
典型的应用场景介绍
以商城选购商品超时未支付为场景
代码实战
死信队列专有名词介绍
跟普通的消息队列模式一样,死信队列也有交换机、路由、队列,不过还加了三位成员
DLX: 死信交换机 ,一种特殊的交换机类型
DLK: 死信路由,跟死信交换机绑定了就是死信路由
TTL: 存活时间 ,指的是进入死信队列的时间,意味着消息死了,注意一下,消息死了是以下三种情况
1.消息被拒绝,比如,channel.basicReject和basicNack表示不再重新接收
2.消息超过了指定的存活时间
3.队列达到了最大长度
放发生上述三种情况的时候,消息将会被重新发送到另外一个交换机,该交换机就是死信交换机,然后就是跟之前一样的流程。简单的说就是将消息延迟一定的时间到另外一个交换机被发送出去了。
死信队列消息模型
首先,消息经过第一个中转站,即基本消息模型中的基本交换机,由基本交换机和基本路由绑定是死信队列,等死信队列的TTL到了,这个的TTL消息时可以设置的,死信队列也可以设置,两种取最小的。TTL的倒计时到了就消息就会被转发到死信交换机,死信交换机跟死信路由绑定的是真正的队列,队列的消费者受到消息后,处理逻辑。这段一定要看明白
- 绑定,就是采用BindingBuilder.bind,构件
- 组成,某个组件的一部分,比如死信队列,它的DLX,TTL,DLK,TTL不是必须的,发现创建new Queue的最后一个参数就是配置这些的
队列和交换机初始化
/**死信队列消息模型构建----------------------------------------------------------------------------------**/
// 创建第一个中转站
//创建死信队列
@Bean(name = "basicDeadQueue")
public Queue basicDeadQueue() {
Map<String, Object> params = new HashMap<>();
// x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
params.put("x-dead-letter-exchange", "basicDeadExchange");
// x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
params.put("x-dead-letter-routing-key", "deal-key");
// 注意这里是毫秒单位,这里我们给10秒
params.put("x-message-ttl", 10000);
return new Queue("basicDeadQueue", true, false, false, params);
}
//创建“基本消息模型”的基本交换机,面向生产者
@Bean
public TopicExchange basicProducerExchange() {
//创建并返回基本交换机实例
return new TopicExchange("basicNormalQueue", true, false);
}
//创建“基本消息模型”的基本绑定(基本交换机+基本路由),面向生产者
@Bean
public Binding basicProducerBinding() {
//创建并返回基本消息模型中的基本绑定(注意这里是正常交换机跟死信队列绑定在一定,不叫死信路由)
return BindingBuilder.bind(basicDeadQueue()).to(basicProducerExchange()).with("normal-deal-key");
}
// 创建第二个中转站
// 创建真正队列,面向消费者
@Bean(name = "realConsumerQueue")
public Queue realConsumerQueue() {
//创建并返回面向消费者的真正队列实例
return new Queue("realConsumerQueue", true);
}
// 创建死信交换机
@Bean
public TopicExchange basicDeadExchange() {
//创建并返回死信交换机实例
return new TopicExchange("basicDeadExchange", true, false);
}
// 创建死信路由及其绑定
@Bean
public Binding basicDeadBinding() {
//创建死信路由及其绑定实例
return BindingBuilder.bind(realConsumerQueue()).to(basicDeadExchange()).with("deal-key");
}
这个死信路由的排场挺大
死信队列的接收和发送
@RequestMapping("/testDealQueue")
public ResultVo testDealQueue(@RequestBody DeadInfo info) throws JsonProcessingException {
Message message= MessageBuilder.withBody(objectMapper.writeValueAsBytes(info))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
// 注意这里是发送到正常的队列 绑定到一个正常的路由
rabbitTemplate.convertAndSend("basicNormalQueue","normal-deal-key", message);
// 输出日志
log.info("死信队列实战-发送对象类型的消息入死信队列-内容为:{} ", info);
return ResultVo.success("死信队列实战-发送对象类型的消息入死信队列成功");
}
/**
* 监听真正队列——消费队列中的消息,面向消费者
* @param info
*/
@RabbitListener(queues = "realConsumerQueue",containerFactory = "singleListenerContainer")
public void consumeMsg(@Payload DeadInfo info){
try {
log.info("死信队列实战-监听真正队列-消费队列中的消息,监听到消息内容 为:{}",info);
}catch (Exception e){
log.error("死信队列实战-监听真正队列-消费队列中的消息 - 面向消费者- 发生异常:{} ",info,e.fillInStackTrace());
}
}
商城平台订单支付超时
整体业务场景介绍
整体业务流程分析
数据库设计
CREATE TABLE user_order (
id int(11) NOT NULL AUTO_INCREMENT,
order_no varchar(255) NOT NULL COMMENT '订单编号',
user_id int(11) NOT NULL COMMENT '用户id',
status int(11) DEFAULT NULL COMMENT '状态(1=已保存;2=已付款;3=已取消)',
is_active int(255) DEFAULT '1' COMMENT '是否有效(1=有效;0=失效)',
create_time datetime DEFAULT NULL COMMENT '下单时间',
update_time datetime DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户下单记录表';
CREATE TABLE mq_order (
id int(11) NOT NULL AUTO_INCREMENT,
order_id int(11) NOT NULL COMMENT '下单记录id',
business_time datetime DEFAULT NULL COMMENT '失效下单记录的时间',
memo varchar(255) DEFAULT NULL COMMENT '备注信息',
PRIMARY KEY ('id')
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='RabbitMQ失效下单记录的历史记录表';
构建死信队列模型
/**死信队列完成订单超时消息模型构建----------------------------------------------------------------------------------**/
// 创建第一个中转站
//创建死信队列
@Bean(name = "basicDealOrderQueue")
public Queue basicOrderQueue() {
Map<String, Object> params = new HashMap<>();
// x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
params.put("x-dead-letter-exchange", "basicDealOrderExchange");
// x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
params.put("x-dead-letter-routing-key", "deal-order-key");
// 注意这里是毫秒单位,这里我们给10秒
params.put("x-message-ttl", 10000);
return new Queue("basicOrderDeadQueue", true, false, false, params);
}
//创建“基本消息模型”的基本交换机,面向生产者
@Bean
public TopicExchange basicOrderExchange() {
//创建并返回基本交换机实例
return new TopicExchange("basicOrderExchange", true, false);
}
//创建“基本消息模型”的基本绑定(基本交换机+基本路由),面向生产者
@Bean
public Binding basicOrderBinding() {
//创建并返回基本消息模型中的基本绑定(注意这里是正常交换机跟死信队列绑定在一定,不叫死信路由)
return BindingBuilder.bind(basicDeadQueue()).to(basicProducerExchange()).with("order-normal-deal-key");
}
// 创建第二个中转站
// 创建真正队列,面向消费者
@Bean(name = "realOrderQueue")
public Queue realOrderQueue() {
//创建并返回面向消费者的真正队列实例
return new Queue("realOrderQueue", true);
}
// 创建死信交换机
@Bean
public TopicExchange basicDeadOrderExchange() {
//创建并返回死信交换机实例
return new TopicExchange("basicDealOrderExchange", true, false);
}
// 创建死信路由及其绑定
@Bean
public Binding basicDeadOrderBinding() {
//创建死信路由及其绑定实例
return BindingBuilder.bind(realConsumerQueue()).to(basicDeadExchange()).with("deal-order-key");
}
业务开发
用户下单
package com.learn.boot.dto;
import com.sun.istack.internal.NotNull;
import javax.validation.constraints.NotBlank;
/**
* zlx
* 2020年9月6日12:00:08
* 接收用户下单实体
*/
public class UserOrderDto {
@NotBlank
//订单编号(必填)
private String orderNo;
//用户id(必填)
@NotNull
private Integer userId;
}
控制层
@RequestMapping("/testDelayOrder")
public ResultVo testDelayOrder(@RequestBody UserOrderDto dto,BindingResult bindingResult) throws Exception {
return deadUserOrderService.createOrder(dto);
}
服务层
public ResultVo createOrder(UserOrderDto dto) {
try {
// 创建用户下单实例
UserOrder userOrder=new UserOrder();
userOrder.setUserId(dto.getUserId());
userOrder.setOrderNo(dto.getOrderNo());
// 设置支付状态为已保存
userOrder.setStatus(1);
// 设置下单时间
userOrder.setCreateTime(new Date());
userOrder.setUpdateTime(new Date());
// 插入用户下单记录
userOrderMapper.insertSelective(userOrder);
log.info("用户成功下单,下单信息为:{}",userOrder);
// 以下代码是将用户下单产生的下单记录id加入死信队列,相应的功能代码将
// 生成用户下单记录id
Integer orderId = userOrder.getId();
// 将生成的用户下单记录id加入死信队列等待延迟处理
Message message= MessageBuilder.withBody(objectMapper.writeValueAsBytes(orderId))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
// 注意这里是发送到正常的队列 绑定到一个正常的路由
rabbitTemplate.convertAndSend("basicOrderExchange","order-normal-deal-key", message);
// 输出日志
log.info("死信队列发送对象类型的消息入死信队列-内容为:{} ", orderId);
return ResultVo.success("死信队列实战-发送对象类型的消息入死信队列成功");
}catch (Exception ex) {
return ResultVo.error("下单异常");
}
}
更新订单状态
/**
* @param orderId
* @param status 2:已付款,3已取消
* @return
*/
@Override
public ResultVo updateOrderStatus(Integer orderId,Integer status) {
UserOrder userOrder = new UserOrder();
userOrder.setId(orderId);
userOrder.setStatus(status);
userOrder.setIsActive(status == 3 ? 2:1);
userOrderMapper.updateByPrimaryKeySelective(userOrder);
if (userOrder.getIsActive() == 1) {
return ResultVo.success("处理订单成功");
}
// 记录“失效用户下单记录”的历史
// 定义RabbitMQ死信队列历史失效记录实例
MqOrder mqOrder=new MqOrder();
// 设置失效时间
mqOrder.setBusinessTime(new Date());
// 设置备注信息
mqOrder.setMemo("更新失效当前用户下单记录Id,orderId="+ userOrder.getId());
// 设置下单记录id
mqOrder.setOrderId(userOrder.getId());
// 插入失效记录
mqOrderMapper.insertSelective(mqOrder);
return ResultVo.success("死信队列订单成功");
}
监听队列
/**
* 监听真正队列——消费队列中的消息,面向消费者
* @param orderId
*/
@RabbitListener(queues = "realOrderQueue",containerFactory = "singleListenerContainer")
public void realOrderQueue(@Payload Integer orderId){
try {
deadUserOrderService.updateOrderStatus(orderId,3);
log.info("死信队列超时订单-监听真正队列-消费队列中的消息,监听到消息内容 为:{}",orderId);
}catch (Exception e){
log.error("死信队列超时订单-监听真正队列-消费队列中的消息 - 面向消费者- 发生异常:{} ",orderId,e.fillInStackTrace());
}
}