一、先搞懂核心概念
1. 什么是接口幂等性
同一请求重复发起N次,业务最终结果和只请求1次完全一致,不会产生脏数据、重复扣款、重复下单、重复消息消费等副作用。
2. 产生重复请求的常见场景
- 前端重复点击提交按钮、网络卡顿重复提交表单;
- 服务内部重试机制(接口超时自动重试);
- MQ消息重复投递、消费者重复消费;
- 网关/负载均衡转发异常造成重复调用;
3. 两类高危操作
- INSERT:重复插入产生多条重复业务数据;
- 带计算的UPDATE:
set num = num + 1、余额扣减,重复执行数据错乱;
简单静态更新set status=1 where id=1天然幂等,无需额外处理。
二、8种幂等实现方案(按落地常用程度排序)
方案1:唯一索引(数据库兜底,最推荐插入场景)
实现
给业务唯一标识(订单号、流水号、设备编码)建立唯一索引/联合唯一索引。
ALTERTABLE`order`ADDUNIQUEKEYuk_order_no(`order_no`);流程:
- 执行insert;
- 捕获唯一键冲突异常
DuplicateKeyException; - 捕获到异常直接返回成功,保证幂等。
优点
- 数据库原生支持,强一致性,并发完全无问题;
- 低成本,不用额外中间件。
缺点
- 仅适用于新增场景;更新场景不适用;
- 大量冲突抛异常,高并发下数据库压力略大。
适用场景
下单、支付流水、新增记录等INSERT业务。
方案2:防重表(多业务隔离,不想主表加唯一索引时用)
实现
单独建一张防重记录表,只存业务唯一ID,加唯一索引;防重表与业务表同一事务。
表结构示例:id, biz_no, create_time,biz_no唯一索引。
流程:
- 事务内先插入防重表;
- 插入成功 → 执行业务操作;
- 插入冲突 → 回滚事务,返回成功。
优点
- 不污染主业务表索引,多业务共用一套防重逻辑;
缺点
- 多一张表,增加数据库IO;
- 仅支持同库事务,跨库无法使用。
适用场景
主表不方便加唯一索引、多类型业务统一防重。
方案3:insert前先select(简易方案,并发有漏洞)
实现
插入前根据唯一编码查询,不存在再insert,存在直接返回。
Orderorder=mapper.selectByOrderNo(orderNo);if(order==null){mapper.insert(order);}致命缺陷
高并发下两个请求同时查询都为空,会同时插入,产生重复数据。
优化
必须搭配锁/唯一索引一起使用,不能单独做幂等。
适用场景
低并发、内部管理后台、非核心业务。
方案4:状态机控制(更新订单/流程类专用)
实现
业务单据存在固定流转状态,更新时带上原始状态作为条件,只允许正向流转。
例:订单状态 1待支付 → 2已支付 → 3已完成
-- 只有状态=待支付时才更新为已支付update`order`setstatus=2whereid=100andstatus=1;流程:
- 执行更新SQL;
- 判断受影响行数:
- 行数=1:正常更新;
- 行数=0:重复请求,直接返回成功。
优点
无额外组件,性能好;天然防止逆向重复操作。
缺点
仅适用于有状态流转的单据(订单、审批单),通用场景不适用。
适用场景
支付回调、订单状态变更、流程审批接口。
方案5:乐观锁version版本号(更新类并发幂等,高并发首选)
实现
表增加version版本字段,更新时携带当前版本号,版本匹配才更新。
-- 查询selectid,amount,versionfromuserwhereid=100;-- 更新updateusersetamount=amount-100,version=version+1whereid=100andversion=1;- 更新行数=0:版本已变,重复请求,返回成功;
- 更新行数=1:正常执行业务。
优点
无数据库行锁,并发性能远高于悲观锁;不阻塞线程。
缺点
存在自旋重试,极端高并发下大量更新失效。
适用场景
余额扣减、库存扣减、积分更新等计算型UPDATE。
方案6:悲观锁 for update(强一致性,低并发资金场景)
实现
InnoDB行锁,事务内先锁定该行再操作:
select*fromaccountwhereid=100forupdate;同一行同一时间只允许一个事务持有锁,其他请求阻塞等待,避免重复扣减。
注意
查询条件必须是主键/唯一索引,否则锁整张表,性能灾难。
优点
数据绝对安全,不会超扣、重复扣款。
缺点
并发量大时大量请求阻塞,接口吞吐量暴跌。
适用场景
支付、转账、大额资金等对数据一致性要求极高、并发不高的场景。
方案7:Redis分布式锁(分布式系统通用,接口入口拦截)
主流实现
Redisson(推荐)、Redis SETNX + 过期时间,避免死锁。
流程:
- 用业务唯一号(orderNo)作为锁key;
- 抢占锁成功执行业务;
- 抢占失败直接返回成功,实现幂等;
- 锁设置过期时间,防止服务宕机死锁。
优点
分布式场景通用,性能高于数据库锁;可统一在网关/拦截器做全局防重。
缺点
依赖Redis,存在缓存与数据库一致性问题;锁超时需合理把控。
适用场景
分布式微服务、MQ消费、外部第三方接口回调。
方案8:Token令牌机制(前端表单重复提交专用)
实现(两次请求)
- 进入页面/打开表单,前端先调用接口获取全局唯一防重Token,存入Redis;
- 用户提交表单携带Token;
- 服务端先删除Redis中Token:
- 删除成功:执行业务;
- 删除失败:Token不存在,判定重复提交,直接返回。
优点
专门拦截前端重复点击,和数据库解耦。
缺点
需要两次接口交互,仅解决前端重复提交,无法处理服务重试、MQ重复消息。
适用场景
页面表单提交、后台新增/编辑页面。
三、方案分层选型(面试加分总结)
- 前端层:Token令牌,拦截页面重复点击;
- 网关层:Redis分布式锁,全接口统一防重;
- 数据库新增:唯一索引 / 防重表;
- 数据库更新(状态流转):状态机;
- 数据库更新(数值计算):乐观锁version(高并发)、悲观锁for update(资金强一致);
- 分布式/MQ场景:Redis分布式锁兜底。
实际项目不会只用单一方案,采用多层组合防护:前端Token防重复点击 + 网关Redis分布式锁拦截重复调用 + 数据库唯一索引/乐观锁兜底,层层保障接口幂等。