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?
答案
因为浮点数有精度问题。
例如:
0.1 + 0.2 = 0.30000000000000004这在金额计算中会导致严重的精度丢失。
正确做法
方案 1:用 decimal
amount DECIMAL(12, 2)方案 2:用整数存"分"
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 使用聚簇索引,数据按主键顺序存储。
如果没有主键:
- InnoDB 会找第一个唯一非空索引作为聚簇索引
- 如果也没有,InnoDB 会自动生成一个隐藏的 6 字节 RowID 作为主键
隐藏主键的问题:
- 无法在业务层使用
- 自动生成的 RowID 对外不可见
- 不利于主从复制和数据管理
所以:
每张 InnoDB 表都应该显式定义主键。
Q7:自增主键和雪花 ID 怎么选?
答案
自增主键
- 简单
- 有序递增,页分裂少,写入性能好
- 不适合分布式场景(全局唯一性问题)
- 适合单库单表
雪花 ID
- 全局唯一
- 趋势递增(对 B+ 树友好)
- 不依赖数据库
- 适合分布式系统
选择建议
- 单库单表:自增主键
- 分布式系统:雪花 ID 或号段模式
Q8:什么是三范式?实际开发中要严格遵守吗?
答案
第一范式
字段不可再分,每个字段都是原子性的。
第二范式
非主键字段完全依赖主键,不能部分依赖。
第三范式
非主键字段不依赖其他非主键字段,消除传递依赖。
实际开发中
不一定严格遵守,因为:
- 纯范式化会导致表太多、join 太多、性能差
- 互联网业务常常做反范式(适度冗余)
典型反范式
订单表冗余用户名、商品名称、收货地址快照, 因为这些是交易发生时的历史数据,不能随原表变化。
Q9:为什么不建议用业务字段做主键?
答案
原因
业务字段可能变化 比如手机号可以换,身份证号可以更正。 主键变了,所有关联数据都要跟着变。
主键越大,索引越胖 InnoDB 二级索引叶子节点会存主键值。 主键越大,所有二级索引都会变大,占用空间多,性能差。
隐私问题 业务字段作主键,可能泄露数据量、注册顺序等信息。
业务耦合 主键和业务绑定,后续业务变化时改造成本高。
正确做法
主键用无业务含义的自增 ID 或雪花 ID, 业务字段单独建唯一索引。
Q10:NULL 值有哪些坑?
答案
1. 统计容易出错
SELECT COUNT(name) FROM user;COUNT(name) 不统计 NULL, 和 COUNT(*) 结果可能不同。
2. 判断逻辑复杂
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+ 树的优点
树更矮 每个节点可以放很多 key(比如几百个), 几千万数据通常只有 3~4 层,IO 次数少。
非叶子节点只存 key 能放更多分支,树更矮。
叶子节点是链表 支持高效范围查询和排序。
IO 友好 每次读一个节点就是读一个磁盘页,利用率高。
Q12:聚簇索引和非聚簇索引有什么区别?
答案
聚簇索引(Clustered Index)
- InnoDB 主键索引就是聚簇索引
- 叶子节点存放整行数据
- 数据按主键顺序存储在磁盘上
- 一张表只有一个聚簇索引
非聚簇索引(Secondary Index / 二级索引)
- 除主键外的所有索引
- 叶子节点存放:索引列值 + 主键值
- 通过二级索引查到主键后,再去主键索引取整行数据(回表)
关键区别
聚簇索引数据和索引在一起, 非聚簇索引需要回表才能拿到完整数据。
Q13:什么是回表?如何减少回表?
答案
什么是回表
通过二级索引找到主键, 再根据主键去聚簇索引查整行数据, 这个过程叫回表。
为什么影响性能
多了一次索引查找,数据量大时影响明显。
如何减少回表
方案 1:覆盖索引 让查询所需字段都在索引中,不需要回表。
-- 有索引 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:
SELECT id, name, age FROM user WHERE name = '张三';需要字段:id, name, age,都在索引里。 不回表,这就是覆盖索引。
怎么判断
用 EXPLAIN 看 Extra 列, 如果出现 Using index,通常说明用了覆盖索引。
Q15:联合索引的最左匹配原则是什么?
答案
联合索引 (a, b, c), 匹配时必须从最左边的字段开始,不能跳过。
能有效利用索引
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3不能有效利用索引
WHERE b = 2 -- 跳过了 a
WHERE c = 3 -- 跳过了 a、b
WHERE b = 2 AND c = 3 -- 跳过了 a为什么
联合索引底层按 a -> b -> c 的顺序排序, 离开 a 就无法利用索引的有序性定位。
Q16:索引失效有哪些情况?
答案
1. 对索引列做函数操作
WHERE DATE(create_time) = '2025-01-01' -- 失效改为:
WHERE create_time >= '2025-01-01' AND create_time < '2025-01-02'2. 对索引列做计算
WHERE age + 1 = 20 -- 失效改为:
WHERE age = 193. 隐式类型转换
WHERE phone = 13800000000 -- phone 是 varchar,失效改为:
WHERE phone = '13800000000'4. like 前导 %
WHERE name LIKE '%张三' -- 失效改为:
WHERE name LIKE '张三%' -- 可以用索引5. or 使用不当
WHERE a = 1 OR b = 2 -- b 没索引时可能失效6. 不符合最左前缀
-- 索引 (a, b, c)
WHERE b = 2 -- 失效7. 范围查询后的列
-- 索引 (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 时
-- 索引 (name, age)
SELECT * FROM user WHERE name LIKE '张%' AND age = 20;- 用索引找到所有
name LIKE '张%'的记录主键 - 回表取整行
- 在 Server 层过滤
age = 20
有 ICP 时
- 用索引找到
name LIKE '张%'的记录 - 在索引层直接过滤
age = 20(因为联合索引里有 age) - 只对符合条件的记录回表
减少了回表次数,提升性能。
EXPLAIN 体现
Extra 列出现 Using index condition。
Q22:前缀索引是什么?什么时候用?
答案
对字符串字段,只索引前 N 个字符。
CREATE INDEX idx_email ON user(email(20));适用场景
- 字段很长(邮箱、URL)
- 前缀区分度足够
优点
- 节省索引空间
- 减少维护成本
缺点
- 可能无法覆盖索引
- 区分度可能不足
如何确定前缀长度
用区分度来判断:
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. 高区分度字段优先 区分度高的字段放前面,能更快缩小范围。
例子
-- 查询:where user_id = ? and status = ? order by create_time desc
-- 推荐索引:
INDEX idx_user_status_ctime(user_id, status, create_time)Q25:如何查看一个表有哪些索引?
答案
SHOW INDEX FROM table_name;或者:
SHOW CREATE TABLE table_name;Q26:什么是自适应哈希索引?
答案
InnoDB 的一种自动优化机制。
当 InnoDB 发现某些索引值被频繁查询时, 会自动为这些值建立内存哈希索引, 让等值查询直接走哈希,比 B+ 树更快。
特点:
- 由 InnoDB 自动管理,不需要手动创建
- 只在内存中,重启后消失
- 只对等值查询有效
- 高并发下可能有锁竞争问题
Q27:range 和 index 的区别是什么?
答案
都是 EXPLAIN type 里的值。
range
使用索引做范围查询。 只扫描索引的某个范围,效率还可以。
WHERE id > 100 AND id < 200index
扫描整棵索引树(索引全扫描)。 虽然走了索引,但扫描的是整个索引,类似全表扫但只扫索引页。
效率通常不如 range,但比 ALL(全表扫描)好, 因为索引通常比数据表小。
Q28:如何强制使用某个索引?
答案
SELECT * FROM user FORCE INDEX(idx_name) WHERE name = '张三';或者:
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 叫慢查询。
开启
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过 1 秒记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';分析工具
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log列出最慢的 10 条 SQL。
Q32:什么是索引合并?
答案
当一个查询有多个条件, MySQL 优化器可能分别使用多个索引, 再把结果合并。
-- a 有索引,b 也有索引
WHERE a = 1 OR b = 2MySQL 可能:
- 用
idx_a查a = 1的主键集合 - 用
idx_b查b = 2的主键集合 - 合并(取并集)
什么时候会出现
主要用于 OR 条件,各字段有独立索引时。
注意
索引合并不一定比单个联合索引好, 设计好联合索引通常更优。
Q33:如何查找没有用到的索引?
答案
SELECT * FROM sys.schema_unused_indexes
WHERE object_schema != 'performance_schema';或者查:
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) 必须从最左字段开始匹配。
范围查询中断原因
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 写失败 -> 主库宕机后恢复没有这条数据,但从库有,不一致
两阶段提交流程
- redo log 写入,标记为
prepare - binlog 写入
- 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。
流程
- 事务开始,每次修改数据前,先把旧值写入 undo log
- 如果需要回滚,根据 undo log 的旧值,把数据恢复
例子
BEGIN;
UPDATE account SET balance = 900 WHERE id = 1;
-- 此时 undo log 记录:id=1 的 balance 旧值是 1000
ROLLBACK;
-- 根据 undo log,把 balance 改回 1000Q41:redo log 和 binlog 最本质的区别是什么?
答案
| 对比项 | redo log | binlog |
|---|---|---|
| 所属层 | InnoDB 引擎层 | Server 层 |
| 日志类型 | 物理日志(页的变更) | 逻辑日志(SQL 或行变更) |
| 写入方式 | 循环写 | 追加写 |
| 主要用途 | 崩溃恢复 | 主从复制、备份恢复 |
| 事务性 | 只记录已提交事务 | 记录已提交事务 |
Q42:大事务有什么危害?怎么避免?
答案
危害
- 锁持有时间长:阻塞其他事务
- undo log 膨胀:占用大量空间,回滚慢
- 主从延迟:binlog 大,从库重放慢
- 回滚代价高:失败后回滚时间长
- 连接超时:长时间占用连接
避免方法
- 分批处理:一次只处理一小部分
- 控制事务范围:只把必要的操作放在事务里
- 异步化:不需要强一致的操作异步处理
- 定期提交:批量操作每处理一批就提交
Q43:autocommit 是什么?有什么影响?
答案
MySQL 默认 autocommit = 1, 每条 SQL 自动包裹在一个事务里,执行完自动提交。
影响
好处:简单,不需要手动管理事务。
坏处:
- 如果你需要多条 SQL 原子性执行,必须显式
BEGIN - 频繁自动提交会增加事务开销
注意
如果你显式 BEGIN,autocommit 暂时失效, 直到你 COMMIT 或 ROLLBACK。
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:最近修改的事务 IDDB_ROLL_PTR:指向 undo log 的旧版本
2. undo log 版本链 每次修改,旧版本写入 undo log,形成版本链。
3. Read View 事务读数据时生成 Read View, 根据可见性规则判断应该读哪个版本。
Q49:什么是快照读和当前读?
答案
快照读
普通 SELECT,不加锁,走 MVCC,读历史快照版本。
SELECT * FROM user WHERE id = 1;当前读
加锁读或写操作,读最新版本。
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 级别。