Kafka假设producer端重复投递消息,consumer端如何保证幂等性?

以下是结合Kafka实现幂等消费的完整Java代码示例,包含消息表设计、状态机处理和幂等控制逻辑:

1. 消息模型与实体类

// 订单消息模型
public class OrderMessage {
    private String messageId;    // 全局唯一消息ID
    private String orderId;      // 业务订单ID
    private String actionType;   // 操作类型:CREATE/CANCEL/REFUND
    private String content;      // 消息内容
    private LocalDateTime timestamp; // 时间戳
    
    // getters/setters
}

// JPA实体:消息日志表
@Entity
@Table(name = "message_log")
public class MessageLog {
    @Id
    private String messageId;
    
    private String orderId;
    private String actionType;
    
    @Enumerated(EnumType.STRING)
    private MessageStatus status; // PROCESSING, SUCCESS, FAILURE
    
    private LocalDateTime createTime;
    
    // 唯一约束:同一订单的同一操作只能执行一次
    @NaturalId
    @Column(unique = true)
    private String uniqueKey; // 格式:orderId:actionType
    
    public MessageLog() {}
    
    public MessageLog(String messageId, String orderId, String actionType, MessageStatus status) {
        this.messageId = messageId;
        this.orderId = orderId;
        this.actionType = actionType;
        this.status = status;
        this.createTime = LocalDateTime.now();
        this.uniqueKey = orderId + ":" + actionType;
    }
    
    // getters/setters
}

// 订单实体
@Entity
@Table(name = "orders")
public class Order {
    @Id
    private String orderId;
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status; // CREATED, CANCELLED, PAID, REFUNDED
    
    private String content;
    
    @Version
    private Integer version;
    
    private LocalDateTime createTime;
    
    // getters/setters
}

2. 消息处理服务

@Service
@Transactional
public class OrderMessageService {
    @Autowired
    private MessageLogRepository messageLogRepository;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    // 处理Kafka消息
    public void processOrderMessage(ConsumerRecord<String, String> record) {
        try {
            // 反序列化消息
            OrderMessage message = deserializeMessage(record.value());
            
            // 幂等性检查
            if (!ensureIdempotency(message)) {
                log.info("消息已处理,跳过:{}", message.getMessageId());
                return;
            }
            
            // 根据操作类型执行不同业务逻辑
            switch (message.getActionType()) {
                case "CREATE":
                    handleCreateOrder(message);
                    break;
                case "CANCEL":
                    handleCancelOrder(message);
                    break;
                case "PAY":
                    handlePayOrder(message);
                    break;
                default:
                    log.warn("未知操作类型:{}", message.getActionType());
            }
            
            // 提交偏移量(手动提交模式下)
            commitOffset(record);
        } catch (Exception e) {
            log.error("处理消息失败:{}", record.value(), e);
            // 可实现重试或DLQ(死信队列)逻辑
            sendToDlq(record);
        }
    }
    
    // 确保幂等性
    private boolean ensureIdempotency(OrderMessage message) {
        // 检查消息是否已处理
        if (messageLogRepository.existsById(message.getMessageId())) {
            return false;
        }
        
        // 检查订单操作是否重复(如重复取消)
        String uniqueKey = message.getOrderId() + ":" + message.getActionType();
        if (messageLogRepository.existsByUniqueKey(uniqueKey)) {
            log.info("订单已执行相同操作,跳过:{} {}", 
                    message.getOrderId(), message.getActionType());
            return false;
        }
        
        // 记录消息处理中
        MessageLog logEntry = new MessageLog(
            message.getMessageId(),
            message.getOrderId(),
            message.getActionType(),
            MessageStatus.PROCESSING
        );
        messageLogRepository.save(logEntry);
        return true;
    }
    
    // 处理创建订单
    private void handleCreateOrder(OrderMessage message) {
        // 检查订单是否已存在
        if (orderRepository.existsById(message.getOrderId())) {
            log.info("订单已存在,忽略重复创建:{}", message.getOrderId());
            return;
        }
        
        // 创建新订单
        Order order = new Order();
        order.setOrderId(message.getOrderId());
        order.setStatus(OrderStatus.CREATED);
        order.setContent(message.getContent());
        order.setCreateTime(LocalDateTime.now());
        
        orderRepository.save(order);
        log.info("创建订单成功:{}", message.getOrderId());
    }
    
    // 处理取消订单
    private void handleCancelOrder(OrderMessage message) {
        // 获取订单并检查状态
        Order order = orderRepository.findById(message.getOrderId())
            .orElseThrow(() -> new IllegalArgumentException("订单不存在:" + message.getOrderId()));
        
        // 状态校验(只能取消已创建的订单)
        if (order.getStatus() != OrderStatus.CREATED) {
            log.info("订单状态不合法,忽略取消操作:{},当前状态:{}", 
                    message.getOrderId(), order.getStatus());
            return;
        }
        
        // 更新订单状态(JPA版本控制自动处理乐观锁)
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);
        log.info("取消订单成功:{}", message.getOrderId());
    }
    
    // 其他操作处理方法...
}

3. Kafka消费者配置

@Configuration
public class KafkaConsumerConfig {
    @Value("${kafka.bootstrap-servers}")
    private String bootstrapServers;
    
    @Value("${kafka.group-id}")
    private String groupId;
    
    @Bean
    public ConsumerFactory<String, String> consumerFactory() {
        Map<String, Object> configProps = new HashMap<>();
        configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        configProps.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        
        // 关键配置:关闭自动提交,手动控制偏移量
        configProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        
        // 确保消费顺序
        configProps.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 1);
        
        return new DefaultKafkaConsumerFactory<>(configProps);
    }
    
    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, String> 
            kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, String> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        
        // 配置手动提交偏移量
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
        
        return factory;
    }
}

4. 消费者监听

@Component
public class OrderMessageConsumer {
    @Autowired
    private OrderMessageService messageService;
    
    @KafkaListener(topics = "${kafka.topic.order}", containerFactory = "kafkaListenerContainerFactory")
    public void listen(ConsumerRecord<String, String> record, Acknowledgment ack) {
        try {
            messageService.processOrderMessage(record);
            // 手动确认偏移量(仅在处理成功后提交)
            ack.acknowledge();
        } catch (Exception e) {
            // 异常处理逻辑(重试或DLQ)
            log.error("消费消息失败:{}", record.value(), e);
        }
    }
}

5. 数据库表结构(MySQL)

CREATE TABLE message_log (
  message_id VARCHAR(32) PRIMARY KEY,
  order_id VARCHAR(32) NOT NULL,
  action_type VARCHAR(20) NOT NULL,
  status ENUM('PROCESSING', 'SUCCESS', 'FAILURE') NOT NULL,
  create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  unique_key VARCHAR(64) UNIQUE  -- 确保同一订单的同一操作只能执行一次
);

CREATE TABLE orders (
  order_id VARCHAR(32) PRIMARY KEY,
  status ENUM('CREATED', 'CANCELLED', 'PAID', 'REFUNDED') NOT NULL,
  content TEXT,
  version INT DEFAULT 0,
  create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

关键幂等性机制

  1. 全局消息ID

    • 通过message_log表的主键约束确保同一消息不重复处理。
  2. 业务操作唯一性

    • 通过unique_key唯一索引防止同一订单的相同操作重复执行(如重复取消)。
  3. 状态机校验

    • 在业务处理前检查订单当前状态,确保操作合法(如只能取消已创建的订单)。
  4. 乐观锁控制

    • JPA的@Version注解自动实现乐观锁,防止并发更新冲突。
  5. 原子性保证

    • 整个处理过程在Spring事务中执行,确保数据一致性。

异常处理与重试

  1. 失败消息处理

    • 处理失败时删除message_log中的处理中记录,允许重试。
    • 可配置重试次数和退避策略。
  2. 死信队列(DLQ)

    • 多次重试失败的消息发送到DLQ,避免阻塞正常消息处理。

通过这套机制,即使Kafka消息重复投递,系统也能保证业务操作的幂等性和最终一致性。

posted @ 2025-06-18 15:27  认真的刻刀  阅读(82)  评论(0)    收藏  举报