大家好,我是小米,今年 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 分布式锁不是银弹。
它适合:
对性能敏感
错一次问题不大的业务
抢券、秒杀、幂等控制
它不适合:
强一致性
金融级别事务
绝对不能出错的核心链路
在这些场景下,你可能需要:
Zookeeper
数据库行锁
更高级的协调组件
写在最后如果你看到这里,我想你已经发现了:
Redis 分布式锁,本质上不是一个 API 问题,而是一个“边界问题”。
它考验的不是你会不会写代码,而是你能不能提前看到“最坏的那条路”。
就像那个公共厕所的故事一样:
锁不是挂出来就完事了
你要考虑人会不会死在里面
门会不会被别人撬开
钥匙会不会被误拿
这些,才是面试真正想听的。
END如果你觉得这篇文章对你有帮助,欢迎点个“在看”,我们下篇见~