深入了解MySQL中的锁(全局锁、表级锁、行锁)
2、案例一:等值查询间隙锁
1.由于表t中没有id=7的记录,根据原则1,加锁单位是next-key lock,sessionA加锁范围就是(5,10]2.根据优化2,这是一个等值查询(id=7),而id=10不满足查询条件,next-key lock退化成间隙锁,因此最终加锁的范围是(5,10)
所以,sessionB要往这个间隙里面插入id=8的记录会被锁住,但是sessionC修改id=10这行是可以的
3、案例二:非唯一索引等值锁
1.根据原则1,加锁单位是next-key lock,因此会给(0,5]加上next-key lock
2.c是普通索引,因此访问c=5这一条记录是不能马上停下来的,需要向右遍历,查到c=10才放弃。根据原则2,访问到的都要加锁,因此要给(5,10]加next-key lock
3.根据优化2,等值判断,向右遍历,最后一个值不满足c=5这个等值条件,因此退化成间隙锁(5,10)
4.根据原则2,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有任何锁,这就是为什么sessionB的update语句可以执行完成
锁是加在索引上的,在这个例子中,lock in share mode只锁覆盖索引,但是如果是for update,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁,这样的话sessionB的update语句会被阻塞住。如果你要用 lock in share mode 来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段
4、案例三:主键索引范围锁
1.开始执行的时候,要找到第一个id=10的行,因此本该是next-key lock(5,10]。根据优化1,主键id上的等值条件,退化成行锁,只加了id=10这一行的行锁2.范围查询就往后继续找,找到id=15这一行停下来,因此需要加next-key lock(10,15]
所以,sessionA这时候锁的范围就是主键索引上,行锁id=10和next-key lock(10,15]
5、案例四:非唯一索引范围锁
这次sessionA用字段c来判断,加锁规则跟案例三唯一的不同是:在第一次用c=10定位记录的时候,索引c上加上(5,10]这个next-key lock后,由于索引c是非唯一索引,没有优化规则,因此最终sessionA加的锁是索引c上的(5,10]和(10,15]这两个next-key lock6、案例五:唯一索引范围锁bug
sessionA是一个范围查询,按照原则1的话,应该是索引id上只加(10,15]这个next-key lock,并且因为id是唯一键,所以循环判断到id=15这一行就应该停止了但是实现上,InnoDB会扫描到第一个不满足条件的行为止,也就是id=20。而且由于这是个范围扫描,因此索引id上的(15,20]这个next-key lock也会被锁上
所以,sessionB要更新id=20这一行是会被锁住的。同样地,sessionC要插入id=16的一行,也会被锁住
7、案例六:非唯一索引上存在等值的例子
insert into t values(30,10,30);
新插入的这一行c=10,现在表里有两个c=10的行。虽然有两个c=10,但是它们的主键值id是不同的,因此这两个c=10的记录之间也是有间隙的
sessionA在遍历的时候,先访问第一个c=10的记录。根据原则1,这里加的是(c=5,id=5)到(c=10,id=10)这个next-key lock。然后sessionA向右查找,直到碰到(c=15,id=15)这一行,循环才结束。根据优化2,这是一个等值查询,向右查找到了不满足条件的行,所以会退化成(c=10,id=10)到(c=15,id=15)的间隙锁
也就是说,这个delete语句在索引c上的加锁范围,就是下图中蓝色区域覆盖的部分,这个蓝色区域左右两边都是虚线,表示开区间
8、案例七:limit语句加锁
加了limit 2的限制,因此在遍历到(c=10,id=30)这一行之后,满足条件的语句已经有两条,循环就结束了。因此,索引c上的加锁范围就变成了从(c=5,id=5)到(c=10,id=30)这个前开后闭区间,如下图所示:再删除数据的时候尽量加limit,这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围
9、案例八:一个死锁的例子
1.sessionA启动事务后执行查询语句加lock in share mode,在索引c上加了next-key lock(5,10]和间隙锁(10,15)2.sessionB的update语句也要在索引c上加next-key lock(5,10],进入锁等待
3.然后sessionA要再插入(8,8,8)这一行,被sessionB的间隙锁锁住。由于出现了死锁,InnoDB让sessionB回滚
sessionB的加next-key lock(5,10]操作,实际上分成了两步,先是加(5,10)间隙锁,加锁成功;然后加c=10的行锁,这时候才被锁住的
七、用动态的观点看加锁
表t的建表语句和初始化语句如下:
CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`) ) ENGINE=InnoDB; insert into t values(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25);
1、不等号条件里的等值查询
begin; select * from t where id>9 and id<12 order by id desc for update;
利用上面的加锁规则,这个语句的加锁范围是主键索引上的(0,5]、(5,10]和(10,15)。加锁单位是next-key lock,这里用到了优化2,即索引上的等值查询,向右遍历的时候id=15不满足条件,所以next-key lock退化为了间隙锁(10,15)
1.首先这个查询语句的语义是order by id desc,要拿到满足条件的所有行,优化器必须先找到第一个id<12的值
2.这个过程是通过索引树的搜索过程得到的,在引擎内部,其实是要找到id=12的这个值,只是最终没找到,但找到了(10,15)这个间隙
3.然后根据order by id desc,再向左遍历,在遍历过程中,就不是等值查询了,会扫描到id=5这一行,所以会加一个next-key lock (0,5]
在执行过程中,通过树搜索的方式定位记录的时候,用的是等值查询的方法
2、等值查询的过程
begin; select id from t where c in(5,20,10) lock in share mode;
这条in语句使用了索引c并且rows=3,说明这三个值都是通过B+树搜索定位的
在查找c=5的时候,先锁住了(0,5]。但是因为c不是唯一索引,为了确认还有没有别的记录c=5,就要向右遍历,找到c=10确认没有了,这个过程满足优化2,所以加了间隙锁(5,10)。执行c=10会这个逻辑的时候,加锁的范围是(5,10]和(10,15),执行c=20这个逻辑的时候,加锁的范围是(15,20]和(20,25)
这条语句在索引c上加的三个记录锁的顺序是:先加c=5的记录锁,再加c=10的记录锁,最后加c=20的记录锁
select id from t where c in(5,20,10) order by c desc for update;
由于语句里面是order by c desc,这三个记录锁的加锁顺序是先锁c=20,然后c=10,最后是c=5。这两条语句要加锁相同的资源,但是加锁顺序相反。当这两条语句并发执行的时候,就可能出现死锁
八、insert语句的锁为什么这么多?
1、insert … select语句
表t和t2的表结构、初始化数据语句如下:
CREATE TABLE `t` ( `id` int(11) NOT NULL AUTO_INCREMENT, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `c` (`c`) ) ENGINE=InnoDB; insert into t values(null, 1,1); insert into t values(null, 2,2); insert into t values(null, 3,3); insert into t values(null, 4,4); create table t2 like t;
在可重复读隔离级别下,binlog_format=statement时执行下面这个语句时,需要对表t的所有行和间隙加锁
insert into t2(c,d) select c,d from t;
2、insert循环写入
要往表t2中插入一行数据,这一行的c值是表t中c值的最大值加1,SQL语句如下:
insert into t2(c,d) (select c+1, d from t force index(c) order by c desc limit 1);
这个语句的加锁范围,就是表t索引c上的(3,4]和(4,supermum]这两个next-key lock,以及主键索引上id=4这一行
执行流程是从表t中按照索引c倒序吗,扫描第一行,拿到结果写入到表t2中,因此整条语句的扫描行数是1
但如果要把这一行的数据插入到表t中的话:
insert into t(c,d) (select c+1, d from t force index(c) order by c desc limit 1);
explain结果中的Extra字段中Using temporary字段,表示这个语句用到了临时表
执行流程如下:
1.创建临时表,表里有两个字段c和d
2.按照索引c扫描表t,依次取c=4、3、2、1,然后回表,读到c和d的值写入临时表
3.由于语义里面有limit 1,所以只取了临时表的第一行,再插入到表t中
这个语句会导致在表t上做全表扫描,并且会给索引c上的所有间隙都加上共享的next-key lock。所以,这个语句执行期间,其他事务不能在这个表上插入数据
需要临时表是因为这类一边遍历数据,一边更新数据的情况,如果读出来的数据直接写回原表,就可能在遍历过程中,读到刚刚插入的记录,新插入的记录如果参与计算逻辑,就跟语义不符
3、insert唯一键冲突
sessionA执行的insert语句,发生唯一键冲突的时候,并不只是简单地报错返回,还在冲突的索引上加了锁,sessionA持有索引c上的(5,10]共享next-key lock(读锁)在sessionA执行rollback语句回滚的时候,sessionC几乎同时发现死锁并返回
1.在T1时刻,启动sessionA,并执行insert语句,此时在索引c的c=5上加了记录锁。这个索引是唯一索引,因此退化为记录锁
2.在T2时刻,sessionA回滚。这时候,sessionB和sessionC都试图继续执行插入操作,都要加上写锁。两个session都要等待对方的行锁,所以就出现了死锁
4、insert into … on duplicate key update
上面这个例子是主键冲突后直接报错,如果改写成
insert into t values(11,10,10) on duplicate key update d=100;
就会给索引c上(5,10]加一个排他的next-key lock(写锁)
insert into … on duplicate key update的语义逻辑是,插入一行数据,如果碰到唯一键约束,就继续执行后面的更新语句。如果有多个列违反了唯一性索引,就会按照索引的顺序,修改跟第一个索引冲突的行
表t里面已经有了(1,1,1)和(2,2,2)这两行,执行这个语句
主键id是先判断的,MySQL认为这个语句跟id=2这一行冲突,所以修改的是id=2的行
思考题:
1、如果要删除一个表里面的前10000行数据,有以下三种方法可以做到:
-
第一种,直接执行
delete from T limit 10000;
-
第二种,在一个连接中循环执行20次
delete from T limit 500;
-
第三种,在20个连接中同时执行
delete from T limit 500;
选择哪一种方式比较好?
参考答案:
第一种方式,单个语句占用时间长,锁的时间也比较长,而且大事务还会导致主从延迟
第三种方式,会人为造成锁冲突
第二种方式相对较好
以上就是深入了解MySQL中的锁(全局锁、表级锁、行锁)的详细内容,更多请关注毛票票其它相关文章!