什么是 MVCC(多版本并发控制,无锁机制 )?它解决了什么问题?
MVCC,即多版本并发控制,是 InnoDB 用来提高并发读写能力的一种机制。它并不是完全无锁,而是让普通
SELECT尽量通过读取数据的历史版本,避免与写操作发生锁等待。InnoDB 在修改数据时,会通过 Undo Log 保留旧版本, 并形成版本链。事务执行快照读时,会根据 Read View 判断某个版本是否可见;如果当前版本不可见,就沿版本链查找符合条件的历史版本。
MVCC 主要解决读写冲突问题,使读操作通常不阻塞写操作、写操作也不阻塞普通读,同时可以避免脏读,并在可重复读隔离级别下保证同一事务多次快照读结果一致,从而减少锁竞争、提高数据库并发性能。
但
UPDATE、DELETE、SELECT ... FOR UPDATE等当前读仍然需要加锁,所以 MVCC 不能理解成数据库完全无锁。

MVCC 解决了什么问题
1. 减少读写阻塞
如果没有 MVCC,当一个事务正在修改数据时,另一个事务查询这条数据,可能只能等待写事务释放锁。
有了 MVCC:
- 写事务修改当前版本;
- 读事务读取符合条件的历史版本。
因此普通查询通常不需要等待写事务完成。
也就是常说的:
读不阻塞写,写不阻塞普通读。
2. 避免脏读
假设事务 B 把余额修改为 800,但还没有提交。
事务 A 不能直接读取这个未提交的 800,因为事务 B 最后可能回滚(如果回滚了,那么就是脏读)。
MVCC 会通过版本可见性判断,让事务 A 读取之前已经提交的版本,例如 1000,从而避免读到未提交的数据。
3. 实现可重复读
在 MySQL InnoDB 的 REPEATABLE READ 隔离级别下,同一个事务中的多次快照读通常复用同一个 Read View。
例如:
事务 A 第一次查询:1000
事务 B 修改并提交:800
事务 A 第二次查询:1000
虽然数据库最新值已经是 800,但事务 A 仍然按照原来的事务快照读取 1000,所以两次结果一致。

InnoDB 如何实现 MVCC
MySQL InnoDB 主要依赖三个部分:
Undo Log
保存数据修改前的历史信息。
例如将:
1000
修改为:
800
当前数据中保存 800,Undo Log 中保留可以还原出 1000 的信息。
版本链
当前数据和 Undo Log 中的历史版本,通过回滚指针连接起来:
600 → 800 → 1000
Read View
Read View 是一套版本可见性规则,用来判断:
某个数据版本对当前事务是否可见。
如果最新版本不可见,InnoDB 就沿着版本链寻找更旧的版本,直到找到一个可见版本。
MVCC 并不是完全不加锁
普通查询属于快照读,通常使用 MVCC:
sql
SELECT * FROM account WHERE id = 1;
但以下操作需要读取最新数据,通常仍然要加锁:
sql
SELECT * FROM account WHERE id = 1 FOR UPDATE;
UPDATE account SET balance = 800 WHERE id = 1;
DELETE FROM account WHERE id = 1;
所以 MVCC主要解决的是:
普通读操作和写操作之间的冲突。
它并不能解决两个事务同时修改同一行数据时的写写冲突。
| 隔离级别 | 普通 SELECT 的处理 | Read View 使用方式 | 两次查询可能结果 |
|---|---|---|---|
READ UNCOMMITTED |
可以读取未提交版本 | 不提供可靠的一致性快照 | 可能脏读 |
READ COMMITTED |
使用 MVCC 快照读 | 每次 SELECT 创建新的 Read View | 第一次 1000,第二次可能 800 |
REPEATABLE READ |
使用 MVCC 快照读 | 第一次快照读创建,后续复用 | 第一次 1000,第二次仍是 1000 |
SERIALIZABLE |
事务内普通 SELECT 通常转为锁定读 | 更依赖锁保证串行执行 | 可能等待其他事务 |
redo log、undo log 和 binlog 有什么区别?
Undo Log 负责撤销,Redo Log 负责宕机后重做,Binlog 负责复制和数据恢复。
Undo Log 和 Redo Log 都属于 InnoDB 存储引擎,而 Binlog 属于 MySQL Server 层。
Undo Log 记录的是数据修改前的信息,主要用于事务回滚,保证事务的原子性;同时还会保存历史版本,配合 Read View 实现 MVCC。
Redo Log 记录的是InnoDB 对数据页所做的修改,主要用于崩溃恢复。事务提交后,即使数据页还没有真正刷入磁盘,MySQL 重启时也可以根据 Redo Log 恢复已提交的数据,从而保证事务的持久性。
Binlog 记录的是数据库已经发生的数据变更,支持 Statement、Row 和 Mixed 三种格式,主要用于主从复制,以及配合全量备份进行时间点恢复。
数据库发生崩溃后,InnoDB 会
先根据 Redo Log 恢复数据页,再通过 Undo Log 回滚未提交的事务。事务正常提交时,MySQL 还会通过两阶段提交协调 Redo Log 和 Binlog,保证主库中的事务状态和 Binlog 记录保持一致。
Undo Log:修改前是什么。Redo Log:数据页改了什么。
Binlog:事务最终改变了什么。
| 日志 | 所属层级 | 记录内容 | 主要作用 |
|---|---|---|---|
| Undo Log | InnoDB 存储引擎 | 数据修改前的信息 | 事务回滚、支持 MVCC |
| Redo Log | InnoDB 存储引擎 | 数据页发生了哪些修改 | 崩溃恢复、保证持久性 |
| Binlog | MySQL Server 层 | 数据库发生了哪些数据变更 | 主从复制、时间点恢复 |

InnoDB 中有哪些常见的锁?
InnoDB 的锁可以按照锁模式和锁定范围分类。
按照锁模式,主要有共享锁 S 和排他锁 X;此外还有表级的意向共享锁 IS 和意向排他锁 IX,用于表示事务准备
在表中的记录上加共享锁或排他锁。
按照锁定范围,可以分为表级锁和索引范围锁。表级锁包括 IS、IX 和 AUTO-INC 锁;索引范围锁主要包括 Record Lock(记录锁)、Gap Lock(间隙锁,开区间)、Next-Key Lock(临界锁,左闭右开) 和 Insert Intention Lock。其中 Record Lock 锁定具体索引记录,Gap Lock 锁定索引间隙,Next-Key Lock 是记录锁与间隙锁的组合。

什么是死锁?如何排查和避免?
死锁是指两个或多个事务互相持有对方需要的锁,同时又等待对方释放锁,形成循环等待,导致事务都无法继续执行。例如事务 A 先锁记录 1 再等待记录 2,而事务 B 先锁记录 2 再等待记录 1,就会产生死锁。
InnoDB 默认会自动检测死锁,并选择一个代价较小的事务进行整体回滚,让其他事务继续执行。排查时可以通过
SHOW ENGINE INNODB STATUS查看最近一次死锁,重点分析各事务正在执行的 SQL、已经持有的锁和正在等待的锁;频繁死锁时可以临时开启innodb_print_all_deadlocks,也可以通过data_locks、data_lock_waits和INNODB_TRX查看实时锁等待。避免死锁的主要方法是统一事务访问数据的顺序、缩短事务时间、建立合适索引、减少锁定范围、避免不必要的
FOR UPDATE,并在应用层对死锁事务进行有限次数的整体重试。
为什么深分页查询比较慢?如何优化?
深分页通常是指使用较大的
LIMIT offset, size进行分页。它慢的原因是 MySQL 不能简单地直接跳到 offset 位置,而是需要按照查询条件和排序规则扫描前offset + size条数据,丢弃前 offset 条,最终只返回 size 条。偏移量越大,扫描和丢弃的数据越多。如果使用二级索引并查询非索引字段,还可能产生大量回表;如果排序字段没有合适索引,还会产生额外排序开销。优化方式主要有:
第一,使用基于主键或排序字段的游标分页,例如通过
WHERE id < lastId ORDER BY id DESC LIMIT 20,避免扫描前面所有数据;第二,必须支持跳页时,可以使用覆盖索引加延迟关联,先从索引中查询当前页主键,再回表查询完整数据;
第三,根据查询条件和排序字段建立合适的联合索引;
此外还可以避免
SELECT *、先分页再关联、减少精确COUNT(*)、限制最大页码以及通过EXPLAIN ANALYZE检查实际扫描和排序情况。
sql
LIMIT 1000000, 10
表示:
跳过前 1,000,000 条数据,再取 10 条数据。
例 使用游标分页
不要传页码,而是传上一页最后一条数据的 ID。
