RabbitMQ消息的可靠性问题
由于网络、软硬件等问题,消息是存在丢失问题
在哪几个方面存在丢失:
- 生产者到交换机
- 交换机到队列
- 队列到消费者
解决方案:
-
针对 生产者到交换机
confirm 机制 确定生产者将消息投递到交换机
- 针对 交换机到队列
return 机制 交换机发送消息到队列失败会执行回调
- 针对 队列到消费者
消息没发 --> 消息持久化
消息发了,不知道是否被消费 —> 手动确认 ack
Confirm&Return机制
rabbitmq:
publisher-confirm-type: correlated # 启动confirm机制
publisher-returns: true # 启动return机制
配置类添加
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//设置confirm机制的回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("消息{}发送到Exchange成功!!!",correlationData);
} else {
log.error("消息{}发送到Exchange失败,原因:{}",correlationData,cause);
}
}
});
//设置return机制的回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
//交换机发送消息到队列失败回调
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.error("消息发送到Queue失败!{}",message);
}
});
return rabbitTemplate;
}
生产者发送消息,设置消息的id
rabbitTemplate.convertAndSend(RabbitMQConfig.COURSE_EXCHANGE,
RabbitMQConfig.KEY_COURSE_SAVE, JSON.toJSONString(course),
new CorrelationData(UUID.randomUUID().toString()))
RabbitMQ消息幂等性
重复消费问题:
队列发送消息给消费者,因为网络原因,消费者没有收到,队列重复发送,网络恢复后会发送多个相同的消息给消费者
可能导致问题:
重复添加数据等非幂等性业务问题
非幂等性业务(每次操作获得不同结果,如:添加)
幂等性业务(每次操作结果相同,如:更新、删除)
消息有唯一id,保存redis,手动确认
解决方案:
- 给每个消息添加id
- 消费者获得消息后,通过id查询redis,判断该id是否存在
- 如果不存在,就修改该id的value为1,执行业务,进行手动确认
- 如果存在,就不执行业务
Redis的setnx命令
setnx key value
如果该键不存在,就设置键和值,返回1
如果该键存在,直接返回
解决方案:
- 加入Redis的依赖和配置文件和配置类
rabbitmq:
host: localhost
port: 5672
virtual-host: myhost
username: admin
password: 123456
listener:
simple:
acknowledge-mode: manual # 消费者手动确认
redis:
host: 192.168.223.223
port: 6379
- 修改监听器
@Autowired
RedisTemplate<String,Object> redisTemplate;
/**
* 监听课程添加操作
*/
@RabbitListener(bindings = {
@QueueBinding(value = @Queue(value = QUEUE_COURSE_SAVE, durable = "true"),
exchange = @Exchange(value = COURSE_EXCHANGE,
type = ExchangeTypes.TOPIC,
ignoreDeclarationExceptions = "true")
, key = KEY_COURSE_SAVE)})
public void receiveCourseSaveMessage(String json, Channel channel, Message message) throws IOException {
log.info("课程添加:{}",json);
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
//获得消息id
String messageId = message.getMessageProperties().getHeader("spring_returned_message_correlation");
log.info("消息id:{}",messageId);
//执行setnx命令,以messageId为键保存数据,设置过期时间
if(ops.setIfAbsent(messageId,"0",100, TimeUnit.SECONDS)){
//如果返回true,则该键不存在,也就是没有重复消费
//将消息转为课程,保存到es中
Course course = JSON.parseObject(json,Course.class);
courseService.saveOrUpdate(course);
log.info("添加完成:{}",course);
//修改messageId键的值为1,代表该消息已经被消费
redisTemplate.opsForValue().set(messageId, "1",100,TimeUnit.SECONDS);
//手动确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}else{
//返回false,代表存在该键
String msg = (String) ops.get(messageId);
log.info("该消息{}已经存在{}",messageId,msg);
if("1".equals(msg)){
//为1代表业务已经执行过了,手动确认结束
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
}
eId,msg);
if("1".equals(msg)){
//为1代表业务已经执行过了,手动确认结束
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
}