MySQL中的MVCC
MVCC的概念
MVCC: Multi-Version Concurrency Control,即多版本并发控制.
是乐观锁的一种实现方式.
并发事务存在的问题:
- 更新丢失(Lost Update):多个事务同时更新同一行时,最后的更新会覆盖之前的更新。
- 脏读(Dirty Reads):一个事务对记录的未提交修改被其他事务读取到。
- 不可重复读(Non-Repeatable Reads):一个事务内多次查询相同记录结果不一致。
- 幻读(Phantom Reads):一个事务重新查询之前检索过的数据,发现出现新的数据。
解决:
- 加读写锁。
- 一致性快照读(MVCC)。
特点
- 用来提高数据库高并发场景下的吞吐性能。
- MySQL中InnoDB引擎支持MVCC。
- 比加行锁效率高,开销低。
- 在读已提交(Read Committed)和可重复读(Repeatable Read)隔离级别下起作用。
- 可以基于乐观锁和悲观锁实现。
- 使用行级锁(
row_level_lock
),而非行锁(innodb_row_lock
). - 同一个事务能够看到数据一致的视图.
- 事务开始的时间不同,看到相同表的数据可能不同.
基本原理
- 通过保留某个时间点的快照实现的.
基本特征
- 每行数据都存在一个版本,每次数据更新时都更新该版本.
- 修改数据时复制当前版本的数据进行修改,各个事务之间互不影响.
- 保存时比较版本号,成功(commit)则覆盖原记录,失败则放弃(rollback).
InnoDB存储引擎MVCC实现策略
细节:
- 每一行保存两个隐藏列:当前行创建时版本号和删除时版本号.
- 版本号是系统版本号,每开始一个新事务,系统版本号自增.而事务的版本号为事务开始时的系统版本号.
- 每个事务有自己的版本号.
MVCC下的InnoDB的增删改查
插入数据
- 设记录的版本号为当前事务的版本号。
- 向表中插入数据。
- 将
create version
设置为当前事务的版本号,delete version
为空。
更新操作
- 将旧的记录标记为已删除,
delete version
为当前事务版本号。 - 插入一行新的记录,
create version
为当前事务版本号,delete version
为当前版本号。
删除操作
- 将待删除的行的
delete version
设置为当前事务版本号。
查询操作
记录需满足两个条件:
delete version
为空或者设置的版本号大于当前事务的版本号(即:删除操作发生在当前事务之后)create version
小于等于当前事务版本号(即:记录创建在当前事务之前)
注:
- MVCC只适用于MySQL中的读已提交(Read Committed)和可重复读(Repeatable Read)。
- Read uncommitted存在脏读,即:读到未提交事务的数据行。
- 串行化是对表加锁。
InnoDB MVCC 实现原理
实现方式:
- 每一行记录都有两个隐藏列:
DATA_TRX_ID
和DATA_ROLL_PTR
。(若没有主键,则还有一个隐藏主键) DATA_TRX_ID
:记录最近更新这条记录的事务ID(6字节)DATA_ROLL_PTR
:指向该行回滚段的指针,通过指针找到之前版本,通过链表形式组织(7字节)DB_ROW_ID
:行标识(隐藏单增ID),没有主键时主动生成(6字节)
多事务并发操作数据
特征:
- 不同事务对同一行的更新操作产生多个版本。
- 通过回滚指针将这些版本链接成一条Undo Log链。
更新操作流程:
- 将待操作的行加排他锁。
- 将该行原本的值拷贝到Undo Log中,
DB_TRX_ID
和DB_ROLL_PTR
保持不变。(形成历史版本) - 修改该行的值,更新该行的
DATA_TRX_ID
为当前操作事务的事务ID,将DATA_ROLL_PTR
指向第二步拷贝到Undo Log链中的旧版本记录。(通过DB_ROLL_PTR
可以找到历史记录) - 记录Redo Log,包括Undo Log中的修改。
INSERT
操作:产生新的记录,其DATA_TRX_ID
为当前插入记录的事务ID。DELETE
操作:软删除,将DATA_TRX_ID
记录下删除该记录的事务ID,真正删除操作在事务提交时完成。
一致性读的实现
- RU隔离级别下 ==> 直接读取版本的最新记录。
- SERIALIZABLE隔离级别 ==> 通过加锁互斥访问数据实现。
- RC和RR隔离级别 ==> 使用版本链(ReadView,可读视图)
RR下的ReadView生成
特点:
- 每个事务首次执行
SELECT
语句时,会将当前系统所有活跃事务拷贝到一个列表中生成ReadView。 - 每个事务后续的
SELECT
操作复用其之前生成的ReadView。 UPDATE
,DELETE
,INSERT
对一致性读snapshot无影响。
示例:事务A,B同时操作同一行数据
- 若事务A的第一个
SELECT
在事务B提交之前进行,则即使事务B修改记录后先于事务A进行提交,事务A后续的SELECT
操作也无法读到事务B修改后的数据。 - 若事务A的第一个
SELECT
在事务B修改数据并提交事务之后,则事务A能读到事务B的修改。
RC下的ReadView生成
特点:
- 每次
SELECT
执行,都会重新将当前系统中的所有活跃事务拷贝到一个列表中生成ReadView。 - ReadView的组成:(当前活跃事务ID列表,称为
m_ids
)- 最小值为
up_limit_id
:最先开始的事务。 - 最大值为
low_limit_id
:最后开始的事务。
- 最小值为
- ID越小,事务开始的越早;ID越大,事务开始的越迟。
- 若被访问版本的
trx_id
小于up_limit_id
== > 生成该版本的事务在ReadView生成前就已提交 == > 该版本可以被当前事务访问。 - 若被访问版本的
trx_id
大于low_limit_id
== > 生成该版本的事务在ReadView生成之后才提交 == > 该版本不可被当前事务访问 == > 通过Undo Log找到之前的版本重新判断。 - 若被访问的版本在
up_limit_id
和low_limit_id
之间 == > 需要判断trix_id
是否在m_ids
中存在 == > 若存在,则生成该版本的事务还在活跃,则该版本不可访问,可由Undo Log找到之前的版本进行重新判断;若不存在,则创建ReadView时该版本对应的事务已提交,可以访问该版本。 - 找到记录后,还要判断
delete_flag
是否为true,若为true,则该记录已被删除,不返回;若为false,则记录可以返回。
注:对于ID较大的事务较ID较小的事务先提交的情况,即事务发生晚但提交的早
- RC的本质:每一条
SELECT
都可以看到其他已经提交的事务对数据的修改,只要事务提交,其结果都可见,与事务开始的先后顺序无关。 - RR的本质:第一条
SELECT
生成ReadView前,已经提交的事务的修改可见。
参考: