Confirm测试
测试确认机制一定要开启publisher-confirms="true"和publisher-returns=“true”,默认是关闭的,即默认不会回调 。
1、配置
rabbitmq.properties配置文件
rabbitmq.host=192.168.131.171
rabbitmq.port=5672
rabbitmq.username=jihu
rabbitmq.password=jihu
rabbitmq.virtual-host=/jihu
spring-rabbitmq-producer.xml配置文件
<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"/>
<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"
publisher-returns="true"/>
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
<rabbit:queue id="test_queue_confirm" name="test_queue_confirm"/>
<rabbit:direct-exchange name="test_exchange_confirm">
<rabbit:bindings>
<rabbit:binding queue="test_queue_confirm" key="confirm"/>
rabbit:bindings>
rabbit:direct-exchange>
beans>
2、测试代码
@Test
public void testConfirm() throws InterruptedException {
final CountDownLatch countDownLatch = new CountDownLatch(1);
//定义回调
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方法被执行了....");
//ack 为 true表示 消息已经到达交换机
if (ack) {
//接收成功
System.out.println("接收成功消息" + cause);
} else {
//接收失败
System.out.println("接收失败消息" + cause);
//做一些处理,让消息再次发送。
}
}
});
rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message confirm ...");
// 不要让程序结束,否则可能无法来得及执行回调
countDownLatch.await();
}
此时,我们关闭“
@Test
public void testConfirmSendFailed() throws InterruptedException {
final CountDownLatch countDownLatch = new CountDownLatch(1);
//定义回调
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方法被执行了....");
//ack 为 true表示 消息已经到达交换机
if (ack) {
//接收成功
System.out.println("接收成功消息" + cause);
} else {
//接收失败
System.out.println("接收失败消息" + cause);
//做一些处理,让消息再次发送。
}
}
});
// 设置一个不存在的交换机的名字
rabbitTemplate.convertAndSend("test_exchange_confirmxxxxxxx", "confirm", "message confirm ...");
// 不要让程序结束,否则可能无法来得及执行回调
countDownLatch.await();
}
Return测试
注意,生产者要想获取到发送队列失败的消息,必须开启:
rabbitTemplate.setMandatory(true);
开启之后才会将发送到队列失败的消息退还给生产者,否则即使发送失败也不会退还!
测试代码
@Test
public void testReturn() throws InterruptedException {
final CountDownLatch countDownLatch = new CountDownLatch(1);
// 设置交换机处理失败消息的模式.设置为true的时候,会将将发送到队列的失败消息返回给生产者。默认为false,不返回
rabbitTemplate.setMandatory(true);
// 定义回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
*
* @param message 消息对象
* @param replyCode 错误码
* @param replyText 错误信息
* @param exchange 交换机
* @param routingKey 路由key
*/
@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);
//处理
}
});
// 交换机名称正确,设置一个错误的路由key来模拟消息发送到队列失败的场景
rabbitTemplate.convertAndSend("test_exchange_confirm", "confirmXXX", "message confirm ...");
// 不要让程序结束,否则可能无法来得及执行回调
countDownLatch.await();
}
<?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}"/>
<bean id="ackListener" class="com.jihu.rabbitmq.listener.AckListener"/>
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" prefetch="1">
<rabbit:listener ref="ackListener" queue-names="test_queue_confirm"/>
</rabbit:listener-container>
</beans>
2、监听类
@Component
public class AckListener implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
// 获取消息的id
long deliveryTag = message.getMessageProperties().getDeliveryTag();
// 获取消息内容
System.out.println("message" + new String(message.getBody()));
// 消息消费,业务处理
System.out.println("===== 业务处理 ======");
// 业务正常执行完成后,完成签收. true代表签收多条消息
channel.basicAck(deliveryTag, true);
}
@Override
public void onMessage(Message message) {}
}
启动类,初始化spring容器:
public class Test {
public static void main(String[] args) {
// 初始化spring容器
ClassPathXmlApplicationContext applicationContext =
new ClassPathXmlApplicationContext("classpath:spring-rabbitmq-consumer.xml");
}
}
随后我们来启动这个启动类。
然后我们再使用生产者发送消息:
@Test
public void testReturn() throws InterruptedException {
final CountDownLatch countDownLatch = new CountDownLatch(1);
// 设置交换机处理失败消息的模式.设置为true的时候,会将将发送到队列的失败消息返回给生产者。默认为false,不返回
rabbitTemplate.setMandatory(true);
// 定义回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
*
* @param message 消息对象
* @param replyCode 错误码
* @param replyText 错误信息
* @param exchange 交换机
* @param routingKey 路由key
*/
@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);
//处理
}
});
// 交换机名称正确,设置一个错误的路由key来模拟消息发送到队列失败的场景
rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message confirm ...");
// 不要让程序结束,否则可能无法来得及执行回调
countDownLatch.await();
}
可以看到,消费端成功接收到消息后,处理了业务,在没有异常的情况下进行了确认消费。
@Component
public class AckListener implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
// 获取消息的id
long deliveryTag = message.getMessageProperties().getDeliveryTag();
// 获取消息内容
System.out.println("message" + new String(message.getBody()));
try {
// 消息消费,业务处理
System.out.println("===== 业务处理 ======");
int error_cause = 5 / 0;
// 业务正常执行完成后,完成签收. true代表签收多条消息
channel.basicAck(deliveryTag, true);
} catch (Exception e) {
System.out.println("业务处理异常!!!");
// 拒绝签收
// 参数3:设置为true,则将消息重新添加到队列中去,broker会重新发送该消息给消费者
// 设置为false, 会丢弃该消息
channel.basicNack(deliveryTag, true, true);
}
}
@Override
public void onMessage(Message message) {}
}
我们此时设置的是处理消息的时候如果发生异常,将消息放到队列中去,重新发送给消费者。但是这样处理很明显不对,因为错误一直存在,导致消息一直被重新发送!!!
比较理想的状态是,我们可以尝试着重新发送一定的次数,比如3次或者5次。如果超过固定次数后仍然无法被消费,考虑将该条消息写入到redis进行持久化处理或者加入到log文件中去。
即,当我们系统的请求瞬间增多的时候,可以将请求发送到MQ中,然后我们再根据性能和需求从MQ中消费消息。
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" prefetch="1">
<rabbit:listener ref="ackListener" queue-names="test_queue_confirm"/>
rabbit:listener-container>
接下来我们模拟一下,生产者同时发送10条消息,消费端逐条消费的场景:
交换机队列配置
<bean id="qosListener" class="com.jihu.rabbitmq.listener.QosListener"/>
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" prefetch="1">
<rabbit:listener ref="qosListener" queue-names="test_queue_confirm"/>
rabbit:listener-container>
生产者代码
/**
* 批量放消息,让消费者每次拉取指定的数量
*/
@Test
public void testQos() {
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message confirm " + i + " ...");
}
}
启动该测试方法,然后我们查看rabbitmq页面中这个队列的消息:
之前存在1条,加上现在生产的10条,总共11条消息。
消费者代码:
因为此时是手动签收,如果手动不签收的话,就应该不会去拉取新的消息来进行消费。我们测试一下不签收消息,看看会怎么样:
@Component
public class QosListener implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
System.out.println("收到的消息: " + new String(message.getBody()));
System.out.println("=== 业务处理 ===");
// 进行消息的签收
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
}
@Override
public void onMessage(Message message) {}
}
随后我们来启动消费端的spring容器,让监听器运行起来:
public class Test {
public static void main(String[] args) {
// 初始化spring容器
ClassPathXmlApplicationContext applicationContext =
new ClassPathXmlApplicationContext("classpath:spring-rabbitmq-consumer.xml");
}
}
可以看到,此时因为没有手动签收,只显示处理的第一条消息。我们再来看看rabbitmq的页面的队列信息:
可以看到,此时有一条消息的状态是Unacked。就是因为我们没有手动的去ack导致的。而且也没有继续消费其他消息了,因为我们设置的是每一次拉取一条。
那,如果我们修改成每一次拉取2条呢?
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" prefetch="2">
<rabbit:listener ref="qosListener" queue-names="test_queue_confirm"/>
rabbit:listener-container>
@Component
public class QosListener implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
System.out.println("收到的消息: " + new String(message.getBody()));
System.out.println("=== 业务处理 ===");
// 进行消息的签收
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
TimeUnit.SECONDS.sleep(2);
}
@Override
public void onMessage(Message message) {}
}
经过验证,确实是!大家可以自己验证
此时就意味着,队列中的消息如果在5s中内没有被消费,就会被自动清除。
然后我们新建一个交换机来测试:
绑定并设置路由key:
然后我们来使用这个交换机发送消息:
5s后,发现消息被自动清除了:
ttl配置:
<rabbit:queue id="test_queue_ttl" name="test_queue_ttl">
<rabbit:queue-arguments>
<entry key="x-message-ttl" value="10000" 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:bindings>
rabbit:topic-exchange>
消息发送:
@Test
public void testTTL() {
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.test", "message ttl ...");
}
}
10s中后发现数据消失了。
配置文件
<rabbit:queue name="test_queue_dlx" id="test_queue_dlx">
<rabbit:queue-arguments>
<entry key="x-dead-letter-exchange" value="exchange_dlx"/>
<entry key="x-dead-letter-routing-key" value="dlx.test"/>
<entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
<entry key="x-max-length" value="10" value-type="java.lang.Integer"/>
rabbit:queue-arguments>
rabbit:queue>
<rabbit:topic-exchange name="test_exchange_dlx">
<rabbit:bindings>
<rabbit:binding pattern="test.dlx.#" queue="test_queue_dlx"/>
rabbit:bindings>
rabbit:topic-exchange>
<rabbit:queue name="queue_dlx" id="queue_dlx"/>
<rabbit:topic-exchange name="exchange_dlx">
<rabbit:bindings>
<rabbit:binding pattern="dlx.#" queue="queue_dlx"/>
rabbit:bindings>
rabbit:topic-exchange>
其实,死信队列和死信交换机也只是普通的队列和交换机,只不过是正常的队列会在过期时间之后将消息通过路由key转发到死信交换机中,死信交换机又将消息发送到死信队列中去。
生产者测试代码
我们运行此代码,会向正常交换机“test_exchange_dlx”中发送一条消息,然后交换机通过路由key将消息转发给正常队列“test_queue_dlx”。
然后该消息会在10s后过期,此时正常队列“test_queue_dlx”会将过期消息发送到死信交换机“exchange_dlx”中,然后死信交换机又通过路由key将消息发送给死信队列“queue_dlx”。
@Test
public void testDeadLetter() {
rabbitTemplate.convertAndSend("test_exchange_dlx", "test.dlx.haha", "死信消息!!!");
}
可以看到消息被成功发送到正常队列中:
10s后,发现消息被转发到死信队列中了:
此时,我们如果用一个消费者来接收死信队列中的消息,那是不是就实现了延迟队列的功能了呢?类似于下单后,我们将订单消息丢入一个订单队列中去,如果超过30min,则将消息转发到订单的死信队列中去,然后由订单服务消费死信消息,执行取消订单,添加库存等业务操作!!!
其实延迟队列就是我们使用消费者监听死信队列!
生产者配置
<rabbit:queue name="order_queue" id="order_queue">
<rabbit:queue-arguments>
<entry key="x-dead-letter-exchange" value="order_exchange_dlx"/>
<entry key="x-dead-letter-routing-key" value="dlx.order.cancle"/>
<entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
rabbit:queue-arguments>
rabbit:queue>
<rabbit:topic-exchange name="order_exchange">
<rabbit:bindings>
<rabbit:binding pattern="order.#" queue="order_queue"/>
rabbit:bindings>
rabbit:topic-exchange>
<rabbit:queue name="order_queue_dlx" id="order_queue_dlx"/>
<rabbit:topic-exchange name="order_exchange_dlx">
<rabbit:bindings>
<rabbit:binding pattern="dlx.order.#" queue="order_queue_dlx"/>
rabbit:bindings>
rabbit:topic-exchange>
消费者代码
注意,此时要实现延迟队列,我们只需要定义一个消费者去监听死信队列即可实现延时功能。
消费者监听配置:
<bean id="orderCancelListener" class="com.jihu.rabbitmq.listener.OrderCancelListener"/>
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" prefetch="1">
<rabbit:listener ref="orderCancelListener" queue-names="order_queue_dlx"/>
rabbit:listener-container>
监听类,业务处理:
public class OrderCancelListener implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
// 消息传递tagId
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
// 1、接收转换消息
System.out.println("message: " + new String(message.getBody()));
// 2、处理业务逻辑
System.out.println("处理业务逻辑...");
System.out.println("根据订单id查询其状态...");
System.out.println("判断状态是否为支付成功...");
System.out.println("取消订单,回滚库存...");
// 3、手动签收
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
System.out.println("取消订单业务处理异常!!! " + e);
// 拒绝签收
// 参数三:将处理失败的消息重新放入到队列
channel.basicNack(deliveryTag, false, true);
}
}
@Override
public void onMessage(Message message) {
}
}
生产者代码:
@Test
public void testDelay() throws InterruptedException {
// 1、发送订单消息,将来是在订单系统中,下单成功后发送消息
rabbitTemplate.convertAndSend("order_exchange", "order.msg", "订单消息:id=10086, user:jihu, phone:18888888888...");
// 2、打印倒计时10s(10s后过期)
for (int i = 10; i > 0; i--) {
System.out.println(i + "...");
TimeUnit.SECONDS.sleep(1);
}
}
接下来我们生产者发送消息,然后再启动消费者(必须先启动生产者创建交换机和队列,否则会报错):
1、启动生产者
2、监听者启动spring容器,然后会自动启动监听类:
public class Test {
public static void main(String[] args) {
// 初始化spring容器
ClassPathXmlApplicationContext applicationContext =
new ClassPathXmlApplicationContext("classpath:spring-rabbitmq-consumer.xml");
}
}
积压原因
将消息记录到数据库,慢慢处理,不要让消息持续积压!从而影响到正常服务。
利用Redis的原子性去实现
我们都知道redis是单线程的,并且性能也非常好,提供了很多原子性的命令。比如可以使用 setnx 命令。
在接收到消息后将消息ID作为key执行 setnx 命令,如果执行成功就表示没有处理过这条消息(每一次消费都会执行setnx,如果失败,这个值下一次无法插入),可以进行消费了,执行失败表示消息已经被消费了。