排它锁之行锁、间隙锁、后码锁

MySQL InnoDB支持三种行锁定

行锁(Record Lock):锁直接加在索引记录上面,锁住的是key。

间隙锁(Gap Lock):锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别而设计的。

后码锁(Next-Key Lock):行锁和间隙锁组合起来就叫Next-Key Lock。

默认情况下,InnoDB工作在可重复读隔离级别下,并且会以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock是行锁和间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。

行锁(Record Lock)

当需要对表中的某条数据进行写操作(insert、update、delete、select for update)时,需要先获取记录的排他锁(X锁),这个就称为行锁。

create table x(`id` int, `num` int, index `idx_id` (`id`)); insert into x values(1, 1), (2, 2); -- 事务A START TRANSACTION; update x set id = 1 where id = 1; -- 事务B -- 如果事务A没有commit,id=1的记录拿不到X锁,将出现等待 START TRANSACTION; update x set id = 1 where id = 1; -- 事务C -- id=2的记录可以拿到X锁,不会出现等待 START TRANSACTION; update x set id = 2 where id = 2;

针对InnoDB RR隔离级别,上述SQL示例展示了行锁的特点:“锁定特定行不允许进行修改”,但行锁是基于表索引的,如果where条件中用的是num字段(非索引列)将产生不一样的现象:

-- 事务A START TRANSACTION; update x set num = 1 where num = 1; -- 事务B -- 由于事务A中num字段上没有索引将产生表锁,导致整张表的写操作都会出现等待 START TRANSACTION; update x set num = 1 where num = 1; -- 事务C -- 同理,会出现等待 START TRANSACTION; update x set num = 2 where num = 2; -- 事务D -- 等待 START TRANSACTION; insert into x values(3, 3); Gap锁(Gap Lock)

在MySQL中select称为快照读,不需要锁,而insert、update、delete、select for update则称为当前读,需要给数据加锁,幻读中的“读”即是针对当前读。

RR事务隔离级别允许存在幻读,但InnoDB RR级别却通过Gap锁避免了幻读

产生间隙锁的条件(RR事务隔离级别下)

使用普通索引锁定

使用多列唯一索引

使用唯一索引锁定多行记录

唯一索引的间隙锁

测试环境

MySQL,InnoDB,默认的隔离级别(RR)

数据表

CREATE TABLE `test` ( `id` int(1) NOT NULL AUTO_INCREMENT, `name` varchar(8) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

数据

INSERT INTO `test` VALUES ('1', '小罗'); INSERT INTO `test` VALUES ('5', '小黄'); INSERT INTO `test` VALUES ('7', '小明'); INSERT INTO `test` VALUES ('11', '小红');

以上数据,会生成隐藏间隙

(-infinity, 1]
(1, 5]
(5, 7]
(7, 11]
(11, +infinity]

只使用记录锁,不会产生间隙锁 /* 开启事务1 */ BEGIN; /* 查询 id = 5 的数据并加记录锁 */ SELECT * FROM `test` WHERE `id` = 5 FOR UPDATE; /* 延迟30秒执行,防止锁释放 */ SELECT SLEEP(30); -- 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句 /* 事务2插入一条 name = '小张' 的数据 */ INSERT INTO `test` (`id`, `name`) VALUES (4, '小张'); # 正常执行 /* 事务3插入一条 name = '小张' 的数据 */ INSERT INTO `test` (`id`, `name`) VALUES (8, '小东'); # 正常执行 /* 提交事务1,释放事务1的锁 */ COMMIT;

以上,由于主键是唯一索引,而且是只使用一个索引查询,并且只锁定一条记录,所以,只会对 id = 5 的数据加上记录锁,而不会产生间隙锁。

产生间隙锁 /* 开启事务1 */ BEGIN; /* 查询 id 在 7 - 11 范围的数据并加记录锁 */ SELECT * FROM `test` WHERE `id` BETWEEN 5 AND 7 FOR UPDATE; /* 延迟30秒执行,防止锁释放 */ SELECT SLEEP(30); -- 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句 /* 事务2插入一条 id = 3,name = '小张1' 的数据 */ INSERT INTO `test` (`id`, `name`) VALUES (3, '小张1'); # 正常执行 /* 事务3插入一条 id = 4,name = '小白' 的数据 */ INSERT INTO `test` (`id`, `name`) VALUES (4, '小白'); # 正常执行 /* 事务4插入一条 id = 6,name = '小东' 的数据 */ INSERT INTO `test` (`id`, `name`) VALUES (6, '小东'); # 阻塞 /* 事务5插入一条 id = 8, name = '大罗' 的数据 */ INSERT INTO `test` (`id`, `name`) VALUES (8, '大罗'); # 阻塞 /* 事务6插入一条 id = 9, name = '大东' 的数据 */ INSERT INTO `test` (`id`, `name`) VALUES (9, '大东'); # 阻塞 /* 事务7插入一条 id = 11, name = '李西' 的数据 */ INSERT INTO `test` (`id`, `name`) VALUES (11, '李西'); # 阻塞 /* 事务8插入一条 id = 12, name = '张三' 的数据 */ INSERT INTO `test` (`id`, `name`) VALUES (12, '张三'); # 正常执行 /* 提交事务1,释放事务1的锁 */ COMMIT;

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpswgz.html