文章总共分为五个部分:html
大而全版(五合一):InnoDB的锁机制浅析(All in One)mysql
InnoDB常见的锁有Record锁、gap锁、next-key锁、插入意向锁、自增锁等。
下面会对每一种锁给出一个查看锁的示例。sql
常见的锁有Record锁、gap锁、next-key锁、插入意向锁、自增锁等。
下面会对每一种锁给出一个查看锁的示例。数据库
示例的基础是一个只有两列的数据库表。并发
mysql> CREATE TABLE test ( id int(11) NOT NULL, code int(11) NOT NULL, PRIMARY KEY(id), KEY (code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; mysql> INSERT INTO test(id,code) values(1,1),(10,10);
数据表test
只有两列,id
是主键索引,code
是普通的索引(注意,必定不要是惟一索引),并初始化了两条记录,分别是(1,1),(10,10)。
这样,咱们验证惟一键索引就可使用id列,验证普通索引(非惟一键二级索引)时就使用code列。性能
要看到锁的状况,必须手动开启多个事务,其中一些锁的状态的查看则必须使锁处于waiting
状态,这样才能在mysql的引擎状态日志中看到。测试
命令:优化
mysql> show engine innodb status;
这条命令能显示最近几个事务的状态、查询和写入状况等信息。当出现死锁时,命令能给出最近的死锁明细。ui
Record Lock
是对索引记录的锁定。记录锁有两种模式,S模式和X模式。
例如SELECT id FROM test WHERE id = 10 FOR UPDATE;
表示防止任何其余事务插入、更新或者删除id =10
的行。spa
记录锁始终只锁定索引。即便表没有创建索引,InnoDB也会建立一个隐藏的聚簇索引(隐藏的递增主键索引),并使用此索引进行记录锁定。
开启第一个事务,不提交,测试完以后回滚。
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> update test set id=2 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
事务加锁状况
mysql> show engine innodb status\G; ... ------------ TRANSACTIONS ------------ ---TRANSACTION 366811, ACTIVE 690 sec 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 2 MySQL thread id 785, OS thread handle 123145432457216, query id 729076 localhost 127.0.0.1 root ...
能够看到有一行被加了锁。由以前对锁的描述能够推测出,update语句给id=1
这一行上加了一个X锁
。
注意:X锁广义上是一种抽象意义的排它锁,即锁通常分为
X模式
和S模式
,狭义上指row或者index上的锁,而Record锁是索引上的锁。
为了避免修改数据,能够用select ... for update
语句,加锁行为和update
、delete
是同样的,insert
加锁机制较为复杂,后面的章节会提到。
第一个事务保持原状,不要提交或者回滚,如今开启第二个事务。
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> update test set id=3 where id=1;
执行update
时,sql语句的执行被阻塞了。查看下事务状态:
mysql> show engine innodb status\G; ... ------- TRX HAS BEEN WAITING 4 SEC FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 62 page no 3 n bits 72 index PRIMARY of table `test`.`test` trx id 366820 lock_mode X locks rec but not gap waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32 0: len 8; hex 0000000000000001; asc ;; 1: len 6; hex 0000000598e3; asc ;; 2: len 7; hex 7e000001a80896; asc ~ ;; ------------------ ...
喜闻乐见,咱们看到了这个锁的状态。状态标题是'事务正在等待获取锁',描述中的lock_mode X locks rec but not gap
就是本章节中的record记录锁,直译一下'X锁模式锁住了记录'。后面还有一句but not gap
意思是只对record自己加锁,并不对间隙加锁,间隙锁的叙述见下一个章节。
间隙锁做用在索引记录之间的间隔,又或者做用在第一个索引以前,最后一个索引以后的间隙。不包括索引自己。
例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
这条语句阻止其余事务插入10和20之间的数字,不管这个数字是否存在。
间隙能够跨越0个,单个或多个索引值。
间隙锁是性能和并发权衡的产物,只存在于部分事务隔离级别。
select * from table where id=1;
惟一索引能够锁定一行,因此不须要间隙锁锁定。
若是列没有索引或者具备非惟一索引,该语句会锁定当前索引前的间隙。
在同一个间隙上,不一样的事务能够持有上述兼容/冲突表中冲突的两个锁。例如,事务T1如今持有一个间隙S锁,T2能够同时在同一个间隙上持有间隙X锁。
容许冲突的锁在间隙上锁定的缘由是,若是从索引中清除一条记录,则由不一样事务在这条索引记录上的加间隙锁的动做必须被合并。
InnoDB中的间隙锁的惟一目的是防止其余事务插入间隙。
间隙锁是能够共存的,一个事务占用的间隙锁不会阻止另外一个事务获取同一个间隙上的间隙锁。
若是事务隔离级别改成RC,则间隙锁会被禁用。
按照官方文档,where
子句查询条件是惟一键且指定了值时,只有record锁,没有gap锁。
若是where
语句指定了范围,gap锁是存在的。
这里只测试验证一下当指定非惟一键索引的时候,gap锁的位置,按照文档的说法,会锁定当前索引及索引以前的间隙。(指定了非惟一键索引,例如code=10,间隙锁仍然存在)
开启第一个事务,锁定一条非惟一的普通索引记录
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code = 10 for update; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.00 sec)
因为预存了两条数据,row(1,1)和row(10,10),此时这个间隙应该是1<gap<10
。咱们先插入row(2,2)来验证下gap锁的存在,再插入row(0,0)来验证gap的边界。
按照间隙锁的官方文档定义,
select * from test where code = 10 for update;
会锁定code=10
这个索引,而且会锁定code<10
的间隙。
开启第二个事务,在code=10
以前的间隙中插入一条数据,看下这条数据是否可以插入。
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(2,2);
插入的时候,执行被阻塞,查看引擎状态:
mysql> show engine innodb status\G; ... ---TRANSACTION 366864, ACTIVE 5 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1 MySQL thread id 793, OS thread handle 123145434963968, query id 730065 localhost 127.0.0.1 root update insert into test values(2,2) ------- TRX HAS BEEN WAITING 5 SEC FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 63 page no 4 n bits 72 index code of table `test`.`test` trx id 366864 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 8; hex 800000000000000a; asc ;; 1: len 8; hex 000000000000000a; asc ;; ------------------ ...
插入语句被阻塞了,lock_mode X locks gap before rec
,因为第一个事务锁住了1到10之间的gap,须要等待获取锁以后才能插入。
若是再开启一个事务,插入(0,0)
mysql> start transaction; mysql> insert into test values(0,0); Query OK, 1 row affected (0.00 sec)
能够看到:指定的非惟一建索引的gap锁的边界是当前索引到上一个索引之间的gap。
最后给出锁定区间的示例,首先插入一条记录(5,5)
mysql> insert into test values(5,5); Query OK, 1 row affected (0.00 sec)
开启第一个事务:
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code between 1 and 10 for update; +----+------+ | id | code | +----+------+ | 1 | 1 | | 5 | 5 | | 10 | 10 | +----+------+ 3 rows in set (0.00 sec)
第二个事务,试图去更新code=5的行:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> update test set code=4 where code=5;
执行到这里,若是第一个事务不提交或者回滚的话,第二个事务一直等待直至mysql中设定的超时时间。
Next-key锁其实是Record锁和gap锁的组合。Next-key锁是在下一个索引记录自己和索引以前的gap加上S锁或是X锁(若是是读就加上S锁,若是是写就加X锁)。
默认状况下,InnoDB的事务隔离级别为RR,系统参数innodb_locks_unsafe_for_binlog
的值为false
。InnoDB使用next-key锁对索引进行扫描和搜索,这样就读取不到幻象行,避免了幻读
的发生。
幻读是指在同一事务下,连续执行两次一样的SQL语句,第二次的SQL语句可能会返回以前不存在的行。
当查询的索引是惟一索引时,Next-key lock会进行优化,降级为Record Lock,此时Next-key lock仅仅做用在索引自己,而不会做用于gap和下一个索引上。
如上述例子,数据表test
初始化了row(1,1),row(10,10),而后插入了row(5,5)。数据表以下:
mysql> select * from test; +----+------+ | id | code | +----+------+ | 1 | 1 | | 5 | 5 | | 10 | 10 | +----+------+ 3 rows in set (0.00 sec)
因为id
是主键、惟一索引,mysql会作优化,所以使用code
这个非惟一键的二级索引来举例说明。
对于code
,可能的next-key锁的范围是:
(-∞,1] (1,5] (5,10] (10,+∞)
开启第一个事务,在code=5
的索引上请求更新:
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code=5 for update; +----+------+ | id | code | +----+------+ | 5 | 5 | +----+------+ 1 row in set (8.81 sec)
以前在gap锁的章节中介绍了,code=5 for update
会在code=5
的索引上加一个record锁,还会在1<gap<5的间隙上加gap锁。如今再也不验证,直接插入一条(8,8):
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(8);
insert
处于等待执行的状态,这就是next-key锁
生效而致使的结果。第一个事务,锁定了区间(1,5],因为RR的隔离级别下next-key锁
处于开启生效状态,又锁定了(5,10]区间。因此插入SQL语句的执行被阻塞。
解释:在这种状况下,被锁定的区域是
code=5
前一个索引到它的间隙,以及next-key的区域。code=5 for update
对索引的锁定用区间表示,gap锁锁定了(1,5),record锁锁定了{5}索引记录,next-key锁锁住了(5,10],也就是说整个(1,10]的区间被锁定了。因为是for update
,因此这里的锁都是X锁,所以阻止了其余事务中带有冲突锁定的操做执行。
若是咱们在第一个事务中,执行了code>8 for update
,在扫描过程当中,找到了code=10
,此时就会锁住10以前的间隙(5到10之间的gap),10自己(record),和10以后的间隙(next-key)。此时另外一个事务插入(6,6),(9,9)和(11,11)都是不被容许的,只有在前一个索引5及5以前的索引和间隙才能执行插入(更新和删除也会被阻塞)。
插入意向锁在行插入以前由INSERT设置一种间隙锁,是意向排它锁的一种。
在多事务同时写入不一样数据至同一索引间隙的时,不会发生锁等待,事务之间互相不影响其余事务的完成,这和间隙锁的定义是一致的。
假设一个记录索引包含4和7,其余不一样的事务分别插入5和6,此时只要行不冲突,插入意向锁不会互相等待,能够直接获取。参照锁兼容/冲突矩阵。
插入意向锁的例子再也不列举,能够查看gap锁的第一个例子。
自增锁(AUTO-INC Locks)是事务插入时自增列上特殊的表级别的锁。最简单的一种状况:若是一个事务正在向表中插入值,则任何其余事务必须等待,以便第一个事务插入的行接收连续的主键值。
咱们通常把主键设置为AUTO_INCREMENT
的列,默认状况下这个字段的值为0,InnoDB会在AUTO_INCREMENT
修饰下的数据列所关联的索引末尾设置独占锁。在访问自增计数器时,InnoDB使用自增锁,可是锁定仅仅持续到当前SQL语句的末尾,而不是整个事务的结束,毕竟自增锁是表级别的锁,若是长期锁定会大大下降数据库的性能。因为是表锁,在使用期间,其余会话没法插入表中。