MySQL 中锁的概念
排它锁(Exclusive Lock)
X 锁,也 称为写锁,若事务T对对象A加上X锁,则只允许T读取和修改A,其他任何事物都不能再对A 加任何锁,直到T释放A上的锁。
SELECT...FOR UPDATE 对读取的行记录加一个X锁,其他事务不能对已锁定的行加上任何锁。
共享锁(Shared Lock)
**S 锁,**也称为读锁,若事务T对数据对象A加上S锁,则事务T可以读A,但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。
SELECT...LOCK IN SHARE MODE对读取的行记录加一个S锁,其他事务可以向被锁定 的行加S锁,但是如果加X锁,则会被阻塞。
活锁
事务T1封锁了R,T2又请求封锁R,于是T2等待,T3也请求封锁R,当T1释放了R 上的锁,系统首先批准了T3的请求,T2继续等待,这就是活锁。
死锁
事务T1封锁了R1,T2封锁了R2,T1又请求封锁R2,因为T2已经封锁了R2,于是T1等待T2释放R2上的锁,接着T2又申请封锁R1,因为T1已经封锁了R1,T2只能等待T1释放R1上的锁,这就是死锁。
解决死锁的方法
一次封锁法 每个事务必须将所有要使用的数据全部加锁,否则就不能执行,弊端 加大封锁范围,降低了并发速度。
乐观锁
总是假设最好的情况,在事务提交前不会对数据进行锁定,而是在更新数据时会进行版本或时间戳的比较,以确定数据是否被其他事务修改过。如果数据没有被修改,则允许提交;如果数据被修改,则需要进行冲突解决
悲观锁
总是假设最坏的情况,在整个事务过程中,假设其他事务会对数据进行修改,因此在读取或修改数据时,会先对数据进行锁定,以防止其他事务对数据进行干扰(排它锁、共享锁都是悲观锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)
MySQL 行锁加锁的分析
版本使用 MySQL 8.2.0
MySQL InnoDB 中支持三种行锁的方式:行锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock),默认加的是临键锁,但是会根据不同的查询条件进行优化。创建一个 user 表用来测试,表中 id 是主键索引,name 是唯一索引,salary 是普通索引,gender 没有索引。
id | name | salary | gender |
---|---|---|---|
10 | 惠月 | 48000 | 女 |
20 | 光济 | 50000 | 男 |
30 | 杰霖 | 55000 | 男 |
40 | 紫妤 | 60000 | 女 |
50 | 娜溱 | 70000 | 女 |
创建表并插入数据
plsql
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`salary` int DEFAULT NULL,
`gender` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `salary`(`salary`) USING BTREE,
UNIQUE INDEX `name`(`name` ASC) USING BTREE
);
INSERT INTO `user` VALUES (10, '惠月', 48000, '女');
INSERT INTO `user` VALUES (20, '光济', 50000, '男');
INSERT INTO `user` VALUES (30, '杰霖', 55000, '男');
INSERT INTO `user` VALUES (40, '紫妤', 62000, '女');
INSERT INTO `user` VALUES (50, '娜溱', 75000, '女');
加锁情况
plsql
-- 普通的select查询是快照读,不加锁
SELECT * FROM user WHERE id=30;
-- 查询时给主键索引加S共享锁时,是当前读
SELECT * FROM user WHERE id=30 LOCK IN SHARE MODE;
-- 查询时加 X排他锁,为当前读
SELECT * FROM user WHERE id=30 FOR UPDATE
执行 SELECT * FROM ... FOR UPDATE 会对表加上 IX 写意向锁,表示有可能会对这些记录进行写操作,并且给记录加一个X,REC_NOT_GAP,锁定了该条数据。 执行SELECT * FROM ... FOR SHARE 会对表加上一个 IS 读意向锁,并且会给记录加一个S,REC_NOT_GAP。
主键索引
主索引等值查询,数据存在的情况
事务 1
plsql
BEGIN;
SELECT * FROM user WHERE id=30 FOR UPDATE;
-- SELECT * FROM user WHERE id=40 FOR SHARE;
-- ROLLBACK
查看锁的情况
plsql
-- mysql 8
SELECT * FROM performance_schema.data_locks;
-- mysql 5.7
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
1、表锁是意向写锁
2、数据加行锁
各字段意思:
INDEX_NAME 锁定索引的名称 PRIMARY 说明是主键索引
LOCK_TYPE 锁的类型,RECORD 行锁 、 TABLE 表锁
LOCK_MODE 锁的模式,IS 读意向锁、IX 写意向锁、S 读锁,又称共享锁、X 写锁,又称排它锁、GAP 间隙锁。
**LOCK_DATA **要锁定的数据,当 LOCK_TYPE 是 RECORD行锁时。当锁在主键索引上时,显示主键索的值。当锁是在辅助索引上时,显示主索引和辅助索引的值。
LOCK_MODE | LOCK_DATA | 锁范围 |
---|---|---|
X,REC_NOT_GAP | 40 | id=40 行锁 |
X,GAP | 40 | id=40 间隙锁,不包含 40(前开后开) |
X | 40 | id=40 临键锁,包含 40(前开后闭) |
事务 2
1、会对主索引 id=30 添加行锁
plsql
BEGIN;
-- 更新会失败,因为事务 1 对 id=30 加行锁。
UPDATE user SET salary = 56000 WHERE id = 30;
主索引等值查询,数据不存在的情况
事务 1
plsql
BEGIN;
SELECT * FROM user WHERE id=31 FOR UPDATE; -- 数据库中没有id=30的记录
查看加锁情况
1、表加的是意向写锁
2、id 加的是 GAP Lock,范围是(30, 40)
注意
LOCK_MODE 是 X,GAP 表示间隙锁,LOCK_DATA 是 40 表示锁定的范围是在 id 为 40 之前的间隙
事务 2
1、会锁住主索引 id=31 所在的间隙
plsql
BEGIN;
-- 可以执行成功
UPDATE `bostore`.`user` SET `salary` = 56000.00 WHERE `id` = 30;
UPDATE `bostore`.`user` SET `salary` = 63000.00 WHERE `id` = 40;
-- 执行失败
INSERT INTO `bostore`.`user` VALUES (33, '六零', 68000.00, '女');
主索引范围查询,前闭后开情况
事务 1
plsql
BEGIN;
SELECT * FROM user WHERE id>=30 AND id<33 FOR UPDATE;
查看锁情况
1、 表示 IX 意向写锁
2、id=30 是行锁
3、id=40 加的是 GAP Lock,范围是(30, 40)
事务 2
plsql
BEGIN;
-- 会阻塞
UPDATE user SET salary = 57000.00 WHERE id = 30;
INSERT INTO user VALUES (35, '六零', 68000.00, '女');
INSERT INTO user VALUES (33, '合吧', 64000.00, '女');
-- 不会阻塞
UPDATE user SET salary = 63000.00 WHERE id = 40;
主索引范围查询,前开后闭情况
事务 1
plsql
BEGIN;
SELECT * FROM user WHERE id>30 AND id<=40 FOR UPDATE;
查看锁情况
1、表加的是意向写锁
2、id=40 加 NEXT-Key Lock,范围是(30, 40]
事务 2
plsql
BEGIN;
-- 会阻塞
UPDATE user SET salary = 63000 WHERE id = 40;
INSERT INTO user VALUES (35, '六零', 68000, '女');
INSERT INTO user VALUES (33, '合吧', 64000, '女');
-- 不会阻塞
UPDATE user SET salary = 57000 WHERE id = 30;
UPDATE user SET salary = 78000 WHERE id = 50;
INSERT INTO user VALUES (53, '考拉', 84000, '女');
普通索引
普通索引(普通索引只针对表中的单一列进行索引,普通索引可以是唯一的,也可以不唯一,普通索引对于等值查询(例如WHERE column = value )和范围查询(例如WHERE column > value)都能提供较好的性能提升)
普通索引等值查询,数据存在的情况
事务 1
plsql
BEGIN;
SELECT * FROM user WHERE salary = 62000 FOR UPDATE;
查看锁情况
1、 表加意向写锁 IX
2、 索引salary加临键锁,范围是(55000, 62000]
3、 主键 id = 40 加行锁
4、 索引salary加间隙锁,范围是(62000, 75000)
事务 2
1、 主键 id=40 加了行锁,不能更新和删除
plsql
-- 修改id=40有行锁会阻塞
UPDATE user SET salary = 63000 WHERE id = 40;
UPDATE user SET name="紫是" where salary = 62000;
2、salary 在(55000, 62000] 范围加了 NEXT-Key Lock(针对该范围的 id 也会加锁 ),insert 时salary 在此范围,会阻塞
3、salary 在(62000, 75000)范围加了 GAP Lock(避免幻读),insert 时salary 在此范围,会阻塞
4、 插入 salary=55000时,id<30可以不阻塞,id>30会阻塞
plsql
-- id=40 salary=62000 之前有临键锁(55000, 62000]
-- 是对salary的锁,但是整个区间都会被锁住包括主索引id
INSERT INTO user VALUES (35, '六零', 55000, '女'); -- 阻塞
INSERT INTO user VALUES (51, '哈西', 55000, '女'); -- 阻塞
-- id=30 salary=55000 之前没有间隙锁
INSERT INTO user VALUES (29, '湖西', 55000, '女'); -- 不阻塞
5、 插入 salary=62000时,会阻塞
plsql
INSERT INTO user VALUES (39, '哈子', 62000, '女');
INSERT INTO user VALUES (44, '靠是', 62000, '女');
INSERT INTO user VALUES (55, '西欧', 62000, '女');
-- 自增id时也是会阻塞
INSERT INTO `bostore`.`user`(`name`, `salary`, `gender`) VALUES ('学律', 62000, '女');
6、插入 salary=75000时,id<50 会阻塞,id>50 不阻塞
plsql
-- LOCK_MODE为X,GAP
-- LOCAK_DATA为75000, 50 表示 要插入salary为75000,id<50时会加锁,即会阻塞
INSERT INTO user VALUES (29, '合吧', 75000, '女');
-- id > 50 不会阻塞
INSERT INTO user VALUES (51, '欧下', 75000, '女');
7、当salary 不在(55000, 62000] 和(62000, 75000)范围时,id 不会加锁
plsql
INSERT INTO user VALUES (31, '湖西', 54000, '女');
INSERT INTO user VALUES (49, '离下', 54000, '女');
INSERT INTO user VALUES (45, '的大', 76000, '女');
UPDATE user SET salary = 78000 WHERE id = 50;
-- id < 30 和 salary < 55000 不阻塞
INSERT INTO user VALUES (29, '六零', 50000, '女');
-- id > 50 和 salary > 75000 不阻塞
INSERT INTO user VALUES (63, '合吧', 94000, '女');
普通索引等值查询,数据不存在的情况
事务 1
plsql
BEGIN;
SELECT * FROM user WHERE salary = 60000 FOR UPDATE;
查看锁情况
1、表加意向写锁
2、salary 加间隙锁,范围是(55000, 62000)
事务 2
1、salary 在(55000, 62000)范围加了 GAP Lock,insert 时salary 在此范围,会阻塞
2、插入 salary=55000时,id<30可以不阻塞,id>30会阻塞
plsql
INSERT INTO user VALUES (35, '六零', 55000, '女'); -- 阻塞
INSERT INTO user VALUES (51, '哈西', 55000, '女'); -- 阻塞
-- id=30 salary=55000 之前没有间隙锁
INSERT INTO user VALUES (29, '湖西', 55000, '女'); -- 不阻塞
3、插入 salary=62000时,id<40 会阻塞,id>40 不阻塞
plsql
-- id < 40 会阻塞
INSERT INTO user VALUES (39, '合吧', 62000, '女');
-- id > 40 不会阻塞
INSERT INTO user VALUES (41, '欧下', 62000, '女');
4、当 salary 不在(55000, 62000),id 不会加锁
plsql
INSERT INTO user VALUES (31, '湖西', 54000, '女');
INSERT INTO user VALUES (39, '离下', 63000, '女');
UPDATE user SET salary = 63000 WHERE id = 40;
-- id < 30 和 salary < 55000 不阻塞
INSERT INTO user VALUES (29, '六零', 50000, '女');
-- id > 40 和 salary > 62000 不阻塞
INSERT INTO user VALUES (41, '合吧', 65000, '女');
普通索引范围查询,前闭后开的情况
事务 1
plsql
BEGIN;
SELECT * FROM user WHERE salary>=55000 AND salary<62000 FOR UPDATE;
查看锁情况
1、表加意向写锁
2、salary 加 NEXT-Key Lock 范围是(50000, 55000]
3、salary 加 NEXT-Key Lock 范围是(55000, 62000]
4、id=30 加行锁
事务 2
1、 插入 salary=50000时,id<20可以不阻塞,id>20会阻塞
plsql
INSERT INTO user VALUES (19, '六零', 50000, '女'); -- 不阻塞
INSERT INTO user VALUES (21, '哈西', 50000, '女'); -- 阻塞
INSERT INTO user VALUES (41, '湖西', 50000, '女'); -- 阻塞
2、 插入 salary=55000时,会阻塞
plsql
INSERT INTO user VALUES (29, '哈子', 55000, '女');
INSERT INTO user VALUES (35, '靠是', 55000, '女');
INSERT INTO user VALUES (44, '西欧', 55000, '女');
-- 自增id时也是会阻塞
INSERT INTO `bostore`.`user`(`name`, `salary`, `gender`) VALUES ('学律', 55000, '女');
3、 插入 salary=62000时,id<40 会阻塞,id>40 不阻塞
plsql
-- 阻塞
INSERT INTO user VALUES (39, '合吧', 62000, '女');
-- 不阻塞
INSERT INTO user VALUES (41, '欧下', 62000, '女');
4、当salary 不在(50000, 55000] 和(55000, 62000]范围时,id 不会加锁
plsql
INSERT INTO user VALUES (31, '湖西', 44000, '女');
INSERT INTO user VALUES (49, '离下', 44000, '女');
INSERT INTO user VALUES (45, '的大', 66000, '女');
-- id < 20 和 salary < 50000 不阻塞
INSERT INTO user VALUES (19, '六零', 40000, '女');
-- id > 40 和 salary > 62000 不阻塞
INSERT INTO user VALUES (53, '合吧', 94000, '女');
5、id=30 加行锁,不能删除和更新
plsql
UPDATE user SET salary = 56000 WHERE id = 30;
UPDATE user SET name="紫下" where salary = 55000;
普通索引范围查询,前开后闭的情况
事务 1
plsql
BEGIN;
SELECT * FROM user WHERE salary>55000 AND salary<=62000 FOR UPDATE;
查看锁情况
1、表加意向写锁
2、salary 加 NEXT-Key Lock 范围是(55000, 62000]
3、salary 加 NEXT-Key Lock 范围是(62000, 75000]
4、id=40 加行锁
事务 2 的加锁情况和上面类似
没有索引的情况
sql
BEGIN;
SELECT * FROM user WHERE gender='男' FOR UPDATE;
查看加锁情况
InnoDB 的锁是加上在索引上的,没有索引的时候,就会给所有的记录都加上锁 NEXT-Key Lock,相当于表锁。