这是一份训练营学员刚发给我的面经,岗位是Go后端开发。
他说整场面试下来最大的感受就是——面试官太会问了,一个问题能揪着往下挖三层。
特别是关于“库存防超卖”这块,从业务方案一直追问到Lua脚本底层指令和线上隐患。
这部分他准备得最充分,交流起来比较顺畅。我把这份面经整理出来,希望能帮到更多正在准备面试的朋友。
第一部分:Redis Lua连环问实录
这一部分是整场面试耗时最长、问得最细的环节。
面试官:你在过去工作经历遇到过哪些问题?印象最深的?
学员的回答思路: 印象最深的是秒杀模块的库存超卖问题。最开始逻辑放在应用代码里:先从Redis查库存,大于0就执行DECR扣减。高并发压测下超卖严重,因为“查询”和“扣减”两步在Redis里不是原子性的——两个请求同时查到库存为1,都认为有货,结果都去扣减,库存变成-1。
面试官:那你是怎么解决的?
学员的回答思路: 把判断和扣减写进一段Lua脚本里,交给Redis执行。Lua脚本执行期间其他命令插不进来,保证“查”和“扣”是连续动作,不会被别的请求打断。
面试官:Lua脚本不会遇到问题吗?比如指令用错或者线上出故障?
学员的回答思路(详细展开): 会的,有几个必须注意的点:
-
指令选择:用
redis.call不用redis.pcall。redis.call遇到语法错误或Key类型错误会直接抛异常并停止脚本;redis.pcall会捕获错误继续执行,如果第一步GET就错了,后面DECR再执行数据就乱了,不符合扣减需求。 -
阻塞风险:Lua脚本里不能有复杂循环或操作几十MB的大Key。Redis是单线程处理命令的,脚本执行期间独占主线程。如果执行时间超过几十毫秒,其他客户端请求都会被卡住,导致Redis延迟飙升,属于线上事故。
-
Cluster限制:Redis Cluster模式下,Lua脚本操作的多个Key必须落在同一个哈希槽上,否则报错。跨槽操作需要用Hash Tag强制让Key落在同一个节点。
-
脚本缓存:直接用
EVAL传完整脚本浪费带宽。线上做法是先用SCRIPT LOAD将脚本缓存到服务端,拿到SHA1摘要,后续用EVALSHA执行。Redis重启后缓存会丢失,客户端代码需处理NOSCRIPT错误,报错时fallback到EVAL重传脚本。
面试官:Lua脚本都有哪些指令?你在防超卖里具体怎么写的?
学员的回答思路(详细指令说明):
Lua脚本里操作Redis数据主要靠 redis.call 函数,第一个参数是Redis命令的字符串,后面跟参数。
他写的防超卖脚本逻辑大概是这样的:
-- KEYS[1] 是商品库存Key,比如 stock:1001
-- ARGV[1] 是购买数量
local stock = tonumber(redis.call('GET', KEYS[1]) or "0")
if stock >= tonumber(ARGV[1]) then
-- 如果库存够,执行扣减
return redis.call('DECRBY', KEYS[1], ARGV[1])
else
-- 库存不够,返回-1代表失败
return -1
end
除了基本的GET、SET、DECRBY,Lua脚本里还可以用一些逻辑控制指令,比如 if...then...else...end、for循环、tonumber类型转换等,这些是Lua语言本身的语法,用来做业务判断。
面试官:商品放Redis查库存,检查通过也发现有货,下一步呢?
学员的回答思路:
下一步就是在Lua脚本内部,直接调用 redis.call('DECRBY', KEYS[1], ARGV[1]) 把库存减掉。
Lua脚本返回的如果是剩余库存数,代表成功;如果是-1,代表库存不足。
业务代码拿到成功状态后,才开始做后续的异步流程:比如发送MQ消息去生成订单、发短信通知。而不是反过来先写数据库再去扣Redis,那样就不是防超卖了。
第二部分:其他高频技术问题实录
除了Redis的连环追问,面试官在其他技术栈上也问得很实,没有太多虚的。
1. etcd选型问题:用别的来做可不可以?为什么选etcd?
学员的回答思路: 可以用ZooKeeper或者Consul替代。 选etcd具体原因有三个:
- 部署简单:Go语言写的,只有一个二进制文件,没有像Zookeeper那种Java环境的依赖。
- 强一致性与Watch:基于Raft共识算法,数据读写都是强一致的。它的Watch API支持监听Key或目录的变化,配合V3版本的Lease租约机制,做服务注册发现的心跳续约很方便。
- 性能:虽然是强一致,但在小到中等规模的数据量下,读写延迟都很低,完全满足配置中心和服务发现的场景。
2. 双网关架构:用什么写的?
学员的回答思路: 用GoFrame框架写的,项目里拆分了H5网关和Admin网关两套服务。 主要是出于流量隔离的考虑。H5网关面对C端用户,对延迟很敏感,只做最简单的鉴权和转发。Admin网关是给内部运营用的,经常要导出Excel或者跑复杂SQL查询。如果把Admin的慢查询请求跟H5混在一个网关里,后台一拉数据,可能把用户下单的线路都拖慢了,所以必须分开部署。
3. 读取一个无缓冲区关闭的channel,返回的零值是什么?Go中有哪些零值?
学员的回答思路:
从一个已经关闭且没有缓冲的channel读取数据,比如 val, ok := <-ch,如果channel已经关了,val会立刻返回这个channel对应元素类型的零值,并且ok的值为false。
Go里的零值列表:
int/float系列:0string:空字符串""bool:falsepointer/slice/map/chan/func/interface:nilstruct:内部每个字段都是各自类型的零值。
4. 事务隔离级别:幻读和不可重复读有什么区别?
学员的回答思路:
- 不可重复读:针对的是同一行数据的UPDATE修改。比如事务A里第一次读某行数据是值X,过一会再读同一行变成了Y,因为中间被事务B修改且提交了。这个现象叫不可重复读。
- 幻读:针对的是INSERT插入或DELETE删除这种范围变化。比如事务A第一次查询
WHERE id > 10返回了5条数据,过一会同样条件再查,返回了6条数据,多出来的那条就像“幻觉”一样出现了。MySQL在可重复读隔离级别下通过间隙锁解决了幻读。
5. 索引数据结构:B树和B+树区别?
学员的回答思路: MySQL InnoDB用的是B+树变体,不用哈希也不用普通B树。
B+树和B树核心区别有两点:
- 数据存储位置不同:B树是所有节点都存数据指针。B+树只有叶子节点才存完整行数据,非叶子节点只存索引键值。这样非叶子节点能存的键值数量大大增加,树的高度就更矮,查找时磁盘IO次数更少。
- 叶子节点结构不同:B+树的叶子节点之间用双向链表串起来了。做范围查询比如
SELECT * FROM t WHERE id > 10 AND id < 20,只要通过索引找到10的位置,然后顺着链表指针往后扫就行,非常快。B树做范围查询只能每查一个数据就回根节点重新遍历一次,效率低很多。
6. 算法题:环形链表
学员的回答思路:
面试官让打开力扣写了一下找环形链表入口点的题。
核心思路是快慢指针。第一步先判断是否有环:快指针走两步慢指针走一步,没环的话快指针会先遇到nil;如果有环,快慢指针一定会在环内某个点相遇。第二步找环入口:相遇后,把其中一个指针移回链表头节点,两个指针都改为一次走一步,再次相遇的那个节点就是环的入口节点。
7. 最后的一些软性问题
比如“给你一件从来没做过的事情,挑战非常大,能胜任吗?”
学员的回答是:能胜任。没做过不代表解决不了。首先会去官方的文档或者开源社区找最佳实践,然后搭一个最小的demo环境跑通逻辑,把未知的坑在开发环境先踩一遍。遇到实在卡住的点,及时找有经验的同事对齐,不闷头瞎搞。
反问环节
学员问了面试流程,说是2到3轮。了解到团队是做软硬件结合的,这个业务形态以前接触不多,但他觉得挺有意思的,有机会接触到底层硬件的交互逻辑,对技术广度的提升会有帮助。
以上就是这份面经的全部内容。如果你觉得这篇内容有帮助,欢迎转发给身边正在准备面试的朋友。
END
写在最后:
最近私信问我面试题的小伙伴实在太多了,一个个回有点回不过来。
我花了两个周末,把星球里大家公认最容易挂的 AI/Go/Java 面试坑点 整理成了一份 PDF 文档。里面不光有题,还有解题思路和避坑指南。
想要的同学,直接关注并私信我 【面试】,我统一发给大家。