1.高级特性
- TTL (过期消息)
- 死信队列
- 消息手动确认
- 消息可靠性投递(从生产者端到队列)
- Consumer ACK(从队列到消费者端)
- 消费端限流
- 延迟队列
- 日志与监控
- 消息可靠性分析与追踪
- 管理
2.TTL (过期消息)
过期时间也叫TTL消息,TTL:Time To Live,如果消息在过期时间内没有消费者进行接受,该条消息就会自动删除,消费过后的消息也会被删除。
消息的过期时间有两种设置方式:(过期消息)
2.1.设置单条消息的过期时间
@Service
public class SendMessage {
@Resource
private RabbitTemplate rabbitTemplate;
public void sendMsg(){
//设置消息过期时间
MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("35000"); //单位:毫秒
//方式一:使用建造者模式创建消息
Message message = MessageBuilder.withBody("hello,I am a ttl!".getBytes()).
andProperties(messageProperties).build();
//方式二:直接通过new方式创建
Message message = new Message("hello.I am a ttl2".getBytes(),messageProperties);
rabbitTemplate.convertAndSend("exchange.direct","error",message);
}
}
2.2.通过队列属性设置消息过期时间
//方式一
@Bean
public Queue queueA(){
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-message-ttl","35000");
return new Queue(queueAName,true,false,false,arguments);
}
//方式二
@Bean
public Queue queueB(){
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-message-ttl","35000");
return QueueBuilder.durable(queueBName).withArguments(arguments).build();
}
3.死信队列
也叫做死信交换机、死信邮箱等说法。
DLX:Dead-Letter-Exchange:死信交换机、死信邮箱
3.1.应用场景
淘宝买东西时,下单未付款30分钟后订单将会自动删除。就可以通过死信队列来实现,当用户下单后将订单信息保存到正常队列中去,等过30分中都未付款则将订单通过死信交换机路由到死信队列中去,让消费者执行一个提醒操作或者直接删除即可。这就实现了下单30分钟后未付款自动取消的操作。而未超过30分钟的订单仍保存在正常队列中,所以要继续付款只需要判断正常队列是否包含该订单信息。 【也可以叫做延迟队列,因为延迟队列就是基于死信队列实现的,我们后面会具体讲解延迟队列,下面的代码演示只是实现死信队列】
3.2.代码演示
1.配置死信队列等信息
my:
exchangeNormalName: exchange.normal.a #正常交换机
queueNormalName: queue.normal.a #正常队列,没有消费者,设置过期时间
exchangeDlxName: exchange.dlx.a #死信交换机
queueDlxName: queue.dlx.a #死信队列
2.配置死信队列等
@Configuration
public class DlxConfig {
@Value("${my.exchangeNormalName}")
private String exchangeNormalName;
@Value("${my.queueNormalName}")
private String queueNormalName;
@Value("${my.exchangeDlxName}")
private String exchangeDlxName;
@Value("${my.queueDlxName}")
private String queueDlxName;
//定义正常交换机
@Bean
public DirectExchange normalExchange(){
return ExchangeBuilder.directExchange(exchangeNormalName).build();
}
//定义正常队列
@Bean
public Queue normalQueue(){
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-message-ttl",20000); //设置队列的过期时间
//重点:这两个配置参数使正常队列中的消息过时后能路由到死信交换机中去
arguments.put("x-dead-letter-exchange",exchangeDlxName); //设置队列的死信交换机
arguments.put("x-dead-letter-routing-key","error"); //设置队列的死信路由key,该值要跟死信交换机到死信队列的路由key一致,因为此处的死信交换机是直连的
return QueueBuilder.durable(queueNormalName).withArguments(arguments).build();
}
//将正常队列和交换机绑定到一起
@Bean
public Binding normalBinding(DirectExchange normalExchange,Queue normalQueue){
return BindingBuilder.bind(normalQueue).to(normalExchange).with("order");
}
//定义一个死信交换机
@Bean
public DirectExchange dlxExchange(){
return ExchangeBuilder.directExchange(exchangeDlxName).build();
}
//定义一个死信队列
@Bean
public Queue dlxQueue(){
return QueueBuilder.durable(queueDlxName).build();
}
//将死信交换机和死信队列进行绑定
@Bean
public Binding dlxBinding(DirectExchange dlxExchange,Queue dlxQueue){
return BindingBuilder.bind(dlxQueue).to(dlxExchange).with("error");
}
}
3.3 最大长度
在正常队列中设置最大长度,当队列中的消息超过最大长度时,原先正常队列中的消息将会进入死信队列中去。例如:队列最大长度为5,发送了1,2,3,4,5,6,7,8,那么1,2,3将会进入死信队列中。
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-max-length",5); //设置队列的最大长度
4.消息手动确认
4.1代码演示
- 配置文件中开启消费者的手动确认
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual #开启消费者的手动确认
- 生产者跟死信队列中的示例一致,当然也可以不配置死信队列
- 消费者代码
@Component
public class MessageReceive {
//该注解表示Spring运行自动监听
@RabbitListener(queues = {"queue.normal.a","fanout.queueB","queue.direct.a"})
public void receiveMsg(Message message, Channel channel){
MessageProperties messageProperties = message.getMessageProperties();
//标识队列中具体的消息
long deliveryTag = messageProperties.getDeliveryTag();
try{
byte[] body = message.getBody();
String mess = new String(body);
System.out.println(mess);
//异常模拟
int a = 1/0;
//消费者手动确认,false只确认当前消息,改成true表示将之前为确认消息都进行确认
//如果不发生异常,这里就对消息进行确认了
channel.basicAck(deliveryTag,false);
}catch (Exception e){
System.out.println("消息处理异常");
try{
//不进行确认,第二个参数同上,第三个参数表示是否重新入队,若为false则表示不重新入队,即进入死信队列中,若没有配置死信队列则直接删除该消息
channel.basicNack(deliveryTag,false,true);
//拒绝确认,第二个参数表示是否重新入队,区别basicNack不可以批量处理
channel.basicReject(deliveryTag,false);
}catch (IOException ex){
throw new RuntimeException(ex);
}
throw new RuntimeException(e);
}
}
}
5. 延迟队列
5.1.应用场景
淘宝买东西时,下单未付款30分钟后订单将会自动删除。就可以通过死信队列来实现,当用户下单后将订单信息保存到正常队列中去,等过30分中都未付款则将订单通过死信交换机路由到死信队列中去,让消费者执行一个提醒操作或者直接删除即可。这就实现了下单30分钟后未付款自动取消的操作。而未超过30分钟的订单仍保存在正常队列中,所以要继续付款只需要判断正常队列是否包含该订单信息。
还是我们上面的死信队列场景。
5.2.技术对比
5.2.1 定时任务扫描
每隔3秒扫描一次数据库,查询过期的订单进行处理。如果超过30分钟则将订单进行删除。
**优点:**简单,容易实现
缺点:
- 存在延迟(延迟时间不准确),每隔1分钟扫描一次,就有可能延迟1分钟。如要求10点发送,9:59:59秒扫描了一次,下次扫描就到了10.00.59秒,发送时间就推迟了近1分钟。
- 性能较差,每次都需要全扫描数据库。
5.2.2 被动取消
当用户查询订单时,再判断订单是否超时,是则取消(交易关闭)。
**优点:**对服务器而言,压力小。
缺点:
- 用户不查询订单,将永远处于待支付状态,会对数据统计等功能造成影响
- 用户打开订单页面,有可能比较慢,因为恰好同一时间要处理大量订单,用户体验少稍差
5.2.3 JDK延迟队列
DelayedQueue:无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素
**优点:**实现简单,任务延迟低。
缺点:
- 服务器重启、宕机,数据将会丢失。
- 只适合单体项目,不适合集群。
- 订单量大,可能内存不足而发生异常。OOM
5.2.4 采用消息中间件实现延迟队列
RabbitMQ本身不支持延迟队列,可以使用TTL结合DLX的方式来实现消息的延迟投递,即把DLX跟某个队列绑定,到了指定时间,消息过期后,就会从DLX路由到这个队列,消费者可以从这个队列取走消息。
我们演示死信队列时是通过两个交换机,两个队列进行的。现在我们通过一个交换机实现延迟队列,当然两个也是可以的。一个交换机方式的话,就是设置交换机路由到正常队列的key=“order”,消息到了过期时间30分钟后又通过交换机路由到死信队列中,路由key=“error”。跟死信队列中的代码演示区别不大,死信交换机跟正常交换机是同一个。
问题讲解
如果先发送的消息,消息延迟时间长,会影响后面的延迟时间段的消息的消费。
解决
- 不同延迟时间的消息要发到不同的队列上,同一个队列的消息,它的延迟时间应该一样。也就是说过期时间最好设置在队列上,不要设置在每条消息中。
- 如果要设置在消息上,最好通过一个全局变量管理这个过期时间。
6.消息可靠性投递
在使用RabbitMQ的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ提供了两种方式用于控制消息的投递可靠性模式。
- confirm:确认模式
- return:退回模式
RabbitMQ整个消息投递的路径为:
producer -> rabbitmq borker -> exchange -> queue -> consumer
- 消息从producer到exchange则会返回一个confirmCallback
- 消息从exchange -> queue 投递失败则会返回一个returnCallback
我们利用这两个callback控制消息的可靠性投递。
注意:要保证消息投递的可靠性就需要牺牲性能,性能与可靠性之间是无法兼容的。
6.1消息投递到交换机的确认代码
- 配置文件application.yml开启确认模式:
spring:
rabbitmq:
host: 127.0.0.1
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: cyf # 用户名
password: 123456
publisher-confirm-type: correlated #开启确认模式,设置为关联模式
my:
exchangeName: exchange.confirm
queueName: queue.confirm
- 定义交换机以及队列
@Configuration
public class ConfirmConfig {
@Value("${my.exchangeName}")
private String exchangeName;
@Value("${my.queueName}")
private String queueName;
@Bean
public DirectExchange directExchange(){
return ExchangeBuilder.directExchange(exchangeName).build();
}
@Bean
public Queue queue(){
return QueueBuilder.durable(queueName).build();
}
@Bean
public Binding binding(DirectExchange directExchange,Queue queue){
return BindingBuilder.bind(queue).to(directExchange).with("info");
}
}
- 写一个类实现RabbitTemplate.ConfirmCallback,判断消息到交换机成功与否的标志ack
@Component
public class MyConfirmCallBack implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
if(b){
System.out.println("消息成功进入交换机");
return;
}
System.out.println("消息没有进入交换机,原因为:"+s);
}
}
可以根据具体的结果,如果ack为false,对消息进行重新发送或记录日志等处理
- 设置rabbitTemplate的确认回调方法,当消息发送时进行判断,判断消息是否到交换机中。
@Service
public class MessageService {
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private MyConfirmCallBack confirmCallBack;
@PostConstruct //构造方法后执行它,相当于初始化作用
public void init() {
rabbitTemplate.setConfirmCallback(confirmCallBack);
}
public void sendMsg() {
Message message = MessageBuilder.withBody("hello world".getBytes()).build();
CorrelationData correlationData = new CorrelationData();//关联数据
correlationData.setId("order_123456"); //发送订单信息
rabbitTemplate.convertAndSend("exchange.confirm", "info", message, correlationData);
}
}
- 消息发送
@SpringBootApplication
public class Application implements ApplicationRunner {
@Resource
private MessageService messageService;
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
messageService.sendMsg();
}
}
在4中交换机名如果不存在则b为false,但交换机存在,路由key不存在,b也为true。
当前类写法
可以将3和4中的代码进行合并,删除3代码,直接在4代码中实现接口。
@Service
public class MessageService implements RabbitTemplate.ConfirmCallback {
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private MyConfirmCallBack confirmCallBack;
@PostConstruct //构造方法后执行它,相当于初始化作用,通过调用this当前对象的confirm方法进行确认
public void init() {
rabbitTemplate.setConfirmCallback(this);
}
public void sendMsg() {
Message message = MessageBuilder.withBody("hello world".getBytes()).build();
CorrelationData correlationData = new CorrelationData();//关联数据
correlationData.setId("order_123456"); //发送订单信息
rabbitTemplate.convertAndSend("exchange.confirm", "info", message, correlationData);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
if(b){
System.out.println("消息成功进入交换机");
return;
}
System.out.println("消息没有进入交换机,原因为:"+s);
}
}
匿名类写法
@Service
public class MessageService{
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private MyConfirmCallBack confirmCallBack;
@PostConstruct //构造方法后执行它,相当于初始化作用,通过调用this当前对象的confirm方法进行确认
public void init() {
new RabbitTemplate.ConfirmCallback(){
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
if(b){
System.out.println("消息成功进入交换机");
return;
}
System.out.println("消息没有进入交换机,原因为:"+s);
}
}
}
public void sendMsg() {
Message message = MessageBuilder.withBody("hello world".getBytes()).build();
CorrelationData correlationData = new CorrelationData();//关联数据
correlationData.setId("order_123456"); //发送订单信息
rabbitTemplate.convertAndSend("exchange.confirm", "info", message, correlationData);
}
}
6.2.从交换机投递到队列的消息确认代码
一般两个确认机制都要同时写,我们这里为了便于学习,就分开写了。
- 文件配置
spring:
rabbitmq:
...
publisher-returns: true # 开启return模式
- 写一个类实现RabbitTemplate.ReturnCallback,判断消息是否由交换机路由到队列,只有失败才会执行该回调方法。
@Component
public class MyReturnCallBack implements RabbitTemplate.ReturnCallback {
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
System.out.println("消息没有正确路由到队列:"+message.toString());
}
}
- 调用该回调方法
@Service
public class MessageService {
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private MyConfirmCallBack confirmCallBack;
@Resource
private MyReturnCallBack returnCallBack;
@PostConstruct //构造方法后执行它,相当于初始化作用
public void init() {
rabbitTemplate.setConfirmCallback(confirmCallBack); //确认消息投递到交换机
rabbitTemplate.setReturnCallback(returnCallBack); //确认消息路由到队列
}
public void sendMsg() {
Message message = MessageBuilder.withBody("hello world".getBytes()).build();
CorrelationData correlationData = new CorrelationData();//关联数据
correlationData.setId("order_123456"); //发送订单信息
rabbitTemplate.convertAndSend("exchange.confirm", "info1", message, correlationData);
}
}
- 其他同1中代码。
6.3.备用交换机
alternate exchange:备用交换机,当消息投递不到队列时就自动投递到配置的备用交换机中,可以进行消息投递失败的提醒。备用队列绑定备用交换机,消费者监听备用队列中的消息,一旦有数据则发送给用户或开发人员实现消息投递的可靠性。
- 交换机名以及队列名的配置
my:
exchangeNormalName: exchange.normal #正常交换机
queueNormalName: queue.normal #正常队列
exchangeAlternateName: exchange.alternate #备用交换机
queueAlternateName: queue.alternate #备用队列
- 设置交换机和队列,在正常交换机中配置备用交换机,所以消息发送到正常交换机后路由不到正常队列才会进入配置的备用交换机中,因此要先投递到正常交换机。
@Configuration
public class RabbitConfig {
@Value("${my.exchangeNormalName}")
private String exchangeNormalName;
@Value("${my.queueNormalName}")
private String queueNormalName;
@Value("${my.exchangeAlternateName}")
private String exchangeAlternateName;
@Value("${my.queueAlternateName}")
private String queueAlternateName;
@Bean
public DirectExchange directExchange(){
return ExchangeBuilder.directExchange(exchangeNormalName)
.alternate(exchangeAlternateName) //设置备用交换机的名字
.build();
}
@Bean
public Queue normalQueue(){
return QueueBuilder.durable(queueNormalName).build();
}
@Bean
public Binding normalBinding(DirectExchange directExchange,Queue normalQueue){
return BindingBuilder.bind(normalQueue).to(directExchange).with("info");
}
@Bean
public FanoutExchange fanoutExchange(){
return ExchangeBuilder.fanoutExchange(exchangeAlternateName).build();
}
@Bean
public Queue alternateQueue(){
return QueueBuilder.durable(queueAlternateName).build();
}
@Bean
public Binding alternateBinding(FanoutExchange fanoutExchange,Queue alternateQueue){
return BindingBuilder.bind(alternateQueue).to(fanoutExchange);
}
}
- 消息发送
@Service
public class MessageService {
@Resource
private RabbitTemplate rabbitTemplate;
@Value("${my.exchangeNormalName}")
private String exchangeNormalName;
public void sendMsg(){
Message message = MessageBuilder.withBody("该消息发送不出去时到达备用交换机".getBytes()).build();
rabbitTemplate.convertAndSend(exchangeNormalName,"info1",message);
}
}
综上所述,备用交换机是用于解决消息路由不到队列时路由到备用队列中的,作用同returnCallBack.
7.消息投递可靠性总结
①代表消息从生产者发送到Exchange,可以使用6.1的confirmCallback实现
②代表消息从Exchange路由到Queue,可以使用6.2的returnCallback实现或者6.3的备用交换机实现
③代表消息在Queue中存储,交换机、队列和消息默认都是持久化存储,也就是即使服务重启,这些数据仍然存在,所以不需要额外设置。
④代表消费者监听Queue并消费消息,使用4.4的消息手动确认实现。
spring:
rabbitmq:
...
publisher-confirm-type: correlated #开启确认模式,设置为关联模式
publisher-returns: true # 开启return模式
my:
exchangeName: exchange.confirm
queueName: queue.confirm
@Configuration
public class ConfirmConfig {
@Value("${my.exchangeName}")
private String exchangeName;
@Value("${my.queueName}")
private String queueName;
@Bean
public DirectExchange directExchange(){
return ExchangeBuilder.directExchange(exchangeName).build();
}
@Bean
public Queue queue(){
return QueueBuilder.durable(queueName).build();
}
@Bean
public Binding binding(DirectExchange directExchange,Queue queue){
return BindingBuilder.bind(queue).to(directExchange).with("info");
}
}
@Service
public class MessageService {
@Resource
private RabbitTemplate rabbitTemplate;
@PostConstruct //构造方法后执行它,相当于初始化作用
public void init() {
rabbitTemplate.setConfirmCallback(
(correlationData, b, s) -> {
if (!b) {
System.out.println("消息没有进入交换机");
}
});
rabbitTemplate.setReturnCallback(
(message, i, s, s1, s2) -> System.out.println("消息没有正确路由到队列:"+message.toString())
);
}
public void sendMsg() {
Message message = MessageBuilder.withBody("hello world".getBytes()).build();
CorrelationData correlationData = new CorrelationData();//关联数据
correlationData.setId("order_123456"); //发送订单信息
rabbitTemplate.convertAndSend("exchange.confirm1", "info", message, correlationData);
}
}
@SpringBootApplication
public class Application implements ApplicationRunner {
@Resource
private MessageService messageService;
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
messageService.sendMsg();
}
}