【场景题】如何防止重复下单1. 如何防止重复下单2. ✅ 方案一数据库唯一索引3. ✅ 方案二前端防抖只是辅助4. ✅ 方案三Token 防重复提交常见4. ✅ 方案四订单状态机控制核心5. ✅ 方案五分布式锁高并发场景6. 真正生产级推荐组合方案7. 不同业务推荐方案1. 如何防止重复下单当网络出现了阻塞用户不断点击下单按钮出现了重复下单应该如何去防止呢?普通的下单流程首先会由用户发起一个下单操作然后会由订单服务去保存一条订单信息此时订单的状态是【未支付】然后再由用户去确认订单。由支付服务把当前这条订单修改为【待支付】然后再跳转到支付界面接着由用户去选择对应的支付方式来进行钱包支付。支付完成之后呢由支付平台进行回调回调到支付服务再去调用订单服务修改状态为【已支付】。哪一步会出现重复操作而出现幂等性操作呢只有在用户下单的哪一步会出现这个问题2. ✅ 方案一数据库唯一索引适用场景同一个用户 同一个业务条件 只能有一个订单例如UNIQUEKEYuk_user_product(user_id,product_id,biz_type)或者UNIQUEKEYuk_order_no(order_no)Java 代码try{orderMapper.insert(order);}catch(DuplicateKeyExceptione){// 已存在订单直接查询返回returnorderMapper.selectByBizKey(...);}✅ 优点简单绝对可靠不怕并发❗注意一定要以数据库为最终裁决者。3. ✅ 方案二前端防抖只是辅助提交后按钮 disabledloading防抖 1~2 秒但这只是体验优化不是安全方案。4. ✅ 方案三Token 防重复提交常见流程用户进入下单页服务端生成一个 UUID token存 Redis有效期 5 分钟下单时必须携带该 token使用 Lua 脚本原子校验并删除Redis Luaifredis.call(get,KEYS[1])ARGV[1]thenreturnredis.call(del,KEYS[1])elsereturn0endJava 逻辑如果返回 1 → 允许下单如果返回 0 → 重复请求✅ 优点抗前端重复点击抗接口重试❗缺点不能防止真正的并发写库冲突还是要配合数据库唯一索引优雅的使用Redission来解决问题先说清楚Token 防重复提交在干嘛同一个页面在短时间内用户重复点“提交”按钮。流程本质是① 页面加载 → 后端生成一个 token ② token 存 Redis设置过期时间 ③ 用户提交时必须带这个 token ④ 后端校验 token 是否存在 ⑤ 校验成功 → 删除 token → 继续执行 ⑥ 再次提交 → token 不存在 → 拒绝核心点token 必须“校验 删除”原子执行 否则会并发通过。标准 Redisson 实现第一步生成 tokenGetMapping(/order/token)publicStringgenerateToken(){StringtokenUUID.randomUUID().toString();RBucketStringbucketredissonClient.getBucket(order:token:token);bucket.set(1,5,TimeUnit.MINUTES);returntoken;}第二步下单时校验 token⚠️ 关键点必须原子删除Redisson 有一个非常好用的方法// 这是原子的bucket.getAndDelete()最终代码PostMapping(/order)publicStringcreateOrder(RequestParamStringtoken){RBucketStringbucketredissonClient.getBucket(order:token:token);Stringvaluebucket.getAndDelete();// 原子操作if(valuenull){thrownewRuntimeException(请勿重复提交);}// 执行业务逻辑return下单成功;}为什么 getAndDelete 是安全的Redis内部执行 GET key DEL key 但在Redisson中是用Lua保证原子执行的。 所以 第一次请求 → 拿到值 → 删除成功 第二次请求 → key 已不存在 → 返回null不会出现并发穿透。4. ✅ 方案四订单状态机控制核心订单必须有明确状态INIT → PAYING → PAID → CANCELLED创建订单前select*from order where user_id?and product_id?and status in(INIT,PAYING)存在则直接返回已有订单。这是业务级幂等。5. ✅ 方案五分布式锁高并发场景RLocklockredisson.getLock(order:userId);lock.lock();try{// 查询是否已有订单// 创建订单}finally{lock.unlock();}适用场景秒杀抢购打车平台库存扣减❗但要注意锁粒度不要锁全局6. 真正生产级推荐组合方案前端防抖(置灰) Token 防重复 数据库唯一索引 订单状态机控制7. 不同业务推荐方案场景推荐方案普通下单唯一索引 状态机秒杀Redis Lua 唯一索引支付回调幂等号 唯一索引打车平台用户锁 状态机MQ 消费幂等表记录 message_id