Skip to content

📚数据库相关

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 标准定义了四个隔离级别,级别越高,安全性越高,但性能越低。

  1. Read Uncommitted(读未提交):

    • 允许读取尚未提交的数据。

    • 问题: 会出现脏读(Dirty Read)。

  2. Read Committed(读已提交 - RC):

    • 只能读取已经提交的数据。

    • 问题: 会出现不可重复读(同一个事务里两次读到的数据不一样)。

    • Oracle、SQL Server 默认级别。

  3. Repeatable Read(可重复读 - RR):

    • 同一个事务中多次读取同样的数据,结果是一致的。

    • 问题: 理论上会出现幻读(Phantom Read,即第一次读没有,第二次读突然多了几行数据)。

  4. Serializable(串行化):

    • 强制事务串行执行。

    • 问题: 性能极差,极少使用。


III. MySQL 是哪一种隔离级别?怎么实现的?

默认隔离级别: Repeatable Read (RR)

面试核心考点: 标准的 RR 级别解决不了幻读,但 MySQL 的 InnoDB 引擎在 RR 级别下,通过特殊的手段解决了绝大部分幻读问题。

实现原理(MVCC + 锁):

MySQL InnoDB 采用了 MVCC(多版本并发控制) 配合  来实现隔离级别。

  1. 读操作(快照读):利用 MVCC

    • 原理: 每个事务开启时会生成一个 Read View(读视图),并通过 Undo Log 维持数据的历史版本链。

    • RC 与 RR 的区别:

      • RC: 每次执行 Select 语句时,都会重新生成一个 Read View(所以能读到别人刚提交的)。

      • RR: 只在事务开始后的第一次 Select 生成一次 Read View,之后复用这个视图(所以保证了可重复读)。

  2. 写操作/当前读(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点):

  1. 高区分度(Cardinality)优先:

    • 索引列的数据重复率越低越好。

    • 例子: UUID、手机号、身份证 适合做索引;性别、状态(只有0/1)不适合(因为你要扫描一半的表,数据库优化器可能直接选择全表扫描)。

  2. 针对核心查询设计(最左前缀):

    • 不要为每个字段单独建索引,而是根据业务 SQL 的 WHERE、ORDER BY、GROUP BY 字段建立联合索引

    • 遵循最左前缀法则,把最常用的筛选字段放在最左边。

  3. 尽量使用覆盖索引:

    • 设计索引时,尽量让索引包含查询所需的所有字段,避免回表,性能提升巨大。
  4. 控制索引数量:

    • 索引不是越多越好。索引会占用磁盘空间,且会降低 INSERT、UPDATE、DELETE 的速度(因为要维护索引树)。

    • 一般单表索引不超过 5 个。

  5. 索引字段要短(前缀索引):

    • 如果是长字符串(如 varchar(255)),尽量指定前缀长度(如只索引前 20 个字符),以节省 B+ 树空间,减少 I/O。

VI. 为什么使用 B+ 树?(高频考点)

面试官通常会问:“为什么不用二叉树?为什么不用 Hash?为什么不用 B 树?”

答案逻辑:

  1. 为什么不用二叉树 / 平衡二叉树(AVL) / 红黑树?

    • 树太高了: 这些树每个节点只能存储一个数据,当数据量大(百万级)时,树的高度会很高。

    • I/O 代价大: 数据库索引存储在磁盘上,树的每一层代表一次磁盘 I/O。树越高,查询越慢。B+ 树一个节点可以存很多 Key,树非常“矮胖”(通常 3 层就能存 2000 万数据),I/O 次数少。

  2. 为什么不用 Hash?

    • 不支持范围查询: Hash 只能进行等值查询(=, IN),无法处理 > 100 这种范围查询,也无法利用索引排序。
  3. 为什么不用 B 树(B-Tree),而用 B+ 树?(核心对比)

    • B 树: 每个节点(包括叶子和非叶子)都存储 data。

    • B+ 树: 只有叶子节点存储 data,非叶子节点只存 key(索引值)。

    • B+ 树的优势:

      1. 范围查询的能力更强: B+ 树的叶子节点使用双向链表连接。如果做范围查询(比如 id > 10),只需要找到 10,然后顺着链表往后取就行了。而 B 树需要进行中序遍历,效率低。

      2. 磁盘读写代价更低: 因为非叶子节点不存 data,只存 key,所以一个磁盘块能容纳更多的节点索引,树会更矮,I/O 次数更少。

      3. 查询效率更稳定: 任何查询都必须走到叶子节点。

总结面试回答话术:

  1. ACID:原子、一致、隔离、持久。分别靠 Undo Log、代码逻辑、锁/MVCC、Redo Log 保证。

  2. 隔离级别:读未提交、读已提交(RC)、可重复读(RR)、串行化。

  3. MySQL:默认 RR。通过 MVCC(解决读)和 Next-Key Lock(解决写时的幻读)实现。

  4. 索引:数据结构,为了减少 I/O。

  5. B+ 树:因为树矮(减少 I/O)、叶子节点有链表(适合范围查询)、数据都在叶子节点(查询稳定)。


VII. 索引失效的情况有哪些?(必考)

只要没用上 B+ 树的快速查找特性,退化成全表扫描,就是失效。记住口诀:“模运空,最左配,类型错”

  1. 违反最左前缀法则:

    • 联合索引 (a, b, c)。

    • 查询 where b = 1 或 where c = 1 失效(跳过了 a)。

    • 查询 where a = 1 and c = 1,a 走了索引,c 没走(因为中间断了 b)。

  2. 在索引列上做运算:

    • where id + 1 = 10 失效。应改为 id = 10 - 1。

    • where substring(name, 0, 3) = 'abc' 失效

  3. 模糊查询 % 在最前面:

    • where name like '%abc' 失效(B+ 树是从左往右排的,不知道开头是啥,没法查)。

    • where name like 'abc%' 有效

  4. 类型隐式转换:

    • 字段是字符串 varchar,查询用了数字 where phone = 123 失效(数据库不仅要把数字转字符串,通常还会因为字符集转换导致全表扫描)。
  5. 使用 OR 条件:

    • where id = 1 or age = 18。如果 id 有索引,age 没索引,整个索引失效(因为 age 那边必须全表扫,索性全表扫了)。
  6. 不等号与 NULL(视情况):

    • !=, <>, IS NULL, IS NOT NULL 在某些版本或数据分布下会导致失效。

VIII. Spring 事务:为什么不推荐滥用声明式事务?

注意:不是“不推荐使用”,而是“不推荐在高性能/复杂业务中滥用 @Transactional 注解”。

核心原因:粒度太大,容易长事务。

  1. 占用连接时间过长:

    • @Transactional 加在方法上,意味着进入方法前开启事务,方法完全结束后才提交。

    • 如果方法里包含 RPC 调用、HTTP 请求、复杂计算 或 文件 I/O,这些耗时操作都会占用数据库连接。数据库连接池(如 Druid/Hikari)很快会被耗尽,导致系统吞吐量下降。

  2. 锁竞争:

    • 事务持有锁的时间变长,增加了死锁和锁等待的概率。

IX. 声明式事务(@Transactional)失效的场景?(八股文必背)

面试官最喜欢问这个,以此判断你有没有踩过坑。

  1. 方法不是 public 的: Spring AOP 默认只拦截 public 方法。

  2. 同类内部调用(最经典):

    • 类 A 中有一个 methodA(无事务)调用了 methodB(有事务)。

    • 失效原因: Spring 事务基于 AOP 代理对象。也就是外部调用类 A 时,是调用了代理。但类内部 this.methodB() 是直接调用的原对象方法,绕过了代理,所以没有切面逻辑。

  3. 异常类型不匹配:

    • 默认情况下,Spring 只在遇到 RuntimeException 或 Error 时回滚。

    • 如果抛出的是 Checked Exception(如 IOException, SQLException),事务不会回滚

    • 解决: @Transactional(rollbackFor = Exception.class)。

  4. 未抛出异常:

    • 你在代码里手动 catch 了异常且没有抛出,代理类以为方法执行成功,就会提交事务。
  5. 数据库引擎不支持: 如 MySQL 选了 MyISAM 引擎(不支持事务)。


X. 推荐怎么实现事务?(编程式事务)

为了解决“粒度太大”的问题,推荐使用 编程式事务。或者在分布式系统中,使用分布式事务中间件。

工具: TransactionTemplate。

代码示例:

java
@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;
    });
}

优点:

  1. 锁粒度小: 事务仅包裹 DB 操作,RPC 等耗时操作在事务外。

  2. 控制精准: 开发者完全掌控何时开始、何时提交。


XI. 补充题库

如果你想拿高薪,以下题目建议准备:

  1. 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)。

  2. 锁相关:

    • 乐观锁 vs 悲观锁:悲观锁是“先锁后改”,适合写入多的情况。乐观锁是“改时检查”即在写入时与旧值进行检查,如果不符更新新值再尝试修改,适合读多写少的情况。

    • 死锁是怎么产生的?如何排查: 两个事务互相持有对方需要的锁且不释放,形成环路导致死锁;排查靠 show engine innodb status 或死锁日志。

  3. Redis 与 MySQL 双写一致性:

    • 先删缓存还是先更新数据库?(延时双删)。

    • 最佳实践:Canal 监听 Binlog 异步更新缓存。

  4. 分库分表:

    • 什么时候分?(单表超 500w~1000w 或 占用空间过大)。

    • 垂直分表 vs 水平分表。

    • 分表后分页查询怎么做?

  5. 深分页解决:

    • 游标分页:

      通过一个有序的索引字段作为游标,通过游标来执行分页,缺点是无法跳页。

    • 子查询优化: 原理: 先通过覆盖索引(只查主键 ID,不需要回表)快速定位到第 100 万条的位置,拿到这 10 个 ID,然后再去主表查数据的详情。

      • 普通写法(慢):

        sql
        SELECT * FROM user LIMIT 1000000, 10;
        -- 慢在:回表了 1000010 次,然后丢弃了前 100 万次结果。
      • **优化写法(快):

        sql
        SELECT t1.* 
        FROM user t1, 
             (SELECT id FROM user LIMIT 1000000, 10) t2  -- 这里只查ID,走覆盖索引,极快
        WHERE t1.id = t2.id;