目录
一、先搞懂:什么是接口幂等?
大白话:同一个请求,不管调用多少次,结果都一样,不会重复处理、不会产生副作用。
比如:你支付订单时,网络卡了点了两次付款按钮,接口只会扣一次钱;你提交表单点了两次提交,系统只会创建一条数据。
核心场景:支付、下单、提交表单、MQ消息重复消费、接口重试(比如Feign重试)。
二、为什么需要幂等?
网络抖动、客户端重复提交、服务端重试、MQ消息重复推送等,都会导致接口被多次调用,没有幂等性会造成:
- 重复下单、重复支付(扣多次钱);
- 数据重复插入(数据库多条相同记录);
- 库存多扣、积分多送等。
三、Java接口幂等的核心实现方案(大白话+简短代码)
1. 方案1:唯一索引(最基础,防数据库重复)
原理:给数据库表的关键字段(比如订单号、用户ID+业务标识)加唯一索引,重复插入会抛异常,接口捕获异常返回“处理成功”即可。
适用场景:新增数据(比如创建订单)。
代码示例:
// 订单Service层
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public String createOrder(OrderDTO orderDTO) {
try {
// 插入订单(订单号orderNo加了唯一索引)
orderMapper.insert(orderDTO);
return "订单创建成功";
} catch (DuplicateKeyException e) {
// 捕获唯一索引冲突异常,返回成功(视为已处理)
return "订单创建成功";
}
}
}
2. 方案2:幂等令牌(前端/调用方传唯一标识)
原理:
- 调用接口前,先向服务端申请一个唯一“令牌”(比如UUID);
- 调用业务接口时带上这个令牌;
- 服务端验证令牌:未使用则执行业务,标记令牌为已使用;已使用则直接返回成功。
适用场景:下单、支付等重要操作(防重复提交)。
核心代码:
// 令牌工具类(用Redis存令牌,过期时间防内存泄漏)
@Component
public class IdempotentTokenUtil {
@Autowired
private StringRedisTemplate redisTemplate;
// 生成令牌
public String generateToken() {
String token = UUID.randomUUID().toString();
// 令牌有效期5分钟(根据业务调整)
redisTemplate.opsForValue().set(token, "UNUSED", 5, TimeUnit.MINUTES);
return token;
}
// 验证并使用令牌(原子操作,防并发)
public boolean validateToken(String token) {
// Redis的setIfAbsent是原子操作,确保只有一个请求能使用令牌
return redisTemplate.opsForValue().setIfAbsent(token, "USED", 5, TimeUnit.MINUTES);
}
}
// 业务接口层
@RestController
public class PayController {
@Autowired
private IdempotentTokenUtil tokenUtil;
@Autowired
private PayService payService;
// 申请令牌接口
@GetMapping("/getToken")
public String getToken() {
return tokenUtil.generateToken();
}
// 支付接口(带令牌调用)
@PostMapping("/pay")
public String pay(@RequestParam String token, @RequestBody PayDTO payDTO) {
// 1. 验证令牌
if (!tokenUtil.validateToken(token)) {
return "请求重复,请稍后再试";
}
// 2. 执行业务
payService.doPay(payDTO);
return "支付成功";
}
}
3. 方案3:状态机控制(防重复更新)
原理:给业务数据加状态字段(比如订单状态:待支付→支付中→已支付→已取消),更新前先检查状态,只有符合条件的状态才允许操作。
适用场景:订单状态更新、支付状态变更。
代码示例:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public String payOrder(Long orderId) {
// 1. 查询订单并检查状态(必须是待支付)
Order order = orderMapper.selectById(orderId);
if (order == null || !"WAIT_PAY".equals(order.getStatus())) {
return "订单状态异常,无需重复支付";
}
// 2. 更新状态为支付中(防并发)
int updateCount = orderMapper.updateStatus(orderId, "WAIT_PAY", "PAYING");
if (updateCount == 0) {
return "请求重复,请稍后再试";
}
// 3. 执行支付逻辑
payService.pay(order);
// 4. 更新状态为已支付
orderMapper.updateStatus(orderId, "PAYING", "PAID");
return "支付成功";
}
}
4. 方案4:Redis分布式锁(防并发重复处理)
原理:用Redis做分布式锁,同一个业务ID(比如订单号)只有一个线程能获取锁执行业务,其他线程等待或直接返回成功。
适用场景:分布式系统下的并发接口调用。
核心代码:
@Service
public class OrderService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private OrderMapper orderMapper;
public String processOrder(String orderNo) {
// 分布式锁key:业务标识+订单号
String lockKey = "ORDER_LOCK:" + orderNo;
// 锁超时时间30秒(防止死锁)
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "LOCK", 30, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(locked)) {
// 未获取到锁,说明已有线程在处理
return "订单处理中,请稍后再试";
}
try {
// 检查订单是否已处理
Order order = orderMapper.selectByOrderNo(orderNo);
if (order != null && "SUCCESS".equals(order.getStatus())) {
return "订单已处理成功";
}
// 执行业务逻辑
orderMapper.insert(new Order(orderNo, "SUCCESS"));
return "订单处理成功";
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
}
}
四、方案选择建议(简短)
- 简单新增场景:用唯一索引;
- 前端重复提交(下单/支付):用幂等令牌;
- 状态更新场景:用状态机控制;
- 分布式并发场景:用Redis分布式锁;
- 极致场景:可组合使用(比如唯一索引+分布式锁)。
总结
- 接口幂等核心是“重复调用结果一致”,解决重复提交、重试、消息重复消费等问题;
- 常用实现方案:唯一索引(数据库层)、幂等令牌(调用方标识)、状态机(业务层)、Redis分布式锁(分布式并发);
- 选择方案时优先匹配业务场景,简单场景用简单方案,分布式场景加锁保障。
浙公网安备 33010602011771号