每日Java面试场景题知识点之-RabbitMQ消息重复消费问题

每日Java面试场景题知识点之-RabbitMQ消息重复消费问题

场景描述

在分布式系统中,由于网络问题或消费者重启,同一条消息可能被多次处理,导致业务逻辑重复执行。这种情况在订单处理、支付回调、库存扣减等关键业务场景中尤为严重,可能造成重复下单、重复支付、库存超卖等问题。

技术栈使用

1. RabbitMQ配置类

java 复制代码
@Configuration
public class RabbitMQConfig {
    @Bean
    public Queue orderQueue() {
        return new Queue("order.queue", true, false, false);
    }
    
    @Bean
    public TopicExchange orderExchange() {
        return new TopicExchange("order.exchange");
    }
    
    @Bean
    public Binding orderBinding() {
        return BindingBuilder.bind(orderQueue())
                .to(orderExchange())
                .with("order.create");
    }
}

2. 消息生产者

java 复制代码
@Service
public class OrderProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void sendOrder(OrderMessage message) {
        // 设置消息确认机制
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (!ack) {
                // 消息发送失败,记录日志并重试
                log.error("消息发送失败: {}", cause);
                // 这里可以实现重试逻辑
            }
        });
        
        // 发送消息
        rabbitTemplate.convertAndSend("order.exchange", "order.create", message);
    }
}

解决方案

1. 幂等性设计

在业务逻辑层面实现幂等性,确保同一条消息多次处理不会产生不同结果。

java 复制代码
@Service
public class OrderService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @RabbitListener(queues = "order.queue")
    public void handleOrder(OrderMessage message) {
        String messageId = message.getMessageId();
        
        // 使用Redis检查消息是否已处理
        Boolean processed = redisTemplate.opsForValue().setIfAbsent(
            "message:processed:" + messageId, 
            "1", 
            24, 
            TimeUnit.HOURS
        );
        
        if (Boolean.TRUE.equals(processed)) {
            // 处理订单逻辑
            processOrder(message);
        } else {
            log.info("消息已处理过,跳过处理: {}", messageId);
        }
    }
    
    private void processOrder(OrderMessage message) {
        // 实际业务逻辑
        // 1. 检查订单是否已存在
        // 2. 创建订单
        // 3. 扣减库存
        // 4. 发送通知
    }
}

2. 数据库唯一约束

在数据库层面添加唯一约束,防止重复数据插入。

java 复制代码
@Entity
@Table(name = "orders", uniqueConstraints = {
    @UniqueConstraint(columnNames = {"order_id", "message_id"})
})
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String orderId;
    private String messageId;
    private BigDecimal amount;
    private String status;
    // 其他字段
}

3. 消息去重表

创建专门的消息去重表,记录已处理的消息ID。

java 复制代码
@Entity
@Table(name = "message_deduplication")
public class MessageDeduplication {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true)
    private String messageId;
    
    private String messageType;
    private Long createTime;
    private Long expireTime;
}

4. 手动确认机制

确保消费者在处理完成后再确认消息,避免消息重复投递。

java 复制代码
@Service
public class OrderService {
    @Autowired
    private MessageDeduplicationRepository deduplicationRepository;
    
    @RabbitListener(queues = "order.queue")
    public void handleOrder(OrderMessage message, Channel channel, 
                          @Headers Map<String, Object> headers) throws IOException {
        try {
            // 检查消息是否已处理
            if (isMessageProcessed(message.getMessageId())) {
                log.info("消息已处理过,跳过处理: {}", message.getMessageId());
                channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false);
                return;
            }
            
            // 处理订单逻辑
            processOrder(message);
            
            // 记录消息处理状态
            recordMessageProcessed(message.getMessageId());
            
            // 手动确认消息
            channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false);
            
        } catch (Exception e) {
            log.error("处理订单消息失败: {}", e.getMessage(), e);
            // 根据业务需求决定是否重新投递
            channel.basicNack((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false, false);
        }
    }
    
    private boolean isMessageProcessed(String messageId) {
        return deduplicationRepository.existsByMessageId(messageId);
    }
    
    private void recordMessageProcessed(String messageId) {
        MessageDeduplication record = new MessageDeduplication();
        record.setMessageId(messageId);
        record.setMessageType("order");
        record.setCreateTime(System.currentTimeMillis());
        record.setExpireTime(System.currentTimeMillis() + 24 * 60 * 60 * 1000);
        deduplicationRepository.save(record);
    }
}

最佳实践

  1. 消息ID唯一性:确保每条消息都有唯一的ID,可以使用UUID或业务相关的唯一标识。
  2. 过期时间设置:为去重记录设置合理的过期时间,避免数据无限增长。
  3. 异常处理:完善异常处理机制,确保消息在处理失败时能够正确重试或进入死信队列。
  4. 监控告警:建立消息处理的监控机制,及时发现和处理异常情况。
  5. 性能优化:合理使用Redis等缓存技术,提高消息去重的性能。

总结

RabbitMQ消息重复消费问题是在分布式系统中常见的技术挑战。通过幂等性设计、数据库唯一约束、消息去重表和手动确认机制等多种手段,可以有效解决消息重复消费的问题。在实际项目中,需要根据具体的业务场景和技术架构选择合适的解决方案,确保系统的稳定性和数据的一致性。

感谢读者观看!

相关推荐
一嘴一个橘子34 分钟前
mybatis - 动态语句、批量注册mapper、分页插件
java
组合缺一35 分钟前
Json Dom 怎么玩转?
java·json·dom·snack4
危险、1 小时前
一套提升 Spring Boot 项目的高并发、高可用能力的 Cursor 专用提示词
java·spring boot·提示词
kaico20181 小时前
JDK11新特性
java
钊兵1 小时前
java实现GeoJSON地理信息对经纬度点的匹配
java·开发语言
jiayong231 小时前
Tomcat性能优化面试题
java·性能优化·tomcat
秋刀鱼程序编程1 小时前
Java基础入门(五)----面向对象(上)
java·开发语言
纪莫1 小时前
技术面:MySQL篇(InnoDB的锁机制)
java·数据库·java面试⑧股
Remember_9931 小时前
【LeetCode精选算法】滑动窗口专题二
java·开发语言·数据结构·算法·leetcode
Filotimo_2 小时前
在java开发中,cron表达式概念
java·开发语言·数据库