一站式了解RocketMQ如何处理重复消费问题😫

184 阅读5分钟

引言

每一种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);
}
  • 锁的粒度:锁的定义要足够细粒度,以便最大化并发能力。
  • 锁的超时:合理设置锁的过期时间,既不能太短以免业务未完成锁就失效,也不能太长以防止单点故障导致长时间锁定资源。
  • 锁的续期:对于耗时较长的任务,考虑在任务执行过程中定期更新锁的过期时间,以防止任务中途因锁过期而被中断。
  • 锁的竞争与重试机制:设计好锁的竞争策略和重试机制,避免因为锁竞争而导致大量请求失败或系统性能下降

状态机判断🤓

这种方法是基于业务逻辑的状态转换来实现的。例如,在一个订单处理系统中,每条消息可能对应着从一个状态到另一个状态的转换。如果当前状态已经是目标状态,则无需再次处理该消息,从而避免了重复消费的影响。

状态机设计

  1. 订单状态

    • CREATED: 订单创建成功,等待用户付款。
    • PAID: 用户已经完成支付。
    • COMPLETED: 订单处理完成。
  2. 状态转换规则

    • CREATEDPAID:当收到用户的支付确认时触发。
    • PAIDCOMPLETED:当订单商品发货或者服务提供完成后触发。

在实现中,每个订单都会记录其当前的状态。每当接收到一条消息(比如支付成功的通知),系统首先检查订单的当前状态是否允许进行预期的状态转换。如果当前状态不允许转换,则忽略这条消息;否则执行相应的状态转换并更新数据库中的订单状态

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!