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

Innodb间隙锁实践

时间:2023-04-01 18:39:22 Java

锁概念InnoDB存储引擎包含三种行锁算法,如下:RecordLock:行锁,针对单行记录;GapLock:间隙锁,锁定一个范围,但不包括记录本身;Next-KeyLock:其实就是行锁+间隙锁,包括记录本身和作用域;为什么需要间隙锁?数据库一般有四种隔离级别,其中最常用的是:提交读(Readcommitted)和可重复读(Repeatableread);在提交读隔离级别下会出现不可重复读,在可重复读隔离级别下会出现幻读(PhantomRead);幻读:在同一个事务的情况下,连续两次执行同??一条SQL可能会导致不同的结果;Innodb引擎在可重复读隔离级别下不会出现幻读,主要是因为Innodb提供了多版本并发控制MVCC和间隙锁;commonsnapshotread其实是使用的MVCC,而currentread使用的是gaplock;下面的例子有两点:在Innodb的可重复读隔离级别下,对当前读使用间隙锁来解决幻读问题,所以下面的例子都是基于默认的隔离级别RR;Innodb的锁机制依赖于索引,所以下面的例子都是围绕索引展开的;在没有索引的实战情况下,先创建一个非索引表并初始化数据:mysql>createtablet1(aint);mysql>insertintot1values(1),(3),(5);启动事务1,执行当前读取:mysql>begin;mysql>select*fromt1wherea=3forupdate;上面的事务没有提交,再次启动事务2,阻塞了以下语句:mysql>select*fromt1wherea=3forupdate;mysql>插入t1值(1);mysql>插入t1值(2);mysql>插入t1值(5);mysql>插入t1值(7);但是此时还是可以进行快照读取的:mysql>select*fromt1wherea=3;可以发现在没有索引的情况下,除了snapshot什么都看不到,感觉和表锁一样,表锁也分为读锁和写锁。在写锁的情况下,快照读也被锁定,在读锁的情况下,可以使用快照读,类似于上面没有索引的情况;mysql>锁定表t1读取;##读锁mysql>锁表t1写;##写锁mysql>解锁表;##如果在没有索引的情况下使用表锁解锁,可以通过以下命令查看,先看表锁情况下的insert操作:mysql>SHOWPROCESSLIST;+-----+------+----------------+------+--------+------+-----------------------------+------------------------+|编号|用户|主持人|分贝|命令|时间|状态|信息|+-----+-----+----------------+-----+---------+-------+----------------------------+------------------------+|75|数据中心|本地主机:65316|测试|查询|98|等待表级锁|插入t1值(7)|+-----+------+----------------+------+--------+------+----------------------------+-------------------------+可以发现status栏显示Waitingfortablelevellock,说明当前插入语句正在等待表锁;查看没有索引的情况:mysql>SHOWPROCESSLIST;+-----+------+----------------+------+--------+-----+-------+----------------------------+|Id|User|Host|db|Command|Time|State|信息|+-----+-----+----------------+-----+--------+------+--------+------------------------+|75|数据中心|本地主机:65316|测试|查询|6|更新|插入t1值(7)|+-----+-----+-----------------+-----+---------+--------+--------+----------------------+总结:以上状态不是在等待表锁。实际上,InnoDB会在没有任何索引的情况下使用隐式主键进行锁定。普通索引的话,先建一个普通索引的表,初始化数据:mysql>createtablet2(aint,key(a));mysql>insertintot2values(1),(3),(5);启动事务1,执行当前读取:mysql>begin;mysql>select*fromt2wherea=3forupdate;启动事务2,以下语句被阻塞:mysql>select*fromt2wherea=3forupdate;mysql>insertintot2values(1);mysql>insertintot2values(2);mysql>insertintot2values(4);以下语句不会被阻塞:mysql>insertintot2values(5);mysql>插入t2值(7);mysql>select*fromt2wherea=1forupdate;mysql>select*fromt2wherea=5forupdate;总结:可以发现在使用普通索引的情况下,加锁包括行锁和间隙锁,行锁是a=3行,间隙锁包括(1,3)和(3,5).注意两边都是开区间,所以当前读取a=1和a=5都可以成功;但是在插入数据的时候,可以发现a=5是可以插入的,但是a=1是不能插入的。这是因为Insert操作会检查下一条要插入的记录是否被锁定。如果被锁定,则不能执行,否则可以执行。如果是唯一索引,首先创建一个带有唯一索引的表并初始化数据:mysql>createtablet3(aint,uniquekey(a));mysql>insertintot3values(1),(3),(5);启动事务1,执行当前读取:mysql>begin;mysql>select*fromt3wherea=3forupdate;启动事务2,以下语句被阻塞:mysql>select*fromt3wherea=3forupdate;以下语句不会被阻塞:mysql>select*fromt3wherea=1forupdate;mysql>插入t3值(2);mysql>插入t3值(4);mysql>插入t3值(7);总结:可以发现,当是唯一索引时,此时会降级为行锁,只会锁定当前记录,不会阻塞其他SQL插入;主键索引的作用也是一样的。在无索引+普通索引的情况下,先创建一张无索引和普通索引的表,并初始化数据:mysql>createtablet4(aint,bint,key(a));mysql>insertintot4values(1,1),(5,5),(9,9);因为有多个字段,此时的加锁情况与查询条件有关:条件是字段a启动事务1,执行当前读:mysql>begin;mysql>select*fromt4wherea=5forupdate;+-----+-----+|一个|b|+-----+------+|5|5|+------+-----+同时检测到记录a=5和b=5,所以会根据无索引和普通索引的情况加锁;启动事务2,以下语句被阻塞:mysql>select*fromt4wherea=5forupdate;mysql>插入t4值(1,5);mysql>插入t4值(2,5);mysql>插入t4值(8,5);mysql>select*fromt4whereb=1forupdate;mysql>select*fromt4whereb=2forupdate;mysql>select*fromt4whereb=9forupdate;mysql>select*fromt4whereb=11forupdate;以下语句不会被阻塞:mysql>insertintot4values(9,5);mysql>插入t4值(10,5);这里a上有一个普通索引,所以效果和直接用普通索引差不多;但同时检查的结果还是b,因为b上没有索引,所以对b的一系列操作都被阻塞了;条件是b字段启动事务1,执行当前读:mysql>begin;mysql>select*fromt4whereb=5进行更新;+-----+-----+|一个|b|+-----+-----+|5|5|+------+------+开始事务2,以下语句被阻塞:mysql>insertintot4values(9,5);mysql>insertintot4values(10,5);mysql>select*fromt4wherea=1forupdate;mysql>select*fromt4wherea=9forupdate;可以发现,改变条件后,在a=5的条件下可以插入的数据在b=5的条件下就不能插入了;a区间1和区间9条件下的两个开局也不可查询;应该使用隐式主键加锁,锁住整张表;条件是a+b字段启动事务1,执行当前读:mysql>begin;mysql>select*fromt4whereb=5anda=5forupdate;+-----+-----+|一个|b|+-----+-----+|5|5|+------+-----+启动事务2,如果阻塞的SQL与条件a相同,Mysql执行计划将优先使用索引字段;可以执行以下SQL:mysql>insertintot4values(9,5);mysql>插入t4值(10,5);通过上面的例子,我们大概可以做到以下两点:使用哪些锁与我们使用的查询有很大关系;检测到记录中有多个字段会启动锁机制,即使查询条件中没有该字段;唯一索引+普通索引,先创建唯一索引和普通索引的表,并初始化数据:mysql>createtablet5(aint,bint,uniquekey(a),key(b));mysql>insertintot5values(1,1),(5,5),(9,9);条件是一个字段mysql>begin;mysql>select*fromt5wherea=5forupdate;+-----+-----+|一个|b|+-----+-----+|5|5|+------+-----+这种情况和使用唯一索引基本一样,可以执行如下SQL:mysql>insertintot5values(2,2);mysql>插入t5值(10,2);mysql>select*fromt5wherea=1forupdate;被阻塞的SQL主要包括:mysql>select*fromt5wherea=5forupdate;mysql>select*fromt5whereb=5forupdate;条件是b字段mysql>begin;mysql>select*fromt5whereb=5forupdate;+------+------+|一个|b|+-----+-----+|5|5|+------+-----+this这种情况和使用普通索引基本一样,下面的SQL会阻塞:mysql>insertintot5values(2,3);mysql>插入t5值(2,7);mysql>插入t5值(2,1);以下插入SQL不会阻塞:mysql>insertintot5values(2,9);条件是a+b字段mysql>begin;mysql>select*fromt5whereb=5anda=5forupdate;+------+------+|一个|b|+-----+-----+|5|5|+------+-----+这种情况和使用条件a基本一样,首选唯一索引,可以查看执行计划:mysql>explainselect*fromt5其中b=5和a=5用于更新;+----+-------------+--------+--------+-------------+------+---------+--------+------+------+|编号|选择类型|表|类型|可能的键|钥匙|密钥长度|参考|行|额外|+----+------------+--------+------+---------------+------+--------+--------+------+--------+|1|简单|t5|常量||一个|5|常量|1|空|+----+------------+--------+--------+--------------+------+--------+--------+-----+-------+在上面的例子中,我们发现在多索引的情况下,根据执行计划中使用的索引,会使用什么类型的锁;如果是复合索引,首先创建一个复合索引的表,并初始化数据:mysql>createtablet6(aint,bint,key(a,b));mysql>insertintot6values(1,1),(5,5),(9,9);条件是a字段先开启事务一,执行如下SQL:mysql>begin;mysql>select*fromt6wherea=5forupdate;+------+------+|一个|b|+------+------+|5|5|+------+-----+启动事务2,以下语句全部被阻塞:mysql>insertintot6values(1,2);mysql>insertintot6values(2,2);mysql>插入t6值(7,2);mysql>插入t6值(9,2);除了(9,2)这条记录与使用普通索引不同外,(9,2)之所以在这里被阻塞是因为存在组合索引,导致(9,9)被记录在(9,2)记录之后,索引将是加锁,同理如果是(9,10),则不会加锁;mysql>插入t6值(9,10);mysql>插入t6值(10,2);条件是先打开b字段事务1,执行如下SQL:mysql>begin;mysql>select*fromt6whereb=5forupdate;+-----+-----+|一个|b|+------+-----+|5|5|+------+-----+开始事务2,以下语句被阻塞:mysql>insertintot6values(9,2);mysql>insertintot6values(9,10);mysql>插入t6值(10,2);这个其实很好理解,因为使用了复合索引,会遵循最左匹配的原则。这时候条件b=5实际上是不能使用索引的,所以这个时候和没有索引的情况一样,上面的语句都会被锁住;条件是a+b字段先开启事务1,执行如下SQL:mysql>begin;mysql>select*fromt6whereb=5anda=5forupdate;+-----+-----+|一个|b|+-----+-----+|5|5|+-----+-----+开始事务2,阻塞和非阻塞的情况基本相同,执行计划会使用复合索引;经过上面的例子测试,大致做了以下几点总结:在没有索引的情况下,会使用隐式主键进行加锁,效果是整个表都被加锁;在普通索引的情况下,会使用间隙锁,在当前值的前后加上具有开区间的间隙锁,锁定当前值。添加行锁;Insert操作会检查下一条要插入的记录是否被锁定;在唯一索引的情况下,直接使用行锁,只锁定当前行;具体使用的锁与我们使用的查询有很大关系;检查输出记录中的多个字段将激活锁定机制,即使查询条件中没有该字段;如果有多个查询条件,会根据执行计划选择索引,然后选择相应的锁机制