大家好,我是小米,今年 31 岁。写这篇文章的时候,我正坐在公司工位上,盯着 禅道 上一个“看似简单”的 Bug 单子发呆。这个 Bug 的标题只有一句话:
“生产环境:订单重复扣款,概率出现”
如果你是 Java 工程师,看到这句话,后背基本已经开始冒冷汗了。那一刻,我脑子里闪过的不是 JVM,不是 GC,也不是 SQL,而是一个老朋友——分布式锁。也是后来,我才意识到:
这玩意,几乎是每个 Java 社招面试都会问的,但真正理解透的人,并不多。
今天我想换一种方式,不从 API 讲,不从源码讲,而是给你讲一个故事。
一个关于 “锁” 的故事。
从一个“公共厕所”的故事说起
我先问你一个问题。假设你在一个旅游景点,这里只有 一个公共厕所,但排队的人非常多。这个厕所就是一个共享资源。
为了不出事,门口挂了一块牌子: “使用中,请勿进入”
谁进去,谁把门锁上,用完再解锁。在单机世界里,这个机制非常简单:
- synchronized
- ReentrantLock
就像你家里的卫生间,门就在你眼前,你一把就锁得住。
但现实是:这是一个“分布式厕所”
问题来了。有一天,这个景点突然火了,游客暴增,管理人员一拍脑袋:“不行了,一个厕所扛不住,我们多建几个入口吧。”
于是厕所不止一个门了。 有东门、西门、南门、北门,每个门口都有一个排队的队伍。而且每个门口都有一个保安,他们之间不认识、也不交流。
这,就是分布式系统。
- 多台服务器
- 多个 JVM
- 多个实例
- 多个线程
这时候你发现:
单机锁,已经不管用了。
面试官的问题,通常从这里开始
我在一次社招面试中,被问到这样一个问题:“你说说,Redis 怎么实现分布式锁?”
我当时的第一反应是:“setnx + expire。”
面试官点了点头,又笑了一下:“那你详细说说。”这一个“详细”,往往就是分水岭。
最朴素的 Redis 分布式锁长什么样?
我们先回到最简单的版本。核心目标只有一个:
在分布式环境中,保证同一时间,只有一个线程能拿到锁。
用 Redis 实现,思路就是:
- 锁 = Redis 中的一个 key
- 拿锁 = 抢这个 key
- 解锁 = 删除这个 key
我们先写一个“原始人版本”的锁。
- setnx lock_key value
含义很简单:
- 如果 key 不存在,设置成功,返回 1
- 如果 key 已存在,设置失败,返回 0
于是:
- 返回 1:我抢到锁了
- 返回 0:锁被别人占了
听起来没毛病,对吧?但问题很快就来了。
第一个坑:人突然死在厕所里
假设有一个用户 A:
- A 成功执行了 setnx
- A 拿到锁
- A 进入临界区
- 服务器宕机了
结果呢?
- lock_key 还在
- 但 A 永远不会再执行 del
厕所门被反锁了,钥匙还在尸体口袋里。 后面的人排到天荒地老。
于是,我们想到:给锁加个“自动解锁”
很快,大家想到:“那我给锁加个过期时间不就好了?”
于是代码变成了:
- setnx lock_key value
- expire lock_key 30
逻辑是:
- A 拿到锁
- 30 秒后自动释放
听起来完美。但面试官如果在这一步打断你,问题就来了。
第二个坑:锁设置成功了,但过期时间没来得及设置
你注意到了吗?这其实是两条命令:
- setnx
- expire
它们 不是原子操作。如果发生这种情况:
- setnx 成功
- 网络抖了一下
- expire 没执行成功
- 服务器又挂了
结果呢?你以为你加了自动解锁,其实没有。 厕所再次被永久占用。
真正成熟方案的第一步:一次性完成所有动作
这时候,Redis 提供了一个“组合拳”:
SET lock_key value NX EX 30
它的含义是:
- NX:key 不存在才能设置
- EX:设置过期时间
- 原子操作
这行命令的出现,直接淘汰了前面 80% 的“伪分布式锁”。
如果你在面试中说到这里,面试官大概率会继续往下追。
value 为什么不能随便写?
我再给你讲一个真实事故。有一次,我们项目里有个新人,解锁的时候直接写了:
DEL lock_key
结果,在并发条件下,出了一个非常隐蔽的问题。场景是这样的:
- 线程 A 拿到锁,设置过期 30 秒
- A 执行逻辑非常慢
- 30 秒到了,锁自动过期
- 线程 B 拿到了新锁
- A 执行完了,调用 del,把 B 的锁删了
你看懂了吗?A 删的,已经不是自己的锁了。
于是,锁必须“认主”
为了解决这个问题,每个锁必须有一个唯一身份标识。最常见的就是:
- UUID
- 线程 ID + JVM ID
流程就变成了:
- 获取锁时:SET lock_key uuid NX EX 30
- 解锁时:
-
- 先判断 value 是否等于自己的 uuid
- 再删除
这时,面试官一般会点头,但马上再问一句:“那你怎么保证判断和删除是原子性的?”
恭喜你,进入最后一关。
最终形态:Lua 脚本的登场
Redis 是单线程模型,并且支持 Lua 脚本原子执行。于是,解锁逻辑通常写成一段 Lua:
- 如果 key 存在
- 并且 value 等于当前线程的 uuid
- 才允许删除
这一步,才是一个“合格的 Redis 分布式锁”的完整形态。
说回面试:面试官真正想考什么?
讲了这么多,其实我想告诉你一件事:
面试官不是想听你背命令,而是想看你“有没有踩过坑”。
他们真正关心的通常是:
- 你是否意识到分布式环境的不确定性
- 你是否考虑过锁失效、误删、并发边界
- 你是否知道这个东西 为什么要这样设计
Redis 分布式锁,到底适不适合你?
我最后说一句非常现实的话。Redis 分布式锁不是银弹。
它适合:
- 对性能敏感
- 错一次问题不大的业务
- 抢券、秒杀、幂等控制
它不适合:
- 强一致性
- 金融级别事务
- 绝对不能出错的核心链路
在这些场景下,你可能需要:
- Zookeeper
- 数据库行锁
- 更高级的协调组件
写在最后
如果你看到这里,我想你已经发现了:
Redis 分布式锁,本质上不是一个 API 问题,而是一个“边界问题”。
它考验的不是你会不会写代码,而是你能不能提前看到“最坏的那条路”。
就像那个公共厕所的故事一样:
- 锁不是挂出来就完事了
- 你要考虑人会不会死在里面
- 门会不会被别人撬开
- 钥匙会不会被误拿
这些,才是面试真正想听的。
END
我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
如果你觉得这篇文章对你有帮助,欢迎点个“在看”,我们下篇见~