云霞资讯网

一道 Redis 面试题,问倒 80% 的 Java 社招:并发 Key 到底怎么破?

前几个月,我在公司食堂吃午饭的时候,遇到了一个挺有意思的场面。那天是周五,中午公司搞活动,限量发 100 份小龙虾饭。



前几个月,我在公司食堂吃午饭的时候,遇到了一个挺有意思的场面。

那天是周五,中午公司搞活动,限量发 100 份小龙虾饭。我端着餐盘排队,前面长得跟 JVM GC 日志一样,看不到尽头。食堂阿姨在窗口喊:“别挤别挤,一个一个来。”

结果呢?还是挤。有人趁阿姨转身,直接伸手拿;有人觉得“反正快轮到我了”,往前多蹭一步;还有人嘴上说着“我只看一眼”,身体已经挪进窗口了。短短几分钟,秩序彻底乱了。

后来食堂经理一拍桌子,说了一句让我这个写 Java 的人瞬间 DNA 动了的话:“一个窗口同时只能服务一个人,谁抢谁滚蛋!”

我当时就笑了,这不就是并发竞争资源吗?而且,这种事每天都在我们代码里发生,只不过小龙虾换成了 Redis 里的 Key。

什么是 Redis 的并发竞争 Key?

社招面试里,Redis 几乎是必问项。而只要 Redis 一问深入,面试官大概率会甩给你一句:

“如果多个线程或多个服务实例 同时操作同一个 Redis Key,你怎么保证数据是对的?”

这句话的潜台词,其实特别简单:

同一份数据,被很多人同时改,会不会乱?

我们先不用技术名词,直接换成人话。还是拿食堂举例。

小龙虾饭:库存 100 份

Redis 中的 Key:stock = 100

每来一个人,就做一次操作:stock = stock - 1

问题来了。如果 100 个人排队,并且他们是“一个一个来”,那肯定没事。但如果 100 个人同时冲到窗口,每个人都看到“窗口里还有饭”,然后一起伸手抢,那结果一定是:

有人没抢到

有人抢了两份

有人抢到了空气

在 Redis 里,这种现象叫:并发竞争 Key 问题,它本质上就是:

多个客户端并发对同一个 Key 做非原子操作,导致数据不一致。

为什么 Redis 单线程,还会并发出问题?

很多同学在这一步就懵了。

“Redis 不是单线程吗?怎么还会有并发问题?”

这问题问得非常好,我当年也被坑过。Redis 的单线程,指的是:

Redis 处理命令的执行是单线程的

但请注意几个关键词:

Redis 在 服务端 是单线程

并发问题,发生在 客户端

换成食堂的例子:

窗口里只有一个阿姨(Redis 单线程)

窗口外排着 100 个人(客户端并发)

如果每个人只做一件事,比如:“阿姨,给我一份小龙虾饭。”

那没问题。但如果每个人都说的是:

“阿姨,里面还有饭吗?”

“有。”

“那我要一份。”

这三步不是原子的。回到 Redis:

GET stock

计算 stock - 1

SET stock

这三步,不是一次性完成的。于是就会出现经典场景:

A 读到 stock = 10

B 也读到 stock = 10

A 写回 9

B 也写回 9

少卖了一份,但系统以为没问题。这就是 Redis 并发竞争 Key 的根源。

面试官真正想听的是什么?

我后来也当过面试官,说句实在话:

面试官不是想听你背API,而是想看你是否理解“并发控制”的本质

他通常在判断三件事:

你知不知道 问题为什么会发生

你能不能说出 至少 3 种解决思路

你是否知道 不同方案的适用场景和坑

如果你只说一句:“用 Redis 分布式锁。”,恭喜你,通常只能拿到 60 分。

第一类解决方案:原子操作(最稳但有限)

我们先从最简单、最底层的办法讲。Redis 有一个非常硬核的能力:

单条命令是原子执行的

就像食堂规定:“不准问还有没有,想吃就直接报数量,由我来扣。”

如果库存扣减是这样完成的,就没问题。在 Redis 里,对应的是:

INCR

DECR

INCRBY

DECRBY

也就是说,把:GET + 计算 + SET,改成:DECR stock,这一步,由 Redis 单线程完成,天然不会并发乱。

优点:

简单

性能极好

不需要锁

缺点:

只能用于 简单数值场景

逻辑一复杂就不行了

所以在面试中,你可以明确说一句:

“如果只是计数、扣库存、点赞数这种场景,优先使用 Redis 原子命令。”

这一句话,会让面试官点头。

第二类解决方案:Lua 脚本(原子中的瑞士军刀)

原子命令太简单,那复杂一点怎么办?比如:

判断库存是否大于 0

判断用户是否已经下单

扣库存 + 写订单记录

这些就不是一条命令能搞定的。这时候,Redis 给你准备了一把 瑞士军刀:Lua 脚本

Redis 天生支持:Lua 脚本整体原子执行,换句话说:你可以把一段逻辑,塞进 Redis 里一次性执行。

这就相当于食堂改制度了:“我不听你说话了,你把点单、扣库存、打饭写在一张纸上,我一次性照着做。”

不管外面多少人排队,一次只处理一个 Lua 脚本。

优点:

真·原子操作

能处理比较复杂的业务逻辑

性能比分布式锁高

缺点:

Lua 成本高,维护难

脚本写复杂了,排查问题很痛苦

面试时可以这样总结:

“当业务逻辑稍复杂,但又要求高性能和强一致性时,Lua 脚本是非常好的选择。”

第三类解决方案:Redis 分布式锁(最常考)

接下来,来到面试官最爱的一段。

“那如果是多个服务实例并发操作 Redis,你怎么控制?”

答案呼之欲出:分布式锁

我们再回到食堂。经理说:“谁要抢饭,先拿号。拿到号的人,才能进窗口。”

这个“号”,就是锁。

1、最基本的思路

谁先拿到 Key

谁就有资格操作共享数据

操作完释放 Key

在 Redis 里,这个“拿号”的动作通常是:SETNX

但注意,真正好的回答,不是说“用 SETNX 就行了”。你必须意识到,这里面到处是坑。

2、面试官最爱追问的几个坑

如果你说“Redis 分布式锁”,面试官往往会继续问:

锁没释放怎么办?

服务死了怎么办?

锁被别人删了怎么办?

主从切换安全吗?

Redis 宕机怎么办?

如果你能顺着这些问题往下聊,基本就稳了。我一般会用一句话总结:“分布式锁的核心不是‘加锁’,而是‘安全地加锁和释放锁’。”

优点:

通用性强

业务逻辑清晰

社招最容易接受的方案

缺点:

实现复杂,坑多

性能不如原子操作和 Lua

第四类解决方案:版本号 / CAS 思路(很多人忽略)

再高级一点,有些面试官喜欢考你“设计者思维”。这时候,可以引入:版本号 / CAS 思路

换成生活例子就是:“我只接受在我看到的状态基础上提交修改,否则就失败。”

在 Redis 中,常见做法是:

数据里带一个 version

更新时校验 version 是否一致

不一致就重试

这种方案非常适合:

允许失败

允许重试

不要求一次成功

比如配置更新、规则更新等。这个点说出来,面试官通常会觉得你思路很全。

到底该怎么选?一句话送你

如果你只记一句话,我建议你记这个:

Redis 并发竞争 Key,没有银弹,核心是看业务场景。

我自己在实际工作中,大概遵循这个顺序:

能用 原子命令,绝不用锁

复杂一点,用 Lua 脚本

再复杂,用 分布式锁

可失败、可重试,用 CAS 思路

面试时,把这四层逻辑说清楚,基本就是高分答案。

故事的结尾

那天的小龙虾饭,最后怎么解决的?

经理最后只保留了一个窗口,地上画了一条线:“谁越线,直接取消资格。”

大家老老实实排队,100 份饭,一份没多,一份没少。我端着那盒小龙虾饭,突然就明白了一件事:

分布式系统解决并发的核心,从来不是快,而是秩序。

而 Redis 并发竞争 Key,本质上也是如此。

END

你不是在写代码,你是在建立规则。好朋友们,我们下篇见~