一,消息可靠性投递
- confifirm 确认模式
- return 退回模式
- 消息从 producer 到 exchange 则会返回一个 confirmCallback 。
- 消息从 exchange 到 queue 投递失败则会返回一个 returnCallback 。
通过 confirmCallback 判定 消息是否成功到达exchange
因为returnCallback 是 启动消息失败返回,比如路由不到队列时触发回调
我们将利用这两个 callback 控制消息的可靠性投递
1,confirm确认模式代码实现
代码实现:
1,创建maven 工程,消息的生产者工程,项目模块名称:rabbitmq-producer-spring
2,添加依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring‐context</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring‐rabbit</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring‐test</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven‐compiler‐plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
3,在 resources 目录下创建 rabbitmq.properties 配置文件,添加链接RabbitMQ相关信息
rabbitmq.host=172.16.98.133
rabbitmq.port=5672
rabbitmq.username=guest
rabbitmq.password=guest
rabbitmq.virtual‐host=/
<?xml version="1.0" encoding="UTF‐8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema‐instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:rabbit="http://www.springframework.org/schema/rabbit" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring‐beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring‐context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring‐rabbit.xsd">
<!‐‐加载配置文件‐‐>
<context:property‐placeholder location="classpath:rabbitmq.properties"/>
<!‐‐ 定义rabbitmq connectionFactory 1. 设置 publisher‐confirms="true" ‐‐> <rabbit:connection‐factory id="connectionFactory"
host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual‐host="${rabbitmq.virtual‐host}"
publisher‐confirms="true" />
<!‐‐定义管理交换机、队列‐‐>
<rabbit:admin connection‐factory="connectionFactory"/>
<!‐‐定义rabbitTemplate对象操作可以在代码中方便发送消息‐‐>
<rabbit:template id="rabbitTemplate" connection‐factory="connectionFactory"/>
<!‐‐2. 消息可靠性投递(生产端)‐‐>
<rabbit:queue id="test_queue_confirm" name="test_queue_confirm"></rabbit:queue>
<rabbit:direct‐exchange name="test_exchange_confirm">
<rabbit:bindings>
<rabbit:binding queue="test_queue_confirm" key="confirm">
</rabbit:binding>
</rabbit:bindings>
</rabbit:direct‐exchange>
</beans>
或者配置yml 文件
5, 编写测试代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring‐rabbitmq‐producer.xml")
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 确认模式:
* 步骤:
* 1. 确认模式开启:ConnectionFactory中开启publisher‐confirms="true"
* 2. 在rabbitTemplate定义ConfirmCallBack回调函数
*/
@Test
public void testConfirm() {
//2. 定义回调 **
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/***
* @param correlationData 相关配置信息
* @param ack exchange交换机 是否成功收到了消息。true 成功,false代表失败
* @param cause 失败原因
*/
@Override public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm方法被执行了....");
if (ack) {
//接收成功
System.out.println("接收成功消息" + cause);
} else {
//接收失败
System.out.println("接收失败消息" + cause);
//做一些处理,让消息再次发送。
}
}
});
//3. 发送消息
rabbitTemplate.convertAndSend("test_exchange_confirm111", "confirm", "message confirm....");
}
}
或
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class confirmTest4 implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 确认模式:
* 步骤:
* 1,确认模式开启:在yml 配置文件里配置 publisher-confirms: true
* 2, 在rabbitTemplate 定义ConfirmCallBack 回调函数
*/
@Test
public void testConfirms() {
rabbitTemplate.setConfirmCallback(new confirmTest4());
Map<String, String> msg = new HashMap<>();
msg.put("pageId", "5abefd525b05aa293098fca6");
//转成json串
String jsonString = JSON.toJSONString(msg);
rabbitTemplate.convertAndSend("ex_routing_cms_postpage", "5abefd525b05aa293098fca6", jsonString);
}
/**
*
* @param correlationData 相关配置信息
* @param ack exchange交换机 是否成功收到了消息,true成功,false代表失败
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm 方法开始执行..............");
if (ack) {
//接收成功
System.out.println("接收成功的消息:" + cause);
} else {
//接收失败
System.out.println("接收失败消息:" + cause);
//接收失败后,我们做一些处理,让消息再次发送,达到消息可靠性传递
}
}
其中重写confirm 方法里的参数,
correlationData:消息唯一标识
ack:确认结果
cause:失败原因
这是发送消息到queue 成功后,展示的内容,
但是如果发送失败是这样的
当发送失败是可以配置return 退回模式代码。
2, return退回模式代码实现
回退模式: 当消息发送给EXchange 后,Exchange 路由到Queue 失败是才会执行ReturnCallBack,具体实现如下:
1,在Spring-rabbitmq-producer.xml 配置文件,在rabbit:connection-factory 节点添加配置
publisher‐returns="true"
或者 在application.yml 中设置
设置交换机处理失败消息的模式,两种方法,一种写在代码里,一种配置文件
1, 配置文件
2,代码方式:
/**
* 步骤:
* 1. 开启回退模式:publisher‐returns="true"
* 2. 设置ReturnCallBack
* 3. 设置Exchange处理消息的模式:
* 1. 如果消息没有路由到Queue,则丢弃消息(默认)
* 2. 如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
*/
@Test
public void testReturn() {
//设置交换机处理失败消息的模式
rabbitTemplate.setMandatory(true);
//2.设置ReturnCallBack
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
*
* @param message 消息对象
* @param replyCode 错误码
* @param replyText 错误信息
* @param exchange 交换机
* @param routingKey 路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText,
String exchange, String routingKey) {
System.out.println("return 执行了....");
System.out.println(message);
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(exchange);
System.out.println(routingKey);
//处理
}
});
//3. 发送消息
rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message
confirm....");
}
或
package com.xuecheng.manage_cms.returns;
import com.alibaba.fastjson.JSON;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.HashMap;
import java.util.Map;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ReturnTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testReturn() {
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
*
* @param message 消息对象
* @param replyCode 错误码
* @param replyText 错误信息
* @param exchange 交换机
* @param routingKey 路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("return 执行了..........");
System.out.println("message = " + message);
System.out.println("replyCode = " + replyCode);
System.out.println("replyText = " + replyText);
System.out.println("exchange = " + exchange);
System.out.println("routingKey = " + routingKey);
}
});
Map<String, String> msg = new HashMap<>();
msg.put("pageId", "5abefd525b05aa293098fca6");
//转成json串
String jsonString = JSON.toJSONString(msg);
rabbitTemplate.convertAndSend("ex_routing_cms_postpage", "5abefd525b05aa293098fca61", jsonString);
}
}
设置routingKey为一个不符合规则的key,观察控制台打印结果。
returnedMessage方法中参数
消息主体message : message
消息主体 message : replyCode
描述:replyText
消息使用的交换器 exchange : exchange
消息使用的路由键 routing : routingKey
总结
对于确认模式
设置 ConnectionFactory 的 publisher-confifirms="true" 开启 确认模式。使用 rabbitTemplate.setConfifirmCallback 设置回调函数。当消息发送到 exchange 后回调 confiirm方法。在方法中判断 ack ,如果为 true ,则发送成功,如果为 false ,则发送失败,需要处理。
设置 ConnectionFactory 的 publisher-returns="true" 开启 退回模式。使用 rabbitTemplate.setReturnCallback 设置退回函数,当消息从 exchange 路由到 queue 失败后,如果设置了 rabbitTemplate.setMandatory(true) 参数,则会将消息退回给 producer 。并执行回调函数returnedMessage 。
在 RabbitMQ 中也提供了事务机制,但是性能较差,此处不做讲解。使用 channel 列方法,完成事务控制:txSelect(), 用于将当前 channel 设置成 transaction 模式txCommit() ,用于提交事务txRollback(), 用于回滚事务
二,Consumer ACK(消息接收确认)
消息消费者如何通知 Rabbit 消息消费成功?
ack指 Acknowledge,确认。 表示消费端收到消息后的确认方式。
代码实现
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring‐context</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring‐rabbit</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring‐test</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven‐compiler‐plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
rabbitmq.host = 172.16.98.133rabbitmq.port = 5672rabbitmq.username = guestrabbitmq.password = guestrabbitmq.virtual‐host = /
<?xml version="1.0" encoding="UTF‐8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema‐instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring‐beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring‐context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring‐rabbit.xsd">
<!‐‐加载配置文件‐‐>
<context:property‐placeholder location="classpath:rabbitmq.properties"/>
<!‐‐ 定义rabbitmq connectionFactory ‐‐>
<rabbit:connection‐factory id="connectionFactory"
host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual‐host="${rabbitmq.virtual‐host}"/>
<context:component‐scan base‐package="com.itheima.listener" />
<!‐‐定义监听器容器 添加 acknowledge="manual" 手动‐‐>
<rabbit:listener‐container connection‐factory="connectionFactory" acknowledge="manual"
>
<rabbit:listener ref="ackListener" queue‐names="test_queue_confirm">
</rabbit:listener>
</rabbit:listener‐container>
</beans>
/**
* Consumer ACK机制:
* 1. 设置手动签收。acknowledge="manual"
* 2. 让监听器类实现ChannelAwareMessageListener接口
* 3. 如果消息成功处理,则调用channel的 basicAck()签收
* 4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
*/
@Component
public class ackListener implements ChannelAwareMessageListener {
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = "ex_routing_cms_postpage",type = "direct"),
value = @Queue(value = "queue_cms_postpage_01",durable = "true"),
key = "#.#"
))
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//1, 接收转换消息
System.out.println("接收消息内容:"+new String(message.getBody()));
//2, 处理业务逻辑
System.out.println("处理业务逻辑");
//int i = 2/0 ; // 代码运行错误
//3,手动签收
channel.basicAck(deliveryTag,true);
}catch (Exception e){
e.printStackTrace();
System.out.println("代码执行错误了。。。。。。");
//4,代码出现错误后,设置拒绝签收,重新发送
//第三个参数:requeue: 消息重回队列,如何设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
channel.basicNack(deliveryTag,true,true);
//了解
//channel.basicReject(deliveryTag,true);
}
}
}
exchange = @Exchange(value = "ex_routing_cms_postpage",type = "direct"),
value = @Queue(value = "queue_cms_postpage_01",durable = "true"),
key = "#.#"
))
小结:
- 在rabbit:listener-container标签中设置acknowledge属性,设置ack方式 none:自动确认,manual:手动确认
- 如果在消费端没有出现异常,则调用channel.basicAck(deliveryTag,false);方法确认签收消息
- 如果出现异常,则在catch中调用 basicNack或 basicReject,拒绝消息,让MQ重新发送消息。
如何保证消息的高可靠性传输?1. 持久化• exchange 要持久化• queue 要持久化• message 要持久化2. 生产方确认 Confifirm3. 消费方确认 Ack4.Broker 高可用
三,消费端限流
代码实现:
1, 编写一个监听类,保证当前的监听类的消息处理机制是ACK(手动方式)
@Component
public class QosListener implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
Thread.sleep(1000);
//1.获取消息
System.out.println(new String(message.getBody()));
//2. 处理业务逻辑
//3. 签收
channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
}
}
2,在配置文件的listener-container 配置属性中添加配置(或者用@Confirguration)
配置说明:perfetch = 1, 表示消费端每次从 mq拉去一条消息来消费,直到手动确认消费完毕后,才会继续拉去下 一条消息。
/**
* Consumer ACK机制:
* 1. 设置手动签收。acknowledge="manual"
* 2. 让监听器类实现ChannelAwareMessageListener接口
* 3. 如果消息成功处理,则调用channel的 basicAck()签收
* 4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
*/
@Component
public class ackListener implements ChannelAwareMessageListener {
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = "ex_routing_cms_postpage",type = "direct"),
value = @Queue(value = "queue_cms_postpage_01",durable = "true"),
key = "#.#"
))
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
System.out.println("一个进来了");
Thread.sleep(10000);
System.out.println("一个结束了");
channel.basicAck(deliveryTag,true);
}catch (Exception e){
e.printStackTrace();
}
}
}
总结:
四,TLL(TIME TO LIVE)
有两种设置过期方式:
1,设置队列过期
2,设置消息过期
存活时间/过期时间: 当消息到达存活时间后,还没有被消费,会被自动清除.
RabbitMQ 可以对消息设置过期时间,也可以对整个队列(Queue) 设置过期时间.
代码实现:
1,设置队列的过期时间:
- 在消息的生产方,在spring-rabbitmq-producer.xml配置文件中,添加如下配置
<!‐‐ttl‐‐>
<rabbit:queue name="test_queue_ttl" id="test_queue_ttl">
<!‐‐设置queue的参数‐‐>
<rabbit:queue‐arguments>
<!‐‐x‐message‐ttl指队列的过期时间‐‐>
<entry key="x‐message‐ttl" value="100000" value‐type="java.lang.Integer"/>
</rabbit:queue‐arguments>
</rabbit:queue>
<rabbit:topic‐exchange name="test_exchange_ttl" >
<rabbit:bindings>
<rabbit:binding pattern="ttl.#" queue="test_queue_ttl"></rabbit:binding>
</rabbit:bindings>
</rabbit:topic‐exchange>
- 或设置 RabbitMqConfig.java
//声明队列
@Bean(QUEUE_CMS_POST)
public Queue QUEUE_CMS_POST(){
Map<String, Object> arguments = new HashMap<String, Object>();
arguments.put("x-message-ttl", 5000);
return QueueBuilder.durable(queue_cms_postpage_name).withArgument("x-message-ttl",5000).build();
}
交换机需要配置topic 模式
/**
* 交换机配置使用topic 类型
*/
@Bean(EX_ROUTING_CMS_POST)
public Exchange EX_ROUTING_CMS_POST(){
return ExchangeBuilder.topicExchange(EX_ROUTING_CMS_POST).durable(true).build();
}
设置生效后,rabbitMq管理页面会出现TTL 图标
这里经常出现 rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(repl 问题,好好排查,能解决掉
https://mp.csdn.net/mp_blog/creation/editor/119564708
2 编写发送消息测试方法
@Test
public void testTtl() {
for (int i = 0; i < 10; i++) {
// 发送消息
rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....");
}
}
设置单个消息的过期时间
编写代码测试,并且设置队列的过期时间为100s,单个消息的过期时间为5s,
@Test
public void testTtl() {
// 消息后处理对象,设置一些消息的参数信息
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//1.设置message的信息
message.getMessageProperties().setExpiration("5000");
//消息的过期时间
//2.返回该消息
return message;
}
};
//消息单独过期
rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....",messagePostProcessor);
for (int i = 0; i < 10; i++) {
if(i == 5){
//消息单独过期
rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....",messagePostProcessor);
}else{
//不过期的消息
rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....");
}
}
}
消息过期就是在发送消息后面加个参数, MessagePostProcessor 类, 这个类里配置过期时间,那么这条消息过期时间就不一样
如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准。队列过期后,会将队列所有消息全部移除。消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉 )
总结:
五,死信队列
死信队列:没有被及时消费的消息存放的队列
消息成为死信的几种情况:
- 消费者拒收消息(basic.reject/ basic.nack) ,并且没有重新入队 requeue=false
- 消息在队列中未被消费,且超过队列或者消息本身的过期时间TTL(time-to-live)
- 队列的消息长度达到极限
- 结果:消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
队列绑定死信交换机步骤:
1,新建死信交换机(和普通交换机一样)
2,新建死信队列(和普通队列一样,名称:dead_queue)
3,死信交换机和死信队列绑定
4,创建一个新的队列(名称:product_qeueu),设置过期时间,绑定死信交换机
测试:直接web控制台往product_qeueu发送消息即可,会看到消息先是在product_qeueu队列停留10秒(因为没有消费者消费),然后该消息从product_qeueu移入到dead_queue。
代码实现:
1,在消息的生产方中,在application.yml 中 配置
声明正常的队列(producer_queue)和交换机(producer_exchange)
package com.tdrc.common.core.rabbitmq;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author dpf
* @version 1.0
* @date 2020-6-22 9:52
* @instruction ...
*/
@Configuration
public class RabbitExChangeConfig {
/**
* 业务交换机
*/
public static final String DESTINATION_NAME = "rabbitMq_direct";
/**
* 业务队列名称
*/
public static final String SMS_QUEUE = "Sms_msg";
/**
* 死信队列交换机名称
*/
public static final String DEAD_LETTER_EXCHANGE_NAME="deadLetter_direct";
/**
* 死信队列名称
*/
public static final String DEAD_LETTER_QUEUE = "deadLetter_queue";
/**
* RouteKey
*/
public static final String SMS_ROUTING_KEY = "sms";
/**
* 配置死信交换机
* @return
*/
@Bean
public DirectExchange deadLetterDirectExchange(){
return new DirectExchange(DEAD_LETTER_EXCHANGE_NAME);
}
/**
* 配置死信队列
* @return
*/
@Bean
public Queue deadLetterQueue(){
return new Queue(DEAD_LETTER_QUEUE);
}
/**
* 绑定死信队列和死信交换机
* @return
*/
@Bean
Binding deadLetterBindingDirect() {
return BindingBuilder.bind(deadLetterQueue()).to(deadLetterDirectExchange()).with(SMS_ROUTING_KEY);
}
/**
* 配置队列
* @return
*/
@Bean
public Queue smsDirectQueue() {
Map<String, Object> args = new HashMap<>(16);
// 队列消息过期时间
args.put("x-message-ttl", 10000);
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE_NAME);
args.put("x-dead-letter-routing-key", SMS_ROUTING_KEY);
// args.put("x-expires", 5000);队列过期时间
// args.put("x-max-length",5 );
return new Queue(SMS_QUEUE, true,false,false,args);
}
/**
* 配置交换机
* @return
*/
@Bean
public DirectExchange directExchange() {
return new DirectExchange(DESTINATION_NAME);
}
/**
* 交换机与队列绑定
* @return
*/
@Bean
Binding smsBindingDirect() {
return BindingBuilder.bind(smsDirectQueue()).to(directExchange()).with(SMS_ROUTING_KEY);
}
@Bean
public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory =
new SimpleRabbitListenerContainerFactory();
//这个connectionFactory就是我们自己配置的连接工厂直接注入进来
simpleRabbitListenerContainerFactory.setConnectionFactory(connectionFactory);
//这边设置消息确认方式由自动确认变为手动确认
simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
//设置消息预取数量
// simpleRabbitListenerContainerFactory.setPrefetchCount(1);
return simpleRabbitListenerContainerFactory;
}
/**
* 每个rabbitTemplate方法只可以有一个回调,不然会报错 only one ConfirmCallback is supported by each RabbitTemplate,解决办法是配成多利的
*
* @param connectionFactory
* @return
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
//成功回调
template.setConfirmCallback(new Callback());
// 开启mandatory模式(开启失败回调)
template.setMandatory(true);
//失败回调
template.setReturnCallback(new Callback());
return template;
}
}
编写给业务交换机发短信的controller
@Resource
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendSms")
private void sendSms() throws InterruptedException {
String msg = "HelloWorld rabbitmq";
for(Integer i=0;i<10;i++){
CorrelationData correlationData = new CorrelationData(i.toString());
rabbitTemplate.convertAndSend(RabbitExChangeConfig.DESTINATION_NAME, RabbitExChangeConfig.SMS_ROUTING_KEY, msg+i ,correlationData);
}
}
启动程序,用postman调用发送信息接口
测试结果:
1,启动程序前消息对列中无程序内创建的业务队列和死信队列。(还没有创建交换机和队列)
2,程序启动后出现业务交换机和死信交换机,业务队列和死信队列。
3,业务交换机有发送过去的消息,私信交换机现在没有
4,等设置的ttl 过期时间过后,死信队列中有了发送的消息,业务队列中没有了(因为过了消费时间并且一直没有消费)
程序中添加监听死信队列监听代码,看的会更详细
@RabbitListener(queues = RabbitExChangeConfig.DEAD_LETTER_QUEUE, containerFactory = "simpleRabbitListenerContainerFactory")
public void reciveDeadLetter(Message message, Channel channel, @Headers Map<String, Object> headers) throws IOException {
long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
try {
System.out.println("死信队列消费者收到消息 : " + new String(message.getBody(), "UTF-8"));
/**
* 手动ack
* deliveryTag:该消息的index
* multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
*/
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
//消息退回 (可以在可视化界面看到)
//批量退回 退回之后重回消息队列 true false的话就是丢弃这条信息,如果配置了死信队列,那这条消息会进入死信队列
channel.basicNack(deliveryTag, false, true);
//单条退回 channel.basicReject();
}
}
实际环境我们还需要对死信队列进行一个监听和处理,当然具体的处理逻辑和业务相关,这里只是简单演示死信队列是否生效。
六,延迟队列
1,什么是延迟队列
⼀种带有延迟功能的消息队列, Producer 将消息发送到消息队列服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某⼀个时间投递到Consumer进行消费,该消息即定时消息。
2,应用场景
- 通过消息触发⼀些定时任务,比如在某⼀固定时间点向用户发送提醒消息
- 用户登录之后5分钟给用户做分类推送、用户多少天未登录给用户做召回推送;
- 消息生产和消费有时间窗⼝要求:比如在天猫电商交易中超时未支付关闭订单的场景,在订单创建时会发送⼀条延时消息。这条消息将会在30分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略
3,实战
需求
JD、淘系、天猫、拼多多电商平台,规定新注册的商家,审核通过后需要在【规定时间】内上架商品,否则冻结账号。
代码实现:
1,定时器
2,延迟队列
1,在消息的生产方,rabbitmqConfig 中,添加如下配置:
定义正常交换机和队列并绑定,定义死信交换机和队列,并绑定
package com.tdrc.common.core.rabbitmq;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author dpf
* @version 1.0
* @date 2020-6-22 9:52
* @instruction ...
*/
@Configuration
public class RabbitExChangeConfig {
/**
* 业务交换机
*/
public static final String DESTINATION_NAME = "rabbitMq_direct";
/**
* 业务队列名称
*/
public static final String SMS_QUEUE = "Sms_msg";
/**
* 死信队列交换机名称
*/
public static final String DEAD_LETTER_EXCHANGE_NAME="deadLetter_direct";
/**
* 死信队列名称
*/
public static final String DEAD_LETTER_QUEUE = "deadLetter_queue";
/**
* RouteKey
*/
public static final String SMS_ROUTING_KEY = "sms";
/**
* 配置死信交换机
* @return
*/
@Bean
public DirectExchange deadLetterDirectExchange(){
return new DirectExchange(DEAD_LETTER_EXCHANGE_NAME);
}
/**
* 配置死信队列
* @return
*/
@Bean
public Queue deadLetterQueue(){
return new Queue(DEAD_LETTER_QUEUE);
}
/**
* 绑定死信队列和死信交换机
* @return
*/
@Bean
Binding deadLetterBindingDirect() {
return BindingBuilder.bind(deadLetterQueue()).to(deadLetterDirectExchange()).with(SMS_ROUTING_KEY);
}
/**
* 配置队列
* @return
*/
@Bean
public Queue smsDirectQueue() {
Map<String, Object> args = new HashMap<>(16);
// 队列消息过期时间
args.put("x-message-ttl", 10000);
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE_NAME);
args.put("x-dead-letter-routing-key", SMS_ROUTING_KEY);
// args.put("x-expires", 5000);队列过期时间
// args.put("x-max-length",5 );
return new Queue(SMS_QUEUE, true,false,false,args);
}
/**
* 配置交换机
* @return
*/
@Bean
public DirectExchange directExchange() {
return new DirectExchange(DESTINATION_NAME);
}
/**
* 交换机与队列绑定
* @return
*/
@Bean
Binding smsBindingDirect() {
return BindingBuilder.bind(smsDirectQueue()).to(directExchange()).with(SMS_ROUTING_KEY);
}
@Bean
public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory =
new SimpleRabbitListenerContainerFactory();
//这个connectionFactory就是我们自己配置的连接工厂直接注入进来
simpleRabbitListenerContainerFactory.setConnectionFactory(connectionFactory);
//这边设置消息确认方式由自动确认变为手动确认
simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
//设置消息预取数量
// simpleRabbitListenerContainerFactory.setPrefetchCount(1);
return simpleRabbitListenerContainerFactory;
}
/**
* 每个rabbitTemplate方法只可以有一个回调,不然会报错 only one ConfirmCallback is supported by each RabbitTemplate,解决办法是配成多利的
*
* @param connectionFactory
* @return
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
//成功回调
template.setConfirmCallback(new Callback());
// 开启mandatory模式(开启失败回调)
template.setMandatory(true);
//失败回调
template.setReturnCallback(new Callback());
return template;
}
}
@RestController
@RequestMapping("/api/admin/merchant")
public class MerchantAccountController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("check")
public Object check(){
//修改数据库的商家账号状态 TODO
rabbitTemplate.convertAndSend(RabbitMQConfig.NEW_MERCHANT_EXCHANGE,RabbitMQConfig.NEW_MERCHANT_ROUTIING_KEY,"商家账号通过审核");
Map<String,Object> map = new HashMap<>();
map.put("code",0);
map.put("msg","账号审核通过,请10秒内上传1个商品");
return map;
}
}
Listener死信队列消费
package net.xdclass.xdclasssp.mq;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@RabbitListener(queues = "lock_merchant_dead_queue")
public class MerchantMQListener {
@RabbitHandler
public void messageHandler(String body, Message message, Channel channel) throws IOException {
long msgTag = message.getMessageProperties().getDeliveryTag();
System.out.println("msgTag="+msgTag);
System.out.println("body="+body);
//做复杂业务逻辑 TODO
//告诉broker,消息已经被确认
channel.basicAck(msgTag,false);
}
}
七,日志与监控
RabbitMQ 日志
RabbitMQ默认日志存放路径: /var/log/rabbitmq/rabbit@xxx.log
RabbitMQ日志详细信息:
web管控台监控:
- 查看队列:rabbitmqctl list_queues
- 对应管理控制台的页面如下:
- 查看用户: rabbitmqctl list_users
查看连接:rabbitmqctl list_connections
八,消息追踪
为什么要消息追踪
怎么使用
在RabbitMQ中可以使用Firehose和rabbitmq_tracing插件功能来实现消息追踪。
消息追踪-Firehose
2,未开启消息追踪之前,我们发送一个消息
2, RabbitMq 应用问题
消息可靠性保障
首先大家要明确任何一个系统都不能保证消息的百分百投递成功,我们是可以保证消息以最高最可靠的发送给目标的
在RabbitMQ中采用 消息补充机制 来保证消息的可靠性
步骤分析:参与部分:消息生产者、消息消费者、数据库、三个队列( Q1 、 Q2 、 Q3 )、交换机、回调检查服务、定时检查服务1. 消息的生产者将业务数据存到数据库中2. 发送消息给 队列 Q13. 消息的生产者等待一定的时间后,在发送一个延迟消息给队列 Q34. 消息的消费方监听 Q1 队列消息,成功接收后5. 消息的消费方会 发送 一条确认消息给 队列 Q26. 回调检查服务监听 队列 Q2 发送的确认消息7. 回调检查服务接收到确认消息后,将消息写入到 消息的数据库表中8. 回调检查服务同时也会监听 队列 Q3 延迟消息, 如果接收到消息会和数据库比对消息的唯一标识9. 如果发现没有接收到确认消息,那么回调检查服务就会远程调用 消息生产者,重新发送消息10. 重新执行 2-7 步骤,保证消息的可靠性传输11. 如果发送消息和延迟消息都出现异常,定时检查服务会监控 消息库中的消息数据,如果发现不一致的消息然后远程调用消息的生产者重新发送消息。
消息可靠性保障,就是说需要一系列的措施来保障rabbitmq 消息不出错,
额外参考:https://www.cnblogs.com/linjiqin/p/12683076.html
2,消息幂等性处理
自动补偿机制
如果消费端接收消息时,消费端消费不成功的话,
@Component
public class Consumer {
public static final String QUEUE_NAME = "byte-zb";
@RabbitListener(queues = QUEUE_NAME)
public void receiveMessage(String message) throws Exception {
System.out.println("接收到的消息为"+message);
int i = 1 / 0;
}
}
我们会看到消费者工程控制台一直在刷新报错,当消费者配出异常,也就是说当消息消费不成功的话,该消息会存放在rabbitmq的服务端,一直进行重试,直到不抛出异常为止。
如果一直抛异常,我们的服务很容易挂掉,那有没有办法控制重试几次不成功就不再重试了呢?答案是有的。我们在消费者application.yml中增加一段配置。
spring:
rabbitmq:
# 连接地址
host: 127.0.0.1
# 端口
port: 5672
# 登录账号
username: guest
# 登录密码
password: guest
# 虚拟主机
virtual-host: /
listener:
simple:
retry:
enabled: true # 开启消费者进行重试
max-attempts: 5 # 最大重试次数
initial-interval: 3000 # 重试时间间隔
上面配置的意思是消费异常后,重试五次,每次隔3s。继续启动消费者看看效果,我们发现重试五次以后,就不再重试了。
结合实践案例来使用消息补偿机制
解决消息幂等性问题
一些刚接触java的同学可能对幂等性不太清楚。幂等性就是重复消费造成结果不一致。为了保证幂等性,因此消费者消费消息只能消费一次消息。我么可以是用全局的消息id来控制幂等性。当消息被消费了之后我们可以选择缓存保存这个消息id,然后当再次消费的时候,我们可以查询缓存,如果存在这个消息id,我们就不错处理直接return即可。先改造生产者代码,在消息中添加消息id:
@RequestMapping("/send")
public void sendMessage(){
JSONObject jsonObject = new JSONObject();
jsonObject.put("email","11111111111");
jsonObject.put("timestamp",System.currentTimeMillis());
String json = jsonObject.toJSONString();
System.out.println(json);
Message message = MessageBuilder.withBody(json.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("UTF-8").setMessageId(UUID.randomUUID()+"").build();
amqpTemplate.convertAndSend(EXCHANGE_NAME,QUEUE_NAME,message);
}
消费者代码改造:
@Component
public class Consumer {
public static final String QUEUE_NAME = "byte-zb";
@RabbitListener(queues = QUEUE_NAME)
public void receiveMessage(Message message) throws Exception {
Jedis jedis = new Jedis("localhost", 6379);
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(),"UTF-8");
System.out.println("接收导的消息为:"+msg+"==消息id为:"+messageId);
String messageIdRedis = jedis.get("messageId");
if(messageId == messageIdRedis){
return;
}
JSONObject jsonObject = JSONObject.parseObject(msg);
String email = jsonObject.getString("email");
String content = jsonObject.getString("timestamp");
String httpUrl = "http://127.0.0.1:8080/email?email"+email+"&content="+content;
// 如果发生异常则返回null
String body = HttpUtils.httpGet(httpUrl, "utf-8");
//
if(body == null){
throw new Exception();
}
jedis.set("messageId",messageId);
}
}
我们在消费者端使用redis存储消息id,只做演示,具体项目请根据实际情况选择相应的工具进行存储。