我为什么把“接口幂等”写进了团队红线

举报
逆行时光 发表于 2026/11/04 09:04:47 2026/11/04
【摘要】 我为什么把“接口幂等”写进了团队红线刚做支付系统时,我觉得“幂等”是个高大上的词,离我很远。直到那次用户投诉——同一条订单被扣了两次钱,我才明白:幂等不是优雅,是保命。那是个雨夜,用户在下单页点了两次“提交”,前端没做防重,两个请求几乎同时到达后端。我的代码是这样的:public Result createOrder(CreateOrderReq req) { // 1. 校验库存 ...

我为什么把“接口幂等”写进了团队红线

刚做支付系统时,我觉得“幂等”是个高大上的词,离我很远。直到那次用户投诉——同一条订单被扣了两次钱,我才明白:幂等不是优雅,是保命。

那是个雨夜,用户在下单页点了两次“提交”,前端没做防重,两个请求几乎同时到达后端。我的代码是这样的:

public Result createOrder(CreateOrderReq req) {
    // 1. 校验库存
    // 2. 创建订单
    // 3. 调用支付
    // 4. 返回成功
}

两个请求都通过了校验,都创建了订单,都调了支付。结果:库存扣了两次,用户付了双份钱,我们多创建了一张订单。客服电话被打爆,我连夜写脚本退款、冲正库存、删脏数据。折腾到凌晨四点,手指都在抖。

那次之后,我把“接口幂等”写进了团队开发规范的第一条:任何写操作接口,必须保证幂等。


一、幂等不是“防重复提交”,是“防一切重复”

很多人以为幂等就是前端加个 loading 按钮,或者后端加个防重锁。但这些只能防“用户手抖”,防不了:

  • 网络超时重试:调用方没收到响应,自动重试
  • 消息队列重复消费:MQ 保证至少一次投递
  • 分布式任务重试:调度框架失败重试
  • 网关层重试:负载均衡健康检查触发重放

真正的幂等,是同一个请求,不管来多少次,效果都一样。一次和一万次,结果完全相同。


二、我们用了四种幂等方案,各管一段

1. 唯一索引:最硬的幂等锁

适合创建类操作。比如订单创建,我们给 order_no 加了唯一索引。不管前端发多少次请求,只要 order_no 相同,第二次插入直接报 DuplicateKeyException,我们捕获后返回“订单已存在”。

try {
    orderMapper.insert(order);
} catch (DuplicateKeyException e) {
    // 幂等:返回已有订单
    return Result.success(orderMapper.selectByOrderNo(order.getOrderNo()));
}

这是最可靠的方式——数据库层面的保证,不可能被绕过。

2. 幂等表:通用方案

建一张 idempotent_record 表:

CREATE TABLE idempotent_record (
    id BIGINT PRIMARY KEY,
    biz_key VARCHAR(128) UNIQUE,  -- 业务唯一键:order_123_pay
    status TINYINT,
    create_time DATETIME
);

请求进来时,先 INSERT 一条记录,唯一键冲突说明已处理过,直接返回上次结果。这个方案适合所有写操作,但需要额外表。

3. Redis 原子锁:高性能方案

用 SETNX 做分布式锁:

String key = "idempotent:" + bizKey;
Boolean locked = redisTemplate.opsForValue()
    .setIfAbsent(key, "1", Duration.ofSeconds(300));
if (locked == null || !locked) {
    return Result.fail("请求处理中,请勿重复提交");
}
// 执行业务

适合高并发场景,但要注意锁过期时间和业务执行时间的平衡。

4. 状态机:业务层幂等

有些操作天然幂等,比如“取消订单”:

public Result cancelOrder(Long orderId) {
    Order order = orderMapper.selectById(orderId);
    if (order.getStatus() == OrderStatus.CANCELLED) {
        return Result.success("订单已取消");  // 幂等返回
    }
    // 执行取消逻辑
}

状态机天然保证幂等——已取消的订单再取消,什么都不做,直接返回成功。


三、幂等键的设计:别用 userId,用业务唯一标识

我见过有人用 userId 做幂等键,结果用户同时操作两个订单,第二个被拦了。幂等键必须是业务维度的唯一标识:

  • 创建订单:orderNo
  • 支付回调:paymentNo
  • 退款申请:refundNo
  • 消息消费:messageId

原则:谁重复,谁做幂等键。


四、一个坑:幂等和并发的微妙关系

有次我们用了 Redis 锁做幂等,但业务逻辑里先查了数据库再插入,结果在高并发下还是出现了重复订单。原因:两个请求同时通过了 SETNX(因为第一个还没插入数据库),然后都查到“不存在”,都插入了。

幂等锁和业务操作必须在同一个原子边界内。 正确做法是:先 SETNX,拿到锁后再执行业务,业务完成后释放锁。或者直接用唯一索引,让数据库兜底。


五、一个心态的转变

以前我觉得幂等是“过度设计”,现在觉得是底线设计。你不知道调用方会重试几次,不知道网络会抖几次,不知道 MQ 会投几次。唯一能确定的是:你的接口会被重复调用,而且一定会在你最忙的时候。

如果你现在的系统里还有没做幂等的写接口,今晚就列个清单。从支付、下单、扣库存这些核心接口开始,一个一个补上。不用一步到位,但每加一个幂等,你的系统就少一分“被用户骂”的风险。毕竟,退款容易,信任难建。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。