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

MVCC的水有点深,但是理解起来真的很爽!

时间:2023-04-01 22:11:27 Java

@[toc]之前写过一篇文章,给大家分享了MySQL中查询表记录数的问题,涉及到一个知识点MVCC多版本并发控制。不懂这个问题,总觉得缺了点什么。所以今天我想抽空跟大家聊一聊MVCC。要理解MVCC,最好理解InnoDB中事务的隔离级别,否则单纯看概念是很难理解MVCC的。1.隔离级别1.1理论上,MySQL中事务有四种隔离级别,分别是:SERIALIZABLE、REPEATABLEREAD、READCOMMITTED和READUNCOMMITTED。隔离级别的含义如下:SERIALIZABLE如果隔离级别是序列化的,则用户当前事务依次执行。此隔离级别提供事务之间的最大隔离。REPEATABLEREAD在此隔离级别,事务不被视为一个序列。但是,当前正在执行的事务的变化对外仍然是不可见的,也就是说,如果用户在另一个事务中多次执行同一个SELECT语句,结果总是一样的。(因为正在执行的事务所产生的数据变化,外部是看不到的)。READCOMMITTEDREADCOMMITTED隔离级别不如REPEATABLEREAD隔离级别安全。READCOMMITTED级别的事务可以看到其他事务所做的数据修改。也就是说,在事务处理过程中,同一个事务的多个SELECT语句,如果其他事务修改了相应的表,可能会返回不同的结果。READUNCOMMITTEDREADUNCOMMITTED提供事务之间的最小隔离。除了容易出现幻读操作和不可重复读操作之外,处于此隔离级别的事务还可以读取其他事务尚未提交的数据。如果这个事务以其他事务未提交的变更为计算依据,那么那些未提交的已提交变更被其父事务撤销,从而导致大量的数据变更。在MySQL数据库中,默认的事务隔离级别是REPEATABLEREAD1.2SQL实践下面通过几个简单的SQL向读者验证以上理论。1.2.1查看隔离级别可以通过如下SQL查看数据库实例默认的全局隔离级别和当前会话的隔离级别:MySQL8之前,使用如下命令查看MySQL隔离级别:SELECT@@GLOBAL.tx_isolation,@@tx_isolation;查询结果如图:可以看到,默认的隔离级别是REPEATABLE-READ,全局隔离级别和当前会话隔离级别都是如此。从MySQL8开始,使用如下命令查看MySQL默认隔离级别:SELECT@@GLOBAL.transaction_isolation,@@transaction_isolation;关键字已更改,但其他所有内容均相同。隔离级别可以通过如下命令修改(建议开发者修改时修改当前会话隔离级别,不要修改全局隔离级别):SETSESSIONTRANSACTIONISOLATIONLEVELREADUNCOMMITTED上面的SQL表示数据库隔离级别为当前会话设置为READUNCOMMITTED。设置成功后,再次查询隔离级别,发现当前session的隔离级别发生了变化,如图1-2所示:注意,如果只修改当前session的隔离级别,改变一个session后,隔离级别将再次改变。它会恢复到默认的隔离级别,所以我们在测试的时候,只要修改当前会话的隔离级别即可。1.2.2READUNCOMMITTED1.2.2.1准备测试数据READUNCOMMITTED是最低的隔离级别。在这个隔离级别里面有脏读、不可重复读和幻读,所以这里我们先来看看这个隔离级别,这样大家就可以明白这三个问题到底是怎么回事了。下面进行介绍。首先创建一个简单的表,预设两个数据,如下:表中的数据很简单,有两个用户javaboy和itboyhub,每个人的账户都有1000元。现在模拟这两个用户之间的转账操作。注意,如果你使用Navicat,不同的查询窗口对应不同的会话。如果使用SQLyog,不同的查询窗口对应同一个会话。因此,如果你使用SQLyog,你需要打开一个新的连接。在新连接中执行查询操作。1.2.2.2脏读当一个事务读取到另一个事务还没有提交的数据时,称为脏读。具体操作如下:首先打开两个SQL操作窗口,假设是A和B,在A窗口输入如下SQL(输入完成后不要执行):STARTTRANSACTION;UPDATEaccountsetbalance=balance+100wherename='javaboy';UPDATEaccountsetbalance=balance-100wherename='itboyhub';COMMIT;在B窗口执行如下SQL,修改默认事务隔离级别为READUNCOMMITTED,如下:SETSESSIONTRANSACTIONISOLATIONLEVELREADUNCOMMITTED接下来在B窗口输入如下SQL,输入完成后,首先执行第一行到开始交易(注意只需要一行):STARTTRANSACTION;从帐户中选择*;犯罪;然后在窗口A执行前两条SQL,即开始交易,给javaboy账户充值100元。进入B窗口,在B窗口执行第二条查询SQL(SELECT*fromuser;),结果如下:可以看到虽然A窗口的事务还没有提交,但是B窗口已经可以查询到数据了相关变化。这就是脏读问题。1.2.2.3不可重复读不可重复读是指一个事务连续读取同一条记录,但两次读取的数据不同,称为不可重复读。具体操作步骤如下(将两个账户中的钱恢复到操作前的1000):首先,打开A、B两个查询窗口,将B的数据库事务隔离级别设置为READUNCOMMITTED。具体的SQL参照上面的,这里不再赘述。在B窗口输入如下SQL,然后只执行前两条SQL,启动事务,查询javaboy的账号:STARTTRANSACTION;从name='javaboy'的帐户中选择*;犯罪;前两条SQL执行结果如下:在windowA中执行如下SQL,给javaboy账户增加100元,如下:STARTTRANSACTION;UPDATEaccountsetbalance=balance+100wherename='javaboy';提交;4。再次回到B窗口,执行第二次Checkjavaboy'saccount,执行如下SQL,结果如下:javaboy'saccount发生变化,即前后两次checkjavaboy'saccount结果不一致,非-可重复读取。和脏读的区别在于,脏读是看其他事务未提交的数据,而不可重复读是看其他事务提交的数据(因为当前SQL也在事务中,可能不想看其他交易已经提交数据)。1.2.2.4幻读幻读与不可重复阅读非常相似。光看名字就是错觉。我举个简单的例子。在窗口A中输入如下SQL:STARTTRANSACTION;insertintoaccount(name,balance)values('zhangsan',1000);COMMIT;然后在窗口B中输入如下SQL:STARTTRANSACTION;SELECT*fromaccount;deletefromaccountwherename='zhangsan';COMMIT;我们执行步骤如下:首先执行窗口B的前两行,开始一个事务,同时查询数据库中的数据。此时查询到的数据只有javaboy和itboyhub。执行窗口A的前两行,在数据库中添加一个名为zhangsan的用户,注意不要提交事务。执行窗口B的第二行,由于脏读问题,此时可以查询到用户zhangsan。执行B窗口第三行,删除名字为zhangsan的记录。这时候删除的时候就会出现问题。B窗口虽然可以查询到zhangsan,但是这条记录还没有提交。这是由于脏读。它就在那里,所以无法删除。就在这个时候,出现了幻觉。有张三,却删不掉。这是幻读。看完上面的案例,你应该明白脏读、不可重复读、幻读是什么意思了。1.2.3与READCOMMITTED和READUNCOMMITTED相比,READCOMMITTED主要解决脏读问题,没有解决不可重复读和幻读问题。将事务的隔离级别改为READCOMMITTED后,对脏读情况重复上述测试,发现脏读问题不复存在;在不可重复读的情况下重复上面的测试,发现不可重复读的问题依然存在。以上案例不适用于幻读测试。我们换一个幻读的测试用例。还有两个窗口A和B,将窗口B的隔离级别改为READCOMMITTED,然后在窗口A中输入如下测试SQL:STARTTRANSACTION;insertintoaccount(name,balance)values('zhangsan',1000);犯罪;在窗口B中输入以下测试SQL:STARTTRANSACTION;SELECT*fromaccount;insertintoaccount(name,balance)values('zhangsan',1000);COMMIT;测试方法如下:先在B窗口执行前两行SQL,打开Transaction查询数据,此时只找到javaboy和itboyhub。在窗口A执行前两行SQL,插入一条记录,但不提交事务。在windowB中执行第二行SQL,由于没有脏读问题,所以此时找不到windowA中添加的数据。在窗口B执行第三行SQL,因为name字段是唯一的,这里不能插入。就在这个时候,出现了幻觉。没有用户zhangsan,但是无法插入zhangsan。1.2.4与READCOMMITTED相比,REPEATABLEREAD进一步解决了不可重复读的问题,但幻读没有。REPEATABLEREAD中的幻读测试和上一节基本相同,不同的是在第二步,记得执行insertSQL后commit事务。由于REPEATABLEREAD解决了不可重复读,即使第二步提交了事务,第三步也找不到提交的数据,第四步继续插入就会出错。请注意,REPEATABLEREAD也是InnoDB引擎的默认数据库事务隔离级别。1.2.5SERIALIZABLESERIALIZABLE提供事务之间的最大隔离。在这种隔离级别下,事务一个接一个地顺序执行,没有脏读和不可重复。阅读和幻读是最安全的。如果当前事务隔离级别设置为SERIALIZABLE,那么此时启动其他事务时,会被阻塞。它必须等待当前事务提交,其他事务才能成功打开。因此,之前的脏读、不可重复读、幻读问题就不存在了。会发生。1.3总结一般来说,隔离级别与脏读、不可重复读、幻读的对应关系如下:隔离级别脏读、不可重复读、幻读允许READUNCOMMITTED,不允许READCOMMITED,并且不允许重复读取。SERIALIZABLEallowed,notallowed,notallowed,notallowed,性能关系如图:宋哥前不久也录制了一个隔离级别的视频,大家可以参考:https://www.bilibili.com/video/BV14L4y1B7mB2。Snapshotread和currentread接下来,我们需要弄清楚另一个问题:snapshotread和currentread。2.1快照读快照读(SnapShotRead)是一种一致的非锁读,这也是InnoDB存储引擎具有如此高并发的核心原因之一。在可重复读隔离级别下,当事务开始时,会为当前库拍摄一张照片(快照),快照读取的数据要么是拍摄照片时的数据,要么是当前事务本身inserted/modified数据。我们日常使用的解锁查询,包括本文第一节涉及的所有查询,都是快照读,这里就不做演示了。2.2当前读对应快照读。当前读取是读取最新的数据,不是数据的历史版本。也就是说,在可重复读隔离级别下,如果当前读被使用,其他读也可以被读到。事务提交的数据。松哥举了个例子:MySQL事务开启了两个会话A和B。首先,在sessionA中开启一个事务,查询id为1的记录:接下来,我们修改sessionB中id为1的数据,如下:注意sessionB不要开启事务或者启用及时提交事务,否则更新statement会占用排他锁,会导致会话A在使用锁时阻塞。接下来回到sessionA继续查询操作,如下:可以看到,sessionA中的第一个查询是snapshotread,读取的是当前事务启动时的数据状态,接下来的两个查询是currentRead,读取最新的数据(sessionB中修改的数据)。3.undolog下面我们来详细了解一下undolog,这也有助于我们理解后面的MVCC。这里我们简单介绍一下。我们知道数据库事务是有回滚能力的。既然是可以回滚的,那么在数据发生变化之前,一定要记录旧的数据,作为以后回滚的依据。那么这条记录就是undolog。当我们要添加一条记录时,在undolog中记录添加的数据id,以后回滚时相应删除数据;当我们要删除或修改数据时,将原始数据记录在undolog日志中,以后根据这些数据恢复。由于查询操作不涉及回滚操作,因此不需要记录在undolog中。4.行格式接下来我们看一下行格式,这也有助于我们理解MVCC。行格式就是InnoDB在保存每一行数据的时候,用什么格式来保存每一行的数据。数据库中有COMPACT、REDUNDANT、DYNAMIC、COMPRESSED等几种行格式,但是无论使用哪种行格式,都无法避免以下隐藏数据列:column1、column2、Column3tocolumnN是我们数据库中表的列,里面存放的是我们正常的数据。除了这些存储数据的列之外,还有另外三个附加数据列。这就是我们在这里要关注的。DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR三列:DB_ROW_ID:该列占6个字节,是行ID,用于唯一标识一行数据。如果用户在建表时没有设置主键,系统会根据该列创建主键索引。DB_TRX_ID:该列占6字节,为交易ID。在InnoDB存储引擎中,当我们要启动一个事务时,我们会向InnoDB事务系统申请一个事务id。这个交易id是一个严格递增的唯一数字。哪个事务修改了当前数据行,就会在当前行中记录对应的事务id。DB_ROLL_PTR:该列占7个字节,是一个回滚指针。这个回滚指针指向一个undolog日志的地址。通过这个undolog日志,可以将这条记录恢复到之前的版本。好的,下面是关于数据行格式的一些内容。5.MVCC有了上一节的初步知识,我们来正式认识一下MVCC。MVCC,英文全称是Multi-VersionConcurrencyControl,中文翻译是多版本并发控制。MVCC的核心思想是保存数据行的历史版本,通过管理数据行的多个版本来实现数据库的并发控制。简单来说,当我们平时看到的一条一条的记录存入数据库时??,可能不是只有一条记录,而是多个历史版本。如下图所示:这张图很好理解,我想大家的MVCC理解不了太多。下面结合不同的隔离级别给大家讲讲这张图。5.1REPEATABLEREAD首先,当我们通过INSERT\DELETE\UPDATE操作一行数据时,会产生一个事务id,这个事务id也会保存在行记录(DB_TRX_ID)中,即当前数据行是记录修改后得到的是哪笔交易。INSERT\DELETE\UPDATE操作会产生对应的undolog日志。每行记录都有一个DB_ROLL_PTR指向undolog日志。通过执行undolog日志,可以将每一行记录恢复到上一条记录,上一条记录,上一条记录。上一条记录...当我们开始一个事务时,我们会先向InnoDB事务系统申请一个事务id。这个id是一个严格递增的数字。系统会在当前事务开启的那一刻创建一个数组,该数组会保存所谓的活跃事务,就是已经开启但还没有提交的事务。这个数组中的最小值很容易理解。有的朋友可能会误以为数组中的最大值就是当前交易的id。其实不一定是这样,可能更大。因为从申请trx_id到创建数组需要时间,在这期间,其他session也可能申请trx_id。当当前事务要查看一行数据时,首先会查看该行数据的DB_TRX_ID:如果这个值等于当前事务id,说明这是被当前事务修改的,数据是可见的。如果这个值小于数组中的最小值,说明当我们启动当前事务时,涉及该行数据修改的事务已经提交,当前数据行可见。如果这个值大于数组中的最大值,说明这一行数据是我们开始事务之后,在我们提交之前,另外一个session也开始了事务修改了这行数据,那么这行数据是不可见的。如果这个值的大小在数组中的最大值和最小值之间(闭区间),并且该值不在数组中,说明这也是一个提交的事务修改的数据,是可见的。如果这个值的大小在数组中的最大值和最小值之间(闭区间),并且这个值在数组中(不等于当前交易id),说明这是一个修改的数据未提交的事务并且不可见。前三种情况应该很好理解,主要是后两种。宋兄举个简单的例子。比如我们有A、B、C、D四个会话,首先A、B、C分别发起一个事务,事务ID分别为3、4、5,然后会话C提交了事务,但是A而B没有。接下来sessionD也启动了一个事务,事务ID为6,所以sessionD启动事务时,数组中的值为[3,4,6]。现在假设有一行数据DB_TRX_ID为5(第4种情况),那么该行数据是可见的(因为在当前事务开启时已经提交);如果有一行数据的DB_TRX_ID为4,则该行不可见(因为未提交)。另外需要注意的是,如果当前事务涉及到数据的更新操作,则更新操作是在当前读的基础上更新的,而不是在快照读的基础上更新的。如果是后者,则可能导致数据丢失。举个例子,假设有下表:现在有两个会话A和B,先在A中启动一个事务:然后在会话B中做一个修改操作(不需要显式启动一个事务,更新SQL会在内部启动一个事务,update事务完成后自动提交事务):接下来回到sessionA,查询记录,发现值没有变化,符合预期(当前隔离级别为可重复读),然后在A中做一个修改操作,修改完成后去查询,如下图:可以看到update其实是在100的基础上更新的,这个很好理解。如果是在99的基础上更新,那么100的更新就会丢失,这显然是错误的。其实MySQL中的update是先读后更新。读取时,默认为当前读取,即锁定。所以在上面的案例中,如果在B会话中显式开启了事务,还没有提交,那么A会话中的update语句就会被阻塞。这就是MVCC,一行记录有多个版本。实现读写并发控制,读写互不阻塞;同时在MVCC中采用了乐观锁,读数据不加锁,写数据只加行锁,减少了死锁的概率;快照读取也可以相应地实现。5.2READCOMMITTEDREADCOMMITTED类似于REPEATABLEREAD,主要区别在于后者在每个事务开始时创建一个一致视图(创建一个数组列出活动事务id),而前者在每个语句执行前重新计算一个新视图处决。所以READCOMMITTED的隔离级别会看到其他session已经提交的数据(即使其他session比当前session晚打开)。6.小结MVCC在一定程度上实现了读写并发,但是只在READCOMMITTED和REPEATABLEREAD这两个隔离级别下有效。READUNCOMMITTED将始终读取最新的数据行,而SERIALIZABLE将锁定所有读取的行,这两者都与MVCC不兼容。嗯,不知道小伙伴们有没有看懂。如果您有任何问题,请留言讨论。