MVCC是多版本并发控制,指的是一条数据记录会有多个版本,每次修改的时候会存储之前的版本,因此MVCC的控制是以一条数据为单位的而不是表。
MVCC能够实现启动的事务无锁地访问不同版本的数据,因此读(普通读)写操作不会受到阻塞。
MVCC有两个重要的概念:即版本链和readView
版本链:
版本链实现了多个版本的存储,其实现原理为当事务改变了数据表的某一条数据时,会附带两个隐藏字段的数据,一个是trx_id,即事务ID,一个是roll_pointer,是指向undolog的指针。
当事务为插入的时候,就会插入生成的trx_id以及roll_pointer,trx_id标识了事务唯一,而roll_pointer则指向mysql自带的一个undolog表,这张表里记录了这次事务的id和修改数据的主键和长度等信息。 然而,由于插入操作本身不会修改现有的行,而是创建新的行,因此这个新行没有前一个版本。数据库在提交该插入事务后,roll_pointer
通常会为空,因为没有旧版本需要回滚, 当这条插入事务提交后,其undolog就会被回收,因为插入数据前是没有回滚的意义的。所以对于插入操作,undolog只在事务未提交时有效,一旦事务提交就会回收。
当执行 UPDATE
或 DELETE
操作时,trx_id
和 roll_pointer
被更新。roll_pointer
指向的 undo log 包含了修改前的旧版本数据。这个 undo log
不仅记录了原始数据,还记录了 roll_pointer
的值,指向再之前的旧版本的 undo log 条目。通过这种方式,多个版本的记录被串联起来,形成所谓的“版本链”。
readView:
readView的作用是说明哪个版本对于新执行的事务而言是可见的,readView只会在有包含查询操作的事务执行的同时存在未提交的增删改事务才会创建,其本质是一个视图。
readView相关的有四个概念,即creator_id(当前事务id),m_ids(生成readView时还活跃的事务id,即还未提交的事务id),min_trx_id(活跃id的最小id),max_trx_id(生成readView时InnoDB将分配给下一个事务的ID的值)。
对于可见版本的判断是沿着版本链逐渐寻找老的版本,如果遇到合适的版本就返回。
判断条件如下:
- 当trx_id==creator_id时,说明当前事务就是修改数据的事务,(例如在同一个事务中先进行修改再进行查询),所以可见。
- 当trx_id<min_trx_id的时候,说明当前事务查询的版本是已经提交了的版本,所以可见。
- 当min_trx_id<trx_id<max_trx_id的时候,如果此时trx_id在m_ids中,说明当前事务还没有提交,因此不可见,如果trx_id不在m_ids中,说明已提交,因此可见。
- 当trx_id>=trx_max_id,说明修改这条数据的事务在当前事务生成readView的时候还未启动,所以不可见。
当隔离级别是读已提交时:
每一个含有查询的事务都会生成一个readView,而单纯的查询事务的creator_trx_id=0,然后根据判断条件选择最大的trx_id那个可见版本进行查询。
当隔离级别是可重复度时:
在第一次查询生成readView之后,后续的查询都共用这一个readView。
注意:MVCC并不能解决幻读的问题,因为MVCC是针对于数据行(即单条数据)进行的控制,当发生了插入和删除操作的时候,读取的数据集结构就发生了改变,即使对于删除操作而言在可重复度情况下readView是一个readView,但是读取的结果中行数仍然会少那一行,因此仍然会出现幻读。
以MVCC进行的读操作为快照读(即根据readView快照向表中数据进行读取),注意快照读不是从视图读而是按照视图提供的字段向数据表中读取相应的版本数据。
要解决幻读,需要实现当前读,即读取数据的最新数据版本,并且加锁以确保数据一致性。即使其他事物在当前读之后修改了数据,也会立即反映在当前读的结果中。
当前读会在要读取的数据中加锁,即间隙锁,锁定范围内的所有记录和间隙,防止其他事务在该范围内插入新数据。
显示使用间隙锁,可以在查询语句中使用如下结构:
SELECT ... FOR UPDATE
:用于在当前事务中锁定所查询的行,并且锁定这些行之间的间隙,防止其他事务插入或删除这些记录。
SELECT ... LOCK IN SHARE MODE
:类似于 FOR UPDATE
,但允许其他事务读取这些行,而不允许修改或插入新的行。