简介
参考https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-gap-locks。
InnoDB引擎实现了标准的行级别锁(S和X)。InnoDB引擎加锁原则遵循二段锁协议,即事务分为两个阶段,事务开始后进入加锁阶段,事务commit或者rollback就进入解锁阶段。InnoDB引擎下锁的影响因素很多,隔离级别不同,是否使用索引等都会产生不同的锁结果。
查看锁和事务
当出现ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction,要解决是一件麻烦的事情。特别是当一个SQL执行完了,但未COMMIT,后面的SQL想要执行就是被锁,超时后结束,DBA光从数据库无法着手找出源头是哪个SQL锁住了。有时候看看 show engine innodb status, 并结合 show full processlist 能暂时解决问题,但一直不能精确定位。在5.5中,information_schema 库中增加了三个关于锁的表(MEMORY引擎)。
INNODB_LOCKS
提供有关InnoDB事务已请求但尚未获得的以及事务正在阻塞另一个事务的锁的信息。
INNODB_LOCK_WAITS
当前等待的锁。
INNODB_TRX
当前事务。
强行解锁
1、
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
获取到blocking_trx_id
2、
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
查找trx_id和上面获取到的blocking_trx_id一样的记录,获取这条记录的trx_mysql_thread_id
3、
kill 上面获取到的trx_mysql_thread_id,这样就把阻塞其他事务的事务线程杀掉了。
共享锁和排它锁
共享锁(S lock):允许持有锁的事务读取一行,select语句后加lock in share mode。
排它锁(X lock):允许持有锁的事务更新或删除一行,select语句后加for update。
共享锁和排他锁都是锁的行记录。当一个事务获取了行r的共享锁,那么另外一个事务也可以立即获取行r的共享锁,因为读取并未改变行r的数据,这种情况就是锁兼容。
但是如果有事务想获得行r的排它锁,则它必须等待其他事务释放行r上的共享锁,这种情况就是锁不兼容。二者兼容性如下表格所示:
对于select 语句,InnoDB不会加任何锁,也就是可以多个并发去进行select的操作,不会有任何的锁冲突,因为根本没有锁。
对于insert,update,delete操作,InnoDB会自动给涉及到的数据加排他锁,只有查询select需要我们手动设置排他锁。
意向锁
InnoDB支持多粒度锁定,允许行锁和表锁共存。例如 LOCK TABLES ... WRITE 这样的语句会获取指定表的排它锁。为了在多个粒度级别上实现锁定,InnoDB使用了意向锁。意向锁是表级锁,指示事务稍后事务对表的行需要的锁的类型(共享锁或排它锁)。意向锁有两种:
意向共享锁(IS):指示事务打算在表中的单个行上设置共享锁。如果需要对记录A加共享锁,那么此时InnoDB会先找到这张表,对该表加意向共享锁之后,再对记录A添加共享锁。
意向排它锁(IX):指示事务打算在表中的单个行上设置排他锁。如果需要对记录A加排他锁,那么此时InnoDB会先找到这张表,对该表加意向排他锁之后,再对记录A添加排他锁。
例如 SELECT ... LOCK IN SHARE MODE 设置IS,SELECT ... FOR UPDATE 设置IX。这两种意向锁都是表锁,都是系统自动添加和自动释放的,整个过程无需人工干预。
意图锁定协议如下:
在事务可以获取表中某一行上的共享锁之前,它必须首先获取表上的IS锁或比IS锁更强的锁;
在事务可以获取表中某一行上的排他锁之前,它必须首先获取表上的IX锁。
表级锁类型兼容性总结如下:
如果请求事务与现有锁兼容,则授予该事务锁,但如果与现有锁冲突,则不授予该事务锁。事务等待冲突的现有锁被释放。如果锁请求与现有锁冲突,并且由于会导致死锁而无法被授予,则会发生错误。意向锁不会阻塞除全表锁请求(例如 LOCK TABLES ... WRITE)之外的任何东西,意向锁定的主要目的是显示某人锁定了一行,或者准备锁定表中的一行。
因为表锁覆盖了行锁的数据,所以表锁和行锁会产生冲突。比如A事务申请表锁,B事务申请行级锁,或者A事务申请行级锁,B事务申请表锁。这时候B事务的申请是需要被阻塞的。那么怎么判断B事务该阻塞呢?遍历表的每一行看看是否有行级锁吗?这样效率非常差。这时候就引入了意向锁。在申请行锁前,数据库自动为我们申请了对应的意向锁,因为意向锁是表锁,这时候如果再申请表锁,就自然会阻塞了。意向锁之间互相兼容,表锁(ALTER TABLE, DROP TABLE, LOCK TABLES等)会和意向锁冲突。
执行 SHOW ENGINE INNODB STATUSG,如果有意向锁,就可以看到类似下面的信息:
TABLE LOCK table `test`.`t` trx id 10080 lock mode IX
记录锁
记录锁是索引记录上的锁,记录锁总是锁定索引记录,即使表没有定义索引。对于这种情况,InnoDB创建一个隐藏的聚簇索引,并使用这个索引来锁定记录。
对主键加锁,加在聚簇索引上;
对二级索引加锁,加在二级索引+聚簇索引上;
对无索引列加锁,加在聚簇索引上。
执行 SHOW ENGINE INNODB STATUSG,如果有记录锁,就可以看到类似下面的信息:
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 ;;
取消自动提交
InnoDB下事务一旦提交或者回滚,就会自动释放事务中的锁。变量autocommit=1开启自动提交,autocommit=0关闭自动提交,默认autocommit=1。在自动提交模式下,每执行一句sql,就自动提交事务,锁也会立即释放。这种情况下无法手动控制事务的提交以及锁的释放时间。
因此,我们会在进行锁的相关操作之前,先执行(set autocommit=0)或者start transaction以关闭自动提交模式,开启手动提交模式,这样事务中的每一行sql执行完成后,锁一直不会释放,直到我们手动提交或者回滚事务,锁才会释放。
间隙锁(GAP)
间隙锁是对索引记录之间的间隙的锁,或对第一个索引记录之前或最后一个索引记录之后的间隙的锁,不包含索引记录本身。间隙锁是性能和并发性之间权衡的一部分,用于某些事务隔离级别,而不是其他事务隔离级别。
对于使用惟一索引锁定行以搜索惟一行的语句,不需要间隙锁(这并不包括搜索条件只包含一个复合多列惟一索引的某些列的情况,在这种情况下,确实会发生间隙锁)。如果列没有索引,或者索引不是惟一的,那么语句将产生间隙锁。
必须在REPEATABLE-READ级别才可以使用间隙锁。可以显式禁用间隙锁,如果将事务隔离级别更改为READ COMMITTED或启用innodb_locks_unsafe_for_binlog系统变量(现在已经不推荐使用该变量),就会发生这种情况。在这种情况下,对搜索和索引扫描禁用间隙锁,只用于外键约束检查和重复键检查。
如何确定间隙锁的区间?根据检索索引记录C向左寻找最靠近C的索引记录值A,作为左区间,向右寻找最靠近C的索引记录值B作为右区间,即锁定的间隙为(A,B),A<C<B,锁定的间隙除了索引记录C之间还包括第一个索引记录A之前和最后一个索引记录B之后。索引记录相同的值是根据主键升序排序的。
间隙锁的目的是为了防止幻读,其主要通过两个方面实现这个目的:
1、防止两个索引区间内有新数据被插入。
2、防止现有数据更新成两个索引区间内的数据。
Next-Key锁
Next-Key锁是索引记录上的记录锁(S或X)和索引记录前的间隙上的间隙上的间隙锁的组合。一般来说MySQL用的都是Next-Key锁。Next-Key锁是左开右闭的区间,例如索引包含10、11、13、20,那么可能的Next-Key锁如下:
(负无穷大, 10]
(10, 11]
(11, 13]
(13, 20]
(20, 正无穷大)
测试环境和数据
MySQL版本为5.7.21,部署于CentOS X86_64上,采用默认配置。
创建表
CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`age` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8;
数据
INSERT INTO `test` (`id`,`age`) VALUES (6,1);
INSERT INTO `test` (`id`,`age`) VALUES (8,5);
INSERT INTO `test` (`id`,`age`) VALUES (10,8);
INSERT INTO `test` (`id`,`age`) VALUES (12,12);
INSERT INTO `test` (`id`,`age`) VALUES (14,14);
INSERT INTO `test` (`id`,`age`) VALUES (16,14);
INSERT INTO `test` (`id`,`age`) VALUES (18,18);
INSERT INTO `test` (`id`,`age`) VALUES (20,20);
+----+-----+
| id | age |
+----+-----+
| 6 | 1 |
| 8 | 5 |
| 10 | 8 |
| 12 | 12 |
| 14 | 14 |
| 16 | 14 |
| 18 | 18 |
| 20 | 20 |
+----+-----+
示例
启动三个会话,会话1和会话2均取消自动提交,会话3查看锁状态。
SELECT * FROM test where age=14 for update 语句的间隙锁范围(id,age)包括(12,12)到(18,18)之间的间隙,同时还会对记录age=14加排他锁。
一、
会话1:
SELECT * FROM test where age=14 for update;
会话2:
INSERT INTO `test` (`id`, `age`) VALUES (13, 12);
结果:
会话2阻塞。
二、
会话1:
SELECT * FROM test where age=14 for update;
会话2:
INSERT INTO `test` (`id`, `age`) VALUES (17, 18);
结果:
会话2阻塞。
三、
会话1:
SELECT * FROM test where age=14 for update;
会话2:
INSERT INTO `test` (`id`, `age`) VALUES (11, 12);
结果:
会话2执行成功。
四、
会话1:
SELECT * FROM test where age=14 for update;
会话2:
INSERT INTO `test` (`id`, `age`) VALUES (11, 14);
结果:
会话2阻塞。
五、
会话1:
SELECT * FROM test where age=14 for update;
会话2:
UPDATE `test` SET `age`=14 WHERE `id`=18;;
结果:
会话2阻塞。
五、
会话1:
SELECT * FROM test where age=14 for update;
会话2:
UPDATE `test` SET `age`=19 WHERE `id`=18;;
结果:
会话2执行成功。