大家好,我是苏三,又跟大家见面了。
前言
最近有位小伙伴去美团面试,被问到一个高频题:"高并发场景下,如何保证接口的幂等性?"
他答出了"唯一索引"和"token机制",但被追问"如果重复请求并发执行,两个请求同时发现token有效怎么办?"
他就卡住了。
事实上,幂等性设计是高并发系统中最容易被忽视、出事后果却最严重的环节之一。
今天就专门跟大家一起聊聊接口幂等性的话题,希望对你会有所帮助。
最近想快速提升项目实战能力(包含多个AI项目),或者最近找工作,或者想学习AI的小伙伴,可以看看下面👇🏻的这个链接(或许真的能够帮到你)susan.net.cn/project
一、什么是幂等性?
幂等(Idempotent):同一个接口,无论调用一次还是多次,对系统产生的副作用都是一样的。
举个反例:支付接口。如果用户点了一次"确认支付"按钮,前端因为网络超时重试了三次,结果银行扣了三次钱,这就是典型的非幂等。
幂等性要解决的核心问题:在网络抖动、用户误操作、消息队列重复消费、RPC重试等场景下,防止数据重复处理,保证业务数据最终一致。
二、高并发下幂等性的难点
在低并发场景,用唯一索引或悲观锁就能解决。
但高并发下,两个请求可能同时"查无记录",然后同时插入重复数据,形成"并发穿透"。

这就是高并发下幂等性设计的最大挑战:多个相同请求同时到达,基于"查询-插入"的判断会失效。
所以,我们不能靠"防君子不防小人"的检查,而要在架构层面建立可靠的防重机制。
三、常见幂等性方案
下面我们逐一剖析 6 种主流方案,每种都给出代码示例、优缺点和适用场景。
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 1. 唯一索引 | 数据库唯一约束 | 简单可靠 | 性能较低,不适用于分表 | 单库单表,对冲突容忍度低 |
| 2. Token 令牌 | 请求前获取token,执行时删除token | 无锁,适合分布式 | 需额外一次调用,需处理token失效 | 表单提交、敏感操作 |
| 3. 乐观锁 | 版本号更新 | 无锁,高并发支持好 | 只能防重复更新,不能防插入 | 更新类接口(如状态变更) |
| 4. 防重表 | 专用去重表+唯一索引 | 业务表无侵入 | 增加一次DB写操作 | 任意幂等场景,可灵活控制 |
| 5. 状态机 | 根据业务状态流转 | 天然幂等,语义清晰 | 局限性大,只适合有状态流转的业务 | 订单、工单生命周期 |
| 6. Redis 分布式锁 | 请求id加锁 | 性能高,分布式友好 | 需考虑锁超时、释放问题 | 高并发、对DB压力敏感 |
下面对每种方案做详细拆解。
四、方案详解
方案一:唯一索引(数据库层防重)
实现:在业务表上的幂等字段(如订单号、流水号)建立唯一索引。
重复插入时数据库会抛异常,应用层捕获后返回"请勿重复操作"。
ALTER TABLE order ADD UNIQUE INDEX uk_order_no (order_no);
try {
orderMapper.insert(order);
} catch (DuplicateKeyException e) {
return "订单号已存在,请勿重复提交";
}
优点 :绝对可靠,实现简单。
缺点 :每次插入都要写索引,高并发下性能下降;分库分表后唯一索引难以维护。
适用场景:单库单表、并发量不高、数据强一致要求场景。
方案二:Token机制(前端携带令牌)
流程:用户点提交前,先调用获取token接口 → 服务端生成token存入Redis → 提交请求时携带token → 服务端删除token并执行业务。
若第二次请求带相同token,删除失败(已不存在),直接拒绝。

@PostMapping("/pay")
public Result pay(@RequestParam String orderNo, @RequestParam String token) {
Boolean deleted = redisTemplate.delete("pay_token:" + token);
if (!deleted) {
return Result.error("请勿重复提交");
}
// 执行支付逻辑(幂等)
orderService.pay(orderNo);
return Result.success();
}
优点 :无锁,适合分布式,性能好。
缺点 :需要额外一次获取token的调用,可能增加网络开销。
适用场景:表单提交、重要操作(支付、下单)。
方案三:乐观锁(基于版本号)
适用:更新类操作,而不是插入。例如更新订单状态"从待支付→已支付"。
UPDATE order SET status = 'PAID', version = version + 1
WHERE order_no = #{orderNo} AND version = #{oldVersion};
int rows = orderMapper.updateByVersion(orderNo, oldVersion);
if (rows == 0) {
throw new BusinessException("订单已被更新,请刷新重试");
}
优点 :高并发下无锁等待,性能好。
缺点 :只适用于更新场景,不能用于新增。
适用场景:订单状态流转、库存扣减。
方案四:防重表(独立去重记录)
原理:在业务操作前,先插入一条记录到"去重表"(唯一索引),插入成功才执行业务;业务失败时删除记录。
利用数据库唯一索引保证同一请求id只能成功一次。
CREATE TABLE idempotent_record (
id BIGINT AUTO_INCREMENT,
request_id VARCHAR(64) NOT NULL,
biz_type VARCHAR(32),
PRIMARY KEY(id),
UNIQUE KEY uk_request_id (request_id)
);
@Transactional
public void createOrder(Order order, String requestId) {
// 1. 插入防重记录
try {
idempotentMapper.insert(requestId, "order_create");
} catch (DuplicateKeyException e) {
throw new RuntimeException("重复请求");
}
// 2. 核心业务
orderMapper.insert(order);
}
优点 :业务表无侵入,可以灵活控制有效期,适合跨系统防重。
缺点 :增加一次DB写操作。
适用场景:MQ消费端、RPC接口幂等。
方案五:状态机驱动
原理:业务状态有严格流转路径(如 待支付→已支付→已发货)。
当一个状态已经进入"已支付",再接收"待支付→已支付"的请求时,状态不满足,直接返回成功(已经是目标状态)。
public void pay(String orderNo) {
Order order = orderMapper.selectByNo(orderNo);
if (order.getStatus() == OrderStatus.PAID) {
return; // 已经是支付状态,直接返回成功
}
if (order.getStatus() != OrderStatus.INIT) {
throw new BizException("状态异常");
}
// 执行支付逻辑
}
优点 :业务语义清晰,天然幂等。
缺点 :有限状态机设计复杂,不适合无状态业务。
适用场景:订单、工单、审批等有明确状态流转的场景。
方案六:Redis 分布式锁
原理:以业务唯一标识(如订单号、请求id)为锁key,加锁成功后执行业务,释放锁。
相同请求并发时,只有一个能获取锁。
public void createOrderWithLock(String orderNo) {
String lockKey = "lock:order:" + orderNo;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
thrownew RuntimeException("请勿重复提交");
}
try {
// 幂等检查 + 业务逻辑
if (orderMapper.exists(orderNo)) {
return;
}
orderMapper.insert(order);
} finally {
redisTemplate.delete(lockKey);
}
}
注意:锁超时时间要大于业务执行时间,否则可能出现"锁提前释放,业务还未完成"的并发问题。可结合 Redisson 看门狗自动续期。
优点 :性能高、分布式友好。
缺点 :依赖Redis高可用;锁超时需要谨慎设计。
适用场景:高并发、可容忍极短时间锁等待。
五、实际项目中的组合策略
在实际系统中,往往根据业务重要性、并发量、接口类型选择组合方案。
- 支付接口:Token + 数据库唯一索引(双保险)。
- 更新订单状态:乐观锁(version) + 状态机。
- MQ 消费端:防重表(基于消息id)。
- 通用写接口:Redis 分布式锁 + 防重表。
一个典型的幂等处理流程 :

六、总结
接口幂等性是高并发系统中数据准确性的生命线。
没有万能的"银弹",只有适合业务场景的合适方案:
- 简单低并发:唯一索引足以。
- 表单类提交:Token 方案最直观。
- 更新操作:乐观锁或状态机。
- 分布式高并发:Redis 分布式锁 + 防重表。
记住一句话 :幂等性的核心不是"防止多个请求同时到达",而是"无论来多少次,最终结果只生效一次"。
在设计时,务必从数据存储层(唯一约束、状态机)和应用层(锁、token)双管齐下,才能在高并发下真正做到"万无一失"。
希望这篇文章能帮你彻底拿下幂等性面试题。
你在实际项目中还用过哪些有趣的幂等方案?欢迎评论区分享!
最近想快速提升项目实战能力(包含多个AI项目),或者最近找工作,或者想学习AI的小伙伴,可以看看下面👇🏻的这个链接(或许真的能够帮到你)susan.net.cn/project