📚数据库相关
I. 数据库 ACID 指的是什么?
ACID 是事务(Transaction)的四个基本特性,用来保证数据库操作的可靠性。
A - Atomicity(原子性):
定义: 事务包含的所有操作要么全部成功,要么全部失败回滚,不存在中间状态。
实现原理(加分项): 依赖 Undo Log(回滚日志)。如果事务失败,数据库利用 Undo Log 将数据回滚到事务开始前的状态。
C - Consistency(一致性):
定义: 事务执行前后,数据库必须从一个一致性状态变换到另一个一致性状态。比如转账,A 扣钱,B 必须加钱,总金额不变。
备注: 这是最终目标,A、I、D 都是为了保证 C。
I - Isolation(隔离性):
定义: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。
实现原理(加分项): 依赖 锁(Lock) 和 MVCC(多版本并发控制)。
D - Durability(持久性):
定义: 一个事务一旦被提交,它对数据库中数据的改变就是永久性的,即使数据库发生故障也不应该丢失。
实现原理(加分项): 依赖 Redo Log(重做日志)。
II. 事务隔离级别有哪几种?
SQL 标准定义了四个隔离级别,级别越高,安全性越高,但性能越低。
Read Uncommitted(读未提交):
允许读取尚未提交的数据。
问题: 会出现脏读(Dirty Read)。
Read Committed(读已提交 - RC):
只能读取已经提交的数据。
问题: 会出现不可重复读(同一个事务里两次读到的数据不一样)。
Oracle、SQL Server 默认级别。
Repeatable Read(可重复读 - RR):
同一个事务中多次读取同样的数据,结果是一致的。
问题: 理论上会出现幻读(Phantom Read,即第一次读没有,第二次读突然多了几行数据)。
Serializable(串行化):
强制事务串行执行。
问题: 性能极差,极少使用。
III. MySQL 是哪一种隔离级别?怎么实现的?
默认隔离级别: Repeatable Read (RR)。
面试核心考点: 标准的 RR 级别解决不了幻读,但 MySQL 的 InnoDB 引擎在 RR 级别下,通过特殊的手段解决了绝大部分幻读问题。
实现原理(MVCC + 锁):
MySQL InnoDB 采用了 MVCC(多版本并发控制) 配合 锁 来实现隔离级别。
读操作(快照读):利用 MVCC
原理: 每个事务开启时会生成一个 Read View(读视图),并通过 Undo Log 维持数据的历史版本链。
RC 与 RR 的区别:
RC: 每次执行 Select 语句时,都会重新生成一个 Read View(所以能读到别人刚提交的)。
RR: 只在事务开始后的第一次 Select 生成一次 Read View,之后复用这个视图(所以保证了可重复读)。
写操作/当前读(For Update):利用锁
为了防止幻读,InnoDB 在 RR 级别下使用了 Next-Key Lock(临键锁)。
Next-Key Lock = Record Lock(行锁) + Gap Lock(间隙锁)。
它不仅锁住记录本身,还锁住记录之间的“间隙”,防止其他事务在这个范围内插入新数据,从而解决了幻读。
IV. 索引是什么?原理是什么?
索引是什么:
索引是帮助 MySQL 高效获取数据的数据结构。可以把它理解为书籍的目录。如果没有索引,数据库必须全表扫描(从头翻到尾),效率极低。
原理:
通过特定的算法(如 B+ 树)将数据组织成有序结构,从而大大减少数据检索时的磁盘 I/O 次数。
常见类型:
B+ 树索引(最常用,InnoDB 默认)。
Hash 索引(适合精确匹配,不支持范围查询)。
Full-text 全文索引。
V. 索引的设计原则是什么?
一句话总结:
“高区分度、核心SQL优先、尽量覆盖、控制数量、短小精悍。”
详细拆解(记这5点):
高区分度(Cardinality)优先:
索引列的数据重复率越低越好。
例子: UUID、手机号、身份证 适合做索引;性别、状态(只有0/1)不适合(因为你要扫描一半的表,数据库优化器可能直接选择全表扫描)。
针对核心查询设计(最左前缀):
不要为每个字段单独建索引,而是根据业务 SQL 的 WHERE、ORDER BY、GROUP BY 字段建立联合索引。
遵循最左前缀法则,把最常用的筛选字段放在最左边。
尽量使用覆盖索引:
- 设计索引时,尽量让索引包含查询所需的所有字段,避免回表,性能提升巨大。
控制索引数量:
索引不是越多越好。索引会占用磁盘空间,且会降低 INSERT、UPDATE、DELETE 的速度(因为要维护索引树)。
一般单表索引不超过 5 个。
索引字段要短(前缀索引):
- 如果是长字符串(如 varchar(255)),尽量指定前缀长度(如只索引前 20 个字符),以节省 B+ 树空间,减少 I/O。
VI. 为什么使用 B+ 树?(高频考点)
面试官通常会问:“为什么不用二叉树?为什么不用 Hash?为什么不用 B 树?”
答案逻辑:
为什么不用二叉树 / 平衡二叉树(AVL) / 红黑树?
树太高了: 这些树每个节点只能存储一个数据,当数据量大(百万级)时,树的高度会很高。
I/O 代价大: 数据库索引存储在磁盘上,树的每一层代表一次磁盘 I/O。树越高,查询越慢。B+ 树一个节点可以存很多 Key,树非常“矮胖”(通常 3 层就能存 2000 万数据),I/O 次数少。
为什么不用 Hash?
- 不支持范围查询: Hash 只能进行等值查询(=, IN),无法处理 > 100 这种范围查询,也无法利用索引排序。
为什么不用 B 树(B-Tree),而用 B+ 树?(核心对比)
B 树: 每个节点(包括叶子和非叶子)都存储 data。
B+ 树: 只有叶子节点存储 data,非叶子节点只存 key(索引值)。
B+ 树的优势:
范围查询的能力更强: B+ 树的叶子节点使用双向链表连接。如果做范围查询(比如 id > 10),只需要找到 10,然后顺着链表往后取就行了。而 B 树需要进行中序遍历,效率低。
磁盘读写代价更低: 因为非叶子节点不存 data,只存 key,所以一个磁盘块能容纳更多的节点索引,树会更矮,I/O 次数更少。
查询效率更稳定: 任何查询都必须走到叶子节点。
总结面试回答话术:
ACID:原子、一致、隔离、持久。分别靠 Undo Log、代码逻辑、锁/MVCC、Redo Log 保证。
隔离级别:读未提交、读已提交(RC)、可重复读(RR)、串行化。
MySQL:默认 RR。通过 MVCC(解决读)和 Next-Key Lock(解决写时的幻读)实现。
索引:数据结构,为了减少 I/O。
B+ 树:因为树矮(减少 I/O)、叶子节点有链表(适合范围查询)、数据都在叶子节点(查询稳定)。
VII. 索引失效的情况有哪些?(必考)
只要没用上 B+ 树的快速查找特性,退化成全表扫描,就是失效。记住口诀:“模运空,最左配,类型错”。
违反最左前缀法则:
联合索引 (a, b, c)。
查询 where b = 1 或 where c = 1 失效(跳过了 a)。
查询 where a = 1 and c = 1,a 走了索引,c 没走(因为中间断了 b)。
在索引列上做运算:
where id + 1 = 10 失效。应改为 id = 10 - 1。
where substring(name, 0, 3) = 'abc' 失效。
模糊查询 % 在最前面:
where name like '%abc' 失效(B+ 树是从左往右排的,不知道开头是啥,没法查)。
where name like 'abc%' 有效。
类型隐式转换:
- 字段是字符串 varchar,查询用了数字 where phone = 123 失效(数据库不仅要把数字转字符串,通常还会因为字符集转换导致全表扫描)。
使用 OR 条件:
- where id = 1 or age = 18。如果 id 有索引,age 没索引,整个索引失效(因为 age 那边必须全表扫,索性全表扫了)。
不等号与 NULL(视情况):
- !=, <>, IS NULL, IS NOT NULL 在某些版本或数据分布下会导致失效。
VIII. Spring 事务:为什么不推荐滥用声明式事务?
注意:不是“不推荐使用”,而是“不推荐在高性能/复杂业务中滥用 @Transactional 注解”。
核心原因:粒度太大,容易长事务。
占用连接时间过长:
@Transactional 加在方法上,意味着进入方法前开启事务,方法完全结束后才提交。
如果方法里包含 RPC 调用、HTTP 请求、复杂计算 或 文件 I/O,这些耗时操作都会占用数据库连接。数据库连接池(如 Druid/Hikari)很快会被耗尽,导致系统吞吐量下降。
锁竞争:
- 事务持有锁的时间变长,增加了死锁和锁等待的概率。
IX. 声明式事务(@Transactional)失效的场景?(八股文必背)
面试官最喜欢问这个,以此判断你有没有踩过坑。
方法不是 public 的: Spring AOP 默认只拦截 public 方法。
同类内部调用(最经典):
类 A 中有一个 methodA(无事务)调用了 methodB(有事务)。
失效原因: Spring 事务基于 AOP 代理对象。也就是外部调用类 A 时,是调用了代理。但类内部 this.methodB() 是直接调用的原对象方法,绕过了代理,所以没有切面逻辑。
异常类型不匹配:
默认情况下,Spring 只在遇到 RuntimeException 或 Error 时回滚。
如果抛出的是 Checked Exception(如 IOException, SQLException),事务不会回滚。
解决: @Transactional(rollbackFor = Exception.class)。
未抛出异常:
- 你在代码里手动 catch 了异常且没有抛出,代理类以为方法执行成功,就会提交事务。
数据库引擎不支持: 如 MySQL 选了 MyISAM 引擎(不支持事务)。
X. 推荐怎么实现事务?(编程式事务)
为了解决“粒度太大”的问题,推荐使用 编程式事务。或者在分布式系统中,使用分布式事务中间件。
工具: TransactionTemplate。
代码示例:
@Autowired
private TransactionTemplate transactionTemplate;
public void doSomething() {
// 1. 做一些耗时的非DB操作(如调用第三方API、计算)
heavyCalculation();
// 2. 只有在真正操作数据库时,才开启事务
transactionTemplate.execute(status -> {
try {
userMapper.update(user);
orderMapper.insert(order);
// 业务逻辑成功,隐式提交
} catch (Exception e) {
status.setRollbackOnly(); // 手动回滚
throw e;
}
return null;
});
}优点:
锁粒度小: 事务仅包裹 DB 操作,RPC 等耗时操作在事务外。
控制精准: 开发者完全掌控何时开始、何时提交。
XI. 补充题库
如果你想拿高薪,以下题目建议准备:
SQL 优化相关:
Explain 命令结果参数怎么看?主要参数如下:
id:优先级,表示每个sql查询的优先级编号
select_type:SQL类型,SIMPLE就是简单SQL,无子查询和unit
table:操作的表名或别名
partitions:分区,分库分表时会用到
type(重要):查询方式,性能由查到好包括包括ALL(全表扫描,需要避免)、index(索引全扫描)、RANGE(范围索引扫描)、ref(使用非唯一性索引或者唯一索引的前缀扫描,返回匹配某个单独值的记录行)、eq_ref(相对于ref来说就是使用的是唯一索引,对于每个索引键值,只有唯一的一条匹配记录)、const/system(单表中最多只有一条匹配行)
possible_keys:可能存在的索引
key:正在使用的索引
如何做慢查询分析?(开启慢查询日志 slow_query_log)。
锁相关:
乐观锁 vs 悲观锁:悲观锁是“先锁后改”,适合写入多的情况。乐观锁是“改时检查”即在写入时与旧值进行检查,如果不符更新新值再尝试修改,适合读多写少的情况。
死锁是怎么产生的?如何排查: 两个事务互相持有对方需要的锁且不释放,形成环路导致死锁;排查靠 show engine innodb status 或死锁日志。
Redis 与 MySQL 双写一致性:
先删缓存还是先更新数据库?(延时双删)。
最佳实践:Canal 监听 Binlog 异步更新缓存。
分库分表:
什么时候分?(单表超 500w~1000w 或 占用空间过大)。
垂直分表 vs 水平分表。
分表后分页查询怎么做?
深分页解决:
游标分页:
通过一个有序的索引字段作为游标,通过游标来执行分页,缺点是无法跳页。
子查询优化: 原理: 先通过覆盖索引(只查主键 ID,不需要回表)快速定位到第 100 万条的位置,拿到这 10 个 ID,然后再去主表查数据的详情。
普通写法(慢):
sqlSELECT * FROM user LIMIT 1000000, 10; -- 慢在:回表了 1000010 次,然后丢弃了前 100 万次结果。**优化写法(快):
sqlSELECT t1.* FROM user t1, (SELECT id FROM user LIMIT 1000000, 10) t2 -- 这里只查ID,走覆盖索引,极快 WHERE t1.id = t2.id;