字节跳动后端一面:MySQL、Java多线程与Spring循环依赖
面试题目
数据库
- MySQL 事务隔离级别有哪些?
- 讲一下脏读、幻读、不可重复读的区别?
- MySQL 索引的工作原理是什么?
- 为什么使用 B+ 树而不用 B 树?
- 联合索引是怎么实现的?
Java & 并发
- 线程和进程的区别?
- Java 线程之间如何传递参数?
Spring
- Spring 如何解决循环依赖?
算法
- 打家劫舍变种(题目较短但情况较多,难度较高)
参考解析
1. MySQL 事务隔离级别
共四个级别(由低到高):READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ(MySQL 默认)、SERIALIZABLE。
级别越高并发性能越低,但数据一致性越强。MySQL InnoDB 默认使用 REPEATABLE READ,并通过 MVCC + Gap Lock 额外解决了幻读问题。
2. 脏读、不可重复读、幻读区别
| 问题 | 描述 | 解决隔离级别 |
|---|---|---|
| 脏读 | 读到其他事务未提交的数据 | READ COMMITTED |
| 不可重复读 | 同一事务内两次读同一行,结果不同(被修改/删除) | REPEATABLE READ |
| 幻读 | 同一事务内两次查询,结果集行数不同(被插入新行) | SERIALIZABLE(或 InnoDB 的间隙锁) |
3. MySQL 索引工作原理
InnoDB 使用 B+ 树存储索引,主键索引(聚簇索引)叶子节点存储完整行数据,二级索引叶子节点存储主键值。 查询时先走二级索引找到主键,再回表查聚簇索引(称为”回表”)。覆盖索引可避免回表。
4. B+ 树 vs B 树
- B+ 树非叶子节点只存键不存数据,同等页大小能存更多键,树更矮,IO 次数更少。
- B+ 树叶子节点用链表相连,范围查询只需遍历链表,效率远高于 B 树的中序遍历。
- B 树每个节点都存数据,不适合范围扫描,且节点存储数据后分叉数减少,树更高。
5. 联合索引实现
联合索引本质仍是一棵 B+ 树,键为多列拼接后按最左前缀原则排序。
例如 (a, b, c) 联合索引,先按 a 排序,a 相同再按 b,b 相同再按 c。
查询必须从最左列开始连续匹配,跳过中间列则后续列无法走索引。
6. 线程与进程区别
- 进程是资源分配的基本单位,拥有独立内存空间;线程是CPU调度的基本单位,共享进程内存。
- 线程切换开销小于进程切换;进程崩溃不影响其他进程,线程崩溃可能导致整个进程退出。
- 线程间通信比进程间通信更便捷(共享堆内存),但需注意同步问题。
7. Java 线程间参数传递
- 共享变量:通过成员变量或
volatile/AtomicXxx传递,需注意可见性与线程安全。 - 构造函数/Callable:创建线程时通过构造参数传入,
Callable+Future可获取返回值。 - ThreadLocal:每个线程独立副本,父子线程可用
InheritableThreadLocal传递。 - 阻塞队列:生产者-消费者模式下通过
BlockingQueue传递数据。
8. Spring 解决循环依赖
Spring 通过三级缓存解决单例 Bean 的构造后循环依赖:
- 一级缓存(singletonObjects):存放完全初始化的 Bean。
- 二级缓存(earlySingletonObjects):存放提前暴露的早期引用(已实例化但未填充属性)。
- 三级缓存(singletonFactories):存放 ObjectFactory,用于生成早期引用(支持 AOP 代理)。
注意:构造器注入的循环依赖无法解决;@Prototype 作用域的循环依赖也无法解决。
算法:打家劫舍变种
经典打家劫舍(LeetCode 198)核心是 DP:dp[i] = max(dp[i-1], dp[i-2] + nums[i])。
变种常见形式:环形(LC 213,拆成两段分别DP取最大)、树形(LC 337,树形DP)、带冷冻期等。
建议先明确约束条件,画出状态转移方程,再处理边界情况。