你的API到底有没有幂等性?这个问题能筛掉一半的CRUD工程师

2026-07-22 9 0

上周五晚上,我正在愉快地打着游戏,突然群里炸了——支付系统出问题了。有个用户反映自己被扣了三次钱。好家伙,这用户体验,直接从VIP变成了仇人。

我赶紧放下手柄(真的,《黑神话》都没这么重要),去查日志。果然,又是那个经典的问题:接口没有做幂等性。

什么是幂等性?

简单来说,幂等性就是:一个操作执行一次和执行无数次,效果是一样的。你按一次按钮和按一百次按钮,结果相同——这才叫幂等。

放到API场景下就是:你调用这个接口一次,和调用一百次,结果应该是一样的。如果不一样,那这个接口就是非幂等的,迟早会出事儿。

有人说,这不是很简单吗?POST请求本来就是非幂等的,设计上就是这样。朋友,说这种话的人,一定没被生产环境毒打过。

哪些场景必须幂等?

说实话,我见过太多程序员写接口的时候压根不思考这个问题。反正能跑就行,管它第几次调用呢。

但下面这些场景,幂等性是必须的:

  • 支付/退款——用户网不好,多点了一次,你敢扣人家三笔钱?
  • 订单创建——重复提交表单,生成了三个订单,用户当场报警
  • 库存扣减——超卖问题了解一下?库存变成负数,老板当场去世
  • 消息消费——MQ消费重复了,数据直接翻倍,找谁说理去

有人可能会说,这些我都知道,我用了事务。兄弟,事务只能保证ACID,它保证不了你的接口被调用一次还是一百次。极端情况下,网络超时、重试机制、用户手滑——这些都会导致你的接口被重复调用。事务只管一次调用内的事,管不了调用之外的事。

几种常见的幂等实现方案

好了,理论讲完了,说点实际的。幂等性怎么实现?

方案一:唯一索引/唯一键

这是最简单粗暴的方案。拿订单来说,你给订单号加个唯一索引,重复创建直接抛异常。

// 伪代码示例
try {
    orderDao.insert(order);
} catch (DuplicateKeyException e) {
    // 已存在,返回原订单
    return orderDao.findByOrderNo(order.getOrderNo());
}

优点:简单粗暴,效果好。缺点:错误处理得嵌套在业务逻辑里,代码看起来有点丑。

方案二:Token机制

服务端生成一个唯一token,客户端每次请求带着这个token。服务端维护一个token集合,一旦用过就标记为已使用。

// 客户端
POST /api/order
Headers: X-Idempotency-Token: uuid-xxx
Body: {productId: 123, token: uuid-xxx}

// 服务端
if (tokenRepository.exists(token)) {
    return tokenRepository.getResult(token); // 返回之前的结果
}
tokenRepository.markUsed(token);
result = businessLogic();
tokenRepository.saveResult(token, result);
return result;

优点:通用性强,跟业务解耦。缺点:得维护一个token存储,不过现在Redis这么便宜,这都不是事儿。

方案三:乐观锁/版本号

适合更新场景,给数据加个version字段。更新的时候比对版本号,只有版本对了才能更新成功。

UPDATE inventory 
SET stock = stock - 1, version = version + 1 
WHERE product_id = ? AND version = ? AND stock > 0

如果影响行数是0,说明版本不对,可能是并发问题,也可能是库存不足。这方案好处是不需要额外的存储,但只适合更新场景,创建场景用不上。

方案四:悲观锁

SELECT FOR UPDATE,了解一下。直接锁住你要操作的那行数据,别的请求排队等着。简单有效,缺点是性能损耗,适合并发量不大的场景。

我的经验之谈

做了这么多年后端,我总结了几条经验:

第一,幂等性要一开始就设计进去。别等出事了再打补丁。在定义接口的时候就想清楚:这个接口会被重复调用吗?如果会,怎么保证幂等?提前想清楚,能省你很多半夜起床的时间。

第二,不同场景用不同方案。不是所有接口都需要一样的幂等策略。创建类接口用唯一索引,更新类接口用版本号,支付类接口用Token+状态机。量体裁衣,别一把梭。

第三,幂等性和性能要平衡。有些人做幂等做到了极致,锁表锁库,结果系统吞吐量掉了一半。这也是病,得治。幂等是为了防错误,不是为了把自己防死。

第四,测试的时候要专门测幂等。我见过太多代码,单元测试、集成测试都过了,一上线就出问题。为什么?因为没人测过重复调用这个场景。测测看,你会发现很多惊喜。

最后说两句

幂等性这事儿,说大不大,说小不小。简单理解就是:让你的接口可以被安全地重试。但偏偏这么简单的事儿,多少人栽在上面。

我见过最离谱的一个案例:有个人做提现功能,提现接口没有幂等,用户点了三下,提现了三次。后来用户报警了,公司差点被请去喝茶。

你说这是技术问题吗?不完全是。这是意识问题。什么时候你能把"这个接口会被重复调用吗"这个问题变成下意识的反应,什么时候你才算真正理解了什么叫后端开发。

好了,这篇文章就到这里。希望下次群里炸了,不是因为你。

相关文章

写了三年API,我还是想把键盘扔了
写了三年API,我还是想把键盘扔了
N+1查询:那个让数据库哭爹喊娘、让老板以为你技术菜的元凶
RESTful API设计踩坑指南:我用惨烈教训换来的7条血泪经验
你以为代码写对了,API就快了?Too young,那些偷偷吃掉你200ms的幽灵
你那console.log调出来的bug,凭什么让我背锅?——日志规范实战

发布评论