首页>安全>正文

接口幂等怎么做才稳:场景、方案与验证方法

Feng 17 阅读 安全

重复调用从三个方向来:用户连点、网络超时后的客户端重试、消息队列的重复投递。单体时代靠数据库事务就能兜住,拆成微服务后,网关、多节点、跨机房,每一跳都可能把同一个请求送两次。幂等的定义一句话:同一接口被调多次,副作用和调一次完全相同。

一个真实背景:某电商平台重构时,订单创建、支付回调接口缺幂等保障,用户在网络波动下重复下单,后台多出多条同名订单,客服投诉量翻倍。修完这个事故,团队对幂等的理解就不再是「加个唯一索引」了。

按场景选方案

支付回调这类资金接口:外部订单号加状态机,先查后写,状态只允许向前流转。别指望单一手段,唯一约束必须兜底。

消息消费端:消费前先记消息 ID(去重表或消息表),和业务主键绑定。只靠业务表唯一索引,遇到事务回滚后重新投递,完整性还是会出问题。

通用写接口:数据库唯一索引加 INSERT … ON DUPLICATE KEY UPDATE 或 INSERT IGNORE,让数据库自己判重,逻辑最短。一个细节:ON DUPLICATE KEY 命中已有行时 affected rows 返回 2,新增是 1,数据没变是 0——靠它做分支判断,先把语义搞清楚。

public Order createOrder(String orderId, OrderRequest request) {
    Order order = orderMapper.selectByOrderId(orderId);
    if (order != null) {
        return order; // 幂等返回
    }
    // 注意:这里需要加分布式锁防并发插入
    try {
        return orderMapper.insert(new Order(orderId, request));
    } catch (DuplicateKeyException e) {
        return orderMapper.selectByOrderId(orderId);
    }
}

全局 ID 怎么选

Snowflake 或带业务含义的单号生成器都行。UUID v4 不是不能用,是完全随机,在 InnoDB 里当主键索引会频繁页分裂,写入性能掉得厉害,分布式环境下不建议。订单创建接口的常规做法:前端生成全局唯一 ID 传给后端,插入前先查该 ID 是否存在,主键冲突就直接返回已有订单,不再重复创建。

两个实测过的坑

Redis setnx 加过期时间分成两步执行不是原子操作:setnx 成功后进程挂掉,没有过期时间的锁就成死锁。用 SET key value NX EX 一条命令替代。

锁丢失更隐蔽:直接用 setnx 做幂等判断,Redis 主从切换后锁没同步过去,重复请求就穿过了校验。用 Redisson 能缓解——看门狗机制默认给锁 30 秒租约,业务没执行完会自动续期(lockWatchdogTimeout 默认 30000 毫秒)——但资金类接口仍要加数据库唯一索引做最后防线。缓存方案永远当第一道,不是唯一一道。

另外注意:分布式环境下时钟偏差会影响时间戳去重,NTP 校时漂移会让时间窗口去重误判,关键链路别用它。

别过度设计

查询接口天然幂等,不需要加锁加表。幂等的本质是权衡:一致性要求越高,实现越复杂。非关键场景用简单的时间戳或去重表就够,资金接口才值得上状态机加唯一约束的双保险。退款是个好例子:除了接口幂等,业务层还要退款单状态机,确保每个退款单只能从待处理到已退款。

上线前怎么验证

并发重放:把同一个请求并发打 N 次,断言数据库里只有一条记录。最小验证脚本:

for i in $(seq 1 50); do
  curl -s -X POST http://localhost:8080/orders \
    -H 'Content-Type: application/json' \
    -d '{"req_id":"R1001","sku":"A1"}' &
done
wait
# 判定:orders 表中 req_id=R1001 只允许 1 行

再补一句容易混淆的:防重复提交(前端按钮置灰)和幂等是两回事。前者是体验优化,后者是后端必须兑现的契约。写接口时先假设这个调用一定会来第二次,代码自然就写对了。

Feng
这位作者很神秘,还没有填写简介。