接口幂等方案总结
接口幂等方案总结
1. 什么是幂等性(Idempotency)?
📝 通俗解释
“按一次和按一百次效果一样。”
电梯按钮,你按一下是去 10 楼,狂按 100 下还是去 10 楼。不会因为你多按了几下就带你去 1000 楼。
- 定义:一个操作无论执行多少次,产生的结果和副作用都是相同的。
- 数学表达:
f(f(x)) = f(x)。 - HTTP 方法:
GET:幂等(读取数据)。PUT:幂等(更新资源,多次更新结果一致)。DELETE:幂等(删除资源,多次删除结果一致)。POST:不幂等(创建资源,多次提交可能创建多个)。
2. 为什么需要幂等性?
📝 通俗解释
因为网络不可靠,手也会抖。
你点支付,网络卡了,没反应,你又点了一次。
如果没有幂等性,你的钱就被扣了两次。
系统得知道,这第二次点击是“重试”,不是“新订单”。
- 网络波动:客户端请求发出但响应丢失,客户端重试。
- 用户重复操作:用户快速点击提交按钮。
- MQ 重复消费:消息中间件的 At-Least-Once 机制。
- 微服务重试:RPC 框架(如 Dubbo、Feign)的自动重试机制。
3. 幂等性的核心思想是什么?
📝 通俗解释
识别“老面孔”。
- 一锁:先拦住(别挤,一个个来)。
- 二判:查户口(这事儿办过没?)。
- 三更新:没办过才办,办完了记下来。
- 一锁、二判、三更新。
- 核心是唯一标识(Unique ID)和状态检查。
4. 常见的幂等性解决方案有哪些?
📝 通俗解释
- 数据库死磕:唯一索引(谁也别想插重复的)。
- Redis 挡箭牌:Token 令牌(没牌子不让进)。
- 状态机:严格流程(付过款的不能再付)。
- 数据库唯一索引(Unique Key)。
- Token 机制(防重令牌)。
- 乐观锁(Optimistic Locking)。
- 悲观锁(Pessimistic Locking / Select for update)。
- 分布式锁(Redis/Zookeeper)。
- 状态机(State Machine)。
- 去重表(De-duplication Table)。
5. 详细讲讲 Token 机制(防重令牌)的流程?
📝 通俗解释
像去银行办事先取号。
- 取号:先去柜台领个号(Token)。
- 办事:拿着号去窗口办事。
- 销号:柜员看这号有效,给办了,然后把号撕了。
- 重复:你拿着撕了的号(或者复印件)再去办?柜员不认。
- 获取 Token:客户端先请求服务端获取一个全局唯一的 Token(存入 Redis,设置过期时间)。
- 携带 Token:客户端发起业务请求时,将 Token 放入 Header 或参数中。
- 校验 Token:服务端接收请求,去 Redis 检查 Token 是否存在。
- 如果存在:执行业务,并删除 Token(原子操作,使用 Lua 脚本)。
- 如果不存在:说明是重复请求,直接返回错误。
- 注意:必须保证“检查+删除”的原子性(
GET+DEL不安全,需用 Lua 或DEL返回值判断)。
6. 数据库唯一索引如何实现幂等?
📝 通俗解释
像身份证号。
每个人身份证号唯一。
你想再造一个身份证号一样的人?派出所(数据库)系统直接报错,不让你造。
- 原理:利用数据库的主键或唯一索引约束。
- 场景:新增操作(Insert)。
- 实现:比如订单号
order_id设为唯一索引,重复插入会报DuplicateKeyException,捕获该异常并返回成功或提示重复即可。
7. 乐观锁如何实现幂等?
📝 通俗解释
“核对暗号”。
更新时带上版本号(version=1)。
“我是针对 version=1 修改的。”
如果数据库里已经是 version=2 了,说明别人改过了,你的请求就无效了。
- 原理:利用版本号(version)机制。
- SQL:
UPDATE table SET count = count + 1, version = version + 1 WHERE id = 1 AND version = 1; - 结果:如果第一次执行成功,
version变了,第二次执行WHERE version = 1就不匹配,更新行数为 0,表示重复操作。
8. 悲观锁(Select for Update)如何实现幂等?
📝 通俗解释
“霸占茅坑”。
进门先把门锁死(For Update)。
确认没人用过(检查状态)。
用完冲水走人(提交事务)。
别人想进?门口排队等着。
- 原理:利用数据库行锁。
- 流程:
- 开启事务。
SELECT * FROM table WHERE id = 1 FOR UPDATE;(锁住该行)。- 检查状态(如:是否已处理)。
- 如果未处理 -> 执行业务 -> 更新状态。
- 提交事务。
- 缺点:性能差,容易死锁,不推荐高并发场景。
9. 分布式锁如何实现幂等?
📝 通俗解释
跨部门抢会议室。
去前台(Redis)登记:“我要用会议室 A”。
前台看 A 空着,给你钥匙(加锁成功)。
别人再来?“有人用了,你回去吧”(加锁失败)。
- 原理:在执行业务前,先去 Redis/ZK 抢锁(Key 通常是业务唯一 ID)。
- 流程:
SETNX lock_key value。- 成功 -> 执行业务 -> 释放锁。
- 失败 -> 说明正在处理或已处理,直接返回。
- 优化:业务执行完后,最好不要立即释放锁,而是等锁自动过期,或者记录“已处理”状态到数据库,防止锁释放后还没落库时的并发问题。
10. 状态机如何实现幂等?
📝 通俗解释
“单行道”。
订单状态:未支付 -> 已支付 -> 发货。
你想把“已支付”变成“已支付”?或者“发货”变成“已支付”?
没门!路不通。
- 原理:业务流转有严格的状态顺序。
- 场景:订单状态流转(待支付 -> 已支付 -> 发货)。
- SQL:
UPDATE order SET status = 'PAID' WHERE id = 1 AND status = 'UNPAID'; - 效果:只有当前状态是
UNPAID时才能更新,重复请求无法满足条件。
11. 什么是去重表?
📝 通俗解释
“记账本”。
专门有个本子记录谁来过。
每次办事先查本子,有名字就不办了。
办完把名字记上去。
- 原理:专门建一张表用于记录已处理的请求 ID(或业务 ID)。
- 流程:
- 开启事务。
- 插入去重表(利用唯一索引)。
- 执行业务逻辑。
- 提交事务。
- 优点:强一致性,利用数据库事务。
12. 支付接口如何保证幂等?
📝 通俗解释
只要涉及到钱,必须万无一失。
- 查:这单付过没?
- 锁:正付着呢,别捣乱。
- 核:再去第三方问问。
- 记:付完了赶紧记下来。
- 请求端:生成唯一
request_id或biz_id。 - 服务端:
- 查单:先用
biz_id查库,看是否已支付。 - 已支付:直接返回支付成功。
- 未支付:
- 使用分布式锁(锁
biz_id)。 - 再次查单(Double Check)。
- 调用三方支付。
- 更新状态(状态机控制)。
- 使用分布式锁(锁
- 查单:先用
13. 消息队列(MQ)消费如何保证幂等?
📝 通俗解释
快递员送包裹。
有时候快递员忘了自己送过(MQ 重发),又送了一次。
你(消费者)得看单号(业务 ID)。
“这单号我签收过了,别给我了。”
- 问题:生产者重发或消费者提交 Offset 失败导致重复消费。
- 方案:
- 业务 ID 去重:消息体中带唯一业务 ID。
- 去重表/Redis:消费前先查去重表或 Redis 是否已处理。
- DB 唯一索引:直接 Insert,依赖数据库报错。
- 乐观锁:带版本号更新。
14. Token 机制中,先删除 Token 还是后删除 Token?
📝 通俗解释
必须先撕票(删 Token)!
- 后撕票:办完事再撕。万一办完事网络断了没撕成,下次拿着票还能再办一次(重复执行)。
- 先撕票:进门就撕。万一办砸了(业务失败),票也没了。大不了让你重新取个号(重试获取 Token),绝对不会重复办。
- 推荐:先删除 Token(或者原子性地 检查+删除)。
- 原因:
- 如果先执行业务,后删除 Token:业务执行完,Token 还没删,网络断了,客户端重试,Token 还在,导致重复执行。
- 如果先删除 Token,后执行业务:Token 删了,业务报错,客户端重试获取不到 Token,虽然业务没成功,但保证了不会重复执行(此时客户端需要重新获取 Token)。
- 最佳实践:使用 Lua 脚本保证
GET+DEL原子性。
15. “一锁、二判、三更新”是什么意思?
📝 通俗解释
标准动作三部曲。
- 锁住门。
- 看一眼做没做过。
- 没做过就做,做完记下来。
- 一锁:加锁(分布式锁或本地锁),防止并发。
- 二判:判断状态,是否已经处理过。
- 三更新:执行业务更新,并更新状态。
16. 对外提供的 API 接口如何设计幂等?
📝 通俗解释
强制要求带“身份证”。
别光说“我要转账”,得说“我是来源 A,这是我的第 123 号请求(source + seq)”。
我记着呢,123 号转过了,不理你。
- 必传参数:要求调用方传入
source(来源)和seq(序列号/唯一 ID)。 - 存储:服务端记录
source + seq的处理结果。 - 逻辑:收到请求先查
source + seq,有结果直接返回,无结果则处理。
17. GET 请求需要做幂等吗?
📝 通俗解释
不需要。
GET 是“看一眼”。
你看一眼美女,再看一眼,美女还是那个美女,不会变(虽然她可能会瞪你)。
只要你不动手(不修改数据),看多少次都行。
- 不需要。GET 语义本身就是幂等的(读取操作),天然安全。但要注意不要在 GET 请求中做数据修改操作。
18. 分布式环境下,生成唯一 ID 的方案有哪些?
📝 通俗解释
- UUID:乱码字符串,太长太乱,数据库不喜欢。
- 雪花算法:有序数字,带时间戳和机器号,好用。
- Redis:找个中心计数器,1, 2, 3... 递增。
- UUID:简单但无序,性能差(页分裂)。
- Snowflake(雪花算法):有序、高性能,依赖机器时钟。
- Redis Incr:简单、高性能,依赖 Redis。
- 数据库号段模式:美团 Leaf。
19. Redis 实现幂等的原子性问题?
📝 通俗解释
“查”和“改”必须连贯。
你刚查完“没人”,正要“占座”,别人插进来了。
用 Lua 脚本,把“查”和“占”打包成一个动作,谁也插不进。
- 问题:
if (get(key) == null) { set(key); }不是原子的。 - 解决:
- 使用
SETNX(Set If Not Exists)。 - 使用 Lua 脚本将检查和设置合并。
- 使用
20. 这里的“防重”和“幂等”有区别吗?
📝 通俗解释
- 防重:拦截。你点太快了,第二次直接弹窗“别点了”。
- 幂等:包容。你点两次,我第一次给你办了,第二次告诉你“办好了”(哪怕其实没重办,但结果是对的)。
- 防重(Anti-Replay):防止重复提交(如用户多次点击),通常返回“请勿重复操作”。侧重于拦截。
- 幂等(Idempotency):无论执行多少次,结果一致。侧重于结果正确性(第二次请求可能返回第一次成功的结果,而不是报错)。
- 关系:幂等包含了防重,防重是实现幂等的一种手段。
21. 微服务调用链中,下游超时,上游重试,如何保证幂等?
📝 通俗解释
传递“令牌”。
A 叫 B,B 叫 C。
A 给 B 一个trace_id。
A 重试叫 B 时,还是那个trace_id。
B 看到还是那个 ID,就知道是重试,也拿着这个 ID 去叫 C。
C 看到 ID 没变,就知道不用重做。
- 上游:传递全局唯一
trace_id或biz_id。 - 下游:使用上述提到的幂等方案(去重表、Redis、状态机等)进行校验。
22. 数据库分库分表后,如何实现唯一约束幂等?
📝 通俗解释
局部不管用,要靠全局。
分家了,各自管各自的账本(分表),没法查重。
得有个总账本(去重路由表)或者全局唯一的号(雪花算法)。
- 问题:分表后,局部主键不唯一。
- 解决:
- 使用全局唯一 ID(雪花算法)。
- 建立一张全局的“去重路由表”(映射 ID 到具体的分表)。
23. 高并发下,Token 机制的性能瓶颈在哪?
📝 通俗解释
Redis 会累死。
所有人都去 Redis 领号、验号。
Redis 再快也怕人多。
解决办法:搞 Redis 集群,或者先在本地(本地缓存)挡一波。
- 瓶颈:Redis 的网络 I/O 和单点压力。
- 优化:
- Redis 集群。
- 本地缓存(Guava Cache)+ Redis(多级缓存),但要注意一致性问题。
- 既然是高并发,通常配合限流(Rate Limiting)。
24. 为什么 POST 不幂等?
📝 通俗解释
POST 是“新建”。
你投递(POST)一封信,就多一封信。
投递两次,就多两封。
想幂等?信封上写个编号(唯一 ID)。
- 语义:POST 用于创建资源。
- 现象:两次 POST
/users可能会创建两个不同的 User(ID 不同)。 - 如何变幂等:POST 时带上自定义的唯一 ID,服务端根据 ID 判重。
25. 乐观锁在高并发下的缺点?
📝 通俗解释
“大家都在改,谁也改不成。”
100 个人同时抢购一件商品。
大家都拿到了 version=1。
第一个人改成了 version=2。
剩下 99 个人提交时发现 version 不对,全失败了。
白忙活一场(CPU 浪费)。
- ABA 问题(CAS 常见,但数据库版本号通常递增,无此问题)。
- 重试成本:大量并发更新同一行,冲突率高,导致大量重试,浪费 CPU 和 DB 资源。
- 解决:高并发写入推荐用 MQ 削峰,或者悲观锁/分布式锁串行化。
26. 幂等性设计对系统复杂度的影响?
📝 通俗解释
麻烦但必要。
本来一句 SQL 搞定。
现在要:加 Redis、写 Lua 脚本、查去重表、处理分布式锁...
代码量翻倍。但为了钱(数据一致性),值!
- 增加复杂度:需要引入额外存储(Redis/去重表)、分布式锁、状态检查逻辑。
- 权衡:核心资金、交易链路必须做;非核心查询、日志记录可不做或弱一致。
27. 定时任务(Job)需要做幂等吗?
📝 通俗解释
要!
尤其是有两台机器跑任务时。
可能会“脑裂”,两台机器同时跑同一个任务。
必须加锁(分布式锁),谁抢到谁跑。
- 需要。
- 场景:分布式调度(如 Elastic-Job, XXL-Job)可能因为网络分区导致任务被分发到两台机器执行。
- 方案:分布式锁、数据库唯一约束(JobLog 表)。
28. 前端如何配合防重?
📝 通俗解释
“物理防挂”。
点完按钮直接变灰(Disable)。
或者转圈圈(Loading)。
防止手抖党狂点。
但防不住黑客(直接调接口),后端还得兜底。
- 按钮置灰:点击后立马 Disable 按钮。
- Loading 遮罩:请求结束前不允许操作。
- PRG 模式:Post/Redirect/Get,提交后跳转,防止刷新页面重复提交。
- 注意:前端防重只能防“君子”,防不了“小人”(直接调接口),后端必须兜底。
29. 幂等 Key 如何选取?
📝 通俗解释
找“身份证号”。
订单号、支付流水号。
如果没有?就把参数拼起来算个 MD5(指纹)。
- 业务属性:
order_id、payment_id。 - 组合属性:
user_id + activity_id(每个用户只能参加一次活动)。 - 摘要:
MD5(param1 + param2 + ...)(全参数摘要)。
30. 插入操作(Insert)和更新操作(Update)的幂等区别?
📝 通俗解释
- Insert:怕重复生孩子。靠唯一索引、Token。
- Update:怕改乱了。靠状态机、乐观锁。
- Insert:主要靠唯一索引、Token。
- Update:主要靠状态机、乐观锁、分布式锁。
31. 如果 Redis 挂了,Token 机制怎么办?
📝 通俗解释
B 计划。
既然大管家(Redis)挂了。
要么直接关门(熔断)。
要么退回到原始的手工记账(数据库去重),虽然慢点,但能用。
- 降级:
- 降级为数据库去重(性能下降)。
- 直接熔断,拒绝请求,保护数据库。
- 依赖 Redis 高可用架构(Sentinel/Cluster)。
32. 业务代码中,幂等逻辑写在哪里?
📝 通俗解释
- 保安亭(Filter/AOP):通用的逻辑(如验 Token)放门口,省事。
- 柜台(Service):复杂的逻辑(如状态机、业务校验)放里面,灵活。
- Filter/Interceptor:统一拦截(基于 Token 或 Header)。
- AOP:自定义注解
@Idempotent,加在 Controller 方法上。 - Service 层:业务逻辑内部显式调用(更灵活,可控制粒度)。
