文章目录
- 前言
- 一、消息重复的根源分析
-
- [1.1 重复消费的发生场景](#1.1 重复消费的发生场景)
- [1.2 各类重复场景详解](#1.2 各类重复场景详解)
- 二、幂等性三大设计方案
-
- [2.1 方案一:业务唯一键去重](#2.1 方案一:业务唯一键去重)
- [2.2 方案二:数据库唯一约束](#2.2 方案二:数据库唯一约束)
- [2.3 方案三:业务逻辑本身幂等](#2.3 方案三:业务逻辑本身幂等)
- 三、三种方案的对比与选择
-
- [3.1 方案对比矩阵](#3.1 方案对比矩阵)
- [3.2 选型建议](#3.2 选型建议)
- [四、Kafka Exactly-Once语义](#四、Kafka Exactly-Once语义)
-
- [4.1 端到端的Exactly-Once](#4.1 端到端的Exactly-Once)
- [4.2 生产者端幂等](#4.2 生产者端幂等)
- [4.3 消费者端事务](#4.3 消费者端事务)
- 五、最佳实践与踩坑经验
-
- [5.1 幂等设计Checklist](#5.1 幂等设计Checklist)
- [5.2 常见问题与解决方案](#5.2 常见问题与解决方案)
- [5.3 不同业务场景的幂等策略](#5.3 不同业务场景的幂等策略)
- 六、总结与要点
-
- [6.1 幂等性核心要点](#6.1 幂等性核心要点)
- [6.2 常见问题](#6.2 常见问题)
- [6.3 加分点](#6.3 加分点)
- 写在最后:
前言
在分布式系统中,消息重复是一个无法完全避免的现实------网络重试导致生产者重复发送、消费者重平衡导致重新消费、宕机恢复后重新拉取...这些问题都会导致同一条消息被处理多次。
对于某些业务(如日志分析),重复可能无关紧要;但对于交易、支付等核心场景,重复消费意味着订单重复创建、优惠券重复发放,这是绝对不能接受的。
本文将深入剖析消息幂等性的方方面面:
- 重复的根源:为什么Kafka会重复消费?
- 幂等设计三大方案:业务唯一键、数据库约束、业务逻辑幂等
- Exactly-Once语义:从消息队列到业务系统的全链路幂等
- 最佳实践:不同场景如何选择合适的幂等方案
一、消息重复的根源分析
1.1 重复消费的发生场景
消费者端重复
处理消息
提交offset前宕机
重启后重新拉取
重复处理
Broker端重复
Leader写入
同步Follower前宕机
新Leader选举
消息未同步,消费者重新拉取
生产者端重复
发送消息
网络超时
重试发送
Broker收到两条相同消息
1.2 各类重复场景详解
| 发生环节 | 具体场景 | 触发原因 | 概率 |
|---|---|---|---|
| 生产者 | 网络超时重试 | 生产者发送后未收到ACK | 中等 |
| Broker | Leader切换 | 消息未同步到所有副本 | 低 |
| 消费者 | Rebalance | 重平衡后分区重新分配 | 中等 |
| 消费者 | 宕机恢复 | offset未及时提交 | 高 |
二、幂等性三大设计方案
2.1 方案一:业务唯一键去重
唯一键去重流程
否
是
收到消息
提取业务唯一键
如订单号
查询去重存储
Redis/DB
是否已处理?
执行业务逻辑
记录处理状态
返回成功
直接返回成功
忽略重复
适用场景:
- 业务自带唯一标识(订单号、流水号、支付ID)
- 可容忍短暂的不一致(去重记录过期后可能重复)
存储方案对比:
| 存储 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis | 性能高、支持过期 | 可能丢数据 | 高吞吐、允许短暂重复 |
| MySQL | 可靠、事务支持 | 性能较低 | 核心业务、严格去重 |
| 本地缓存 | 性能极高 | 分布式不共享 | 单机消费 |
2.2 方案二:数据库唯一约束
唯一约束流程
是
否
收到消息
提取消息唯一ID
或业务ID
尝试插入消费记录表
message_id唯一
插入成功?
执行业务逻辑
更新记录状态
提交事务
捕获唯一键冲突
记录已处理,忽略
消息唯一ID的生成:
java
message_id = topic + partition + offset // 天然唯一
// 或
message_id = UUID.randomUUID() // 生产者生成
消费记录表设计要点:
| 字段 | 作用 | 索引 |
|---|---|---|
| message_id | 消息唯一标识 | 唯一索引 |
| topic | 主题 | 普通索引 |
| partition | 分区 | 普通索引 |
| offset | 偏移量 | 普通索引 |
| status | 处理状态 | 普通索引 |
| create_time | 创建时间 | 普通索引 |
2.3 方案三:业务逻辑本身幂等
幂等操作示例
1
0
成功
忽略
更新操作
update order set status=PAID where id=123 and status=UNPAID
影响行数
首次更新
已更新过
插入操作
insert ignore into order values...
插入结果
首次插入
已存在
常见的幂等操作:
| 操作类型 | 非幂等方式 | 幂等方式 |
|---|---|---|
| 更新 | update set amount=amount+100 |
update set amount=100 where id=xx |
| 状态变更 | 直接更新 | 带条件更新:where status='OLD' |
| 插入 | insert |
insert ignore 或 on duplicate key |
| 删除 | delete |
delete where id=xx(本身就幂等) |
三、三种方案的对比与选择
3.1 方案对比矩阵
方案对比
业务唯一键
优点:简单、性能好
缺点:依赖Redis/DB
数据库约束
优点:可靠、事务支持
缺点:性能较低、需建表
业务幂等
优点:无额外存储
缺点:业务改造复杂
3.2 选型建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 交易核心 | 数据库唯一约束 | 强一致性,不丢不重 |
| 高吞吐场景 | Redis唯一键 + 业务幂等 | 性能优先,允许短暂不一致 |
| 已有幂等业务 | 业务逻辑幂等 | 无额外成本 |
| 复杂业务逻辑 | 组合方案 | 多层级保障 |
四、Kafka Exactly-Once语义
4.1 端到端的Exactly-Once
Exactly-Once架构
生产者
幂等发送
事务消息
Broker
去重存储
事务协调器
消费者
事务消费
幂等处理
4.2 生产者端幂等
| 配置 | 作用 | 说明 |
|---|---|---|
| enable.idempotence=true | 开启幂等 | 防止重试导致重复 |
| acks=all | 所有副本确认 | 保证不丢 |
| max.in.flight.requests.per.connection=5 | 允许并发 | 幂等时可大于1 |
幂等生产者原理:
- 每个生产者有唯一的Producer ID
- 每条消息有递增的Sequence Number
- Broker端根据<PID, Partition, Seq>去重
4.3 消费者端事务
事务消费流程
开启事务
poll消息
处理业务
提交offset
提交事务
处理失败
回滚事务
消息重新消费
事务消费的关键:
- 业务处理和offset提交在同一个事务中
- 要么都成功,要么都失败
- 需要Kafka 0.11+版本支持
五、最佳实践与踩坑经验
5.1 幂等设计Checklist
幂等设计要点
选择幂等键
业务天然ID
订单号/支付流水
组合键
topic+partition+offset
选择存储
Redis:高性能
MySQL:强一致
设置过期时间
根据业务需要
避免记录无限增长
监控告警
重复率监控
去重存储健康
5.2 常见问题与解决方案
| 问题 | 现象 | 解决方案 |
|---|---|---|
| Redis击穿 | 缓存过期后重复处理 | 设置合理的过期时间,加分布式锁 |
| 唯一键冲突 | 并发插入报错 | 捕获异常,重试或忽略 |
| 事务超时 | 长事务导致超时 | 拆分事务,异步补偿 |
| 存储单点 | Redis/DB宕机 | 高可用部署,降级方案 |
5.3 不同业务场景的幂等策略
| 业务类型 | 幂等策略 | 说明 |
|---|---|---|
| 订单创建 | 订单号唯一约束 | 数据库层保证 |
| 账户扣款 | 流水号+乐观锁 | 防止重复扣款 |
| 状态更新 | 条件更新 | where status=旧状态 |
| 积分发放 | 业务唯一键+Redis | 高吞吐场景 |
| 短信发送 | 业务幂等+去重表 | 防止重复发送 |
六、总结与要点
6.1 幂等性核心要点
幂等性保障体系
生产者
幂等发送
事务消息
消费者
业务唯一键
数据库约束
业务幂等
组合保障
Exactly-Once
6.2 常见问题
| 问题 | 回答要点 |
|---|---|
| 什么是幂等性? | 多次执行结果与一次执行相同 |
| Kafka为什么重复? | 生产者重试、Rebalance、宕机恢复 |
| 如何实现幂等? | 业务唯一键、数据库约束、业务幂等 |
| Redis去重会丢吗? | 可能,需设置合理过期时间 |
| Exactly-Once怎么实现? | 生产者幂等 + 事务 + 消费者幂等 |
6.3 加分点
- 幂等生产者的实现原理:Producer ID + Sequence Number
- 事务消息的两阶段提交:协调器的作用
- Kafka 0.11+的Exactly-Once语义:事务API的使用
- 与RocketMQ的对比:RocketMQ的事务消息设计
- 分布式事务的最终一致性:本地消息表方案
写在最后:
消息幂等性是分布式系统的必修课。没有绝对的"不重复",只有通过合理的设计将重复的影响降到最低。理解重复的根源,选择合适的幂等方案,才能构建真正可靠的消息系统。