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

感谢读者观看!

相关推荐
辣机小司2 小时前
【踩坑记录:EasyExcel 生产级实战:策略模式重构与防御性导入导出校验指南(实用工具类分享)】
java·spring boot·后端·重构·excel·策略模式·easyexcel
醒过来摸鱼2 小时前
Spring Cloud Gateway
java·spring·spring cloud
2501_944441752 小时前
Flutter&OpenHarmony商城App消息通知组件开发
java·javascript·flutter
we1less2 小时前
[audio] AudioTrack (四) getOutputForAttr 分析
android·java
计算机毕设指导62 小时前
基于微信小程序的博物馆文创系统【源码文末联系】
java·spring boot·微信小程序·小程序·tomcat·maven·intellij-idea
我爱学习好爱好爱2 小时前
Prometheus监控栈 监控tomcat和消息队列
消息队列·tomcat·prometheus
嘉禾望岗5032 小时前
Spark-Submit参数介绍及任务资源使用测试
大数据·分布式·spark
后端小张2 小时前
【JAVA 进阶】Spring Boot自动配置详解
java·开发语言·人工智能·spring boot·后端·spring·spring cloud