"好的系统不是没有错误,而是能够优雅地处理错误。" ------ 分布式系统设计箴言
一、什么是接口幂等性?
1.1 数学概念到编程实践
在数学中,幂等运算满足 f(f(x)) = f(x)
的特性。比如绝对值函数 abs(abs(x)) = abs(x)
。在编程领域,接口幂等性指:无论调用次数多少,对系统状态的影响与单次调用相同。
举个真实案例:某电商平台支付接口未做幂等处理,用户点击支付按钮后因网络延迟重复提交,导致同一订单被扣款3次,最终引发用户投诉。这就是典型的幂等性缺失导致的问题。
1.2 为什么需要关注幂等性?
现代分布式系统面临三大不可靠要素:
- 用户不可靠(手抖多点)
- 网络不可靠(超时重传)
- 系统不可靠(服务重试)
二、典型应用场景分析
2.1 前端重复提交

2.2 接口超时重试
某金融系统调用第三方支付接口超时后的处理流程:

2.3 消息队列重复消费
消息中间件的重试机制可能导致重复消费:

三、六大核心解决方案
3.1 Token机制(防抖利器)

实现要点:
- Token需要设置合理过期时间(建议5-30秒)
- Redis操作要保证原子性(Lua脚本实现)
- 前端需要防止Token泄露
java
// SpringBoot示例代码
@PostMapping("/createOrder")
public Result createOrder(@RequestHeader("X-Token") String token) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList("order:token:" + token),
token);
if(result == 1) {
// 执行业务逻辑
return Result.success();
} else {
return Result.error("重复请求");
}
}
3.2 唯一索引(简单有效)
适用场景:创建类操作(注册、下单等)
sql
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
order_no VARCHAR(32) UNIQUE,
...
);
异常处理示例:
java
try {
orderDao.insert(order);
} catch (DuplicateKeyException e) {
log.warn("重复订单:{}", order.getOrderNo());
return Result.error("订单已存在");
}
3.3 乐观锁(更新操作首选)
通过版本号控制数据更新:

订单状态变更示例:
sql
UPDATE orders
SET status = 'PAID', version = version + 1
WHERE order_no = '202404211234'
AND version = 2;
3.4 分布式锁(高并发场景)
Redisson实现示例:
java
public Result deductStock(String productId) {
String lockKey = "lock:product:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
if(lock.tryLock(3, 30, TimeUnit.SECONDS)) {
// 业务逻辑
return doDeductStock();
}
return Result.error("系统繁忙");
} finally {
lock.unlock();
}
}
3.5 状态机(业务流程控制)
电商订单状态流转设计:

3.6 请求序列号(复杂业务流)
金融交易系统常用方案:

四、实战案例解析
4.1 电商秒杀系统设计
挑战 :
10万QPS下如何保证库存扣减的幂等性?
解决方案:
- 预扣库存:Redis缓存库存数
- 请求序列号:用户ID+秒杀场次生成唯一ID
- 异步落库:MQ消费保证最终一致性
java
// 伪代码示例
public Result seckill(String userId, String activityId) {
String bizId = userId + ":" + activityId;
if(redis.setnx(bizId, "1") == 0) {
return Result.error("重复请求");
}
redis.expire(bizId, 30);
// 预扣库存
Long stock = redis.decr("stock:" + activityId);
if(stock < 0) {
return Result.error("已售罄");
}
// 发送MQ消息
mq.send(new OrderMessage(userId, activityId));
return Result.success("排队中");
}
4.2 银行转账系统
关键需求 :
保证转账请求即使重复也不会多扣款
技术方案:
- 全局交易流水号(支付系统生成)
- 事务表唯一索引
- 账户余额变更使用CAS操作
sql
UPDATE account
SET balance = balance - 100,
version = version + 1
WHERE user_id = 123
AND version = 5;
五、方案选型指南
方案 | 适用场景 | 性能影响 | 实现复杂度 | 可靠性 |
---|---|---|---|---|
Token机制 | 表单提交类场景 | 中 | 中 | 高 |
唯一索引 | 数据创建类操作 | 低 | 低 | 高 |
乐观锁 | 数据更新类操作 | 低 | 中 | 高 |
分布式锁 | 高并发写操作 | 高 | 高 | 中 |
状态机 | 多状态流转业务 | 低 | 高 | 高 |
请求序列号 | 金融级复杂事务 | 中 | 高 | 最高 |
选型建议:
- 简单业务优先使用唯一索引/乐观锁
- 高并发场景选择Redis+Token机制
- 资金交易类必须使用请求序列号
- 复杂业务流程结合状态机设计
六、常见问题解答
Q:已经用了数据库事务还需要做幂等吗?
A:事务只能保证操作的原子性,不能防止重复请求。例如重复提交相同参数的请求,事务中仍然会插入重复数据。
Q:GET请求需要做幂等处理吗?
A:根据HTTP规范,GET是天然幂等的。但实际开发中如果GET请求有副作用(如记录日志),仍需要特殊处理。
Q:如何测试接口幂等性?
推荐测试方案:
- 使用Jmeter进行并发重复请求测试
- 自动化测试框架重复调用接口
- Chaos Engineering模拟网络重传