当前位置: 首页 > news >正文

十万个why:锁明明还没过期,为什么另一个线程能抢进去?

做分布式开发的时候,大家对 Redis 分布式锁应该都不陌生。

为了防止锁死,比如服务器突然断电,锁永远不释放,我们通常都会给锁加一个过期时间(TTL)

写代码的时候,我们心里的算盘是这样打的:

“我的业务逻辑跑完只需要 200 毫秒,但我为了保险,给锁设了 10 秒的过期时间。这 10 秒够我跑 50 次了,绝对稳如老狗。”

但现实往往喜欢给人大嘴巴子。

在线上高并发场景下,你可能会遇到一种极其诡异的并发现象:

监控显示,线程 A 还在执行业务逻辑,锁的过期时间也没到(理论上),但线程 B 竟然大摇大摆地抢到了锁,开始修改同一份数据。脏数据就这么产生了~

那这把明明还没过期的锁,到底是怎么失效的。

1. 案发现场

为了复现这个问题,我们先看一段看似标准的分布式锁伪代码:

// 1. 加锁,设置 10秒 过期 if (redis.setnx("lock_key", "thread_A", 10s)) { try { // 2. 执行业务逻辑 (预计 200ms) doBusiness(); } finally { // 3. 释放锁 // (这里通常会校验是不是自己的锁,先省略) redis.del("lock_key"); } }

这段代码在 99.99% 的时间里都能完美工作,但你的价值往往就是去解决那 0.01% 疑难问题。

直到有一天,服务器负载突然飙升,应用触发了一次严重的Full GC,或者宿主机发生了短时间的卡顿

就在这瞬间,事故发生了。

2. 时间被冻结

我们总以为时间是连续的、均匀流逝的。但在计算机的世界里,尤其是 Java 的世界里,时间是可以暂停的。

导致锁失效的真凶,正是 JVM 的STW(Stop-The-World)机制。

我们把时间放慢看,在微观的时间轴上到底发生了什么:

(0s) 线程 A 成功拿到锁,过期时间10s

(0.1s) 线程 A 刚开始执行doBusiness(),才跑了一行代码。

(0.2s)事故来了!JVM 触发了一次耗时极长的 Full GC,可能是因为内存泄漏,或者堆太大回收慢。

此时JVM 暂停了包括线程A在内所有工作线程,线程 A 停在了第 0.2s,它觉得自己才刚开始跑。但Redis 服务端的时间并没有停!Redis 那边的倒计时还在正常走。

(10.2s) 10 秒过去了,Redis 发现lock_key过期了,于是删除了这个 Key。

(10.3s) 线程 B 进来请求加锁,因为它发现 Redis 里没锁,所以成功拿到了锁。

(12s)Full GC 结束,线程 A 被唤醒了。线程 A 完全不知道自己sleep 12 秒,以为自己才跑 0.2s,手里还攥着锁。于是线程 A 继续执行剩下的业务逻辑,往数据库里写数据。

(12.1s) 线程 B 同时也在写数据。

结果线程 A 和 线程 B 同时在操作数据,锁彻底失效了。

这就是分布式系统中最经典的时间跳变问题,你以为你拥有 10 秒,其实在 STW 面前,这 10 秒可能瞬间就蒸发了。

3. 加长过期时间行不行?

很多同学的第一反应是:那我就把过期时间设长点,设成 10 分钟,GC 总不能停 10 分钟吧?

这确实能降低概率,但治标不治本。

  1. 副作用大:如果你的服务真的挂了,锁要等 10 分钟才能释放,这期间业务就瘫痪了。

  2. 不可控:你永远不知道下一次 STW 会停多久,或者网络延迟会有多大。

Watchdog 续命

这是目前业界最主流的方案,比如 Java 的Redisson客户端就实现了这个机制,俗称看门狗

它的原理是:

  1. 线程 A 拿到锁,过期时间设为 30s。

  2. Redisson 会在后台启动一个守护线程

  3. 每隔 10s(默认是过期时间的 1/3),守护线程就去 Redis 检查一下:线程 A 还活着吗?还持有锁吗?

  4. 如果还持有,就自动把锁的过期时间重新续满到 30s。

这样一来,只要线程 A 的进程没挂,即使正在 Full GC,只要 GC 结束,守护线程也会恢复工作去续期,锁就永远不会过期。

只有当线程 A 的机器彻底宕机,守护线程也挂了,锁才会因为没人续期而自动释放。

4. 看门狗就万无一失了吗?

这就完了?如果是普通的业务,Redisson 确实够用了。

但如果你做的是金融级的核心业务,还要考虑到一种更极端的黑天鹅场景:

如果 GC 暂停发生在“最后一步”怎么办?

想象一下这个场景:

  1. 线程 A 拿到了锁,看门狗也在正常工作。

  2. 线程 A 查完数据库,计算完了金额,正准备执行最后一步UPDATE语句

  3. 突然,超长 Full GC 来了。

  4. 这次 GC 停得太久,连后台的“看门狗”线程也被暂停了,没法去 Redis 续期。

  5. Redis 里的锁过期了。

  6. 线程 B 拿到了锁,修改了金额。

  7. GC 结束线程 A 苏醒,它不需要再请求 Redis,而是直接把那条UPDATE语句发给了数据库。

这又是数据覆盖了,即使有看门狗,在极端并发下,分布式锁依然无法保证 100% 的互斥安全性。

这也不能算 Bug,这是 CAP 理论总是不能十全十美。在异步网络模型中,仅仅依靠锁和时间,是无法做到绝对安全的。

终极解法:乐观锁

要彻底解决这个问题,我们不能光靠锁,还得靠存储层数据库兜底

这个方案叫Fencing Token 栅栏令牌,或者通俗点叫乐观锁/版本号

  1. 加锁时返回版本号:线程 A 抢 Redis 锁的时候,Redis 生成一个递增的数字 Token,比如 33。

  2. 带版本号更新:线程 A 在操作数据库时,必须带上这个 33。

    UPDATE accountSET money = 100 WHEREid = 1AND current_token < 33; -- 甚至更简单的乐观锁: UPDATEaccountSET money = 100, version = version + 1 WHEREid = 1ANDversion = old_version;
  3. 校验:如果中间有线程 B 抢占了锁,拿到了 Token 34 并修改了数据,线程 A 的 Token 33 就会变成旧版本,数据库的更新操作就会失败,影响行数为 0。

写在最后

回到标题的问题:锁明明还没过期,为什么会被别人抢走?

因为在分布式的世界里,我的时间大家的时间往往不是一回事。要好好的去理解这句话!!!

当你下次写分布式锁的时候,对于剩下的那 1%,如果你在做涉及钱的核心业务,请务必加上数据库层面的乐观锁做最后的兜底。

千万别太相信时间,很多奇怪问题都是时间引起的。

看完等于学会,点个赞吧!!!

http://www.jsqmd.com/news/433246/

相关文章:

  • 归来仍是菜鸡-Charles断点
  • 一招搞定!自定义MyBatis拦截器,这才是我想看的SQL日志!
  • 千万级订单表新增字段,不想锁表这么弄!
  • 智慧猪场管理系统云迈科技数字化解决方案解析— 破解传统规模化养殖痛点,实现降本增效
  • P4551 最长异或路径
  • FAST-LIVO2 快速总结(相对详细版)
  • AI辅助的投资组合归因分析
  • 2026年北京办公室装修公司推荐:性价比高的 - 余小铁
  • 阵列信号处理——学习笔记 第6章 波束旁瓣设计
  • 揭秘大数据领域数据压缩的高效秘诀
  • 阵列信号处理——学习笔记 第7章 波束主瓣设计
  • 大数据时代:数据标注的5大核心技术与实践指南
  • 智能插座:AI Agent的用电优化管理
  • Docker 容器端口映射不生效 - Higurashi
  • 大数据安全攻防演练:真实案例分析与解决方案
  • OpenClaw安装部署教程
  • 爬虫开发实战:普通代理与隧道代理的选择指南
  • VK_KHR_WIN32_SURFACE_EXTENSION_NAME 未定义的分析和解决
  • 2026 AI招聘软件技术实测:Top5排行榜大揭秘!传统ATS只是“油改电”?这款原生智能体才是全兜底标配 | 工具测评 | 简历筛选 | 降本增效
  • 玩转全协议快充移动电源 SOC:高压 SCP + 双向 PD3.0 实战指南
  • 专业的贵州商务酒店大型会展会议酒店 - 品牌企业推荐师(官方)
  • 雷电预警装置部署于:机场、景区、学校等场所,有效规避雷击事故
  • 可以替换 sap的中大型开源erp软件erp5的新旧界面风格对比
  • 资深鸿蒙开发工程师:技术深度、生态融合与实战精要
  • 数组TOP-K问题:求前K个最小元素的多种解法与C++实现
  • 鸿蒙系统开发工程师:深入解析技术栈与面试指南
  • 新疆大量元素水溶肥哪家好? - 品牌企业推荐师(官方)
  • 【vllm】DP 负载均衡
  • 华为鸿蒙开发指南:从基础到实战与面试准备
  • 问舟科技GEO AI搜索优化 开启AI搜索营销新时代 - 品牌企业推荐师(官方)