news 2026/10/14 16:26:19

大厂 Java 面试实录:谢飞机挑战电商秒杀系统(Redis、Lua、Kafka 与幂等设计)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大厂 Java 面试实录:谢飞机挑战电商秒杀系统(Redis、Lua、Kafka 与幂等设计)

大厂 Java 面试实录:谢飞机挑战电商秒杀系统(Redis、Lua、Kafka 与幂等设计)

场景:某互联网公司的电商平台正在招聘高级 Java 工程师。面试围绕秒杀活动展开,从基础接口设计逐步深入到并发、缓存、消息队列和故障恢复。

面试官姓严,表情严肃。候选人谢飞机坐下后,先把简历往桌上一推。

“严老师,我做过很多高并发项目。”

面试官看了一眼简历:“你简历上写着,最高并发是同时三个人点外卖?”

谢飞机:“对,三个人都没抢到,是因为我忘了点支付。”

第一轮:先把业务说清楚

问题一:设计秒杀接口

**面试官:**用户点击“立即抢购”,后端需要提供什么接口?你会怎样设计?

**谢飞机:**我会做一个POST /seckill,传商品 ID 和用户 ID,然后查一下库存,库存大于零就创建订单。为了防止用户重复点,可以在按钮上加个 loading。

**面试官:**接口方向是对的。前端限制重复点击可以改善体验,但不能承担服务端防重复的职责。攻击者可以绕过页面直接发请求。服务端要校验用户身份、活动状态、购买资格和幂等性。

谢飞机点点头:“明白,前端按钮负责礼貌,后端校验负责法律。”

问题二:如何防止超卖

**面试官:**多个请求同时读取到库存为 1,再各自创建订单,会发生什么?如何避免超卖?

**谢飞机:**大家都看到库存还有 1,就都下单了,最后库存可能变成负数。可以加锁。

**面试官:**判断正确。具体会用什么锁?锁的范围多大?数据库更新能否通过条件语句保证原子性?

谢飞机:“可以给整个数据库加把锁,这样谁也超不了。”

面试官停顿片刻:“锁整个数据库确实很有约束力,问题是其他商品也不用卖了。”

问题三:为什么不能只依赖数据库

**面试官:**如果秒杀瞬间涌入大量请求,每个请求都直接访问数据库,会有哪些风险?

**谢飞机:**数据库可能扛不住,连接池耗尽,查询变慢,订单也写不进去。可以多买几台数据库。

**面试官:**你抓到了连接池耗尽和数据库过载的问题。扩容有帮助,但成本高,也不一定能跟上突发流量。我们还需要在请求到达数据库之前做流量控制。

谢飞机:“那数据库前面放个保安,让它少接点客人。”

面试官:“可以这么理解,但保安还得知道库存和排队规则。”

第二轮:进入高并发设计

问题四:Redis 扣减库存

**面试官:**如果把秒杀库存放进 Redis,怎样做到并发扣减时不超卖?

**谢飞机:**先GET库存,判断大于零,再DECR。负数了就加回来。

**面试官:**这两步之间存在并发窗口。两个请求都可能先读到 1,然后都执行扣减。补偿加回也会引入复杂状态。可以把判断和扣减放在一个原子操作里,例如 Redis Lua 脚本。

谢飞机:“Lua 我不太熟。不过我知道 Lua 有月亮,秒杀一般晚上搞,挺匹配。”

面试官:“语言名字匹配不是技术方案。”

问题五:为什么要引入消息队列

**面试官:**Redis 扣减成功后,是不是马上同步写数据库、创建订单、调用积分服务和短信服务?

**谢飞机:**同步做最简单。失败了就重试,重试到成功为止。

**面试官:**同步链路越长,用户等待越久,任意一个下游服务变慢都可能拖垮入口。通常会先快速完成资格检查和库存预扣,再通过 Kafka 或其他消息队列异步创建订单。你要考虑消息重复、消费失败和状态不一致。

谢飞机:“那用户点完以后先告诉他抢到了,后面再看能不能真的抢到?”

面试官:“不能把‘预扣成功’说成‘订单成功’。可以明确返回处理中,等订单创建结果再更新状态。”

问题六:消息重复消费怎么办

**面试官:**Kafka 的消息可能重复投递。订单消费者如何避免重复创建订单?

**谢飞机:**给消费者加一个if,查一下有没有这个订单,有的话就不创建。

**面试官:**方向正确,但只做先查后写也有并发竞争窗口。幂等性最好由稳定的业务键和数据库唯一约束共同保证。消费者收到重复消息时,发现唯一键冲突,就可以按“已处理”完成确认。

谢飞机:“所以数据库唯一索引像门票,一张票只能进一次场。”

面试官:“这个比喻可以。还要注意:唯一约束防的是重复订单,不会自动解决所有消息和库存一致性问题。”

第三轮:可靠性与故障恢复

问题七:库存预扣后服务崩溃

**面试官:**Redis 库存预扣成功了,但应用在发送 Kafka 消息之前崩溃,会发生什么?怎样补救?

**谢飞机:**库存扣了,订单没了,用户的钱……还没付,所以用户应该没损失。

**面试官:**用户可能抢不到库存,但系统也不知道这次预扣需要继续处理还是释放。需要记录请求状态,并设计超时检查和补偿机制;更严格的一致性方案可以使用 Redis Stream、可靠消息表或事务消息等模式。

谢飞机:“那我定时扫 Redis,把扣过库存但没下单的人加回去。”

面试官:“思路是补偿,但不能只凭‘没查到订单’就加库存。还要区分消息延迟、订单处理中、订单创建失败等状态,否则可能把别人的库存加回来。”

问题八:服务超时与重试

**面试官:**订单服务调用库存服务超时,你会怎样处理?重试是不是越多越好?

**谢飞机:**超时就重试十次,十次还不行再重启服务器。

**面试官:**无限重试会放大故障。应设置合理超时、有限重试和退避策略,并对可恢复与不可恢复错误进行区分。重试还必须有幂等保障,否则一次超时可能导致请求实际上已成功,再重试就重复扣库存。

谢飞机:“那就是服务已经做完了,但消息还没来得及告诉我,我又让它做一遍。”

面试官:“对,这就是分布式系统里很常见的‘结果未知’问题。”

问题九:如何监控秒杀链路

**面试官:**活动开始后,你关注哪些指标?如果用户反馈“抢到了却没订单”,怎么排查?

**谢飞机:**先看服务器是不是红了,再问用户是不是点错了。

面试官:“服务器颜色和用户操作都不是指标。”

谢飞机:“那我看错误日志。”

面试官:“日志有用,但还要能沿着同一个请求追踪全链路。至少要观察请求成功率和延迟、Redis 扣减结果、Kafka 积压、订单创建耗时、数据库连接池使用率,以及超时和补偿数量。用 traceId 串联入口、消息和订单服务,排查才有依据。”

谢飞机:“我之前日志里只有‘出错了’,traceId 是不是就是给错误起个名字?”

面试官:“更准确地说,是给一次跨服务请求一个可关联的标识。”

面试结束

面试官合上笔记本:“今天先到这里。你对接口、并发和幂等有一些基础认识,但消息可靠性和故障恢复还需要继续梳理。”

谢飞机:“那我算抢到 offer 了吗?”

面试官:“你这次只是完成了预扣。”

“那订单什么时候创建?”

“回家等通知。”

参考答案:从入口到订单的完整思路

1. 秒杀接口:用户身份与业务幂等

秒杀接口不应信任客户端传来的用户 ID。用户身份应从登录态或访问令牌中获取;商品、活动和购买资格也要在服务端校验。

可以为一次购买生成稳定的业务幂等键,例如“活动 ID + 用户 ID”。同一用户重复提交时,系统应识别为同一笔业务,而不是创建多个订单。

public record SeckillRequest(Long activityId, Long skuId) {} public record SeckillResponse(String requestId, String status) {} @PostMapping("/seckill") public SeckillResponse seckill( @AuthenticationPrincipal AppUser user, @RequestBody SeckillRequest request) { String requestId = seckillService.submit( user.id(), request.activityId(), request.skuId()); return new SeckillResponse(requestId, "PROCESSING"); }

示例返回PROCESSING,是因为库存预扣和订单落库可能分属不同步骤。接口需要明确表达当前状态,不能把“进入异步处理”误报成“订单已创建”。

生产实现还要校验活动时间、商品状态、用户限购、参数合法性和权限,并对请求进行限流。

2. 数据库防超卖:条件更新优先于先查后改

单纯执行“查询库存,再修改库存”不是原子操作。并发请求可能同时读到相同库存。

数据库可以使用带库存条件的原子更新:

UPDATE sku_inventory SET available = available - 1 WHERE sku_id = ? AND available > 0;

应用检查受影响的行数:

  • 更新 1 行:扣减成功。
  • 更新 0 行:库存不足。

这种方法能避免库存被扣成负数。它仍然需要考虑数据库写入压力、事务边界和订单创建的整体一致性。对于高峰流量,通常不会让所有请求都直接打到数据库。

3. Redis Lua:原子检查并扣减

Redis 的单条命令具有原子性,但“读取、判断、扣减”由多条命令组成时,整体不是原子操作。Lua 脚本可以把这段逻辑放进 Redis 的一个原子执行单元:

local stock = tonumber(redis.call('GET', KEYS[1])) if not stock or stock <= 0 then return 0 end redis.call('DECR', KEYS[1]) return 1

Java 侧执行脚本时,传入库存键。返回1表示预扣成功,0表示库存不足。

这个脚本解决的是 Redis 中的并发扣减问题,并不自动保证 Redis 和数据库之间永远一致。应用还需要处理 Redis 故障、消息发送失败、进程崩溃和超时等情况。

4. 消息队列:削峰与异步处理

高峰请求进入系统后,可以将处理拆为几个阶段:

  1. 校验活动、用户和商品资格。
  2. 使用 Redis 原子预扣库存。
  3. 生成稳定的业务请求 ID。
  4. 将订单创建事件可靠地交给消息系统。
  5. 消费者异步创建订单,并更新请求状态。
  6. 前端查询状态,或通过推送机制接收结果。

消息队列可以削峰并解耦服务,但它不意味着“消息一定只投递一次”。消费者仍需要应对重复消息、消费失败和消息积压。

5. 幂等消费:业务键加数据库约束

消费者可以用请求 ID 作为订单业务唯一键,并在数据库中设置唯一索引:

CREATE TABLE seckill_order ( id BIGINT PRIMARY KEY, request_id VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, sku_id BIGINT NOT NULL, status VARCHAR(24) NOT NULL, created_at TIMESTAMP NOT NULL, UNIQUE KEY uk_request_id (request_id) );

消费消息时,即使收到重复事件,也只允许同一个request_id创建一笔订单。遇到唯一键冲突后,消费者应确认这条消息已完成处理,而不是无限重试。

如果“查是否存在,再插入”而没有唯一约束,两个消费者并发处理同一业务时仍可能同时通过检查。因此,数据库唯一约束是幂等设计的重要保障。

6. 故障恢复:记录状态,再做补偿

库存预扣后,如果消息没有可靠送达,系统需要知道这笔请求处于什么阶段。常见做法包括:

  • 记录请求状态,如RESERVED、ORDER_CREATED、FAILED。
  • 为处理过程设置超时与重试策略。
  • 定期扫描超时请求,核对订单和消息处理状态。
  • 确认订单无法创建后,再执行库存释放。
  • 为释放操作本身设计幂等键,避免多次补偿导致库存增加过量。

补偿不是简单地“没订单就加库存”。订单可能仍在处理中,消息也可能只是延迟。补偿前需要明确状态并确认操作可重复执行。

7. 超时、重试与熔断

调用下游服务时应设置合理的连接和读取超时。对短暂故障可进行有限重试,并采用退避策略,避免所有请求在同一时刻集中重试。

重试之前要回答两个问题:

  • 错误是否值得重试?例如参数错误一般不值得重试。
  • 操作是否幂等?如果一次请求实际上已成功,只是响应丢失,再执行一次是否会重复扣库存或创建订单?

对于持续失败的依赖,可以使用 Resilience4j 等工具配置熔断、隔离和限流。熔断的目标是阻止故障扩散,不是让失败请求自动变成功。

8. 可观测性:指标、日志和链路追踪各有用途

秒杀链路至少应关注:

  • 接口请求量、成功率、错误率和延迟分位数。
  • Redis 库存预扣成功、失败和异常数量。
  • Kafka 消息积压、消费延迟和重试次数。
  • 订单创建成功率与耗时。
  • 数据库连接池使用率和慢查询。
  • 超时请求、库存补偿和状态长期未完成的数量。

日志用于记录具体事件,指标用于发现整体趋势,分布式链路追踪用于关联跨服务调用。三者结合,才能从“用户没收到订单”继续定位到是入口校验、Redis 预扣、消息投递还是订单消费出了问题。

面试要点总结

秒杀系统的关键不是单独使用 Redis、Kafka 或数据库,而是明确每个阶段的状态与责任:数据库通过条件更新保护最终库存;Redis 原子操作承接高并发预扣;消息队列负责异步削峰;幂等键和唯一约束阻止重复订单;超时扫描与补偿处理不完整流程;指标、日志和链路追踪帮助定位生产问题。

面试时,与其只回答“加锁、上缓存、上消息队列”,不如说明并发窗口在哪里、失败后状态是什么、重试会不会重复执行,以及系统如何恢复。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 16:26:05

自研轻量级Modbus TCP命令行工具:批量轮询与协议实现实战

1. 项目缘起&#xff1a;为什么放着现成工具不用&#xff0c;非要自己造一个轮子做工业自动化上位机开发的同行应该都有这种体会&#xff1a;现场调试设备时&#xff0c;手里没一个顺手的 Modbus TCP 客户端&#xff0c;简直寸步难行。搜索引擎一搜&#xff0c;“Modbus 调试工…

作者头像 李华
网站建设 2026/10/14 16:25:53

OMPL运动规划实战:从安装到避障的完整指南

1. 从一次路径规划需求说起&#xff1a;OMPL 到底解决什么问题如果你正在做机器人运动规划、机械臂轨迹生成&#xff0c;或者任何涉及“让一个东西从 A 点安全移动到 B 点”的项目&#xff0c;大概率会碰到一个绕不开的名字——OMPL。它的全称是 Open Motion Planning Library&…

作者头像 李华
网站建设 2026/10/14 16:25:34

Python技术面试三条主线:数据结构、函数对象与避坑实践

简介&#xff1a;面向Python技术面试的基础知识整理包&#xff0c;覆盖编程语法、数据结构、算法、面向对象、异常处理、标准库、文件、网络、数据库及项目实践&#xff0c;为求职者提供系统化复习指引。包体大小约33.16MB&#xff0c;文件目录需下载后查阅&#xff1b;目前已有…

作者头像 李华
网站建设 2026/10/14 16:25:04

一台设备解决传统监控痛点:Y-GVW100融合架构实战解析

1. 传统监控的痛点到底在哪干了十多年弱电和安防项目&#xff0c;我越来越觉得&#xff0c;传统监控系统就像一台开了十年的老车——能跑&#xff0c;但浑身响。你问甲方最头疼什么&#xff0c;十个里有八个会告诉你&#xff1a;不是摄像头不够清晰&#xff0c;而是整套系统太散…

作者头像 李华
网站建设 2026/10/14 16:24:37

八数码问题BFS实战:Java手写状态空间搜索入门

简介&#xff1a;这是一份面向Java初学者与算法学习者的八数码问题&#xff08;滑动拼图&#xff09;宽度优先搜索实现资源&#xff0c;用于求解3x3面板上数字从初始布局到目标布局的最短移动路径。资源以Java语言编写&#xff0c;借助队列存储待扩展状态、哈希表去重&#xff…

作者头像 李华