mysql-mvcc

mysql-mvcc

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

什么是innodb下的当前读快照读

当前读

当前读读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,所以会对读取的记录进行加锁:select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)

快照读

快照读不加锁的非阻塞读,比如不加锁的select操作。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。

快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读;相对的,当前读实际上是一种加锁的操作,是悲观锁的实现。

MVCC解决什么问题

数据库并发场景有3种:

  1. 读-读,不需要并发控制
  2. 读-写,有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  3. 写-写,有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

MVCC是解决上述的读-写冲突的问题,可以做到无所并发控制,简单的描述逻辑:为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。

也就是MVCC可以做到:

  1. 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
  2. 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

总结

为了解决并发下的读-写冲突和写-写冲突,有两种解决方案:

  1. MVCC + 悲观锁 >MVCC解决读写冲突,悲观锁解决写写冲突
  2. MVCC + 乐观锁 >MVCC解决读写冲突,乐观锁解决写写冲突

MVCC实现原理

MVCC实现原理主要是依赖:

  1. 行记录中的3个隐式字段
  2. undo日志
  3. Read View

3个隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段。

DB_TRX_ID

6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID

DB_ROLL_PTR

7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)

DB_ROW_ID

6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了

(此处应有图)

DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本。

undo log

undo基本概念

先了解undo基本概念:

进行回滚操作时使用。undo存放在数据库内部的一个特殊段中,成为undo段。位于共享表空间中。undo是逻辑日志,是将数据库逻辑地恢复到原来的样子。这是因为在多用户并发系统中,可能有成百上千个并发事务,如果直接物理回滚页记录,会影响其他正在进行的事务。

所以undo的回滚操作是逻辑操作,对于insert,进行对应的delete;对于delete,执行对象的Insert,对于update,进行反向的update。

undo的另外一个作用是MVCC,实现了非锁定读取。

undo log也会产生redo log。这是因为undo log也需要持久性的保护。

undo存储管理

采用段的方式进行管理,首先有rollback segment,每个回滚段中记录了1024个undo log segment,而在每个undo log segment段中进行undo页的申请。

事务在undo log segment分配页并写入undo log的这个过程同样需要写入重做日志redo log。

将undo log放入列表中,以供之后的purge操作

判断undo log所在的页是否可以重用,若可以分配给下个事务使用。

事务提交之后并不能马上删除undo log以及undo log所在的页,这是因为可能有MVVC使用。所以将undo log放在一个链表中,是否可以最终删除由purge线程判断

undo log格式

undo log分两种:

insert undo log

代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃。

update undo log

事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除。

其中,对MVCC有帮助的实质是update undo log ,undo log实际上就是存在rollback segment中旧记录链,它的执行流程如下:

  1. 有个事务插入person表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL
  2. 现在来了一个事务1对该记录的name做出了修改,改为Tom
    2.1 在事务1修改该行(记录)数据时,数据库会先对该行加排他锁
    2.2 然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本
    2.3 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它
    2.4 事务提交后,释放锁
  3. 又来了个事务2修改person表的同一个记录,将age修改为30岁
    3.1 在事务2修改该行数据时,数据库也先为该行加锁
    3.2 然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面
    3.3 修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录
    3.4 事务提交,释放锁

不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。其实insert undo log在事务提交之后就会被删掉,这里留着是为了演示undo log链表。

read view

Read View是事务进行快照读操作的时候生产的读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID。

Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图。
Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本

read view判断可见性

read view有3个全局属性:

  1. trx_list(名字我随便取的)一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID
  2. up_limit_id 记录trx_list列表中事务ID最小的ID
  3. low_limit_id ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1

判断过程:

  1. 首先比较DB_TRX_ID < up_limit_id, 如果小于,则当前事务能看到DB_TRX_ID 所在的记录,如果大于等于进入下一个判断
  2. 接下来判断 DB_TRX_ID 大于等于 low_limit_id , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
  3. 判断DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在,则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在Read View生成之前就已经Commit了,你修改的结果,我当前事务是能看见的

MVCC主要过程

了解完MVCC所依赖的3点后,看看MVCC的整体过程:

MVCC与隔离级别的关系

隔离级别影响的是read view生成的时机不同,从而影响快照读结果:

  1. 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
  2. 即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
  3. 而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因

总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。

具体隔离级别下的快照读解读:

READ COMMINTED和REPEATABLE READ(默认事务隔离级别)的事务隔离级别下,INNODB存储引擎使用非锁定的一致性读。

  1. READ COMMITED:总是读取被锁定行的最新一份快照数据,理论上来讲是违反了ACID中的I特性
  2. REPEATABLE READ:总是读取事务开始时的行数据版本。

purge线程
从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit(逻辑删除),并不真正将过时的记录删除。
InnoDB有专门的purge线程来清理deleted_bit为true的记录。
为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

参考链接:

  1. https://blog.csdn.net/SnailMann/article/details/94724197

你可能感兴趣的:(mysql-mvcc)