在 Spring Boot 中,幂等性是实现分布式系统设计和接口调用的一个重要概念,尤其在高并发、分布式环境下,确保接口重复调用不会引发系统数据异常至关重要。
幂等性概念
幂等性(Idempotence)是指一次请求和重复多次请求对系统的影响完全相同。在接口调用中,如果一个接口满足幂等性,那么无论调用多少次,最终结果是一样的。
场景分析
- 支付系统
防止重复支付。例如用户多次点击支付按钮,导致重复扣款。 - 订单创建
防止用户重复下单,产生多个相同订单。 - 短信发送
防止重复发送短信,避免浪费资源。 - 库存扣减
防止并发扣减库存,导致库存不足或超卖。 - 分布式任务处理
防止任务重复执行,保证最终一致性。
如何实现幂等性
在 Spring Boot 中,常用以下几种方法实现幂等性:
1.基于数据库唯一约束
原理:
利用数据库的唯一约束机制,确保同一请求只能操作一次。
实现:
- 在数据库表中增加一个唯一字段(如订单号、请求 ID)。
- 插入数据时,利用唯一约束防止重复写入。
代码示例:
@Entity
@Table(name = "orders", uniqueConstraints = {@UniqueConstraint(columnNames = "orderId")})
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String orderId; // 唯一订单号
@Column(nullable = false)
private BigDecimal amount;
}
当重复提交时,数据库会抛出 DuplicateKeyException,可以捕获并返回提示。
2.基于唯一 Token 实现
原理:
- 每次请求都需要携带唯一的 token,服务器校验 token 是否已使用。
- 若已使用,则拒绝请求。
实现步骤:
1.客户端向服务器申请唯一 token(如 UUID)。
2.在请求时携带 token。
3.服务端验证 token:
- 若 token 未使用,处理业务并标记 token 为已使用。
- 若 token 已使用,直接返回提示。
代码示例:
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private StringRedisTemplate redisTemplate;
@PostMapping("/create")
public String createOrder(@RequestParam String token) {
// 校验 token 是否已存在
Boolean isTokenExists = redisTemplate.opsForValue().setIfAbsent(token, "1", 10, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(isTokenExists)) {
return "重复请求,请勿再次提交";
}
// 执行业务逻辑
// ...
return "订单创建成功";
}
}
优点:
- 无需修改数据库结构。
- 使用 Redis 提高性能,适用于高并发场景。
3.基于幂等字段校验
原理:
- 接口请求体中包含幂等字段(如订单号、请求 ID)。
- 服务端通过幂等字段判断请求是否已处理。
实现步骤:
- 在业务表中增加 requestId 字段,标记唯一请求。
- 每次请求前查询是否存在相同的 requestId。
- 若存在,直接返回处理结果。
代码示例:
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
public String createOrder(String requestId, Order order) {
// 校验幂等字段
if (orderRepository.existsByRequestId(requestId)) {
return "订单已创建,请勿重复提交";
}
// 保存订单
order.setRequestId(requestId);
orderRepository.save(order);
return "订单创建成功";
}
}
4.基于分布式锁
原理:
- 利用分布式锁(如 Redis 的 SETNX)对关键资源加锁,确保同一时刻只有一个请求处理。
实现步骤:
- 请求时加锁,锁的唯一标识为幂等字段(如订单号)。
- 若加锁成功,执行业务逻辑。
- 业务执行完成后释放锁。
代码示例:
@Service
public class SmsService {
@Autowired
private StringRedisTemplate redisTemplate;
public String sendSms(String phoneNumber) {
String lockKey = "sms:lock:" + phoneNumber;
// 加锁
Boolean isLockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 2, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(isLockAcquired)) {
return "短信发送过于频繁,请稍后再试";
}
try {
// 执行业务逻辑
// ...
return "短信发送成功";
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
}
}
5.基于状态校验
原理:
- 根据业务状态判断请求是否重复。
- 常用于支付、库存等有明确状态的场景。
实现步骤:
- 增加状态字段(如订单状态、支付状态)。
- 请求前校验状态是否已完成。
代码示例:
@Service
public class PaymentService {
@Autowired
private OrderRepository orderRepository;
public String payOrder(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow(() -> new RuntimeException("订单不存在"));
// 校验状态
if (order.getStatus().equals("PAID")) {
return "订单已支付,请勿重复操作";
}
// 修改订单状态
order.setStatus("PAID");
orderRepository.save(order);
return "支付成功";
}
}
幂等性设计注意事项
1.选择合适的幂等方案
- 数据库唯一约束适合低并发场景。
- Redis 分布式锁适合高并发场景。
- 幂等字段校验适合需要记录请求 ID 的场景。
2.幂等字段的设计
- 幂等字段应具有唯一性,如订单号、请求 ID。
- 客户端生成或服务端分配均可。
3.幂等性与事务
- 确保幂等校验与业务逻辑在同一事务中执行,避免校验通过但业务未执行完成的情况。
4.性能优化
- 使用缓存(如 Redis)提高幂等校验性能,减少数据库压力。
总结
Spring Boot 中的幂等性实现,是确保接口安全性和数据一致性的关键。根据业务场景的不同,选择合适的幂等方案至关重要:
- 数据库唯一约束:简单场景,直接使用。
- Redis 分布式锁:高并发场景,提升性能。
- 幂等字段校验:需要记录唯一请求的场景。
幂等性设计不仅是接口安全的保障,更是系统稳定性的核心体现。