重复消费消息,会对非幂等行操作造成问题
重复消费消息的原因是,消费者没有给RabbitMQ一个ack
解决方案
为了解决消息重复消费的问题,可以采用Redis,在消费者消费消息之前,现将消息的id放到Redis中,id-0(正在执行业务)
id-1(执行业务成功)
如果ack失败,在RabbitMQ将消息交给其他的消费者时,先执行setnx,如果key已经存在,获取他的值,如果是0,当前消费者就什么都不做,如果是1,直接ack。
极端情况:第一个消费者在执行业务时,出现了死锁,在setnx的基础上,再给key设置一个生存时间。
生产者,发送消息时,指定messageId
生产者
private final static String QUEUE_NAME = "refuse repeat queue ";
@Test
public void publish() throws Exception {
//1. 获取Connection
Connection connection = RabbitMQUtil.getConnection();
//2. 创建Channel
Channel channel = connection.createChannel();
//3. 发布消息到exchange,同时指定路由的规则
String msg = "防止重复提交的消息";
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
for(int i=0;i<100;i++){
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
.deliveryMode(1) //指定消息书否需要持久化 1 - 需要持久化 2 - 不需要持久化
.messageId(UUID.randomUUID().toString())
.build();
channel.basicPublish("",QUEUE_NAME,properties,(i+msg).getBytes());
}
// Ps:exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息。
System.out.println("生产者发布消息成功!");
//4. 释放资源
channel.close();
connection.close();
}
消费者
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQUtil.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
channel.basicQos(1); // 不要 一次性的把消息都给消费者 容易丢失 一次给一条 安全
//4. 开启监听Queue
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
Jedis jedis = new Jedis("127.0.0.1",6379);
String messageId = properties.getMessageId();
//1. setnx到Redis中,默认指定value-0
String result = jedis.set(messageId, "0", "NX", "EX", 10);
if(result != null && result.equalsIgnoreCase("OK")) {
System.out.println("1接收到消息:" + new String(body, "UTF-8"));
//2. 消费成功,set messageId 1
jedis.setex(messageId,10,"1");
//2. 手动ack
// 参数1 long类型 标识队列中哪个具体的消息 参数2:boolean 类型 是否开启多个消息同时确认
channel.basicAck(envelope.getDeliveryTag(),false);
}else {
//3. 如果1中的setnx失败,获取key对应的value,如果是0,return,如果是1
String s = jedis.get(messageId);
if("1".equalsIgnoreCase(s)){
// 消息被别人消费完了
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
}
};
//3. 指定手动ack
channel.basicConsume(QUEUE_NAME,false,consumer);
System.out.println("消费者1开始监听队列!");
// System.in.read();
System.in.read();
//5. 释放资源
channel.close();
connection.close();
}
@Test
public void consume() throws Exception {
//1. 获取连接对象
Connection connection = RabbitMQUtil.getConnection();
//2. 创建channel
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
channel.basicQos(1); // 不要 一次性的把消息都给消费者 容易丢失 一次给一条 安全
//4. 开启监听Queue
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
Jedis jedis = new Jedis("127.0.0.1",6379);
String messageId = properties.getMessageId();
//1. setnx到Redis中,默认指定value-0
String result = jedis.set(messageId, "0", "NX", "EX", 10);
if(result != null && result.equalsIgnoreCase("OK")) {
System.out.println("2接收到消息:" + new String(body, "UTF-8"));
//2. 消费成功,set messageId 1
jedis.setex(messageId,10,"1");
//2. 手动ack
// 参数1 long类型 标识队列中哪个具体的消息 参数2:boolean 类型 是否开启多个消息同时确认
channel.basicAck(envelope.getDeliveryTag(),false);
}else {
//3. 如果1中的setnx失败,获取key对应的value,如果是0,return,如果是1
String s = jedis.get(messageId);
if("1".equalsIgnoreCase(s)){
// 消息被别人消费完了
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
}
};
//3. 指定手动ack
channel.basicConsume(QUEUE_NAME,false,consumer);
System.out.println("消费者2开始监听队列!");
// System.in.read();
System.in.read();
//5. 释放资源
channel.close();
connection.close();
}
记得连接redis和maven依赖