为什么80%的码农都做不了架构师?>>>
Message acknowledgment 消息确认
默认情况下,RabbitMQ 会顺序的分发每个Message。当分发后,会将该Message删除,然后将下一个Message分发到下一个Consumer。这种分发方式叫做round-robin
每个Consumer可能需要一段时间才能处理完收到的数据。如果在这个过程中,Consumer出错了,异常退出了,而数据还没有处理完成,那么非常不幸,这段数据就丢失了。因为我们采用no-ack的方式进行确认,也就是说,每次Consumer接到数据后,而不管是否处理完成,RabbitMQ Server会立即把这个Message标记为完成,然后从queue中删除了。
如果一个Consumer异常退出了,它处理的数据能够被另外的Consumer处理,这样数据在这种情况下就不会丢失了(注意是这种情况下)。
为了保证数据不被丢失,RabbitMQ支持消息确认机制,即acknowledgments。为了保证数据能被正确处理而不仅仅是被Consumer收到,那么我们不能采用no-ack。而应该是在处理完数据后发送ack。
在处理数据后发送的ack,就是告诉RabbitMQ数据已经被接收,处理完成,RabbitMQ可以去安全的删除它了。
如果Consumer退出了但是没有发送ack,那么RabbitMQ就会把这个Message发送到下一个Consumer。这样就保证了在Consumer异常退出的情况下数据也不会丢失。
这里并没有用到超时机制。RabbitMQ仅仅通过Consumer的连接中断来确认该Message并没有被正确处理。也就是说,RabbitMQ给了Consumer足够长的时间来做数据处理。
消息确认,对于spring-boot来说,就是一个开关,它就是spring.rabbitmq.listener.simple.acknowledge-mode
acknowledgeMode有三值:
A、NONE = no acks will be sent (incompatible with channelTransacted=true).
RabbitMQ calls this "autoack" because the broker assumes all messages are acked without any action from the consumer.
B、MANUAL = the listener must acknowledge all messages by calling Channel.basicAck().
C、AUTO = the container will acknowledge the message automatically, unless the MessageListener throws an exception.
Note that acknowledgeMode is complementary to channelTransacted - if the channel is transacted then the broker requires a commit notification in addition to the ack. This is the default mode. See also txSize.
一、编写代码
1、修改配置文件application.yml
spring:
rabbitmq:
host: 192.168.2.202 #服务地址 内网地址
port: 5672 #端口 内网地址
username: admin #用户名
password: qaz123 #密码
virtual-host: / #vhost
publisher-confirms: true #支持发布确认
publisher-returns: true #支持发布返回
listener:
simple:
acknowledge-mode: manual #acknowledgeMode设置为手动模式(NONE,MANUAL,AUTO)
concurrency: 1 #当前监听容器数
max-concurrency: 1 #最大数
retry:
enabled: true #是否支持重试
direct:
acknowledge-mode: manual #acknowledgeMode设置为手动模式
2、编写常量类RabbitAckConstant
package com.lvgang.springbootrabbitmq.messageack;
/**
* @author lvgang
*/
public class RabbitAckConstant {
public static final String QUEUQ = "Queue_ACK";
}
3、编写配置类RabbitAckConfig
package com.lvgang.springbootrabbitmq.messageack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author lvgang
*/
@Configuration
public class RabbitAckConfig {
private static Logger logger = LoggerFactory.getLogger(RabbitAckConfig.class);
/**
* Queue 可以有4个参数
* 1.队列名
* 2.durable 持久化消息队列 ,rabbitmq重启的时候不需要创建新的队列 默认true
* 3.auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
* 4.exclusive 表示该消息队列是否只在当前connection生效,默认是false
*/
@Bean
public Queue createAckQueue() {
return new Queue(RabbitAckConstant.QUEUQ,true);
}
}
4、编写消息生产者AckSender
package com.lvgang.springbootrabbitmq.messageack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.UUID;
/**
* @author lvgang
*/
@Component
public class AckSender implements RabbitTemplate.ConfirmCallback ,RabbitTemplate.ReturnCallback {
private static Logger logger = LoggerFactory.getLogger(AckSender.class);
@Autowired
private RabbitTemplate rabbitTemplate;
public void send() {
//设置回调对象
this.rabbitTemplate.setConfirmCallback(this);
this.rabbitTemplate.setReturnCallback(this);
//构建回调返回的数据
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
String content = "Confirm= " + new Date() + ", content= " + UUID.randomUUID().toString();
this.rabbitTemplate.convertAndSend(RabbitAckConstant.QUEUQ,(Object) content,correlationData);
logger.info("Confirm Send ok,"+new Date()+","+content);
}
/**
* 消息回调确认方法
* 如果消息没有到exchange,则confirm回调,ack=false
* 如果消息到达exchange,则confirm回调,ack=true
* @param
*/
@Override
public void confirm(CorrelationData correlationData, boolean isSendSuccess, String s) {
//logger.info("confirm--message:回调消息ID为: " + correlationData.getId());
if (isSendSuccess) {
//logger.info("confirm--message:消息发送成功");
} else {
logger.info("confirm--message:消息发送失败" + s);
}
}
/**
* exchange到queue成功,则不回调return
* exchange到queue失败,则回调return(需设置mandatory=true,否则不回回调,消息就丢了)
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
logger.info("return--message:" + new String(message.getBody()) + ",replyCode:" + replyCode
+ ",replyText:" + replyText + ",exchange:" + exchange + ",routingKey:" + routingKey);
}
}
5、编写消息消费者A AckReceiverA
package com.lvgang.springbootrabbitmq.messageack;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.util.Date;
/**
* @author lvgang
*/
@Component
@RabbitListener(queues = RabbitAckConstant.QUEUQ)
public class AckReceiverA {
private static Logger logger = LoggerFactory.getLogger(AckReceiverA.class);
@RabbitHandler
public void process(String str,Channel channel, Message message) {
logger.info("ReceiverA : " + str +","+ new Date());
try {
//告诉服务器收到这条消息 已经被我消费了 可以在队列删掉 这样以后就不会再发了 否则消息服务器以为这条消息没处理掉 后续还会在发
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
logger.info("消息消费A成功!");
} catch (Exception e) {
logger.error("消息消费A失败:"+ e.getMessage(),e);
//丢弃这条消息
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消息的标识,false只确认当前一个消息收到,true确认所有consumer获得的消息
//channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
//ack返回false,并重新回到队列,api里面解释得很清楚
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
//拒绝消息
//channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
6、编写消息消费者B AckReceiverB
package com.lvgang.springbootrabbitmq.messageack;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import java.util.Date;
/**
* @author lvgang
*/
@Component
@RabbitListener(queues = RabbitAckConstant.QUEUQ)
public class AckReceiverB {
private static Logger logger = LoggerFactory.getLogger(AckReceiverB.class);
@RabbitHandler
public void process(String str,Channel channel, Message message){
logger.info("ReceiverB : " + str +","+ new Date());
try {
//告诉服务器收到这条消息 已经被我消费了 可以在队列删掉 这样以后就不会再发了 否则消息服务器以为这条消息没处理掉 后续还会在发
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
logger.info("消息消费B成功!");
} catch (Exception e) {
logger.error("消息消费B失败:"+ e.getMessage(),e);
//丢弃这条消息
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消息的标识,false只确认当前一个消息收到,true确认所有consumer获得的消息
//channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
//ack返回false,并重新回到队列,api里面解释得很清楚
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
//拒绝消息
//channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
二、测试结果
1、编写测试类TopicTests
package com.lvgang.springbootrabbitmq.messageack;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author lvgang
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class AckTests {
@Autowired
private AckSender ackSender;
@Test
public void hello() {
int i=1;
while (true) {
try {
if(i<10) {
ackSender.send();
}
i++;
Thread.sleep(1000);
} catch (Exception e) {
;
}
}
}
}
2、执行测试类,并查看结果
通过执行测类,查看到了消息消费的情况,生产者共计生产了10个消息,被消费者A消费了8条,消费都B消费了2条。
每一次消费都消费完成消息之后需要回复消息中心一个信息就是channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
这样消息中心才会把下一条消息发给消费者。