RabbitMQ 消息可靠性投递(实际项目改造)
文章目录
1.前言
目前业务系统,RabbitMQ
出现了如下两个问题
- 消息没有持久化机制
- 消息传递的可靠性不高
业务要求不能丢失一条消息!!!
2.解决方案
2.1 rabbitmq 消息传递的流程图
从上图可知,在以下 4
个地方有可能出现消息丢失的情况
-
①表示 消息从生产者发送到 Broker(rabbitmq服务器)
-
②表示 消息从 Exchange(交换机) 路由到 Queue
-
③表示 消息存储在 Queue 中,是存储在内存中,重启消息丢失
-
④表示 消费者订阅 Queue 并消费消息
2.2 第①处修改
采用服务端确认方式
rabbitmq 服务端确认收到消息;分为两种模式
第一:Transaction模式
-
阻塞
-
性能低
-
严重浪费服务器资源
第二:Confirm模式又分为三种
-
发送一条,确认一条(性能低)
-
发送N条,批量确认(N这个值不好把握),一条失败,这N条全部重新发送
-
异步确认(推荐,每次批量确认的条数不一样)
小结
根据目前业务的特性,大量的消息会发送到 rabbitmq
服务端,且数量不固定,选择性能和可靠性适中的异步确认模式
生产者端RabbitMQConfig
配置类的代码修改
- template.setConfirmCallback()
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
//异步确认服务端是否收到消息 如果未收到抛出异常 会重新发送消息
template.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (!ack) {
System.out.println("发送消息失败:" + cause);
throw new RuntimeException("发送异常:" + cause);
}
}
});
template.setMessageConverter(new Jackson2JsonMessageConverter());
return template;
}
2.3 第②处修改
路由保证
在第二步中,交换机的消息有可能不能正确路由到对应的队列上,比如 路由键 错误或者队列不存在等等
-
可以设置消息会回发到生产者服务端,再进行后续处理(推荐)
-
也可以设置备份交换机(本文并未采用,修改量相对比较复杂)
小结
采用消息回发
原因:比较简单
生产者端的代码修改
- template.setMandatory(true);
- template.setReturnCallback()
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
//路由失败 回发消费者端
template.setMandatory(true);
template.setReturnCallback(new RabbitTemplate.ReturnCallback() {
public void returnedMessage(Message message,
int replyCode,
String replyText,
String exchange,
String routingKey) {
service.insert("replyCode: " + replyCode + "replyText: " + replyText + "exchange: " + exchange + "routingKey: " + routingKey,
1, new String(message.getBody()));
}
});
//异步确认服务端是否收到消息 如果未收到抛出异常 会重新发送消息
template.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (!ack) {
System.out.println("发送消息失败:" + cause);
throw new RuntimeException("发送异常:" + cause);
}
}
});
template.setMessageConverter(new Jackson2JsonMessageConverter());
return template;
}
建议将回发的数据写入数据库,后续处理的代码,有数据之后再进行开发也可以
service.insert()
为插入数据库的代码
2.4 第③处修改
持久化
持久化设置好之后,即使
rabbitmq
或者web
应用挂掉了,重启相关应用队列中的消息还在,而且可以重新消费
-
队列的持久化 (消费者配置类)
- 第二个参数true表示持久化
@Bean("employeeQueue") public Queue employeeQueue() { // 参数: 队列名称 是否持久化 是否独占 是否自动删除 return new Queue("employee.queue", true, false, false); }
-
交换机的持久化 (消费者配置类)
- 第二个参数true表示持久化
@Bean("mobileExchange") public DirectExchange exchange() { // 参数: 队列名称 是否持久化是否自动删除 return new DirectExchange("mobile.direct", true, false); }
-
消息的持久化 (生产者配置类)
- 这个比较复杂,本文在实际生产环境做了一个封装类 MessageHelper
- 关键代码
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
设置消息的持久化
public class MessageHelper { public static <T> T msgToObj(Message message, Class<T> clazz) { if (null == message || null == clazz) { return null; } String str = new String(message.getBody()); T obj = JSONObject.parseObject(str, clazz); return obj; } public static <T> List<T> msgToList(Message message, Class<T> clazz) { if (null == message || null == clazz) { return null; } String str = new String(message.getBody()); List<T> obj = JSONObject.parseArray(str, clazz); return obj; } public static Message objToMsg(Object obj) { if (null == obj) { return null; } MessageProperties messageProperties = new MessageProperties(); messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT); messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON); String str = JSONObject.toJSONString(obj); return new Message(str.getBytes(), messageProperties); } }
- 发送消息代码修改
- 注意
RabbitTemplate
rabbitTemplate.send
方法MessageHelper.objToMsg
将原对象leaders
转为Message
对象
- 注意
@Service public class LeaderProducer implements ILeaderProducer { @Autowired RabbitTemplate rabbitTemplate; @Override public void sendSaveQuotaScore(List<Leader> leaders) { rabbitTemplate.send("mobile.direct", "employee", MessageHelper.objToMsg(leaders)); } }
2.5 第④处修改
消费者确认
代码 | 意义 |
---|---|
NONE | 自动应答 |
MANUAL | 手动应答 |
AUTO | 自动应答(在程序运行完) |
AUTO有点区别
1.没有发生异常,正常 ACK 答应
2.手动抛出AmqpRejectAndRequeueException
nack , requeue 拒绝,重回队列
3.手动抛出AmqpRejectAndDontRequeueException
nack,不重新入队
消费者端修改
本文采用的 MANUAL
, 监听类配置,设置为手动提交factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
factory.setAutoStartup(true);
return factory;
}
监听类修改
@RabbitListener
设置containerFactory = "rabbitListenerContainerFactory"
- channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false); 表示手动确认
- 出现异常,将信息插入到数据库
@RabbitListener(queues = "employee.queue", containerFactory = "rabbitListenerContainerFactory")
public void saveQuotaComment(Message msg, Channel channel) throws IOException {
try {
List<Leader> leaders = MessageHelper.msgToList(msg, Leader.class);
leaderMapper.batchAddLeader(leaders);
} catch (Exception e) {
exceptionExecute(e, msg);
} finally {
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
}
}
void exceptionExecute(Exception e, Message msg) {
//将异常信息插入到数据库
String str = new String(msg.getBody());
StackTraceElement[] stackTrace = e.getStackTrace();
String exceptionContent = "";
for (int i = 0; i < stackTrace.length; i++) {
exceptionContent += stackTrace[i].toString();
}
exceptionMapper.insert(e.getMessage() + " >> " + exceptionContent.substring(0, 1000), 2, str);
e.printStackTrace();
}
3.集群
建议有条件的可以使用 HAproxy + keepalived+ 多个 rabbitmq 节点
4.总结
经过上面的 4
步可以确保每一条消息都被正确的消费,即使有异常也会被捕获,将异常信息和原始消息信息保存,以便后续处理。
如果还不放心,可以做一个数据核对的模块,在发送消息的时候,将主键插入日志表,在消息被消费完成的时候,再插入一条,两者进行对比。