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

内容分享1周前发布
2 4 0

#晒图笔记大赛#

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

那天是周五,中午公司搞活动,限量发 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 个人(客户端并发)

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

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

  1. “阿姨,里面还有饭吗?”
  2. “有。”
  3. “那我要一份。”

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

  1. GET stock
  2. 计算 stock – 1
  3. SET stock

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

  • A 读到 stock = 10
  • B 也读到 stock = 10
  • A 写回 9
  • B 也写回 9

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

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

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

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

他一般在判断三件事:

  1. 你知不知道 问题为什么会发生
  2. 你能不能说出 至少 3 种解决思路
  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,没有银弹,核心是看业务场景。

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

  1. 能用 原子命令,绝不用锁
  2. 复杂一点,用 Lua 脚本
  3. 再复杂,用 分布式锁
  4. 可失败、可重试,用 CAS 思路

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

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

故事的结尾

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

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

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

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

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

END

你不是在写代码,你是在建立规则

我是小米,一个喜爱分享技术的31岁程序员。如果你喜爱我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!

好朋友们,我们下篇见~

© 版权声明

相关文章

4 条评论

您必须登录才能参与评论!
立即登录
  • 头像
    女士 读者

    不要把这种计算放到客户端就行了啊,所有计算按照操作进队列,再快也会有先后顺序,就不会出现错误

    无记录
  • 头像
    嗖嗖屋包袋皮具 读者

    通用方案,分布式锁,还要看锁什么?

    无记录
  • 头像
    河南星派环保科技有限公司 读者

    通用方案,分布式锁

    无记录
  • 头像
    杨发哥 读者

    收藏了,感谢分享

    无记录