大家好,我是小林。之前写过一篇关于MySQL的MVCC工作原理的文章。近日,有读者在网站学习时在评论区指出了一些问题。而且这个知识点很重要,面试问的太频繁了,所以重写了这篇文章!驾驶!正文这是我的钱包,一共100万元。今天我心情很好。我决定转100万元给你。最后的结果肯定是我的余额变成了0元,而你的余额增加了100万元。想想你开心吗?转账这个动作在程序中会涉及到一系列的操作,假设给你转100万的过程包括以下几个步骤:可以看到这个转账过程涉及修改数据库的两个操作。假设执行到第三步后,服务器突然断电,就会发生痛苦的事情。我的账户扣了100万,但是钱没有到你的账户,也就是说100万消失了!要解决这个问题,需要保证转账业务中的所有数据库操作密不可分,要么全部执行成功,要么全部失败,不允许有中间状态数据。数据库中的“事务”可以达到这种效果。我们在转账操作之前启动事务,在所有数据库操作完成后提交事务。对于已经提交的交易,该交易对数据库所做的修改将永久生效。如果中途发生中断或错误,那么事务期间对数据库所做的修改将回滚到事务执行前的状态。交易的特点是什么?事务由MySQL引擎实现,我们常用的InnoDB引擎支持事务。但是,并非所有引擎都可以支持事务。例如,MySQL原生的MyISAM引擎不支持事务,这也是大多数MySQL引擎使用InnoDB的原因。事务看似简单,但是要实现事务,必须遵守四个特性,分别是:原子性:事务中的所有操作要么完成,要么不完成,不会在中间的某个环节结束,发生错误在事务执行过程中,会回滚到事务开始前的状态,就好像事务没有执行过一样;一致性(Consistency):不会因为事务的执行而破坏数据库的完整性,比如表中有一个字段叫name,它有一个唯一约束,即表中的name不能是重复。如果一个事务修改了name字段,但是事务提交后,表中的name就变得不唯一了。这违反了事务的一致性要求,数据库将取消事务并返回到初始化状态。隔离性:数据库允许多个并发事务同时读取、写入和修改其数据的能力。隔离可以防止多个事务并发执行时交叉执行导致的数据不一致。持久性:事务处理结束后,对数据的修改是永久性的,即使系统出现故障也不会丢失。InnoDB引擎是用什么技术来保证事务的这四个特性的呢?持久化由重做日志(redolog)保证;原子性由undolog(回滚日志)保证;隔离性通过MVCC(多版本并发控制)或者锁机制来保证;一致性由持久性+原子性+隔离来保证;这次我们重点讲一下事务的隔离,这也是面试中问的最多的知识点。为什么事务要隔离,我们要知道并发事务会带来什么问题。并行事务会带来哪些问题?MySQL服务器允许多个客户端连接,这意味着MySQL将同时处理多个事务。那么当同时处理多个事务时,可能会出现脏读、不可重复读、幻读等问题。接下来,我将举例说明这些问题是如何发生的。脏读如果一个事务“读取”了另一个“被未提交事务修改的数据”,就说明发生了“脏读”现象。举个栗子。假设同时处理两个事务A和B,事务A开始先从数据库中读取小林的余额数据,然后进行更新操作。如果此时事务A还没有提交事务,而事务B恰好是从数据库中读取小林的余额数据,那么事务B读取的余额数据就是事务A刚才的更新数据,即使事务不是坚定的。因为事务A还没有提交事务,即随时可能有回滚操作。如果事务A在上述情况下回滚,那么事务B刚才获取的数据就是过期数据,这种现象就消除了。称为脏读。不可重复读在一个事务中多次读取相同的数据。如果前后两次读取的数据不一样,说明已经出现了“不可重复读”的现象。举个栗子。假设同时处理了两笔交易A和B,交易A开始从数据库中读取小林的余额数据,然后继续执行代码逻辑处理。在此过程中,如果事务B更新了这条数据并提交了事务,那么当事务A再次读取数据时,会发现前后两次读取的数据不一致,这种现象称为不可重复读。幻读在一个事务中多次查询满足查询条件的“记录数”。如果前后两次查询到的记录条数不一样,说明出现了“幻读”现象。举个栗子。假设同时处理A和B两笔交易,交易A开始查询数据库中账户余额大于100万的记录,发现一共有5条记录,然后交易B也根据相同的搜索条件查询5条记录。接下来,事务A插入一个余额超过100万的账户,提交事务。此时数据库中余额大于100万的账户数变为6,然后事务B再次查询账户余额大于100万的记录。此时查询的记录数为6条,发现读取的记录数和之前的不一样。感觉像是幻觉。这种现象称为幻读。事务的隔离级别是多少?前面我们提到,当多个事务并发执行时,可能会遇到“脏读、不可重复读、幻读”的现象。这些现象会对交易的一致性产生不同的程序影响。脏读:从其他事务中读取未提交的数据;不可重复读:前后读取的数据不一致;幻读:前后读取的记录条数不一致。这三种现象的严重程度排序如下:SQL标准提出了四种隔离级别来避免这些现象。隔离级别越高,性能效率越低。四种隔离级别分别是:readuncommitted(读未提交),指的是当一个事务还没有提交时,它所做的更改可以被其他事务看到;readcommitted表示事务提交后,它所做的更改可以被其他事务看到;可重复读(repeatableread),指的是事务执行过程中看到的数据,与事务启动时看到的数据始终保持一致。MySQLInnoDB引擎的默认隔离级别;可序列化(serializable);会给记录加一个读写锁,当多个事务对这条记录进行读写时,如果发生读写冲突,后面访问的事务必须等待前面的事务执行完成后才能继续执行;按隔离级别排序如下:对于不同的隔离级别,并发事务时可能出现的现象也会不同。也就是说:在“readuncommitted”隔离级别下,可能会出现脏读、不可重复读、幻读;在“readcommitted”隔离级别下,可能会出现不可重复读和幻读,但不会出现脏读现象;在“可重复读”隔离级别下,可能会出现幻读现象,但不可能出现脏读和不可重复读现象;在“序列化”隔离级别下,脏读、不可重复读和幻读现象是不可能发生的。因此,要解决脏读现象,需要升级到“readcommitted”以上的隔离级别;解决不可重复读的现象,需要升级到“可重复读”的隔离级别。但是不建议将隔离级别升级为“序列化”来解决幻读现象,因为这会导致数据库在并发事务中表现不佳。虽然InnoDB引擎默认的隔离级别是“可重复读”,但是它使用next-keylock锁(行锁和间隙锁的组合)锁住记录和记录本身之间的“间隙”,以防止其他事务Insert记录之间的新记录,从而避免幻读。下面举一个具体的例子来说明这四种隔离级别。有一个账户余额表,里面有一条记录:然后有两个并发的事务,事务A只负责查询余额,事务B将我的余额改为200万。以下是按时间顺序执行两个事务的行为:在不同的隔离级别下,事务A执行过程中查询到的余额可能不同:在“readuncommitted”隔离级别下,事务B修改了余额后,虽然事务还没有提交,此时的余额已经可以被事务A看到,所以事务A中查询余额V1的值为200万,余额V2和V3自然是200万;在“读提交”隔离下层,事务B修改余额后,由于事务未提交,事务A中余额V1的值仍然是100万。交易B提交后,交易A可以看到最新的余额数据,所以金额V2和V3为200万;在“可重复读”隔离级别下,事务A只能看到事务启动时的数据,所以余额V1和余额V2的值都是100万。当交易A提交交易时,可以看到最新的余额数据,所以余额V3的值为200万;在“序列化”隔离级别下,当事务B执行将余额从100万变为200万时,因为事务A之前执行了读操作,所以发生了读操作。写冲突,所以会被锁住,事务B可以继续执行,直到事务A提交,所以从A的角度来看,余额V1和V2的值为100万,余额V3的值为200万。这四种隔离级别是如何实现的呢?对于“readuncommitted”隔离级别的事务,由于可以读取未提交事务修改的数据,直接读取最新的数据即可;对于"string"行化隔离级别的事务,通过加读写锁避免并行访问;对于"readcommit"和"repeatableread"隔离级别的事务,通过ReadView实现,即不同的是ReadView创建的时机不一样,可以把ReadView理解为数据的快照,就像用相机拍照一样,把风景定格在某个瞬间。“ReadCommit”隔离级别是重新生成“每条语句执行之前”一个ReadView,而“RepeatableRead”隔离级别是在“开始一个事务时”生成一个ReadView,然后在整个事务期间使用这个ReadView。注意执行“开始事务”"command并不表示事务开始。MySQL中启动事务的命令有两种,即:第一种:begin/starttransaction命令;第二种:starttransactionwithconsistentsnapshot命令;这两种启动事务的命令启动事务的时机不同:执行完begin/starttransaction命令后,并不代表事务已经启动。只有执行完这条命令,执行完增删改查操作的SQL语句,才是事务真正开始的时间;执行starttransactionwithconsistentsnapshot命令后,事??务会立即启动。接下来说说ReadView在MVCC中是如何工作的?阅读视图在MVCC中如何工作?我们需要明白两点:ReadView中四个字段的作用;聚集索引记录隐藏列中的两个事务相关字段;阅读视图到底是什么?ReadView有四个重要的字段:m_ids:指ReadView创建时当前数据库中“活跃事务”的事务id列表。注意是一个列表,“active”Transaction指的是一个已经开始但还没有提交的事务。min_trx_id:指ReadView时当前数据库中“activetransaction”中事务id最小的事务created,即m_ids的最小值。max_trx_id:这个不是m_ids的最大值,而是创建ReadView时应该给当前数据库下一个事务的id值,即“活动事务”中事务id的最大值+1;creator_trx_id:指创建ReadViewView的事务的事务id。知道了ReadView的字段,我们还需要了解聚集索引记录中隐藏的两个列。假设在账户余额表中插入一条小林的余额100万的记录,然后我画出这两个隐藏列,记录的整个示意图如下:对于一个使用InnoDB存储引擎的数据库表,它的集群indexrecord都包含以下两个隐藏列:trx_id,当一个事务改变某条聚簇索引记录时,该事务的transactionid会记录在trx_id隐藏列中;roll_pointer,每次对某条聚簇索引记录进行修改时,会将旧版本的记录写入undolog,然后这个隐藏列就是指向旧版本每条记录的指针,所以修改前的记录可以通过它发现。创建ReadView后,我们可以将记录中的trx_id分为这三种情况:当一个事务访问记录时,除了自己的更新记录一直可见外,还有几种情况:如果记录的trx_id值是小于ReadView中的min_trx_id值,说明该版本的记录是由创建ReadView之前已经提交的事务生成的,所以该版本的记录对当前事务可见。如果该记录的trx_id值大于等于ReadView中的max_trx_id值,则说明该版本记录是由该ReadView创建后启动的事务生成的,因此该版本记录是不可见的到当前交易。如果记录的trx_id值在ReadView的min_trx_id和max_trx_id之间,则需要判断trx_id是否在m_ids列表中:如果记录的trx_id在m_ids列表中,则表示活跃事务生成的版本记录仍然是活跃的(事务还没有提交),所以这个版本的记录对于当前事务是不可见的。如果该记录的trx_id不在m_ids列表中,则说明生成该版本记录的活动事务已经提交,则该版本的记录对当前事务可见。这种控制并发事务访问同一条记录行为的“版本链”称为MVCC(多版本并发控制)。可重复阅读是如何工作的?可重复读隔离级别是在一个事务开始的时候生成一个ReadView,然后在整个事务过程中使用这个ReadView。假设事务A(事务id为51)启动后,事务B(事务id为52)也立即启动,则这两个事务创建的ReadViews如下:事务A和ReadViews的明细事务B如下:在事务A的ReadView中,它的事务id为51,由于是第一个启动的事务,此时活跃事务的事务id列表只有51,最小的事务id在活跃事务的transactionidlist是事务A本身,下一个事务id是52。在事务B的ReadView中,它的transactionid是52。由于事务A是活跃的,此时活跃事务的transactionid列表time是51和52,活跃事务id中最小的事务id是事务A,一个事务id应该是53,那么在可重复读隔离级别下,事务A和事务B分别进行了如下操作ionsinsequence:事务B读取小林的账户余额记录,余额为100万;交易A修改小林账户余额记录为20010000,未提交交易;事务B读取小林的账户余额记录,余额还是100万;交易A提交交易;事务B读取小林的账户余额记录,余额还是100万;接下来,我们来详细分析一下。交易B第一次读取小林的账户余额记录。找到记录后,它会先看这条记录的trx_id。此时发现trx_id为50,小于事务B的ReadView中的min_trx_id值(51),这说明修改这条记录的事务早在事务B开始之前就已经提交了,所以记录该版本的记录对事务B可见,即事务B可以获得这条记录。然后事务A通过update语句修改这条记录(事务还没有提交),把小林的余额改成200万。此时MySQL会记录对应的undolog,并以链表的形式将它们连接在一起,形成版本链,如下图所示:在上图中的“记录字段”中可以看到,由于事??务A修改记录,之前的记录变成旧版本记录,所以最新记录和旧版本记录通过链表连接,最新记录的trx_id就是事务A的事务id(trx_id=51)。然后事务B第二次读取记录,发现这条记录的trx_id值为51,如果在事务B的ReadView的min_trx_id和max_trx_id之间,则需要判断trx_id值是否在m_id的范围。判断结果为Yes,说明这条记录被一个还没有提交的事务修改了。此时事务B不会去读取这个版本的记录。而是沿着undolog链往下找旧版本的记录,直到在事务B的ReadView中找到trx_id“小于”min_trx_id值的第一条记录,这样事务B就可以读取到trx_id为50的记录,也就是小林的余额是100万的记录。最后,当事务A提交事务时,由于隔离级别为“可重复读”,事务B再次读取区域记录时,仍然根据事务启动时创建的ReadView来判断当前版本是否为记录可见。因此,即使事务A将小林的余额修改为200万并提交了事务,当事务B第三次读取记录时,读取的所有记录都是小林的余额为100万的记录。它是这样实现的,在“可重复读”隔离级别下,事务中读取的记录都是事务开始前的记录。读取提交如何工作?读取提交隔离级别在每次读取数据时都会生成一个新的读取视图。也意味着如果在事务中多次读取同一条数据,前后两次读取的数据可能不一致,因为另一个事务可能在这期间修改了记录并提交了事务。读取提交隔离级别如何工作?还是用前面的例子来说说吧。假设事务A(事务id为51)启动后,事务B(事务id为52)也立即启动,然后依次执行以下操作:事务B读取数据(创建ReadView),小林的账户余额为100万;交易A修改数据(交易尚未提交),将小林的账户余额从100万修改为200万;事务B读取数据(创建ReadView),小林账户余额为100万;事务A提交Transaction;事务B读取数据(创建ReadView),小林的账户余额为200万;它是如何工作的?我们重点关注事务B每次读取数据时创建的ReadView。事务B前两次读取数据时创建的ReadView如下图所示:下面分析一下为什么事务B在事务B读取数据时读取不到事务A(未提交的事务)修改的数据第二次?事务B找到小林查看这条记录,会看到这条记录的trx_id为51,介于事务B的ReadView的min_trx_id和max_trx_id之间,接下来需要判断trx_id值是否在范围内m_ids。如果判断的结果是yes,那么说明这条A记录被一个还没有提交的事务修改了。此时事务B不会去读取这个版本的记录。而是沿着undolog链往下找旧版本的记录,直到在事务B的ReadView中找到trx_id“小于”min_trx_id值的第一条记录,那么事务B能读到的就是事务B的trx_id50条记录,即小林的余额为100万的记录。我们来分析一下,为什么事务A提交后,事务B可以读取到事务A修改的数据?事务A提交后,由于隔离级别是“readcommitted”,事务B每次读取数据都会重新读取数据。创建阅读视图。此时事务B第三次读取数据时创建的ReadView如下:【外链图片传输失败,源站可能有防盗链机制,建议保存图片直接上传(img-NhC5bZpC-1648719236189)(https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/mysql/transactionisolation/readcommittransaction2.drawio.png)]当事务B找到小林的记录时,会发现这条记录的trx_id为51,小于事务B的ReadView中的min_trx_id值(52),说明修改这条记录的事务在创建ReadView之前已经提交了,所以这个版本的该记录用于事务B。可见。正是因为在read-committed隔离级别下,事务每次读取数据都会重新创建ReadView,那么同一条数据在事务中被多次读取,前后两次读取的数据可能不一致,因为这段时间可能不一样。事务修改记录并提交事务。总结事务在MySQL引擎层实现。我们常用的InnoDB引擎支持事务。事务的四大特征是原子性、一致性、隔离性和持久性。这次主要讲隔离。当多个事务并发执行时,会造成脏读、不可重复读、幻读等问题。为了避免这些问题,SQL提出了四种隔离级别,分别是readuncommitted、readcommitted、repeatableread、Serialization,隔离级别从左到右依次递增,隔离级别越高,性能越差,默认InnoDB引擎的隔离级别是可重复读。解决脏读现象,必须将隔离级别提升到readcommitted之上的隔离级别,而解决不可重复读现象,则需要将隔离级别提升到repeatableread之上的隔离级别。对于幻读,不建议将隔离级别升级为序列化,因为这会导致数据库并发时性能不佳。虽然InnoDB引擎默认的隔离级别是“可重复读”,但是它使用next-keylock锁(行锁+间隙锁的组合)锁住记录与记录本身之间的“间隙”,以防止其他事务插入新记录记录之间,从而避免幻读。对于“ReadCommit”和“RepeatableRead”隔离级别的事务,它们是通过**ReadView**实现的,它们的区别在于创建ReadView的时机:“ReadCommit”隔离级别是在每个selectwill生成一个新的ReadView,这也意味着如果在事务中多次读取同一条数据,前后两次读取的数据可能会不一致,因为另一个事务可能在这期间修改了记录,而该事务是坚定的。“可重复读”隔离级别是在一个事务启动时生成一个ReadView,然后在整个事务过程中使用这个ReadView,从而保证事务过程中读取的数据都是事务启动前的记录。这两个隔离级别的实现是通过比较“事务的读视图中的字段”和“记录中的两个隐藏列”来控制并发事务访问同一条记录的行为。这称为MVCC(多版本并发控制)。在可重复读隔离级别,普通的select语句是基于MVCC的快照读,即不会加锁。select..forupdate语句不是snapshotread,而是currentread,即每次读取都会得到最新版本的数据,但是会在readrecord上加一个next-keylock。
