在分布式系统中,接口重复调用是家常便饭 —— 网络抖动导致重试、用户重复提交、网关超时重发等,都可能造成数据重复创建、金额重复扣减等严重问题。接口幂等性设计,就是确保同一请求被多次调用时,结果始终一致的技术手段。
什么是幂等性?
幂等性(Idempotence)源于数学概念,指一个操作执行多次与执行一次的效果相同。在后端接口中,体现在:
- 查询操作天然幂等(SELECT)
- 删除操作幂等(DELETE 多次结果一致)
- 新增、更新操作可能非幂等(如 POST 创建订单、PUT 累加金额)
核心实现方案
1. 唯一 ID 令牌机制(推荐)
这是最通用的方案,流程如下:
-
前端请求获取令牌(Token),后端生成唯一 UUID 存入 Redis,设置过期时间(如 5 分钟)
-
前端提交业务请求时携带该 Token
-
后端校验 Redis 中是否存在该 Token:
-
存在:执行业务逻辑,删除 Token(确保只能用一次)
-
不存在:返回 “重复提交” 提示
-
代码示例:
// 获取令牌接口
@GetMapping("/get-token")
public String getToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("order:token:" + token, "1", 5, TimeUnit.MINUTES);
return token;
}
// 提交订单接口(幂等处理)
@PostMapping("/create-order")
public Result createOrder(@RequestParam String token, @RequestBody Order order) {
// 1. 校验令牌
Boolean hasToken = redisTemplate.delete("order:token:" + token);
if (!hasToken) {
return Result.fail("请勿重复提交");
}
// 2. 执行订单创建逻辑
orderService.create(order);
return Result.success();
}
2. 基于业务唯一键去重
利用业务中天然的唯一标识(如订单号、手机号),在执行操作前校验该标识是否已存在:
-
新增订单时,用订单号作为唯一键,先查库再插入
-
支付接口用 “订单号 + 用户 ID” 作为唯一键,防止重复支付
SQL 示例:
-- 插入时校验,仅当订单号不存在时执行
INSERT INTO orders (order_no, user_id, amount)
SELECT #{orderNo}, #{userId}, #{amount}
WHERE NOT EXISTS (
SELECT 1 FROM orders WHERE order_no = #{orderNo}
)
3. 乐观锁机制
适用于更新操作,通过版本号控制,确保只有版本匹配时才执行更新:
// 实体类添加版本号字段
public class Product {
private Long id;
private Integer stock;
private Integer version; // 版本号
}
// 更新库存时校验版本
@Update("UPDATE product SET stock = stock - #{num}, version = version + 1 " +
"WHERE id = #{id} AND version = #{version}")
int deductStock(@Param("id") Long id, @Param("num") int num, @Param("version") int version);
调用时若返回 0,说明版本已变(被其他线程修改),需重试或返回失败。
方案选择策略
- 高频写操作(如订单创建):优先用唯一 ID 令牌
- 有天然唯一键的场景(如用户注册):用业务唯一键去重
- 库存扣减、金额变动:乐观锁 + 事务结合
- 分布式事务场景:结合 TCC、Saga 等分布式事务框架
注意事项
-
令牌过期时间需合理设置(太短导致正常请求失败,太长占用 Redis 空间)
-
并发场景下需用 Redis 的
SET NX或DELETE原子操作,避免校验逻辑非原子性 -
非幂等接口需明确标识(如文档中注明 “此接口不保证幂等,请避免重复调用”)
接口幂等性设计不是银弹,但它是后端数据安全的最后一道防线,尤其在支付、订单等核心场景,必须做到 “零容忍”。