接口幂等方案设计

一、幂等性概念

1.1 定义

幂等性:同一操作执行多次,结果保持相同。

f(f(x)) = f(x)

1.2 应用场景

场景说明
网络波动用户重复点击提交按钮
超时重试客户端收不到响应后重试
消息重发MQ消息重复投递
分布式事务补偿操作重复执行

二、幂等令牌方案(推荐)

2.1 完整流程

幂等令牌流程图:
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│   前端页面   │      │    Redis    │      │   后端服务   │
└──────┬──────┘      └──────┬──────┘      └──────┬──────┘
       │                    │                    │
       │ 1. 请求获取令牌     │                    │
       │───────────────────>│                    │
       │                    │ 生成UUID           │
       │ 2. 返回token       │                    │
       │<───────────────────│                    │
       │                    │                    │
       │ 3. 携带token请求    │                    │
       │─────────────────────────────────────────>│
       │                    │                    │
       │                    │ 4. 查询token      │
       │                    │<───────────────────│
       │                    │                    │
       │                    │ token存在?        │
       │                    │     │              │
       │                    │ 是  │ 否           │
       │                    │     ↓              ↓
       │                    │ 删除token     返回重复请求
       │                    │     │              │
       │                    │     ↓              │
       │                    │ 5. 执行业务        │
       │                    │<───────────────────│
       │                    │                    │
       │ 6. 返回成功         │                    │
       │<─────────────────────────────────────────│

2.2 代码实现

import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
 
@Service
public class IdempotentTokenService {
    private static final String PREFIX = "idempotent:";
    private static final long TTL_SECONDS = 300;  // 5分钟过期
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 生成幂等令牌
    public String generateToken(String businessKey) {
        String token = UUID.randomUUID().toString();
        String key = PREFIX + businessKey + ":" + token;
        redisTemplate.opsForValue().set(key, "true", TTL_SECONDS, TimeUnit.SECONDS);
        return token;
    }
    
    // 验证并消费令牌
    public boolean validateToken(String businessKey, String token) {
        String key = PREFIX + businessKey + ":" + token;
        // 使用Lua脚本保证原子性
        String script = """
            if redis.call('exists', KEYS[1]) == 1 then
                redis.call('del', KEYS[1])
                return 1
            else
                return 0
            end
            """;
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(key)
        );
        return result != null && result == 1;
    }
}

2.3 控制器层集成

@RestController
@RequestMapping("/api/payment")
public class PaymentController {
    @Autowired
    private IdempotentTokenService tokenService;
    @Autowired
    private PaymentService paymentService;
    
    // 获取支付令牌
    @GetMapping("/token")
    public ResponseEntity<Map<String, String>> getToken(Long orderId) {
        String token = tokenService.generateToken("payment:" + orderId);
        return ResponseEntity.ok(Map.of("token", token));
    }
    
    // 执行支付(幂等)
    @PostMapping("/pay")
    public ResponseEntity<?> pay(
        @RequestParam Long orderId,
        @RequestParam String token,
        @RequestBody PaymentRequest request
    ) {
        // 验证幂等令牌
        if (!tokenService.validateToken("payment:" + orderId, token)) {
            return ResponseEntity.badRequest().body("请勿重复支付");
        }
        
        // 执行支付业务
        PaymentResult result = paymentService.processPayment(orderId, request);
        return ResponseEntity.ok(result);
    }
}

三、其他幂等实现方案

3.1 数据库唯一约束

-- 创建唯一索引
CREATE UNIQUE INDEX idx_order_payment_unique 
ON payment_records (order_id);
 
-- 插入时自动失败(重复订单号)
INSERT INTO payment_records (order_id, amount, status)
VALUES (123, 200, 'SUCCESS')
ON CONFLICT (order_id) DO NOTHING;

3.2 状态机控制

public enum OrderStatus {
    PENDING_PAYMENT,  // 待支付
    PAID,             // 已支付
    REFUNDED,         // 已退款
    CANCELLED;        // 已取消
    
    // 状态转移校验
    public boolean canTransitionTo(OrderStatus target) {
        return switch (this) {
            case PENDING_PAYMENT -> target == PAID || target == CANCELLED;
            case PAID -> target == REFUNDED;
            case REFUNDED, CANCELLED -> false;  // 终态不可转移
        };
    }
}

3.3 乐观锁版本号

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    
    @Transactional
    public boolean updateStatus(Long orderId, OrderStatus newStatus, int expectedVersion) {
        Order order = orderRepository.findById(orderId).orElse(null);
        if (order == null || order.getVersion() != expectedVersion) {
            return false;  // 版本不匹配,拒绝更新
        }
        
        order.setStatus(newStatus);
        order.setVersion(order.getVersion() + 1);
        orderRepository.save(order);
        return true;
    }
}

3.4 分布式锁

@Service
public class DistributedLockService {
    @Autowired
    private RedissonClient redissonClient;
    
    public boolean tryLock(String lockKey, long waitTime, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

四、方案对比与选型

方案适用场景优点缺点
幂等令牌支付、下单等关键操作通用、可靠需要额外存储
唯一约束任意需要幂等的写入数据库原生支持只能防重复写入
状态机订单状态流转业务语义清晰需要维护状态转移规则
乐观锁更新操作无锁竞争需要版本字段
分布式锁复杂业务流程粒度可控性能开销较大

推荐组合方案

推荐架构:
┌──────────────────────────────────────────────────────┐
│                  客户端请求                           │
│                        ↓                             │
│          ┌──────────────────────────┐               │
│          │   1. 幂等令牌校验        │               │
│          │   (Redis + Lua原子操作)  │               │
│          └──────────┬───────────────┘               │
│                     ↓                               │
│          ┌──────────────────────────┐               │
│          │   2. 数据库唯一约束      │               │
│          │   (order_id唯一索引)     │               │
│          └──────────┬───────────────┘               │
│                     ↓                               │
│          ┌──────────────────────────┐               │
│          │   3. 状态机校验          │               │
│          │   (防止非法状态跳转)      │               │
│          └──────────┬───────────────┘               │
│                     ↓                               │
│                业务逻辑执行                          │
└──────────────────────────────────────────────────────┘

五、面试要点

5.1 核心概念

概念说明
幂等性同一操作执行多次结果相同
幂等令牌一次性使用的唯一标识
Redis原子操作Lua脚本保证check-then-delete原子性
状态机状态转移规则约束

5.2 常见面试问题

Q:为什么需要幂等性?

A:分布式系统中网络不可靠,请求可能重复到达,幂等性保证系统稳定性。

Q:幂等令牌为什么要设置过期时间?

A:防止令牌无限占用内存,同时处理用户放弃操作的场景。

Q:为什么要用Lua脚本删除令牌?

A:保证”检查存在”和”删除”操作的原子性,防止并发问题。

Q:如果Redis宕机怎么办?

A:可以降级到数据库唯一约束,或使用本地缓存+定时同步。


参考链接