9. mysql 锁详解

前言

了解锁前需要明白数据库事务,附上一篇内容比好的文章地址:https://www.cnblogs.com/jay-huaxiao/p/12639435.html

InnoDB行锁是通过给索引加锁来实现的,如果没有索引,InnoDB会通过隐藏的聚簇索引来对记录进行加锁(全表扫描,也就是表锁)。
但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会放锁,最终持有的,是满足条件的记录上的锁。但是不满足条件的记录上的加锁/放锁动作是不会省略的。所以在没有索引时,不满足条件的数据行会有加锁又放锁的耗时过程。所以表中一定要定义索引

索引分为主键索引和非主键索引两种。如果一条sql语句操作了主键索引,MySQL就会锁定对应主键索引;如果一条语句操作了非主键索引,MySQL会先锁定非主键索引,再锁定对应的主键索引。

示例sql(下面演示中需要在会话中关闭自动提交)

CREATE TABLE users
(
	id INT PRIMARY KEY AUTO_INCREMENT,
	NAME VARCHAR(20) NOT NULL DEFAULT '',
	age  INT(10) NOT NULL DEFAULT 0,
	KEY age (age)
);

INSERT INTO users ( NAME, age ) VALUES ( 'zhangsan', 10 );
INSERT INTO users ( NAME, age ) VALUES ( 'lisi', 20 );
INSERT INTO users ( NAME, age ) VALUES ( 'wangwu', 30 );
INSERT INTO users ( NAME, age ) VALUES ( 'zhaoliu', 40 );

锁介绍

共享/排它锁(Shared and Exclusive Locks)

共享锁:
share locks,简称 S 锁,为读锁,如果加上了这个锁,别的事务就只能读不能写。

排他锁:
Exclusive Locks,简称 X 锁,为写锁,如果机上了这个锁,别的事务就不能读也不能写。

可以模拟加锁:

-- 增加 S 锁
SELECT * FROM users  where id = 1 LOCK IN SHARE MODE;

-- 增加 X 锁
SELECT * FROM users where id = 1  FOR UPDATE;

其实就是读写锁,总结下来就是:读读相互,读写相斥,写写相斥

示例

情况1:同一条数据读写 与 写写

事务1 事务2
update users set name = ‘zhangsan1’ where id = 1;使用了一个 X 锁 -
- select * from users where id = 1;发现不加读锁还是没问题的
- select * from users where id = 1 LOCK IN SHARE MODE;发现加了 S 锁后就阻塞了,说明读写相斥
- update users set name = ‘zhangsan2’ where id = 1;使用了一个 X 锁,发现也阻塞了。说明写写相斥

结论:不同事务操作同一行数据时候:读写相斥,写写相斥

情况2:不同行,写写情况(演示之后直接 rollback 回滚掉,免得影响后面操作。)

事务1 事务2
update users set name = ‘zhangsan1’ where id = 1;使用了一个 X 锁 -
- update users set name = ‘zhangsan1’ where id = 2;使用了一个 X 锁

结论:事务1 锁定了id为1的数据,然后事务 2 修改 id 为 2 的数据,修改成功。说明读写锁锁定当前行时候,不会影响其他行。

情况3:筛选 name。

事务1 事务2
update users SET age = 21 WHERE NAME = ‘zhangsan’; -
- select * from users where id = 1 LOCK IN SHARE MODE;使用了一个 S 锁,发现阻塞了
- select * from users where id = 1 for update;使用了一个 X 锁,发现也阻塞了

结论:因为 name 列没有索引,所以走了全变扫描。然后直接锁表了。一定要注意不要犯这种错

意向锁(Intention Locks)

意向锁的主要让行锁与表锁能够共存,意向锁是表级别的锁,标识着事务可能要加共享/排他锁,所以又分为为以下:
1.意向共享锁:表示事务有意向表中某些行共享锁(S)
2. 意向独占锁:表示事务有意向表中某些行排他锁(X)

事务要获取某些行的 S/X 锁,必须先获取表对应的 IS/IX 锁,意向锁表示你想加 X 还是 S 锁,意向锁之间相互兼容。

记录锁(Record Locks)

官方说明:记录锁是对索引记录的锁。记录锁总是锁定索引记录,即使表定义时没有索引。对于这种情况,InnoDB创建一个隐藏的聚集索引,并使用这个索引来锁定记录
大白话就是,就是行锁,锁定对应行的聚集索引。
例如:

-- id 列为主键列或唯一索引列
SELECT * FROM table WHERE id = 1 FOR UPDATE; 

这里获取 排他意向锁(IX),然后再获取该行的 X 锁。会阻止其他事务对c1=10的数据行进行插入、更新、删除等操作。

间隙锁(Gap Locks)

间隙锁就是锁定一个范围,比如你的如下情况:

update users set money = 11 where id > 1;
update users set money = 11 where id < 5;
update users set money = 11 where id between 1 and 4;

会把符合条件的数据范围都给加上一个 X锁,在这期间别的事务都只能访问不能写了。目的就是为了防止其他事务在此期间插入数据,导致“不可重复读”。如果把事务隔离级别修改为 RC( Read Committed),间隙锁就会失效。

如果修改某个范围时候,此时再想插入数据,就会处于等待状态,这样性能就会造成影响。所以需要明白间隙锁的危害

示例
事务1 事务2
update users set age = 25 where id <= 3;使用范围搜索修改符合条件的内容 -
- update users set age = 20 where id = 2;发现阻塞了

总结:如果使用范围搜索修改,符合范围的都会加锁。

临键锁(Next-key Locks)

这个临键锁就有点麻烦了,我也是看了好久想了好久才顿悟。临键锁可以理解为特殊的间隙锁,等于 间隙锁 + 记录锁。每个数据行上除去 唯一列与主键列 的索引上都有一个临键锁。

当某个事物针对某行持有了临键锁时候,分为 2 步,一步锁住对应的 聚集索引,一步锁住对应 非聚集索引 的前面 与 下一个索引(左开右闭区间),以来阻塞插入。在隔离级别RR中有效

然后很多文章说通过这种形式可以解决幻读的问题,但是这样做只是锁住了前面与后面一个,后面还有很多都无锁,还是可以任意插入更改,所以就想不通怎么解决幻读。这里有点不知其所以然了。

如果单独读上面话,可能比较难理解究竟原理是怎样的,通过下面图可以大概了解下原理:
9. mysql 锁详解_第1张图片
在上图中,表 id为主键,age 为索引列。事务 A 在执行 update 操作时候,根据name列在非聚集索引树上定位到数据后,将 当前叶子前面的所有叶子 + 当前叶子后面一个叶子上锁,然后再将对应聚集索引页上一个记录锁。

示例

注意 age 值的变化

事务1 事务2
UPDATE users SET name = ‘laoli2’ WHERE age = 20;临键锁非聚集所以之前与之后的范围,也就是 10-20-30 如果插入这之间的数都会阻塞 -
- UPDATE users SET name = ‘laoli2’ WHERE age = 10;不会阻塞,发现临键锁不会阻塞范围内的 update 操作
- -
- INSERT INTO users ( NAME, age ) VALUES ( ‘laoli1’, 10 );
INSERT INTO users ( NAME, age ) VALUES ( ‘laoli1’, 11 );
INSERT INTO users ( NAME, age ) VALUES ( ‘laoli1’, 21);
INSERT INTO users ( NAME, age ) VALUES ( ‘laoli1’, 29);
-以上都会阻塞,所以说 10-30 之间都会阻塞

总结:临键锁只会锁住当前非聚集索引叶子之前的叶子 与 下一个叶子,这个之间的范围。锁住后修改的话不会阻塞,插入的话就会阻塞。

自增锁(Auto-inc Locks)

自增锁是表级别的锁,专门针对事务插入 AUTO_INCREMENT 类型的列。
假设 A 事务 insert,还未提交;
事务 B 此时 insert 会处于等待状态,直到 A 提交了。

总结

  1. 读写锁、间隙锁、临键锁 这三个需要明白
  2. 日常使用中进来避免间隙与临键带来性能问题
  3. 修改数据时候要使用索引列来筛选数据,不然会锁表。

演示间隙锁区间范围 与 不存在id时候的范围插入。

模拟死锁

死锁前提需要 2 个事务,1一个读一个写。造成互相等待。
9. mysql 锁详解_第2张图片

事务1 事务2
SELECT * FROM users where id = 1 LOCK IN SHARE MODE;增加一个读锁 -
- SELECT * FROM users where id = 1 FOR UPDATE;此时这个事务去增加一个写锁,由于读写相斥,这里就阻塞起来了,等待事务1执行完毕
SELECT name FROM users where id = 1 FOR UPDATE;但是事务1,这里又加入了一个写锁,由于事务2占用着写锁,所以这里永远不可用获取写锁,出现死锁。 -
- 直接报死锁,InnoDB会自动检测死锁。据官方文档可知,目前InnoDB处理死锁的机制是:发现有循环等待的现象,立即回退(rollback)开销更小的事务,也就是插入、修改、删除了更少记录的事务。

查询死锁。可以在任意会话查询到

SHOW ENGINE INNODB STATUS;

发现找到出现死锁的语句,从画红圈的发现,2个事务主键索引竞争出现死锁,并打印出了 2 个死锁语句。并且 lock_mode X locks rec but not gap waiting 与 lock mode S locks rec but not gap 说明是 共享 与 排他 锁导致的死锁
9. mysql 锁详解_第3张图片

分析行锁争夺情况

SHOW STATUS LIKE 'innodb_row_lock%';

Innodb_row_lock_current_waits:当前正在等待锁定的数量
Innodb_row_lock_time:从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg:每次等待所话平均时间
Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间
Innodb_row_lock_waits:系统启动后到现在总共等待次数。

你可能感兴趣的:(mysql)