1.消息确认机制
友情提示该文章是跟着上一篇文章继续的,连接:https://blog.csdn.net/qq_41085151/article/details/107102962
问题1:如果在发送消息的时候,消费者出现了异常,那么你监听的消息就会一直循环消费。比如
一.提供者:
@Component
public class HelloProvider{
@Autowired
private RabbitTemplate rabbitTemplate1;
public void send2(User user) {
JSONObject jsonObject=new JSONObject();
String userjson = jsonObject.toJSONString(user);
Message message = MessageBuilder.withBody(userjson.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8").build();
rabbitTemplate1.convertAndSend("directExchange","direct", message);
}
}
二.消费者:
@Component
public class HelloConsumer {
@RabbitListener(queues = "direct")
@RabbitHandler
public void process(Message message, Channel channel) throws Exception {
String msg = new String(message.getBody());
JSONObject jsonObject = JSONObject.parseObject(msg);
String name = jsonObject.getString("name");
System.out.println(jsonObject);
int a = 1 / 0;
}
}
需要加入pom
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.28</version>
</dependency>
在控制层方法中调用提供者的send2()方法,控制台就会一直报错,因为rabbitmq中消息确认机制是发送消者(Producer)发现消息到交换机中,消费者是否确定消费此条消息,如果没有开启ack消息确认,rabbitmq会认为这条消息没有被消费,会将消息再次放入到队列中,再次让你消费,形成死循环。
三.在配置文件中添加设置消费端手动ack确认
server.port=8889
spring.rabbitmq.host=192.168.221.150
spring.rabbitmq.port=5672
spring.rabbitmq.username=zl
spring.rabbitmq.password=123
#开启消息确认机制
spring.rabbitmq.publisher-confirms=true
#支持消息发送失败返回队列
spring.rabbitmq.publisher-returns=true
#设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
spring.rabbitmq.template.mandatory=true
spring.rabbitmq.connection-timeout=15000
#用户虚拟机权限名称
spring.rabbitmq.virtual-host=/
#设置消费端手动 ack none不确认 auto自动确认 manual手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
四.消费端中配置手动确定
@RabbitListener(queues = "direct")
@RabbitHandler
public void process(Message message, Channel channel) throws Exception {
String msg = new String(message.getBody());
JSONObject jsonObject = JSONObject.parseObject(msg);
String name = jsonObject.getString("name");
System.out.println(jsonObject);
try {
int a=1/0;
/**
* 确认一条消息:<br>
* channel.basicAck(deliveryTag, false); <br>
* deliveryTag:该消息的index <br>
* multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息 <br>
*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
//出现异常
/**
* 拒绝确认消息:<br>
* channel.basicNack(long deliveryTag, boolean multiple, boolean requeue) ; <br>
* deliveryTag:该消息的index<br>
* multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息。<br>
* requeue:被拒绝的是否重新入队列 <br>
*/
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
e.printStackTrace();
}
}
分析:一般如果是代码中出现的异常比如类型转换的异常,是必须要修改代码更正的。而一般在真实的业务场景,是我调用某个
系统的接口返回的是空,那么就可以做处理。现在又会发现一个问题,一般业务场景不会轻易的把消息丢失也就是代码中的
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
最后一个参数requeue一般都会为true,此次没调用到数据,把这个消息返回到队列中再消费,如果代码中出现了int a=1/0,那么还是会造成死循环。
2.消息重试机制
当你开启了手动ack的时候再消费端如果在消费的时候出现异常也会导致循环消费,所以要启动消息重试机制,默认是3次重试去消费一条消息,如果没有消费完成,则丢弃(删除)该消息或者放入死信队列中或者进行人工补偿。
一.配置文件
server.port=8889
spring.rabbitmq.host=192.168.221.150
spring.rabbitmq.port=5672
spring.rabbitmq.username=zl
spring.rabbitmq.password=123
#开启消息确认机制
spring.rabbitmq.publisher-confirms=true
#支持消息发送失败返回队列
spring.rabbitmq.publisher-returns=true
#设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
spring.rabbitmq.template.mandatory=true
spring.rabbitmq.connection-timeout=15000
#用户虚拟机权限名称
spring.rabbitmq.virtual-host=/
#设置消费端手动 ack none不确认 auto自动确认 manual手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#消费者最小数量
spring.rabbitmq.listener.simple.concurrency=1
#消费之最大数量
spring.rabbitmq.listener.simple.max-concurrency=1
#开启消费者重试机制(为false时关闭消费者重试,这时消费端代码异常会一直重复收到消息)
spring.rabbitmq.listener.simple.retry.enabled=true
#重试次数5
spring.rabbitmq.listener.simple.retry.max-attempts=5
#重试时间间隔
spring.rabbitmq.listener.simple.retry.initial-interval=5000
#重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列)
spring.rabbitmq.listener.simple.default-requeue-rejected=true
#在单个请求中处理的消息个数,他应该大于等于事务数量(unack的最大数量)
spring.rabbitmq.listener.simple.prefetch=2
触发重试机制需要消费者抛出异常,而不能try/catch捕捉异常,不然会死循环。
二.消费者代码
public class HelloConsumer {
@RabbitListener(queues = "direct")
@RabbitHandler
public void process(Message message, Channel channel) throws Exception {
String msg = new String(message.getBody());
JSONObject jsonObject = JSONObject.parseObject(msg);
String name = jsonObject.getString("name");
System.out.println(jsonObject);
if ("李四".equals(name)) {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}else
{
System.out.println("消息重试!");
throw new RuntimeException("查询到的结果为空!");
}
}
}
控制台
{"pass":"123456","name":"张三"}
消息重试!
{"pass":"123456","name":"张三"}
消息重试!
{"pass":"123456","name":"张三"}
消息重试!
{"pass":"123456","name":"张三"}
消息重试!
{"pass":"123456","name":"张三"}
消息重试!
重试5次后就删除该消息了。具体看你如果没有消费完成,则丢弃(删除)该消息或者放入死信队列中或者进行人工补偿。
3.消息幂等性
Rabbitmq中消息重复消费的问题,在消费者有些消息处理了,但是没来的及提交offset,消费者挂了的情况,再重启可能导致重复消费。怎么判断该消息是否已经消费了呢?
解决办法:
使用全局MessageID判断消费方使用同一个,解决幂等性。
一.修改提供方的send2方法
public void send2(User user) {
JSONObject jsonObject=new JSONObject();
String userjson = jsonObject.toJSONString(user);
UUID uuid = UUID.randomUUID();
// 设置消息唯一id 保证每次重试消息id唯一
Message message = MessageBuilder.withBody(userjson.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
.setMessageId(uuid + "").build(); //消息id设置在请求头里面 用UUID做全局ID
rabbitTemplate1.convertAndSend("directExchange","direct", message);
}
二.修改消费端方法
@Component
public class HelloConsumer {
@RabbitListener(queues = "direct")
@RabbitHandler
public void process(Message message, Channel channel) throws Exception {
String messageId = message.getMessageProperties().getMessageId(); //id获取之
String ok= redis.get("msg"+messageId);//从redis中获取是否消费过的id
if (ok=="sucess"){ //消费过了的消息直接丢弃然后返回。
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false, false);
return;
}
String msg = new String(message.getBody());
JSONObject jsonObject = JSONObject.parseObject(msg);
String name = jsonObject.getString("name");
System.out.println(jsonObject);
if ("李四".equals(name)) {
System.out.println("消费成功");
redis.set("msg"+messageId,"success");//设置redis
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}else
{
System.out.println("消息重试!");
throw new RuntimeException("查询到的结果为空!");
}
}
}
其实解决方式:用一个消息消费表来记录每一条消息,给每个一个消息设置一个id(uuid),消费了就保存到表中去。消息过来的时候先查询是否已经消费。消息的幂等性。是差不多的。看你怎么用吧。
其实使用redis可以看我的springboot整合redis篇链接:https://blog.csdn.net/qq_41085151/article/details/106904937
文章中都是我个人理解,也是亲测过,欢迎各位小伙伴提出问题喔,觉得好的可不可以点个赞呢!