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
);
关键幂等性机制
-
全局消息ID:
- 通过
message_log表的主键约束确保同一消息不重复处理。
- 通过
-
业务操作唯一性:
- 通过
unique_key唯一索引防止同一订单的相同操作重复执行(如重复取消)。
- 通过
-
状态机校验:
- 在业务处理前检查订单当前状态,确保操作合法(如只能取消已创建的订单)。
-
乐观锁控制:
- JPA的
@Version注解自动实现乐观锁,防止并发更新冲突。
- JPA的
-
原子性保证:
- 整个处理过程在Spring事务中执行,确保数据一致性。
异常处理与重试
-
失败消息处理:
- 处理失败时删除
message_log中的处理中记录,允许重试。 - 可配置重试次数和退避策略。
- 处理失败时删除
-
死信队列(DLQ):
- 多次重试失败的消息发送到DLQ,避免阻塞正常消息处理。
通过这套机制,即使Kafka消息重复投递,系统也能保证业务操作的幂等性和最终一致性。

浙公网安备 33010602011771号