一、什么是接口幂等性?------ 原理篇
幂等性(Idempotence):一个操作无论执行一次还是多次,其结果都完全相同,不会产生副作用。
在 Web 接口层面,这意味着:
对同一个请求(含相同参数、用户、业务标识等),重复调用 N 次,和调用 1 次的效果一致。
数学类比(帮助理解):
- 幂等函数:
f(f(x)) = f(x)
例如:abs(abs(-5)) = abs(-5) = 5 - 非幂等函数:
f(x) = x + 1→f(f(1)) = 3 ≠ f(1) = 2
接口幂等 ≠ HTTP 方法幂等
- HTTP 标准中,GET/PUT/DELETE 是语义幂等的,但业务实现可能破坏幂等性。
- 例如:
DELETE /order/123如果每次删除都记录日志并扣减库存,就不是真正幂等。
✅ 关键结论 :幂等性必须由业务逻辑 + 数据设计共同保障,不能依赖 HTTP 方法语义。
二、为什么需要幂等性?------ 使用场景篇
以下场景极易引发重复请求,必须做幂等控制:
| 场景 | 说明 |
|---|---|
| 网络超时重试 | 客户端未收到响应,自动重发请求(如 Feign、Dubbo 重试) |
| 用户重复点击 | 支付按钮连点、表单重复提交 |
| 浏览器刷新/后退 | POST 请求刷新导致重复提交 |
| 消息队列重复消费 | Kafka/RocketMQ 至少一次投递语义 |
| 定时任务重叠执行 | 分布式调度框架(如 XXL-JOB)任务重入 |
⚠️ 若不做幂等,后果严重:重复下单、重复扣款、库存超卖、数据错乱。
三、Spring Boot 实现幂等性 ------ 代码实战篇
下面以 "创建订单"接口 为例,展示两种主流方案。
方案1:基于数据库唯一索引(最常用、最可靠)
步骤:
- 订单表增加业务唯一 ID 字段(如
biz_order_no) - 该字段加唯一索引
- 插入前不校验,直接插入,靠 DB 抛异常拦截重复
java
// Order.java
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "bizOrderNo"))
public class Order {
@Id
private Long id;
private String bizOrderNo; // 业务订单号,由前端或上游生成
private String userId;
private BigDecimal amount;
// ...
}
java
// OrderService.java
@Service
@Transactional
public class OrderService {
@Autowired
private OrderRepository orderRepository;
public Order createOrder(String bizOrderNo, String userId, BigDecimal amount) {
Order order = new Order();
order.setBizOrderNo(bizOrderNo);
order.setUserId(userId);
order.setAmount(amount);
try {
return orderRepository.save(order); // 若 bizOrderNo 重复,DB 抛出 DuplicateKeyException
} catch (DataIntegrityViolationException e) {
// 幂等处理:查询已存在的订单返回
return orderRepository.findByBizOrderNo(bizOrderNo)
.orElseThrow(() -> new RuntimeException("订单创建失败且无法找回"));
}
}
}
✅ 优点 :简单、可靠、天然支持分布式
❌ 缺点:依赖 DB 唯一索引,需上游传唯一 ID
方案2:基于 Redis Token 机制(适合表单提交防重)
思路:
- 进入下单页面时,后端生成 token 返回给前端
- 提交时携带 token
- 后端用 Lua 脚本原子校验并删除 token
java
// IdempotentController.java
@RestController
public class OrderController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private OrderService orderService;
// Step 1: 获取防重 Token
@GetMapping("/token")
public String getToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("idempotent:" + token, "1", Duration.ofMinutes(5));
return token;
}
// Step 2: 创建订单(带幂等 Token)
@PostMapping("/order")
public ResponseEntity<Order> createOrder(@RequestHeader("X-Idempotent-Token") String token,
@RequestBody CreateOrderDTO dto) {
// Lua 脚本保证原子性:存在则删除,不存在则拒绝
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
Boolean consumed = redisTemplate.execute(
new DefaultRedisScript<>(script, Boolean.class),
Collections.singletonList("idempotent:" + token),
"1"
);
if (Boolean.TRUE.equals(consumed)) {
Order order = orderService.createOrder(dto.getBizOrderNo(), dto.getUserId(), dto.getAmount());
return ResponseEntity.ok(order);
} else {
throw new RuntimeException("重复提交,请勿重复操作");
}
}
}
✅ 优点 :用户体验好,适合前端交互场景
⚠️ 注意:Token 有效期要合理,防止误杀;高并发下需 Lua 保证原子性
四、 专业术语推荐
🎯 高频面试问题:
- 什么是接口幂等性?哪些 HTTP 方法是幂等的?
- 如何保证支付接口的幂等性?
- POST 请求是非幂等的,如何让它变成幂等?
- 消息队列重复消费怎么处理?
- 数据库唯一索引和 Redis Token 方案各有什么优劣?
💬 推荐使用的专业术语(显得有水准):
- 业务唯一标识(Business Unique Key)
- 去重表(Deduplication Table)
- 状态机幂等(State Machine Idempotency)
- 乐观锁版本控制(Optimistic Locking with Version)
- 原子操作(Atomic Operation via Lua Script)
- 至少一次投递语义(At-Least-Once Delivery Semantics)
- 副作用(Side Effect)
- 最终一致性(Eventual Consistency)
🔍 加分回答技巧:
- 强调:"幂等性是服务端责任,不能依赖前端防重"
- 区分:"HTTP 语义幂等 ≠ 业务逻辑幂等"
- 举例:"我们在订单系统中通过 biz_order_no + 唯一索引实现创建幂等,退款通过 refund_no 防重"
- 提及监控:"我们会记录幂等拦截日志,用于分析重复请求来源"
总结一句话:
"在分布式系统中,网络不可靠是常态,幂等性不是可选项,而是保障数据一致性的底线设计。"