InnoDB为锁定一行提供了三种算法
Record Lock:锁定一行
Gap Lock:锁定一个范围,但是不包含当前行
Next_key Lock:锁定一个范围,同时包含当前行
对于Next_Key Lock,如果我们锁定了一个行,如果我们使用的唯一索引(主键就是一个唯一索引)锁定的行,那么这个时候InnoDB会将锁优化成Record Lock,也就是锁定当前行,而不是锁定当前行加一个范围;如果我们使用的不是唯一索引锁定一行数据,那么此时InnoDB就会锁定。Next_Key Lock是为了解决数据库出现幻读的问题。
第一步,创建一个表test,插入数据
mysql> drop table if exists test;
Query OK, 0 rows affected, 1 warning (0.02 sec)
mysql> create table if not exists test(id int primary key,age int(11),name varchar(20),key(age));
Query OK, 0 rows affected (0.44 sec)
mysql> insert into test(id,age,name) values(1,23,'zou');
Query OK, 1 row affected (0.19 sec)
mysql> insert into test(id,age,name) values(2,23,'nan');
Query OK, 1 row affected (0.04 sec)
mysql> insert into test(id,age,name) values(5,32,'fei');
Query OK, 1 row affected (0.04 sec)
mysql> insert into test(id,age,name) values(7,24,'liu');
Query OK, 1 row affected (0.06 sec)
mysql> select *from test;
+----+------+------+
| id | age | name |
+----+------+------+
| 1 | 23 | zou |
| 2 | 23 | nan |
| 5 | 32 | fei |
| 7 | 24 | liu |
+----+------+------+
CLIENT A开启事务,并执行一次查询,查询的同时需要加锁
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select *from test where id = 2 for update;
+----+------+------+
| id | age | name |
+----+------+------+
| 2 | 23 | nan |
+----+------+------+
1 row in set (0.00 sec)
CLIENT B开启事务,并执行一次插入,
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test(id,age,name) values(4,27,'Li');
Query OK, 1 row affected (0.00 sec)
我们看到,此时事务B执行了一次插入操作,根据我们之前提到的行锁算法Next_Key Lock,我们发现此时的插入操作并没有阻塞,这是因为如果我们是此时是对具有主键(具有唯一索引特性)的索引进行了锁定,那么此时此时锁定的就是一行,而不是锁定一个范围,也就是此时Next_Key Lock会降级为Record Lock。
下面我们来看一个如果我们锁定的是一个非唯一索引,(或者我们压根就没使用索引)
另一个事物执行插入操作会不会出现阻塞的情况。
CLIENT A
mysql> select *from test;
+----+------+------+
| id | age | name |
+----+------+------+
| 1 | 31 | zou |
| 2 | 23 | nan |
| 4 | 27 | Li |
| 5 | 32 | fei |
| 7 | 24 | liu |
+----+------+------+
5 rows in set (0.00 sec)
mysql> start transaction ;
Query OK, 0 rows affected (0.00 sec)
mysql> select *from test where age= 27 for update;
+----+------+------+
| id | age | name |
+----+------+------+
| 4 | 27 | Li |
+----+------+------+
1 row in set (0.00 sec)
CLIENT B此时想要插入一个年龄为25和28的数据,我们看一下效果
mysql> insert into test(id,age,name) values(8,25,'qi');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into test(id,age,name) values(3,28,'qi');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
这个时候,我们看到,这个时候事物B发生了阻塞的情况。这是因为当我们在事务A中使用非唯一索引进行查询的时候,这个时候因为查询加锁了,所以InnoDB会使用Next_Key Lock 锁定一个范围是(24,27]以及(27,31)这个范围内的所有的行,同时会使用Record Lock锁定id = 4这个唯一索引这一行。因此当我们要插入的age =25和 28的时候,就会发生这个区间被锁定而出现等待的情况。
mysql> insert into test(id,age,name) values(3,33,'qi');
Query OK, 1 row affected (0.00 sec)
我们看到上面这个插入操作就成功了。因为33不在(24,27]以及(27,31)区间内,索引就不会发生阻塞的情况。
CLIENT A开启事务,并执行一次加锁的查询操作,这个时候我们
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select *from test where age= 27 for update;
+----+------+------+
| id | name | age |
+----+------+------+
| 4 | Li | 27 |
+----+------+------+
1 row in set (0.00 sec)
CLIENT B 开启事务,并执行一次插入操作,而且这个插入值是在Next_Key Lock这个区间的
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert test(id,name,age) values(6,'Da',29);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert test(id,name,age) values(6,'Da',33);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
我们发现,这一次好像和Next_Key LOCK没有什么关系了。这是为什么呢?因为Next_Key Lock是对于行的算法,但是对于这个事务A的加锁查询因为没有使用索引,而使用的是扫描整张表,因此这个查询操作并不会使用行锁,而使用的是表锁。
mysql> explain select *from test where age= 27 for update\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 6
filtered: 16.67
Extra: Using where
1 row in set, 1 warning (0.02 sec)
《Mysql技术内幕-InnoDB引擎探析》