|

Aimee

Write the Code. Change the World.

双击了下单按钮,收到两条扣款短信

· 分享镜

双击了下单按钮,收到两条扣款短信

用户来投诉:买了一件商品,银行卡被扣了两笔钱,订单系统里也出现了两条一模一样的记录。

前端按钮有 loading 状态,点完就禁用了。但这次是网络抖动,第一个请求超时,浏览器自动重发了一次,两个请求都到了后端,都创建成功了。


Submit Token:服务端颁发的一次性票据

防重复下单最可靠的方式是 Submit Token(也叫幂等 Token、防重 Token):

  1. 打开下单页面时,前端向服务端请求一个 Token
  2. 服务端生成 UUID,写入 Redis(TTL 10 分钟)
  3. 用户提交订单时,把这个 Token 带上
  4. 服务端收到请求,先用 SET NX 消耗 Token:Token 存在才消耗,不存在说明已被用过或过期
  5. 消耗成功 → 创建订单;消耗失败 → 直接返回"请勿重复提交"
// 消耗 Token(原子操作:存在才删,不存在返回 false)
Boolean consumed = redisTemplate.delete("order:token:" + submitToken);
if (!Boolean.TRUE.equals(consumed)) {
    throw new BizException("请勿重复提交,或提交已超时");
}
// Token 消耗成功,继续创建订单
createOrder(userId, items);

Token 由服务端颁发,客户端只能持有不能伪造,比客户端自己生成 requestId 更可靠——客户端可能在不同标签页重用同一个 requestId。


数据库兜底:唯一索引

Token 机制拦截了大多数情况,但如果 Redis 出现短暂问题,两个请求都越过了 Token 检查,数据库要做最后一道防线。

订单表加联合唯一索引:

UNIQUE KEY uk_user_biz (user_id, biz_no)
-- biz_no = 业务流水号,由前端生成(用户会话 + 时间戳 + 随机数)

两笔重复的订单必然带着同样的 biz_no,数据库唯一键冲突,第二条写入直接失败,应用层捕获异常后返回已有订单。


为什么不只用数据库唯一索引

数据库唯一索引能防重复写入,但有个问题:两个请求同时通过应用层校验、同时到数据库,一个成功、一个报主键冲突,这期间锁库存的操作可能已经执行了两次。

Submit Token 在更早的阶段就拦住了,不让重复的业务逻辑跑两遍。两道防线的分工:Token 拦业务逻辑唯一索引拦数据写入


重试和幂等的区别

有个容易混的概念:这里说的防重不是"让接口重试安全",而是"让用户操作防双提"。

重试安全(如支付回调):同一个操作重试多次,结果一样。 防双提:用户操作了两次(或客户端发了两次),只生效一次。

两者都要做,但用的机制不同:重试安全用 requestId + CAS,防双提用 Submit Token + 唯一索引。下单场景同时需要两个——Token 防用户操作层面的重复,唯一索引防网络重传层面的重复。

评论0

登录后参与评论。

还没有评论,来抢沙发吧。

回到顶部