MVCC,全称是 Mutil-Version Concurrency Control(多版本并发控制),MySQL就利用了MVCC来判断在一个事务中,哪个数据可以被读出来,哪个数据不能被读出来。
数据版本链
在看MVCC之前,我们有必要知道另外一个知识点,数据库存储一行行数据,是分为两个部分来存储的,一个是数据行的额外信息,一个是真实的数据记录,MySQL会为每一行真实数据记录添加两三个隐藏的字段
- row_id 非必须,如果表中有自定义的主键或者有Unique键,就不会添加row_id字段,如果两者都没有,MySQL会“自作主张”添加row_id字段。
每次插入一行加1,到达最大值循环复用,一个长期运行的MySQL里,如果频繁插入删除行(像日志类的表),即使最终表规模不是很大,仍可能会出现值row_id重用,数据将有可能被覆盖。
- transaction_id 必须,事务Id,代表这一行数据是由哪个事务id创建的。
- roll_pointer 必须,回滚指针,指向这行数据的上一个版本。
在这里需要着重说明下事务id,当我们开启一个事务,并不会马上获得事务id,哪怕我们在事务中执行select语句,也是没有事务id的(事务id为0),只有执行 insert/update/delete 语句才能获得事务id,这一点尤为重要。
其中和MVCC紧密相关的是transaction_id和roll_pointer两个字段,在开发过程中,我们无需关心,但是要研究MVCC,我们必须关心。
代表这行数据是由transaction_id为9的事务创建出来的,roll_pointer是空的,因为这是一条新纪录。
当我们开启事务,对这条数据进行修改,会变成这样:
这就像一个单向链表,称之为“版本链”,最上面的数据是这个数据的最新版本,roll_pointer指向这个数据的旧版本,给人的感觉就是一行数据有多个版本,是不是符合“多版本并发控制”中的“多版本”这个概念, 那么“并发控制”又是怎么做到的呢,别急,继续往下看。
ReadView
ReadView 包含四个比较重要的内容:
- m_ids:表示在生成ReadView时,系统中活跃的事务id集合。
- min_trx_id:表示在生成ReadView时,系统中活跃的最小事务id。
- max_trx_id:表示在生成ReadView时,系统应该分配给下一个事务的id。
- creator_trx_id:表示生成该ReadView的事务id。
按照下面的判断方式就可以解决“我到底可以读取这个数据的哪个版本”这个问题
如果被访问的版本的trx_id和ReadView中的creator_trx_id相同,就意味着当前版本就是由你“造成”的,可以读出来。
如果被访问的版本的trx_id小于ReadView中的min_trx_id,表示生成该版本的事务在创建ReadView的时候,已经提交了,所以该版本可以读出来。
如果被访问版本的trx_id大于或等于ReadView中的max_trx_id值,说明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被读出来。
如果生成被访问版本的trx_id在min_trx_id和max_trx_id之间,那就需要判断下trx_id在不在m_ids中:如果在,说明创建ReadView的时候,生成该版本的事务还是活跃的(没有被提交),该版本不可以被读出来;如果不在,说明创建ReadView的时候,生成该版本的事务已经被提交了,该版本可以被读出来。
如果某个数据的最新版本不可以被读出来,就顺着roll_pointer找到该数据的上一个版本,继续做如上的判断,以此类推,如果第一个版本也不可见的话,代表该数据对当前事务完全不可见,查询结果就不包含这条记录了。
如果落在中间水位,包含两种情况: a. 如果当前版本的trx_id在活跃事务列表中,代表这个版本是由还没有提交的事务生成的,这个版本不可见; b. 如果当前版本的trx_id不在活跃事务列表中,代表这个版本是由已经提交的事务生成的,这个版本可见。
READ UNCOMMITTED —— 直接读取最新版本数据
对于 READ UNCOMMITTED 来说,可以读取到其他事务还没有提交的数据,所以直接把这个数据的最新版本读出来就可以了
READ COMMITTED —— 每次读取数据都会创建ReadView
假设,现在系统只有一个活跃的事务T,事务id是100,事务中修改了数据,但是还没有提交,形成的版本链是这样的:
现在A事务启动,并且执行了select语句,此时会创建出一个ReadView,m_ids是【100】,min_trx_id是100, max_trx_id是101,creator_trx_id是0。
- 判断最新的数据版本,name是“梦境地底王”,对应的trx_id是100,trx_id在m_ids里面,说明当前事务是活跃事务,这个数据版本是由还没有提交的事务创建的,所以这个版本不可见。
- 顺着roll_pointer找到这个数据的上一个版本,name是“地底王”,对应的trx_id是99,而ReadView中的min_trx_id是100,trx_id<min_trx_id,代表当前数据版本是由已经提交的事务创建的,该版本可见。
所以读到的数据的name是“地底王”。
我们把事务T提交了,事务A再次执行select语句,此时,事务A再次创建出ReadView,m_ids是【】,min_trx_id是0, max_trx_id是101,creator_trx_id是0
因为事务T已经提交了,所以没有活跃的事务。
- 判断最新的数据版本,name是“梦境地底王”,对应的trx_id是100,不在m_ids里面,说明这个数据版本是由已经提交的事务创建的,该版本可见。
所以读到的数据的name是“梦境地底王”。
REPEATABLE READ —— 首次读取数据会创建ReadView
现在A事务启动,并且执行了select语句,此时会创建出一个ReadView,m_ids是【100】,min_trx_id是100, max_trx_id是101,creator_trx_id是0。
- 判断最新的数据版本,name是“梦境地底王”,对应的trx_id是100,trx_id在m_ids里面,说明当前事务是活跃事务,这个数据版本是由还没有提交的事务创建的,所以这个版本不可见。
- 顺着roll_ponit找到这个数据的上一个版本,name是“地底王”,对应的trx_id是99,而ReadView中的min_trx_id是100,trx_id<min_trx_id,代表当前数据版本是由已经提交的事务创建的,该版本可见。
随后,事务T提交了事务,由于是首次读取数据才会创建ReadView,所以事务A再次执行select语句,不会再创建ReadView,用的还是上一次的ReadView,所以判断流程和上面也是一样的,所以读到的name还是“地底王”。
SERIALIZABLE —— 加锁访问记录
用加锁的方式来访问记录,不使用 ReadView