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

71 阅读6分钟



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

  1. A 成功执行了 setnx
  2. A 拿到锁
  3. A 进入临界区
  4. 服务器宕机了

结果呢?

  • lock_key 还在
  • 但 A 永远不会再执行 del

厕所门被反锁了,钥匙还在尸体口袋里。 后面的人排到天荒地老。

于是,我们想到:给锁加个“自动解锁”

很快,大家想到:“那我给锁加个过期时间不就好了?”

于是代码变成了:

  1. setnx lock_key value
  2. expire lock_key 30

逻辑是:

  • A 拿到锁
  • 30 秒后自动释放

听起来完美。但面试官如果在这一步打断你,问题就来了。

第二个坑:锁设置成功了,但过期时间没来得及设置

你注意到了吗?这其实是两条命令

  1. setnx
  2. expire

它们 不是原子操作。如果发生这种情况:

  • setnx 成功
  • 网络抖了一下
  • expire 没执行成功
  • 服务器又挂了

结果呢?你以为你加了自动解锁,其实没有。 厕所再次被永久占用。

真正成熟方案的第一步:一次性完成所有动作

这时候,Redis 提供了一个“组合拳”:

SET lock_key value NX EX 30

它的含义是:

  • NX:key 不存在才能设置
  • EX:设置过期时间
  • 原子操作

这行命令的出现,直接淘汰了前面 80% 的“伪分布式锁”。

如果你在面试中说到这里,面试官大概率会继续往下追。

value 为什么不能随便写?

我再给你讲一个真实事故。有一次,我们项目里有个新人,解锁的时候直接写了:

DEL lock_key

结果,在并发条件下,出了一个非常隐蔽的问题。场景是这样的:

  1. 线程 A 拿到锁,设置过期 30 秒
  2. A 执行逻辑非常慢
  3. 30 秒到了,锁自动过期
  4. 线程 B 拿到了新锁
  5. 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岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!

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