幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
消息重复出现在两个阶段
1.生产者重复发送消息,导致消息重复发送到消息队列。
2.MQ的一条消息被消费者消费了多次。
消费者重复消费原因
正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息(ack)给消息队列,消息队列知道该消息被消费了,就会将该消息从消息队列中删除。
在保证MQ消息不重复的情况下,消费者消费消息成功后,在给MQ发送消息确认的时候出现了网络异常(或者是服务中断),MQ没有接收到确认(ack),此时MQ不会将发送的消息删除,
为了保证消息被消费,当消费者网络稳定后,MQ就会继续给消费者投递之前的消息。这时候消费者就接收到了两条一样的消息。
如何解决消息重复消费的问题
1.消息发送者发送消息时携带一个全局唯一的消息id
2.消费者监听到消息后,根据id在redis或者db中查询是否存在消费记录
3.如果没有消费就正常消费,消费完毕后,写入redis或者db
4.如果消息消费过则直接丢弃
注释:为了确保即使服务挂了也能保持幂等性,你可以在处理消息的过程中引入一些持久化的机制。比如,你可以将已经处理过的消息的唯一标识符存储在数据库中,这样即使服务挂了,重新启动后仍然可以从数据库中检查已处理的消息。这样即使服务挂了,也能保证消息的幂等性。
编码示例
生产者服务:
/**
* @Description: 发送消息 模拟消息重复消费
* 消息重复消费情景:消息生产者已把消息发送到mq,消息消费者在消息消费的过程中
* 突然因为网络原因或者其他原因导致消息消费中断
* 消费者消费成功后,在给MQ确认的时候出现了网络波动,MQ没有接收到确认,
* 为了保证消息被消费,MQ就会继续给消费者投递之前的消息。这时候消费者就接收到了两条一样的消息
*/
@GetMapping("/rabbitmq/sendMsgNoRepeat")
public String sendMsgNoRepeat() {
String message = "server message sendMsgNoRepeat";
for (int i = 0; i <10000 ; i++) {
Message msg = MessageBuilder.withBody((message+"--"+i).getBytes()).setMessageId(UUID.randomUUID()+"").build();
amqpTemplate.convertAndSend("queueName4",msg);
}
return message;
}
消费者服务:
方案1:将id存入string中(单消费者场景):
这样一个队列,redis数据只有一条,每次消息过来都覆盖之前的消息,但是消费者多的情况不适用,可能会存在问题,一个消息被多个消费者消费。
@RabbitListener(queues = "queueName4")//发送的队列名称 @RabbitListener注解到类和方法都可以
@RabbitHandler
public void receiveMessage(Message message) throws UnsupportedEncodingException {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(),"utf-8");
String messageRedisValue = redisUtil.get("queueName4","");
if (messageRedisValue.equals(messageId)) {
return;
}
System.out.println("消息:"+msg+", id:"+messageId);
redisUtil.set("queueName4",messageId);//以队列为key,id为value
}
方案2:将id存入list中(多消费者场景)
这个方案可以解决多消费者的问题,但是随着mq的消息增加,redis数据越来越多,需要去清除redis数据。
@RabbitListener(queues = "queueName4")//发送的队列名称 @RabbitListener注解到类和方法都可以
@RabbitHandler
public void receiveMessage1(Message message) throws UnsupportedEncodingException {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(),"utf-8");
List<String> messageRedisValue = redisUtil.lrange("queueName4");
if (messageRedisValue.contains(messageId)) {
return;
}
System.out.println("消息:"+msg+", id:"+messageId);
redisUtil.lpush("queueName4",messageId);//存入list
}
方案3:将id以key值增量存入string中并设置过期时间:
以消息id为key,消息内容为value存入string中,设置过期时间(可承受的redis服务器异常时间,比如设置过期时间为10分钟,如果redis服务器断了20分钟,那么未消费的数据都会丢了)
@RabbitListener(queues = "queueName4")//发送的队列名称 @RabbitListener注解到类和方法都可以
@RabbitHandler
public void receiveMessage2(Message message) throws UnsupportedEncodingException {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(),"utf-8");
String messageRedisValue = redisUtil.get(messageId,"");
if (msg.equals(messageRedisValue)) {
return;
}
System.out.println("消息:"+msg+", id:"+messageId);
redisUtil.set(messageId,msg,10L);//以id为key,消息内容为value,过期时间10分钟
}