当前位置: 首页 > 后端技术 > Java

InnoDB学习(五)MVCC多版本并发控制

时间:2023-04-01 16:12:21 Java

MVCC多版本并发控制是数据库管理系统的一种并发控制方法。在MVCC多版本并发控制下,数据库中的数据会有多个版本,对应不同的事务,从而实现事务间并发数据的隔离。MVCC最大的优点就是读没有锁,读写不冲突。在读多写少的场景下,非冲突的读写可以极大的提升数据库的并发性能。MVCC多版本并发控制在MYSQL中,MyISAM存储引擎使用表锁,InnoDB存储引擎使用行锁。InnoDB的事务分为四种隔离级别,其中默认的隔离级别是可重复读。可重复读要求两个并行事务之间的数据修改不会相互影响。虽然通过加行锁可以实现两个并行事务之间的数据修改互不影响,但是两个事务之间存在锁等待的情况,影响了数据库的效率。因此,InnoDB的可重复读并没有使用行锁,而是使用了更强大的MVCC。MVCC只在repeatableread和readcommitted隔离级别下生效。另外两个隔离级别与MVCC不兼容,因为readuncommitted总是读取最新的数据行,不管事务版本如何,而序列化会将读取的所有行都锁定。由于可重复读的情况比较复杂,而且是MySQL默认的隔离级别,本文将以可重复读来讲解MVCC的原理。可重复读数据库有四种隔离级别:读未提交/读已提交/可重复读/序列化,可重复性是MySQL默认的事务隔离级别,它保证同一个事务的多个实例并发读取数据,你会看到一致的数据行。数据行的一致性包括两部分:Case1:已有数据的内容被改变。如果在同一个事务中查询多次,查询结果应该是一样的。如果在当前事务中进行了修改,则查询结果应与当前事务中的查询结果相同。修改结果相同;case2:数据行的增减,同一个事务只能查看该事务开始前数据库中的数据,或者事务本身增/删的结果集,看不到其他事务的增删在事务开始期间删除的结果集;InnoDB默认的隔离级别是可重复读,可以解决以上两种情况下的数据行一致性问题。其中,解决案例1中的数据行一致性问题是通过MVCC多版本并发控制实现的。InnoDB已经使用Gap锁实现了Case2中的数据行一致性问题,但本文不介绍Gap锁。MVCC的作用MVCC可以保证在同一个事务中,从事务的开始到结束读到的某个数据是一致的,多个事务之间不会互相阻塞。下面以一张用户表为例来说明MVCC版本控制的作用。首先,我们需要创建一个用户表,并向其中插入一条用户数据。SQL语句如下:createtableuser_info(ageint,namevarchar(255));insertintouser_info(age,name)value(23,'张三');假设有3个事务A、B、C、在这三个事务中,分别在不同的时间读取插入用户的信息,修改用户信息。时间线如下:T1时刻,事务A开始,事务A读取年龄=23的用户,用户名为张三;T2时刻,事务B启动,事务B读取年龄=23的用户,用户名为张三;T3时,事务A修改age=23用户,改名为Lisi;T4时,事务A读取年龄=23的用户,用户名为李斯,事务A提交事务;T5时刻,事务B读取年龄=23的用户,用户姓名为张三,事务B提交事务;T6时,事务C启动,事务C读取年龄=23的用户,用户名为李四,事务C提交事务;MVCC的作用在T5时就可以体现出来,此时事务A已经提交,修改了age=23的用户名字为李四,但是事务B看不到这个修改,而名字交易B看到的年龄=23的用户是张三。这是因为在repeatability隔离级别下,InnoDB事务读取的数据是快照读取,即为事务B开始的数据生成一个快照,事务B读取的数据永远是这个快照,对应快照读Currentread:当前读:读取最新版本的记录,并保证其他并发事务在读取时不能修改当前记录,并锁定读取的记录;snapshotread:MVCC使用snapshotread,在事务启动时为数据生成一个快照。快照读取可以避免加锁操作,提高数据库性能;MVCC的原理。部分内容:数据库中的3个隐藏字段、UndoLog日志、ReadView阅读视图。这三部分在MVCC中的作用如下:隐藏字段:为数据添加额外的版本信息,是MVCC版本控制的基石;UndoLog:存储了多个版本的数据,不同版本数据的隐藏字段内容不同;ReadView:判断当前事务应该读取哪个版本的数据;隐藏字段隐藏字段是指我们无法通过SQL语句找到这些字段,但是这些字段是真实存在于数据库中并占用存储空间的。为了实现MVCC版本控制,InnoDB在每行数据中增加了如下3个隐藏字段:DB_TRX_ID:6字节,最后修改这条记录的事务ID;DB_ROLL_PTR:7字节,回滚指针,指向这条记录Version的前一条记录(存放在RollbackSegment中);DB_ROW_ID:6字节,隐藏主键,如果数据表没有显式主键,InnoDB使用DB_ROW_ID建立聚簇索引;我们使用如下SQL创建一个用户表,并向表中插入一条数据,新建的表默认会包含三个隐藏字段,表结构如下表所示。createtableuser_info(ageint,namevarchar(255));insertintouser_info(age,name)value(23,'张三');|age|name|DB_TRX_ID|DB_ROLL_PTR|DB_ROW_ID||--|--|--|--|||23|张三|1|0x222333|1|UndoLog我在另一篇文章中介绍了UndoLog日志,从名字就可以看出,UndoLog日志主要用于回滚事务。但是InnoDB中MVCC的snapshotread也是用的UndoLog。UndoLog可以分为两类:InsertUndoLog:事务中Insert语句对应的UndoLog只有在事务回滚时才需要,所以在事务提交后可以立即丢弃;UpdateUndoLog:事务执行Update或Delete时产生的UndoLog;不仅事务回滚时需要,读取快照时也需要;所以,不能随便删除。只有当日志不涉及快照读取或事务回滚时,相应的日志才会被Purge线程清除;Purge线程:InnoDB,被删除的数据不会直接删除,而是先标记为删除,不会立即删除无用的UpdateUndoLog。这些数据是通过InnoDB中的后台任务Purge线程删除的。下面我们以上面的user表和数据为例,来说明UpdateUndoLog的工作流程。user_info表空间一开始的数据状态如下:T1时刻,事务A开始,事务Id为2,事务A读取age=23个用户,用户名为张三;此时没有修改数据库数据,没有产生UndoLog,表空间也没有变化;T2时刻,事务B开始,事务Id为3,事务B读取年龄=23的用户,用户姓名为张三;此时没有修改数据库数据,没有产生UndoLog,表空间保持不变;T3时刻,事务A修改了年龄=23的用户,改名为李四;此时,因为事务A还没有提交,所以会为事务A生成一个UndoLog,里面存放的是事务A修改前的数据,表空间最新数据中的回滚指针指向这条日志;T4时,事务A读取age=23的用户,因为表数据中记录的事务ID与事务A的事务ID一致,所以事务A会读取表数据中的记录,读取用户的姓名为李四,交易A提交交易;在T5时刻,事务B读取到年龄=23的用户,由于表空间中的数据不满足可见性条件(下节详述),事务B会去查找表数据的UndoLog,而数据UndoLog中的满足可见性条件,所以查询UndoLog中的数据。用户,用户名为张三,交易B提交交易;T6时刻,事务C启动,事务ID为3,事务C读取age=23的用户。由于事务C启动时事务A已经提交,事务C可以查询提交的数据,事务C读取到用户名为李四;T7时刻,事务C开始,事务ID为3,事务C修改年龄=23的用户,改名为王五;此时由于事务C还没有提交,所以会为事务C生成一个UndoLog,存放事务C修改前的数据;从上面的例子可以看出,在不同事务或者同一个事务中对同一条记录的修改都会导致记录被删除。UndoLog成为记录版本的线性链表。UndoLog链的头部是最新的旧记录,链尾是最早的旧记录(UndoLog的节点可能被Purge线程清除)。UndoLog用于回滚,具体内容是将事务之前的数据库记录分行到UndoBuffer中,在合适的时候将UndoBuffer中的内容刷入磁盘。UndoBuffer和RedoBuffer一样,也是一个环形缓冲区,但是当缓冲区满时,UndoBuffer中的内容也会被刷新到磁盘;与RedoLog不同的是,磁盘上没有单独的UndoLog文件,所有的UndoLog都存储在主要的ibd数据文件(表空间)中,即使客户端设置了一个表一个数据文件。ReadView读视图ReadView是事务执行快照读操作时产生的读视图。在事务执行快照读取的那一刻,会生成一个数据库系统的当前快照,记录并维护当前活跃事务在系统中的ID(当每个事务启动时,都会分配了一个ID,这个ID是递增的,所以最新的事务ID值更大)所以我们知道ReadView主要是用来做可见性判断的,也就是当某个事务执行快照读的时候,创建一个ReadView读视图forrecord,和一个条件进行比较,判断当前事务能看到哪个版本的数据,可能是最新的当前数据,也可能是该行记录的UndoLog中某个版本的数据。ReadView遵循一个可见性算法,主要是取出要修改的数据的最新记录中的DB_TRX_ID(即当前交易ID),与系统中其他活跃交易的ID(由ReadView维护)进行比较,如果DB_TRX_ID与ReadView的属性进行了一些比较,不满足可见性,则使用DB_ROLL_PTR回滚指针取出UndoLog中的DB_TRX_ID再次进行比较,即遍历该对象的DB_TRX_ID链表(从链首到链尾,即从最新修改开始),直到找到满足指定条件的DB_TRX_ID,则DB_TRX_ID所在的旧记录就是最新的旧版本当前交易可以看到。ReadView判断可见性的原理如下。在InnoDB中,创建新事务后,当新事务读取数据时,数据库会为该事务生成一个ReadView读视图,InnoDB会在当前系统中创建一个活跃事务列表的副本保存到ReadView中。当用户要读取本次事务中的一行记录时,InnoDB会将该行的当前版本号与ReadView进行比较。具体算法如下:设置该行的当前事务ID为cur_trx_id,ReadView中最早的事务ID为min_trx_id,最新的事务ID为max_trx_id;如果cur_trx_idmax_trx_id,说明该行记录所在的事务是在新事务创建后开启的,所以该行记录的当前值是不可见的。跳至第5步;ifmin_trx_id<=cur_trx_id<=max_trx_id,则说明创建这个新事务时记录该行的事务是活跃的,从min_trx_id遍历到max_trx_id,如果cur_trx_id等于其中的一个事务id,则为不可见。跳至第5步;从该行记录的DB_ROLL_PTR指针指向的回滚段中取出最新的UndoLog版本号,赋值给cur_trx_id,然后跳到第2步;返回可见行的值;summarize:在MVCC版本控制中,以事务的第一次快照读取为分界线。事务后只能找到第一次读取快照和之前提交的数据版本,之后提交的数据版本是不可见的。ReadCommitted和RepeatableReads在Committed和Repeatable隔离级别下InnoDB快照读取有什么区别?答案是:ReadView生成的时机不同,导致repeatability级别的readcommitted和snapshotread的结果不同:在repeatableread隔离级别下,当一个事务的第一个snapshotread生成ReadView时,ReadView会记录这次其修改对当前事务不可见的所有其他活动事务的快照。所有早于ReadView创建的事务所做的修改都是可见的;在read-committed隔离级别下,事务每次快照读都会产生一个新的snapshot和ReadView,这就是我们在RC级别下的事务中可以看到事务提交更新的原因;总之,在readcommitted隔离级别下,每次快照读取都会生成并获取最新的ReadView;而在可重复读隔离级别下,是同一个事务中的第一个快照读取才会创建ReadView,后续的快照读取都会获得同一个ReadView。MVCC与幻读幻读是指在同一个事务中,同一条SQL语句执行两次,可能导致结果不同的问题。第二个SQL语句可能会返回以前不存在的行。例如:在T1时刻,同时打开事务A和事务B,分别进行快照读取,然后事务A向数据库中插入一条新记录。如果事务B可以读取这条记录,就会发生“幻读”,因为B在第一次快照读取时没有读取这条数据。MVCC能解决幻读问题吗?答案是有的时候可以解决,有的时候不能。如果事务B中读入的是快照读,那么MVCC版本控制可以解决幻读问题;如果当前读在事务B中使用,那么MVCC就无法解决幻读问题。快照读取基于MVCC和UndoLog实现,适用于简单的Select语句;当前读取是基于Gap锁实现的,适用于Insert,Update,Delete,Select...ForUpdate,Select...LockInShareMode语句,以及锁定的Select语句;事实上,MVCC对所有当前读取都是无效的。例如,事务A修改数据后,事务B更新相应的数据,Update语句的过滤条件针对的是数据库中的当前数据。而不是快照数据。我是狐神,欢迎大家关注我的微信公众号:wzm2zsd参考文档MySQLMVCC与幻读
正确理解MySQLMVCC及实现原理
MySQL数据库事务锁定在各隔离级别情况--readcommitted&&MVCC本文首发于微信公众号,版权所有,禁止转载!