我为什么把“接口幂等”写进了团队红线
我为什么把“接口幂等”写进了团队红线
刚做支付系统时,我觉得“幂等”是个高大上的词,离我很远。直到那次用户投诉——同一条订单被扣了两次钱,我才明白:幂等不是优雅,是保命。
那是个雨夜,用户在下单页点了两次“提交”,前端没做防重,两个请求几乎同时到达后端。我的代码是这样的:
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 会投几次。唯一能确定的是:你的接口会被重复调用,而且一定会在你最忙的时候。
如果你现在的系统里还有没做幂等的写接口,今晚就列个清单。从支付、下单、扣库存这些核心接口开始,一个一个补上。不用一步到位,但每加一个幂等,你的系统就少一分“被用户骂”的风险。毕竟,退款容易,信任难建。
- 点赞
- 收藏
- 关注作者
评论(0)