被询问如何防止MQ消息被重复消费时,其实是在考察候选人对消息队列、分布式系统设计以及容错机制的理解,通过这些问题,可以全面了解候选人在处理MQ消息重复消费问题时的思考方式、技术能力和实践经验,从而评估其是否适合担任相关岗位。
MQ实现策略
MQ提供了以下几种方式来防止消息被重复消费:
1.消费者手动确认消息
在消费者消费消息后,通过调用basic.ack()方法手动确认消息已被消费。这样一来,RabbitMQ就会从队列中删除该消息,防止消息被重复消费。
java代码示例:
// 创建连接和频道
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 设置消息确认模式为手动确认
channel.basicQos(1);
// 定义消息消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
// 模拟消费消息
String message = new String(body, "UTF-8");
System.out.println("Received message: " + message);
// 手动确认消息消费
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (Exception e) {
// 消息消费失败,进行异常处理
channel.basicNack(envelope.getDeliveryTag(), false, true);
}
}
};
// 开始消费消息
channel.basicConsume(queueName, false, consumer);
2.消息去重(使用乐观锁)
在消费者消费消息前,可以将消息的唯一标识保存在数据库或缓存中。在消费者接收到消息后,先检查数据库或缓存中是否存在该消息的唯一标识,如果存在,则表示该消息已经被消费过,可以忽略;如果不存在,则表示该消息是新的,可以进行消费。
java代码示例:
// 创建连接和频道
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 定义消息消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
// 模拟消费消息
String message = new String(body, "UTF-8");
System.out.println("Received message: " + message);
// 判断消息是否已经消费过,可以通过数据库或缓存进行判断
if (!isMessageConsumed(message)) {
// 进行消息消费
consumeMessage(message);
}
// 手动确认消息消费
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (Exception e) {
// 消息消费失败,进行异常处理
channel.basicNack(envelope.getDeliveryTag(), false, true);
}
}
};
// 开始消费消息
channel.basicConsume(queueName, false, consumer);
3.使用消息的全局唯一标识
可以在消息的属性中添加一个全局唯一标识,例如UUID,确保每条消息都具有唯一性。消费者在消费消息时,可以通过检查全局唯一标识来判断消息是否已经被消费过。
java代码示例:
// 创建连接和频道
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 定义消息消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
// 模拟消费消息
String message = new String(body, "UTF-8");
System.out.println("Received message: " + message);
// 判断消息是否已经消费过,可以通过全局唯一标识进行判断
String messageId = properties.getMessageId();
if (!isMessageConsumed(messageId)) {
// 进行消息消费
consumeMessage(message);
// 将消息的全局唯一标识保存到数据库或缓存中
saveMessageId(messageId);
}
// 手动确认消息消费
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (Exception e) {
// 消息消费失败,进行异常处理
channel.basicNack(envelope.getDeliveryTag(), false, true);
}
}
};
// 开始消费消息
channel.basicConsume(queueName, false, consumer);
4.设置消息的过期时间
可以为消息设置一个过期时间,在消费者消费消息时,先判断消息是否已经过期,如果已经过期,则不进行消费。
java代码示例:
// 创建连接和频道
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 设置队列的消息过期时间
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl", 5000); // 设置消息过期时间为5秒
channel.queueDeclare(queueName, true, false, false, arguments);
// 发布消息
String message = "Hello, RabbitMQ!";
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.expiration("5000") // 设置消息的过期时间为5秒
.build();
channel.basicPublish("", queueName, properties, message.getBytes("UTF-8"));
System.out.println("Sent message: " + message);
// 关闭连接和频道
channel.close();
connection.close();
通过在队列声明时设置x-message-ttl
参数来设置队列的消息过期时间。然后,通过发布消息时设置expiration
属性来设置消息的过期时间。这里将消息的过期时间设置为5秒。需要注意的是,RabbitMQ的消息过期时间精度是毫秒级别的,可以通过设置整数或字符串形式的时间间隔来指定过期时间。如果同时设置了队列和消息的过期时间,以较小的那个为准。
5.使用幂等操作
对于一些幂等操作,可以将操作的唯一标识保存在数据库或缓存中。在消费者消费消息时,先检查操作的唯一标识是否存在,如果存在,则表示该操作已经执行过,可以忽略;如果不存在,则表示该操作是新的,可以进行执行。
java代码示例:
// 创建连接和频道
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 定义消息消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
// 模拟消费消息
String message = new String(body, "UTF-8");
System.out.println("Received message: " + message);
// 判断消息是否已经处理过,可以通过幂等操作来判断
if (!isMessageProcessed(message)) {
// 进行消息处理
processMessage(message);
// 标记消息已处理,将消息的唯一标识保存到数据库或缓存中
saveProcessedMessage(message);
}
// 手动确认消息消费
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (Exception e) {
// 消息消费失败,进行异常处理
channel.basicNack(envelope.getDeliveryTag(), false, true);
}
}
};
// 开始消费消息
channel.basicConsume(queueName, false, consumer);
// 关闭连接和频道
channel.close();
connection.close();
通过isMessageProcessed()
方法判断消息是否已经被处理过。如果消息已经被处理过,则忽略该消息;如果消息还未被处理过,则进行消息处理,并将消息的唯一标识保存起来。这里的processMessage()
方法是具体的消息处理逻辑,saveProcessedMessage()
方法将消息的唯一标识保存到数据库或缓存中,以便后续判断消息是否已经被处理过。需要根据具体的业务逻辑实现isMessageProcessed()、processMessage()
和saveProcessedMessage()
方法来实现幂等操作。
消息重复消费的原因多种多样,不可避免。所以只能从消费者端入手,只要能保证消息处理的幂等性就可以确保消息不被重复消费。SS 而幂等性的保证又有很多方案:
-
给每一条消息都添加一个唯一id,在本地记录消息表及消息状态,处理消息时基于数据库表的id唯一性做判断
-
消息去重使用乐观锁,同样是记录消息表,利用消息状态字段实现基于乐观锁的判断,保证幂等
-
基于业务本身的幂等性。比如根据id的删除、查询业务天生幂等;新增、修改等业务可以考虑基于数据库id唯一性、或者乐观锁机制确保幂等。本质与消息表方案类似。
-
设置消息的过期时间,可以为消息设置一个过期时间,在消费者消费消息时,先判断消息是否已经过期,如果已经过期,则不进行消费。
-
消费者手动确认消息,在消费者消费消息后,通过调用basic.ack()方法手动确认消息已被消费。这样一来,RabbitMQ就会从队列中删除该消息,防止消息被重复消费。