云霞资讯网

能进大厂的 Redis 分布式锁,和你现在写的差在哪?

大家好,我是小米,今年 31 岁。写这篇文章的时候,我正坐在公司工位上,盯着 禅道 上一个“看似简单”的 Bug 单子



大家好,我是小米,今年 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

如果你觉得这篇文章对你有帮助,欢迎点个“在看”,我们下篇见~