案例分析
如果消费者 运行时候 报错了
package com.toov5.msg.SMS;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@RabbitListener(queues="fanout_sms_queue")
public class SMSConsumer {
@RabbitHandler
public void process(String mString) {
System.out.println("短信消费者获取生产者消息msg"+mString);
int i = 1/0;
}
}
当生产者投递消息后:
消费者会不停的进行打印:
消息一直没有被消费
原因 :
Rabbitmq 默认情况下 如果消费者程序出现异常情况 会自动实现补偿机制 也就是 重试机制
底层原理:
@RabbitListener底层使用AOP进行拦截,如果程序没有抛出异常,自动提交事务。 如果Aop使用异常通知 拦截获取异常信息的话 , 自动实现补偿机制,该消息会一直缓存在Rabbitmq服务器端进行重放,一直重试到不抛出异常为准。
可以修改重试策略
一般来说默认5s重试一次,
消费者配置:
listener:
simple:
retry:
####开启消费者重试
enabled: true
####最大重试次数(默认无数次)
max-attempts: 5
####重试间隔次数
initial-interval: 3000
效果: 重试5次 不行就放弃了
MQ重试机制机制 需要注意的问题
如何合适选择重试机制
-
情况1: 消费者获取到消息后,调用第三方接口,但接口暂时无法访问,是否需要重试?
需要重试 别人的问题不是我自己的问题 -
情况2: 消费者获取到消息后,抛出数据转换异常,是否需要重试?
不需要重试 重试一亿次也是如此 木有必要 需要发布版本解决
总结:
- 对于情况2,如果消费者代码抛出异常是需要发布新版本才能解决的问题,那么不需要重试,重试也无济于事。应该采用 日志记录+定时任务job健康检查+人工进行补偿
- 把错误记录在日志里面,通过定时Job去自动的补偿,或通过人工去补偿。
传统的HTTP请求 如果失败了没法自动重试 ,当然自己可以写个循环实现。MQ完全自己自带的。
情况2的拓展延申:
将之前的案例改为 邮件消费者 调用邮件第三方接口
伪代码:
在consumer 中 调用接口后 判断返回值 由于RabbitMQ 在消费者异常时候 会进行重试机制 进行补偿
所以可以抛出个异常 来实现
伪代码Consumer:
String result = template.Email();
if(result == null){
throw new Exception("调用第三方邮件服务器接口失败!");
}
Producer:
package com.itmayiedu.rabbitmq;
import java.util.UUID;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
@Component
public class FanoutProducer {
@Autowired
private AmqpTemplate amqpTemplate;
public void send(String queueName) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("email", "xx@163.com");
jsonObject.put("timestamp", System.currentTimeMillis());
String jsonString = jsonObject.toJSONString();
System.out.println("jsonString:" + jsonString);
// 设置消息唯一id 保证每次重试消息id唯一
/*Message message = MessageBuilder.withBody(jsonString.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
.setMessageId(UUID.randomUUID() + "").build();*/
amqpTemplate.convertAndSend(queueName, jsonString);
}
}
Controller:
package com.itmayiedu.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.itmayiedu.rabbitmq.FanoutProducer;
@RestController
public class ProducerController {
@Autowired
private FanoutProducer fanoutProducer;
@RequestMapping("/sendFanout")
public String sendFanout(String queueName) {
fanoutProducer.send(queueName);
return "success";
}
}
真正的Consumer:
package com.itmayiedu.rabbitmq;
import java.util.Map;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
import com.itmayiedu.rabbitmq.utils.HttpClientUtils;
import com.rabbitmq.client.Channel;
//邮件队列
@Component
public class FanoutEamilConsumer {
@RabbitListener(queues = "fanout_email_queue")
public void process(String msg) throws Exception {
System.out.println("邮件消费者获取生产者消息msg:" + msg);
JSONObject jsonObject = JSONObject.parseObject(msg);
// 获取email参数
String email = jsonObject.getString("email");
// 请求地址
String emailUrl = "http://127.0.0.1:8083/sendEmail?email=" + email;
JSONObject result = HttpClientUtils.httpGet(emailUrl);
if (result == null) {
// 因为网络原因,造成无法访问,继续重试
throw new Exception("调用接口失败!");
}
System.out.println("执行结束....");
}
}
邮件服务器:
package com.mayikt.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class MsgController {
// 模拟第三方发送邮件
@RequestMapping("/sendEmail")
public Map<String, Object> sendEmail(String email) {
System.out.println("开始发送邮件:" + email);
Map<String, Object> result = new HashMap<String, Object>();
result.put("code", "200");
result.put("msg", "发送邮件成功..");
System.out.println("发送邮件成功");
return result;
}
public static void main(String[] args) {
SpringApplication.run(MsgController.class, args);
}
}
在没有启动邮件服务器时候,消费者调用接口失败会一直重试,重试五次。
在此期间,如果启动成功,则重试成功,不再重试, 不再进行补偿机制。
消费者如果保证消息幂等性,不被重复消费
背景:
网络延迟传输中,或者消费出现异常或者是消费延迟,会造成进行MQ重试进行重试补偿机制,在重试过程中,可能会造成重复消费。
解决办法:
使用全局MessageID判断消费方使用同一个,解决幂等性。
只要重试过程中,判断如果已经走完了 不能再继续走 继续执行了
MQ消费者的幂等行的解决 一般使用全局ID 或者写个唯一标识比如时间戳 或者UUID 或者订单号
更详细的方案可参考我的文章:
https://blog.csdn.net/belongtocode/article/details/104237704
改进:
Producer:
添加:
// 设置消息唯一id 保证每次重试消息id唯一
Message message = MessageBuilder.withBody(jsonString.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
.setMessageId(UUID.randomUUID() + "").build(); //消息id设置在请求头里面 用UUID做全局ID
amqpTemplate.convertAndSend(queueName, message);
全部代码:
package com.itmayiedu.rabbitmq;
import java.util.UUID;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
@Component
public class FanoutProducer {
@Autowired
private AmqpTemplate amqpTemplate;
public void send(String queueName) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("email", "xx@163.com");
jsonObject.put("timestamp", System.currentTimeMillis());
String jsonString = jsonObject.toJSONString();
System.out.println("jsonString:" + jsonString);
// 设置消息唯一id 保证每次重试消息id唯一
Message message = MessageBuilder.withBody(jsonString.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
.setMessageId(UUID.randomUUID() + "").build(); //消息id设置在请求头里面 用UUID做全局ID
amqpTemplate.convertAndSend(queueName, message);
}
}
同样的 消费者也需要修改:
方法参数类型为 Message 然后可以获取这个ID 然后可以进行业务逻辑操作
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message) throws Exception {
// 获取消息Id
String messageId = message.getMessageProperties().getMessageId(); //id获取之
String msg = new String(message.getBody(), "UTF-8"); //消息内容获取之
System.out.println("-----邮件消费者获取生产者消息-----------------" + "messageId:" + messageId + ",消息内容:" +
msg);
if (messageId == null) {
return;
}
JSONObject jsonObject = JSONObject.parseObject(msg);
// 获取email参数
String email = jsonObject.getString("email");
// 请求地址
String emailUrl = "http://127.0.0.1:8083/sendEmail?email=" + email;
JSONObject result = HttpClientUtils.httpGet(emailUrl);
if (result == null) {
// 因为网络原因,造成无法访问,继续重试
throw new Exception("调用接口失败!");
}
System.out.println("执行结束....");
//messId 的情况写入到redis 中 成功就修改为空
}
重试机制都是间隔性的 每次都是一个线程 单线程重试
关于应答模式Ack:
Spring boot 中进行 AOP拦截 自动帮助做重试
手动应答的话 ,如果不告诉服务器已经消费成功,则服务器不会删除 消息。告诉消费成功了才会删除。
消费者的yml加入:
acknowledge-mode: manual
spring:
rabbitmq:
####连接地址
host: 192.168.91.6
####端口号
port: 5672
####账号
username: admin
####密码
password: admin
### 地址
virtual-host: /admin_toov5
listener:
simple:
retry:
####开启消费者异常重试
enabled: true
####最大重试次数
max-attempts: 5
####重试间隔次数
initial-interval: 2000
####开启手动ack
acknowledge-mode: manual
server:
port: 8081
开启模式之后:
消费者参数需要加入: @Headers Map<String, Object> headers, Channel channel
代码逻辑最后面加入:
// // 手动ack
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
// 手动签收 告诉RabbitMQ 消费成功了 消息可以删除了
channel.basicAck(deliveryTag, false);
代码如下:
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
// 获取消息Id
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("邮件消费者获取生产者消息" + "messageId:" + messageId + ",消息内容:" + msg);
JSONObject jsonObject = JSONObject.parseObject(msg);
// 获取email参数
String email = jsonObject.getString("email");
// 请求地址
String emailUrl = "http://127.0.0.1:8083/sendEmail?email=" + email;
JSONObject result = HttpClientUtils.httpGet(emailUrl);
if (result == null) {
// 因为网络原因,造成无法访问,继续重试
throw new Exception("调用接口失败!");
}
// // 手动ack
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
// 手动签收
channel.basicAck(deliveryTag, false);
System.out.println("执行结束....");
}
本文主要参考:
https://www.cnblogs.com/toov5/p/10287183.html