引言
每一种MQ发送消息给消费者消费时,有可能会因为各种原因(比如网络波动)导致消费者没有及时发回ack,这就导致broker会重发消息给消费者,消费者需要幂等机制来保证消息不会被重复消费。今天就来谈谈RocketMQ如何处理重复消费问题。
数据层的唯一索引🥱
这种方法主要用于防止重复数据的插入。通过为数据库中的关键字段(例如订单号)设置唯一索引,可以避免因为重复消费导致的数据重复插入问题。如果尝试插入一条已经存在的记录,则会因违反唯一约束而失败,从而间接地防止了重复消费带来的副作用。这种一般是兜底的作用了。
public void processOrderCreationMessage(OrderMessage message) {
String transactionId = message.getTransactionId();
Order order = new Order(message.getOrderId(), message.getProductId(),
message.getQuantity(), message.getUserId(), transactionId);
try {
// 尝试插入订单
orderRepository.insert(order);
System.out.println("Order created successfully with transaction id: " + transactionId);
} catch (DuplicateKeyException e) {
// 如果捕获到唯一键冲突异常,说明已经存在相同transaction_id的订单
System.out.println("Duplicate order detected for transaction id: " + transactionId);
// 这里可以根据实际需求选择忽略或者做其他处理
}
}
通过上面的例子,在数据库为业务唯一标识设置唯一索引,即可防止重复插入相同数据到数据库,也就可以在根本上防止重复消费问题。
Redis的分布式锁😣
分布式锁不是直接用来防止重复消费的,而是用来控制多个消费者实例对共享资源的安全访问。然而,在某些场景下,可以通过使用Redis的SETNX命令来创建一种简单的去重机制,即只有当某个业务标识首次被写入时操作才会成功,后续相同的业务标识尝试写入时会失败,以此达到去重的效果
public boolean processMessageWithLock(String messageId) {
Jedis jedis = new Jedis("localhost");
String lockKey = "lock:" + messageId;
String lockValue = UUID.randomUUID().toString(); // 使用随机值保证解锁的安全性
int expireTime = 10; // 锁的过期时间为10秒
try {
// 尝试获取锁
Long result = jedis.setnx(lockKey, lockValue);
if (result == 1) { // 获取锁成功
// 设置锁的过期时间
jedis.expire(lockKey, expireTime);
// 执行业务逻辑
executeBusinessLogic(messageId);
return true;
} else {
System.out.println("Message already being processed.");
return false;
}
} finally {
// 确保只删除自己的锁
String currentValue = jedis.get(lockKey);
if (lockValue.equals(currentValue)) {
jedis.del(lockKey); // 释放锁
}
jedis.close();
}
}
private void executeBusinessLogic(String messageId) {
// 模拟业务逻辑处理
System.out.println("Processing message: " + messageId);
}
- 锁的粒度:锁的定义要足够细粒度,以便最大化并发能力。
- 锁的超时:合理设置锁的过期时间,既不能太短以免业务未完成锁就失效,也不能太长以防止单点故障导致长时间锁定资源。
- 锁的续期:对于耗时较长的任务,考虑在任务执行过程中定期更新锁的过期时间,以防止任务中途因锁过期而被中断。
- 锁的竞争与重试机制:设计好锁的竞争策略和重试机制,避免因为锁竞争而导致大量请求失败或系统性能下降
状态机判断🤓
这种方法是基于业务逻辑的状态转换来实现的。例如,在一个订单处理系统中,每条消息可能对应着从一个状态到另一个状态的转换。如果当前状态已经是目标状态,则无需再次处理该消息,从而避免了重复消费的影响。
状态机设计
-
订单状态:
CREATED: 订单创建成功,等待用户付款。PAID: 用户已经完成支付。COMPLETED: 订单处理完成。
-
状态转换规则:
- 从
CREATED到PAID:当收到用户的支付确认时触发。 - 从
PAID到COMPLETED:当订单商品发货或者服务提供完成后触发。
- 从
在实现中,每个订单都会记录其当前的状态。每当接收到一条消息(比如支付成功的通知),系统首先检查订单的当前状态是否允许进行预期的状态转换。如果当前状态不允许转换,则忽略这条消息;否则执行相应的状态转换并更新数据库中的订单状态
public class OrderService {
// 模拟查询和更新订单状态的方法
private Order getOrder(String orderId) {
// 假设这里是从数据库获取订单信息
return new Order(orderId, "PAID"); // 返回一个已经支付的订单作为示例
}
private void updateOrderStatus(String orderId, String newStatus) {
// 假设这里是更新数据库中的订单状态
System.out.println("Updating order " + orderId + " to status: " + newStatus);
}
public void processPaymentConfirmation(String orderId) {
Order order = getOrder(orderId);
switch (order.getStatus()) {
case "CREATED":
// 如果订单状态是CREATED,可以进行支付操作
updateOrderStatus(orderId, "PAID");
break;
case "PAID":
// 如果订单已经是PAID状态,说明消息可能已经被处理过,无需再次处理
System.out.println("Order " + orderId + " has already been paid.");
break;
case "COMPLETED":
// 如果订单已经是COMPLETED状态,说明消息可能已经被处理过,无需再次处理
System.out.println("Order " + orderId + " has already been completed.");
break;
default:
// 对于其他未知状态,可以根据实际情况处理
System.out.println("Unexpected order status for order " + orderId);
break;
}
}
static class Order {
private String id;
private String status;
public Order(String id, String status) {
this.id = id;
this.status = status;
}
public String getStatus() {
return status;
}
}
public static void main(String[] args) {
OrderService service = new OrderService();
service.processPaymentConfirmation("123456");
}
}
在这个例子中,当我们尝试处理支付确认消息时,首先检查订单的当前状态。如果订单已经是“已支付”(PAID)或“已完成”(COMPLETED)状态,则直接跳过进一步处理,因为这意味着这条消息可能已经被处理过了。这种方式有效地防止了由于消息重复消费而导致的错误状态转换
总结❤️
如果你看了这篇文章有收获可以点赞+关注+收藏🤩,这是对笔者更新的最大鼓励!如果你有更多方案或者文章中有错漏之处,请在评论区提出帮助笔者勘误,祝你拿到更好的offer!