Skip to content

L11: MySQL 大厂面试题 - 基础、索引与事务(Q1-Q50)


专题一:基础与表设计(10 题)


Q1:MySQL 有哪些常用存储引擎?有什么区别?

答案

常用的有 InnoDB 和 MyISAM。

InnoDB

  • 支持事务(ACID)
  • 支持行锁
  • 支持外键
  • 支持崩溃恢复(redo log)
  • 聚簇索引,数据和主键索引放在一起
  • MySQL 5.5+ 默认引擎

MyISAM

  • 不支持事务
  • 表锁(并发写性能差)
  • 不支持崩溃恢复
  • 非聚簇索引
  • 存储了表的总行数,count(*) 很快

结论

生产环境几乎都用 InnoDB。 MyISAM 适合只读、对事务没要求的场景。


Q2:为什么不建议用外键?

答案

不是说外键绝对不能用,而是互联网业务里通常谨慎使用。

原因:

1. 影响写入性能

每次写入都要检查外键约束,性能开销大。

2. 增加锁范围

外键约束检查会触发额外锁,可能导致死锁。

3. 分库分表后失效

外键只在单库内有效,分库后无法使用。

4. 业务耦合

外键把业务逻辑放到数据库层,不灵活。

正确做法

由应用层代码保证数据一致性,而不是靠数据库外键。


Q3:char 和 varchar 怎么选?

答案

char

  • 定长
  • 不管实际长度,都占固定空间
  • 适合固定长度字段:MD5(32位)、手机号(11位)、状态码

varchar

  • 变长
  • 只占实际内容的空间
  • 适合长度不固定的字段:用户名、标题、地址

性能角度

  • char 查询略快(定长,偏移计算简单)
  • varchar 节省空间
  • 大多数业务场景用 varchar

Q4:为什么金额不能用 float 或 double?

答案

因为浮点数有精度问题。

例如:

text
0.1 + 0.2 = 0.30000000000000004

这在金额计算中会导致严重的精度丢失。

正确做法

方案 1:用 decimal

sql
amount DECIMAL(12, 2)

方案 2:用整数存"分"

sql
amount_cent BIGINT  -- 单位:分

100 元存为 10000,彻底避免精度问题。


Q5:datetime 和 timestamp 怎么选?

答案

datetime

  • 范围:1000-01-01 到 9999-12-31
  • 不受时区影响
  • 占 8 字节
  • 适合业务时间字段

timestamp

  • 范围:1970-01-01 到 2038-01-19
  • 受时区影响,存储 UTC
  • 占 4 字节
  • 适合记录系统操作时间

推荐

大多数业务场景用 datetime, 因为范围更大,不受时区影响,跨时区部署更稳定。


Q6:为什么每张表都建议有主键?

答案

InnoDB 使用聚簇索引,数据按主键顺序存储。

如果没有主键:

  1. InnoDB 会找第一个唯一非空索引作为聚簇索引
  2. 如果也没有,InnoDB 会自动生成一个隐藏的 6 字节 RowID 作为主键

隐藏主键的问题:

  • 无法在业务层使用
  • 自动生成的 RowID 对外不可见
  • 不利于主从复制和数据管理

所以:

每张 InnoDB 表都应该显式定义主键。


Q7:自增主键和雪花 ID 怎么选?

答案

自增主键

  • 简单
  • 有序递增,页分裂少,写入性能好
  • 不适合分布式场景(全局唯一性问题)
  • 适合单库单表

雪花 ID

  • 全局唯一
  • 趋势递增(对 B+ 树友好)
  • 不依赖数据库
  • 适合分布式系统

选择建议

  • 单库单表:自增主键
  • 分布式系统:雪花 ID 或号段模式

Q8:什么是三范式?实际开发中要严格遵守吗?

答案

第一范式

字段不可再分,每个字段都是原子性的。

第二范式

非主键字段完全依赖主键,不能部分依赖。

第三范式

非主键字段不依赖其他非主键字段,消除传递依赖。

实际开发中

不一定严格遵守,因为:

  • 纯范式化会导致表太多、join 太多、性能差
  • 互联网业务常常做反范式(适度冗余)

典型反范式

订单表冗余用户名、商品名称、收货地址快照, 因为这些是交易发生时的历史数据,不能随原表变化。


Q9:为什么不建议用业务字段做主键?

答案

原因

  1. 业务字段可能变化 比如手机号可以换,身份证号可以更正。 主键变了,所有关联数据都要跟着变。

  2. 主键越大,索引越胖 InnoDB 二级索引叶子节点会存主键值。 主键越大,所有二级索引都会变大,占用空间多,性能差。

  3. 隐私问题 业务字段作主键,可能泄露数据量、注册顺序等信息。

  4. 业务耦合 主键和业务绑定,后续业务变化时改造成本高。

正确做法

主键用无业务含义的自增 ID 或雪花 ID, 业务字段单独建唯一索引。


Q10:NULL 值有哪些坑?

答案

1. 统计容易出错

sql
SELECT COUNT(name) FROM user;

COUNT(name) 不统计 NULL, 和 COUNT(*) 结果可能不同。

2. 判断逻辑复杂

sql
WHERE name != '张三'  -- 不会包含 name IS NULL 的行

NULL 不等于任何值,包括 NULL 本身。

3. 索引问题

NULL 值在索引中处理比较特殊,容易引发一些预期外的问题。

4. 语义混乱

NULL 代表"未知",但有时候业务逻辑需要区分"未设置"和"空字符串"。

建议

  • 字段尽量 NOT NULL
  • 给合理默认值(空字符串、0 等)
  • 确实需要表达"未知"时再用 NULL

专题二:索引(25 题)


Q11:MySQL 为什么用 B+ 树,不用红黑树?

答案

核心原因:数据库的性能瓶颈是磁盘 IO,不是内存比较次数。

红黑树的问题

  • 每个节点只有 2 个子节点,树高 = log2(N)
  • 数据量大时树很高,一次查询需要多次磁盘 IO

B+ 树的优点

  1. 树更矮 每个节点可以放很多 key(比如几百个), 几千万数据通常只有 3~4 层,IO 次数少。

  2. 非叶子节点只存 key 能放更多分支,树更矮。

  3. 叶子节点是链表 支持高效范围查询和排序。

  4. IO 友好 每次读一个节点就是读一个磁盘页,利用率高。


Q12:聚簇索引和非聚簇索引有什么区别?

答案

聚簇索引(Clustered Index)

  • InnoDB 主键索引就是聚簇索引
  • 叶子节点存放整行数据
  • 数据按主键顺序存储在磁盘上
  • 一张表只有一个聚簇索引

非聚簇索引(Secondary Index / 二级索引)

  • 除主键外的所有索引
  • 叶子节点存放:索引列值 + 主键值
  • 通过二级索引查到主键后,再去主键索引取整行数据(回表)

关键区别

聚簇索引数据和索引在一起, 非聚簇索引需要回表才能拿到完整数据。


Q13:什么是回表?如何减少回表?

答案

什么是回表

通过二级索引找到主键, 再根据主键去聚簇索引查整行数据, 这个过程叫回表。

为什么影响性能

多了一次索引查找,数据量大时影响明显。

如何减少回表

方案 1:覆盖索引 让查询所需字段都在索引中,不需要回表。

sql
-- 有索引 idx_name_age(name, age)
-- 这个查询不回表
SELECT id, name, age FROM user WHERE name = '张三';

方案 2:减少查询字段 不要 SELECT *,只查需要的字段, 更容易形成覆盖索引。

方案 3:合理设计联合索引 把高频查询字段加入索引,覆盖更多场景。


Q14:什么是覆盖索引?

答案

覆盖索引是指:

查询所需的所有字段,都能从索引中直接获取,不需要回表。

例子

索引:idx_name_age(name, age) 二级索引叶子节点有:name, age, id(主键)

SQL:

sql
SELECT id, name, age FROM user WHERE name = '张三';

需要字段:id, name, age,都在索引里。 不回表,这就是覆盖索引。

怎么判断

EXPLAINExtra 列, 如果出现 Using index,通常说明用了覆盖索引。


Q15:联合索引的最左匹配原则是什么?

答案

联合索引 (a, b, c), 匹配时必须从最左边的字段开始,不能跳过。

能有效利用索引

sql
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3

不能有效利用索引

sql
WHERE b = 2            -- 跳过了 a
WHERE c = 3            -- 跳过了 a、b
WHERE b = 2 AND c = 3  -- 跳过了 a

为什么

联合索引底层按 a -> b -> c 的顺序排序, 离开 a 就无法利用索引的有序性定位。


Q16:索引失效有哪些情况?

答案

1. 对索引列做函数操作

sql
WHERE DATE(create_time) = '2025-01-01'  -- 失效

改为:

sql
WHERE create_time >= '2025-01-01' AND create_time < '2025-01-02'

2. 对索引列做计算

sql
WHERE age + 1 = 20  -- 失效

改为:

sql
WHERE age = 19

3. 隐式类型转换

sql
WHERE phone = 13800000000  -- phone 是 varchar,失效

改为:

sql
WHERE phone = '13800000000'

4. like 前导 %

sql
WHERE name LIKE '%张三'  -- 失效

改为:

sql
WHERE name LIKE '张三%'  -- 可以用索引

5. or 使用不当

sql
WHERE a = 1 OR b = 2  -- b 没索引时可能失效

6. 不符合最左前缀

sql
-- 索引 (a, b, c)
WHERE b = 2  -- 失效

7. 范围查询后的列

sql
-- 索引 (a, b, c)
WHERE a = 1 AND b > 2 AND c = 3
-- c 的索引利用受限

Q17:什么情况下索引会失效但不容易发现?

答案

1. 隐式类型转换(最容易忽略)

字段是 varchar,传了数字。 MySQL 会做类型转换,导致索引失效。

2. 字符集不一致

两张表关联,字段字符集不同(比如 utf8 和 utf8mb4), 会导致隐式转换,索引失效。

3. 区分度太低

比如 is_deleted 只有 0 和 1, 优化器认为走索引不如全表扫, 自动放弃索引。

4. 统计信息不准确

MySQL 优化器基于统计信息估算, 如果统计信息不准,可能做出错误选择。

可以用 ANALYZE TABLE 更新统计信息。


Q18:EXPLAIN 各字段怎么看?

答案

type(最重要)

从好到差:

  • const:主键或唯一索引等值查询
  • eq_ref:唯一索引扫描(join 时)
  • ref:非唯一索引等值查询
  • range:范围查询
  • index:索引全扫描
  • ALL:全表扫描(要警惕)

key

实际使用的索引,NULL 表示没用索引。

rows

预估扫描行数,越小越好。

Extra

  • Using index:覆盖索引
  • Using where:需要过滤
  • Using filesort:需要额外排序,注意
  • Using temporary:用了临时表,注意

Q19:为什么 SELECT * 不建议用?

答案

1. 影响覆盖索引

SELECT * 需要所有字段,几乎不可能用覆盖索引, 导致回表增多。

2. 增加网络传输

传输多余字段,浪费带宽。

3. 受表结构变更影响

加了新字段,SELECT * 会自动带上, 可能影响业务逻辑。

4. 影响 Buffer Pool 利用率

查询更多字段,需要加载更多数据页, Buffer Pool 利用率下降。


Q20:唯一索引和普通索引有什么区别?性能上有什么差异?

答案

功能区别

  • 唯一索引:保证字段值唯一
  • 普通索引:只是加速查询,不保证唯一

查询性能

  • 唯一索引:找到第一条就停止
  • 普通索引:找到第一条后还要继续找相同值的记录

唯一索引查询略快。

写入性能

唯一索引写入时需要检查唯一性, 而普通索引利用 Change Buffer 可以延迟写入, 所以普通索引写入性能略好。

选择建议

  • 字段业务上唯一:用唯一索引(保证数据正确性)
  • 只是查询加速:用普通索引

Q21:什么是索引下推(Index Condition Pushdown)?

答案

索引下推(ICP)是 MySQL 5.6+ 的优化。

没有 ICP 时

sql
-- 索引 (name, age)
SELECT * FROM user WHERE name LIKE '张%' AND age = 20;
  1. 用索引找到所有 name LIKE '张%' 的记录主键
  2. 回表取整行
  3. 在 Server 层过滤 age = 20

有 ICP 时

  1. 用索引找到 name LIKE '张%' 的记录
  2. 在索引层直接过滤 age = 20(因为联合索引里有 age)
  3. 只对符合条件的记录回表

减少了回表次数,提升性能。

EXPLAIN 体现

Extra 列出现 Using index condition


Q22:前缀索引是什么?什么时候用?

答案

对字符串字段,只索引前 N 个字符。

sql
CREATE INDEX idx_email ON user(email(20));

适用场景

  • 字段很长(邮箱、URL)
  • 前缀区分度足够

优点

  • 节省索引空间
  • 减少维护成本

缺点

  • 可能无法覆盖索引
  • 区分度可能不足

如何确定前缀长度

用区分度来判断:

sql
SELECT
  COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) AS ratio_10,
  COUNT(DISTINCT LEFT(email, 20)) / COUNT(*) AS ratio_20
FROM user;

选区分度接近完整字段的最短长度。


Q23:为什么索引不是越多越好?

答案

索引有以下代价:

1. 占用磁盘空间

每个索引都是一棵 B+ 树,占用额外存储。

2. 降低写入性能

每次 INSERT / UPDATE / DELETE, 都需要维护所有相关索引,索引越多写越慢。

3. 增加 Buffer Pool 压力

索引页也要占 Buffer Pool, 索引太多会挤占数据页的缓存空间。

4. 让优化器选择更复杂

索引太多,优化器可能选错索引。

建议

索引要围绕核心查询场景设计, 不用的索引及时删除。


Q24:如何设计一个高效的联合索引?

答案

原则

1. 等值条件放前面 等值匹配后索引可以继续利用后续字段。

2. 范围条件放后面 范围查询之后的字段利用受限。

3. 考虑排序字段 如果查询有 ORDER BY,把排序字段加入索引, 可以避免 filesort。

4. 考虑覆盖索引 如果查询字段都在索引里,不需要回表。

5. 高区分度字段优先 区分度高的字段放前面,能更快缩小范围。

例子

sql
-- 查询:where user_id = ? and status = ? order by create_time desc
-- 推荐索引:
INDEX idx_user_status_ctime(user_id, status, create_time)

Q25:如何查看一个表有哪些索引?

答案

sql
SHOW INDEX FROM table_name;

或者:

sql
SHOW CREATE TABLE table_name;

Q26:什么是自适应哈希索引?

答案

InnoDB 的一种自动优化机制。

当 InnoDB 发现某些索引值被频繁查询时, 会自动为这些值建立内存哈希索引, 让等值查询直接走哈希,比 B+ 树更快。

特点:

  • 由 InnoDB 自动管理,不需要手动创建
  • 只在内存中,重启后消失
  • 只对等值查询有效
  • 高并发下可能有锁竞争问题

Q27:range 和 index 的区别是什么?

答案

都是 EXPLAIN type 里的值。

range

使用索引做范围查询。 只扫描索引的某个范围,效率还可以。

sql
WHERE id > 100 AND id < 200

index

扫描整棵索引树(索引全扫描)。 虽然走了索引,但扫描的是整个索引,类似全表扫但只扫索引页。

效率通常不如 range,但比 ALL(全表扫描)好, 因为索引通常比数据表小。


Q28:如何强制使用某个索引?

答案

sql
SELECT * FROM user FORCE INDEX(idx_name) WHERE name = '张三';

或者:

sql
SELECT * FROM user USE INDEX(idx_name) WHERE name = '张三';

区别

  • USE INDEX:建议优化器优先用这个索引,但不强制
  • FORCE INDEX:强制使用这个索引

什么时候用

  • 优化器选错了索引
  • 你明确知道哪个索引更好

但一般情况下,不建议强制使用索引, 应该从根本上优化索引和 SQL。


Q29:count(*) / count(1) / count(字段) 哪个快?

答案

count(*)

统计所有行数(包括 NULL)。 MySQL 会选择最小的索引扫描。 InnoDB 优化了这个操作。

count(1)

count(*) 几乎一样, 现代 MySQL 版本两者性能基本相同。

count(字段)

统计该字段非 NULL 的行数。 如果字段有 NULL,结果和 count(*) 不同, 而且可能更慢(需要判断 NULL)。

推荐

直接用 count(*),MySQL 已经对它做了优化。


Q30:为什么 InnoDB 的 count(*) 比 MyISAM 慢?

答案

MyISAM

MyISAM 在磁盘上维护了表的总行数, count(*) 直接读这个值,是 O(1) 的操作。

InnoDB

InnoDB 支持事务和 MVCC, 不同事务看到的行数可能不同(可见性问题), 所以无法维护一个简单的总行数, 每次 count(*) 需要扫描索引统计。

优化

如果需要频繁统计总行数, 可以用 Redis 计数器维护, 写入时 +1,删除时 -1。


Q31:什么是慢查询?怎么开启慢查询日志?

答案

执行时间超过阈值的 SQL 叫慢查询。

开启

sql
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 超过 1 秒记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

分析工具

bash
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

列出最慢的 10 条 SQL。


Q32:什么是索引合并?

答案

当一个查询有多个条件, MySQL 优化器可能分别使用多个索引, 再把结果合并。

sql
-- a 有索引,b 也有索引
WHERE a = 1 OR b = 2

MySQL 可能:

  1. idx_aa = 1 的主键集合
  2. idx_bb = 2 的主键集合
  3. 合并(取并集)

什么时候会出现

主要用于 OR 条件,各字段有独立索引时。

注意

索引合并不一定比单个联合索引好, 设计好联合索引通常更优。


Q33:如何查找没有用到的索引?

答案

sql
SELECT * FROM sys.schema_unused_indexes
WHERE object_schema != 'performance_schema';

或者查:

sql
SELECT * FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
AND count_star = 0;

定期清理没用到的索引, 减少索引维护开销。


Q34:什么是最左匹配,为什么范围查询会中断匹配?

答案

最左匹配

联合索引 (a, b, c) 必须从最左字段开始匹配。

范围查询中断原因

sql
WHERE a = 1 AND b > 10 AND c = 5

索引按 a -> b -> c 排序。

  • a = 1:精确定位到 a=1 的范围
  • b > 10:在 a=1 的范围内,按 b 找 b>10 的记录

b > 10 是范围, 在 b 的范围内,c 的值并不是有序排列的。 所以 c 的条件无法继续利用索引的有序性快速过滤。


Q35:建表时索引越多越好吗?实际应该怎么做?

答案

不是越多越好。

实际做法

第一步:先不加索引 根据业务需求建表,先跑起来。

第二步:根据查询场景加索引 分析高频 SQL,针对性加联合索引。

第三步:用 EXPLAIN 验证 确认索引有效使用。

第四步:定期清理 删除长期未使用的索引。

建议每张核心表索引不超过 5~6 个, 过多索引会拖累写入性能。


专题三:事务与日志(15 题)


Q36:ACID 是什么?

答案

A:原子性(Atomicity)

事务中的操作要么全部成功,要么全部失败。 依赖:undo log

C:一致性(Consistency)

事务执行前后,数据库都处于一致状态。 目标:保证数据正确性

I:隔离性(Isolation)

并发事务互不干扰。 依赖:锁 + MVCC

D:持久性(Durability)

事务提交后,数据永久保存。 依赖:redo log


Q37:redo log、undo log、binlog 分别是什么?

答案

undo log

  • InnoDB 引擎层
  • 记录数据修改前的旧值
  • 用于:事务回滚 + MVCC 版本链

redo log

  • InnoDB 引擎层
  • 记录数据修改后的物理变更
  • 用于:保证持久性,崩溃恢复
  • 循环写(空间固定)

binlog

  • Server 层
  • 记录逻辑变更(SQL 或行变更)
  • 用于:主从复制 + 数据备份恢复
  • 追加写(不会覆盖)

Q38:为什么需要两阶段提交?

答案

为了保证 redo log 和 binlog 的一致性。

问题场景

如果不用两阶段提交:

  • redo log 写成功,binlog 写失败 -> 主库有数据,从库没有,主从不一致

  • binlog 写成功,redo log 写失败 -> 主库宕机后恢复没有这条数据,但从库有,不一致

两阶段提交流程

  1. redo log 写入,标记为 prepare
  2. binlog 写入
  3. redo log 标记为 commit

这样崩溃恢复时,根据状态判断是否真正提交:

  • 有 redo log prepare + binlog:提交
  • 只有 redo log prepare:回滚

Q39:什么是 WAL?

答案

WAL = Write-Ahead Logging(预写日志)。

先写日志,再写数据。

InnoDB 的实现:

  • 修改数据时,先把变更写入 redo log
  • 数据页不立即刷盘,后台慢慢刷

好处:

  • redo log 是顺序写,比随机写数据页快很多
  • 即使数据页没刷盘,宕机后也能用 redo log 恢复

这就是为什么 MySQL 写入性能好,同时又能保证持久性。


Q40:事务是怎么回滚的?

答案

依赖 undo log。

流程

  1. 事务开始,每次修改数据前,先把旧值写入 undo log
  2. 如果需要回滚,根据 undo log 的旧值,把数据恢复

例子

sql
BEGIN;
UPDATE account SET balance = 900 WHERE id = 1;
-- 此时 undo log 记录:id=1 的 balance 旧值是 1000
ROLLBACK;
-- 根据 undo log,把 balance 改回 1000

Q41:redo log 和 binlog 最本质的区别是什么?

答案

对比项redo logbinlog
所属层InnoDB 引擎层Server 层
日志类型物理日志(页的变更)逻辑日志(SQL 或行变更)
写入方式循环写追加写
主要用途崩溃恢复主从复制、备份恢复
事务性只记录已提交事务记录已提交事务

Q42:大事务有什么危害?怎么避免?

答案

危害

  1. 锁持有时间长:阻塞其他事务
  2. undo log 膨胀:占用大量空间,回滚慢
  3. 主从延迟:binlog 大,从库重放慢
  4. 回滚代价高:失败后回滚时间长
  5. 连接超时:长时间占用连接

避免方法

  1. 分批处理:一次只处理一小部分
  2. 控制事务范围:只把必要的操作放在事务里
  3. 异步化:不需要强一致的操作异步处理
  4. 定期提交:批量操作每处理一批就提交

Q43:autocommit 是什么?有什么影响?

答案

MySQL 默认 autocommit = 1, 每条 SQL 自动包裹在一个事务里,执行完自动提交。

影响

好处:简单,不需要手动管理事务。

坏处

  • 如果你需要多条 SQL 原子性执行,必须显式 BEGIN
  • 频繁自动提交会增加事务开销

注意

如果你显式 BEGIN,autocommit 暂时失效, 直到你 COMMITROLLBACK


Q44:事务的隔离级别有哪些?

答案

隔离级别脏读不可重复读幻读
Read Uncommitted可能可能可能
Read Committed不会可能可能
Repeatable Read不会不会理论可能
Serializable不会不会不会

MySQL InnoDB 默认:Repeatable Read(RR)


Q45:脏读、不可重复读、幻读分别是什么?

答案

脏读

读到别的事务未提交的数据。

不可重复读

同一事务里,同一行数据前后两次读结果不同 (被别的事务修改并提交了)。

幻读

同一事务里,同条件查询,前后两次结果集行数不同 (别的事务插入或删除了记录)。

关键区别

  • 脏读:读到未提交数据
  • 不可重复读:同一行内容变了
  • 幻读:结果集行数变了

Q46:为什么 MySQL 默认用 RR 而不是 RC?

答案

原因 1:更强的一致性

RR 避免了不可重复读, 事务内视图稳定,对业务更友好。

原因 2:MVCC 支撑

InnoDB 通过 MVCC 实现 RR, 性能代价可以接受。

原因 3:历史原因

早期 MySQL 的 binlog 用 Statement 格式, RC 下 Statement binlog 可能导致主从不一致, 所以默认用 RR。

(RC + Row 格式 binlog 也是安全的,很多公司也用 RC)


Q47:RC 和 RR 在 MVCC 实现上有什么区别?

答案

RC

每次执行快照读都生成新的 Read View。 每次读到的都是当前最新已提交版本。 所以会有不可重复读。

RR

事务第一次快照读时生成 Read View,之后复用。 同一事务内多次读,Read View 不变,结果一致。 所以避免了不可重复读。


Q48:什么是 MVCC?它是怎么实现的?

答案

MVCC = Multi-Version Concurrency Control,多版本并发控制。

让读操作不加锁也能读到一致数据,提高并发性能。

实现机制

1. 隐藏字段 每行记录有:

  • DB_TRX_ID:最近修改的事务 ID
  • DB_ROLL_PTR:指向 undo log 的旧版本

2. undo log 版本链 每次修改,旧版本写入 undo log,形成版本链。

3. Read View 事务读数据时生成 Read View, 根据可见性规则判断应该读哪个版本。


Q49:什么是快照读和当前读?

答案

快照读

普通 SELECT,不加锁,走 MVCC,读历史快照版本。

sql
SELECT * FROM user WHERE id = 1;

当前读

加锁读或写操作,读最新版本。

sql
SELECT * FROM user WHERE id = 1 FOR UPDATE;
UPDATE ...
DELETE ...
INSERT ...

关键区别

快照读当前读
加锁不加锁加锁
读版本历史快照最新版本
走 MVCC

Q50:RR 能完全解决幻读吗?

答案

不完全能,要分情况。

快照读场景

RR 下普通 SELECT 走 MVCC, Read View 固定,看不到新插入的行, 从结果上规避了幻读。

当前读场景

SELECT FOR UPDATE、UPDATE、DELETE 等当前读, 会读最新版本,需要靠:

  • 间隙锁(Gap Lock)
  • 临键锁(Next-Key Lock)

来防止范围内插入新行,从而控制幻读。

结论

RR 通过 MVCC + 间隙锁/临键锁, 在大多数场景控制了幻读, 但严格说并不能完全等同于 Serializable 级别。

基于 VitePress 构建