加了分布式锁,为什么用户还是被扣了两次钱?

张开发
2026/4/14 10:46:09 15 分钟阅读

分享文章

加了分布式锁,为什么用户还是被扣了两次钱?
分布式锁理想中的“单人单间”在程序员的逻辑里加了锁的业务应该是绝对安全的阶段描述加锁lock(order_123, timeout30s)成功。执行慢慢处理业务逻辑查库、算钱、写库。释放业务完成主动删锁。结果无论多少人并发同一个订单永远只会被处理一次。现实是你的业务执行太慢了或者网络抖动了一下锁的“有效期”到了而你的代码还在跑。这时候**“幽灵”**就出现了。⏳ 第一关消失的守护神Lock Timeout场景你给一个扣款操作加了 10 秒的分布式锁。结果那天数据库有点卡或者 JVM 发生了 Full GC垃圾回收停顿导致你的业务代码跑了 12 秒。恐怖故事第 10 秒你的锁自动过期了Redis 把锁删了。第 10.1 秒另一个线程 B 看到锁没了开心地加锁成功开始执行同样的扣款逻辑。第 12 秒线程 A 终于跑完了提交了事务扣了一次钱。第 15 秒线程 B 也跑完了提交了事务又扣了一次钱。结果用户明明只买了一个东西卡里却被扣了两次钱。 第二关上游的“夺命连环催”Retry Storm如果说锁过期是内忧那上游重试就是外患。场景上游系统比如 App 或网关调用你的扣款接口设置了 5 秒超时。恐怖故事你加锁成功开始干活。第 5 秒上游等不及了认为你挂了直接报错超时。用户视角看到“支付失败”于是疯狂点击“重试”按钮。第 6 秒上游发起了第二次请求。此时你第一个请求的锁还没释放。死循环第二次请求因为拿不到锁而失败等你第一个请求终于跑完释放锁时第三个重试请求正好钻进来再次执行了业务。后果你的日志里全是锁冲突的报错而数据库里却出现了重复的流水。 第三关误删他人的锁The Wrong Unlock场景线程 A 业务超时锁过期了。线程 B 拿到了新锁。恐怖故事线程 A 终于干完活了它执行了del(lock_order_123)。悲剧它删掉的其实是线程 B 刚刚加的锁连锁反应线程 C 看到锁没了也冲进来……结果整个系统的并发保护像多米诺骨牌一样全线崩溃锁成了摆设。️ 拆弹专家如何让锁更硬气为了爬过这座山你需要三件装备1. 给锁续命看门狗机制 (Watchdog)不要拍脑袋定死一个超时时间。使用像Redisson这样的库它有一个“看门狗”线程。只要你的业务没跑完它会每隔 10 秒帮你自动延长锁的有效期。只有当你主动释放或者进程彻底死掉锁才会消失。2. 解铃还须系铃人唯一标识 (Lock Value)加锁时Value 不要写1要写一个随机的UUID。释放锁时先判断里面的 UUID 是不是自己当初写的。如果是才删如果不是说明这是别人的锁动都别动。3. 终极防御幂等性 (Idempotency) —— 唯一补救方案记住永远不要把系统的安全性完全寄托在“锁”上。在数据库层面加唯一索引。记录每个请求的RequestID。业务逻辑第一步查一下这个RequestID是否已经处理成功。只有锁和幂等并用才能在重试风暴中活下来。 结语分布式锁只能解决“性能”问题防止大家一拥而上不能解决“正确性”问题。当网络不可靠、时间不可靠时锁随时会背叛你。只有把数据库当成最后一道防线通过幂等设计让系统无视重试你才能在深夜里睡个安稳觉。

更多文章