单条索引记录上加锁,record lock锁住的永远是索引,而非记录自己,即便该表上没有任何索引,那么innodb会在后台建立一个隐藏的汇集主键索引,那么锁住的就是这个隐藏的汇集主键索引。因此说当一条sql没有走任何索引时,那么将会在每一条汇集索引后面加X锁,这个相似于表锁,但原理上和表锁应该是彻底不一样的。
html
在索引记录之间的间隙中加锁,或者是在某一条索引记录以前或者以后加锁,并不包括该索引记录自己。gap lock的机制主要是解决可重复读模式下的幻读问题,关于幻读的演示和gap锁如何解决了幻读。关于这一块,先给出几个定义
mysql
简单的select操做,没有lock in share mode或for update,快照读不会加任何的锁,并且因为mysql的一致性非锁定读的机制存在,任何快照读也不会被阻塞。可是若是事务的隔离级别是SERIALIZABLE的话,那么快照读也会被加上共享的next-key锁,本文不对SERIALIZABLE隔离级别作叙述。
程序员
官方文档的术语叫locking read,也就是insert,update,delete,select..in share mode和select..for update,当前读会在全部扫描到的索引记录上加锁,无论它后面的where条件到底有没有命中对应的行记录。当前读可能会引发死锁。sql
innodb的意向锁主要用户多粒度的锁并存的状况。好比事务A要在一个表上加S锁,若是表中的一行已被事务B加了X锁,那么该锁的申请也应被阻塞。若是表中的数据不少,逐行检查锁标志的开销将很大,系统的性能将会受到影响。为了解决这个问题,能够在表级上引入新的锁类型来表示其所属行的加锁状况,这就引出了“意向锁”的概念。数据库
举个例子,若是表中记录1亿,事务A把其中有几条记录上了行锁了,这时事务B须要给这个表加表级锁,若是没有意向锁的话,那就要去表中查找这一亿条记录是否上锁了。若是存在乎向锁,那么假如事务A在更新一条记录以前,先加意向锁,再加X锁,事务B先检查该表上是否存在乎向锁,存在的意向锁是否与本身准备加的锁冲突,若是有冲突,则等待直到事务A释放,而无须逐条记录去检测。事务B更新表时,其实无须知道到底哪一行被锁了,它只要知道反正有一行被锁了就好了。session
说白了意向锁的主要做用是处理行锁和表锁之间的矛盾,可以显示“某个事务正在某一行上持有了锁,或者准备去持有锁”
并发
下文之知乎大神观点:https://www.zhihu.com/question/51513268ide
innodb的意向锁有什么做用? mysql官网上对于意向锁的解释中有这么一句话
“The main purpose of IX and IS locks is to show that someone is locking a row, or going to lock a row in the table.”
意思是说加意向锁的目的是为了代表某个事务正在锁定一行或者将要锁定一行。
那么,意向锁的做用就是“代表”加锁的意图,但是为何要代表这个 意图呢?
若是仅仅锁定一行仅仅须要加一个锁,那么就直接加锁就行了,这里要代表加锁意图的缘由是由于要锁定一行不只仅是要加一个锁,而是要作一系列操做吗? 做者:尹发条地精 我最近也在看这个,我说一下个人理解
①在mysql中有表锁,LOCK TABLE my_tabl_name READ; 用读锁锁表,会阻塞其余事务修改表数据。LOCK TABLE my_table_name WRITe; 用写锁锁表,会阻塞其余事务读和写。
②Innodb引擎又支持行锁,行锁分为共享锁,一个事务对一行的共享只读锁。排它锁,一个事务对一行的排他读写锁。
③这两中类型的锁共存的问题考虑这个例子:
事务A锁住了表中的一行,让这一行只能读,不能写。以后,事务B申请整个表的写锁。若是事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。
数据库须要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。
数据库要怎么判断这个冲突呢?
step1:判断表是否已被其余事务用表锁锁表
step2:判断表中的每一行是否已被行锁锁住。
注意step2,这样的判断方法效率实在不高,由于须要遍历整个表。
因而就有了意向锁。在乎向锁存在的状况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。在乎向锁存在的状况下,
上面的判断能够改为
step1:不变
step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,所以,事务B申请表的写锁会被阻塞。
注意:申请意向锁的动做是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不须要咱们程序员使用代码来申请。
总结:为了实现多粒度锁机制(白话:为了表锁和行锁都能用)
指的是在同一个事务中,连续几回快照读,读取的记录应该是同样的post
不可重复读的演示较为简单,本文不作讨论。性能
指的是在一个事务A中执行了一个当前读操做,而另一个事务B在事务A的影响区间内insert了一条记录,这时事务A再执行一个当前读操做时,出现了幻行。这和不可重复读的主要区别就在与事务A中一个是快照读,一个当前读;而且事务B中一个是任何的dml操做,一个只是insert。好比在A中select * from test where id<10 lock in share mode结果集为(1,2,3),这时在B中对test表插入了一条记录4,这时在A中从新查询结果集就是(1,2,3,4),和事务A在第一次查询出来的结果集不一致,这里的4就是幻行。
演示条件:因为可重读的隔离级别下,默认采用Next-Key Locks,就是Record lock和gap lock的结合,即除了锁住记录自己,还要再锁住索引之间的间隙,因此这个gap lock机制默认打开,并不会产生幻行,那么咱们要演示幻行的话,要么将隔离级别改成read-commited,要么在REPEATABLE-READ模式下禁用掉gap lock,这里咱们采用的是第二种方式。
在演示以前又引入了innodb_locks_unsafe_for_binlog参数,该参数能够禁用gap lock。
innodb_locks_unsafe_for_binlog:静态参数,默认为0,表示启动gap lock,若是设置为1,表示禁用gap lock,这时mysql就只有record lock了,不过值得注意的是,即便了设置了1,关于外键和惟一键重复检查方面用到的gap lock依旧有效。这时能够简单地理解成事务的隔离级别退化成可重复读,而后二者应该仍是有所区别的。建议是不要随便设置,咱们这里设置只是作个简单的幻读演示,mysql后续的版本可能都会废弃掉这个参数了。
session 1 先将myid>95的记录加一个当前读
mysql> show create table test_gap_lock\G
*************************** 1. row ***************************
Table: test_gap_lock
Create Table: CREATE TABLE `test_gap_lock` (
`id` int(11) NOT NULL,
`name` varchar(100) DEFAULT NULL,
`myid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_name` (`name`),
KEY `idex_myid` (`myid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> begin;
mysql> select * from test_gap_lock where myid>95 for update;
+----+------------+------+
| id | name | myid |
+----+------------+------+
| 1 | jiang | 99 |
| 2 | hubingmei | 99 |
| 5 | hubingmei4 | 100 |
+----+------------+------+
3 rows in set (0.00 sec)
session 2 这时session 2插入myid=98的记录成功了。
insert into test_gap_lock values(6,'jiang2',98);
Query OK, 1 row affected (0.00 sec)
session 1 这时session 1再次查看时发现记录myid=98的记录已经存在了,这条记录就是幻行。
mysql> select * from test_gap_lock where myid>95 for update;
+----+------------+------+
| id | name | myid |
+----+------------+------+
| 1 | jiang | 99 |
| 2 | hubingmei | 99 |
| 5 | hubingmei4 | 100 |
| 6 | jiang2 | 98 |
+----+------------+------+
4 rows in set (0.00 sec)
条件:咱们再把innodb_locks_unsafe_for_binlog值改回默认值0,而且tx_isolation为REPEATABLE-READ,演示时务必explain下,确保该sql走了非惟一索引idx_myid(由于若是测试数据较少的话,可能优化器直接走全表扫描,那就致使锁住全部记录,没法模拟出gap锁)
mysql> show create table test_gap_lock\G
*************************** 1. row ***************************
Table: test_gap_lock
Create Table: CREATE TABLE `test_gap_lock` (
`id` int(11) NOT NULL,
`name` varchar(100) DEFAULT NULL,
`myid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_name` (`name`),
KEY `idex_myid` (`myid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
session 1 先explain确保session的当前读sql执行走了索引idx_myid
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> explain select * from test_gap_lock where myid>100 for update;
+----+-------------+---------------+-------+---------------+-----------+---------+------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------+-------+---------------+-----------+---------+------+------+-----------------------+
| 1 | SIMPLE | test_gap_lock | range | idex_myid | idex_myid | 5 | NULL | 2 | Using index condition |
+----+-------------+---------------+-------+---------------+-----------+---------+------+------+-----------------------+
1 row in set (0.00 sec)
mysql> select * from test_gap_lock where myid>100 for update;
+----+------------+------+
| id | name | myid |
+----+------------+------+
| 5 | hubingmei4 | 101 |
| 98 | test | 105 |
+----+------------+------+
2 rows in set (0.00 sec)
session 2 先插入myid=56的成功,由于锁住的间隙是myid>100,56不在该范围内;再插入myid=109时,会一直卡住直到session 1commit,rollback或者直接锁等待超时,在锁等待超时前在session 1中执行一样的sql,获得的结果依旧只有id=5,98的记录,这样就避免了幻读问题
mysql> insert into test_gap_lock values(999,'test2',56);
Query OK, 1 row affected (0.00 sec)
mysql> insert into test_gap_lock values(123,'test4',109);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from test_gap_lock;
+-----+------------+------+
| id | name | myid |
+-----+------------+------+
| 1 | jiang | 98 |
| 2 | hubingmei | 99 |
| 5 | hubingmei4 | 101 |
| 6 | jiang2 | 100 |
| 7 | jiang22 | 70 |
| 67 | jiang222 | 80 |
| 98 | test | 105 |
| 123 | test4 | 109 |
| 999 | test2 | 56 |
+-----+------------+------+
9 rows in set (0.00 sec)
session 1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> explain delete from test_gap_lock where myid=100;
+----+-------------+---------------+-------+---------------+-----------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------+-------+---------------+-----------+---------+-------+------+-------------+
| 1 | SIMPLE | test_gap_lock | range | idex_myid | idex_myid | 5 | const | 2 | Using where |
+----+-------------+---------------+-------+---------------+-----------+---------+-------+------+-------------+
1 row in set (0.00 sec)
mysql> delete from test_gap_lock where myid=100;
Query OK, 2 rows affected (0.00 sec)
session 2 插入myid=99的记录依旧阻塞,存在gap锁;插入myid=97的记录成功
mysql> insert into test_gap_lock values(676,'gap recored test',99);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into test_gap_lock values(675,'gap recored test1',97);
Query OK, 1 row affected (0.00 sec)
mysql> select * from test_gap_lock ;
+-----+------------+------+
| id | name | myid |
+-----+------------+------+
| 1 | jiang | 98 |
| 2 | hubingmei | 98 |
| 5 | hubingmei4 | 100 |
| 6 | jiang2 | 100 |
| 7 | jiang22 | 70 |
| 67 | jiang222 | 80 |
| 98 | test | 105 |
| 123 | test4 | 109 |
| 999 | test2 | 56 |
+-----+------------+------+
9 rows in set (0.00 sec)
session 1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> explain select * from test_gap_lock where id > 100 for update;
+----+-------------+---------------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | test_gap_lock | range | PRIMARY | PRIMARY | 4 | NULL | 2 | Using where |
+----+-------------+---------------+-------+---------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)
mysql> select * from test_gap_lock where id > 100 for update;
+-----+-------+------+
| id | name | myid |
+-----+-------+------+
| 123 | test4 | 109 |
| 999 | test2 | 56 |
+-----+-------+------+
2 rows in set (0.00 sec)
session 2(id=3可插入;id=108没法插入,存在gap lock;id=123的记录没法select..in share mode,由于该记录上存在record lock;id=125能够被select..in share mode和update,这点比较奇怪,应该这也算是当前读,不事后来查看官方文档得知,gap锁只会阻塞insert操做,由于gap间隙中是不存在任何记录的,除了insert操做,其余的操做结果应该都等价于空操做,mysql就不去阻塞它了)
mysql> insert into test_gap_lock values(108,'gap lock test3',123);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into test_gap_lock values(3,'gap lock test3',123);
Query OK, 1 row affected (0.00 sec)
mysql> select * from test_gap_lock where id=125 lock in share mode;
Empty set (0.00 sec)
mysql> explain select * from test_gap_lock where id=125 lock in share mode;
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------------------------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Impossible WHERE noticed after reading const tables |
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------------------------------+
1 row in set (0.00 sec)
mysql> update test_gap_lock set myid=12345 where id=125;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
1 事务隔离级别为REPEATABLE-READ,innodb_locks_unsafe_for_binlog参数为0,且sql走的索引为非惟一索引
2 事务隔离级别为REPEATABLE-READ,innodb_locks_unsafe_for_binlog参数为0,且sql是一个范围的当前读操做,这时即便不是非惟一索引也会加gap lock
针对上面的范例1(非惟一索引+范围当前读)和范例3(主键索引+范围当前读)比较好理解,那为何范例2(非主键索引+等值当前读)为何也会产生gap lock,这要从btree 索引的原理讲起,咱们都知道,btree索引是按照顺序排列的,而且innodb存在主键汇集索引,本人绘图能力有限,已范例2的加锁过程分析举例,手写加锁过程以下图
从图中的数据组织顺序能够看出,myid=100的记录有两条,若是加gap锁就会产生三个间隙,分别是gap1(98,100),gap2(100,100),gap3(100,105),在这三个开区间(若是我高中数学没记错的话)内的myid数值没法插入,显然gap1还有(myid=99,id=3)(myid
=99,id=4)等记录,gap2无实际的间隙,gap3还有(myid=101,id=7)等记录。而且,在myid=100的两条记录上加了record lock,也就是这两条数据业务没法被其余session进行当前读操做(范例三能够看出)
在默认状况下,mysql的事务隔离级别是可重复读,而且innodb_locks_unsafe_for_binlog参数为0,这时默认采用next-key locks。所谓Next-Key Locks,就是Record lock和gap lock的结合,即除了锁住记录自己,还要再锁住索引之间的间隙。
下面咱们针对大部分的SQL类型分析是如何加锁的,假设事务隔离级别为可重复读。
不加任何类型的锁
在扫描到的任何索引记录上加共享的(shared)next-key lock,还有主键汇集索引加排它锁
在扫描到的任何索引记录上加排它的next-key lock,还有主键汇集索引加排它锁
在扫描到的任何索引记录上加next-key lock,还有主键汇集索引加排它锁
简单的insert会在insert的行对应的索引记录上加一个排它锁,这是一个record lock,并无gap,因此并不会阻塞其余session在gap间隙里插入记录。不过在insert操做以前,还会加一种锁,官方文档称它为insertion intention gap lock,也就是意向的gap锁。这个意向gap锁的做用就是预示着当多事务并发插入相同的gap空隙时,只要插入的记录不是gap间隙中的相同位置,则无需等待其余session就可完成,这样就使得insert操做无须加真正的gap lock。想象一下,若是一个表有一个索引idx_test,表中有记录1和8,那么每一个事务均可以在2和7之间插入任何记录,只会对当前插入的记录加record lock,并不会阻塞其余session插入与本身不一样的记录,由于他们并无任何冲突。
假设发生了一个惟一键冲突错误,那么将会在重复的索引记录上加读锁。当有多个session同时插入相同的行记录时,若是另一个session已经得到改行的排它锁,那么将会致使死锁。
mysql> show create table t1\G
*************************** 1. row ***************************
Table: t1
Create Table: CREATE TABLE `t1` (
`i` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`i`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
session 1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO t1 VALUES(1);
Query OK, 1 row affected (0.00 sec)
session 2 这时session2一直被卡住
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO t1 VALUES(1);
session 3 这时session3也一直被卡住
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO t1 VALUES(1);
session 1 这时咱们回滚session1
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
发现session 2的insert成功,而session3检测到死锁回滚
session 2 Query OK, 1 row affected (28.87 sec)
session 3 ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
首先session1插入一条记录,得到该记录的排它锁,这时session2和session3都检测到了主键冲突错误,可是因为session1并无提交,因此session1并不算插入成功,因而它并不能直接报错吧,因而session2和session3都申请了该记录的共享锁,这时还没获取到共享锁,处于等待队列中。这时session1 rollback了,也就释放了该行记录的排它锁,那么session2和session3都获取了该行上的共享锁。而session2和session3想要插入记录,必须获取排它锁,但因为他们本身都拥有了共享锁,因而永远没法获取到排它锁,因而死锁就发生了。若是这时session1是commit而不是rollback的话,那么session2和session3都直接报错主键冲突错误。查看死锁日志也是一目了然
另一个相似的死锁是session1删除了id=1的记录并未提交,这时session2和session3插入id=1的记录。这时session1 commit了,session2和session3须要insert的话,就须要获取排它锁,那么死锁也就发生了;session1 rollback,则session2和session3报错主键冲突。这里再也不作演示。