接口幂等设计

0 阅读1分钟

幂等接口设计

  • 网络重试

客户端因超时重试导致请求重复提交

  • 消息重复

消息队列中间件可能重复投递消息

  • 用户误操作

用户多次点击提交按钮

  • 系统容错

分布式系统依赖重试保证最终一致性,幂等性是基础

常见策略

唯一索引/约束

利用数据库的唯一索引防止重复数据插入。
适用于创建业务单据(如订单号、支付流水号唯一)


CREATE TABLE `order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(64) NOT NULL COMMENT '订单号',
  `status` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`)
) ENGINE=InnoDB;

去重表(配合唯一索引)

使用独立的去重表记录已处理的请求标识,利用数据库唯一约束实现幂等


@Transactional
public void processOrder(Request request) {
    String requestId = request.getRequestId();
    // 插入去重记录,利用数据库唯一约束
    try {
        deduplicationDao.insert(requestId);
    } catch (DuplicateKeyException e) {
        // 已处理,直接返回或抛出异常
        return;
    }
    // 执行实际业务逻辑
    orderDao.create(request.getOrder());
}

状态机幂等

对于有状态变更的业务(如订单状态流转),通过限制状态转移方向实现幂等


public boolean payOrder(String orderId) {
    Order order = orderDao.selectByOrderId(orderId);
    if (order.getStatus() == OrderStatus.PENDING_PAY) {
        order.setStatus(OrderStatus.PAID);
        orderDao.update(order);
        return true;
    }
    // 已经支付过了,视为成功但不再重复处理
    return false;
}

Token 机制(适用于防止重复提交)

服务端生成唯一token下发到客户端(如表单隐藏域)



// 生成token
public String generateToken(String userId) {
    String token = UUID.randomUUID().toString();
    redisTemplate.opsForValue().set(TOKEN_PREFIX + userId, token, 30, TimeUnit.MINUTES);
    return token;
}

// 执行业务前校验token
public boolean checkAndRemoveToken(String userId, String token) {
    String key = TOKEN_PREFIX + userId;
    String storedToken = redisTemplate.opsForValue().get(key);
    if (token.equals(storedToken)) {
        redisTemplate.delete(key);
        return true;
    }
    return false;
}

分布式锁(Redis/ Zookeeper)

public void handleRequest(String orderId) {
    String lockKey = "lock:order:" + orderId;
    boolean locked = redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS);
    if (!locked) {
        // 未获取到锁,可能是重复请求,直接返回或稍后重试
        return;
    }
    try {
        // 检查是否已处理(结合去重表)
        if (processedChecker.isProcessed(orderId)) {
            return;
        }
        // 业务处理
        doBusiness(orderId);
        // 标记已处理
        processedChecker.markProcessed(orderId);
    } finally {
        redisLock.unlock(lockKey);
    }
}

总结

新增数据:唯一索引、去重表
状态变更:状态机、乐观锁
防止重复提交:Token、分布式锁
通用方案:幂等键+存储去重+结果缓存