锁是计算机用以协调多个进程间并发访问同一共享资源的一种机制
共享锁,又称读锁,简称 S 锁。当事务对数据加上读锁后,其他事务只能对该数据加读锁,不能加写锁。
共享锁加锁方法: select …lock in share mode
排他锁,又称为写锁,简称 X 锁,当事务对数据加上排他锁后,其他事务无法对该数据进行查询或者修改
MySQL InnoDB引擎默认 update,delete,insert 都会自动给涉及到的数据加上排他锁,select 语句默认不会加任何锁类型。
全局锁,从名称上可以理解,全局锁就是对整个 MySQL 数据库实例加锁,加锁期间,对数据库的任何增删改操作都无法执行。
MySQL 提供了一个加全局读锁的方法,命令是Flush tables with read lock (FTWRL)
全库数据备份时,可以使用全局锁,其他情况不要使用
表级锁,给当前操作的这张表加锁, MyISAM 与 InnoDB 引擎都支持表级锁定
MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)
页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。BDB 引擎支持页级锁
行级锁是 MySQL 粒度最细的锁,发生锁冲突概率最低,但是加锁慢,开销大
MySQL 中只有 InnoDB 引擎支持行锁,其他不支持
MySQL 中,行级锁并不是之间锁记录,而是锁的索引 。
普通的 select 语句是不会对记录加锁的,因为它属于快照读,是通过MVCC(多版本并发控制) 实现的。
MySQL 在执行 update、delete 语句时会自动加上行锁
不同隔离级别下,行级锁的种类是不同的。
在读已提交隔离级别下,行级锁的种类只有记录锁,也就是仅仅把一条记录锁上。
在可重复读隔离级别下,行级锁的种类除了有记录锁,还有间隙锁(目的是为了避免幻读),所以行级锁的种类主要有三类:
Record Lock,记录锁,也就是仅仅把一条记录锁上;
Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身;
Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
Record Lock
Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的:
当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);
当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。
举个例子,当一个事务执行了下面这条语句:
mysql > begin;
mysql > select * from t_test where id = 1 for update;
事务会对表中主键 id = 1 的这条记录加上 X 型的记录锁,这样其他事务就无法对这条记录进行修改和删除了。
当事务执行 commit 后,事务过程中生成的锁都会被释放。
Gap Lock
Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。
间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。
Next-Key Lock
Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。
所以,next-key lock 即能保护该记录,又能阻止其他事务将新记录插入到被保护记录前面的间隙中。
next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。
比如,一个事务持有了范围为 (1, 10] 的 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,就会被阻塞。
虽然相同范围的间隙锁是多个事务相互兼容的,但对于记录锁,我们是要考虑 X 型与 S 型关系,X 型的记录锁与 X 型的记录锁是冲突的。
意向锁是表锁,为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存
当有事务A有行锁时,MySQL会自动为该表添加意向锁,事务B如果想申请整个表的写锁,那么不需要遍历每一行判断是否存在行锁,而直接判断是否存在意向锁,增强性能。
意向共享锁(IS) | 意向排他锁(IX) | |
---|---|---|
共享锁 | 兼容 | 互斥 |
排他锁 | 互斥 | 互斥 |
记录锁、间隙锁、临键锁都是排它锁,而记录锁的使用方法跟排它锁介绍一致
记录锁是封锁记录,记录锁也叫行锁,例如:
select * from people where id = 1 for update;
间隙锁基于非唯一索引,它锁定一段范围内的索引记录。使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据
select * from people where id < 10 for update;
即所有在 [1,10)区间内的记录行都会被锁住,所有id 为 1、2、3、4、5、6、7、8、9 的数据行的插入会被阻塞
由于InnoDB引擎才支持行级锁,以下内容都是基于InnoDB引擎介绍。
对一条记录加锁本质上是内存中创建的一个锁结构跟这条记录相关联。
所以锁本质上就是内存中的一种数据结构。
那么我们在操作一个事务的时候,如果对应多条记录,是不是要针对多条记录生成多个内存的锁结构呢?比如我们执行select * from people for update的时候,people 表中如果存在1万条数,那么难道要生成1万个内存的锁结构吗?那当然不会是这样的。其实,如果符合以下几个条件,那么这些记录的锁就可以放到一个内存中的锁结构里了,条件如下所示:
那么这么多次的锁结构,它到底是怎么组成的呢?
主要是由6部分组成的。分别为:锁所在的事务信息、索引信息、表锁或行锁信息、type_mode、其他信息、与heap_no对应的比特位。
update people set age = 10 where id = 49;
说明:
update people set age = 10 where name = 'Tom';
说明:
对应sql语句,其中age字段上没有索引:
update people set name = 'Tom' where age = 10;
说明:
注意是通过Next Key Lock锁定的全表范围,而不是通过表级锁直接锁表。
-- 查看当前所有事务
select * from information_schema.innodb_trx;
-- 查看加锁信息(MySQL5.X)
select * from information_schema.innodb_locks;
-- 查看锁等待(MySQL5.X)
select * from information_schema.innodb_lock_waits;
--查看加锁信息(MySQL8.0)
SELECT * FROM performance_schema.data_locks;
--查看锁等待(MySQL8.0)
SELECT * FROM performance_schema.data_lock_waits;
-- 查看表锁
show open tables where In_use>0;
-- 查看最近一次死锁信息
show engine innodb status;
这里主要介绍通过查询performance_schema.data_locks表,查看事务中加锁的情况。
DROP TABLE if EXISTS people;
CREATE TABLE `people` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`account` varchar(30) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '账号',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`age` int DEFAULT NULL COMMENT '年龄',
`email` varchar(50) DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_account` (`account`) USING BTREE,
KEY `ik_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `people` (`id`,`account`,`name`, `age`, `email`) VALUES (3, '000003', '老万', 12, '[email protected]');
INSERT INTO `people` (`id`,`account`, `name`, `age`, `email`) VALUES (10, '000010', '老张', 15, '[email protected]');
INSERT INTO `people` (`id`,`account`, `name`, `age`, `email`) VALUES (20, '000020', '老王', 15, '[email protected]');
INSERT INTO `people` (`id`,`account`, `name`, `age`, `email`) VALUES (30, '000030', '老王', 30, '[email protected]');
select * from performance_schema.data_locks\G
lock_mode说明
这里需要重点对 LOCK_MODE 加锁模式进行说明:
LOCK_MODE值 | 锁类型 |
---|---|
IX | 意向排他锁 |
IS | 意向共享锁 |
AUTO_INC | 自增主键锁 |
X | 排他临键锁(Next Key) ,既锁记录,也锁间隙 |
S | 共享临键锁(Next Key) ,既锁记录,也锁间隙 |
X,REC_NOT_GAP | 排他标准记录锁(Record),只锁记录,不锁间隙 |
S,REC_NOT_GAP | 共享标准记录锁(Record) ,只锁记录,不锁间隙 |
S,GAP | 共享间隙锁(GAP),只锁间隙 |
X,GAP | 排他间隙锁(GAP) ,只锁间隙 |
INSERT_INTENTION | 插入意向锁 |
可串行化完全符合普通程序员对数据竞争加锁的理解,如果不考虑性能优化的话,对事务所有读、写的数据全都加上读锁、写锁和范围锁即可做到可串行化(“即可”是简化理解,实际还是很复杂的,要分成 Expanding 和 Shrinking 两阶段去处理读锁、写锁与数据间的关系,称为Two-Phase Lock,2PL)。但数据库不考虑性能肯定是不行的,并发控制理论(Concurrency Control)决定了隔离程度与并发能力是相互抵触的,隔离程度越高,并发访问时的吞吐量就越低。现代数据库一定会提供除可串行化以外的其他隔离级别供用户使用,让用户调节隔离级别的选项,根本目的是让用户可以调节数据库的加锁方式,取得隔离性与吞吐量之间的平衡。
可串行化的下一个隔离级别是可重复读(Repeatable Read),可重复读对事务所涉及的数据加读锁和写锁,且一直持有至事务结束,但不再加范围锁。可重复读比可串行化弱化的地方在于幻读问题(Phantom Reads),它是指在事务执行过程中,两个完全相同的范围查询得到了不同的结果集
SELECT count(1) FROM books WHERE price < 100 /* 时间顺序:1,事务: T1 */
INSERT INTO books(name,price) VALUES ('深入理解Java虚拟机',90) /* 时间顺序:2,事务: T2 */
SELECT count(1) FROM books WHERE price < 100 /* 时间顺序:3,事务: T1 */
根据前面对范围锁、读锁和写锁的定义可知,假如这条 查询SQL 语句在同一个事务中重复执行了两次,且这两次执行之间恰好有另外一个事务在数据库插入了一本小于 100 元的书籍,这是会被允许的,那这两次相同的查询就会得到不一样的结果,原因是可重复读没有范围锁来禁止在该范围内插入新的数据,这是一个事务受到其他事务影响,隔离性被破坏的表现。
可重复读的下一个隔离级别是读已提交(Read Committed),读已提交对事务涉及的数据加的写锁会一直持续到事务结束,但加的读锁在查询操作完成后就马上会释放。读已提交比可重复读弱化的地方在于不可重复读问题(Non-Repeatable Reads),它是指在事务执行过程中,对同一行数据的两次查询得到了不同的结果
SELECT * FROM books WHERE id = 1; /* 时间顺序:1,事务: T1 */
UPDATE books SET price = 110 WHERE id = 1; COMMIT; /* 时间顺序:2,事务: T2 */
SELECT * FROM books WHERE id = 1; COMMIT; /* 时间顺序:3,事务: T1 */
如果隔离级别是读已提交,这两次重复执行的查询结果就会不一样,原因是读已提交的隔离级别缺乏贯穿整个事务周期的读锁,无法禁止读取过的数据发生变化,此时事务 T2 中的更新语句可以马上提交成功,这也是一个事务受到其他事务影响,隔离性被破坏的表现。假如隔离级别是可重复读的话,由于数据已被事务 T1 施加了读锁且读取后不会马上释放,所以事务 T2 无法获取到写锁,更新就会被阻塞,直至事务 T1 被提交或回滚后才能提交。
读已提交的下一个级别是读未提交(Read Uncommitted),读未提交对事务涉及的数据只加写锁,会一直持续到事务结束,但完全不加读锁。读未提交比读已提交弱化的地方在于脏读问题(Dirty Reads),它是指在事务执行过程中,一个事务读取到了另一个事务未提交的数据
参考
https://icyfenix.cn/architect-perspective/general-architecture/transaction/local.html
https://blog.csdn.net/qq_43842093/article/details/131737316
https://blog.csdn.net/weixin_50205273/article/details/127818382