1. 引言
在数据库系统中,事务隔离是保证并发环境下数据一致性的核心机制。mysql 作为最流行的关系型数据库之一,其 innodb 存储引擎通过一套精妙的设计实现了 sql 标准中定义的四种隔离级别。本文将深入剖析 mysql 底层是如何实现事务隔离的,帮助读者真正理解 mvcc、锁机制与隔离级别之间的关系。
2. 事务隔离级别概述
sql 标准定义了四种事务隔离级别,分别对应不同的并发问题容忍度:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| read uncommitted(读未提交) | 可能 | 可能 | 可能 |
| read committed(读已提交) | 不可能 | 可能 | 可能 |
| repeatable read(可重复读) | 不可能 | 不可能 | 可能(innodb 已解决) |
| serializable(串行化) | 不可能 | 不可能 | 不可能 |
mysql innodb 默认使用 repeatable read(可重复读) 隔离级别,但通过 mvcc 和间隙锁的配合,实际上已经解决了幻读问题,达到了接近 serializable 的效果。
3. 核心机制一:mvcc(多版本并发控制)
mvcc 是 mysql 实现事务隔离的基石。它的核心思想是:读操作不阻塞写操作,写操作不阻塞读操作,通过保存数据的历史版本来实现。
3.1 隐藏列
innodb 的每行记录除了用户定义的字段外,还包含三个隐藏列:
db_trx_id:最近一次修改该行的事务 iddb_roll_ptr:回滚指针,指向 undo log 中的旧版本记录db_row_id:隐藏主键(当表没有主键时自动生成)
3.2 undo log 版本链
每次事务修改数据时,innodb 会先将旧值写入 undo log,然后通过回滚指针将新旧版本串联成一条版本链。这条链上的每个版本都记录了对应的事务 id,供后续的可见性判断使用。
3.3 readview(读视图)
readview 是 mvcc 实现快照读的关键数据结构,它记录了事务启动时活跃事务的 id 列表,主要包含:
m_ids:生成 readview 时当前活跃(未提交)的事务 id 列表min_trx_id:活跃事务中最小的 idmax_trx_id:下一个将被分配的事务 idcreator_trx_id:创建该 readview 的事务 id
3.4 可见性判断规则
当执行快照读时,沿着版本链从新到旧逐个判断版本的可见性:
- 若版本的事务 id 等于
creator_trx_id,则可见(自己修改的) - 若版本的事务 id 小于
min_trx_id,则可见(已提交) - 若版本的事务 id 大于等于
max_trx_id,则不可见(未来事务) - 若版本的事务 id 在
m_ids中,则不可见(活跃事务) - 否则可见(已提交)
-- 示例:查看当前事务隔离级别 select @@transaction_isolation; -- 示例:开启一个事务并查询 start transaction; select * from user where id = 1; commit;
4. 核心机制二:锁机制
mvcc 解决了快照读的隔离问题,但对于当前读(如 select ... for update、update、delete)以及写操作之间的冲突,则需要依赖锁机制。
4.1 锁的分类
innodb 的锁主要分为以下几类:
- 共享锁(s lock):允许其他事务读取,但阻止写入
- 排他锁(x lock):阻止其他事务读取和写入
- 记录锁(record lock):锁定单条索引记录
- 间隙锁(gap lock):锁定一个范围,但不包含记录本身
- 临键锁(next-key lock):记录锁 + 间隙锁的组合,锁定范围及范围内的记录
4.2 当前读与快照读
- 快照读:普通的
select语句,使用 mvcc 读取历史版本,不加锁 - 当前读:
select ... for update、update、delete等,读取最新版本并加锁
-- 当前读示例:加排他锁 select * from user where id = 1 for update; -- 当前读示例:加共享锁 select * from user where id = 1 lock in share mode;
5. 各隔离级别的实现原理
5.1 read uncommitted
- 读操作不加锁,直接读取最新版本
- 不生成 readview,因此可能读到未提交的数据(脏读)
- 实际使用场景极少
5.2 read committed
- 每次执行快照读时都生成一个新的 readview
- 因此每次 select 都能看到最新已提交的数据
- 解决了脏读,但无法避免不可重复读
-- 会话 a set session transaction isolation level read committed; start transaction; select * from user where id = 1; -- 读到 value = 100 -- 会话 b 此时提交 update user set value = 200 where id = 1; select * from user where id = 1; -- 再次读到 value = 200(不可重复读) commit;
5.3 repeatable read(innodb 默认)
- 事务第一次执行快照读时生成 readview,后续复用同一个 readview
- 因此整个事务期间看到的数据快照是一致的,解决了不可重复读
- 通过间隙锁和临键锁解决幻读问题
-- 会话 a set session transaction isolation level repeatable read; start transaction; select * from user where id = 1; -- 读到 value = 100 -- 会话 b 此时提交 update user set value = 200 where id = 1; select * from user where id = 1; -- 仍然读到 value = 100(可重复读) commit;
5.4 serializable
- 所有读操作都自动加共享锁,写操作加排他锁
- 通过强制串行执行来保证最高的隔离性
- 并发性能最低,实际使用较少
6. 幻读的解决:间隙锁与临键锁
在 repeatable read 级别下,innodb 通过临键锁(next-key lock) 来解决幻读问题。临键锁是记录锁和间隙锁的组合,它锁定的不仅是指定记录,还包括该记录之前的间隙。
-- 假设 user 表 id 有 1、5、10 三条记录 -- 执行以下当前读: select * from user where id between 1 and 10 for update; -- innodb 会锁定: -- 间隙 (1, 5)、记录 5、间隙 (5, 10)、记录 10、间隙 (10, +∞) -- 其他事务无法在 (1, 10] 范围内插入新记录,从而防止幻读
7. 总结
mysql innodb 实现事务隔离的核心可以概括为:
- mvcc 负责快照读:通过隐藏列、undo log 版本链和 readview 实现非锁定读,让读操作不阻塞写操作
- 锁机制负责当前读与写冲突:通过记录锁、间隙锁和临键锁保证写操作的隔离性
- 隔离级别决定 readview 的生成时机:read committed 每次生成新 readview,repeatable read 复用首次生成的 readview
- repeatable read + 临键锁:让 innodb 在默认隔离级别下就解决了幻读问题
理解这些底层机制,不仅能帮助我们正确设置隔离级别,还能在排查死锁、优化并发性能时做到心中有数。
以上就是mysql实现事务隔离的方法汇总的详细内容,更多关于mysql实现事务隔离的资料请关注代码网其它相关文章!
发表评论