对于InnoDB 的gap locks 有很多文章介绍,包括官网也有相应的介绍。在这中间,我推荐这篇文章。https://www.percona.com/blog/2012/03/27/innodbs-gap-locks/
以下是我对这篇文章的翻译。
InnoDB最重要的功能之一是行级锁定。 此功能在繁重的写入负载下提供更好的并发性,但需要额外的预防措施以避免幻读以便在复制时候能够保持一致性。 为此,行级锁定数据库还获取间隙锁。
在正在运行的事务中,两个相同的语句获得不同的值就是幻读,因为某些其他事务已修改了表的行。 例如:
transaction1> START TRANSACTION;
transaction1> SELECT * FROM t WHERE i > 20 FOR UPDATE;
+------+
| i |
+------+
| 21 |
| 25 |
| 30 |
+------+
transaction2> START TRANSACTION;
transaction2> INSERT INTO t VALUES(26);
transaction2> COMMIT;
transaction1> select * from t where i > 20 FOR UPDATE;
+------+
| i |
+------+
| 21 |
| 25 |
| 26 |
| 30 |
+------+
如果只是执行SELECT,则不会发生幻读。它们仅在执行UPDATE或DELETE或SELECT FOR UPDATE时发生。 InnoDB为只读SELECT提供REPEATABLE READ,但是对所有写查询就跟使用READ COMMITTED一样,不管选择的事务隔离级别(仅考虑两个最常见的隔离级别,REPEATABLE READ和READ COMMITTED)。
gap lock 是索引记录之间的锁。由于有gap lock,当您运行相同的查询两次时,无论该表上的其他session作何修改,您都会得到相同的结果。这使得读取一致,因此使服务器之间的复制保持一致。如果执行SELECT * FROM id> 1000 FOR UPDATE两次,则期望获得两次相同的值。 为了实现这一点,InnoDB使用独占锁锁定WHERE子句找到的所有索引,并使用共享gap lock锁定它们相邻的索引。
此锁定不仅影响SELECT … FOR UPDATE。 这是DELETE语句的示例:
transaction1 > SELECT * FROM t;
+------+
| age |
+------+
| 21 |
| 25 |
| 30 |
+------+
开始一个事务并且删除25这条记录。
transaction1 > START TRANSACTION;
transaction1 > DELETE FROM t WHERE age=25;
此时我们假设只有记录25被锁定。 然后,我们尝试在第二个会话中插入另一个值:
transaction2 > START TRANSACTION;
transaction2 > INSERT INTO t VALUES(26);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
transaction2 > INSERT INTO t VALUES(29);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
transaction2 > INSERT INTO t VALUES(23);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
transaction2 > INSERT INTO t VALUES(31);
Query OK, 1 row affected (0.00 sec)
在第一个会话上运行delete语句后,不仅受影响的索引记录已被锁定,而且该记录之前和之后的间隙还有一个共享gap lock,阻止将数据插入其他会话。
可以使用SHOW ENGINE INNODB STATUS检测gap locks:
---TRANSACTION 72C, ACTIVE 755 sec
4 lock struct(s), heap size 1248, 3 row lock(s), undo log entries 1
MySQL thread id 3, OS thread handle 0x7f84a78ba700, query id 163 localhost msandbox
TABLE LOCK table test.t trx id 72C lock mode IX
RECORD LOCKS space id 19 page no 4 n bits 80 index age of table test.t trx id 72C lock_mode X
RECORD LOCKS space id 19 page no 3 n bits 80 index GEN_CLUST_INDEX of table test.t trx id 72C lock_mode X locks rec but not gap
RECORD LOCKS space id 19 page no 4 n bits 80 index age of table test.t trx id 72C lock_mode X locks gap before rec
如果事务中有很多gap locks影响并发性和性能,则可以通过两种不同的方式禁用它们:
这两个选项之间最重要的区别是第二个选项是影响所有会话的全局变量,需要服务器重新启动才能更改其值。 这两个选项都会导致幻读(不可重复读取),因此为了防止复制问题,您应该将二进制日志格式更改为row。
sql语句不同,这些锁的行为可能不同。 以下链接做了很好的解释:
http://dev.mysql.com/doc/refman/5.1/en/innodb-locks-set.html.
MySQL使用REPEATABLE READ作为默认隔离级别,因此需要锁定索引记录和间隙以幻读并在复制时候能够获得一致性。 如果您的应用程序可以处理幻读并且您的二进制日志是row,则将ISOLATION更改为READ COMMITTED将帮助您避免所有这些额外的锁定。 作为最终建议,请保持事务尽量短。