当前位置: 首页 > 科技观察

MySQL的不同隔离级别使用什么锁?

时间:2023-03-14 12:00:49 科技观察

大家好,我是树哥。如果查询或者更新的时候数据很多,行锁会升级为表锁吗?另外,有朋友留言说:不同的隔离级别可能使用不同的锁,可以结合隔离级别说说。其实上面虽然有两个问题,但是弄清楚了不同隔离级别下的锁问题,那么第一个问题自然就清楚了。今天小编就带大家说说不同隔离级别下都使用了哪些锁!文章的思维导图讲解了MySQL的锁机制。在深入讨论不同隔离级别的锁内容之前,我们需要先回顾一下MySQL锁的本质和一些基本内容,对后面的理解有帮助。对于MySQL,如果只支持串口访问,效率会很低。因此,为了提高数据库的运行效率,MySQL需要支持并发访问。在并发访问的情况下,会出现各种问题,比如:脏读、不可重复读、幻读等问题。为了解决这些问题,事务隔离级别出现了。本质上,事务隔离级别是为了解决并发访问下的数据一致性问题。不同的事务隔离级别解决不同程度的数据一致性。我们所说的全局锁、表锁、行级锁等,其实都是事务隔离级别的具体实现。MVCC和意向锁是一些局部的性能优化。上面这段话,基本上算是把MySQL的锁机制理解透彻了。当我们理解了这些概念之间的关系时,我们就能更清楚地理解知识点。事务隔离级别相信大家都知道。MySQL有四种事务隔离级别如下:READCOMMITTEDREADCOMMITTEDREPEATABLEREADSerializedREADUNCOMMITted,可以读到其他尚未被事务提交的Data。在这个隔离级别下,由于可以读取未提交的值,所以会出现“脏读”的问题。例如:事务A更新了价格为30,但是还没有提交。此时事务B读到的价格是30,但是后面的事务A回滚了,所以事务B读到的价格是错误的(脏的)。Readcommitted,只能读取其他事务已经提交的数据。这种隔离级别解决了脏读的问题,不会读取未提交的值,但是会造成“不可重复读”的问题。“不可重复读”是指在同一个事务范围内,前后两次读取的数据是不同的。例如:交易A第一次读取价格10。然后事务B将价格更新为20,然后事务A再次将价格读取为30。A事务前后两次读取的数据不一样,是不可重复读。思考题:MySQLreadcommitted可以解决脏读问题,那么它是怎么解决的呢?可重复读是指同一事务范围内读取的数据是一致的。这个隔离级别解决了“不可重复读”的问题。只要是在同一个事务范围内,读取的数据都是一样的。对于MySQLInnodb来说,其实是通过MVCC来实现的。但是,“可重复读”隔离级别会造成幻读问题,即对于一定范围的数据读取,前后两次读取的结果可能不同。例如:数据库中有价格分别为1、3、5的三种商品。此时事务A查询price<10的商品,找到3个商品。然后事务B插入了一个价格为7的商品,然后事务A继续查询价格<10的商品,但是这次查到了4个商品。可以看出,“幻读”与“不可重复读”有些相似,但“不可重复读”更多的是指某条记录,而“幻读”是指一定范围内的数据。对于MySQLInnodb,它通过行级锁级GapLock来解决幻读问题。序列化是指所有事务的串行执行。这个是最简单的,不需要比拼,一个一个去执行,但是效率也是最低的。MySQL中的MySQL锁分为三种:全局锁、表级锁和行级锁。关键的是表级锁和行级锁。对于表级锁,分为表锁、元数据锁、意向锁三种。对于元数据锁,基本上是数据库自己操作的,我们不需要关心。在Innodbstorage存储引擎中,表锁的使用也比较少。对于行级锁,它还记录锁、间隙锁和Next-Key锁。记录锁是索引记录上的锁,间隙锁是两个索引记录之间的间隙锁,Next-Key是前两者的组合。在Innodb存储引擎中,我们可以使用如下命令来查询锁的状态。//打开锁日志setglobalinnodb_status_output_locks=on;//查看innodb引擎的信息(包括锁信息)showengineinnodbstatus\G;查询结果大致如下图所示:以上不同类型的锁,它们各自的关键字是:table-levelintentexclusivelock(IX):锁模式IX。表级插入意图锁(LOCK_INSERT_INTENTION):lock_modeXlocksgapbeforerecinsertintention行级记录锁(LOCK_REC_NOT_GAP):lock_modeXlocksrecbutnotgap行级间隙锁(LOCK_GAP):lock_modeXlocksgapbeforerecrowLevelNext-keylock(LOCK_ORNIDARY):lock_modeX通过上面的命令,我们可以知道不同的事务隔离级别使用了哪些锁。接下来我们一一来看:用什么锁来实现不同的事务隔离级别。ReadUncommitted首先,我们创建一个price_test表并插入一些测试数据。//创建price_test表CREATETABLE`test`.`price_test`(`id`BIGINT(64)NOTNULLAUTO_INCREMENT,`name`varchar(32)notnull,`price`INTEGER(4)NULL,PRIMARYKEY(`id`));//插入测试数据INSERTINTOprice_test(name,price)values('apple',10);然后,我们打开两个命令行窗口,将事务隔离级别修改为“readuncommitted”。//设置隔离级别SETsessionTRANSACTIONISOLATIONLEVELREADUNCOMMITTED;//检查隔离级别select@@transaction_isolation;然后事务A执行如下命令查询id为1的记录的price值。//执行命令beign;select*fromprice_testwhereid=1;//执行结果+----+------+------+|编号|姓名|价格|+----+-------+-------+|1|苹果|10|+----+--------+--------+1rowinset(0.00sec)接下来,事务B执行以下命令并将价格更改为20.begin;updateprice_testsetprice=20whereid=1;然后,事务A再次读取id为1的记录的price值。从price_test中选择*,其中id=1;从下图可以看出,事务A读取了事务B未提交的数据,实际上是脏读。从这个例子中,我们可以得出一些结论:在“readuncommitted”事务隔离级别下,读写可以同时进行,不会阻塞。看到这里,突然想到一个问题:写写会不会被屏蔽?接下来我们继续做一个测试:事务A和事务B同时更新id为1的记录,看是否更新成功。如上图所示,我先在事务A(上窗口)中执行如下命令,将价格改为15。begin;updateprice_testsetprice=15whereid=1;结果执行成功了,但是事务A还没有提交。接下来我先在事务B(下图窗口)中执行如下命令,将价格改为20。从图中可以看出,事务B被阻塞卡住了。从这个例子中我们可以得出一个结论:在“readuncommitted”事务隔离级别下,写和写不能同时进行,会被阻塞。此时,我们通过查看锁信息可以看出,添加了一个行级记录锁,如下图所示。当我使用rollback命令回滚事务A时,立即执行了事务B,同时事务A也读取了事务B设置的值,如下图所示。有朋友会说:如果指定非索引列作为查询条件,会不会触发间隙锁?接下来我们测试一下。我们在price_test表中再插入一条数据,数据库中的数据如下。接下来,我们在事务A中执行如下命令查询价格>15的记录。mysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>select*fromprice_testwhereprice>15forupdate;+----+--------+--------+|编号|姓名|价格|+----+--------+--------+|2|橙色|30|+----+---------+--------+1rowinset(0.00sec)接下来,我们在事务B中执行以下命令,查询价格>的记录5.begin;select*fromprice_testwhereprice>5进行更新;从下面的结果可以看出,事务B被阻塞了。此时我们查看事务A中的锁情况,如下图所示。从上图可以看出,MySQL只加了记录锁,并没有加间隙锁。最后总结一下:在“readuncommitted”隔离级别下,可以同时进行读和写操作,但是不能同时进行写和写操作。同时,该隔离级别下只会使用行级记录锁,不会使用间隙锁。ReadCommitted在“ReadCommitted”隔离级别下,我们像以前一样进行测试。首先,让我们将隔离级别设置为“readcommitted”。//设置隔离级别SETsessionTRANSACTIONISOLATIONLEVELREADCOMMITTED;//查看隔离级别select@@transaction_isolation;接下来我们测试同时更新id为1的数据,看看会发生什么。事务A执行以下命令:begin;updateprice_testsetprice=15whereid=1;事务B执行以下命令begin;updateprice_testsetprice=20whereid=1;事务B被阻止。查看解锁信息,如下图所示。可以看出该锁是行级记录锁,结果与“读未提交”相同。接下来我们继续看范围查询是否会触发间隙锁。交易A执行:begin;select*fromprice_testwhereprice>5forupdate;事务B执行:begin;select*fromprice_testwhereprice>15forupdate;事务B会被阻塞,锁信息可以如下图查看。可以看到仍然只有一个行级记录锁,并没有间隙锁。看到这里,你会发现“readcommitted”和“readuncommitted”非常相似。那么它们之间有什么区别呢?其实它们最大的区别就是“readcommitted”解决了脏读的问题。Repeatableread在“readcommitted”隔离级别下,我们和之前一样测试。首先,让我们将隔离级别设置为“readcommitted”。//设置隔离级别SETsessionTRANSACTIONISOLATIONLEVELREPEATABLEREAD;//查看隔离级别select@@transaction_isolation;接下来我们测试同时更新id为1的数据,看看会发生什么。事务A执行以下命令:begin;updateprice_testsetprice=15whereid=1;事务B执行以下命令:begin;updateprice_testsetprice=20whereid=1;事务B被阻止。查看锁信息,毫无疑问,这里只会有间隙锁,因为指定了索引。接下来我们继续看范围查询是否会触发间隙锁。交易A执行:begin;select*fromprice_testwhereprice>5forupdate;事务B执行:begin;select*fromprice_testwhereprice>15forupdate;事务B会被阻塞,锁信息可以如下图查看。可以看到,这里就变成了Next-Key锁,它是记录锁和间隙锁的组合。总结一下:在“可重复读”隔离级别下,使用了三种锁:记录锁、间隙锁和next-key锁。值得一提的是,前面我们说过:可重复读存在幻读问题,但实际上在MySQL中,由于使用了间隙锁,在“可重复读”的隔离级别下是不存在幻读的。问题。因此,MySQL使用“可重复读取”作为其默认隔离级别。总结看到这里,我想我们可以回答文章开头提出的问题了:MySQL针对不同的隔离级别使用什么样的锁?对于任何隔离级别,都会使用表级表锁、元数据锁和意向锁,但行级锁略有不同。在“readuncommitted”和“readcommitted”隔离级别下,只会使用record锁,不会使用gap锁,当然也不会有Next-Key锁。对于“可重复读取”隔离级别,使用记录锁、间隙锁和Next-Key锁。今天我们从隔离级别的角度来看锁的应用,但是什么时候会用到记录锁呢?什么时候使用间隙锁?以后有机会再说这部分的问题。