MySQL - 通俗易懂的MVCC

先一句话总结下:

        MVCC采用非阻塞的方式解决Mysql InnoDB 读写并发的冲突,实现Mysql读已提交(RC),可重复读(RR)的事务隔离级别。

实现的主要方式是:通过undolog版本链 + Read View 的方式实现。

一、什么是MVCC

MVCC(multi-version-concurrent-control) 

 MVCC即多版本并发控制,MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

二、MVCC的作用

        MVCCMySQL InnoDB中的实现主要是为了提高数据库的并发性能,用更好的方式去处理读-写冲突,做到==即使有读写冲突时也能做到不加锁,非阻塞并发读==。  

        对于数据库来说涉及到并发那么就涉及到事物的问题。MVCC主要在非阻塞情况下解决事务的读已提交(RC,不可重复读),可重复读(RR)的问题。


MVCC 是基于 “数据版本”对并发事务进行访问。

思考: 那事务的隔离级别  读未提交和串行化又是如何解决的呢?


三、MVCC的原理

MVCC的目的就是多版本的并发控制,在数据库中的实现,就是为了解决读-写冲突的问题,
它的实现原理主要是依赖记录中的 3个隐式字段、undo日志、read view 来实现的。

 InnoDB在每行数据都增加三个隐藏字段,一个唯一行号,一个记录创建的版本号,一个记录回滚的版本号。

                                        图一  

 DB_ROW_ID:
6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID生成一个聚簇索引。
DB_TRX_ID:
6byte,最近修改(修改、插入)事务ID:记录创建这条记录以及最后一次修改该记录的事务的ID,是一个指针。
DB_ROLL_PTR:
7byte,回滚指针,指向这条记录的上一个版本(上一个版本存储于,rollback segment里)。
DELETED_BIT:
1byte,记录被更新或删除并不代表真的删除,而是删除flag变了,相当于记录一次逻辑删除。

3.1 版本链

         在多版本并发控制中,为了保证数据操作在多线程过程中,保证事务隔离的机制,降低锁竞争的压力,保证较高的并发量。在每开启一个事务时,会生成一个事务的版本号,被操作的数据会生成一条新的数据行(临时),但是在提交前对其他事务是不可见的,对于数据的更新(包括增删改)操作成功,会将这个版本号更新到数据的行中,事务提交成功,将新的版本号更新到此数据行中,这样保证了每个事务操作的数据,都是互不影响的,也不存在锁的问题。

图二

3.2 undo-log

        undo log是为回滚而用,具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘。undo buffer与redo buffer一样,也是环形缓冲,但当缓冲满的时候,undo buffer中的内容会也会被刷新到磁盘;与redo log不同的是,磁盘上不存在单独的undo log文件,所有的undo log均存放在主ibd数据文件中(表空间),即使客户端设置了每表一个数据文件也是如此。

3.3 Read View 快照

事务快照是用来存储数据库的事务运行情况。
一个事务快照的创建过程可以概括为:

        1、查看当前所有的未提交并活跃的事务,存储在数组中 (trx_ids)
        2、选取未提交并活跃的事务中最小的XID,记录在快照的xmin中 (limit_trx_id)
        3、选取所有已提交事务中最大的XID,加1后记录在xmax中 (max_trx_id)

Read View (主要是用来做可见性判断的):创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他事务id列表。

        对于Read View快照的生成时机,也非常关键,正是因为生成时机的不同,造成了RC,RR两种隔离级别的不同可见性;

        在innodb中(默认repeatable read级别),事务在begin/start transaction之后的第一条select读操作后,会创建一个快照(Read View),将当前系统中活跃的其他事务记录记录起来
在innodb中(read committed级别),事务中每条select语句都会创建一个快照(Read View)

 RC是语句级多版本(事务的多条只读语句,创建不同的ReadView,代价更高),RR是事务级多版本(一个ReadView);
        快照读:就是最普通select查询sql语句。

        像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本


        当前读:指 待执行下列语句时进行数据读取的方式

                        Insert /update / Delete
                        Select... for update
                        Select... lock in share mode

                像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

ReadView的数据结构:


 

3.4 内存快照中事务数据读取的规则

 

read committed 总是读最新一份快照数据,而repeatable read 读事务开始时的行数据版本。
 
read Commited隔离级别判断算法在每次语句执行的过程中,都关闭read_view, 
重新创建当前的一份新的read_view。
 
read view中事务id  min_trx_id~max_trx_id,当前事务T1。
...执行sql,创建一份最新的read_view;
...T1<min_trx_id,说明T1事务比较早,该行对当前事务T1可见。
...T1 > max_trx_id,说明T1比较晚,该行对当前事务不可见,根据DB_ROLL_PTR找到上一个判断再次判断。
...min_trx_id <= T1 <= max_trx_id,如果read_view中有该事务,则不可见,找上一个版本。
如果不在则可见(在read commited下)。
 
repeatable read各级离别下判断算法:创建事务trx结构的时候,就生成了当前的global read view。
...trx_id_1< min_trx_id那么表明该行记录所在的事务已经在本次新事务创建之前就提交了,
所以该行记录的当前值是可见的。
...trx_id_1>max_trx_id的话,那么表明该行记录所在的事务在本次新事务创建之后才开启,
所以该行记录的当前值不可见。通过DB_ROLL_PTR找到上一版数据判断
...min_trx_id<=trx_id_1<=max_trx_id,
 那么表明该行记录所在事务在本次新事务创建的时候处于活动状态,
从min_trx_id到trx_id_max进行遍历,
如果trx_id_1等于他们之中的某个事务id的话,那么不可见。
通过DB_ROLL_PTR找到上一版数据判断`


四、案例分析

案例出自(IT老齐) 
 

图三所示的是A,B,C,D 四个事务之间的状态。
图四是A,B,C,D 四个事务对应的版本链。


           图三 事务状态

图四  事务对应的undolog版本链
 

        在undo_log 版本链中为啥第一条数据因为是原始数据所有没有事务trx_id和上一个版本指针 。
undo_log不是会被删除吗?中间数据万一被删除了版本链不就断了吗?
答案:undo_log版本链不是立即山粗,Mysql会确保版本链数据不再被引用后再进行删除。

4.1 RC 情况下案例分析
 

 在可重复读隔离级别下,图五可知事务D两次查询的时间先后节点,及当时Read View的状态。

图六分析的是事务D第一次查询时Read View的状态,并分析结果。图七分析的是事务D第一次查询时Read View的状态,并分析结果。

图五 RC事务隔离界别下,重复读的案例分析  

 图六 RC事务隔离界别下,事务D第一个select查询数据的规则

        对于事务D第一个select,当前未提交的事务有m_ids(2,3,4) ,我们根据版本链的访问规则分析哪个事务的数据是可读。(注意:RC隔离级别下,每个事务都都会获取一个新的Read View)。我们个根据回滚指针从后往前去套用规则。当trx_id=3时,min_trx_id(2)<3<= max_trx_id(5),存在m_ids(2,3,4)中,无法访问,那么回溯到上一个版本trx_id = 2,此时也无法访问,继续回溯,当trx_id = 1时,1< min_trx_id(2),可以访问,那么第一个select就获取到版本链中事务trx_id =1 的数据的值。

图七 RC事务隔离级别下,事务D第二个select查询数据的规则

 同理对于第二个select,trx_id = 2 使我们可以访问的数据。对于同一个事务D,我们重复去查询,得到的却是不同的结果,造成无法重复读。

图八 RR事务隔离级别下,事务D第第一、二个select查询数据的规则

 对于可重复读(Repeatable Read)隔离级别下,事务D,整个过程中访问的是同一份ReadView,所以他们前后两次获取的数据是一致的。这个解决了不可重复读的问题。但是仍然会出现幻读的情况。

五、 再总结下MVCC:

 MVCC采用非阻塞的方式解决Mysql InnoDB 读写并发的冲突,实现Mysql读已提交(RC),可重复读(RR)的事务隔离级别。

实现的主要方式是:通过undolog版本链 + Read View 的方式实现。

猜你喜欢

转载自blog.csdn.net/u010445301/article/details/126090740