06、SpringBoot + RabbitMQ 死信队列实战Topic/manual/Aop/安全性(做冗余和备份)/ 定时器
死信队列: 接盘侠,
在开发中,延时和延迟处理一些指定的业务逻辑是非常常见的事情,
- 比如商城平台订单处理超过30分钟的订单未支付将自动关闭
- 商城订单完成后,用户一直未评价,5天后自动好评
- 会员到期前15天,到期前3天发送短信提醒
- 或者12306的抢票也是如此,10分钟支付时间,如果你没支付就自动取消,让其他的用户的去抢票
- 或者银行取钱给你180s取钱时间,如果你没取钱当前视为放弃,银行卡退出。。。
这些场景都可以使用中间件RabbitMQ提供的死信队列,进行得以处理。
01、死信队列 — 春运 12306 抢票
死信队列(ex - routing - queue)又称之为”延迟队列“或者”延时队列”,也是RabbitMQ队列的一种,指的是进入队列的消息会被延迟消费的队列,这种队列根普通的队列相比,最大的差异在于消息一旦进入普通队列将会立即被消费处理,而进入死信队列则会过一定的时间在被消费处理。
在传统企业级的应用系统中实现消息,业务数据的延迟处理一般是通过开启定时器的方式,轮询扫描并获取数据表中满足条件的业务数据记录,比较数据记录的业务时间和当前时间,如果当前时间大于记录中的业务时间,则说明数据记录已经超过了指定的时间而未被处理,此时需要执行相应的业务逻辑,比如:失效该数据记录,发送通知信息给指定用户等。
这种处理方式中,定时器每隔一定的时间不间断地去扫描数据库表,并不断地获取满足业务条件的数据,直到手动关闭定时器,如果不关闭,定时器开启的线程会一直执行下去。
思考:
假设如果这个时候我们开发的是淘宝,京东或者12306这样的网站使用定时器这种方式可取吗?以12306为例,相g都用12306抢票过,在抢票的过程中一般会提醒用户:“请在30分钟内付款”。一般用户会点击“立即付款”,然后输入相应的支付密码支付车票的费用。扣款成功后,12306官网会发送邮件或者短信通知用户抢票成功等信息。
然后实际情况却存在一些特殊情况,比如用户抢到了火车票,由于各种因素迟迟没有付款,过了30分钟以后仍然没有支付车票的费用,最后导致系统自动取消该笔订单。
如果这个时候采用的是定时器的方式进行开发,并判断用户下单实际距离当前实际是否已经超过30分钟,如果是,则表示用户在30分钟内仍然没有付款,系统将自动失效该笔订单并回收该车票,整个业务流程如下:
众所周知,抢票完全是一个非常庞大的数据量,高并发场景(全国几乎上千万,上亿的)人都在抢票,在某一时刻车票开抢之后,正常情况下将陆续会又用户抢到火车票,但是距离车票付款成功是有一定的时间间隔的,在这段时间内,如果定时器频繁地从数据库中获取:“未付款”状态的订单,其数据量之大难以想象,而且如果大批量的用户在30分钟内迟迟不付款,那从数据库中获取的数据量将一直增长,当达到一定程度时,将给数据库服务器和应用服务器带来巨大的压力,甚至之间压垮服务器,导致抢票等业务全线崩溃,带来的直接后果不堪设想。
所以在早期的很多抢票软件每当赶上春运高峰期的时候,经常会出现“网站崩溃”,点击购买车票却一直没响应等状况。某种程度上可能是因为某一时刻产生的高并发请求,或者频繁访问数据得到的数据过大导致内存,CPU,网络和数据库服务器等负载过高引起的。
而死信队列的引入,不管是从业务层面还是应用的性能层面来看,都大大地改善了原有的处理流程,下面是“抢票成功后30分钟内未付款的处理流程”:
优化后的处理流程中可以看出RabbitMQ的引入主要替代了传统处理的“定时器”处理逻辑,采用RabbitMQ的死信队列进行处理
死信队列是指:是一种可以延迟一定时间的在处理相应的业务逻辑,而这也可以看做是死信队列的作用,即死信队列可以实现特定的消息、业务数据等待一定的时间后再被消费者监听消费处理。
02、死信队列 — 商城用户购买商品未付款订单
和抢票一样,在一些电商或者有支付的场景中,也会存在这种类似的情况。用户在选购商品后点击“去付款”后,商城将会引导用户跳转到支付页面,此时系统会为用户生成一笔对应购物车中商品的订单,并将该订单的付款状态设置为0。即代表未付款,同时该订单id或者订单编号加入RabbitMQ的死信队列中,并设置延迟时间为30分钟。
如果用户在30分钟内选择了某种付款方式进行付款,则系统将更新该订单ID对应的订单的付款状态为1,即已付款状态。同时在商品表的库存表中更新订单中所包含的商品对应的库存,
如果用在30分钟内未付款,则RabbitMQ的死信队列对应的消费者将会在30分钟后监听到该订单id或编号,根据订单id或编号查询数据库的商品订单表,如果该订单的付款状态仍然为0,即未付款,则表示用户在30分钟内仍然未付款,此时需要失效该笔订单,同时更新回退商品库存,这以业务场景的整体流程如下图所示:
从上图可以看出,RabbitMQ死信队列的引入主要是用于延迟一定时间在处理特定的业务逻辑,而这种延迟在RabbitMQ中是一种“自动化”的,无需人为进行干预,即只需要指定延迟队列中绑定的交换机所对应处理业务逻辑的“真正队列”,并开发这个“真正队列”对应的消费者的监听消费功能即可。
03、RabbitMQ死信队列实战
RabbitMQ的死信队列,相对于传统的定时器轮询的处理方式,死信队列具有占有系统资源少,(比如不需要轮询数据库获取数据,减少DB层面资源的消耗),人为很少干预,只需要搭建好死信队列消息模型,就可以不需要认为去进行干预了,以及自动处理消息和数据,不夸张的讲,在实际项目中,任何需要延迟,延时处理的业务都可以使用死信队列这个强大的组件。
死信队列的专有术语和词汇
与普通的队列相比,死信队列同样具有三个核心成员:
- 交换机
- 路由
- 队列
只不过死信队列,增加了另外三个成员即:
- DLX:死信交换机:即Dead -Letter-Exchange。是一种特殊的交换机。
- DLK:死信路由:即Dead-Letter-Routing-Key。主要和DLX一起使用,
- TTL:存活时间,即Time-To-Live,指的是进入死信队列的消息可以存活的时间,一旦达到TTL,讲以为这该消息“死了”,从而进入下一个中转站,等待被正在的消息队列监听消费。
什么样子的消息会进入死信DLX —死信队列 —DLX–DLK
- 消息被拒绝,比如通过调用basic.reject或者basic.nack方法的时候,会进入到死信中,并且不在重新投递,即requeue参数的取值是false.
- 消息超过指定的存活时间(比如通过调用messageProperties.setExpiration()设置消息的TTL时间即可实现)。
- 队列达到最大长度。
当发生上述的情况时,将会出现死信的情况,而之后的消费讲被重新投递到另一个交换机,此时该交换机就是死信队列交换机。由于该死信队列交换机和死信路由绑定在一起对应真正的队列。导致消息讲被分发到真正的队列。最终被该队列对应的消费者所监听消费,简单说:就是没有被死信队列消费的消息,讲换个地方重新被消费从而实现消息:“延迟”,“延时”消费,而这个地方就是消息的下一个中转站,即死信交换机。
在前面我们得知,RabbitMQ的基础消息模型是由:交换机,路由和队列及其绑定所组成。生产者生产消息后,将消息发送到消费模型的交换机中,又由交换机与绑定的路由key找到对应的队列,然后监听该队列的消费者进行消费和处理,如下图:
死信队列消息模式实战
消息的载体
package com.pug.mq.dead;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class DeadInfo implements java.io.Serializable{
private Integer id;
private String message;
}
关系绑定配置类
package com.pug.mq.dead;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DeadRabbitmqConfiguration {
public static final String DEAD_ORDER_EXCHANGE_NAME = "pug.dead.order.exchange";
public static final String DEAD_ORDER_ROUTING_KEY = "pug.dead.order.routing.key";
public static final String DEAD_ORDER_QUEUE = "pug.dead.order.queue";
// 面向生产者的交换机
public static final String PRODUCER_ORDER_EXCHANGE = "pug.producer.order.exchange";
public static final String PRODUCER_ORDER_ROUTING_KEY = "pug.producer.order.routing.key";
/**
* 创建死信队列
*
* @return
*/
@Bean
public Queue basicDeadQueue() {
// 使用Map存放死信队列的三个核心组成部分
Map<String, Object> args = new HashMap<>();
// 创建死信队列交换机
args.put("x-dead-letter-exchange", DEAD_ORDER_EXCHANGE_NAME);
// 创建死信队列路由
args.put("x-dead-letter-routing-key", DEAD_ORDER_ROUTING_KEY);
// 设定TTL,单位是ms,下面的单位是10s
args.put("x-message-ttl", 10000);
// 创建队列并返回死信队列实例
return QueueBuilder.durable(DEAD_ORDER_QUEUE).withArguments(args).build();
}
/**
* 创建 基本消息模型的基础交换机,面向生产者
* @return
*/
@Bean
public TopicExchange basicProducerOrderExchange(){
return ExchangeBuilder.topicExchange(PRODUCER_ORDER_EXCHANGE).durable(true).build();
}
/**
* 创建基本绑定,(基础交换机+基础路由),面向生产者
* @return
*/
@Bean
public Binding basicProducerOrderBinding(){
return BindingBuilder.bind(basicDeadQueue())
.to(basicProducerOrderExchange())
.with(PRODUCER_ORDER_ROUTING_KEY);
}
/*************************************面向消费者*********************************/
public static final String CONSUMER_ORDER_QUEUE = "pug.consumer.order.queue";
/**
* 真正的队列,面向消费者
* @return
*/
@Bean
public Queue consumerOrderQueue(){
return QueueBuilder.durable(CONSUMER_ORDER_QUEUE).build();
}
/**
* 创建死信队列交换机
* @return
*/
@Bean
public TopicExchange basicDeadExchange(){
return ExchangeBuilder.topicExchange(DEAD_ORDER_EXCHANGE_NAME).durable(true).build();
}
/**
* 创建死信路由并绑定
* @return
*/
@Bean
public Binding basicDeadBinding(){
return BindingBuilder.bind(consumerOrderQueue()).to(basicDeadExchange()).with(DEAD_ORDER_ROUTING_KEY);
}
}
生产者发送消息
消费者等者死信过期消息消费