侧边栏壁纸
博主头像
PPP的日记

行动起来,活在当下

  • 累计撰写 13 篇文章
  • 累计创建 14 个标签
  • 累计收到 23 条评论

目 录CONTENT

文章目录

MySQL

MySQL 增删改查与高性能查询

1. 直击要点 (TL;DR)

(面试开场 30 秒口述范本)

“关于 SQL 查询,我认为核心不在于语法,而在于执行计划

  1. 单表查询重点在于理解 SQL 的执行顺序(先 FROM/WHERE 再 SELECT)以及如何利用索引下推减少回表。

  2. 复杂查询(如 Group By/Order By)的关键是避免使用临时表和文件排序,尽量利用索引有序性

  3. 多表查询的核心原则是小表驱动大表,并且被驱动表的连接字段必须有索引,以利用 NLJ(Nested-Loop Join)算法避免全表扫描。

总是使用 EXPLAIN 分析性能瓶颈是我开发的习惯。”


2. 详细拆解

2.1 单表查询:执行顺序与回表

面试中常问:“为什么我的 SQL 写得很简单,但还是很慢?” —— 往往是因为不懂执行顺序或索引失效。

  • 基础概念

    SQL 的书写顺序(SELECT... FROM... WHERE...)和执行顺序是相反的。

    执行顺序FROM (加载表) -> ON/JOIN -> WHERE (过滤) -> GROUP BY -> HAVING -> SELECT (投影) -> ORDER BY -> LIMIT

  • 核心原理(回表与覆盖索引)

    • 回表:普通索引只存了(索引列 + 主键 ID)。如果查询了索引没覆盖到的列,MySQL 必须拿着 ID 回到主键索引(聚簇索引)里再查一次。

    • 强制比喻

      回表就像你去图书馆查书。

      • 普通索引是“索引卡片柜”:你查到了书名(索引列)和 索书号(主键ID)。

      • 回表:你拿着索书号,跑去“书架”(聚簇索引/数据行)把整本书(所有列)拿下来。

      • 覆盖索引:你要查的信息(比如只要书名和作者)刚好都写在“索引卡片”上了,你不用跑去书架就能直接回复,这就叫“覆盖索引”,速度极快。

  • 亮点/加分项

    提到 ICP (Index Condition Pushdown) 索引下推。在 MySQL 5.6 以后,存储引擎会在遍历索引时,直接判断 WHERE 条件是否满足,不满足的直接跳过,减少回表次数。

2.2 复杂查询:分页与排序

这是线上故障的高发区,尤其是深分页(Deep Pagination)。

  • 基础概念

    ORDER BYLIMIT 配合使用。

  • 核心原理(Sort Buffer & 深度分页)

    • 排序:MySQL 会为每个线程分配一块内存叫 sort_buffer。如果排序的数据量太大,内存装不下,就必须利用磁盘临时文件,效率瞬间暴跌。

    • 深度分页LIMIT 1000000, 10。MySQL 需要读取 100万+10 条记录,扔掉前 100万 条。

    • 强制比喻

      深度分页就像是让你从一本书的第 10000 页开始朗读 10 行。

      笨办法(普通 Offset):你必须从第 1 页开始,一页一页翻(扫描),翻过 9999 页扔掉,才读那 10 行。累死且无意义。

      聪明办法(子查询优化):你先看目录(覆盖索引),直接找到第 10000 页对应的“章节ID”(主键ID),然后直接跳到那一页开始读。

  • 亮点/加分项

    优化方案口述:“对于深度分页,我会使用延迟关联(Late Row Lookup),先通过覆盖索引查出主键 ID,再用 INNER JOIN 关联原表获取完整数据。”

    img

2.3 多表查询:Join 的艺术

大厂现在往往禁止三张表以上的 Join,甚至建议在 Java 代码层组装数据,但 Join 原理必问。

  • 基础概念

    Left Join, Inner Join。

  • 核心原理(NLJ vs BNL)

    • NLJ (Index Nested-Loop Join):被驱动表有索引。

    • BNL (Block Nested-Loop Join):被驱动表无索引(性能杀手,MySQL 8.0 引入 Hash Join 优化)。

    • 强制比喻

      假设 A 表(小表,班级 A) 要找 B 表(大表,班级 B) 里同名的学生。

      • NLJ (有索引):班级 A 的同学拿着名单,去班级 B 的门口叫名字。因为班级 B 有座位表(索引),一叫名字立马知道人在哪里。效率高。

      • BNL (无索引):班级 B 没有座位表,乱坐的。班级 A 的每一个同学进去,都要把班级 B 的所有人从头到尾问一遍“你是某某吗?”。如果 A 班 100 人,B 班 1000 人,就要问 10 万次。这就是全表扫描的笛卡尔积,系统直接卡死。

  • 亮点/加分项

    “小表驱动大表” 原则。无论语法怎么写,优化器通常会选择数据量小(结果集小)的表作为驱动表,去“撩”大表。


3. 深挖追问 (模拟压力面)

面试官听完你的陈述,眉头一皱,推了推眼镜,开始追问:

  1. 关于 SELECT \*

    • Q: “你刚才说要避免 SELECT *,除了无法利用覆盖索引,它还有什么坏处?”

    • A: (提示方向:网络带宽消耗、大字段如 Text/Blob 带来的磁盘 IO 压力、不仅不能覆盖索引还会导致额外的排序开销。)

  2. 关于最左前缀原则

    • Q: “我有联合索引 (a, b, c)。如果 SQL 语句是 WHERE a = 1 AND c = 3,索引怎么走?如果是 WHERE b = 2 呢?”

    • A: (考察是否理解索引中断。a=1 走索引,c=3 变为普通条件(除非索引下推);只查 b 完全不走索引。)

  3. 关于 Join 优化

    • Q: “如果业务必须 Join 一个千万级的大表,且被驱动表对应的字段因为某些原因无法加索引,你会怎么处理?”

    • A: (考察非常规手段:代码层拆分查询、临时表、或者强行在业务低峰期做,甚至引入 ES/ClickHouse 解决分析需求,而不是死磕 MySQL)。


常用函数

1. 直击要点 (TL;DR)

(30秒口述/速记)

“面试中的 SQL 函数核心就在于‘转化’‘统计’

  1. 排名神器:必须掌握 窗口函数 (ROW_NUMBER, DENSE_RANK),解决 ‘分组取 Top N’ 问题,这是必考题。

  2. 逻辑转换:熟练使用 CASE WHEN 进行行转列统计。

  3. 时间处理:用 DATEDIFFDATE_SUB 处理用户留存连续登录问题。

  4. 字符串:用 GROUP_CONCAT 处理多行合并。

    掌握这四类,能覆盖 90% 的算法类 SQL 题。”


2. 详细拆解:核心函数与场景

第一梯队:窗口函数 (Window Functions) —— 面试必杀技

场景:找出每个部门薪资最高的 3 个人;找出连续登录 3 天的用户。

注意:MySQL 8.0+ 才支持,但面试默认可以用。如果面试官问 5.7 怎么实现,那是另一套(自连接)逻辑,但优先写窗口函数。

  • 核心函数

    • ROW_NUMBER() OVER (...)流水号。1, 2, 3, 4... (不管有没有并列,强制排序)。

    • RANK() OVER (...)奥运奖牌。1, 1, 3, 4... (有并列,名次会跳跃)。

      • DENSE_RANK() OVER (...)密集排名。1, 1, 2, 3... (有并列,名次跳跃)。

    窗口函数

    作用

    典型用法(PARTITION BY)

    SUM()/AVG()

    分区内求和 / 平均值

    按用户分区,计算用户所有订单的总金额

    MAX()/MIN()

    分区内最大值 / 最小值

    按商品分类分区,找分类内最贵 / 最便宜的商品

    LAG()/LEAD()

    分区内取前 / 后 N 行数据

    按用户分区,取用户上一次下单的金额

  • 强制比喻

    假设三个同学考分是:100分, 100分, 90分。

    • ROW_NUMBER 像是发卷子的顺序:你是第1个拿卷子的,你是第2个... (1, 2, 3)。

    • RANK 像是发奖金:前两个都是第一名,奖金分完了,第三个人只能算第三名 (1, 1, 3)。

    • DENSE_RANK 像是评优等生:前两个都是一等奖,第三个人依然是二等奖,不会变成三等奖 (1, 1, 2)。

      面试技巧:求 "Top N" 通常用 DENSE_RANK 最稳妥,防止并列导致漏人。

  • 代码套路

    SQL

     窗口函数 OVER (
         PARTITION BY 分区字段1, 分区字段2  -- 按哪些字段切分窗口(可选)
         ORDER BY 排序字段 [ASC/DESC]       -- 分区内的行排序(可选)
     )
     ​
     SELECT * FROM (
         SELECT *,
                DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) as ranking
         FROM employee
     ) t WHERE t.ranking <= 3;
 -- 1. PARTITION BY 写法(保留所有行)
 SELECT emp_id, dept, sales_num,
        SUM(sales_num) OVER (PARTITION BY dept) AS dept_total
 FROM sales;
 ​
 -- 2. GROUP BY 写法(合并行,丢失明细)
 SELECT dept, SUM(sales_num) AS dept_total
 FROM sales
 GROUP BY dept;

emp_id

dept

sales_num

dept_total_sales

1

销售一部

1000

3300

(1000+1500+800)

2

销售一部

1500

3300

3

销售一部

800

3300

4

销售二部

2000

5000

(2000+1200+1800)

5

销售二部

1200

5000

6

销售二部

1800

5000

dept

dept_total

销售一部

3300

销售二部

5000

第二梯队:逻辑与聚合 (Logic & Aggregation) —— 行转列神器

场景:统计每个班级的男生人数、女生人数(原本是两行,要变成一行的两列)。

  • 核心函数

    • CASE WHEN condition THEN result ELSE result END:SQL 里的 if-else。

    • IF(expr1, expr2, expr3):简易版三元运算符。

    • GROUP_CONCAT(col):将多行数据合并到一个格子里,用逗号分隔。

  • 强制比喻

    GROUP_CONCAT 就像打包行李

    原本散落在地上的衣服(多行数据),你用一个袋子把它们全部装起来(合并成一个字符串),中间用逗号隔开,一次性提走。

  • 代码套路(行转列)

     -- 统计各部门 'active' 和 'inactive' 的人数
     SELECT dept_id,
            SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_count,
            SUM(CASE WHEN status = 'inactive' THEN 1 ELSE 0 END) as inactive_count
     FROM users
     GROUP BY dept_id;
     组合作用
     SELECT 
         emp_id,
         dept,
         sales_num,
         -- 高销总额:分区内sales_num>1000的金额求和
         SUM(CASE WHEN sales_num > 1000 THEN sales_num ELSE 0 END) 
             OVER (PARTITION BY dept) AS dept_high_sales_total,
         -- 低销总额:分区内sales_num≤1000的金额求和
         SUM(CASE WHEN sales_num <= 1000 THEN sales_num ELSE 0 END) 
             OVER (PARTITION BY dept) AS dept_low_sales_total
     FROM sales;
     SELECT 
         emp_id,
         IF(sales_num > 1000, '高销', '低销') AS sales_tag
     FROM sales;
     -- 等价 CASE WHEN 写法
     CASE WHEN sales_num > 1000 THEN '高销' ELSE '低销' END AS sales_tag

第三梯队:日期与字符串 (Date & String) —— 留存分析

场景:计算次日留存率;查找邮箱后缀;格式化日期。

  • 核心函数

    • DATEDIFF(date1, date2):计算天数差。注意顺序,通常是 前-后 还是 后-前,面试时容易写反,建议记准 DATEDIFF('2023-01-02', '2023-01-01') = 1

    • DATE_ADD(date, INTERVAL 1 DAY) / DATE_SUB(...):日期的加减。

    • DATE_FORMAT(date, '%Y-%m-%d'):截取日期。

  • 面试常考逻辑

    判断连续两天登录

    DATEDIFF(a.login_date, b.login_date) = 1a.user_id = b.user_id


3. 深挖追问 (模拟压力面)

你熟练背出了上面的函数,面试官点了点头,在白板上写了一行字,开始刁难:

  1. 关于 Count 的陷阱

    • Q: “在代码里统计总行数,COUNT(*)COUNT(1)COUNT(字段名) 有什么区别?哪个性能最高?”

    • A: (必须回答COUNT(字段) 会忽略 NULL 值,而 COUNT(*) 包含 NULL。在 MySQL 5.6+ 之后,COUNT(*)COUNT(1) 效果一样,优化器会自动选择成本最小的辅助索引进行扫描,性能极高。不要迷信 COUNT(1) 更快。)

  2. 关于 NULL 处理

    • Q: “我在做 SUM(salary) 的时候,如果所有行都是 NULL,结果是 0 还是 NULL?怎么避免?”

    • A: (结果是 NULL。必须使用 IFNULL(SUM(salary), 0) 包裹,这在金融/电商算钱时是致命细节。)

  3. 窗口函数的替代方案

    • Q: “如果公司的数据库版本很老(MySQL 5.6),不支持窗口函数,你怎么求每个班级的第一名?”

    • A: (考察自连接变量法。最简单的思路:SELECT * FROM t1 WHERE salary = (SELECT MAX(salary) FROM t1 as t2 WHERE t2.class_id = t1.class_id)。这是经典的非相关子查询优化问题。)


事务

MySQL 的事务(Transaction)是数据库区别于文件系统的关键特性,它保证了一组数据库操作要么全部成功,要么全部失败。

这是后端面试中最核心、最高频的考点之一。要回答好这个问题,不能只背诵“ACID”,必须把它们对应的底层实现机制(日志系统和锁机制)讲清楚。

我们通常所说的 MySQL 事务,默认指的都是 InnoDB 存储引擎


一、 事务的四大特性 (ACID)

1. 原子性 (Atomicity)

  • 含义: 并不是说事务是物理上的“原子”,而是指不可分割。事务中的所有操作,要么全部完成,要么全部不完成。如果在执行过程中发生错误,会回滚(Rollback)到事务开始前的状态,就像什么都没发生过一样。

  • 口诀: 同生共死,要么全做,要么不做。

2. 一致性 (Consistency)

  • 含义: 事务执行前后,数据库的完整性约束没有被破坏。

  • 例子: A 给 B 转账 100 元。不管中间怎么操作,A 和 B 的账户余额总和在转账前后必须是一样的。不能 A 扣了钱,B 没收到,钱凭空消失了。

  • 地位: 一致性是事务的最终目标。原子性、隔离性、持久性都是为了服务于一致性。

3. 隔离性 (Isolation)

  • 含义: 多个事务并发执行时,一个事务的执行不应影响其他事务。

  • 例子: A 正在修改数据但还没提交,B 此时去读数据,不应该读到 A 修改了一半的脏数据。

4. 持久性 (Durability)

  • 含义: 事务一旦提交(Commit),它对数据库的改变就应该是永久的。接下来的数据库故障(断电、宕机)不应该导致数据丢失。


二、 实现原理 (底层机制)

这是拉开差距的关键。ACID 是由 MySQL 的不同组件来实现的:

特性

实现机制 (靠什么保证)

核心原理简述

原子性

undo log (回滚日志)

记录“反向操作”,用于回滚

持久性

redo log (重做日志)

记录“物理修改”,用于崩溃恢复

隔离性

锁 + MVCC (多版本并发控制)

解决并发读写冲突

一致性

以上三者 + 代码逻辑

最终目的

下面详细讲解这三个核心机制:

1. 原子性原理 —— undo log (后悔药)

  • 问题: 如果事务执行了一半,服务器宕机了,或者用户执行了 ROLLBACK,怎么恢复原样?

  • 原理:

    InnoDB 会在执行操作前,生成一条 undo log。它记录的是逻辑日志,即“反向操作”。

    • 如果你执行 INSERT,undo log 记一条 DELETE

    • 如果你执行 UPDATE 将 id=1 的值从 10 改为 20,undo log 记一条 UPDATE 将 20 改回 10。

  • 作用: 一旦事务需要回滚,数据库就依照 undo log 做逆向操作,撤销之前的修改。

2. 持久性原理 —— redo log (记账本)

  • 问题: 数据库的数据存在磁盘上。如果每次提交事务都直接把数据刷入磁盘(随机 I/O),性能会非常差。但如果只改内存,断电了数据就丢了。怎么办?

  • 原理: WAL 技术 (Write-Ahead Logging,日志先行)

    1. 当事务提交时,不直接修改磁盘上的数据文件,而是先把修改记录写到 redo log 文件中。

    2. 写 redo log 是顺序 I/O,速度极快。

    3. 只要 redo log 写入成功,事务就算提交成功。

    4. 系统空闲时,再把内存里的数据慢慢刷回磁盘数据文件。

  • 作用: 如果数据库宕机,重启后读取 redo log,把还没来得及刷盘的数据“重做”一遍,保证数据不丢失。

3. 隔离性原理 —— 锁 + MVCC (平行空间)

隔离性是为了解决并发问题。MySQL 提供了四种隔离级别(读未提交、读已提交、可重复读、串行化)。

  • 写-写冲突:使用 锁 (Lock)

    • InnoDB 支持行级锁(Row Lock),只锁住操作的那一行,并发性能高。

  • 读-写冲突:使用 MVCC (Multi-Version Concurrency Control,多版本并发控制)

    • 原理: 当一个事务在写数据时,另一个事务去读,不需要加锁等待

    • 实现:

      1. 隐藏字段: 每一行数据背后都有两个隐藏列:trx_id(最近修改的事务ID)和 roll_pointer(回滚指针,指向 undo log)。

      2. 版本链: 通过 roll_pointer 可以在 undo log 中找到这行数据的“旧版本”。

      3. ReadView (读视图): 读事务生成一个 ReadView,根据规则判断自己应该看到哪个版本的数据(是看最新的,还是看旧的)。

    • 效果: 实现了 “读不加锁,读写不冲突” ,极大提升了并发性能。

总结记忆表

ACID 特性

就像...

核心保障技术

原子性

Ctrl + Z (撤销)

undo log

持久性

写在记账本上 (防丢)

redo log

隔离性

平行宇宙 (互不干扰)

锁 & MVCC

一致性

最终结果

由原子性+持久性+隔离性共同保证

面试建议:

如果在面试中被问到,建议先快速说出四大特性,然后把重点放在 redo log(如何保证不丢数据) 和 MVCC(如何实现高并发读取) 的原理解析上,这才是体现深度的部分。


并发事务的问题和解决方式

为了保证隔离性,数据库需要处理并发带来的脏读、幻读等问题。不同的处理程度,就对应了不同的隔离级别。而在 MySQL InnoDB 中,这是通过 MVCC(多版本并发控制)锁(Next-Key Lock) 共同实现的。

我们把这个过程分为三个层次来拆解:“病症(并发问题)” -> “药方(隔离级别)” -> “药理(实现原理)”


一、 病症:并发事务带来的三大问题

当多个事务同时操作同一批数据时,如果缺乏隔离,会产生三种典型的“数据污染”现象。

1. 脏读 (Dirty Read) —— “读到了假数据”

  • 场景:事务 A 修改了数据(把余额从 100 改为 200),但还没提交。此时事务 B 读到了 200。结果事务 A 回滚了(余额变回 100)。

  • 后果:事务 B 拿着 200 去做后续业务,导致逻辑错误。B 读到的就是“脏数据”。

2. 不可重复读 (Non-repeatable Read) —— “前后读取不一致”

  • 场景:事务 A 先查询余额,是 100。然后事务 B 进来了,把余额改成 200 并提交了。事务 A 再次查询余额,发现变成 200 了。

  • 后果:在事务 A 的同一个事务里,两次读到的数据不一样。这违反了“事务内状态一致”的要求。

    • 注:重点在于 Update/Delete 操作导致的数据变动。

3. 幻读 (Phantom Read) —— “活见鬼了”

  • 场景:事务 A 查询“工资大于 5000 的人”,查到了 10 个人。此时事务 B 进来了,插入 (Insert) 了一个工资 6000 的新人并提交。事务 A 再次查询,发现查到了 11 个人。

  • 后果:就像产生了幻觉一样,莫名其妙多出来(或少)了一行数据。

    • 注:重点在于 Insert/Delete 操作导致的数据量变动。


二、 药方:四大隔离级别

为了解决上述问题,SQL 标准定义了四个隔离级别。级别越高,安全性越好,但并发性能越差(因为锁得越严)。

隔离级别

脏读

不可重复读

幻读

说明

读未提交 (Read Uncommitted)

裸奔。性能最好,但数据最不安全。基本没人用。

读已提交 (Read Committed, RC)

主流选择之一(Oracle/PG 默认)。只能读别人提交后的数据。解决脏读。

可重复读 (Repeatable Read, RR)

✅ (标准SQL) ❌ (InnoDB)

MySQL 默认级别。保证一个事务内看到的数据是一致的。

串行化 (Serializable)

最严。强制事务排队执行。完全无并发,极慢。

⚠️ 关键点(面试高频):

标准 SQL 规范中,RR(可重复读) 是无法解决幻读的。但是!MySQL 的 InnoDB 引擎通过特殊的手段(MVCC + 间隙锁),在 RR 级别下就把幻读问题基本解决了。


三、 药理:实现原理 (MVCC + 锁)

MySQL InnoDB 是如何实现这些隔离级别的?核心技术是 MVCC

1. 读已提交 (RC) & 可重复读 (RR) 的区别:MVCC 的 ReadView 生成时机

我们在上一节讲过 MVCC(多版本并发控制),它通过 ReadView (读视图) 来判断版本可见性。RC 和 RR 的核心区别就在于 ReadView 生成的时机不同

  • RC (读已提交)“每次查询都生成新的 ReadView”

    • 事务 A 第一次查,生成 ReadView 1,看到 100。

    • 事务 B 修改并提交为 200。

    • 事务 A 第二次查,重新生成 ReadView 2。因为 B 已经提交,ReadView 2 判定 200 是可见的。

    • 结果:看到了变化 -> 不可重复读。

  • RR (可重复读)“只在第一次查询生成 ReadView,后面复用”

    • 事务 A 第一次查,生成 ReadView 1,看到 100。

    • 事务 B 修改并提交为 200。

    • 事务 A 第二次查,复用 ReadView 1。根据规则,B 的提交是在我 ReadView 生成之后发生的,所以我当做没看见,依然读 undo log 里的旧版本 100。

    • 结果:前后一致 -> 可重复读。

2. InnoDB 如何在 RR 级别解决“幻读”?

这是一个非常深的问题。InnoDB 采用了 “快照读”“当前读” 两种策略结合。

情况 A:普通的 Select (快照读)

  • 原理:依靠 MVCC

  • 事务 A 开启后,生成 ReadView。即使事务 B 插入了新行并提交,由于新行的事务 ID 晚于 A 的 ReadView,A 是看不见这行新数据的。

  • 结论:MVCC 解决了普通查询的幻读。

情况 B:Select ... for update / Update / Delete (当前读)

  • 原理:依靠 Next-Key Lock (临键锁)

  • 如果是这就不仅是读快照了,而是要对数据加锁。MVCC 挡不住并发插入,这时候必须上锁。

  • Next-Key Lock = Record Lock (行锁) + Gap Lock (间隙锁)

    • 比如表中已有 id 为 1, 5, 10 的数据。

    • 事务 A 执行 select * from table where id > 5 for update。(for update:这是关键 —— 给查询结果集中的所有行加排他锁(X 锁),且触发 InnoDB 的「当前读」(读取最新版本数据,而非快照读))

    • InnoDB 不仅锁住 id=10 这行数据,还会锁住 (5, 10](10, +∞) 这两个间隙

    • 此时事务 B 想插入 id=8?被阻塞! 想插入 id=12?被阻塞!

  • 结论:通过锁住“空隙”,不让别人插入新数据,从而物理上根除了幻读。

总结

  1. 并发问题:脏读(读未提交)、不可重复读(Update变了)、幻读(Insert多了)。

  2. 隔离级别

    • RC:解决脏读。本质是每次查询都生成新 ReadView

    • RR(MySQL默认):解决不可重复读,并在很大程度上解决幻读。本质是复用同一个 ReadView

  3. MySQL 特技:在 RR 级别下,通过 MVCC 解决普通查询的幻读,通过 Next-Key Lock (间隙锁) 解决加锁修改时的幻读。


MVCC

MVCC(Multi-Version Concurrency Control,多版本并发控制)是 MySQL InnoDB 存储引擎实现隔离级别(尤其是 RCRR)的核心基石。

它的核心思想是:通过保留数据的历史版本,实现“读写不冲突”。写操作更新数据时,不直接覆盖旧数据,而是生成一个新版本;读操作则根据一定的规则,找回那个“属于它的版本”。


一、 MVCC 的三大支柱

要实现“跨时空”取数据,MVCC 依靠三个核心组件:隐藏字段Undo Log(回滚日志)ReadView(读视图)

1. 每行数据的隐藏字段

在 InnoDB 中,每一行记录除了你定义的字段外,还有三个极其重要的隐藏列:

  • DB_TRX_ID (6字节):最后一次修改本行记录的事务 ID

  • DB_ROLL_PTR (7字节)回滚指针。它指向这条记录在上一个版本的数据(存在 Undo Log 里)。

  • DB_ROW_ID (6字节):如果表没有主键,InnoDB 会自动生成这个隐藏主键。

2. Undo Log 版本链

当你修改一条数据时,InnoDB 会把旧数据拷贝到 Undo Log 中。

通过隐藏的 DB_ROLL_PTR 指针,所有的历史版本被串联成一条版本链。链表的头部是最新的数据,越往后越旧。

3. ReadView (读视图)

这是 MVCC 判断“哪个版本对我可见”的依据。ReadView 就像是一张快照,记录了当前系统中有哪些事务正在运行。

ReadView 包含 4 个关键信息:

  1. m_ids:生成 ReadView 时,系统中所有活跃的(未提交的) 事务 ID 列表。

  2. min_trx_idm_ids 中最小的值。

  3. max_trx_id:系统即将分配给下一个事务的 ID 值(即目前最大事务 ID + 1)。

  4. creator_trx_id:创建这个 ReadView 的事务 ID。


二、 核心算法:可见性判断规则

当事务执行查询(快照读)时,它会拿着当前记录的 trx_id 去跟自己的 ReadView 规则做对比。判断逻辑如下:

  1. trx_id == creator_trx_id

    • 这个版本是我自己改的,可见

  2. trx_id < min_trx_id

    • 生成 ReadView 时,这个事务已经提交了,可见

  3. trx_id >= max_trx_id

    • 这个版本是在我生成 ReadView 之后才开启的事务改的,不可见

  4. min_trx_id <= trx_id < max_trx_id

    • 如果在 m_ids 列表中:说明生成 ReadView 时这个事务还在跑,没提交,不可见

    • 如果不在 m_ids 列表中:说明生成 ReadView 时这个事务已经提交了,可见


三、 实战案例:事务如何确定版本?

1. 初始环境

  • 表结构user (id, name)

  • 初始数据id=1, name='Original', trx_id=10(已提交)。

2. 事务时间轴

我们有三个并发事务:

  • 事务 A (trx_id=100):我是观察者(只读)。

  • 事务 B (trx_id=200):我是捣乱者 1 号(修改并提交)。

  • 事务 C (trx_id=300):我是捣乱者 2 号(修改但在整活,未提交)。

执行步骤:

  1. T1时刻:事务 A、B、C 同时开启(BEGIN)。

  2. T2时刻事务 A 执行第一次 SELECT

    • 关键点:此时生成 ReadView!

    • m_ids (活跃列表)[100, 200, 300](意思说:拍照这会儿,咱们仨都活着,都没提交)。

    • min_trx_id:100。

    • max_trx_id:301(下一个要分配的 ID)。

  3. T3时刻事务 Bname 改为 'Bob',并执行 COMMIT

    • 状态:B 已经完事了,数据变成了 'Bob'trx_id=200

  4. T4时刻事务 Cname 改为 'Charlie'未提交

    • 状态:C 持有行锁。数据变成了 'Charlie'trx_id=300

  5. T5时刻事务 A 执行第二次 SELECT

    • 问题:A 读到什么?

3. 版本链判定过程(模拟 CPU 运行)

当事务 A 在 T5 时刻去读数据时,物理磁盘(Buffer Pool)上的数据是这样的:

  • 当前最新记录name='Charlie', trx_id=300, roll_ptr -> Bob

  • Undo Log 历史

    • name='Bob', trx_id=200, roll_ptr -> Original

    • name='Original', trx_id=10, roll_ptr -> null

事务 A 拿着 T2 时刻拍的 ReadView (m_ids=[100, 200, 300]) 开始遍历:

  1. 第一轮:看最新记录 'Charlie' (trx_id=300)

    • 判断:300 在 m_ids 列表里吗?

    • 答案:在!

    • 含义:说明生成快照时,300 还是活跃的(没提交)。虽然你现在可能提交了(或者没提交),但我不管,根据 RR 规则,我不认

    • 动作:不可见!顺着 roll_ptr 找上一版本。

  2. 第二轮:看历史记录 'Bob' (trx_id=200)

    • 判断:200 在 m_ids 列表里吗?

    • 答案在!(这是最大的坑点!)

    • 含义:虽然事务 B 在 T3 时刻已经 Commit 了,但是!事务 A 的 ReadView 是在 T2 生成的。在 T2 那一瞬间,B 还是活跃的。

    • 强制比喻

      这就像拍毕业照。 哪怕拍完照下一秒钟,B 同学就去染了黄头发(变成 Bob)甚至毕业离校了(Commit)。 你拿出 T2 时刻拍的那张照片,B 同学永远是黑头发。照片是不会变的。

    • 动作:不可见!继续找上一版本。

  3. 第三轮:看老记录 'Original' (trx_id=10)

    • 判断:10 在 m_ids 列表里吗?不在。10 < min_trx_id (100) 吗?是的。

    • 含义:这是很久以前就已经落袋为安的数据。

    • 动作可见!

最终结论:事务 A 读到的结果是 'Original'

4.追问

这个案例讲完,面试官通常会非常满意,然后立刻抛出 变种问题 试图把你绕晕:

追问 1:“如果我把隔离级别改成 RC(读已提交),结果会有什么变化?”

  • 分析:RC 级别的特点是 “每次 SELECT 都会重新生成 ReadView”

  • 推演

    • 事务 A 在 T5 时刻再次 Select,会重新生成一张 ReadView。

    • 此时 B 已经提交了,C 还没提交。

    • 新的 m_ids[100, 300](B 已经不在活跃列表里了)。

  • 判定

    • 看 'Charlie' (300):在 m_ids 里 -> 不可见。

    • 看 'Bob' (200):不在 m_ids 里,且 200 < max_trx_id -> 可见!

  • 结果:在 RC 级别下,A 会读到 'Bob'

追问 2:“如果事务 A 在 T5 时刻不是 SELECT,而是执行 UPDATE user SET name = 'Dave',会发生什么?”

  • 分析:这是考 当前读 (Current Read)。UPDATE 语句不走 ReadView,必须读最新的已提交数据。

  • 推演

    • A 想要修改,必须申请锁。

    • 此时 'Charlie' (300) 这行数据正被 事务 C 的行锁(X锁)锁住。

  • 结果:事务 A 会被 阻塞 (Block),直到事务 C 提交或回滚。

  • 后续:假设 C 提交了。A 拿到锁,会在 'Charlie' 的基础上修改,变成 'Dave'。此时 A 再 Select,就会看到 'Dave'(因为是自己改的,符合可见性规则)。


四、 总结:MVCC 考察的精髓

面试中考察 MVCC,通常会顺着这个逻辑问:

  • 为什么要设计版本链?(为了回溯历史数据,实现 Undo)。

  • ReadView 的作用是什么?(确定可见性边界)。

  • RC 和 RR 的本质区别是什么?

    • RC:每次 SELECT 都重新生成 ReadView,所以能读到别人新提交的数据。

    • RR:事务中第一次 SELECT 生成 ReadView,后续复用,所以实现了可重复读。

一句话总结:

MVCC 通过 Undo Log 版本链 存储了过去,通过 ReadView 定义了当前,从而让每个事务都能在属于自己的“平行时空”里进行互不干扰的读取。


MySQL 约束 (Constraints)

1. 直击要点 (TL;DR)

(面试开场 30 秒口述范本)

“关于 MySQL 约束,我认为它核心是为了保证数据的完整性一致性

  1. 基础层面:最常用的包括 主键约束(保证唯一且非空)、唯一约束(保证业务字段不重复)、非空约束(避免 NULL 值陷阱)。

  2. 核心差异:在互联网大厂架构中,我们严格禁止使用外键约束 (Foreign Key)

  3. 性能视角:约束往往伴随着索引的建立(如主键和唯一键),所以添加约束时必须考虑写操作的性能损耗。

我的原则是:数据准确性靠代码逻辑保证,数据库只负责存取,尽量‘轻装上阵’。


2. 详细拆解

2.1 基础四大金刚

  • Primary Key (主键)

    • 定义:唯一标识一条记录,不能为 NULL。

    • 底层:在 InnoDB 中,主键是聚簇索引,数据和索引存在一起。

    • 强制比喻

      主键就是身份证号

      每个人必须有,而且全中国唯一。你丢了身份证(没有主键),在系统里就是个“黑户”,查都查不到。

  • Unique (唯一)

    • 定义:字段值不能重复,但允许为 NULL(这是个考点)。

    • 底层:辅助索引(二级索引)。

    • 强制比喻

      唯一约束就是手机号

      你可以没有手机号(为 NULL),但只要你有,就不能跟别人重复。

  • Not Null (非空)

    • 定义:字段内容不能为空。

    • 大厂习惯:通常建议所有字段都设为 Not Null 并给一个 Default 值(如 0 或空字符串)。因为 NULL 在索引统计和计算时会有很多坑。

  • Foreign Key (外键)

    • 定义:强制维护两个表之间的关系。

    • 现状阿里 Java 开发手册强制禁止使用外键

2.2 核心原理:为什么大厂禁用外键?

这是本环节的重中之重

  • 性能杀手

    • 原理:每次插入或更新子表数据时,数据库必须去父表查一下“这个 ID 存不存在”。这需要额外的 加锁 (Locking)IO 开销

    • 强制比喻

      外键就像是“妈宝男”

      你(子表)每次想买个东西(Insert/Update),都必须先打电话给妈妈(父表)确认一下:“妈,我能买吗?”。

      也就是每次操作都要多一次网络通话和确认。如果并发高了,电话线这就占满了,系统直接堵死。

  • 死锁风险

    • 在高并发下,父表和子表互相持有锁,容易导致 Deadlock。

  • 分库分表困难

    • 一旦业务做大了,表 A 在数据库 1,表 B 在数据库 2。物理层面的外键约束直接失效,根本没法做。

2.3 亮点/加分项:唯一索引的“Change Buffer”优化

面试官问:“唯一约束和普通索引在性能上有什么区别?”

  • 普通索引:利用 Change Buffer 优化写操作。如果数据页不在内存中,先记在缓存里,稍后合并。

  • 唯一约束不能利用 Change Buffer。

  • 原理:因为要保证“唯一”,数据写入时,MySQL 必须先把数据页读到内存里判断一下“有没有重复”。这一“读”,就产生了 IO,所以唯一索引的写入性能比普通索引差


3. 深挖追问 (模拟压力面)

面试官看你对外键的观点很明确,决定考考你的细节:

  1. 关于 NULL 与 唯一索引

    • Q: “我有一个字段设了 Unique 约束。我插入第一行是 NULL,成功了。我插入第二行也是 NULL,会报错吗?”

    • A: (关键点不会报错。在 MySQL 中,NULLNULL 是不相等的(NULL != NULL)。所以唯一约束无法拦截多个 NULL 值。这在业务逻辑中是一个常见的 Bug 源头。)

  2. 关于业务逻辑层的约束

    • Q: “既然不让用外键,那怎么保证数据一致性?比如删了班级,学生表里的班级ID不就成脏数据了吗?”

    • A: (依靠 事务 (Transaction)代码逻辑。在 Service 层,删除班级前,先 check 一下有没有学生,或者在一个事务里级联删除。虽然代码写得麻烦点,但把压力从数据库转移到了应用层,应用层是可以无限横向扩展的,数据库不行。)

  3. 关于主键选择

    • Q: “主键是用 自增 ID (Auto Increment) 好,还是用 UUID 好?为什么?”

    • A: (必须回答自增 ID 更好。

      1. 空间:Int/BigInt 占用空间小,索引更紧凑。

      2. 性能:B+ 树要求索引有序。自增 ID 是追加写入,不会导致页分裂 (Page Split)

      3. UUID:太长且无序,插入时会导致频繁的页分裂和数据移动,严重降低写性能。)


InnoDB存储引擎

理解 InnoDB 的底层,实际上就是要理解 “数据库是如何在有限的内存和缓慢的磁盘之间做平衡的”

一、 InnoDB 如何存取和修改数据?(宏观视角)

首先,我们要打破一个误区:MySQL 并不是一行一行地从磁盘读数据的。那样太慢了(磁盘 I/O 是昂贵的)。

1. 基本单位:页 (Page)

InnoDB 将磁盘数据切分成一个个固定大小的块,叫做 “页 (Page)”,默认大小是 16KB

  • 无论是读还是写,硬盘和内存交互的最小单位都是“页”。

  • 这就好比你读《哈利波特》,你不会只把“第 10 页第 5 行”的那句话剪下来读,而是把整页纸(甚至整本书) 拿来看。

2. 修改数据的流程 (Buffer Pool)

InnoDB 有一个非常重要的内存区域,叫 Buffer Pool (缓冲池)

  • 读数据

    1. 先看 Buffer Pool 里有没有这一页?

    2. 有:直接返回(纯内存操作,极快)。

    3. 没有:从磁盘加载这一页到 Buffer Pool,然后返回。

  • 写数据 (修改)

    1. 只修改内存:先把 Buffer Pool 里对应的页改了。此时内存里的页是新的,磁盘上的是旧的,这叫 “脏页 (Dirty Page)”

    2. 记日志 (Redo Log):为了防止断电数据丢失,把修改记录写到 Redo Log(顺序写磁盘,很快)。只要 Redo Log 写完了,就算事务提交成功了。

    3. 刷盘 (Flush):后台线程会在空闲时,慢慢把内存里的“脏页”刷回到磁盘数据文件中。

总结: InnoDB 的存取修改主要在内存 (Buffer Pool) 中进行,配合 Redo Log 保证安全,配合 Page 机制减少磁盘 I/O 次数。


二、 核心拷问:为什么使用 B+ 树?

要回答好这个问题,我们需要对比 B 树 (B-Tree)B+ 树 (B+Tree)

1. 结构差异

  • B 树

    • 每个节点(包括根节点、中间节点、叶子节点)都存 索引 + 真实数据(Row)

  • B+ 树

    • 非叶子节点(内节点):只存 索引(Key) + 指针,不存数据。

    • 叶子节点:存 索引 + 真实数据。且所有叶子节点之间通过 双向链表 串联起来。

2. 为什么 B+ 树更适合数据库?(三大理由)

理由一:树更矮,磁盘 I/O 更少(最核心原因)

  • 原理:数据库的瓶颈在磁盘 I/O。树越高,查一次数据需要读取磁盘的次数就越多。

  • 分析

    • InnoDB 的一页只有 16KB。

    • 在 B 树中:因为节点里塞了真实数据(比如一行数据可能有 1KB),那么一页只能存 16 个节点。如果数据量大,树就会变得很高(比如 10 层),查一次要读 10 次磁盘,慢死。

    • 在 B+ 树中:非叶子节点只存 ID 和指针(很小,比如 ID 8字节 + 指针 6字节 = 14字节)。一页 16KB 可以存 1000 多个 索引指针!

    • 结果:这也是 B+ 树“矮胖”的原因。通常 3 层 的 B+ 树就能存 2000万 行左右的数据(1000 \times 1000 \times \text{叶子节点容量})。也就是说,查 2000 万数据只需要 3 次 I/O(甚至根节点常驻内存,只需要 2 次)。

理由二:范围查询更强

  • 场景SELECT * FROM user WHERE id > 100

  • B 树:找到 id=100 后,因为数据分散在树的各个层级,必须通过“中序遍历”在树上跳来跳去,非常痛苦。

  • B+ 树:数据都在叶子节点,且叶子节点有双向链表连接。

    • 做法:先找到 id=100 的叶子节点,然后顺着链表往后一拉,所有数据都出来了。这是数据库扫描最常用的模式。

理由三:查询性能稳定

  • B 树:有时候在根节点就找到了(极快),有时候在叶子节点才找到(慢)。速度忽快忽慢。

  • B+ 树:无论查谁,都必须走到最后一步(叶子节点)。每一次查询的耗时是稳定的。


三、 其他高频面试补充点

除了 B+ 树,面试官还喜欢问这几个和 InnoDB 存储相关的概念:

1. 聚簇索引 vs 非聚簇索引 (Clustered vs Non-Clustered Index)

这是 InnoDB 和 MyISAM 的最大区别之一。

  • 聚簇索引 (Clustered Index)

    • 定义索引结构和数据文件是同一个文件

    • 实现:InnoDB 的 主键索引 就是聚簇索引。它的叶子节点里,直接存了完整的整行数据

    • 优势:根据主键查数据特别快,不用回表。

  • 非聚簇索引 (Secondary Index / 辅助索引)

    • 定义索引和数据分离

    • 实现:也就是我们要查询的 name, age 这种普通索引。它的叶子节点里存的是 索引列的值 + 主键 ID

    • 劣势回表 (Look up)。如果你查 SELECT * FROM user WHERE name='Alice',InnoDB 会先在 name 索引树找到 id=5,然后再拿着 id=5 去主键索引树里找完整的行数据。这叫“回表”。

    • 优化索引覆盖 (Covering Index)。如果只查 SELECT id, name,因为 name 索引树里已经包含了 id 和 name,就不需要回表了。

2. 页分裂与页合并 (Page Split & Merge)

这是为什么建议主键最好是自增 ID 的原因。

  • 顺序插入 (自增 ID):数据像俄罗斯方块一样,填满一页 16KB,就开辟新的一页继续填。紧凑、高效。

  • 随机插入 (UUID)

    • 如果一页已经满了(比如存了 ID 10, 20, 30),你突然要插入一个 ID 15。

    • 为了保持 B+ 树的有序性,InnoDB 不得不把这一页从中间撕开,把部分数据搬到新页去,这就是页分裂

    • 后果:不仅耗费性能,还会导致页很多空洞(磁盘碎片),空间利用率低。

3. 自适应哈希索引 (Adaptive Hash Index)

  • 虽然 InnoDB 用的是 B+ 树,但它内部有一个监控机制。

  • 如果它发现某些热点数据被频繁访问(比如 id=1 每秒被查 1000 次),它会自动在内存中给这些数据建立一个 哈希索引 (Hash Index)

  • 效果:下次再查 id=1,直接通过哈希定位(O(1) 复杂度),跳过 B+ 树的层层遍历。这是 InnoDB 的一种自优化机制。

总结

  1. 存取:以 页 (16KB) 为单位,利用 Buffer Pool 缓存热点数据,利用 Redo Log 保证持久性。

  2. 数据结构B+ 树

    • 内节点只存 key -> 增加扇出 -> 降低树高 -> 减少磁盘 I/O

    • 叶子节点连成链表 -> 支持范围查询

  3. 索引

    • 聚簇索引(主键):叶子节点 = 数据。

    • 辅助索引:叶子节点 = 主键 ID(需要回表)。

  4. 建议:主键尽量用自增 ID,避免页分裂。


日志

一、 三大日志核心脑图

1. Redo Log(重做日志)—— 保证“持久性”

  • 所属层级:InnoDB 引擎层特有。

  • 记录内容物理日志。记录的是“在某个数据页上做了什么修改”(例如:在 10 号表空间 5 号页偏移量 100 处改成了值 2)。

  • 写入方式循环写。空间固定,write pos 追赶 checkpoint

  • 核心作用Crash-Safe(崩溃恢复)。即使数据库宕机,也能根据日志把还没刷盘的数据找回来。

  • 工作机制:WAL(Write-Ahead Logging),先写日志,再改内存,最后异步刷盘。

2. Undo Log(回滚日志)—— 保证“原子性”与“隔离性”

  • 所属层级:InnoDB 引擎层特有。

  • 记录内容逻辑日志。记录的是相反的操作(例如:执行了 Insert,它记一条 Delete;执行了 Update,它记一条旧值的 Update)。

  • 核心作用

    1. 事务回滚:当事务报错或手动 Rollback 时,靠它恢复。

    2. MVCC(多版本并发控制):通过版本链提供非锁定读。

  • 存储位置:专门的 Undo 表空间(内存中在 Buffer Pool 的 Undo 页面)。

3. Binlog(归档日志)—— 用于“复制”与“备份”

  • 所属层级:MySQL Server 层,所有存储引擎共用。

  • 记录内容逻辑日志。记录的是 SQL 语句或行数据的变动。

  • 写入方式追加写。文件写满会切到下一个,不会覆盖旧日志。

  • 核心作用

    1. 主从复制:Master 把 Binlog 发给 Slave,Slave 重做实现同步。

    2. 数据恢复:通过 mysqlbinlog 工具恢复到指定时间点。


二、 深度对比:Redo Log vs Binlog

这是面试官最喜欢问的“陷阱题”。

维度

Redo Log

Binlog

实现层

InnoDB 存储引擎层

MySQL Server 层(全局)

日志格式

物理日志(页的物理改动)

逻辑日志(SQL 或行数据)

写入模式

循环写(空间有限,会覆盖)

追加写(空间不限,保留历史)

持久化目标

奔溃恢复(Crash-Safe),保证不丢数据

主从同步、数据备份

事务关联

事务执行过程中不断写入

事务提交时一次性写入


三、 两阶段提交(Two-Phase Commit, 2PC)

面试必考点: Redo Log 和 Binlog 是两个独立的系统,如何保证它们的一致性?如果 Redo 写了但 Binlog 没写,主从就不一致了。

MySQL 采用了 两阶段提交 机制:

  1. Prepare 阶段:InnoDB 将修改写入 Redo Log,并将事务状态设为 prepare 状态。

  2. 写 Binlog 阶段:MySQL Server 将对应的 SQL/行改动写入 Binlog。

  3. Commit 阶段:InnoDB 将 Redo Log 里的状态改为 commit 状态。

崩溃恢复时的逻辑:

  • 如果 Redo Log 处于 commit:直接提交。

  • 如果 Redo Log 处于 prepare:去查 Binlog。

    • Binlog 完整:提交。

    • Binlog 不完整:回滚(用 Undo Log)。


四、 面试高频追问

Q1:为什么有了 Binlog 还需要 Redo Log?

A:Binlog 虽然有数据,但它是“逻辑”的,且不具备 Crash-Safe 能力。Binlog 记录的是“做了什么”,但它不知道磁盘上对应的物理页到底刷盘了没有。Redo Log 记录的是物理页状态,它能精确知道哪些页需要重做。

Q2:为什么不能只用 Redo Log 删掉 Binlog?

A

  1. 生态问题:MySQL 的主从复制生态完全依赖 Binlog。

  2. 功能问题:Redo Log 是循环写的,没有历史全量记录。无法做“按时间点恢复(Point-in-time Recovery)”。

Q3:Redo Log 刷盘(fsync)的时机?

A:由参数 innodb_flush_log_at_trx_commit 控制:

  • 0:每秒刷一次(性能高,崩了丢 1 秒数据)。

  • 1:每次事务提交都刷(最安全,性能稍低,默认值)。

  • 2:写到系统缓存,由 OS 决定刷盘。


锁机制

1. 直击要点 (TL;DR)

(面试开场 30 秒口述范本)

“关于 MySQL 的锁,我认为应该从粒度算法两个维度来看。

  1. 从粒度上:分为 全局锁(用于全库备份)、表级锁(如 MyISAM 的读写锁和 MDL 元数据锁)和 行级锁(InnoDB 独有)。

  2. 核心重点:InnoDB 的行锁是基于索引实现的。如果 SQL 语句没走索引,行锁会升级为表锁。

  3. 算法细节:在 RR 隔离级别下,InnoDB 使用 Next-Key Lock(临键锁)来解决幻读问题。它由 Record Lock(锁数据)和 Gap Lock(锁间隙)组成。

理解锁的关键在于:锁是为了解决并发冲突,但锁用得不好(如死锁、锁升级)是系统性能的头号杀手。


2. 详细拆解

2.1 表锁 vs 行锁 (Table Lock vs Row Lock)

这是存储引擎最本质的区别。

  • 表锁 (MyISAM)

    • 特点:开销小,加锁快;不会死锁;但在高并发下,写锁会阻塞所有读写,并发度极低。

    • 强制比喻

      表锁就像“包场”

      你去网吧上网(操作一张表),直接把网吧的大门锁上。不管你只是上一台电脑,还是用所有电脑,其他人(别的线程)都进不来,只能在门口排队。安全但效率极低。

  • 行锁 (InnoDB)

    • 特点:开销大,加锁慢;会出现死锁;但锁定粒度最小,并发度最高。

    • 强制比喻

      行锁就像“占座”

      同样去网吧,你只把你自己坐的那台电脑(一行数据)锁住,别人不能碰你的电脑,但可以去玩旁边的电脑。大家互不干扰。

2.2 意向锁 (Intention Lock - IS/IX)

这是一个高频概念题,很多人不懂为什么需要它。

  • 问题场景:事务 A 锁住了表里的一行(行锁)。事务 B 想给整张表加表锁。事务 B 怎么知道表里有没有行锁?难道要一行一行去查?太慢了!

  • 解决方案:事务 A 在申请行锁前,必须先在“表”的大门口贴一张条子:“里面有人在干活”。这张条子就是意向锁

  • 强制比喻

    意向锁就是厕所大门口的“有人”指示灯

    你(事务 A)进隔间(行)上厕所,先把大门口的灯(意向锁)按亮。

    清洁工(事务 B)想来打扫整个厕所(加表锁),一看大门口灯亮着,就知道里面有人,直接在门口等,不用挨个去推隔间的门。效率极高。

2.3 InnoDB 的三大行锁算法 (重点!)

在 RR (Repeatable Read) 级别下,为了解决幻读,InnoDB 引入了极其复杂的锁机制。

  1. Record Lock (记录锁)

    • 定义:仅仅锁住索引记录本身。

    • 场景SELECT * FROM t WHERE id = 1 FOR UPDATE; (id 是主键)。

    • 比喻:锁住具体的“某个人”

  2. Gap Lock (间隙锁)

    • 定义:锁住索引记录之间的间隙(开区间),确保索引记录之间无法插入新记录。这是解决幻读的核心。

    • 场景:表中有 id = 1, 5。SELECT * FROM t WHERE id = 3 FOR UPDATE;

    • 结果:MySQL 会锁住 (1, 5) 这个区间。你此时想 INSERT id = 24 会被阻塞。

    • 比喻

      间隙锁就是“拉警戒线”

      警察(MySQL)在 1 号楼和 5 号楼之间拉起了警戒线。虽然 3 号楼现在不存在,但为了防止犯罪分子(幻读)偷偷在这个空地上盖一栋楼(Insert),警察禁止任何人进入这个区域。

  3. Next-Key Lock (临键锁)

    • 定义:Record Lock + Gap Lock。锁住数据本身,同时锁住它前面的间隙(左开右闭区间)。

    • 默认:InnoDB 在 RR 级别下,默认使用 Next-Key Lock。


3. 深挖追问 (模拟压力面)

面试官对你的比喻很满意,但决定考考你的实战排查能力。

  1. 关于锁升级(锁全表)

    • Q: “我执行 UPDATE user SET age = 20 WHERE name = 'ZhangSan';(name 字段没有索引),会发生什么?为什么?”

    • A: (灾难场景。MySQL 只能给索引加锁。如果 WHERE 条件没走索引,InnoDB 无法定位到具体行,就会扫描全表。虽然逻辑上是行锁,但实际上会把表里所有行都锁住,效果等同于锁表。线上服务会瞬间挂起。)

  2. 关于死锁 (Deadlock)

    • Q: “如果出现死锁,你会怎么排查?MySQL 是怎么处理死锁的?”

    • A:

      • 处理:MySQL 有死锁检测机制(innodb_deadlock_detect),默认开启。发现死锁后,会选择一个持有锁最少(回滚成本最低)的事务进行回滚。

      • 排查

        1. 看日志:SHOW ENGINE INNODB STATUS,里面有 LATEST DETECTED DEADLOCK 部分,详细记录了死锁时的 SQL 和锁持有情况。

        2. 代码层面:检查是不是两个事务获取锁的顺序不一致(A 先锁 1 再锁 2;B 先锁 2 再锁 1)。

  3. 关于 Gap Lock 的副作用

    • Q: “间隙锁听起来很好,它有什么缺点吗?”

    • A: (并发性能下降。间隙锁容易导致死锁。比如两个事务都持有了 (1, 5) 的间隙锁——这是允许的,因为间隙锁之间兼容——但它们接着都想在这个间隙里执行 INSERT,就会互相等待对方释放间隙锁,形成死锁。)


SQL优化

1. 直击要点 (TL;DR)

(面试开场 30 秒口述范本 —— 展示方法论)

“关于 SQL 优化,我通常遵循 ‘三步走’ 策略:

  1. 发现问题:通过开启 慢查询日志 (Slow Query Log),定位执行时间超过阈值(如 1秒)的 SQL。

  2. 分析根因:使用 EXPLAIN 命令分析执行计划。重点关注 type(有没有走全表扫描)、key(有没有用到索引)、rows(扫描行数)和 Extra(有没有 Using filesortUsing temporary)。

  3. 对症下药

    • 索引层面:遵循最左前缀原则,利用覆盖索引减少回表。

    • 语法层面:避免在索引列上做运算,优化深度分页,用 Union 代替 OR。

    • 架构层面:如果单表数据量过大(超 2000 万),考虑分库分表或读写分离。”


2. 详细拆解

2.1 核心武器:EXPLAIN 执行计划

面试官会问:“拿到慢 SQL 你第一步做什么?” —— 必须回答 EXPLAIN

  • 基础概念:在 SQL 前加 EXPLAIN,MySQL 不会执行语句,而是返回优化器的执行计划。

  • 核心字段解读(必背)

    • id:执行顺序(id 越大越先执行)。

    • type(访问类型,性能从好到坏)

      • system/const:主键/唯一索引精确查找(一次命中)。

      • ref:普通索引查找。

      • range:索引范围查找(Between, >, <)。(及格线)

      • index:全索引扫描(扫描整个索引树)。

      • ALL全表扫描(扫描磁盘上的所有数据行)。(必须优化)

    • key:实际使用的索引。

    • Extra(额外信息,重点看坏味道)

      • Using filesort文件排序。无法利用索引顺序,需在内存/磁盘排序(大忌)。

      • Using temporary临时表。常见于 Group By,性能杀手。

      • Using index覆盖索引。不需要回表,性能最好。

2.2 索引优化:最左前缀与失效场景

这是面试中“大家来找茬”环节的高频题。

  • 核心原理:最左前缀原则 (Leftmost Prefix)

    • 定义:对于联合索引 (a, b, c),查询必须从最左边的 a 开始匹配,不能跳过。

    • 强制比喻

      联合索引就像“电话簿”

      电话簿是按 “姓 -> 名” 排序的。

      • 查“张三”:先找“张”,再找“三”。(走索引)

      • 查“三”:直接找名字叫“三”的人?你得把整本电话簿从头翻到尾。(索引失效)

      • 查“姓张的”:可以。(走索引)

      • 查“名字以‘三’结尾的”:不行。(索引失效)

  • 常见失效场景(避坑指南)

    1. 不满足最左前缀:跳过第一列,直接查第二列。

    2. 索引列上做计算/函数WHERE YEAR(create_time) = 2023

      • 比喻“整容识别”。索引存的是原始指纹,你把手指头烫伤了(做函数运算)再去按指纹锁,系统当然不认识你。

    3. 类型隐式转换:字符串字段不加单引号。WHERE phone = 1380000(phone 是 varchar)。

    4. 范围查询右边的列失效WHERE a=1 AND b>2 AND c=3ab 走索引,c 不走。因为 b 是范围,后面的 c 在索引树里就是无序的了。

    5. 用 OR 连接条件:除非 OR 两边的字段都有索引,否则索引全部失效。

2.3 语法优化:深度分页与 Join

  • 深度分页 (Deep Pagination)

    • 问题LIMIT 1000000, 10

    • 优化延迟关联 (Late Row Lookup)

    • SQL

       -- 优化前
       SELECT * FROM t LIMIT 1000000, 10;
       -- 优化后
       SELECT * FROM t JOIN (SELECT id FROM t LIMIT 1000000, 10) a ON t.id = a.id;
    • 原理:先在索引树里快速把 ID 找出来(覆盖索引),再回表拿数据。

  • Join 优化

    • 原则小表驱动大表

    • 要求:被驱动表的连接字段必须有索引(为了走 NLJ 算法,避免 BNL 算法)。


3. 深挖追问 (模拟压力面)

面试官听完你的体系,决定出几个刁钻的场景题:

  1. 关于 SELECT \* 的本质危害

    • Q: “除了网络传输慢,SELECT * 在数据库内核层面有什么最大的坏处?”

    • A: (关键点:阻止了覆盖索引。如果只是 SELECT id, name 且建立了 (name) 索引,MySQL 可以直接在索引树上拿结果,一次 I/O 都不用读数据页。写了 SELECT * 就必须回表,随机 I/O 次数暴增。)

  2. 关于 OR 的优化

    • Q: “业务必须查 WHERE a=1 OR b=2,a 有索引,b 也有索引。MySQL 会怎么执行?怎么优化?”

    • A: (MySQL 5.0+ 可能会用 Index Merge(索引合并),即分别查两个索引然后取并集。但更推荐 UNION ALL

      SELECT * FROM t WHERE a=1 UNION ALL SELECT * FROM t WHERE b=2

      因为 UNION 更加明确,且容易分别利用索引。)

  3. 关于 inexists

    • Q:SELECT * FROM A WHERE id IN (SELECT id FROM B)EXISTS 怎么选?”

    • A: (原则:小表驱动大表

      • IN:先执行子查询 B。适合 B 表小 的情况。

      • EXISTS:先执行主查询 A,再把 A 的数据扔进去匹配。适合 A 表小 的情况。

      • 比喻IN 是拿着名单(B)去找人;EXISTS 是把人(A)一个个拉过来查名单。)


分库分表

1. 直击要点 (TL;DR)

(面试开场 30 秒口述范本)

“关于分库分表,我的核心原则是 ‘能不分就不分,数据量不到 2000 万或者 10GB 不要动’

  1. 拆分策略:分为 垂直拆分(按业务模块拆库,或把大字段拆表)和 水平拆分(按 Hash 或 Range 规则把数据切分到多个库/表中)。

  2. 核心难点:拆分后会引入 分布式事务(Seata)、跨库 Join(禁止)、全局 ID 生成(雪花算法)以及 分页排序 等复杂问题。

  3. 技术选型:在 Java 生态中,首选 ShardingSphere-JDBC(轻量级,客户端路由),其次是 MyCat(代理层)。

我们的目标是利用水平扩展解决单机 I/O 和存储瓶颈,但代价是代码复杂度的指数级上升。”


2. 详细拆解

2.1 什么时候拆?(阈值与信号)

面试官会问:“你上一家公司为什么要分库分表?” —— 别说是老板让分的。

  • 垂直分库:微服务化。把“单体应用”拆成“用户服务”、“订单服务”。

  • 垂直分表:大字段剥离。表里有个 text 类型的“商品详情”,拖慢了查询,把它单独拎出来放到一张扩展表里。

  • 水平分表:单表数据量爆炸。

    • 经验值:单表 > 2000 万行数据文件 > 10GB

    • 现象:B+ 树层高变高(超过 3 层),磁盘 I/O 成为瓶颈,简单的 SQL 执行时间超过 1 秒。

2.2 怎么拆?(两种维度)

  • 垂直拆分 (Vertical Sharding)

    • 定义剪切。把一张宽表(很多列)剪成两张窄表;或者把一个大库剪成两个小库。

    • 强制比喻

      垂直拆分就像“公司部门改革”

      原来一个办公室里(一个库)挤着销售、财务、人事。人太多了,电话线(连接数)不够用。

      现在把销售部赶到楼下办公室(订单库),人事部留在楼上(用户库)。

      好处:各干各的,互不干扰。坏处:以前喊一嗓子就能沟通(Join),现在得打电话(RPC/API)。

  • 水平拆分 (Horizontal Sharding)

    • 定义切片。表结构不变,数据变少。

    • 强制比喻

      水平拆分就像“电话簿分册”

      电话簿(表)太厚了,有 1 亿页,翻不动。

      于是你把它撕成 10 本。第一本存 A-C 开头的名字,第二本存 D-F...

      好处:每本书都很薄,翻得飞快。坏处:你要找的人如果不知道首字母(没有分片键),你就得把 10 本书全翻一遍(全路由扫描)。

2.3 核心组件:路由规则与全局 ID

这是落地的关键。

  1. 路由算法 (Routing)

    • Range(范围分片):按时间(2023年表,2024年表)或 ID 区间(1-100w,100w-200w)。

      • :扩容简单(加新表就行)。:数据热点(最近的数据都在新表,旧表没人查)。

    • Hash(哈希分片 - 主流)id % 库数量

      • :数据极其均匀。:扩容是灾难(模数变了,所有数据都要迁移)。

  2. 分布式 ID (Global ID)

    • 痛点:拆分后,不能用 MySQL 的 Auto Increment 了,否则两个表都会出现 ID=1。

    • 方案雪花算法 (Snowflake)

    • 结构1bit(符号) + 41bit(时间戳) + 10bit(机器ID) + 12bit(序列号)

    • 比喻

      雪花 ID 就像“火车票号”

      它包含了 日期(时间戳) + 车次(机器ID) + 座位号(序列号)

      只要时间在走,车次不同,生成的票号永远不会重复,而且大致是有序的。


3. 深挖追问 (模拟压力面)

面试官微微一笑,开始抛出分库分表的“三大死穴”:

  1. 关于非分片键查询 (The "Non-Sharding Key" Problem)

    • Q: “你按 user_id 分库。现在我要查 order_id = 123 的订单,但我不知道它是哪个用户的,怎么查?”

    • A: (必考题

      • 方案一(笨)广播路由。把所有库都查一遍,聚合结果。性能极差。

      • 方案二(常规)映射表。单独建一张索引表 mapping (order_id, user_id),先查这张表拿到 user_id,再去查分片库。

      • 方案三(大厂)基因法。生成 order_id 时,把 user_id 的最后几位(比如后 4 位)嵌入到 order_id 里。这样读 order_id 的时候,直接提取那几位就能算出库号。)

  2. 关于扩容迁移 (Scaling Data)

    • Q: “你现在是 2 个库,业务暴涨要扩容到 4 个库。怎么在不停机的情况下把数据迁移过去?”

    • A: (双写方案 - Double Write

      1. 同步双写:代码升级,所有增删改操作,同时写“老库”和“新库”。

      2. 历史数据追平:后台跑脚本,把老库的历史数据读出来,写到新库(遇到 ID 冲突以新库为准)。

      3. 校验:跑几轮数据比对。

      4. 切换:将读流量切到新库。

      5. 停老库。这是阿里/美团最常用的在线平滑扩容方案。)

  3. 关于跨库分页 (Global Pagination)

    • Q:SELECT * FROM order ORDER BY time LIMIT 100, 10。如果分散在 10 个表里,怎么查?”

    • A: (性能杀手

      • 中间件(如 ShardingSphere)必须去 10 个表里,每个表都查前 110 条数据(注意是 100+10)。

      • 汇总这 1100 条数据。

      • 在内存中重新排序。

      • 取第 100 到 110 条。

      • 结论:页码越深,内存爆炸。禁止深度分页,或者要求客户端记录 last_max_id 进行游标查询。)


主从复制

1. 直击要点 (TL;DR)

(面试开场 30 秒口述范本)

“MySQL 主从复制的核心是 Binlog

简单来说,分为三步:

  1. Master 把数据更改记录到二进制日志 (Binlog) 中。

  2. Slave 启动一个 IO 线程,去请求 Master 的 Binlog,并把它写入到本地的中继日志 (Relay Log)。

  3. Slave 再启动一个 SQL 线程,读取 Relay Log,将其中的操作在本地重放 (Replay) 一遍。

这是一个异步过程,所以天然存在延迟。在生产环境中,我们通常开启 GTIDMTS (多线程复制) 来提升稳定性和性能。”


2. 详细拆解

2.1 核心组件与流程 (The Three Threads)

这是面试必画的图,脑子里必须有画面。

  • Master 侧

    • Binlog Dump Thread:当 Slave 连接上来时,Master 创建这个线程,负责把 Binlog 的内容发给 Slave。

  • Slave 侧

    • IO Thread:负责“搬运”。连上 Master,拿回 Binlog,写入本地的 Relay Log(中继日志)。

    • SQL Thread:负责“执行”。读取 Relay Log,解析成 SQL,在 Slave 上再执行一遍。

  • 强制比喻:

    主从复制就像是“大厨带徒弟”

    • Master (大厨):正在炒菜(处理写请求)。他一边炒,一边把菜谱(操作步骤)写在黑板上 (Binlog)。

    • IO Thread (徒弟的眼睛):一直盯着黑板,把大厨写的步骤抄到自己的笔记本上 (Relay Log)。

    • SQL Thread (徒弟的手):看着笔记本上的步骤,自己在灶台上重新炒一遍 (Replay)。

    关键点:徒弟炒菜的速度通常比大厨慢,因为大厨是并发炒(多线程写),徒弟以前只能一个个炒(单线程复制),所以会有延迟。

2.2 复制模式的进化 (Sync Modes)

这是区分“只会配环境”和“懂高可用”的关键。

  • 异步复制 (Asynchronous - 默认)

    • 原理:Master 写完 Binlog,不等 Slave 确认,直接给客户端返回 Success。

    • 风险:Master 挂了,Binlog 还没传给 Slave,数据丢失

  • 半同步复制 (Semi-Sync)

    • 原理:Master 写完 Binlog,必须等至少一个 Slave 收到并写入 Relay Log(返回 ACK),才给客户端返回 Success。

    • 比喻

      异步是发平信,扔进邮筒就不管了,丢没丢不知道。

      半同步是发挂号信,必须看到对方签收了,我才放心。

  • 全同步复制 (Group Replication - MGR)

    • 原理:基于 Paxos 协议,多节点协商,保证强一致性(MySQL 5.7/8.0 的新特性)。

2.3 亮点/加分项:并行复制 (MTS)

面试官问:“Slave 延迟太高怎么办?” —— 必须提到 MTS。

  • 痛点:Master 是多线程并发写入,TPS 很高。早期的 Slave 的 SQL 线程是单线程的,根本追不上。

  • 解决:MySQL 5.6 引入库级别并行,5.7 引入 基于 Logical Clock 的并行复制 (MTS)

  • 原理:如果不冲突的事务(在 Master 上能并发提交的),在 Slave 上也可以并发执行。


3. 深挖追问 (模拟压力面)

面试官面无表情,开始针对“延迟”和“一致性”进行轰炸:

  1. 关于主从延迟(Replication Lag)

    • Q: “线上出现主从延迟,导致用户刚注册完(写主库),立刻登录(读从库),提示‘账号不存在’。怎么解决?”

    • A: (这是经典场景)

      • 方案一(最次):前端强行 Sleep 500ms(不推荐,影响体验)。

      • 方案二(常用)强制读主。对于“注册后立即登录”、“支付后立即查询”这种关键业务,在代码层面强制路由到 Master 查询。

      • 方案三(高级):判断 Seconds_Behind_Master 参数,或者对比 GTID。如果延迟过大,暂时把流量切回主库或报错。

  2. 关于循环复制

    • Q: “如果是双主架构(Master A <-> Master B),A 的数据同步给 B,B 又同步给 A,岂不是死循环了?”

    • A: (核心参数 Server_ID。Binlog 只有在 Server_ID 不同时才会被执行。A 收到 B 发回来的日志,一看 Server_ID 是自己的,直接丢弃,不执行。)

  3. 关于 Binlog 格式

    • Q: “Binlog 有 Statement, Row, Mixed 三种格式,推荐用哪种?为什么?”

    • A: (必须回答 Row 格式

      • Statement:记 SQL 语句。优点是日志小;缺点是如果 SQL 里有 NOW()UUID() 这种函数,回放时结果会不一致。

      • Row:记每一行数据的具体变更(Before -> After)。优点是数据绝对一致;缺点是日志量大(比如 UPDATE 全表,会产生巨量日志)。

      • 结论:现在带宽不是瓶颈,为了数据安全,互联网大厂强制要求使用 Row 格式。)


数据宽表

1. 直击要点 (TL;DR)

(面试开场 30 秒口述范本)

“在 MySQL 中,我极度不推荐设计超过 50 个字段的宽表,尤其是高频读写的核心业务表。

  1. 物理限制:MySQL 一行记录最大不能超过 65,535 字节。字段太多容易触顶报错。

  2. 缓存效率低:InnoDB 是按页 (Page, 16KB) 读取数据的。如果一行数据太胖,一页只能存几行,导致 Buffer Pool 内存命中率骤降,磁盘 IO 激增。

  3. 维护灾难:宽表在做 ALTER TABLE(加减字段)时,会锁表或导致长时间的主从延迟,极大影响线上可用性。

如果业务非要存 100 个字段,我的方案是做垂直拆分,将‘热字段’和‘冷字段’分表存储。”


2. 详细拆解

2.1 基础概念:宽表的物理限制

  • 硬限制:虽然 MySQL 理论上支持 4096 列,但有一个更硬的指标 —— Row Size (行大小)

  • 规则:所有列(不包括 TEXT/BLOB 的溢出部分)加起来的长度,不能超过 65,535 Bytes(约 64KB)。

  • 场景:如果你定义了 10 个 VARCHAR(10000),建表直接报错。

2.2 核心原理:为什么宽表慢?(Page 密度与 IO)

这是面试的核心得分点,必须讲清楚 InnoDB 的页管理。

  • InnoDB 的最小单位Page (默认 16KB)。磁盘 I/O 是一次读一页,内存 Buffer Pool 也是一页一页存。

  • 核心逻辑

    • 瘦表(窄表):一行数据 100 Bytes。一个 Page 能存 160 块 数据。读取一次 IO,就能拿到 160 行。

    • 胖表(宽表):一行数据 8KB(因为字段多)。一个 Page 只能存 2 行 数据。

  • 性能崩塌

    • 你要查 100 行数据。

    • 瘦表:只需要 1 次 IO(加载 1 页)。

    • 胖表:需要 50 次 IO(加载 50 页)。

    • 结论:宽表会让你的磁盘 IO 暴涨 50 倍,且 Buffer Pool 很快被填满,导致热点数据被挤出内存。

  • 强制比喻

    宽表就像“满载的公交车”

    • InnoDB 的 Page 就是一辆公交车(固定大小 16KB)。

    • 窄表(正常人):大家身材标准,一辆车能挤 100 个人。运送 100 人只需要发 1 趟车。

    • 宽表(相扑选手):每个字段都很大,一行记录就像一个相扑选手。一辆车只能坐 2 个人。运送 100 人需要发 50 趟车。

    • 后果:车队(IO)排长龙,系统拥堵不堪。

2.3 亮点/解决方案:垂直拆分 (Vertical Partitioning)

如果业务真的有 100 个属性,怎么办?

  • 策略冷热分离

  • 主表 (User):只存高频查询的字段。如 id, name, status, balance。这些字段短小精悍,保证主表查询极快。

  • 扩展表 (User_Extra):存低频/大字段。如 address, bio (简介), config (配置 json)。

  • 关联:通过 user_id 进行 1:1 关联。

  • 查询

    • 列表页(90% 流量):只查主表,速度飞快。

    • 详情页(10% 流量):查主表 + 扩展表(按需加载)。


3. 深挖追问 (模拟压力面)

面试官看你懂冷热分离,决定考考你的“极端场景”和“运维能力”。

  1. 关于 SELECT \* 的再次伤害

    • Q: “在宽表中执行 SELECT *,除了慢,还会对数据库造成什么隐性伤害?”

    • A: (Buffer Pool 污染。因为宽表数据量大,全表扫描或大量查询会将 Buffer Pool 中的热点数据页(Hot Page)挤出去,导致其他核心业务的查询突然变慢,产生雪崩效应。)

  2. 关于行溢出 (Row Overflow)

    • Q: “如果我有一个字段是 TEXT 类型,存了 1MB 的文章,它在 InnoDB 页里是怎么存的?”

    • A: (必须提到 Off-Page 存储。InnoDB 的 Page 只有 16KB,存不下 1MB。

      • 主键索引页里只存这个字段的前 768 字节(Antelope 格式)或 20 字节指针(Barracuda 格式)。

      • 真正的数据存放在另外的 溢出页 (Overflow Page) 里。

      • 读取代价:每次读取这行数据,都需要额外的 随机 I/O 去找溢出页。所以千万别在列表查询里包含 TEXT 字段。)

  3. 关于宽表的替代方案

    • Q: “如果业务就是这么复杂,有几百个属性,而且都要查,MySQL 实在扛不住了,你会引入什么技术?”

    • A: (NoSQL 或 列式存储

      • MongoDB:适合存这种松散的、字段特别多的文档型数据。

      • HBase:适合海量列族数据。

      • ClickHouse/Doris:如果是为了做分析(OLAP),列式存储天生适合宽表,查哪列读哪列,完全没有 IO 浪费。)


视图

1. 直击要点 (TL;DR)

(面试开场 30 秒口述范本)

“视图本质上是一张虚拟表,它不存物理数据,只保存 SQL 逻辑。

  1. 核心作用:简化复杂的 SQL 操作(如封装 100 行的 Join 逻辑),以及做数据权限控制(只暴露部分列给用户)。

  2. 大厂现状:在互联网高并发架构中,我们几乎不使用视图

  3. 原因:视图把业务逻辑藏在了数据库层,难以通过 Git 版本控制,难以调试,且在 MySQL 中往往会导致索引失效或性能不可控(特别是使用临时表算法时)。

所以,我的原则是:复杂的逻辑在 Java 代码层解决,数据库只做最简单的读写。


2. 详细拆解

2.1 基础概念:什么是视图?

  • 定义:视图就是一条被保存起来的 SELECT 语句

  • 用法:你创建一个视图 CREATE VIEW v_user_orders AS SELECT ...,以后就可以像查表一样 SELECT * FROM v_user_orders

  • 强制比喻

    视图就像是电脑桌面的“快捷方式”

    双击它(查询视图),系统会自动带你去真正的文件夹(物理表)找文件。

    快捷方式本身不占硬盘空间(不存数据),删了快捷方式,源文件(物理表数据)还在。

2.2 核心原理:MySQL 视图的两种算法

面试官如果深究,会问:“查询视图和查询普通表,性能一样吗?” 这取决于算法。

  1. Merge 算法(合并算法 - 性能好)

    • 原理:MySQL 把“查询视图的 SQL”和“视图定义的 SQL”合并成一条 SQL,直接去查物理表。

    • 比喻文本替换。你说“我要 A 套餐”,服务员直接把它翻译成“汉堡+可乐+薯条”下单。

    • 条件:视图里没有聚合函数(SUM/COUNT)、没有 GROUP BY、没有子查询等复杂逻辑时,通常用这个。

  2. Temptable 算法(临时表算法 - 性能差)

    • 原理:MySQL 先执行视图定义的 SQL,把结果存到一个内存临时表中,然后再从这个临时表里查数据。

    • 痛点临时表没有索引! 如果数据量大,全表扫描直接卡死。

    • 比喻先做饭再点菜。厨房先把 A 套餐做好放在盘子里(临时表),你再从盘子里挑你想要的。这中间无法利用原材料的“分类标签”(索引)。

2.3 亮点/加分项:物化视图 (Materialized View)

这是 MySQL 面试中的杀手锏

  • 痛点:普通视图是虚拟的,每次查都要重新算一遍。如果计算量超级大(比如报表),实时查根本扛不住。

  • 解决方案物化视图。把查询结果真的存下来(落盘),变成一张物理表,定期刷新。

  • MySQL 现状MySQL 原生不支持物化视图(Oracle/PostgreSQL 支持)。

  • 大厂替代方案

    • 手动挡:写个定时任务(Schedule),每 5 分钟算一次,插入到一张“中间表”里。

    • 自动挡:使用 Canal 监听 Binlog,把数据同步到 ElasticsearchClickHouse 这类 OLAP 引擎中做实时统计。


3. 深挖追问 (模拟压力面)

面试官看你对“不使用视图”的态度很坚决,决定反向考察:

  1. 关于可更新视图

    • Q: “可以通过视图去 UPDATE/INSERT 数据吗?”

    • A: (原则上可以,但限制极多。只有当视图和底层表是 1对1 映射时(无聚合、无 DISTINCT、无 GROUP BY)才能更新。一旦涉及复杂逻辑,更新会报错。工程实践中严禁通过视图更新数据,因为很难预测副作用。)

  2. 关于架构解耦

    • Q: “如果业务逻辑很复杂,SQL 写了几百行,用视图封装一下不是更清晰吗?为什么非要在 Java 代码里拼 SQL?”

    • A: (考察架构思维

      1. 版本控制:Java 代码有 Git 管理,谁改了什么一清二楚。数据库的 View 修改记录很难追踪。

      2. 性能优化:在 Java 层可以拆分 SQL,做应用层 Join,利用 Redis 缓存。封装在 View 里就是个黑盒,DBA 想优化都无从下手。

      3. 迁移成本:万一以后去 IOE(去 Oracle 换 MySQL),大量的视图逻辑迁移就是灾难。)

  3. 关于数据安全

    • Q: “如果不许用视图,怎么保证敏感字段(如用户手机号、薪资)不被普通开发人员查到?”

    • A: (数据脱敏 Data Masking。通常不在数据库层做,而是在 MyBatis 拦截器API 网关层 统一做脱敏处理。或者在数据库层面通过 Column-level Privilege(列级权限) 控制,而不是依赖视图。)


存储过程

1. 直击要点 (TL;DR)

(面试开场 30 秒口述范本 —— 必须表明立场)

“关于存储过程,我的观点很明确:在互联网高并发架构中,应该严格禁止使用

虽然它能减少网络交互,但它有三大致命缺陷:

  1. 难以维护:无法使用 Git 版本控制,难以进行单元测试和 CI/CD 部署。

  2. 资源争抢:数据库是系统的核心瓶颈(I/O密集型),不应该承担计算逻辑(CPU密集型)。

  3. 扩展性差:应用服务器(Java)可以水平扩展,数据库很难。把逻辑绑死在数据库上,就是给扩展性判了死刑。

所以,《阿里 Java 开发手册》中也是强制禁止使用存储过程的。”


2. 详细拆解

2.1 基础概念:什么是存储过程?

  • 定义:一组为了完成特定功能的 SQL 语句集,经编译后存储在数据库中。

  • 用法:客户端不需要传 SQL,只需要传参数调用即可(类似 RPC)。

  • 强制比喻

    存储过程就像“自动贩卖机”

    • 普通 SQL:是你进厨房(数据库),自己拿面粉、拿糖、开火(一条条执行 SQL),最后做成面包。

    • 存储过程:是你往贩卖机投币(传参数),机器内部咔嚓咔嚓一顿响(执行预编译的逻辑),直接吐个面包给你。

    • 问题在于:贩卖机坏了很难修(难调试),而且贩卖机很贵(数据库资源宝贵),不能让它还要负责揉面团(复杂计算)。

2.2 核心原理:为什么大厂要“去存储过程化”?

这是面试的得分点,要从计算与存储分离的角度来谈。

  1. CPU vs IO 的博弈

    • 数据库(MySQL):设计初衷是为了高效地存取数据(IO 密集型)。它的 CPU 资源极其宝贵,要留给索引查找和锁等待。

    • 应用层(Java/Tomcat):设计初衷就是为了处理逻辑(CPU 密集型)。而且应用服务器便宜,不够了可以随便加机器(水平扩展)。

    • 结论:把计算逻辑放在数据库里,就是“让会计去搬砖”,既浪费人才,又干不好活。

  2. 工程化困境

    • 版本控制:Java 代码都在 Git 里,回滚、Review 清清楚楚。存储过程存在 DB 里,谁改了什么、什么时候改的,根本是一笔糊涂账。

    • 调试困难:Java 可以断点调试。存储过程报错了,你只能靠猜,或者打印毫无意义的 Error Code。

  3. 移植性(Vendor Lock-in)

    • MySQL 的存储过程语法跟 Oracle、SQL Server 都不一样。一旦用了存储过程,你的系统就被这个数据库厂商绑死了,想切库(比如去 IOE)几乎不可能。

2.3 亮点/加分项:什么场景下可以用?

面试官如果非要抬杠:“真的就一点用没有吗?”

  • 回答策略:承认其存在价值,但限定场景。

  • 场景

    • 报表/数据清洗(非在线业务):如果是离线数据仓库,或者临时跑一个洗数据脚本,用存储过程批量处理非常快,因为它就在数据旁边,省去了海量的网络 I/O。

    • 极度敏感时延的内部系统:比如某些极其古老的证券交易核心(非互联网架构),为了那 0.01ms 的网络延迟,可能会牺牲架构灵活性。


3. 深挖追问 (模拟压力面)

面试官看你立场坚定,决定考考你的迁移能力替代方案

  1. 关于遗留系统改造

    • Q: “如果你入职的新公司,核心交易系统里全是几千行的存储过程,老板让你维护,或者让你重构,你怎么办?”

    • A: (考察稳健性。千万别说“一把梭全删了重写”。

      • 策略绞杀者模式 (Strangler Fig Pattern)

      • 步骤:不要直接改存储过程。新功能用 Java 写。对于旧功能,逐步把逻辑抽离到 Java 层(Service),边缘化存储过程。虽然初期会有网络开销增加,但为了长期的可维护性是值得的。)

  2. 关于批量处理性能

    • Q: “我有一个需求,要给 100 万用户发优惠券。用存储过程一个循环全搞定,只要 10 秒;用 Java 哪怕多线程跑也要 5 分钟(因为有网络 IO)。这时候你还坚持不用存储过程吗?”

    • A: (考察架构权衡

      • 回答:是的,我依然坚持不用。

      • 理由:虽然存储过程快,但它会长时间占用数据库 CPU,导致这 10 秒内正常的线上用户请求被卡死(数据库阻塞)。

      • 替代方案:在 Java 层用线程池 + 批量插入 (Batch Insert)。虽然慢一点,但可以通过控制并发度,保护数据库不被打挂。安全比速度更重要。)

  3. 关于触发器 (Trigger)

    • Q: “那触发器(Trigger)呢?比如在这个表插入数据,自动在那个表记录日志,这总能用吧?”

    • A: (更要严禁。触发器是“隐形的杀手”。代码里明明只写了 Insert A,结果数据库偷偷去 Update B。一旦出 Bug,开发人员查代码查到死都不知道为什么数据变了。逻辑必须显式地写在代码里。)


MySQL 线上故障排查

1. 直击要点 (TL;DR)

(面试开场 30 秒口述范本 —— 此时你要表现得像个战地医生)

“面对 MySQL 线上故障,我的处理原则是 ‘先止血,后查因’

  1. 宏观定位:先看监控面板(Prometheus/Grafana)或 top 命令,确认是 CPU 飙升IO 打满 还是 连接数爆表

  2. 精准抓捕:如果是 CPU 100%,通常是慢 SQL 导致的。我会立刻登录数据库执行 SHOW FULL PROCESSLIST,找那些状态为 Sending dataSorting 且执行时间长的 SQL。

  3. 紧急止血:如果该 SQL 阻塞了核心业务,且无法快速优化,我会果断执行 KILL 命令杀掉线程,或者进行降级/限流

  4. 根因分析:事后分析 Slow Query LogExplain 执行计划,追加索引或改写 SQL 上线。”


2. 详细拆解:三大高频故障场景

场景一:CPU 飙升 100% (最常见)

  • 现象:应用响应极慢,报警群炸锅,DBA 电话打爆。

  • 原因:通常是 一两条烂 SQL 在做大量的计算(全表扫描、Group By、排序)。

  • 排查步骤

    1. 系统层top -c 确认是 mysqld 进程占用了 CPU。

    2. 数据库层SHOW FULL PROCESSLIST;

    3. 观察重点

      • State: 重点抓 Sending data (正在读磁盘/扫描数据)、Copying to tmp table (在做临时表排序)。

      • Time: 执行时间很长的。

    4. 解决:找到 SQL ID,执行 KILL <id>;

  • 强制比喻

    CPU 100% 就像“高速公路大堵车”

    通常不是因为车太多(并发高),而是因为 前面有一辆龟速的拖拉机(烂 SQL) 占着快车道,导致后面所有的法拉利(正常请求)都跑不起来。

    你的任务就是把这辆拖拉机 吊销驾照(KILL) 挪走。

场景二:磁盘 I/O 打满 (IO Util 100%)

  • 现象:CPU 可能不高,但系统卡顿,查询超时。

  • 原因

    • 没有索引:全表扫描导致大量物理读。

    • 内存太小:Buffer Pool 命中率低,频繁换页。

    • Redo Log 太小:频繁触发刷盘(Checkpoint)。

  • 排查步骤

    1. 系统层iostat -x 1%util 是否接近 100%。

    2. 数据库层:查看 Slow Query Log,按照扫描行数(Rows_examined)排序,找那些扫描了 100 万行但只返回 1 行的 SQL。

场景三:连接数爆表 (Too many connections)

  • 现象:应用报错 Can not connect to MySQL server

  • 原因

    • 慢 SQL 阻塞:所有连接都在等锁或等结果,新连接进不来。

    • 应用连接池泄露:代码没 close 连接。

  • 排查步骤

    1. SHOW PROCESSLIST 统计连接数。

    2. 如果发现大量 Sleep 状态的连接:说明应用层连接池配置不当(空闲连接没释放)。

    3. 如果发现大量 Locked 状态:说明有死锁或行锁等待。


3. 深挖追问 (模拟压力面)

面试官看你有一套排查思路,决定考考你的深度细节

追问 1:“如果不让你用 SHOW PROCESSLIST(因为显示不全),怎么精准定位是哪个 OS 线程占用了 CPU?”

  • 背景top 看到的是进程 ID,MySQL 内部是线程。怎么对应起来?

  • A: (高阶技巧

    1. top -H -p <pid>:查看 MySQL 进程下的所有 线程,找到 CPU 最高的那个 线程 ID (OS_Thread_ID),转成 16 进制。

    2. 查询 performance_schema.threads 表:

      SQL

       SELECT * FROM performance_schema.threads WHERE THREAD_OS_ID = <16进制ID>;
    3. 根据查出来的 PROCESSLIST_IDPROCESSLIST 表里找具体的 SQL。这是最精准的定位方法。)

追问 2:“执行 KILL 命令后,SQL 并没有停,状态变成了 Killed,但数据库还是很卡,为什么?”

  • A: (事务回滚是需要时间的

    • 如果那个 SQL 正在做一个巨大的 UPDATE(比如改了 100 万行),你 KILL 它,MySQL 必须执行 Rollback(利用 Undo Log 把 100 万行改回去)。

    • 回滚消耗的资源往往比执行还要大

    • 怎么办:这时候千万别重启数据库(重启后还要继续回滚),只能。教训是:大事务必须拆分。)

追问 3:“线上出现了死锁(Deadlock),怎么找到是哪两条 SQL 打架?”

  • A: (死锁日志

    • 执行 SHOW ENGINE INNODB STATUS;

    • 找到 LATEST DETECTED DEADLOCK 这一段。

    • 它会详细记录 事务 1 拿着什么锁在等什么,事务 2 拿着什么锁在等什么。

    • 分析业务逻辑,调整加锁顺序,或者给字段加索引减少锁粒度。)


4. 全局总结:你的 MySQL 面试知识图谱

同学,恭喜你。经过这几轮的高强度对练,你已经建立了一套完整的 Java 后端 MySQL 面试体系

如果你现在去面试阿里/字节,你的回答应该覆盖了以下核心:

  1. 架构与基础

    • 知道执行路径(连接器->分析器->优化器->执行器)。

    • 绝不使用 存储过程、外键、视图。

    • 慎用 宽表(冷热分离)。

  2. 索引 (Index)

    • B+树:3 层存 2000 万,减少 IO,范围查询强。

    • 优化:最左前缀、覆盖索引、索引下推 (ICP)。

  3. 事务 (Transaction)

    • ACID:原子性(Undo)、一致性(Code)、隔离性(MVCC/Lock)、持久性(Redo)。

    • MVCC:ReadView + Undo Log 版本链,解决读写冲突。

  4. 锁 (Lock)

    • Next-Key Lock:解决幻读。

    • 死锁:调整顺序,减少锁粒度。

  5. 高可用 (HA)

    • 主从复制:Binlog (Row格式) -> Relay Log -> Replay。

    • 分库分表:不到 2000 万不分,分了就要面对分布式 ID、跨库分页等难题。

  6. 实战 (Troubleshooting)

    • CPU 飙升 -> top -H -> performance_schema -> KILL -> Explain

1

评论区