MySQL InnoDB锁机制(三)
前面两篇文章讨论了MySQL InnoDB的锁类型与加锁方式,这次,我们来看看在不同的场景下,不同的SQL会以什么样的方式加什么类型的锁。
在开始之前,我们先了解一下什么是聚族索引?
每一张InnoDB表都有且仅有一表特殊的索引,聚族索引(Clustered Index),表中的数据是直接存放在聚族索引的叶子节点页面中,这样,根据聚族索引查询就会比普通索引更快,因为少了一次IO操作。通常,聚族索引就是表的主键;如果表没有主键,那InnoDB会把第一个非空的唯一索引当作聚族索引;如果表既无主键,又无非空的唯一索引,那么InnoDB会创建一个隐藏的索引。表中的其它全部索引,都叫做第二索引(Secondary Index),第二索引中只包含自身索引列和聚族索引列的内容,所以当一个表的主键很长时,其它的索引都会受到影响。
为什么要先讲聚族索引呢?因为这对理解InnoDB加锁机制很重要,InnoDB加锁的对象不是返回的数据记录,而是查询这些数据时所扫描过的索引。当我们执行一个锁读(SELECT ... LOCK IN SHARE MODE或者SELECT ... FOR UPDATE)时,InnoDB不是对最终的返回结果加锁,而是对查询这些结果时所扫描的索引加锁,如果被扫描的索引不是聚族索引,那被扫描的索引所指向的聚族索引以及其它指向相同聚族索引的索引也会被加锁。由此可知,当一个锁读无法使用索引的话,InnoDB就是遍历整个表(遍历整个聚族索引),从而把整张表都锁住。
我们来看一个例子,首先创建一张表:
CREATE TABLE `tb` ( `id1` int(11) NOT NULL, `id2` int(11) NOT NULL, `id3` int(11) NOT NULL, `id4` int(11) DEFAULT NULL, PRIMARY KEY (`id1`), UNIQUE KEY `uidx` (`id2`), KEY `idx` (`id3`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
插入一些数据:
mysql> select * from tb; +-----+-----+-----+------+ | id1 | id2 | id3 | id4 | +-----+-----+-----+------+ | 1 | 1 | 1 | 1 | | 5 | 5 | 5 | 5 | | 9 | 9 | 9 | 9 | +-----+-----+-----+------+
会话S1根据id4查询一条记录
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from tb where id4 = 1 lock in share mode; +-----+-----+-----+------+ | id1 | id2 | id3 | id4 | +-----+-----+-----+------+ | 1 | 1 | 1 | 1 | +-----+-----+-----+------+ 1 row in set (0.00 sec) mysql>
接着会话S2中尝试对id2=5的记录加锁。
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from tb where id2 = 5 for update; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql>
发生了锁等待超时,因为会话S1根据非索引字段id4查询,InnoDB会扫描整个聚族索引(字段id1),并对扫描过的聚族索引及所有指向相同聚族索引的其它索引都加锁(本例中所有的索引都被加锁了),所以会话S2在尝试对id2=5的记录加锁时只能等待了。由此可见,正确的设计和使用索引,不光对性能有影响,对并行性的影响也至关重要。
再看一个例子,在可重复读隔离级别下,会话S1以id3=5(普通索引)字段加锁查询tb表
mysql> select * from tb; +-----+-----+-----+------+ | id1 | id2 | id3 | id4 | +-----+-----+-----+------+ | 1 | 1 | 1 | 1 | | 5 | 5 | 5 | 5 | | 9 | 9 | 9 | 9 | +-----+-----+-----+------+ 3 rows in set (0.01 sec) mysql> set session transaction isolation level repeatable read; Query OK, 0 rows affected (0.00 sec) mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from tb where id3=5 for update; +-----+-----+-----+------+ | id1 | id2 | id3 | id4 | +-----+-----+-----+------+ | 5 | 5 | 5 | 5 | +-----+-----+-----+------+ 1 row in set (0.01 sec) mysql>
会话S2的情况如下
mysql> set session transaction isolation level repeatable read; Query OK, 0 rows affected (0.00 sec) mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from tb where id3 = 5 for update; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into tb(id1,id2,id3,id4) values(2,2,2,2); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into tb(id1,id2,id3,id4) values(8,8,8,8); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> update tb set id4 = 6 where id2 = 5; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> update tb set id4 = 6 where id1 = 5; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql>
- 在可重复读级别下,InnoDB以Next-Key Lock的方式对索引加锁;在读已提交级别下,InnoDB以Index-Record Lock的方式对索引加锁。
- 被加锁的索引如果不是聚族索引,那被锁的索引所指向的聚族索引以及其它指向相同聚族索引的索引也会被加锁。
- SELECT * FROM ... LOCK IN SHARE MODE对索引加共享锁;SELECT * FROM ... FOR UPDATE对索引加排他锁。
- SELECT * FROM ... 是非阻塞式读,(除Serializable级别)不会对索引加锁。在读已提交级别下,总是查询记录的最新、有效的版本;在可重复读级别下,会记住第一次查询时的版本,之后的查询会基于该版本。例外的情况是在串行化级别,这时会以Next-Key Lock的方式对索引加共享锁。
- UPDATE ... WHERE 与DELETE ... WHERE对索引加排他锁。
- INSERT INTO ... 以Index-Record Lock的方式对索引加排他锁。