谢飞机面Java大厂:本地生活服务‘到店核销’系统中的Spring Boot + Redis分布式锁 + MySQL间隙锁 + Kafka事务三轮硬核拷问

面试官:张工(某一线大厂本地生活平台高级架构师)
求职者:谢飞机(昵称,自称“会写HelloWorld的全栈搬运工”,简历写着‘精通Spring全家桶(指能启动)’)


🌟 第一轮:基础链路与高并发痛点(3问|稳住局面)

面试官张工:我们本地生活业务有个核心场景——用户在线购买‘洗车券’,到店后扫码核销。券是限量的,比如‘XX洗车行今日仅剩50张’。你用Spring Boot实现这个核销接口,怎么保证不超卖?

谢飞机:(挺直腰板)那必须加锁啊!我用@Transactional包住整个方法,查库存、扣减、更新,数据库自己保证原子性!

张工:(点头)嗯,有事务意识,不错。那如果并发1000人同时请求呢?MySQL行锁能扛住吗?

谢飞机:(挠头)呃……应该……可以?毕竟InnoDB默认RR隔离级别,查的时候加读锁,更新加写锁……

张工:(微笑)那如果查库存用的是 SELECT * FROM coupon WHERE shop_id = ? AND status = 'AVAILABLE' LIMIT 1,没走索引呢?

谢飞机:(秒怂)啊?……那个……我一般都加@Select("SELECT ... FOR UPDATE")……

张工:很好,主动想到FOR UPDATE,加分。但注意——它只在事务内有效,且要走索引,否则升级为表锁。我们进入第二轮。


🌟 第二轮:分布式锁与缓存穿透(4问|渐入深水)

张工:现在服务拆成微服务了,核销服务部署了5个节点。单机synchronized和数据库FOR UPDATE都失效了。你怎么跨JVM加锁?

谢飞机:(眼睛一亮)Redis!SET key value NX PX 10000!我还会用Lua脚本删锁防误删!

张工:(赞许)对,这是标准解法。那如果恶意用户狂刷/verify?couponId=-1这种根本不存在的券ID呢?

谢飞机:(卡壳)啊?……Redis里没这个key,就直接查DB?……哦!缓存穿透!我……我缓存个空值?

张工:空值可以,但怎么防永久空缓存?万一真实券明天上架呢?

谢飞机:(支吾)设个短点的过期时间?比如30秒?

张工:接近了。更优解是布隆过滤器预检——但你先记住:空值+短TTL是兜底方案。第三轮,我们看最终一致性。


🌟 第三轮:异步核销与事务可靠性(4问|直击本质)

张工:核销成功后,要同步通知3个下游:① 用户推送核销成功消息;② 商户后台更新销量;③ 风控系统记录行为日志。这三个操作必须全部成功,否则要回滚。你怎么设计?

谢飞机:(自信)Kafka!发个消息,下游监听消费就行!

张工:那如果Kafka发消息失败了呢?或者消息发出去了,但商户服务消费失败重试10次还失败?

谢飞机:(额头冒汗)……我加个重试?……或者……用RocketMQ事务消息?

张工:我们用的是Kafka。正确做法是:本地事务表 + 定时任务补偿。核销成功后,在同一MySQL事务里,往outbox_table插入一条待发送消息记录(状态=‘pending’),再由独立线程扫描该表,调用Kafka Producer发消息,成功则更新状态=‘sent’。失败则重试+告警。这是业界成熟的Saga模式变体。

张工:(合上笔记本)谢同学,基础框架使用你很熟练,对常见问题有基本敏感度,也愿意学。但分布式事务、缓存治理、消息可靠性这些生产级细节,还需深入理解底层机制和落地经验。今天就到这里吧。

谢飞机:(长舒一口气)谢谢张工!我回去一定把《Kafka权威指南》第7章抄三遍!

张工:(起身握手)欢迎保持联系。结果我们HR会在5个工作日内邮件通知。祝你后面面试顺利。


✅ 【答案详解|小白也能懂】

🧩 业务场景还原

本地生活‘到店核销’是典型高并发、强一致性、多系统协同场景:

  • 高并发:节假日/促销时段瞬时流量洪峰(如10w+/秒核销请求);
  • 强一致:一张券只能被核销一次,绝不允许超卖或重复核销;
  • 多协同:核销成功 ≠ 业务完成,需联动推送、结算、风控等至少3个子系统。

⚙️ 技术点逐层拆解

🔹 第一轮:MySQL间隙锁(Gap Lock)防超卖
  • 为什么SELECT ... FOR UPDATE不够? 因为若查询条件无索引,InnoDB会锁全表;即使有索引,若查询范围过大(如WHERE stock > 0),也可能锁住大量无关记录。
  • 间隙锁作用:锁定索引记录之间的“间隙”,防止其他事务在间隙中插入新记录。例如:SELECT * FROM coupon WHERE shop_id = 123 AND status = 'AVAILABLE' FOR UPDATE,若索引是(shop_id, status),则会锁住所有满足该条件的记录及其间隙,避免并发插入同shop同status的新券导致超卖。
  • 实操建议:务必为WHERE字段建立联合索引,并用EXPLAIN验证执行计划是否走索引。
🔹 第二轮:Redis分布式锁 + 布隆过滤器防穿透
  • Redis锁关键点
    • NX(Not eXists)确保互斥;
    • PX(毫秒级过期)防死锁;
    • Lua脚本保证“判断锁归属+删除”原子性;
    • 锁key建议含业务标识,如lock:verify:coupon:10086
  • 缓存穿透终极解法
    • 初级:空值缓存(SET cache:coupon:-1 "null" EX 60);
    • 进阶:布隆过滤器(Bloom Filter)前置拦截——将所有合法couponId哈希进位图,查询前先过BF,不在则直接返回,不查缓存也不查DB;
    • 生产必备:BF支持动态扩容(如RedisBloom模块)、定期重建(结合Binlog监听)。
🔹 第三轮:本地事务表(Outbox Pattern)保障Kafka最终一致
  • 为什么不用Kafka事务? Kafka事务仅保证Producer端消息“发出去”这一动作的原子性,无法控制下游消费逻辑是否成功。
  • Outbox核心流程
    1. BEGIN TRANSACTION
    2. UPDATE coupon SET status = 'USED' WHERE id = ? AND status = 'AVAILABLE'
    3. INSERT INTO outbox (topic, payload, status) VALUES ('verify.success', '{...}', 'pending')
    4. COMMIT
    
  • 补偿服务职责
    • 定时扫描outboxstatus='pending'记录;
    • 调用Kafka Producer发送消息;
    • 若成功,UPDATE outbox SET status='sent'
    • 若失败,记录错误日志、触发企业微信告警、最多重试3次后转人工核查。
  • 优势:完全解耦、可监控、可追溯、符合SOLID原则。

📚 延伸学习清单(免费可学)

  • MySQL锁机制:官方文档 — Locks Set by Different SQL Statements
  • Redis分布式锁:Antirez《Redlock》论文(辩证看待)+ 阿里《Redis开发规范》
  • Outbox模式:Martin Fowler《Event-Driven Architecture》+ Netflix OSS Conductor源码
  • 本地生活案例库:美团技术团队《到店团购系统架构演进》(CSDN可搜)

💡 小结:大厂面试不考八股文背诵,而考你能否把技术点精准锚定到业务痛点。谢飞机输在“知道有这回事”,赢在“敢于开口说”。真正的高手,是能把FOR UPDATE讲成一场防止商家破产的战争。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐