面灵AI→

易宝软件 Java 一面:MVCC、ThreadLocal 与层序遍历

轮次
一面
base
杭州
结果
一面已过
时间
2026-10
来源
牛客网

《面试题目》

  1. 自我介绍。
  2. 讲一下做过的项目(支付宝生活助手 Agent 开发)。
  3. MySQL 的 MVCC 是怎么实现的?
  4. 讲讲 ThreadLocal。
  5. 手撕:二叉树的层序遍历。

《参考解析》

MVCC 解决的是「读不阻塞写、写不阻塞读」,代价是读到的可能是历史版本。 InnoDB 给每行加了两个隐藏列:最近修改它的事务 ID(DB_TRX_ID)和指向 undo log 中上一个版本的指针(DB_ROLL_PTR),靠 undo 串成版本链。读的时候生成一个 ReadView,里面记四样东西:当前活跃事务 ID 集合、集合最小值、下一个待分配事务 ID、以及自己的事务 ID。可见性判断就是拿行上的 DB_TRX_ID 去比:小于最小值说明早已提交、可见;大于等于下一个待分配 ID 说明是之后才开的事务、不可见;落在活跃集合里说明还没提交、不可见,于是沿版本链往回找可见版本。两个隔离级别的差别只在 ReadView 的生成时机:读已提交每次 select 都重新生成,所以能看到别人新提交的数据;可重复读只在第一次 select 时生成,之后整事务复用同一份,因此同一事务里多次读结果一致。

追问通常落在「RR 到底有没有解决幻读」上,要答得有层次。 标准定义里可重复读只挡不可重复读,幻读要靠串行化;MySQL 的 RR 用「快照读走 MVCC + 当前读加 next-key lock(记录锁 + 间隙锁)」把大部分幻读挡掉了,但严格说并不能完全避免,因为当前读(select ... for update、update、delete)读的是最新版本,会看到别的事务新插入并提交的行。间隙锁的代价也值得说:它锁的是区间,会阻塞并发插入、抬高死锁概率,所以互联网业务里不少团队把隔离级别降到读已提交来换并发。再往下会被问到 purge 线程怎么清理不再被任何 ReadView 需要的 undo、长事务为什么会拖大 undo 表和版本链,这些都是同一个知识点的延伸。

ThreadLocal 的关键是「Thread 持有 Map,而不是 Map 持有 Thread」。 每个 Thread 内部有一个 ThreadLocalMap,key 是 ThreadLocal 对象的弱引用,value 是强引用。用弱引用的目的是让 ThreadLocal 对象本身能被回收,但回收之后 key 变成 null,value 还挂在 Map 里没人能再访问到,就形成了 key 为 null 的脏 Entry —— 这就是常说的内存泄漏。所以规则是「用完必须 remove」,尤其是线程池场景:线程不会销毁,这个 Map 会一直活到线程结束,泄漏的不只是内存,还可能是上一个请求的用户身份被下一个请求读到(串号)。另外它的哈希冲突用的是开放定址线性探测,而不是链表,所以扩容逻辑和 HashMap 不一样;跨线程传递用 InheritableThreadLocal 只在新建线程时拷贝一次,线程池里时机不对,需要显式透传或专门的可传递方案。

层序遍历是队列题,但手写时几个细节决定成败。 主流程是根节点入队,循环里先取出当前队列长度作为「这一层」的节点数,逐个出队时把左右孩子入队,这样每一轮正好处理一层。要主动处理头节点为空、以及空树返回空列表的边界;Java 里用 ArrayDeque 而不是 LinkedList。复杂度是时间 O(n)、额外空间 O(最大层宽)。面试官常顺着加码:之字形遍历(按层反转结果)、每层最大值、二叉树右视图、填充每层节点的 next 指针(进阶要求 O(1) 空间,用已建立的 next 链当队列),答完基础版可以主动提一句这些变体。

这场面试本身反映了一条规律:简历上写了什么,就会被问什么。 简历里点名写了 MVCC 和 ThreadLocal,面试官就只问了这两样,说明中小厂与外包岗的八股范围往往就是简历自述的技术点,准备时应该按简历逐条准备「原理 + 追问 + 实际用过什么」三件套,而不是无差别刷题。手撕难度不高,重点是写得快、边界不漏、能一边写一边讲思路。这类岗位还值得提前问清作息、有没有年终奖、外包身份与转正通道——信息在面试阶段问,比入职后才知道要好。