
MVCC概念
Coding的哔哔叨叨
Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,实现对数据库的并发访问,提高读操作的性能。只在写写之间相互阻塞,其他场景如读读、读写、写读都可以并行,大大提高mysql的并发度;是实现事务隔离性的关键。
注意:MVCC只在 READ COMMITTED
和 REPEATABLE READ
两个隔离级别下工作。其他两个隔离级别不和MVCC不兼容, 因为READ UNCOMMITTED
总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE
则会对所有读取的行都加锁。
隐藏字段
Coding的哔哔叨叨
DB_TRX_ID(6字节)
DB_TRX_ID表示最近一次对本行记录行做修改(insert|update|delete)的事务ID。这儿注意,delete操作,innodb也认为是一个update操作,会更新一个删除为delete_bit,将行表示为deleted,并非真正的删除。DB_ROLL_PTR(7字节)
DB_ROLL_PTR表示回滚指针,指向当前记录行的undo log信息。DB_ROW_ID(6字节)
当表没有设置主键或唯一非空索引时,innodb便会使用这个row_id自动生成聚簇索引。如果表有主键或唯一索引,聚簇索引就不会包含这个字段,所以这个字段跟MVCC关系不大。
注意,添加的隐藏字段并不是很多人认为的创建时间和删除时间,同时在MySQL中MVCC的实现也不是通过什么快照来实现的。之所以有这种说法可能是源自于《高性能MySQL》一书中对MySQL中的MVCC的错误结论,然后就人云亦云传开了(注意,我这里一直强调是MySQL中的MVCC的实现,是因为在不同的数据库中可能会有不同的实现)。所以说看源码和看官方文档才是最权威的解释。
InnoDB Muti-version:https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html

Read View
Coding的哔哔叨叨
源码地址:https://github.com/facebook/mysql-5.6/blob/42a5444d52f264682c7805bf8117dd884095c476/storage/innobase/include/read0read.h#L125

low_limit_id,目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。
up_limit_id:活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id。
因为trx_ids中的活跃事务号是逆序的,所以最后一个为最小活跃事务ID。(up_limit_id 并不是已提交的最大事务ID+1,后面的会证明这是错误的)。trx_ids:Read View创建时其他未提交的活跃事务ID列表。意思就是创建Read View时,将当前未提交事务ID记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。
注意:Read View中trx_ids的活跃事务,不包括当前事务自己和已提交的事务(正在内存中)。creator_trx_id:当前创建事务的ID,是一个递增的编号。这个编号并不是数据行里的DB_ROW_ID。
undo log
Coding的哔哔叨叨
Undo log中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本。
大多数对数据的变更操作包括 insert/update/delete,在InnoDB里,undo log分为如下两类:
insert undo log : 事务对insert新记录时产生的undo log,只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。 update undo log : 事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除。 事务回滚后会将此事务所有的undo log都删除。
purge
线程:为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下旧记录的deleted_bit,并不真正将旧记录删除。为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。purge线程自己也维护了一个read view,如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
修改数据行的具体流程
Coding的哔哔叨叨
假设有一行数据,最后一次修改的事务id是1:
| id | name | age | db_row_id | db_trx_id | db_roll_ptr |
| 1 | Bob | 18 | - | 1 | 0x1234 |
现在有一个事务A,对该行数据进行修改,将name改为“Jack”。
事务A将该行数据加行级排它锁。 然后将该行数据拷贝到undo log中,作为旧版本。 拷贝完成后,将该行数据的name改为“Jack”,修改事务id为2(事务A的id),并修改db_roll_ptr指针指向拷贝到undo log中的旧版本数据。 事务提交,释放排它锁,并将修改后的最新数据写入redo log中。(redo log的相关知识我会在后面也写一篇文章来专门去讲,所以这儿大家不必深究)。

接着又有一个事务B来修改该行数据,修改age为35。
事务B将该行数据加行级排它锁。 然后将该行数据拷贝到undo log中,作为旧版本。 拷贝完成后,将该行数据的name改为“Jack”,修改事务id为3(事务B的id),并修改db_roll_ptr指针指向拷贝到undo log中的旧版本数据。 事务提交,释放排它锁,并将修改后的最新数据写入redo log中(这儿描述有误,是在事务提交前写入)。

可见性比较算法
Coding的哔哔叨叨
DB_TRX_ID(最新修改本行数据的事务id)记为
trx_id与此read view中的变量
up_limit_id(活跃事务列表trx_ids中最老的事务id)、
low_limit_id(生成此read view时,系统中的最大事务id+1,即还未分配的事务id)、
trx_ids(其他活跃事务id,不包含本事务)进行比较,判断其是否满足可见性条件。
当
trx_id < up_limit_id
时,表示对此行数据最新一次修改的事务在本事务创建此read view前,就已经commit了,所以该行数据的值对本事务可见,直接跳到下面情况2。当trx_id >= low_limit_id时,表示本事务创建此read view后才修改的该行数据,该行数据对本事务不可见,直接跳到下面情况1.
当up_limit_id <= trx_id < low_limit_id时,表示本事务创建此read view的时候,系统有事务可能正在对该行数据进行修改还未提交事务,也有可能已经完成修改且已经提交事务,此时就需要用到trx_ids来进一步的判断。
若trx_id在trx_ids中找到了,说明本事务创建此read view前,id为trx_id的事务正在对该行数据进行修改,事务还未commit;也可能是在本事务创建此read view后,id为trx_id的事务对该行数据进行了修改(不论此时该trx_id事务有没有commit),该行数据对本事务不可见,直接跳到下面情况1。
若trx_id在trx_ids中没找到,说明本事务创建此read view前,已经提交了事务trx_id,直接跳到下面情况2。
https://github.com/facebook/mysql-5.6/blob/42a5444d52f264682c7805bf8117dd884095c476/storage/innobase/include/read0read.ic#L84

当前读和
快照读?
当前读、快照读
Coding的哔哔叨叨
当前读
像select ... lock in share mode(共享锁
), select ... for update、update、insert 、delete(排他锁
)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新数据,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。快照读
像不加锁
的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
最后想再多啰嗦几句,我理解的普通select语句就是快照读,它们不会对访问的数据加锁。
只有普通select语句才会创建read view,select ... lock in share mode(共享锁
), select ... for update、update、insert 、delete(排他锁
)这些操作都不会创建快照,他们都是当前读,因为他们都会对当前访问到的数据加锁。
RR下的MVCC如何保证可重复读,避免幻读
Coding的哔哔叨叨
先强调一下,在RR级别下,只靠MVCC是是可以实现可重复读,还能防止部分幻读,但是并不是完全防止。
RC和RR下read view创建的时机
Coding的哔哔叨叨
Read commited:innoDB在RC隔离级别下,事务begin后,执行每条select(读操作)时,都会重置read view,即会新创建一个read view。
Reapeatable Read:innoDB在RR隔离级别下,事务begin后,执行第一个select(读操作)时,会生成一个read view,将当前系统中对本事务不可见的其他活跃事务id都是记录起来,并且在此事务一直使用这个read view,直到事务被commit。
举例加深印象
Coding的哔哔叨叨
| id | name | age | db_row_id | db_trx_id | db_roll_ptr |
| 1 | Bob | 18 | - | 10000 | 0x1234 |

不积跬步,无以至千里。
文章有帮助的话,点个转发、在看呗
。
谢谢支持哟 (*^__^*)
END
👇





。