本文将描述InnoDB用到的锁类型。
1)共享和排他锁(Shared and Exclusive Locks)。
2)意向锁(Intention Locks)。
3)记录锁( Record Locks)。
4)间隙锁(Gap Locks)。
5)下一键值锁(Next-Key Locks)。
6)插入意向锁(Insert Intention Locks)。
7)自动插入锁( AUTO-INC Locks)。
8)空间索引谓词锁(Predicate Locks for Spatial Indexes)。

1.共享锁(S)和排他锁(X)
InnoDB实现标准的行级锁,其中,分为两种锁,共享锁和排他锁。
1)共享锁允许持有共享锁的事务读取一行数据。
2)排他锁允许持有排他锁的事务更新或删除一行数据。如果事务T1在数据行r上持有一个共享锁,那么,另一个请求在数据行r上获取一个锁的不同事务被做如下处理:
  --事务T2请求获取一个共享锁能被立刻被授予。结果,事务T1和T2在数据行r上都持有共享锁。
  --事务T2请求获取一个排他锁不能立刻被授予。如果事务T1在数据行r上持有一个排他锁,某个不同事务T2在数据行r上的任何模式的锁都不能被立刻授予。相反,事务T2必须等待事务T1释放数据行r上的锁。

2.意向锁
InnoDB支持多个粒度的锁,从而允许行锁和表锁的共存。例如:一个像lock table ... where的语句获取特定表上的排他锁。为了实现多粒度级别的锁,InnoDB用意向锁。意向锁为表级锁,其表示一个事务稍后将获取的表中数据行上的锁模式。有两种意向锁模式:
1)意向共享锁(IS)表示一个事务打算在表的单独数据行上设置共享锁。
2)意向排他锁(IX)表示一个事务打算在表的单独数据行上设置排他锁。
例如:select ... lock in share mode设置一个意向共享锁,而select ... for update则设置一个意向排他锁。
意向锁协议如下所示:
1)事务在获取表中数据行上的共享锁前,必须先获取表上的意向共享锁或更强的锁。
2)事务在获取表中数据行上的排他锁前,必须先获取表上的意向排他锁。
表级锁模式间的兼容性总结如下:
           X           IX          S          IS
X          Conflict    Conflict    Conflict   Conflict
IX         Conflict    Compatible  Conflict   Compatible
S          Conflict    Conflict    Compatible Compatible
IS         Conflict    Compatible  Compatible Compatible
如果被事务请求的锁与已经存在的锁兼容,则被授予,但如果与已经存在的锁冲突,则不被授予。事务将一直等待到存在冲突的已存在锁被释放。如果请求的锁与已存在锁冲突而不能被授予,从而导致死锁而报错。
除了全表请求(例如:lock tables ... write)外,意向锁并不阻塞任何事情。意向锁的主要目的是显示某人正锁定一个数据行,或将要锁定表中的一个数据行。
意向锁的事务数据显示与下列show engine innodb status和InnoDB监视器输出类似:
TABLE LOCK table `test`.`t` trx id 10080 lock mode IX

3.记录锁
记录锁是索引记录上的一个锁。例如:select c1 from t where c1=10 for update;阻止任何其他事务插入,更新或删除t.c1为10的数据行。
记录锁总是锁定索引记录,即使表上没有定义索引。对这种情况,InnoDB创建一个隐式簇索引,并用该索引进行记录锁定。
记录锁的事务数据便是与下列show engine innodb status和InnoDB监视器输出类似:
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;

4.间隙锁
间隙锁是索引记录之间间隙上的锁,或第一个索引记录之前或最后一个索引记录之后间隙上的锁。例如:select c1 from t where c1 between 10 and 20 for update;阻止其他事务将15数值插入t.c1,而不管该列中是否已有该值,因为该范围内的所有已存在值间的间隙已经被锁定。
一个间隙也许跨越单个索引值,多个索引值,或甚至为空。
间隙锁是性能和并发间的权衡,被用于某些事务隔离级别而非其他隔离级别。
用唯一索引锁定数据行来查找唯一数据行的语句不需要间隙锁。(这并不包括查找条件只包含多列唯一索引中部分列的场景;这种场景,确实会发生间隙锁。)例如:如果id列有一个唯一索引,下面的语句只用一个索引记录锁锁定id值为100的数据行,其并不关心其他会话是否在前面的间隙插入数据行:
select * from child where id=100;
如果id列上没建索引,或建有非唯一索引,那么该SQL确实会锁定前述的间隙。
这里,也值得注意的是,不同的事务能在一个间隙上持有相互冲突的锁。例如:事务A能在一个间隙上持有一个共享间隙锁(gap S-lock),而事务B在同样的间隙上持有一个排他间隙锁(gap X-lock)。允许冲突间隙锁的原因是如果一个索引记录被purge掉,不同事务在该记录上的间隙锁必须被合并。InnoDB中的间隙锁是“纯粹禁止的(purely inhibitive)”,这意味着它们只是阻止其他事务往指定间隙插入数据。它们并不阻止不同事务在同一个间隙上获取间隙锁。这样,间隙锁X-lock和间隙锁S-lock有同样的效果。
用户可以显式的关闭间隙锁。如果将事务隔离级别改为read committed或开启innodb_locks_unsafe_for_binlog系统变量(现已被弃用)时,就可以关闭间隙锁。这种场景下,间隙锁对查找和索引扫描关闭,而仅用于外键约束检查和重复键检查。
使用read committed隔离级别或开启innodb_locks_unsafe_for_binlog也会有其他影响。mysql评估了where条件后,将会释放不匹配数据行上的记录锁。对update语句,InnoDB进行一个“半一致”读,这样,InnoDB将向mysql返回最近被提交的版本,因此,mysql能决定数据行是否与update语句的where条件匹配。

5.下一键值锁(Next-Key Locks)
下一键值锁是索引记录的记录锁与该索引记录前间隙上间隙锁的组合。
InnoDB查找或扫描一个表索引时,会以这种方式执行行级锁定,其在遇到的索引记录上放置共享或排他锁。这样,行级锁实际上是索引记录锁。索引记录上的下一键值锁页影响该索引记录前的“间隙”。也就是,下一键值锁时一个索引记录锁加上一个该索引记录前间隙上的间隙锁。如果一个会话在索引记录R上有一个共享或排他锁,另一个会话不能将一个新的索引记录立即插入索引记录R前的间隙中。
假设索引包含值10,11,13和20。该索引可能的下一键值锁将覆盖下列间隔,这里,一个圆括号表示不包括间隔端点,而一个方括号表示包括间隔端点。
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
对最后的间隔,下一键值锁锁定索引最大值与“上确界(supremum)”伪记录间的间隙,这里,“上确界”伪记录为大于索引中实际存在的任何值的一个数值。上确界不是一个真实的索引记录,因此,实际上,该下一键值锁仅锁定最大索引值后的间隙。默认地,InnoDB按照repeatable read事务隔离级别进行操作。因此,InnoDB使用下一键值锁进行查找和索引扫描,从而阻止幻影行的发生。
下一键值锁事务数据看上去类似下面show engine innodb status和InnoDB监视器输出:
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10080 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;

6.插入意向锁(Insert Intention Locks)
插入意向锁是一种数据行插入前insert操作设置的间隙锁。该锁表示,当多个事务往相同索引间隙的不同位置插入数据时,这些事务无需相互等待。假设有值为4和7的索引记录。不同事务分别想要插入值5和6,每个事务在获得插入行的排他锁前,都用插入意向锁锁定4和7间的间隙,但因为它们插入的数据行并不冲突,因此它们并不会相互阻塞。
下面的例子说明一个事务获得插入记录的排他锁前获得了一个插入意向锁。该例子涉及两个客户端,A和B。
客户端A创建包含两个索引记录(90和102)的一张表,接着开始了一个事务,该事务在ID大于100的索引就上放置了一个排他锁。排他锁包含一个记录102前的间隙锁:
mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
mysql> INSERT INTO child (id) values (90),(102);
mysql> START TRANSACTION;
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
+-----+
| id |
+-----+
| 102 |
+-----+
客户端B开始了一个往前述间隙插入一条记录的事务。该事务等待获取一个排他锁时获取了一个插入意向锁。
mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (101);
插入意向锁事务数据看上去类似下面show engine innodb status和innodb监视器输出:
RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child`
trx id 8731 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 80000066; asc f;;
1: len 6; hex 000000002215; asc " ;;
2: len 7; hex 9000000172011c; asc r ;;...

7.自增锁(AUTO-INC Locks)
自增锁是一种向带有auto_increment列的表中插入数据时获取的特殊的表级锁。最简单的场景中,如果一个事务正向表中插入数值,其他事务往该表中插入数据的操作必须等待,以便第一个事务插入的数据行能得到连续的主键值。
innodb_autoinc_lock_mode配置选项控制用于自增锁的算法。其允许用户选择如何在插入操作自增值的可预期顺序与最大并发间权衡。

8.空间索引谓词锁(Preicate Locks for Spatial Indexes)
InnoDB支持在包含空间数据的列上创建空间索引。
为了处理设计空间索引操作的锁定,下一键值锁支持repeatable read或serializable事务隔离级别时运行并不好。多维数据中并没有绝对的顺序概念,因此,哪个是“下一个”键值并不清晰。
为了支持建有空间索引表的隔离级别,InnoDB使用了谓词锁。一个空间索引包含最小边界矩形(minimum bounding rectangle,MBR)值,因此,InnnoDB通过在查询所用的MBR上放置一个谓词锁,从而强制该索引上的一致性读。期间,其他事务不能插入或修改匹配该查询条件的数据行。