多公司后端与全栈面经合集:字节、百度、美团、B站、米哈游
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
米哈游 · 平台研发(Go)
- 手撕:二分查找。
- Go 语言相关八股(问得比较深)。
- 数据库相关八股。
- 实习经历相关的问题。
bilibili · 服务端开发
- 你的论文是什么时候开始做的,现在是什么状态?
- 论文具体解决什么问题,方法是怎么做的?
- 你的方案为什么可以说和 Agent 有关系?
- 如果换一个能力比较弱的大模型,这套方法还能工作吗?
- 大模型生成的算法不合法怎么办,会修复吗?
- 你怎么保证换模型以后整个 Workflow 还能运行?
- 介绍一下你接触 AI / 大模型以来的发展过程,最近有没有关注 Agent 方向的论文或者技术?
- 为什么 Go 适合高并发?
- Go 里面有没有遇到并发安全问题,怎么处理?
- 你了解 sync.Map 的底层实现吗?
- 你了解操作系统的内存管理吗?常见的内存分配策略有哪些?
- 怎么判断一个内存分配算法好不好?
- 如果让你实现空闲内存管理,你会怎么做?
- 介绍一下你实习期间主要做了什么。
- 实习中做过什么性能优化?为什么当时不通过加索引解决?你们看过 EXPLAIN 吗,查询有没有走索引?
- MySQL 有哪些索引?索引是不是越多越好,加太多索引有什么问题?
- 设计索引时有哪些原则?
- MySQL 除了 B+ 树索引,还有什么索引结构?Hash 索引和 B+ 树索引有什么区别?
- 讲一下 MySQL 的 B+ 树索引,为什么不用普通二叉搜索树而使用 B+ 树?
- 如果让你设计短链接系统,你会怎么做?短链接和长链接应该是什么关系?
- 怎么进一步缩短 URL?
- 手撕:链表反转。你刚才这段链表反转代码有什么问题?
百度 · 北京后端
- 平时刷算法题和做项目分别使用什么语言?为什么项目更倾向使用 Go?
- Go 中 make 和 new 有什么区别?make 可以初始化哪些类型?
- Go 中数组和切片有什么区别?切片的底层结构是什么,len 和 cap 分别表示什么?
- append 时容量不足会发生什么?切片的扩容机制是怎样的?
- 什么是 Goroutine?为什么它比操作系统线程更轻量?
- Go 的 GMP 调度模型是什么?Goroutine 和线程的上下文切换有什么区别?
- 进程、线程和协程三者有什么区别和联系?
- 线程之间有哪些同步或通信方式?
- 操作系统如何管理物理内存?为什么需要虚拟内存?
- 虚拟地址如何通过页表转换为物理地址?页和页框分别是什么?
- 什么是生产者—消费者模型?多线程下如何保证生产者和消费者访问共享数据的并发安全?
- 什么是读写锁?它相比普通互斥锁有什么优势?
- HTTP 和 HTTPS 有什么区别?HTTPS 如何实现防窃听、防篡改和身份认证?
- 数字证书在 HTTPS 中有什么作用?TLS 握手的大致流程是什么?
- DNS 中的 A 记录和 CNAME 记录分别是什么,有什么区别?
- TCP 连接建立后如果一端突然断网,另一端继续发送数据会发生什么?一直收不到 ACK 时会如何处理?
- TCP 的超时重传和滑动窗口机制是什么?TIME_WAIT 状态有什么作用?
- Redis 有哪些常见数据结构?Redis 为什么快?
- 多个客户端并发操作 Redis 时如何保证安全?Lua 脚本为什么可以保证多个操作的原子执行?
- 什么是缓存穿透?会带来什么问题,如何解决?
- Redis 事务的 MULTI/EXEC 是如何工作的?Redis 事务支持回滚吗,它和传统数据库事务有什么区别?
- 什么是 Redis 分布式锁?如何使用 SET NX EX 实现?
- 释放分布式锁时为什么通常需要 Lua 脚本?
- 算法题:小于 n 的最大数。
字节跳动 · 飞书/飞书云文档后端 · 一面
- 算法题:缺失的第一个正数。
- 场景题:几十 TB 数据的用户 id,内存只有 4G,如何找出出现频率最高的 Top 10 id?
- 竞赛项目里 Redis Lua 起什么作用,为什么要用?Lua 的原子性如何理解和保证?
- Lua 执行失败会不会回退,为什么不回退?
- 实习时 SQL 优化是怎么做的?讲讲索引,为什么要用 B+ 树?
- 索引越多越好吗,为什么?
- 讲讲 TCP 的三次握手和四次挥手,目的是什么?
- 一次 URL 到页面返回渲染的全过程是怎样的?DNS 解析如何做的,每次都要走完整流程吗,缓存怎么实现?
- HTTPS 怎么实现安全?数字证书怎么实现?如果我伪造一个证书说自己是百度会怎样?
- CPU 满了怎么办?火焰图之前看什么,了解 load 吗?
- 内存满了怎么办,怎么检测?
字节跳动 · 飞书/飞书云文档后端 · 二面
- 实习项目跟 AI 搜索有什么关系?
- 搜索引擎相关的问题。
- MySQL 架构、数据库架构相关问题。
- 集群哨兵、Redis 各种架构相关问题。
- Go 的 GC 是怎么做的?什么是 STW?
- AI 方面问了 RAG 相关内容;场景题:点杀黑产灰产、秒杀。
- 手撕:不重复最长子串。
字节跳动 · 生态 Agent 后端开发工程师(火山引擎)
- 你最有代表性的项目是什么?介绍一下这个 AI 全栈挑战赛项目。
- 你在项目里主要负责什么?这个项目最大的难点和亮点是什么?
- 整体系统架构是怎样的?权限系统怎么设计?
- 为什么使用 WebSocket 而不是普通 HTTP?WebSocket 房间是怎么设计的?
- 一次用户加价请求是怎么走的?为什么竞拍裁决不直接操作 MySQL?
- 为什么要使用 Redis Lua?Lua 脚本里主要做哪些事情,操作顺序为什么重要?
- 为什么不直接在 Lua 里写 MySQL?
- 为什么用 Redis Stream 而不是 Kafka、RocketMQ?Redis Stream 后面的数据怎么落库?
- 这里的 Room 是一个真实实体吗?WebSocket 断线以后怎么恢复,怎么保证重连后还是进入原来的房间?
- Redis 中这些数据怎么组织?为什么参与用户适合用 Set?为什么排行榜使用 ZSet?
- Redis ZSet 底层是什么数据结构?
- WebSocket 是怎么建立连接的?属于哪一层协议?什么时候创建 WebSocket Room?
- 给定整数 N 和一组允许使用的数字,如何找到不超过 N 的最大整数?这道算法题的思路是什么,现场代码的问题在哪里?
- 一个经常按 create_at 和 user_id 查询的表,你怎么优化?索引有什么优点和缺点?
- TCP 和 UDP 有什么区别?TCP 的「有连接」是什么意思,一个 TCP 连接怎么唯一标识?
- TCP 和 UDP 分别适合什么场景?UDP 不可靠,怎么实现可靠传输?
- 你们项目里的 AI 功能是什么?为什么传统关键词搜索不够?
- AI 找拍品具体怎么实现,用了什么模型和技术栈?这个功能算 Agent 吗?
- 你怎么理解 Agent 的设计?论文里的 Agent / Workflow 是怎么设计的?
- 为什么需要 LLM 来设计启发式算法?一轮迭代具体是怎么进行的,下一轮一定会比上一轮好吗?
- 谁负责选择最好的策略,是 Agent 吗?为什么这种 Agent 需要保存上一轮结果?
- ReAct 模式有什么优点和缺点?ReAct 和 Plan-and-Execute 怎么选?
- 你怎么定义「复杂任务」?
- 科研里的大模型 API 从哪里来?为什么你觉得 LLM 适合做算法设计?
- 你说的「不同环境」具体指什么?论文做的是实际地图路径规划吗?
- 这种算法能落地到真实路径规划吗?真实约束能直接加入你的求解框架吗?
字节跳动 · 后端开发工程师(字节云)· AI 算力基础设施 · 一面
- 进程和线程有什么区别?线程之间共享哪些资源、哪些资源是私有的?
- 协程和线程有什么区别?
- 详细讲一下 Go 的 GMP 调度模型。
- 用户态和内核态有什么区别?从用户态切换到内核态有哪些方式?
- 详细讲一下虚拟内存机制,一个程序使用内存的完整过程是什么?
- 缺页中断的处理流程是什么?
- 网络协议栈了解多少?OSI 七层模型分别是什么?
- UDP 为什么不可靠?TCP 为什么可靠?TCP 快速重传机制是什么?
- Cookie 和 Session 分别是什么,两者有什么关系?
- RESTful 是什么?
- MySQL 常用索引有哪些?InnoDB 的索引是怎么实现的,B+ 树有什么特点?
- 数据库/DBMS 的主要特点是什么?
- LangGraph 是什么样的框架?
- 如果设计一个安全系统,核心安全模块有哪些?
- 大模型如何与传统算法、领域工具结合并进行迭代?
- 复杂 Agent 流程能否直接封装成 Skill?
- 是否了解 Kubernetes/K8s、Agent Sandbox、DeepSeek 开源的 Harness 项目?
- 设计一个带统一 TTL 的 LRU Cache:容量为 n,超过容量时优先淘汰已过期 KV,如果没有过期 KV 则淘汰最近最少使用的数据,查询到过期 KV 时返回空。这道题的实现思路是什么?
字节跳动 · 后端开发工程师(字节云)· AI 算力基础设施 · 二面
- 介绍一个你认为做得比较好的项目。
- 高并发场景下多个用户同时操作时,核心业务逻辑是怎么裁决的?
- 「读取当前值 → 判断 → 写入」不是原子操作,并发情况下怎么避免竞态?两个请求同时基于旧数据判断,怎么防止后写入的低值覆盖先写入的高值?
- Redis Lua 是怎么实现「判断 + 更新」原子操作的?脚本内部具体做了哪些校验和状态更新?
- Redis 操作成功以后,什么时候把数据写入 MySQL?
- Redis 到 MySQL 采用异步落库时,怎么保证数据最终一定能写入?异步消费者挂掉、重启后怎么保证消息不丢?
- 消息重复消费时,怎么保证 MySQL 写入幂等?
- 最终结算时应该以 Redis 还是 MySQL 中的数据为准?两者存在延迟时怎么保证最终结果一致?
- 结算与最后一次并发操作同时发生时,怎么保证拿到的是最终状态?如何通过状态机阻止结算之后的新请求?
- Redis Stream 或 Redis 出现异常、数据丢失时怎么办?
- 为什么采用 Redis + Lua,而不是 Redis 分布式锁 + MySQL?
- 这个架构里使用 Redis 的核心目的是抗并发、低延迟,还是保证原子性?为什么要把实时热路径和 MySQL 写路径解耦?
- 幂等键是怎么设计的、在哪里生成?服务端拿到幂等键后,怎么判断这个请求之前是否已经处理过?
- 幂等键应该在请求处理的哪个阶段写入 Redis?写入时怎么区分业务处理成功、失败、处理中三种状态?
- 如果业务请求被拒绝,失败结果需不需要也做幂等缓存?
- 幂等键应该放在 Lua 脚本内部一起写,还是 Lua 返回以后再写?如果 Lua 已执行成功但幂等键还没写入,此时客户端重试怎么办?
- 如果重新设计一套幂等机制,你会怎么实现?SETNX 能不能直接解决幂等问题,它和业务处理结果之间如何保证一致性?
- 平时怎么使用 AI Coding?和传统开发相比,项目开发流程有什么区别?
- 用过哪些 Skill?Skill 是怎么工作的?
- 与 AI Coding 工具交互时,你一般怎么提供上下文、描述需求?
- 通常会让 AI 帮你完成哪些工作?哪些内容由人设计,哪些交给 Agent 生成?
- 项目的功能优先级、任务拆分是人定义的还是 Agent 自动定义的?用过哪些 AI Coding 工具?能否现场演示一次完整的 AI Coding 开发流程?
- 启动三个线程分别只打印 1、2、3,如何让它们循环输出 123123123……?可以使用什么同步机制实现线程之间的顺序协作?
- 代码中的 flag 状态变量有什么作用?多个线程是在「竞争资源」还是在「协作」,两者怎么理解?
- 线程抢到锁后发现还没轮到自己应该怎么办?wait 被唤醒返回以后,为什么还要重新判断 flag?
- 什么是虚假唤醒?为什么条件判断通常要写在 while 中?
- 设计一个支持 get/set 的 LRU Cache,核心数据结构应该怎么设计?为什么通常使用 HashMap + 双向链表?
- 如果要求某个 Key 在 X 秒内没有被访问就自动过期,应该怎么实现?过期数据采用惰性删除、后台定时清理,还是两者结合?
- 写出 LRU Cache 的核心数据结构和伪代码。如何严格保证缓存中的对象数量最多为 N?
- Cache 已满时,应该先插入再淘汰,还是先淘汰再插入?更新一个已经存在的 Key 时需要触发容量淘汰吗?
- 过期时间应该在什么时机设置?get 时发现 Key 已过期应该怎么处理?get 和 set 操作是否应该刷新过期时间?
- 并发执行 get/set 时,如何保证 HashMap、链表和容量的一致性?
- LRU 具体是什么意思?get 为什么也算一次「最近使用」?set 是否也应该改变节点在链表中的位置?
- LRU 中的 Node 应该包含哪些字段?双向链表具体是怎么组织的,prev/next 如何维护?
- 删除节点时如何保证 HashMap 与链表中的节点同步删除?节点删除不彻底可能产生什么内存或引用问题?
- 如果使用 C++ 的 list,是否可以直接把它作为 LRU 的双向链表?
美团 · 财务平台-财务科技-全栈
- K 个一组翻转链表。
- 讲一下 K 个一组翻转链表的代码逻辑。
- 如何把题目通过自然语言或结构化 Prompt 描述给 AI,让 AI 自动生成代码?
- 这道 AI Coding 题的 Prompt 设计思路是什么?怎么保证 Prompt 能覆盖输入输出、边界条件和核心逻辑?整体实现思路是什么?
- 从简历里挑一个最有意义或复杂度最高的项目讲一下:背景是什么,要达成什么目标,你具体是怎么做的?
- 过程中遇到了什么问题,最后是怎么解决的?
- 为什么在竞拍系统里使用 Redis Lua 脚本?脚本操作了多少个 Redis Key,每个 Key 分别是干什么的?
- 竞拍实时状态的 Redis Key 里具体存了哪些数据?
- 竞拍排行榜是怎么实现的?数据存在哪个 Key,用什么数据结构,具体怎么排序和维护?
- Lua、SETNX、WATCH 等方案有没有做过对比,为什么最终选择 Lua?
- 什么是 RAG,主要用来解决什么问题?你的项目里为什么使用 RAG,具体用在什么场景?
- 你对 DeepSeek Harness 了解多少?
- 你了解 LangChain 吗?LangChain 和 LangGraph 是什么关系,为什么会从 LangChain 发展到 LangGraph / Workflow?
- 你实际使用过哪些大模型,还了解哪些大模型?哪些大模型的编程能力比较强?
- 智谱、Kimi 这些模型了解吗?豆包了解或使用过吗?
- 有了 AI Coding 工具之后,你和 AI 分别扮演什么角色?相比没有 AI Coding 的传统开发方式,开发者的角色发生了什么变化?
- 测试驱动开发是什么?规范驱动开发是什么?TDD 和 Spec-Driven Development / Spec Coding 怎么理解?
- 你用过业界的 Spec Coding 工具吗?亚马逊的相关 AI Coding / Spec Coding 工具了解或用过吗?用过字节的 AI Coding 工具吗?
《参考解析》
1. 海量数据 Top K:哈希分片 + 小根堆:几十 TB 的 id 不可能一次读进 4G 内存,正确解法是两阶段分治。先按 hash(id) 取模把数据分成 M 个小文件(比如 4096 个),保证同一个 id 永远落到同一个文件;单个文件的大小要压到内存放得下的量级。然后逐个文件用哈希表统计词频,同时维护一个大小为 10 的小根堆,堆顶是当前第 10 名的频次,新词频大于堆顶才入堆并弹掉堆顶,候选集从 O(n) 降到 O(k)。面试里要主动补三个细节:哈希函数要选分布均匀的(MurmurHash、xxHash),否则分片倾斜;哈希表装填因子到 0.7 左右就要扩容,空间不能按 id 数量掐着算;必须先把这个分片的词频统计完再和堆顶比较,边统计边过滤会让同一个 id 被拆到多个批次里算错。最后合并 M 个局部 Top 10 得到全局结果。
2. Redis Lua 的原子性与「失败不回退」:Redis 的命令执行是单线程的,一个 Lua 脚本被当成整体执行,期间不会插入其他客户端的命令,所以「读—判断—写」放进脚本就天然原子,不需要额外加锁。但它和数据库事务的语义完全不同:脚本中途报错不会回滚已经执行过的写命令,也不支持 ROLLBACK,唯一强保证是「执行期间不被穿插」。因此脚本要写成幂等、参数在外部校验好、尽量不触发运行时报错的形式。之所以不用 WATCH/MULTI/EXEC,是因为 MULTI 只能把命令排队,做不到「先读结果再决定后续命令」,WATCH 遇到冲突只能整段重试,高并发下重试率很高。Lua 的收益是把判断与更新合成一次网络往返、一次原子执行,代价是逻辑散在脚本里、调试和版本管理更麻烦,业务复杂后要控制脚本长度。
3. 幂等键怎么设计:幂等键的本质是给「一次业务意图」发一个唯一标识,通常由客户端生成(UUID、雪花号)随请求上行,服务端在入口用它去重。放在客户端生成更贴合语义,因为用户重复点击应当被识别为同一次意图。写入时机最关键:必须和业务处理处在同一段原子逻辑里,实践上就是放进 Lua 脚本一起执行,用 SETNX 或状态位记录三态——处理中(先占位并带短 TTL,挡住并发重复进入)、成功、失败。先处理业务再写幂等键,中间宕机就会出现「钱已扣、无记录」的窗口,重试必然重复;先写幂等键再处理业务,则必须能识别处理中状态,让后来者快速失败或短暂轮询。失败结果要不要缓存取决于失败类型:余额不足这类确定性失败可以缓存以便快速返回,超时、依赖不可用这类瞬时失败应当允许重试。更稳的做法是把幂等键与业务状态放在同一存储、同一事务里(MySQL 唯一索引 + 状态机),Redis 只当前置快速拦截层。
4. 竞拍与秒杀的并发裁决:核心矛盾是「读—判断—写」不是原子操作:两个请求都读到当前价 100、都判断 110 大于 100,后写入的 105 会覆盖先写入的 110。三条路里,数据库悲观锁性能差,乐观锁版本号在冲突密集时重试多,把裁决收敛到单点执行才是主流,也就是 Redis Lua。脚本内要依次校验:拍品是否在有效期内、用户有没有出价资格与保证金、出价是否高于当前最高价并满足最小加价幅度、是否超过封顶价,全部通过才写入最高价和出价人,并把出价流水推进 Stream。落库走异步:消费者组读取 Redis Stream,用 XACK 加待处理列表兜住「读到一半进程挂掉」,消费端用幂等键或 MySQL 唯一索引防重复写。最终结算以 MySQL 为准,Redis 只是热路径加速层;要保证一致性,就用品类状态机把状态原子地从「竞拍中」切到「已结算」,其后的出价一律拒绝,结算读冻结的快照而不是还在变的实时值。
5. 缓存穿透:查一个数据库里也不存在的 key,缓存永远不命中,请求全部落到数据库,被恶意构造不存在的 id 时足以打垮 DB。三种手段:入口做参数校验,格式、范围、权限先过滤掉明显非法的请求;缓存空值,查不到时也写一个带短 TTL(30~60 秒)的空标记,代价是短暂不一致,且要防止大量随机 key 把内存打满;布隆过滤器,把所有存在的 key 预先映射到位图,查询前先过一遍,判定不存在就直接返回,它有小概率误判但不漏判,适合当前置拦截。布隆过滤器的难点是删除与扩容——标准实现不支持删除,可以用计数布隆或定期重建,数据量大时也可换 Cuckoo Filter。生产上通常是三者叠加,再配全局限流兜底。
6. MySQL 索引:B+ 树与设计原则:InnoDB 选 B+ 树是因为它矮胖:非叶子节点只存键值和指针,一个 16KB 页能放上千个键,三四层就能覆盖千万级数据,磁盘 IO 次数稳定;数据全部在叶子节点且用双向链表相连,天然支持范围查询和 ORDER BY,这是 B 树做不到的(数据分散在各层,范围扫描要中序遍历跳来跳去)。和普通二叉搜索树比,关键是树高:BST 在有序插入时退化成链表,即便平衡成红黑树,两千万数据也有二十多层,每层一次磁盘随机 IO。另一种结构是 Hash 索引(Memory 引擎默认,InnoDB 有自适应哈希索引),等值查询 O(1),但不支持范围、排序和最左前缀,冲突严重时会退化。索引不是越多越好:每个二级索引都占空间,写入时要同步维护,增删改都会变慢,优化器可选路径变多还可能选错。设计原则可概括为高频查询优先、区分度高的列靠前、尽量做覆盖索引、遵守最左前缀、避免在索引列上做函数运算和隐式类型转换,最后用 EXPLAIN 看 type、key、rows 验证。
7. CPU 与内存告警怎么排查:CPU 打满先看 top/uptime,区分用户态(us 高,业务计算密集)、内核态(sy 高,系统调用与上下文切换多)和 iowait(wa 高,磁盘瓶颈)。us 高就 top -H 找线程,把线程 id 转十六进制去 jstack 定位栈(Go 用 pprof 的 CPU profile),再用火焰图看热点调用链:横轴采样占比、纵轴调用栈,宽而平的就是优化目标。load average 是运行队列里可运行与不可中断任务的平均数,要和核数一起看;load 高而 CPU 不高,一般是 IO 阻塞或大量 D 状态进程。内存告警先看 free -h 与 /proc/meminfo,判断是堆上涨、page cache 占满还是真泄漏:Java 看 jmap/jstat 的堆与 GC 频率,Go 看 heap profile 的 inuse_space,并排查大对象、无上限的本地缓存和 goroutine 泄漏。OOM 前后看 dmesg 的 oom-killer 记录与容器 limit,确认是被内核杀还是自己退出。
8. 短链接系统设计:核心是「长转短」的映射与「短转长」的跳转。短码生成两条主流路线:发号器加 Base62 编码(自增 id 或号段模式,全局唯一、无冲突,长度随 id 增长,可加起始偏移或打散让短码不连续、不易被遍历),以及哈希取前几位(MD5/Murmur 后截取 6~7 位,需要处理冲突重试,且不可逆推)。存储用 MySQL 保存 short_code 到 long_url 的映射,short_code 建唯一索引,热点数据前面加一层 Redis;跳转用 302 还是 301 要按业务取舍,302 便于统计点击和随时改目标,301 省服务器压力但客户端会长期缓存。防遍历和防滥用靠足够大的短码空间(62^6 约 568 亿)、黑名单与风控、单用户创建配额和敏感链接审核。要进一步缩短 URL,可以从三处下手:压缩短码长度、换更短的域名(这是最大头,协议头和域名本身都占长度)、把同域名长链接的参数做归并。另外还要考虑过期策略、点击统计的异步写入,以及高并发下的缓存与限流。
9. 带 TTL 的 LRU Cache 实现:基础结构是 HashMap 加双向链表:前者 O(1) 定位节点,后者维护访问顺序,头部最近使用、尾部最久未用。get 命中后要把节点移到头部,这就是「get 也算一次最近使用」的原因;set 已存在的 key 只更新值并移到头部,只有新增 key 才判断容量,淘汰尾部节点,且淘汰必须发生在插入之前,否则中间态会短暂超过容量 N。加 TTL 后节点多存一个过期时间戳,淘汰优先级是先扫有没有过期节点、有就优先删,没有才按 LRU 淘汰尾部。清理用惰性删除加后台定时清理:get 发现过期就当作 miss 并删除以保证语义正确,同时用定时任务或采样淘汰分批清理死节点,避免内存堆积。get 到过期 key 要不要续期取决于语义——滑动过期每次访问续期,绝对过期写死时间点。并发下要加锁或分段加锁,保证 HashMap、链表指针和 size 三者同步;用 C++ 的 std::list 时节点指针不暴露,必须把迭代器存进 map 才能在 O(1) 内删任意节点,手写 Node 则要防止删除后留下悬空引用。
10. 三线程循环打印 123123:这是典型的协作而非竞争问题:三个线程不是抢同一个资源,而是按固定顺序轮流执行,所以同步机制要能表达「轮到我了吗」。最直接的写法是共享一把锁加一个状态变量:每个线程在 while 里检查 flag 是否等于自己的编号,不是就 wait 挂起,是就打印、把 flag 改成下一个编号、再 notifyAll 唤醒其他线程。三个必被追问的点:wait 必须写在 while 里而不是 if 里,因为被唤醒后条件可能又不成立(别的线程先抢到锁改了状态),这就是虚假唤醒和「唤醒后要重判」的由来;wait/notify 必须在持有锁的临界区里调用,否则抛 IllegalMonitorStateException;要用 notifyAll 而不是 notify,notify 可能唤醒到不该跑的那个,导致全部挂起。更好的写法是 ReentrantLock 加三个 Condition,每个线程只等自己的条件、用 signal 精确唤醒下一个;也可以直接用三个 Semaphore 串联。
11. ReAct 与 Plan-and-Execute 怎么选:ReAct 是 Thought、Action、Observation 交替推进,每一步先想再调工具、看到结果再决定下一步。优点是实现简单、能根据中间反馈动态调整,适合工具链短、环境变化快的任务;缺点是每步一次 LLM 调用,延迟和 token 成本高,长任务容易跑偏或陷入循环,需要最大步数、重复动作检测这类刹车。Plan-and-Execute 先让模型产出完整计划再逐步执行、必要时重规划:好处是把规划与执行解耦,执行步可以换更便宜的小模型,长任务的目标一致性更好,也更容易人工审核和展示进度;缺点是计划基于错误假设生成时前几步都可能白做,重规划本身也不便宜。选择可以按任务的确定性和长度来分:步骤少、需要频繁与外部世界交互试错的用 ReAct;步骤多、依赖关系明确、能离线拆解的用 Plan-and-Execute。实践中常见两级结构——上层拆解,下层每个子任务内部用 ReAct 执行。
12. WebSocket 与房间设计:WebSocket 靠一次 HTTP 请求完成握手(带 Upgrade: websocket 与 Sec-WebSocket-Key,服务端返回 101 切换协议),之后在同一条 TCP 连接上全双工通信,帧头只有 2~10 字节,省掉 HTTP 每请求的头部开销,属于应用层协议。竞拍场景选它而不是轮询,是因为出价要即时广播给房间内所有人。房间是逻辑概念而非实体表:以 roomId 为聚合根,Redis 里维护成员 Set、出价流水 Stream 和排行榜 ZSet。断线重连要解决「回到原房间」:连接时带上 token 与 roomId,服务端把连接注册进会话表并订阅对应频道;重连时客户端带上最后收到的事件序号,服务端从 Stream 补发断线期间的消息再续推增量,既不丢也不重复。房间数据不必落 MySQL,但结算结果必须落库,房间数据要设 TTL 或用完即删。
13. HTTPS 如何防窃听、防篡改、身份认证:HTTPS 就是 HTTP 加 TLS。防窃听靠密钥交换协商出的对称会话密钥(常用 ECDHE,具备前向安全),之后的业务数据用 AES-GCM 这类对称加密,因为对称加密比非对称快几个数量级。防篡改靠 AEAD 的认证标签或 MAC,每条记录都带完整性校验,中间人改一个字节就会校验失败。身份认证靠数字证书:服务端把公钥、域名、有效期、签发机构交给 CA,CA 用自己的私钥对证书摘要签名,客户端内置受信任的根证书,逐级验签即可确认「这个公钥确实属于这个域名」;证书链任何一环对不上、域名不匹配、过期或被吊销都会告警。TLS 1.3 握手大致是 ClientHello(带支持的套件与 key_share)、ServerHello 加证书与 Finished、双方用 ECDHE 算出共享密钥、客户端 Finished,1-RTT 完成。至于伪造证书冒充百度:攻击者没有百度的私钥,签不出能被验证的证书,自签证书不在系统信任链里会直接报错,真正的攻击面只剩诱导用户手动信任恶意根证书或 CA 被攻破错签。
14. 手撕题的常见扣分点:二分查找的坑集中在边界:循环条件用 left <= right 还是 left < right,mid 用 left + (right - left) / 2 防溢出,取整方向决定了下一轮是 left = mid 还是 left = mid + 1,写错就死循环;求第一个或最后一个满足条件的位置时模板与普通查找不同,动手前先口述要找的是哪个边界。链表反转的核心是三指针 prev、cur、next:先把 next 存下来再改 cur.next,否则链会断;循环结束返回的是 prev 而不是 cur(此时 cur 为 null);递归写法要能讲清 base case 与每层返回值的含义。面试官追问「你刚才这段代码有什么问题」,通常就是这几处:没处理空链表和单节点、没保存 next、返回值写错、变量命名混乱。K 个一组翻转是把反转限定在区间内,需要哨兵节点和区间前后指针的重新连接,最后别忘处理不足 K 个的尾部。