【MapSheep】
[好记性不如烂笔头]

一、先搞懂:什么是接口幂等?

大白话:同一个请求,不管调用多少次,结果都一样,不会重复处理、不会产生副作用
比如:你支付订单时,网络卡了点了两次付款按钮,接口只会扣一次钱;你提交表单点了两次提交,系统只会创建一条数据。
核心场景:支付、下单、提交表单、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:幂等令牌(前端/调用方传唯一标识)

原理

  1. 调用接口前,先向服务端申请一个唯一“令牌”(比如UUID);
  2. 调用业务接口时带上这个令牌;
  3. 服务端验证令牌:未使用则执行业务,标记令牌为已使用;已使用则直接返回成功。
    适用场景:下单、支付等重要操作(防重复提交)。
    核心代码
// 令牌工具类(用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);
        }
    }
}

四、方案选择建议(简短)

  1. 简单新增场景:用唯一索引
  2. 前端重复提交(下单/支付):用幂等令牌
  3. 状态更新场景:用状态机控制
  4. 分布式并发场景:用Redis分布式锁
  5. 极致场景:可组合使用(比如唯一索引+分布式锁)。

总结

  1. 接口幂等核心是“重复调用结果一致”,解决重复提交、重试、消息重复消费等问题;
  2. 常用实现方案:唯一索引(数据库层)、幂等令牌(调用方标识)、状态机(业务层)、Redis分布式锁(分布式并发);
  3. 选择方案时优先匹配业务场景,简单场景用简单方案,分布式场景加锁保障。
posted on 2026-01-27 14:17  (Play)  阅读(20)  评论(0)    收藏  举报