MVCC(Multi-Version Concurrency Control)是一种提高数据库并发性能的技术,其核心思想是允许多个事务同时访问相同的数据项,而不需要对它们加锁。这种方法通过维护数据项的多个版本来实现,每个事务在修改数据时都会生成一个新的版本。
在介绍MVCC前,需要先了解当前读和快照读的概念。
当前读(Current Read)和快照读(Snapshot Read)是InnoDB存储引擎在处理事务和并发控制时使用的两种不同的读取数据的方法。它们主要区别在于读取数据版本、并发控制以及隔离级别等方面。
当前读
当前读是一种锁定读(Locking Read),它会锁定所读取的数据行,防止其他事务在这期间修改数据。当前读总是返回数据的最新版本,即读取的是最新的已经提交或者未提交的数据。
举例说明:
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 当前读,使用排他锁,阻塞其他事务的写操作
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; -- 使用共享锁,阻塞其他事务的写操作
快照读
快照读是非锁定读(Non-locking Read),它不会产生任何行级锁,因此也称为“一致性读”(Consistent Read)。快照读基于MVCC(多版本并发控制)机制,读取的是在事务开始时所建立的数据快照版本,而不是当前最新数据,在不同的隔离级别下读取的快照版本也不同。
举例说明:
SELECT * FROM users WHERE id = 1;
普通的SELECT
查询是快照读,不会看到其他事务未提交的更改。
MVCC原理
MVCC通过为每行数据维护多个版本来工作,每个版本都有自己的创建时间和事务ID,主要是为了提高数据库并发性能,用更好的方式处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
依赖机制
事务ID(Transaction ID)
每当一个新事务开始时,它会被分配一个唯一的事务ID。
版本号(Version Number)
当数据行被修改时,系统会为这一行数据生成一个新的版本,并与事务ID关联。
隐藏列
InnoDB存储引擎在每行记录后都隐式地添加了三个隐藏列:
- DB_TRX_ID:记录最后修改行的事务ID。
- DB_ROLL_PTR:回滚指针,指向该行的回滚段(undolog中的rollback segment),用于MVCC和事务回滚。
- DB_ROW_ID:如果表没有主键或非空唯一索引时,InnoDB会使用这个隐藏的行ID来生成聚簇索引。
ReadView
ReadView
是事务进行快照读操作时产生的一个“视图”,它维护了系统中当前活跃的事务ID等信息,用于判断当前事务能看到哪些版本的数据。生成Read View时,系统会比较被访问版本的事务ID与Read View中的几个关键ID,从而决定该版本是否对当前事务可见。Read View只针对 RC 和 RR级别。
ReadView的组成
ReadView
包含了以下信息:
名称 | 描述 |
---|---|
活跃事务ID列表(trx_ids ) | 在创建ReadView 时,包含当前事务开始时系统中当前未提交事务的ID列表。 |
最大事务ID(max_trx_id ) | 在创建ReadView 时,系统分配给下一个即将开始的事务的ID。 |
最小事务ID(min_trx_id ) | 在创建ReadView 时,事务ID列表中最小的事务ID。 |
创建ReadView的事务ID(creator_trx_id ) | 创建该 Read View 的事务 ID。 |
ReadView的创建
- 在
REPEATABLE READ
隔离级别下,ReadView
是在事务中的第一个SELECT查询执行时创建的。 - 在
READ COMMITTED
隔离级别下,ReadView
是在每次SELECT查询执行时创建的。
当一个事务提交时,它的事务 ID 会被从所有当前活动的 Read View 的事务列表中移除。
事务提交后,它的事务 ID 会成为最小事务ID,这意味着所有比它低的事务 ID 创建的行版本都将对后续的事务可见。
ReadView的工作原理
当事务尝试读取数据时,InnoDB使用以下规则来判断数据行是否对当前事务可见:
- 如果数据行的
DB_TRX_ID
(最后修改该行的事务ID)等于creator_trx_id
(创建ReadView的事务ID),那么该行数据是可见的,因为当前事务正在访问自己所修改的记录。 - 如果数据行的
DB_TRX_ID
小于ReadView
中的min_trx_id
(最小事务ID),那么该行数据是可见的,因为它是在当前事务开始之前提交的。 - 如果数据行的
DB_TRX_ID
大于或等于ReadView
中的max_trx_id
(最大事务ID),那么该行数据是不可见的,因为它是在当前事务开始之后或者正在被其他活跃事务修改。 - 如果数据行的
DB_TRX_ID
在ReadView
中的min_trx_id
和max_trx_id
之间,那么需要进一步判断:- 如果
DB_TRX_ID
在trx_ids
列表中,表示这个事务在当前事务开始时是活跃的,因此该行数据是不可见的。 - 如果
DB_TRX_ID
不在trx_ids
列表中,表示这个事务在当前事务开始前已经提交,因此该行数据是可见的。
- 如果
- 如果数据行被标记为删除(例如,通过UPDATE或DELETE操作),InnoDB会通过
DB_ROLL_PTR
回滚指针找到该行的历史版本,并使用上述规则来判断历史版本是否对当前事务可见。
条件 | 是否可以访问 | 说明 |
---|---|---|
DB_TRX_ID== creator_trx_id | 可以访问该版本 | 成立,说明数据是当前这个事务更改的。 |
DB_TRX_ID< min_trx_id | 可以访问该版本 | 成立,说明数据已经提交了。 |
DB_TRX_ID> max_trx_id | 不可以访问该版本 | 成立,说明该事务是在ReadView生成后才开启。 |
min_trx_id <= DB_TRX_ID <= max_trx_id 且trx_id不在trx_ids中 | 如果trx_id不在m_ids中,是可以访问该版本的 | 成立,说明数据已经提交。 |
MVCC会通过隐藏列中的回滚指针找到该行数据的历史版本,根据事务隔离级别来确定哪个版本的数据对当前事务是可见的。
隔离级别
MVCC的行为会受到事务隔离级别的影响。
READ UNCOMMITTED
可以读取未提交的数据,不使用MVCC,不支持快照读。
READ COMMITTED
在READ COMMITTED
隔离级别下,MVCC的工作方式如下:
- 非锁定读取:事务在执行SELECT操作时,不会对读取的数据行加锁。
- 每次读取生成快照:每次执行SELECT语句时,都会创建一个新的事务快照,这意味着事务中的每次读取都可能看到不同的数据版本,只要这些版本是在SELECT执行之前提交的。
- 当前读:对于UPDATE和DELETE操作,MySQL会执行当前读(current read),即读取最新提交的数据行版本。如果其他事务已经修改了这些行,当前事务将等待这些修改提交或回滚,以确保数据的一致性。
具体行为:
- 如果一个事务在执行SELECT操作,而另一个事务正在修改同一行数据,那么第一个事务可能会在每次SELECT操作时看到不同的数据版本,因为每次SELECT都尝试读取最新提交的数据。
- 对于已经读取过的数据行,如果它们在其他事务中被修改并提交,那么后续的SELECT操作将看到新的数据版本。
REPEATABLE READ
在REPEATABLE READ
隔离级别下(这是InnoDB的默认隔离级别),MVCC的工作方式有所不同:
- 一致性非锁定读取:在事务开始时创建一个快照,事务中的所有SELECT操作都会读取这个快照版本的数据,而不是每次读取都创建新快照。
- 可重复读取:事务中的所有SELECT操作都会看到相同的数据版本,即使其他事务已经对这些数据进行了修改并提交。
- 幻读保护:解决了快照读的幻读问题。
SERIALIZABLE
事务序列化执行,不会相互冲突,不使用MVCC,相当于当前读,因为这种隔离级别下会锁定涉及到的所有数据行。
总结差异
- 事务快照的创建时机:
READ COMMITTED
:每次SELECT操作都会创建新的事务快照。REPEATABLE READ
:事务开始时创建一次快照,整个事务期间使用这个快照。
- 数据一致性的保证:
READ COMMITTED
:事务中的每次SELECT可能会看到不同的数据版本,因为它们读取的是最新提交的数据。REPEATABLE READ
:事务中的所有SELECT操作保证看到的数据是一致的,不会受到其他事务的影响。
- 幻读问题:
READ COMMITTED
:不保证避免幻读,因为每次SELECT都可能看到不同的数据集。REPEATABLE READ
:通过MVCC避免快照读的幻读,通过间隙锁和next-key锁来避免当前读的幻读。
参考链接: