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