面灵AI→

满帮 全栈(后端)一面

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

  1. 这个项目是自己动手做的,还是实习过程中做的?
  2. 技术选型时为什么用 Lua 脚本判断加减库存,而不是直接用 Redis 的 Redisson?
  3. 了解分布式锁吗,Redis 的 Java 分布式锁?
  4. 秒杀场景中商品信息和库存的预热,你会额外注意哪些点?
  5. 客户端缓存和 Redis 缓存比较大的区别在哪?做缓存方案选型时你会额外关注什么?
  6. 本地缓存实现不了的场景是什么?
  7. 扣减库存之后,除了 Lua 脚本,你还会做什么兜底处理?库存信息的持久化同步机制是什么样?
  8. 一个业务场景里做数据库写操作,Redis 和数据库的缓存同步是什么样?
  9. 延迟双删怎么实现?
  10. 先更新数据库再删缓存仍然有读到脏数据的可能,可以通过什么措施减少?
  11. 做知识库搭建时,你的搭建思路是什么?做知识单元拆分时的思路是什么?
  12. 1000 字符、200 重叠这两个数字是怎么定义出来的?如果是 1000 字符、500 重叠就不行吗?
  13. 物流场景下,用户说要你直接退款,怎么保证模型不自主回答「好的,我帮你退款」?非知识库文档相关的用户问题怎么处理,在哪些环节做拦截?
  14. 单次对话 N 条消息总结一次,这个总结是大模型总结还是其他方式?
  15. 如果让 AI 做总结,用户侧体验是什么感觉?用户愿不愿意等?你怎么思考?
  16. select count(*)、select count(1)、select count(2) 有什么区别?
  17. count(*) 和 count(1) 的区别是什么?日常建议用哪个?
  18. MySQL 里常用的函数有哪些?
  19. 什么样的场景会让索引失效?
  20. 优惠券表需要按用户查近 N 天的券,你会建什么样的索引?
  21. 如果顺序是 user_id 和 create_time,反过来会有什么问题?
  22. 索引 A、B、C 按这个顺序建出来,实际上相当于建了几个索引?
  23. 系统频繁 Full GC,你会排查什么?怎么排查?
  24. ConcurrentHashMap 解决了什么问题?它的锁是什么锁?
  25. 消息队列用 Redis Stream 实现,它和 RocketMQ、RabbitMQ 这种专业 MQ 的区别在哪?为什么选型时没用它们?
  26. 手撕 LeetCode 74 搜索二维矩阵(只需说思路)

《参考解析》

为什么用 Lua 而不是 Redisson:两者解决的不是同一个问题。Lua 是为了把「判断库存 + 扣减 + 防重复购买」多步操作打成一次原子执行,避免多轮网络往返之间被插队;Redisson 是分布式锁的封装,锁住的是临界区,适合「先查再改、中间还夹着别的逻辑」的场景。秒杀的最优解通常是 Lua 原子扣减(无锁)而不是加锁——加锁会引入等待和锁竞争,量一大反而更慢。Redisson 的价值在可重入、看门狗续期、公平锁这些语义,用在业务操作需要跨多步互斥时。回答里要讲清「这里用不到锁语义,只需要原子性」,而不是简单说「Lua 更快」。

库存兜底与缓存一致性:Lua 扣完只是缓存层的预扣,必须落到数据库。做法是扣减成功即发消息(RocketMQ / Kafka / Redis Stream)异步落库,用「本地消息表 + 定时对账」保最终一致:消息发送失败靠本地表补偿重投,消费端幂等(订单号 + 唯一索引)防重。同时要有一条「缓存为准还是库为准」的明确答案——秒杀场景以 Redis 为准、库最终追平,并设置库存回滚(超时未支付回流)。缓存一致性部分:先更新数据库再删缓存是常用方案,但仍有两个窗口——读请求在删除前读到旧值并回填、删除本身失败。减少脏读的手段有:删除失败进重试队列(或走 binlog 订阅 Canal 异步删)、给缓存设 TTL 兜底、延迟双删(更新库 → 删缓存 → 等一小段大于「读旧值 + 回填」的时间 → 再删一次)、以及对强一致的少量数据直接读库。

知识库切分与 AI 安全:chunk 大小没有魔法数字,取决于文档结构和检索粒度:太碎会丢上下文,太大则召回不精准、浪费 token。1000 字符 + 200 重叠是常见起点(重叠是为了防止关键句被切断),要按自己的文档评测调——固定的判据是「用一组问题测召回率和答案忠实度」,所以 1000 / 500 不是不行,要看效果和成本。更稳的做法是按语义切(按标题层级分块、再按段落合并到目标长度),表格和列表单独处理。退款拦截是分层做的:意图分类里把「退款、改地址」这类高风险意图单独识别并降级,提示词里明确「不能承诺任何资金操作」,输出层用规则或 schema 校验拦截关键词,执行层根本不给模型调用资金接口的权限,转人工或走标准工单。核心思想是「能在确定性层拦的绝不交给模型」。

MySQL 索引与 count:InnoDB 里 count(*) 和 count(1) 都走最小的可用索引(通常是二级索引)逐行计数,优化器把 count(*) 转成 count(0),表现一致,官方建议用 count(*)。count(2) 等价于 count(常量),语义上也是数行数,但写出来会让人误解,不要用;count(字段) 语义不同——它只统计该字段非 NULL 的行。索引失效的典型场景:在索引列上用函数或表达式(date(create_time))、隐式类型转换(字符串列传数字)、like '%xxx' 前导模糊、or 连接的非索引列、违背最左前缀、优化器判断走全表更快(区分度低)、以及范围查询之后的列无法再用于排序。优惠券表按「用户 + 时间范围」查,索引建 (user_id, create_time);反过来 (create_time, user_id) 在去掉 user_id 条件时还能用,但同时有 user_id 等值条件时无法兼顾——等值列要在范围列前面。索引 (a,b,c) 能覆盖 a、(a,b)、(a,b,c)、a + c(c 走不到索引但能覆盖)这几种前缀,所以一个联合索引等价于多个前缀索引,但中间断掉(只查 b)就失效。explain 里看 type、key_len、Extra 比背结论可靠。

Full GC 排查:先确认现象(jstat -gcutil 看老年代使用率和 GC 前后回收量),再看是不是真的频繁。常见根因分三类:内存泄漏(老年代只增不减,jmap -histo / dump 下来用 MAT 看谁的支配树最大,典型是大 Map 缓存、ThreadLocal、监听器未注销)、单次分配过大(大对象直接进老年代,比如一次性查全表、Excel 导出全量、大数组)、以及参数不当(新生代太小导致对象过早晋升,或堆本身不够)。处理上先加监控和告警,再定位对象来源,短期可扩容、长期改代码。ConcurrentHashMap 解决的是 HashMap 并发不安全(成环、丢数据)和 Hashtable 一把大锁性能差的问题,JDK 8 用 CAS + synchronized 锁单个桶头节点,扩容可多线程协助。

MQ 选型与手撕:Redis Stream 的优势是复用已有 Redis、部署简单、支持消费者组和 ACK,适合中小规模或已有 Redis 的场景;不足是没有专业 MQ 的堆积能力(内存型,量大成本高)、不保证严格顺序、事务与死信/延迟队列能力弱、持久化可靠性不如 RocketMQ/RabbitMQ/Kafka。选型时按「消息量、可靠性要求、是否要顺序、是否要延时、运维成本」逐项对比。搜索二维矩阵的思路:矩阵行列都升序,从左下角(或右上角)出发,比目标大就上移、比目标小就右移,每次排除一行或一列,O(m+n);也可以把矩阵当作一维有序数组做二分,O(log(mn))。

这一轮的特点:这是「项目深挖型」面试,几乎每道题都从简历项目里长出来,再延伸到通用知识。这种面试不能靠背八股,得把自己的项目按「为什么这么选、代价是什么、有没有替代方案、怎么验证」四问准备一遍。作者被追问最狠的是「数字是怎么定出来的」(重叠 200、阈值 N 条),这类问题考的是你有没有真的做过实验——答「约定了就用了」是减分项,答「测过三组,看召回率和成本曲线选的」才是加分项。