库存扣减必须做幂等,因前端防重不可靠,双击、超时重试等会导致重复执行而超卖;应以唯一业务ID+数据库唯一约束实现强幂等,通过幂等表记录状态并驱动扣减,全程事务保障一致性。

为什么库存扣减必须做幂等,而不是靠前端防重
前端防重按钮、请求拦截、Loading 状态,对库存扣减完全不可靠。用户双击、网络超时重试、网关重发、消息队列重复投递——这些场景下,POST /api/v1/order 可能被真实执行 2 次以上。一旦 UPDATE stock SET quantity = quantity - 1 WHERE sku_id = ? AND quantity >= 1 被执行两次,就超卖。幂等不是“可选优化”,是库存类接口的生存底线。
用唯一业务 ID + 数据库唯一约束实现强幂等
不要依赖 Redis 的 SETNX 做判断再扣减(存在竞态),也不要只查一次库存再更新(查+改非原子)。最稳的方式是把“扣减动作”本身变成一条带唯一约束的插入记录,再用该记录驱动扣减逻辑。
- 建一张幂等表:
idempotent_records,字段含idempotency_key(唯一索引)、order_id、sku_id、status('success'/'failed')、created_at - 扣减流程:先
INSERT INTO idempotent_records (idempotency_key, order_id, sku_id, status) VALUES (?, ?, ?, 'pending');若主键冲突(ERROR: duplicate key value violates unique constraint),直接查该记录状态并返回结果 - 插入成功后,用事务执行真正的库存扣减:
UPDATE stock SET quantity = quantity - ? WHERE sku_id = ? AND quantity >= ?;成功则UPDATE idempotent_records SET status = 'success' WHERE idempotency_key = ?,失败则设为'failed'
关键点:整个流程必须在一个数据库事务里完成(或至少保证幂等记录插入与库存更新的最终一致性),且 idempotency_key 必须由客户端生成(如 UUID 或订单号+SKU 组合),服务端不生成、不猜测。
Go 微服务中如何透传和校验幂等 Key
在 Service Mesh 或多跳调用链路中(比如 gateway → order-service → inventory-service),idempotency-key HTTP Header 不能只在第一层校验。每个下游服务都应检查并复用该值,否则 inventory-service 收到的是 order-service 生成的中间 key,失去端到端语义。
立即学习“go语言免费学习笔记(深入)”;
- gateway 层必须校验
idempotency-key是否存在,拒绝空值请求 - 所有 RPC 调用(gRPC 或 HTTP)需将该 key 作为 metadata 或 query 参数透传,inventory-service 的 handler 必须用它作为
idempotency_key字段写入数据库 - 避免在 service 层重新拼接 key(如
req.OrderID + "-" + req.SKU),客户端已提供就直接用——不同语言/序列化方式可能导致拼接结果不一致
如果你用 gRPC,建议在 proto 中显式定义 string idempotency_key = 1;,而非塞进 metadata,减少解析歧义。
幂等状态查询和超时兜底怎么设计
用户发起扣减后网络断开,客户端不知道成功与否,会重试。此时服务端不能只返回“已存在”,还得告诉客户端上次操作的真实结果(成功扣减了,还是因库存不足失败了)。
- 幂等表必须存
status和可选的error_code(如'INSUFFICIENT_STOCK'),查询时直接返回该状态,不重新执行业务逻辑 - 设置合理超时:如果某条幂等记录卡在
'pending'超过 5 分钟,说明事务卡住或服务崩溃,需后台任务扫描清理(设为'failed'并告警),否则该 key 永久阻塞后续请求 - 注意:不要用 TTL 自动删记录——业务审计、对账、问题排查都依赖历史幂等记录,删了就丢上下文
真正难的不是第一次实现,而是当库存服务拆成多个分片、或引入缓存层时,仍要保证幂等键全局唯一、状态查询强一致。这时候唯一约束和事务边界就得仔细推演,别让缓存穿透或分库分表路由把 idempotency_key 查偏了。


















