前言
MVCC
指的是快照读。MySQL
中仅在RC
读已提交级别、RR
可重复读级别才会使用MVCC
机制。- 在
RC
级别中,MVCC
机制是会在每次select
语句执行前,都会生成一个ReadView
。 - 在
RR
级别中,一个事务只会在首次执行select
语句时生成快照,后续所有的select
操作都会基于这个ReadView
来判断,这样也就解决了RC
级别中存在的不可重复问题。
隔离级别
-
读未提交
(READ UNCOMMITTED):这是事务的最低隔离级别,事务中的读取操作可以看到其他未提交事务的变动。这种隔离级别可能导致脏读
、不可重复读
和幻读
问题。 -
读提交
(READ COMMITTED):这是大多数数据库系统的默认隔离级别。事务中的读取操作只能看到其他事务已经提交的变动。这种隔离级别解决了脏读问题
,但可能会出现不可重复读
和幻读
问题。 -
可重复读
(REPEATABLE READ):这是MySQL的默认隔离级别。事务中多次读取同一行数据,而不会看到其他事务对这一行数据的修改。这种隔离级别解决了脏读
和不可重复读
问题,但仍然可能出现幻读
问题。 -
可串行化
(SERIALIZABLE):这是事务的最高隔离级别。在事务执行期间,使用该隔离级别可以保证事务串行执行,避免了脏读
、不可重复读
和幻读
问题。但是这种隔离级别效率低下,因为事务通常需要等待前一个事务完成,才能继续执行。
读方式
-
当前读
是指在事务执行过程中,直接读取数据,而不是等待事务提交。这种读取方式会阻塞其他事务的写入操作,因此在并发性能方面可能较差。-
排他锁(也称为写锁):使用 SELECT ... FOR UPDATE 语句来对选定的数据行添加排他锁。例如:
SELECT * FROM table_name WHERE condition_column = 'condition_value' FOR UPDATE;
将返回满足条件的数据行,并对这些行添加排他锁。其他事务需要对这些行进行修改或删除时,必须等待排他锁被释放。 -
共享锁(也称为读锁):使用 SELECT ... LOCK IN SHARE MODE 语句来对选定的数据行添加共享锁。例如:
SELECT * FROM table_name WHERE condition_column = 'condition_value' LOCK IN SHARE MODE;
将返回满足条件的数据行,并对这些行添加共享锁。其他事务可以继续读取这些行,但是不能修改或删除。其他事务必须等待共享锁被释放。
-
-
快照读
是指在某个时间点上,一个事务读取另一个已经提交的事务的数据。在大多数情况下,读取操作不会阻塞其他事务的写入操作,这样可以提高数据库的并发性能。SELECT * FROM table_name WHERE condition_column = 'condition_value';
隐藏字段
DB_ROW_ID
:这是一个隐藏的列,它存储了行的唯一标识符。每一行都有一个唯一的ID,这个ID是在创建行时自动生成的。DB_ROW_ID列通常在InnoDB表的物理结构中存在,但在SELECT、INSERT、UPDATE等SQL语句中是隐藏的,不能直接访问或修改。DB_Deleted_Bit
:这是一个内部标记位,用于表示该行是否被标记为删除。当一行被删除时,该标记位将被设置为1,表示该行已被删除。这个字段仅在内部使用,对用户来说是隐藏的。DB_TRX_ID
:这是一个隐藏的列,它存储了当前事务的ID。当一行被修改或删除时,该事务的ID将被存储在该列中。通过这个字段,InnoDB引擎可以追踪事务的修改操作,并在需要时进行回滚操作。DB_ROLL_PTR
:这是一个隐藏的指针,它指向回滚日志中的位置,用于在需要回滚时恢复数据。当事务需要回滚时,InnoDB引擎将使用DB_ROLL_PTR来找到对应的回滚日志,并根据日志中的信息将数据恢复到事务开始之前的状态。
undo-log
sql
SELECT * FROM `users` WHERE user_id = 1;
+---------+-----------+----------+
| user_id | user_name | user_age |
+---------+-----------+----------+
| 1 | xyc | 18 |
+---------+-----------+----------+
UPDATE `users` SET user_name = "neil" WHERE user_id = 1;
UPDATE `users` SET user_age = 20 WHERE user_id = 1;
比如上述这段SQL
隶属于trx_id=1
的事务,undo-log
日志中存储的数据为:
不同的旧版本数据,会以roll_ptr
回滚指针作为链接点,然后将所有的旧版本数据组成一个单向链表。最新的旧版本数据,都会插入到链表头中,而不是追加到链表尾部。
ReadView
ReadView
就是一个事务在尝试读取一条数据时,MVCC
基于当前MySQL
的运行状态生成的快照,也被称之为读视图。当一个事务启动后,首次执行select
操作时,MVCC
就会生成一个数据库当前的ReadView
,通常而言,一个事务与一个ReadView
属于一对一的关系,ReadView
一般包含四个核心内容:
-
creator_trx_id
:代表创建当前这个ReadView
的事务ID
。 -
trx_ids
:表示在生成当前ReadView
时,系统内活跃的事务ID
列表,活跃事务是指还在执行的事务,即未结束(提交/回滚)的事务。 -
up_limit_id
:活跃的事务列表中,最小的事务ID
。 -
low_limit_id
:表示在生成当前ReadView
时,系统中要给下一个事务分配的ID
值,MySQL
的事务ID是按序递增的。
例如:
json
{
"creator_trx_id" : "0",
"trx_ids" : "[1,2,4]",
"up_limit_id" : "1",
"low_limit_id" : "5"
}
MVCC机制实现原理
- 当一个事务尝试改动某条数据时,会将原本表中的旧数据放入
undo-log
中。 - 当一个事务尝试查询某条数据时,
MVCC
会生成一个ReadView
快照。 - 其中
undo-log
主要实现数据的多版本,ReadView
则主要实现多版本的并发控制。
sql
-- 事务T1:trx_id=1
UPDATE `users` SET user_name = "neil" WHERE user_id = 1;
UPDATE `users` SET user_sex = 20 WHERE user_id = 1;
sql
-- 事务T2:trx_id=2
SELECT * FROM `users` WHERE user_id = 1;
目前存在T1、T2
两个并发事务,T1
目前在修改user_id=1
的这条数据,而T2
则准备查询这条数据,那么T2
在执行时具体过程如下:
-
当事务中出现
select
语句时,会先根据MySQL
的当前情况生成一个ReadView
。 -
判断行数据中的隐藏列
trx_id
与ReadView.creator_trx_id
是否相同:- 相同:代表创建
ReadView
和修改行数据的事务是同一个,自然可以读取最新版数据。 - 不相同:代表目前要查询的数据,是被其他事务修改过的,继续往下执行。
- 相同:代表创建
-
判断隐藏列
trx_id
是否小于ReadView.up_limit_id
最小活跃事务ID:- 小于:代表改动行数据的事务在创建快照前就已结束,可以读取最新版本的数据。
- 不小于:则代表改动行数据的事务还在执行,因此需要继续往下判断。
-
判断隐藏列
trx_id
是否小于ReadView.low_limit_id
这个值:- 大于或等于:代表改动行数据的事务是生成快照后才开启的,因此不能访问最新版数据。
- 小于:表示改动行数据的事务
ID
在up_limit_id、low_limit_id
之间,需要进一步判断。
-
判断隐藏列
trx_id
是否在trx_ids
中:- 在:表示改动行数据的事务目前依旧在执行,不能访问最新版数据。
- 不在:表示改动行数据的事务已经结束,可以访问最新版的数据。
就是首先会去获取表中行数据的隐藏列,然后经过上述一系列判断后,可以得知:目前查询数据的事务到底能不能访问最新版的数据 。如果能,就直接拿到表中的数据并返回,反之,不能则通过roll_ptr
去undo-log
日志中获取链表头的旧版本数据,然后依次遍历整个链表,从而检索到最合适的一条数据并返回。