Skip to content

事务

事务是一组要么全部成功、要么全部失败的操作集合,用来保证数据的一致性和可靠性。在数据库中,事务常用于处理需要保持一致性的复杂业务逻辑。

事务的四大特性(ACID)是面试的重点:

  • 原子性:事务中的操作要么全部执行,要么全部不执行。
  • 一致性:事务执行前后,数据要保持逻辑一致。
  • 隔离性:不同事务之间相互独立,互不干扰。
  • 持久性:事务提交后的数据修改会被永久保存。

当多个事务并发执行时,如果没有足够的隔离,就可能出现以下问题:

  • 脏读:一个事务读取到了另一个事务未提交的数据。
  • 不可重复读:同一个事务多次读取到的数据不一致。
  • 幻读:事务执行过程中,另一事务插入了新数据,导致查询结果集发生变化。

而隔离级别就是控制这些问题的:

隔离级别脏读不可重复读幻读
Read Uncommitted
Read Committed×
Repeatable Read(默认)××
Serializable×××
  • read uncommitted:可能出现脏读、不可重复读、幻读。
  • read committed:避免脏读,但仍可能出现不可重复读和幻读。
  • repeatable read(MySQL 默认):避免脏读和不可重复读,但仍可能有幻读。
  • serializable:最高隔离级别,避免所有问题,但并发性最差。

事务开启方式

  • SQL 层面
sql
  start transaction;
  set autocommit = 0;
  • JDBC 层面
java
  connection.setAutoCommit(false);
  connection.commit();
  connection.rollback();

掌握事务的 ACID 特性和四种隔离级别,是 MySQL 面试的基础部分,尤其要熟练解释各隔离级别与脏读、幻读的关系。

内连接和外连接

SQL 连接查询的核心在于:把多张表的数据按照某个关联条件“拼”在一起。不同的连接方式决定了拼出来的结果集中,哪些数据会被保留。

  • 如果两张表都必须匹配,使用内连接。
  • 如果希望保留一方的所有记录,使用外连接。
  • 遇到分组统计场景,要注意连接方式会影响最终结果。

内连接

内连接返回两张表在关联字段上匹配成功的记录,也就是交集部分。如果某一行在任意一张表中找不到匹配,它就不会出现在结果中。

sql
SELECT *
FROM A
INNER JOIN B
ON A.id = B.id;
  • 只保留匹配成功的数据
  • 适合处理必须存在关联关系的场景(比如订单和用户)

外连接

外连接分为左外连接(LEFT JOIN)和右外连接(RIGHT JOIN),区别在于哪一边的数据会被完整保留。

  • 左外连接:以左表为主,返回左表的所有记录,如果右表没有匹配,用 NULL 填充。
sql
  SELECT *
  FROM A
  LEFT JOIN B
  ON A.id = B.id;
  • 右外连接:以右表为主,返回右表的所有记录,如果左表没有匹配,用 NULL 填充。
sql
  SELECT *
  FROM A
  RIGHT JOIN B
  ON A.id = B.id;

外连接会保留一张表的全部数据,即使没有匹配。常用于“主从关系”不对称的场景,比如保留所有用户,即使他没有订单。

分组与连接

在实际开发中,连接查询常常和分组统计结合出现,比如:

sql
SELECT u.id, u.name, COUNT(o.id) AS order_count
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 0;

这里 GROUP BYHAVING 可以对连接结果进一步筛选,比如筛掉没有订单的用户。

Statement 与 PreparedStatement

在 JDBC 中,这两个接口都能执行 SQL,但它们在安全性、性能与适用场景上有明显差异。

  • 为什么 PreparedStatement 更安全?
    因为参数不会与 SQL 直接拼接,而是通过占位符和绑定机制传入,数据库不会把参数当成 SQL 逻辑解析。
  • 性能为什么更高?
    因为预编译后的 SQL 执行计划可以重复利用,数据库不需要每次重新解析、优化语句。
  • Statement 还有用吗?
    在少量一次性 SQL 或 DDL 语句中(如建表)有时仍会用到,但业务开发中主流几乎都是 PreparedStatement

Statement

Statement 是最原始的 SQL 执行方式,它的工作流程是:

  1. 程序通过 Statement 拼接出完整 SQL;
  2. 每次执行时,SQL 都要被数据库重新编译、解析;
  3. 服务器执行并返回结果。

示例代码:

java
Statement stmt = connection.createStatement();
String sql = "SELECT * FROM wolf WHERE wolf_name = '" + name + "'";
ResultSet rs = stmt.executeQuery(sql);
  • 每次执行都需要重新编译 SQL,对频繁执行的语句效率较低;
  • 极易产生 SQL 注入风,如果 name 中有恶意输入,就可能破坏原有 SQL 逻辑。

输入 name = "黑客' OR '1'='1",原本的查询就会变成“全表扫描”的注入攻击。

PreparedStatement

PreparedStatementStatement 的增强版,支持 预编译 SQL占位符参数。它的工作流程是:

  1. 先把 SQL 语句发送给数据库进行预编译;
  2. 之后每次只传递参数,不再重新编译 SQL;
  3. 数据库复用执行计划,执行效率更高。

示例代码:

java
String sql = "SELECT * FROM wolf WHERE wolf_name = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, name);
ResultSet rs = pstmt.executeQuery();
  • SQL 结构与参数分离,彻底防止 SQL 注入;
  • 预编译 SQL 只编译一次,多次执行性能更高;
  • 代码更整洁,尤其是在插入、更新等复杂语句时更易维护;
  • 参数绑定类型安全,减少拼接错误。

数据库设计三范式

数据库三范式(3NF)是关系型数据库设计的基本原则之一,旨在减少数据冗余、避免更新异常、提高数据一致性

  1. 1NF 保证字段最小粒度,便于查询与更新;
  2. 2NF 保证字段与主键的依赖清晰,避免部分依赖;
  3. 3NF 保证字段不传递依赖,减少数据冗余。

虽然在真实项目中,有时会为了性能适当反范式化(比如冗余一些字段避免频繁 join),但这属于在掌握范式设计后有意识的取舍。

第一范式(1NF):字段不可再分

每个字段必须是原子值,不能再拆,也不能在一个字段中放多个数据。这是最基础的规范,解决的是“数据结构混乱”的问题。

idnameaddress
1狼王“四川省, 成都市, 天府大道”

这里的 address 同时包含省、市、街道,查询或筛选时很难精确匹配。
正确做法是:

idnameprovincecitystreet
1狼王四川省成都天府大道

第二范式(2NF):非主属性完全依赖主键

当表使用联合主键时,容易违反第二范式。
规则是:每个非主键字段必须依赖整个主键,而不是其中一部分。

wolf_idmission_idmission_title
11001守护森林

这里主键是 (wolf_id, mission_id),但 mission_title 只依赖 mission_id,不是整个主键。
应拆成两张表:

  • 任务表:mission_id → mission_title
  • 分配表:wolf_id + mission_id → 分配关系

这样修改任务名称时,只改任务表即可,避免重复更新。

第三范式(3NF):非主属性不能传递依赖主键

非主键字段不能依赖于另一个非主键字段,否则会产生冗余。

例如:

wolf_idwolf_namepack_idpack_name
1狼王10影月之森

pack_name 依赖 pack_id,而 pack_id 又依赖主键 wolf_id
应拆为两张表:

  • 狼表(wolf):wolf_id → wolf_name, pack_id
  • 狼群表(pack):pack_id → pack_name

当狼群改名时,只需更新一张表,不会产生数据不一致。

MySQL B+ 树索引结构

在关系型数据库中,索引是提高查询速度的关键手段。
MySQL(尤其是 InnoDB 引擎)选择 B+ 树 作为默认索引结构, B+ 树 在磁盘存储、查询效率与范围查询等方面都比其他结构更契合数据库的特性。


在理解 B+ 树前,可以先从 B 树说起。
B 树是一种多路平衡搜索树,每个节点既可以存储键值,又可以存储数据。
这意味着它在查找时可能在中间节点就结束,不一定走到叶子层。
但这种结构带来一个问题:每个节点既放数据又放索引,能容纳的分支数减少,树会变“高”,磁盘 IO 次数也会增加。

相比之下,B+ 树只在叶子节点存放数据,非叶节点只保存索引信息(键与指针),形成类似“目录页”的结构。
这让每个节点能容纳更多索引项,树的高度大大降低。无论查哪个数据,都必须走到叶子节点,这让所有查询路径长度相同,性能更加稳定

除此之外,B+ 树的叶子节点之间还有双向链表相连,使得数据库可以轻松地进行范围查询。
比如:

sql
SELECT * FROM wolf WHERE wolf_age BETWEEN 10 AND 20;

数据库只需定位到 10 的叶子节点,然后顺着链表扫到 20 即可,无需重复从根节点检索。
这就是 B+ 树在数据库中最突出的优势之一。

从物理角度看,B+ 树也更“省 IO”。
非叶子节点没有实际数据,体积小,能在内存中缓存更多层级。
而查找时,只需从根节点到叶子层,三到四次磁盘访问就能定位百万级数据,效率极高。

InnoDB 的 B+ 树根据用途又分为两类:

  • 聚簇索引(主键索引):叶子节点直接存储整行数据;
  • 非聚簇索引(二级索引):叶子节点只存主键值,需要再通过主键“回表”才能取到完整行。

如果查询字段都被索引覆盖,就不必回表,这类查询称为“覆盖索引”,性能最佳。


总结来看,MySQL 选择 B+ 树作为索引结构的原因很直接:

  • 树更矮,磁盘访问次数更少;
  • 查找路径统一,性能稳定;
  • 叶子链表让范围扫描极其高效;
  • 非叶节点轻量,缓存命中率高;
  • 结构契合页存储机制,能充分利用磁盘和内存。

简而言之,B+ 树是兼顾磁盘结构与查询特性的最优平衡方案。
它既能快速定位单条记录,又能高效支持范围、排序与分页查询,这就是它成为 MySQL 默认索引结构的根本原因。

MySQL 优化(面试题整理)

MySQL 的优化问题是高频面试题,面试官不会只问“你了解优化吗”,而是会一步步逼近你的理解深度。
比如:“查询很慢你怎么排查?”、“Explain 你怎么看?”、“慢 SQL 你怎么处理?”——考察的是你对优化思路是否清晰手段是否落地


一、优化的层次

数据库优化不能一上来就调 SQL,它是有层次的。常见分为三个层面:

  1. 系统层面(运维):操作系统、网络、磁盘、服务器资源。
  2. 数据库服务层面:MySQL Server 的配置,如缓存、连接池、参数调优。
  3. SQL 语句层面:开发者最常接触,针对查询本身进行优化。

👉 面试时,能清楚区分这三层,会让回答更有条理。


二、慢 SQL 的排查流程

性能瓶颈往往集中在少数 SQL 上,因此第一步通常是定位问题 SQL。常用手段:

  • 开启并分析 慢查询日志
    sql
    SET GLOBAL slow_query_log = 1;
    找出执行时间超过阈值的 SQL 语句。
  • 使用 EXPLAIN 查看执行计划:
    sql
    EXPLAIN SELECT * FROM wolf WHERE wolf_name = '影月';
    重点关注 typekeyrowsExtra 等字段,判断索引是否被使用。
  • 使用 SHOW PROCESSLIST 查看当前正在执行的 SQL,排查阻塞或长事务。
  • 借助中间件监控(如连接池的内置监控)分析运行中的慢 SQL 分布。

👉 这一整套排查链路是“有章法”的流程,能展示出实际工程经验。


三、SQL 语句的优化技巧

在定位到具体 SQL 之后,下一步就是改写或重构语句,让它充分利用索引和优化器特性。常用方法包括:

  • 优先考虑 走索引,避免全表扫描。
  • 遵循最左匹配原则,合理设计复合索引。
  • 避免在索引列上使用函数或运算,会导致索引失效。
  • 避免 OR 链接条件,改写为 IN 或拆分多次查询。
  • 控制返回列,减少不必要的 SELECT *
  • 小结果集驱动大结果集,尽可能让过滤条件在前。
  • 避免模糊匹配前置 %,否则无法利用索引。
  • 善用 覆盖索引,减少回表。

👉 优化的关键,是让 SQL 尽可能少访问磁盘、少做无效计算、少走回表路径。


四、Explain 执行计划的关键字段

EXPLAIN 是 SQL 优化的基础工具。以下是几个关键字段及意义:

字段含义说明
type访问类型ALL(全表扫描)性能最差,refrange 较优
key使用的索引如果为空,说明没有走索引
rows扫描的行数估算值越小越好
Extra额外信息例如 Using index 表示覆盖索引,无需回表

👉 面试时,如果你能根据 EXPLAIN 输出判断 SQL 是否合理,基本就已经达到工程实战水平了。


五、索引设计的优化

SQL 再怎么写得好,如果没有合适的索引,也无济于事。
优化常见要点:

  • 为高频 WHERE 条件字段建立 B+ 树索引。
  • 不为唯一性太差的字段(如性别)建立单列索引。
  • 频繁更新的字段不宜建索引。
  • 组合索引遵循最左匹配,字段顺序非常关键。
  • 优先考虑能“覆盖查询”的索引,避免回表。

👉 索引是优化的基础,而 SQL 改写是锦上添花。


六、大表与架构层面的优化

如果 SQL 已经写得很优,但依然很慢,就需要考虑更高层次的优化:

  • 分库分表:缓解单表数据过大导致的扫描压力。
  • 读写分离:主库写、从库读,提升并发能力。
  • 缓存:用 Redis 或 Memory Engine 减少热点查询的压力。
  • 异步化:减少长事务阻塞的风险。

👉 面试官喜欢听到你能从“单条 SQL 优化”扩展到“整体架构优化”,这显示的是你能站在更高维度思考问题。


七、优化的思维方式

数据库优化没有银弹,它依赖一整套思维:

  1. 定位问题,而不是拍脑袋猜;
  2. 先测量,再调整;
  3. 优化路径先从 SQL 入手,再到架构;
  4. 所有优化都必须有数据支撑,否则容易适得其反。

👉 这种思维方式,是面试加分的核心。

索引失效(面试题整理)

SQL 再优秀,如果没能利用上索引,也可能彻底“跑不动”。所谓“索引失效”,就是原本可以利用 B+ 树索引的查询,被优化器放弃走索引,转而进行全表扫描或低效扫描。
这是 MySQL 查询性能下降的常见原因之一,也是面试中非常高频的考点。


最左匹配原则被破坏

MySQL 的 B+ 树索引遵循最左匹配原则,这意味着:

  • 查询条件必须从索引定义的最左列开始;
  • 只有连续使用索引前缀的部分才能生效;
  • 一旦中间断了,后面的部分都无法使用索引。

例如建了复合索引 (wolf_name, wolf_age)

sql
SELECT * FROM wolf WHERE wolf_name = '影月' AND wolf_age = 20;  -- 走索引
SELECT * FROM wolf WHERE wolf_age = 20;                        -- 索引失效

在第二条语句中,因为跳过了 wolf_name,索引直接失效。
这类错误非常常见,尤其是字段多时。


对索引列使用函数或运算

如果在查询条件中对索引列做了函数处理、表达式运算或类型转换,优化器无法利用索引中的有序性,只能全表扫描:

sql
SELECT * FROM wolf WHERE UPPER(wolf_name) = '影月';
SELECT * FROM wolf WHERE wolf_age + 1 = 20;

一旦出现这种写法,MySQL 只能逐行计算,再比对结果。
这种场景要么改写 SQL,要么改造索引列的数据格式。


不等于操作导致失效

使用 !=<> 等不等式时,MySQL 通常无法走索引,因为无法准确定位需要扫描的范围:

sql
SELECT * FROM wolf WHERE wolf_age <> 20;

不等式会迫使优化器扫描大量区间,成本太高时,它会直接选择全表扫描。
同理,NOT IN 也容易导致类似的结果。


模糊匹配的前置通配符

LIKE 查询中如果通配符出现在前面,B+ 树索引就无法使用:

sql
SELECT * FROM wolf WHERE wolf_name LIKE '%影月';   -- 索引失效
SELECT * FROM wolf WHERE wolf_name LIKE '影月%';   -- 可以走索引

这是因为前缀模糊查询破坏了有序性,MySQL 无法通过树结构定位范围。
如果确实需要模糊匹配,可以考虑倒排索引或全文索引方案。


OR 条件过多

WHERE 中包含多个 OR 条件,而这些字段没有统一索引或优化器判断代价太高时,MySQL 也可能放弃索引:

sql
SELECT * FROM wolf WHERE wolf_name = '影月' OR wolf_age = 20;

OR 会导致查询拆成多个分支,代价评估不理想时就会直接全表扫描。
这类情况通常通过拆分查询或使用 UNION ALL 优化。


隐式类型转换

当索引列与查询条件类型不一致时,MySQL 会进行隐式转换,这也会导致索引失效:

sql
-- 假设 wolf_age 是 INT
SELECT * FROM wolf WHERE wolf_age = '20';  -- 隐式类型转换,可能失效

一旦发生转换,索引列无法直接参与 B+ 树查找,查询退化为全表扫描。
这种问题在字符类型与数值类型混用时尤为隐蔽。


索引列选择性差

即使语法正确,有时候 MySQL 也会“主动放弃”使用索引。
当字段的区分度(选择性)太低时,例如“性别”、“是否启用”这类列,优化器判断使用索引的代价高于直接扫描全表,就不会走索引。

这种情况并不是语法错误,而是优化器的代价选择


小结重点

  • 最左匹配 是基础,索引列必须按定义顺序使用;
  • 避免在索引列上做函数、运算、隐式转换;
  • 不等式、OR、模糊匹配前缀都会削弱或破坏索引;
  • 索引字段选择性太低时,优化器可能主动放弃索引。

理解这些失效场景,有助于在面试和实际排查中迅速判断 SQL 是否真的“用了”索引,而不是只是建了索引。

读写分离(面试题整理)

当数据库的读请求远多于写请求时,单台数据库往往会成为性能瓶颈。
读写分离 是 MySQL 中非常常见的扩展方案,通过分担主库压力、提升整体吞吐量,在实际生产和面试场景中都属于高频考点。


核心思想

简单来说,读写分离就是:

  • 主库(Master)负责 写操作(INSERT、UPDATE、DELETE);
  • 从库(Slave)负责 读操作(SELECT);
  • 主从之间通过 复制机制 保持数据同步;
  • 业务通过中间件或路由层来区分读写请求。

这种架构最大的好处,是把压力从一台数据库分摊到多台机器,从而显著提升并发能力。


基本架构流程

  1. 应用把写请求发送到主库。
  2. 主库执行写入,并将变更记录写入 binlog
  3. 从库通过复制线程(I/O + SQL)实时或准实时同步 binlog
  4. 应用把读请求路由到从库执行。
text
[应用] → 写 → [主库] → binlog → [从库1]
                      → binlog → [从库2]
[应用] → 读 ←———————  [从库1/2]

主从之间的同步依赖于 binlog 日志和复制线程。只要同步延迟足够小,对业务而言,读写就像发生在一个数据库上。


读写分离的优势

  • 减轻主库的查询压力,提升系统整体吞吐量。
  • 从库可以横向扩展,灵活应对高并发读请求。
  • 主库专注写操作,从库专注读操作,角色清晰,便于运维。
  • 有利于实现高可用与负载均衡。

需要注意的同步延迟

虽然主从复制通常很快,但并不是完全实时。当主库写入刚完成,从库同步可能还没赶上,这时如果立刻在从库读,就会出现数据不一致

这种延迟导致的问题非常典型,比如:

  • 用户刚修改了资料,但刷新页面看到的还是旧数据;
  • 订单刚生成,在从库查询不到;
  • 并发下容易出现“读到旧数据”的错觉。

常见的处理方式:

  • 关键操作强制走主库,例如订单、支付类查询。
  • 加强复制机制,如半同步复制。
  • 对延迟不敏感的查询走从库,对一致性要求高的走主库。
  • 通过中间件做延迟感知,必要时自动切换。

架构实现方式

读写分离有多种实现方式,取决于系统规模和复杂度:

  • 应用层实现:在代码中手动区分读写库连接。适合中小型项目,灵活但耦合度高。
  • 中间件实现:使用如 MyCAT、ProxySQL、Atlas 等读写分离代理。对应用无侵入。
  • 负载均衡结合 DNS 或 VIP:大规模场景下可进一步提升容错能力。

与主从复制的关系

读写分离通常依赖主从复制实现。从本质上看:

  • 主从复制解决的是“数据分发”问题;
  • 读写分离解决的是“流量分担”问题。

复制是前提,分离是策略。它们是相辅相成的一对机制。


面试常问追击点

  • 如何保证主从一致性?
  • 从库延迟导致读到旧数据怎么办?
  • 什么时候必须强制走主库?
  • 是否所有业务都适合读写分离?

👉 优秀的回答不只是背定义,而是能说明:在实际系统中,哪些业务能容忍延迟,哪些不能,以及对应的架构取舍。

主从复制(面试题整理)

主从复制是 MySQL 架构中最核心的高可用与扩展基础之一。
它的作用是让一台主库(Master)的数据变化,自动同步到一台或多台从库(Slave),实现数据分发和负载分担,为读写分离和扩展提供支撑。


核心机制

MySQL 的主从复制依赖 binlog(二进制日志),它记录了主库上所有的增删改操作。从库通过读取并重放这些日志,达到与主库一致的状态。整个过程可以分为三个关键步骤:

  1. 主库写入 binlog
    当主库执行完写操作后,会将变更记录写入二进制日志 binlog
    这一步是整个复制的起点。
  2. 从库拉取 binlog
    从库上的 I/O 线程与主库建立连接,从主库读取 binlog 并写入本地的中继日志 relay log
    这一步是数据传输过程。
  3. 从库重放 relay log
    从库上的 SQL 线程读取中继日志,将日志内容“重放”到自己的数据库上,完成数据同步。
    这一步是数据落盘过程。

👉 整个复制链路相当于“写在主库 → 主库记日志 → 从库读日志 → 从库重放”。


复制模式

MySQL 提供了多种复制方式,适应不同业务一致性和性能的需求:

  • 异步复制(默认)
    主库写入 binlog 就算完成,立即向客户端返回成功,不等待从库确认。
    优点是性能好,缺点是延迟期间主从数据可能不一致。
  • 半同步复制
    主库在至少一台从库收到 binlog 并写入中继日志后,才向客户端返回成功。
    兼顾性能与一致性,是较为常用的方式。
  • 全同步复制
    所有从库都确认完成,主库才返回成功。
    一致性最强,但性能损耗也最大,实际中不常用。

👉 面试官常喜欢追问“主从延迟”时,半同步复制就是应对延迟不一致问题的一种方案。


主从复制的优点

  • 提升读性能:多台从库可以分担查询压力。
  • 增强容错性:主库故障时可以快速切换到从库。
  • 支持横向扩展:在业务增长时,通过增加从库提高吞吐量。
  • 为读写分离打下基础:复制机制是读写分离的技术前提。

常见问题与挑战

  • 主从延迟
    高并发写入时,从库可能会滞后,导致读到旧数据。
    处理方式:对一致性要求高的操作强制走主库、优化复制延迟、使用半同步。
  • 主库故障切换
    主从架构下要考虑主从切换的一致性和可用性,例如提升从库为主库时,如何确保数据完整。
  • 复制中断与重连
    网络抖动、磁盘写入异常都会导致同步中断,需要通过 SHOW SLAVE STATUS 等命令排查问题。

关键命令与监控

常用操作和排查手段包括:

sql
-- 查看主库 binlog 状态
SHOW MASTER STATUS;

-- 查看从库复制状态
SHOW SLAVE STATUS\G;

-- 启动复制
START SLAVE;

-- 停止复制
STOP SLAVE;

复制状态中 Seconds_Behind_Master 是衡量延迟的重要指标。
在高并发系统中,持续监控这个值至关重要。


面试追问方向

  • 主从复制的原理能否手画流程图?
  • 如何处理主从延迟带来的读不一致?
  • 什么情况下会出现复制中断?怎么排查?
  • 半同步和异步复制的差异是什么?

👉 掌握这些,不只是“背定义”,而是能对主从架构的可用性与一致性问题给出实际应对策略。

主从复制(面试题整理)

主从复制是 MySQL 实现高并发、高可用和读写分离的关键基础。
它通过把主库上的变更同步到从库,实现多节点的数据一致性,从而分摊压力、提升容错能力。


复制的核心机制

主从复制的本质,是主库把发生的所有数据变更操作记录在 binlog 中,从库通过复制线程读取并执行这份日志,从而保证主从数据同步。
整个过程可以拆解为三个阶段:

  1. 主库记录 binlog
    所有 DML(增删改)和 DDL 操作,都会被记录到二进制日志 binlog 中。
  2. 从库接收 binlog
    从库的 I/O 线程与主库建立连接,从主库读取 binlog,并保存到本地的中继日志 relay log。
  3. 从库回放日志
    从库的 SQL 线程持续读取 relay log,将其中的操作按顺序执行一遍,从而实现数据同步。

这个过程是持续、流式的,一旦主库有更新,从库就会不断追赶。


主从复制的类型

根据复制时机与反馈机制的不同,MySQL 的主从复制大致分为三类:

  • 异步复制
    主库只要把数据写入 binlog,就立刻返回成功,不关心从库是否同步完成。这是默认模式。
    优点是性能高,但从库延迟可能导致短暂的不一致。
  • 半同步复制
    主库写入 binlog 后,会等待至少一个从库接收并写入 relay log,再返回成功。
    相比异步复制,它牺牲了一部分性能,换来更强的数据一致性保障。
  • 全同步复制
    主库必须等待所有从库都同步完成才返回成功。
    一致性最强,但延迟也最高,在大多数业务场景中并不常用。

在面试中,能明确区分“异步”和“半同步”的差别,就是加分点。


主从复制的作用

主从复制不仅仅是读写分离的基础,还具有多重价值:

  • 提高读性能:通过从库分担读压力,缓解主库负载;
  • 增强容灾能力:主库宕机时可以快速切换到从库;
  • 支持数据备份:通过从库进行备份操作,不会影响主库性能;
  • 支撑分布式架构:作为后续分库分表和水平扩展的基础。

常见延迟与一致性问题

复制并非完全同步完成,因此从库可能会有延迟,造成短时间的数据不一致。
这种延迟来源于:

  • 主库写入速度过快,从库来不及同步;
  • 网络延迟造成 binlog 传输阻塞;
  • 从库执行 relay log 的效率不足;
  • 单线程 SQL 重放造成的积压。

这种延迟意味着,刚在主库完成的写操作,短时间内从库可能还查不到。
典型的表现是:“刚下单 → 查询订单 → 结果为空”。

常见的应对策略:

  • 关键查询强制走主库;
  • 调整复制模式(如启用半同步);
  • 优化从库执行效率,减少 SQL 延迟;
  • 对非强一致场景容忍短暂延迟。

面试追问方向

主从复制属于“基础中的重点”,面试官常常围绕它展开多层追问:

  • 复制的过程和原理是怎样的?
  • 异步和半同步的区别?
  • 如何处理主从延迟?
  • 主从切换时如何保证数据一致性?
  • 复制与读写分离的关系?

如果能把这几类问题回答得流畅,说明你不仅理解原理,还具备工程思维。

分库分表(面试题整理)

随着业务的发展,单台数据库往往难以支撑持续增长的并发与数据量。
当 SQL 优化、索引优化、缓存、读写分离这些手段都不再足够时,很多团队会走到同一条路上——分库分表
这一步意味着系统正式进入分布式数据库架构阶段,是面试中非常能体现工程能力的高频考点。


分库分表的核心目的

分库分表的根本出发点,是为了缓解单库或单表的性能瓶颈
当一张表动辄上千万甚至上亿行时,即便有索引,查询效率也会明显下降。
同时,单台数据库的 CPU、IO 和连接数也可能成为瓶颈。

通过分库分表,可以:

  • 降低单库单表数据量,提升查询性能;
  • 分摊并发压力,避免单点瓶颈;
  • 提高可扩展性,随着业务增长按需增加节点;
  • 减少维护风险,单点故障影响面更小。

换句话说,这是一种从垂直扩展转向水平扩展的手段。


分库与分表的区别

分库和分表常常一起出现,但它们的关注点并不完全相同:

  • 分表:将一张数据量巨大的表拆成多张结构相同的表。
    • 例如,把 wolf_mission 拆成 wolf_mission_0wolf_mission_1wolf_mission_2
    • 目的在于提升单次查询效率,减少索引深度和扫描范围。
  • 分库:将原本在一个数据库中的表,分散到多个数据库实例中。
    • 例如,将用户数据分散到 user_db_0user_db_1 两个库。
    • 目的在于缓解单机资源压力,分摊连接和磁盘 IO。

通常实际应用中,会同时使用分库和分表的组合策略,以达到最佳的性能提升效果。


拆分方式

在设计分库分表方案时,如何“拆”是核心问题。常见拆分策略有两类:

垂直拆分

  • 垂直分库:按照业务模块划分,把不同业务的表放到不同库中,例如用户库、订单库、日志库。
  • 垂直分表:将一个字段很多的表,按照功能拆分成多张表,例如基础信息表和扩展信息表分开。

这种拆分方式改动小、实现简单,但扩展性有限,适合初期快速拆分。

水平拆分

  • 水平分库/分表:把同一张表的数据按某种规则拆分到多张表或多个库中。
    例如根据 user_id % 4 把用户分散到 4 张表或 4 个库。

这种方式能显著分摊压力,扩展性强,但也引入更多复杂性,比如路由和聚合查询的问题。


拆分后的问题与挑战

分库分表并不是性能灵药,它带来的复杂性甚至远大于 SQL 优化。
面试中,如果能说清这些挑战,会让你的答案更具含金量。

路由与定位
拆分后,单个逻辑表对应多张物理表,系统必须有明确的路由规则来判断:

  • 这条 SQL 该打到哪个库?哪张表?
  • 是哈希路由、范围路由,还是通过中间件自动计算?

跨分片查询
如果查询的数据分布在多个库或表上,原本一条简单的 SELECT,就可能变成多个查询的并行执行与结果聚合。
例如,分页、排序、聚合(COUNT、SUM、MAX)都需要额外处理逻辑。

事务一致性
单库事务很容易处理,但跨库事务就复杂得多。
MySQL 并不原生支持分布式事务,需要额外引入两阶段提交(2PC)或本地消息表等方案。

ID 唯一性
分表后,主键不能再简单用自增 ID。
必须使用全局唯一 ID(如雪花算法、UUID、号段发号器等)来保证分片间不冲突。


中间件与工具

大多数中大型项目不会直接在业务代码里处理所有分库分表逻辑,而是引入中间件来降低复杂度,比如:

  • MyCAT
  • ShardingSphere
  • Vitess

这些中间件可以帮你自动完成分片路由、结果归并、分布式事务等操作,业务层几乎不感知数据库拆分的存在。


适用与取舍

分库分表是一种强手段,但不适合所有系统。
一般而言,只有在数据量和并发量确实达到瓶颈,其他优化手段(索引、缓存、读写分离)都不足以支撑时,才会考虑这一步。

同时,设计初期也要谨慎预估拆分的复杂度,不要为了“看起来高大上”而盲目上分库分表
它的代价不仅在技术实现上,还包括后续的维护、测试和故障排查。


面试追问方向

面试官常常会顺着分库分表往下问:

  • 你们是按照什么规则分的?哈希还是范围?
  • 如何做跨库的分页和统计?
  • 分库分表后,主键怎么保证唯一?
  • 怎么处理分布式事务?
  • 为什么选择中间件,而不是自己实现?

能在这些问题上回答得清楚、有逻辑,通常就意味着你不仅知道“是什么”,还理解“为什么”和“怎么做”。

什么是索引(面试题整理)

在 MySQL 中,索引是提升查询效率最直接、最核心的手段。
没有索引时,数据库只能一行一行地扫描表来定位结果;有了索引,就可以像查字典一样快速定位数据。
理解索引不仅仅是知道它“加快查询”,更要明白它是如何加快查询的。


索引的本质

索引是一个数据结构,用于加速数据的查找。
它的作用就像一本字典的目录页:通过目录可以快速找到单词所在的页码,而不必从第一页翻到最后一页。

在 MySQL 中,大多数索引底层使用 B+ 树 实现(也有哈希、全文等特殊索引类型),B+ 树能在海量数据中通过极少的 IO 定位到目标记录,效率远高于顺序扫描整张表。


索引的分类

索引并非只有一种类型,实际开发中根据目的不同,会使用不同的索引类型来达到最优效果。

  • 主键索引(Primary Key)
    每张表只能有一个主键索引,且不允许重复或为空。
    主键索引本身就是一个聚簇索引,叶子节点存储实际数据。
    因为主键是唯一标识,主键索引的查询效率通常是最高的。
  • 唯一索引(Unique Index)
    不允许索引列出现重复值,但允许为空。
    常用于用户 ID、邮箱、身份证号等唯一性字段。
  • 普通索引(Normal Index)
    最基础的索引类型,不对列的值做唯一性约束。
    多用于频繁查询但不要求唯一的场景,比如昵称、分类名。
  • 复合索引(Composite Index)
    由多个列组合而成。
    它不仅能加速多列联合查询,还能利用最左匹配规则来优化单列查询。
  • 全文索引(Fulltext Index)
    用于大文本字段的全文检索,适合搜索引擎类的场景。
    LIKE '%keyword%' 不同,全文索引通过分词提高检索效率。
  • 哈希索引(Hash Index)
    适合等值查询,查找速度极快,但不支持范围查询。
    在 MySQL 中,Memory 引擎默认使用哈希索引。

索引的工作方式

当执行 SELECT 语句时,MySQL 会先判断查询条件能否利用索引:

  • 如果能,就通过 B+ 树直接找到目标记录的存储位置;
  • 如果是主键索引,直接返回数据;
  • 如果是二级索引,会先找到主键值,再通过主键索引“回表”获取完整记录;
  • 如果不能利用索引,只能全表扫描。

这就是为什么在优化 SQL 时,除了“建索引”,还必须关注查询语句本身能否“命中索引”。


索引的优势与代价

索引能显著提升查询效率,但它并非没有代价。设计索引时必须在性能与资源之间权衡。

优势

  • 加快数据检索速度;
  • 减少磁盘 IO,提升整体查询性能;
  • 对排序、分组、范围查询有明显优化效果;
  • 唯一索引可以保证数据完整性。

代价

  • 索引本身会占用额外的存储空间;
  • 索引越多,写入和更新的成本越高;
  • 不恰当的索引可能反而拖慢性能。

这也是为什么很多项目里并不是“能建就建”,而是精确挑选合适的字段建立索引。


设计索引的原则

想让索引真正发挥作用,设计阶段的选择至关重要。实际项目中,往往遵循以下几个基本原则:

  • 高频查询的字段建立索引,而不是随便加;
  • 选择区分度高的列(如 ID),避免对性别这种低选择性的字段单独建索引;
  • 对多列查询,优先考虑复合索引;
  • 尽可能利用覆盖索引,减少回表操作;
  • 避免给频繁更新的字段建索引;
  • 结合 EXPLAIN 分析执行计划,确保索引真正被使用。

索引与查询性能

很多时候,索引的作用并不是让查询“更快一点”,而是决定查询能否在高并发场景下稳定运行
一条等值查询如果命中主键索引,查找路径可能只需 3 次磁盘 IO;
但如果索引失效,就可能变成几万次扫描。

索引的有无,往往是大规模系统性能的分水岭。


索引与其他优化手段的关系

索引并不是孤立存在的,它与其他数据库优化手段是相辅相成的:

  • 与 SQL 改写配合,才能确保查询真正走上索引路径;
  • 与读写分离结合,减轻主库压力;
  • 与分库分表结合,帮助快速定位分片中的数据;
  • 与缓存结合,进一步降低数据库压力。

索引是一切优化的起点,也是一切优化的基石。

什么情况下适合建索引(面试题整理)

建立索引是提升查询性能最常用也最有效的手段之一,但并不是所有字段都适合建立索引。
索引不仅会加快查询,也会带来空间和写入成本。
因此在 MySQL 中,是否建立索引,不是“越多越好”,而是要基于查询场景和数据特性来判断。


高频出现在查询条件中的字段

索引的核心价值在于加速定位
如果某个字段经常出现在 WHEREORDER BYGROUP BYJOIN 的条件中,它就是天然的索引候选。

比如用户系统中,user_idcreate_time 通常会频繁参与筛选或排序,对这样的字段建索引,能显著减少扫描量。
相反,如果一个字段几乎不参与查询,仅仅是冗余信息,那么索引就是资源浪费。


区分度高的字段

所谓“区分度高”,就是字段值的重复率低,比如主键、订单号、手机号、邮箱。
B+ 树索引的查找效率取决于它能多快缩小搜索范围。
如果字段值的唯一性强,每次查询能快速定位,索引的收益就非常高。

而如果字段取值只有两三个,比如“性别”、“是否启用”,即便建了索引,MySQL 优化器也可能认为全表扫描更划算,从而放弃使用索引。


参与多表关联的字段

当表与表之间通过某个字段进行 JOIN 操作时,对这个关联字段建立索引,可以让连接的代价大幅下降。
在多表查询中,能否快速定位匹配记录,往往就是性能的关键。
如果不建索引,JOIN 可能变成一次次嵌套循环,导致性能急剧下降。


排序和分组频繁的字段

如果查询经常对某个字段进行 ORDER BYGROUP BY 操作,对它建立合适的索引能避免排序时的额外扫描和临时表创建。
MySQL 的优化器会优先使用索引顺序来完成排序,这类“顺序访问”比无序扫描快得多。

例如一张日志表中,create_time 字段通常就非常适合建立索引,以便快速获取时间区间内的记录或按时间排序。


长查询和大表查询中的关键字段

当表的数据量很大时,任何一次查询的代价都会被放大。
对于这种大表,命中索引与否往往就是性能的生死线。

在这种场景下,对经常用于筛选的字段提前建立索引,可以避免成千上万行的无效扫描,显著降低延迟。
尤其是用户 ID、订单号、时间戳这类字段,在大表中基本属于“必须索引”。


适合建立组合索引的场景

当查询经常同时对多个字段进行过滤时,比起单独给每个字段建索引,更推荐建立复合索引,利用最左匹配原则提高命中率。

例如:

sql
SELECT * FROM orders
WHERE user_id = 1001 AND create_time BETWEEN '2025-01-01' AND '2025-01-31';

这种情况下建 (user_id, create_time) 的复合索引往往比两个单列索引更高效,因为它能一次性缩小范围,无需额外的索引合并开销。


不适合建立索引的情况

明确知道哪些字段不适合建索引,往往比盲目加索引更重要。
有些场景看似可以建索引,但收益很小,甚至会拖慢写入速度:

  • 字段取值过少(例如性别、是否启用等布尔字段)
  • 更新非常频繁的字段(索引更新的成本不容忽视)
  • 不参与查询、排序、分组、连接的字段
  • 大字段(如 TEXT、BLOB)不适合直接建普通索引

另外,即使建了索引,如果查询无法命中(比如违反最左匹配、出现函数计算),也等于白建。


索引的性价比原则

索引并不是越多越好,每一个索引都在加速查询拖慢写入之间寻找平衡。
合适的索引应当满足以下特征之一:

  • 能显著减少扫描的数据行数;
  • 能支撑高频查询场景;
  • 能配合排序、分组、关联等优化执行计划;
  • 能长期保持高命中率,而不是“理论上可用”。

一套好的索引策略,往往是在业务数据特性、查询习惯和性能之间反复权衡出来的。

B+ 树叶子节点存储的内容(面试题整理)

MySQL 使用 B+ 树 作为主要索引结构,而它的真正数据恰恰就藏在叶子节点中。
理解叶子节点存储的内容,是彻底搞清楚“为什么 MySQL 索引这么高效”的关键一步。


B+ 树的分层结构

B+ 树是一棵多路平衡查找树,它的节点分为两类:

  • 非叶子节点:只存储索引键和指向下层节点的指针,不存放实际数据;
  • 叶子节点:存储索引对应的数据或数据的指针,是查找的最终落点。

非叶子节点像目录,帮助你快速定位到目标范围;叶子节点才是“真正的仓库”。


聚簇索引(主键索引)叶子节点

当你在一张 InnoDB 表中使用主键索引时,B+ 树的叶子节点存放的是整行数据
也就是说,主键索引本身就是表数据的物理组织结构。

例如在一张 wolf 表中:

sql
CREATE TABLE wolf (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50),
    age INT
) ENGINE = InnoDB;

主键索引的叶子节点结构大致如下:

键(id)值(整行数据)
1(id=1, name='影月', age=5)
2(id=2, name='落雪', age=3)

查找主键时,B+ 树的路径搜索到叶子节点的同时,也就完成了数据定位,不需要二次查询
这就是为什么主键查询往往是 InnoDB 中效率最高的一种查询方式。


非聚簇索引(二级索引)叶子节点

如果对非主键字段(比如 name)建立了索引,那么叶子节点就不再存整行数据,而是存储:

  • 索引键(name
  • 对应的主键值(id

结构大致如下:

键(name)值(主键 id)
影月1
落雪2

这种索引也叫“二级索引”。
当你使用 WHERE name = '影月' 查询时,MySQL 会先通过二级索引定位到主键 ID,然后再通过主键索引去叶子节点取出整行数据。
这个过程叫做“回表”。


为什么主键设计很重要

因为非聚簇索引的叶子节点里存的是主键值,主键越大、越复杂,所有二级索引的体积也会随之变大
体积变大,就意味着树高增加、磁盘 IO 变多、缓存效率变低。
这就是为什么实际项目中主键常常设计为一个短小的数值型字段,比如自增 ID 或雪花 ID。


叶子节点与区间查询

B+ 树叶子节点还有一个关键特性:它们之间通过指针串联,形成一个有序链表
这意味着一旦定位到某个范围的起点,后续的范围查询不需要重新从根节点开始搜索,而是沿着叶子节点链表顺序扫描即可。

这也是为什么 B+ 树特别适合范围查询,比如:

sql
SELECT * FROM wolf WHERE id BETWEEN 100 AND 200;

在叶子节点找到 100 后,沿着链表往后扫到 200,效率极高。


叶子节点与“覆盖索引”

有些查询不一定要真的“回表”才能拿到结果。
如果查询中请求的所有字段都能在二级索引的叶子节点中找到,就可以直接返回结果,而不必再通过主键查一次。
这种情况叫“覆盖索引”。

例如:

sql
SELECT name FROM wolf WHERE name = '影月';

因为 name 本身就在二级索引的叶子节点里,所以无需回表。
覆盖索引能显著减少 IO,是 SQL 优化的常用技巧之一。


叶子节点的意义

理解叶子节点,几乎就理解了 MySQL 索引的工作核心:

  • 主键索引的叶子节点存放的是整行数据;
  • 二级索引的叶子节点存放的是主键值;
  • 主键设计会直接影响二级索引的大小和性能;
  • 叶子节点链表让范围查询高效;
  • 覆盖索引可以省掉回表的成本。

这些机制结合在一起,使 B+ 树在海量数据场景下依然能保持非常稳定的查询性能。

MySQL 日志:redo log、binlog、undo log 的区别与作用(面试题整理)

在 MySQL 的底层架构中,日志系统扮演着至关重要的角色。
它不仅决定了事务的可靠性与一致性,也直接影响数据库的性能表现。
其中最核心的三类日志是:redo logbinlogundo log
搞清楚它们的职责与配合关系,是掌握事务原理和故障恢复的关键。


redo log:崩溃恢复的保障

redo log 是 InnoDB 存储引擎独有的日志,主要用于实现崩溃恢复
它记录的是数据页的物理修改,也就是“数据库被改成了什么样子”。

比如执行:

sql
UPDATE wolf SET age = 5 WHERE id = 1;

InnoDB 会先把这次修改写到 redo log 中,然后再异步刷新到磁盘上的数据页。
即便在修改还没真正落盘时数据库突然崩溃,重启后也能根据 redo log 把修改“重做”一遍,恢复数据一致性。

这种“先写日志,后写数据”的方式被称为 WAL(Write Ahead Logging)
它能显著提升写入性能,同时保证事务的持久性(ACID 中的 D)。

redo log 的特征非常明确:

  • 属于 InnoDB 引擎层;
  • 记录物理变化,写入高效;
  • 大小固定(循环写),不会无限增长;
  • 是数据库异常宕机后“恢复”的依据。

binlog:数据同步与审计的依据

binlog 是 MySQL Server 层的二进制日志,记录的是逻辑操作,也就是“执行了什么语句”。
无论底层使用哪种存储引擎,只要发生了 DML 或 DDL,MySQL 都会将操作以事件的形式写入 binlog。

例如执行上面的 UPDATE 语句,binlog 会记录一条“id=1 的 age 从 3 改为 5”的事件。
binlog 不仅用于审计与回溯,更是主从复制的核心基础。

binlog 的特征:

  • 属于 Server 层,全局统一;
  • 记录逻辑操作,便于数据同步和恢复;
  • 会持续追加,不会像 redo log 那样覆盖;
  • 常用于主从复制、增量备份、审计追踪。

在主从复制中,从库就是通过不断拉取主库的 binlog 来重放操作,实现同步的。


undo log:事务回滚的关键

undo log 是 InnoDB 用来实现事务回滚MVCC(多版本并发控制) 的日志。
它记录的是数据修改前的旧值,这样当事务失败或者被回滚时,可以利用 undo log 把数据恢复到修改前的状态。

例如:

sql
UPDATE wolf SET age = 5 WHERE id = 1;

事务开始前,age=3
执行更新时,InnoDB 会在 undo log 中记录“id=1, age=3”的旧值。
如果事务回滚,undo log 就会把这条记录还原回 3。

undo log 不仅支撑事务的回滚,还支持 MVCC 中“快照读”功能,让并发事务之间可以互不干扰地读取历史版本。

undo log 的特征:

  • 属于 InnoDB 引擎层;
  • 记录数据修改前的旧值;
  • 主要用于事务回滚与 MVCC;
  • 生命周期与事务绑定,事务提交后可能被清理。

三者的关系与执行顺序

在事务执行过程中,这三类日志是协同工作的:

  1. 执行 SQL 时,先写 undo log(记录旧值),以便回滚;
  2. 然后修改内存数据页(Buffer Pool);
  3. 再写 redo log(记录物理变更);
  4. 提交事务时写 binlog(记录逻辑操作);
  5. binlog 和 redo log 共同保证数据一致性,支持崩溃恢复和主从同步。

这里有一个关键点:redo log 和 binlog 需要通过“两阶段提交”保证一致性,避免出现主从不一致或崩溃恢复后状态不同步的情况。


为什么要有两种不同的日志(redo log 与 binlog)

很多人第一次接触时都会问一个问题:“既然 binlog 已经记录了操作,为什么还需要 redo log?”

二者的分工其实非常清晰:

  • redo log 负责快速恢复崩溃前的状态,提升写入性能;
  • binlog 负责记录全量操作,方便数据同步与备份。

redo log 是面向性能和本地一致性的;
binlog 是面向数据复制和历史追溯的。
二者结合,既能保证事务可靠落盘,也能支持主从架构和容灾备份。


实际应用场景

  • redo log
    • 数据库异常重启后恢复未刷盘的数据;
    • 事务提交前写入 redo log,提升写性能。
  • binlog
    • 主从复制、容灾、增量备份、审计;
    • 回放 binlog 可以重建数据变化过程。
  • undo log
    • 事务回滚、并发一致性控制(MVCC);
    • 实现隔离级别中的“可重复读”和“读已提交”。

总结区别

项目redo logbinlogundo log
所属层级InnoDB 引擎MySQL Server 层InnoDB 引擎
记录内容物理修改逻辑操作修改前的旧值
作用崩溃恢复、持久性保证主从复制、备份、追溯回滚、MVCC
生命周期循环写追加写随事务结束可能清理
使用场景提升写性能、故障恢复数据同步与容灾事务一致性与隔离性

redo log 是恢复,binlog 是复制,undo log 是回滚。
三者各司其职,又共同构成了 MySQL 事务与高可用的底层基石。

MySQL 锁(面试题整理)

在 InnoDB 里,锁的设计既要保证并发下的数据正确性,也要尽量减少互相阻塞。理解“有哪些锁”“何时加锁”“如何排查与优化”,是面试与实战都会反复落脚的主线。


锁都有哪些:读/写锁、意向锁、行锁/表锁、间隙与临键

  • 读锁 / 写锁(S/X):读锁允许并发读但阻塞写,写锁会阻塞读与写。SELECT … LOCK IN SHARE MODE 会显式加读锁,SELECT … FOR UPDATE 会显式加写锁。
  • 意向锁(IS/IX):表级的“标识”锁,用来标记表内已有行级锁,方便更快判定表锁是否冲突,不需要逐行检查。
  • 行锁 vs 表锁:InnoDB 以行锁为主;MyISAM 针对读写分别加表锁。并发场景下,InnoDB 的行级锁整体吞吐更高。
  • 间隙锁 / 临键锁:在 RR(可重复读) 下为阻止幻读而生。间隙锁锁住“两个值之间的空隙”;临键锁是“行锁 + 间隙锁”的组合。

InnoDB 为什么“偏爱索引”:行锁是“锁索引项”

InnoDB 的行锁是“给索引记录加锁”;只有走索引才能使用行级锁,否则可能退化为表锁。这也是很多“本应不冲突却被锁住”的根源。

关键要领

  • 让检索命中索引,锁的粒度才会保持在“行”;
  • 条件不走索引或类型不匹配,容易放大锁范围,甚至升级为表锁。

RR 下的“看不见的守卫”:间隙锁如何避免幻读

在 RR 级别下,InnoDB 会对不存在的记录边界加间隙锁。例如当某会话锁住 (10,20) 区间内的“空隙”,其他会话无法在该区间插入新行,于是避免了同一事务前后两次读到“新增的行”(幻读)。


怎么看系统“被锁住了”:排查与定位

  • 先看行锁争用指标show status like 'innodb_row_lock%'; 聚焦 *_waits / *_time / *_time_avg 三个量。
  • 再查谁在锁、锁了谁:8.0 以前可查看 INFORMATION_SCHEMA.INNODB_TRX/LOCKS/LOCK_WAITS,8.0 之后使用 performance_schema.data_locks / data_lock_waits
  • 最后读引擎状态SHOW ENGINE INNODB STATUS; 能输出近期死锁与等待明细。

死锁并不可怕:让系统自己判、必要时手动解

MySQL 大多数情况下会自动检测死锁并回滚其中一个事务;极端场景下,可依据日志找到线程并 KILL 之。


实战握法:把锁的“副作用”降到最低

  • 尽量走索引,缩小加锁范围,避免无索引访问触发行锁 → 表锁。
  • 收紧检索条件,少扫区间,减少间隙锁命中面。
  • 控制事务粒度与顺序:事务尽量短,小心把“加锁语句”置于事务尾部,减少持锁时间与冲突窗口。
  • 根据业务选择隔离级别:能用更低隔离级别就不要滥用 RR,以减少间隙锁带来的阻塞面。

一句话把握主线

InnoDB 靠索引项加锁实现高并发下的行级控制;在 RR 下再借助间隙/临键锁挡住“幻读”;当出现阻塞或死锁,用状态/系统表/引擎状态三板斧定位与处置;而所有优化的本质都是——让检索命中索引,缩小锁的范围与时间

MySQL 锁(面试题整理)

MySQL 的锁机制,是它在高并发场景下保持数据一致性和事务隔离的核心。理解锁的类型、触发时机以及对性能的影响,是掌握事务和优化查询性能的基础。


锁的基本分类:读锁与写锁

**读锁(共享锁)**允许多个事务同时读取同一份数据,但会阻塞写操作。
**写锁(排他锁)**则会阻塞其他事务的读和写操作,只有加锁的事务能修改这部分数据。

  • MyISAM 在 SELECT 时会自动加读锁,INSERT/UPDATE/DELETE 会自动加写锁;
  • InnoDB 在普通 SELECT 时默认不加锁,只有执行 SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE 才会显式加锁。

锁粒度越小,并发性能越高,但锁管理的开销也随之增加。


表锁与行锁

  • 表锁:一次性锁住整张表,开销小、加锁快,但并发度最低。
    MyISAM 主要使用表锁,适合读多写少的场景。
  • 行锁:只锁定命中的记录,开销较大,但能支持更高的并发访问。
    InnoDB 使用行级锁,依赖索引精确定位数据,才能发挥行锁优势。

如果查询未命中索引或索引失效,行锁可能退化为表锁,这是性能下降的常见原因之一。


意向锁:协调表锁与行锁的“标记”

InnoDB 引入 意向锁(Intention Lock) 来解决“表锁和行锁并存时的冲突判断”问题。
当一个事务要给某行加锁时,InnoDB 会先在表级加一个意向锁(IS 或 IX),用来标记“这张表里有行被加锁”,从而避免遍历整表来判断冲突。

  • IS(意向共享锁):标记有行被加共享锁;
  • IX(意向排他锁):标记有行被加排他锁。

这种机制让大表在高并发下的锁冲突判断更高效。


间隙锁与临键锁

间隙锁(Gap Lock) 是 InnoDB 在 可重复读(RR) 隔离级别下的一种特殊锁,锁住的是“两个值之间的空隙”。
它的作用是防止其他事务在这个空隙中插入数据,从而解决幻读问题。

例如:

  • 如果当前事务锁定了 (10, 20) 区间,那么其他事务无法在这个区间内插入新行;
  • 但间隙锁不锁定边界值本身。

临键锁(Next-Key Lock) 则是 行锁 + 间隙锁 的组合。
它同时锁定目标行和相邻间隙,是 InnoDB RR 模式下加锁的默认策略。

这意味着即使查询锁定的是某一行,实际上锁住的范围可能会更大。


锁等待与死锁

锁的存在不可避免带来等待,在竞争激烈时甚至可能出现死锁

排查锁等待可以从三个方向入手:

  • 查看行锁状态:
    sql
    SHOW STATUS LIKE 'innodb_row_lock%';
    关注等待次数、等待总时长、平均等待时间等指标。
  • 查询锁信息:
    sql
    SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
    8.0 之后改用 performance_schema.data_locksdata_lock_waits
  • 查看引擎状态:
    sql
    SHOW ENGINE INNODB STATUS;
    可定位最近的死锁详细信息。

MySQL 大多数情况下会自动检测死锁,并主动回滚其中一个事务。如果无法自动检测,可以根据日志找到事务线程 ID 并手动 KILL


锁优化的核心策略

  • 尽量让查询命中索引,保证使用行锁,避免退化为表锁。
  • 收紧检索范围,减少间隙锁覆盖区间。
  • 缩短事务时间,降低锁持有时间,减轻竞争。
  • 避免不必要的 RR 隔离级别,在能满足业务需求的情况下,使用 RC 降低间隙锁的干扰。
  • 提前统一加锁顺序,降低死锁概率。

这些策略的本质,都是为了减小锁的粒度、缩短锁的时间、减少锁冲突,让并发性能最大化。


总结主线

MySQL 的锁机制并不只是“加不加锁”这么简单。
它是一整套从 读/写锁 → 行/表锁 → 意向锁 → 间隙与临键锁 → 死锁处理 的多层控制体系:

  • 行锁依赖索引;
  • 间隙锁避免幻读;
  • 意向锁协调冲突;
  • 死锁有检测与处理机制;
  • 优化目标是控制锁范围与时间,提高并发效率。

理解这些原理,是写出高性能 SQL 和稳定事务逻辑的基础。

MVCC(多版本并发控制)(面试题整理)

在 MySQL 的 InnoDB 引擎中,MVCC 是支撑高并发性能的核心机制之一。
它的全称是 Multi-Version Concurrency Control,直译就是“多版本并发控制”。

MVCC 的目标很直接:
👉 让读操作不被写阻塞,也不去阻塞写;
👉 让不同事务在同一时刻看到“各自版本”的数据;
👉 降低加锁频率,提升并发能力。


MVCC 的作用

如果没有 MVCC,事务之间就只能靠加锁来实现隔离。
比如 A 事务在更新一行数据时,B 事务想读取同一行,就得等待 A 提交。这样一来,即便是频繁的只读查询也会因为锁而阻塞。

MVCC 的出现,就是为了避免这种“所有并发都靠锁”的笨办法。
它让读取操作在大多数情况下 无需加锁,也能读取到正确的数据版本,从而大幅提升系统吞吐量。


MVCC 的基础:隐藏字段 + Undo Log + Read View

InnoDB 在每一行记录的物理结构中,都会自动维护两个隐藏字段:

  • trx_id:最近一次修改这行记录的事务 ID;
  • roll_pointer:指向 Undo Log(撤销日志)的指针,用来追溯历史版本。

而 Undo Log 本身就保存了“修改前的数据”,只要顺着 roll_pointer 往回查,就能找到这条记录在不同时刻的历史版本。

此外,还有一个关键组件叫 Read View
它是在事务执行时拍下的一张“活跃事务快照”,记录了当前有哪些事务还没提交。
当事务读取一行记录时,会根据 Read View 判断它该看到哪一个版本。


MVCC 的读取逻辑(以 RR 隔离级别为例)

当事务执行一条普通的 SELECT(快照读)时,InnoDB 并不会给记录加锁,而是按照以下逻辑读取:

  1. 拿到当前事务的 Read View;

  2. 读取目标记录的最新版本;

  3. 检查记录的 trx_id 是否在 Read View 中:

    • 如果小于 Read View 的低水位,说明这个版本在事务开始前就提交了,可以直接读;
    • 如果在活跃事务列表中,说明还没提交,不可见;
    • 如果大于高水位,说明是事务开始之后产生的新版本,不可见。
  4. 如果当前版本不可见,就根据 roll_pointer 追溯 Undo Log,找上一版本继续判断;

  5. 直到找到一个可见版本为止。

整个过程中,不需要加锁,读取的是“自己能看到的那个版本”,也就是“快照”。


MVCC 与“当前读”的区别

MVCC 并不是对所有查询都生效的。
它只作用于快照读,即普通的 SELECT

而以下语句属于“当前读”:

  • SELECT ... FOR UPDATE
  • SELECT ... LOCK IN SHARE MODE
  • UPDATE / DELETE / INSERT

当前读需要保证读到的是“最新版本”,同时防止其他事务修改,因此会加锁,不使用 MVCC 的多版本机制。

类型示例是否使用 MVCC
快照读SELECT * FROM wolf✅ 使用
当前读SELECT ... FOR UPDATE❌ 不使用

MVCC 与事务隔离级别的关系

MVCC 能实现 RC(读已提交)和 RR(可重复读)这两种隔离级别。

  • RC(Read Committed):每次 SELECT 都会重新生成一个 Read View。
    所以在同一个事务中,可能两次查询读到的结果不一样(别的事务提交了)。
  • RR(Repeatable Read):事务开始时生成 Read View,并一直沿用。
    所以同一事务中多次读取的结果一致。

Serializable 隔离级别则会加锁,不再依赖 MVCC。
Read Uncommitted 则不需要 MVCC,因为直接读取未提交的数据。


为什么 MVCC 能提升性能

  1. 读不阻塞写:快照读不需要加锁,因此不会阻塞更新事务;
  2. 写不阻塞读:更新事务生成新版本,不会影响已经拍好 Read View 的其他事务;
  3. 高并发友好:大多数查询都是读操作,MVCC 让它们几乎“无锁执行”,吞吐量显著提高;
  4. 避免大量锁竞争:锁冲突减少,也降低了死锁的概率。

MVCC 的清理

Undo Log 不能无限增长。
当没有事务再依赖某个旧版本时,InnoDB 会清理这些 Undo Log,释放空间。
这种机制确保 MVCC 的版本链不会无限变长。


核心总结

  • MVCC 通过 隐藏字段 + Undo Log + Read View 实现“读写不冲突”;
  • 仅作用于快照读,当前读仍然需要加锁;
  • RC 每次查询新建 Read View,RR 只在事务开始时创建;
  • 它是 InnoDB 能在高并发场景中稳定运行的底层支撑。

换句话说,MVCC 让 MySQL 在大多数情况下能做到“无锁读”,这就是它的威力。

MVCC 解决的问题(面试题整理)

MySQL 的 InnoDB 通过 MVCC(Multi-Version Concurrency Control,多版本并发控制),在高并发事务场景下解决了两个核心矛盾:

  • 读与写的并发冲突
  • 高并发场景下的锁竞争与性能瓶颈

这两类问题在传统的“锁式并发控制”中往往很棘手,而 MVCC 的多版本机制很好地化解了它们。


1. 解决“读写互相阻塞”的问题

如果没有 MVCC,当一个事务正在更新一行数据时,其他事务读取这行数据就必须等待,反之亦然。
这种基于“排他锁 / 共享锁”的方式在高并发场景下会导致大量线程阻塞,整体吞吐量下降。

MVCC 的做法是——让读操作不加锁,而是读取该行数据的“历史版本”。

例如:

  • 事务 A 开始时读取 id=1 的记录,看到的是 age=10;
  • 事务 B 随后将 id=1 的 age 更新为 20,但事务 A 的快照里依然能看到 10;
  • A 不用等待 B 提交,也不用加锁。

👉 这意味着读写可以同时进行,互不干扰。


2. 避免不必要的加锁,降低锁竞争

传统的并发控制通过锁来实现一致性:写锁阻塞读,读锁阻塞写。
在高并发系统中,频繁的锁等待会导致:

  • QPS 急剧下降;
  • 阻塞链条加长;
  • 死锁概率升高。

MVCC 则通过 多版本 + 快照读 避免了这种情况:

  • 普通 SELECT 不加锁,直接读取事务开始时的“可见版本”;
  • 写操作创建新版本,不会影响正在读取旧版本的事务。

这就把“冲突”变成了“版本共存”,极大提高了并发性能。


3. 保证读一致性(尤其是在 RR 隔离级别下)

MVCC 还能解决“读不一致”的问题。
在 RR(可重复读)隔离级别下,事务在开始时生成一个 Read View,在整个事务期间反复读取同一行数据时,看到的版本始终一致,即便其他事务已经提交了更新。

例如:

  • A 事务开始时读取 id=1,看到 age=10;
  • B 事务更新 age=20 并提交;
  • A 事务再次读取 id=1,仍然看到 age=10。

👉 这就是“可重复读”能力,也是 RR 隔离级别最重要的特性之一。


4. 减少死锁概率

锁冲突越多,死锁概率越高。
MVCC 通过减少读操作上的锁依赖,大大缩短了事务之间的“等待链”,也就降低了死锁发生的可能性。
虽然写写冲突仍可能导致死锁,但读写之间大多已被“版本隔离”消解掉了。


5. 提高系统整体吞吐量

MVCC 让大多数读操作不必等待,也不必持有锁,这意味着数据库可以同时处理更多的事务请求:

  • 读操作快速完成,不阻塞;
  • 写操作专注生成新版本,不用协调一堆等待中的读;
  • 系统能承载更高的并发访问量。

这就是为什么现代数据库(不仅仅是 MySQL,PostgreSQL 也一样)几乎都使用 MVCC 作为主力并发控制机制的原因。


🐺 一句话总结

MVCC 不是锦上添花的“优化”,而是支撑高并发数据库的底层基础设施
它解决的问题本质上有四个:

  • ✅ 让读写不再互相阻塞;
  • ✅ 降低锁竞争,避免性能崩溃;
  • ✅ 实现一致性读,支撑 RR 隔离级别;
  • ✅ 减少死锁,提升系统吞吐量。

没有 MVCC,高并发事务系统将不得不依赖大量锁,性能与稳定性都会大打折扣。

MySQL 执行流程(面试题整理)

MySQL 的 SQL 执行过程并不是简单地“接收 SQL → 执行 → 返回结果”这么直白。
在内部,它其实经过了连接、解析、优化、执行等多个环节。
每个环节都可能成为性能瓶颈或调优切入点。

理解 MySQL 执行流程,能帮助你真正看懂执行计划、解释慢查询,也能在面试中展示对底层机制的扎实掌握。


1. 连接器(Connection)

当客户端发起请求时,首先经过的是 连接器
它负责:

  • 验证用户名和密码;
  • 校验权限;
  • 建立连接;
  • 为这条连接分配资源(线程、内存)。

MySQL 支持短连接和长连接:

  • 短连接:一次请求后断开连接,适合低并发。
  • 长连接:多次请求复用同一连接,减少频繁握手的开销。

⚠️ 但长连接也会导致内存占用不断增加,必要时需要 mysql_reset_connection 清理状态。


2. 查询缓存(Query Cache)(已废弃)

在 MySQL 5.7 及更早版本中,执行 SQL 之前会先查查询缓存,判断是否有完全相同的 SQL 结果可以直接返回。

不过因为缓存失效机制复杂、更新成本高,在高并发场景下反而会拖慢性能,所以在 MySQL 8.0 中已彻底移除这一模块。
现代系统更倾向于在 业务层或缓存中间件(如 Redis)做缓存。


3. 解析器(Parser)

解析器负责语法分析与语义分析

  • 词法分析:把 SQL 拆解为关键字、标识符、运算符等基本单元;
  • 语法分析:判断 SQL 是否符合语法规则;
  • 语义分析:检查表名、列名、权限等是否正确存在。

如果 SQL 语法错误,就是在这个阶段报出来的。

解析完成后,MySQL 会生成一棵“解析树”,交给优化器进一步处理。


4. 预处理器(Preprocessor)

预处理器会对解析树进行进一步检查,例如:

  • 表名和列名是否唯一;
  • 权限是否满足;
  • 视图、子查询的展开;
  • 处理 * 通配符。

这一步的目的,是保证进入优化器的查询树逻辑正确且清晰


5. 优化器(Optimizer)

优化器决定怎么执行这条 SQL,也就是选择最优的执行计划。

在这里,MySQL 会:

  • 确定访问路径(是否走索引、走哪个索引);
  • 确定连接顺序(多表 join 的执行顺序);
  • 优化 where 条件、子查询、排序、分组等操作;
  • 根据统计信息计算代价,选择代价最小的方案。

举个例子:

sql
SELECT * FROM wolf WHERE age = 5 AND name = '影月';

优化器可能选择 age 索引,也可能选择 name 索引,或者索引合并,具体取决于执行代价估算。

👉 这一步直接决定了 SQL 的执行效率。


6. 执行器(Executor)

优化器确定好执行计划后,执行器根据计划真正去操作存储引擎

  • 调用存储引擎接口;
  • 读取或写入数据;
  • 根据权限再检查是否可访问;
  • 把结果集返回给客户端。

执行器并不直接操作磁盘,而是通过存储引擎完成所有底层读写。


7. 存储引擎(InnoDB)

MySQL 是 Server 层 + 存储引擎层的结构。
Server 层处理 SQL 的解析、优化、权限;
存储引擎负责数据的存取和事务控制

常见存储引擎有:

  • InnoDB(默认):支持事务、行锁、MVCC;
  • MyISAM:不支持事务,表锁,轻量级;
  • Memory、Archive、CSV 等特殊用途引擎。

InnoDB 内部还会经过:

  • Buffer Pool 缓存 →
  • B+ 树索引定位 →
  • Redo/Undo log 记录 →
  • Binlog 写入(Server 层)
    → 事务提交。

8. 返回结果

数据通过执行器从存储引擎中取出后,会经过格式化处理,最终通过网络协议返回给客户端。

在这个过程中 MySQL 还可能:

  • 做结果集缓冲;
  • 分块返回(防止大结果集一次性占用太多内存);
  • 响应客户端的流式读取。

MySQL 执行流程整体示意

客户端

连接器(Connection)

(查询缓存 - 已废弃)

解析器(Parser)

预处理器(Preprocessor)

优化器(Optimizer)

执行器(Executor)

存储引擎(InnoDB)

结果返回

⚔️ 面试/实战常考点

  • 优化器为什么重要?
    决定是否走索引、走哪个索引、join 顺序——同一条 SQL,计划不同,性能可能相差百倍。
  • 存储引擎在哪一步起作用?
    执行器之后,真正的物理读写都在存储引擎里。
  • 为什么说 SQL 优化重点是“让优化器选对计划”?
    因为大多数瓶颈都不在解析器和执行器,而在索引命中与执行计划选择。

🐺 一句话总结

MySQL 的执行流程是:

连接 → 解析 → 预处理 → 优化 → 执行 → 存储引擎 → 返回结果

  • 上层决定“怎么查”;
  • 下层决定“去哪查、怎么拿”;
  • 优化器是性能的灵魂;
  • 存储引擎是数据真正的执行者。

理解这个流程,你就能更清楚地知道:
为什么一条 SQL 慢,慢在了哪一环,应该从哪里动手去调。