此次实验所使用的mysql版本为mysql8:
因为我们可以在performance_schema.data_locks
中看到锁的情况。
引擎是innodb。事务隔离级别是RR。
mysql中的锁可以按照锁的粒度分,表锁就是其中的一种(另一种是行锁)。
虽然锁一整张表在实际操作中是不明智的,但我们还是探究一下。
准备的表:
CREATE TABLE `test`.`test_innodb_lock` (
`id` int NOT NULL,
`name` varchar(255) NOT NULL,
`score` int NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_score` (`score`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
我们有主键索引id
以及二级索引score
。
然后插入一些数据:
INSERT INTO `test`.`test_innodb_lock`(`id`, `name`, `score`) VALUES (1, 'Jack', 100);
INSERT INTO `test`.`test_innodb_lock`(`id`, `name`, `score`) VALUES (2, 'Rose', 60);
INSERT INTO `test`.`test_innodb_lock`(`id`, `name`, `score`) VALUES (3, 'Ocean', 77);
为了看到performance_schema.data_locks
中锁的情况,我们需要设置:
set autocommit=0;
首先看看表级别的读锁:
读锁可以被多次获取(session1和session2同时获取到了读锁)。
我们可以看到in_use
的数量增加了。
然后我们查看锁的情况:
SELECT
ENGINE,
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME,
LOCK_TYPE,
LOCK_MODE,
LOCK_STATUS,
LOCK_DATA
FROM performance_schema.data_locks;
可以看到一个S
锁(Share Lock)。
在读锁锁整张表的情况下:
三个session都能够读。
但是:
锁表的session1和session2报错,旁观者session3阻塞。
session1和session2回滚并解锁表。
重新做实验。
在session1获取表级别的写锁之后,session2无法再次获取。我们取消session2的获取操作。
现在我们看到了一把X
锁(Exclusive Lock)。
持有锁的session1能读能写,其他session则不行。
我们可以看一下mysql官网的总结:
我们不会主动去锁表,我们常用的是行级别的锁。
行级别的锁有很多。我们看二级索引score,它有几个值:60,79,100,由此可以划分区间。
小于60
60
60至79
79
79至100
大于100
如果我们要添加锁,我们可以锁一行,比如score=60这一行,也可以锁区间,比如60至79这个区间,一切都要看innodb怎么去实现了。
全部开启事务。
我们看到了两种锁:IS
表示Intention Share Lock,S
表示Share Lock,REC_NOT_GAP
表示Record Lock but Not a Gap Lock(行锁而非间隙锁)。所以S,REC_NOT_GAP
就表示这是一个行锁,是个共享锁,不是一个间隙锁。
IS
是一种表锁,如果一个session想去获取一个S
锁,就必须先去获取IS
锁,即使你最终可能获取S
锁失败了,你也要去持有IS
锁(表达自己的一个意愿)。
S
锁是一种行级的共享锁,作用在主键上(一定要有主键,你没有人家给你生成一个)。
既然id=1的行被session1持有了行锁,我们看看session2能做些什么?
可以查,但是不能修改。
我们可以看到有人试图去获取id=1这一行X
锁(写锁),显然这是session2的意图。在waiting地去获取id=1条记录的X
锁时,他已经成功地获取了IX
锁(Intention Exclusive Lock)。
当session2因为无法获取写锁而超时时:
IX
锁还是被session2持有着,这个Intention还是保留着。
session2rollback
。
此时IX
锁才被释放。
和表级S
锁一样行级的S
锁也可以被多个session获得。
读锁的含义其实就是当前事务进入了读的状态,当然所有事务都可以这么做。但是,如果你想改,不好意思,没有一个事务中能改,不管你持不持有读锁。只有当这一行的读锁全部释放,它才能够被修改。
同理,我们可以看看行级的X
锁。
只有持有写锁的session1才能修改。
所以for update
或者说X
锁的含义究竟是什么?他就像是在说:我锁定了(session1),接下来要有写操作了,其他人不能进入read mode,也不能进入write mode,其他人就等我完事吧。
我们的表里有主键索引id和二级索引score,暂时我们都在使用主键索引,我们可以先看看如果查询语句不使用索引会怎样(使用二级索引score作为查询条件的话情况会更加复杂)。
三条记录都被锁住了。而且是我们没有接触到的一种锁,单纯的S
锁(next-key lock),等下我们会介绍这个锁。
当前就认为id=1,id=2,id=3三条记录都添加了行锁就行。
此时session2对id=3的记录也不能更新了。
作为对比,我们看看如果使用主键索引的情况:
只锁id=1一行的话session2是可以更新id=3的数据的。
另外,我们常用的其他当前读:update和delete,mysql也是给加了写锁的。
gap锁就是间隙锁,我们上面看到过,我们不仅能对一行加锁,还能对行与行之间的间隙加锁。
next-key锁就是锁两个东西,一个是锁行,另一个是锁行前面的那个间隙,就是这两把锁的合并。
为了更好的演示,我们修改一下数据:
我们为二级索引加写锁:
IX
是意向锁,要拿到X
必须先拿到IX
锁。X
是next-key锁,锁住score=60对应的行和score在50至60之间的数据。X,REC_NOT_GAP
是行锁,锁住id=21这一行。X,GAP
是间隙锁,锁住score在60至70间的数据。边界数据score=50是否锁住需要查看。
我们先看插入的情况:
50(包含)到60,60到76都不能插入,但是score=77可以。这里就可以看出next-key lock和gap lock之间的区别了。
对于update操作,51(包含)到60,60到76都不行,score改成50或者77可以。
从这里也可以看出gap lock和next-key lock作用范围也太大了。
如果没有使用索引,则给全表加上next-key lock。
这时候没有任何插入操作能够成功。
当我们使用RR的时候,需要注意gap lock和next-key lock带来的开销。
死锁就是两个事务(或者多个)互相持有并去索取锁(和java死锁一样)。
session1先去持有id=2这一行的S
锁。
session2尝试去获取id=2这行的X
锁,由于S
和X
互斥,session2等待。
在session2等待的过程中,session1去获取id=2这行的X
锁。这时候死锁就发生了:
session2已经发出请求X
锁的需要,等待session1释放S
锁;session1又去请求正被session2“持有”的X
锁,互相盯着对方碗里的锁,就会发生死锁。(session1的S
锁不能直接升级成X
锁,因为它已经被session2请求了)
innodb自己会发现死锁并打破循环。
此时session1同时获取id=2这一行的S
锁和X
锁。
使用
SHOW ENGINE INNODB STATUS
我们可以看到更多细节:
session2的情况:
session1的情况: