6

一文搞懂MySQL行锁、表锁、间隙锁详解

 3 years ago
source link: https://segmentfault.com/a/1190000037534778
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

uAJvYvN.png!mobile

前言

我们前几篇讲了索引是什么,如何使用 explain 分析索引使用情况,如何去优化索引,以及 show profiles 分析 SQL 语句执行资源消耗的学习。今天我们来讲讲 MySQL 的各种锁,这里存储引擎我们使用 InnoDB

准备工作

创建表 tb_innodb_lock

drop table if exists test_innodb_lock;
CREATE TABLE test_innodb_lock (
    a INT (11),
    b VARCHAR (20)
) ENGINE INNODB DEFAULT charset = utf8;
insert into test_innodb_lock values (1,'a');
insert into test_innodb_lock values (2,'b');
insert into test_innodb_lock values (3,'c');
insert into test_innodb_lock values (4,'d');
insert into test_innodb_lock values (5,'e');

创建索引

create index idx_lock_a on test_innodb_lock(a);
create index idx_lock_b on test_innodb_lock(b);

MySQL 各种锁演示

set autocommit=0;

行锁(写&读)

  • A 窗口执行
update test_innodb_lock set b='a1' where a=1;
SELECT * from test_innodb_lock;

yAVvq2Y.png!mobile

我们可以看到 A 窗口可以看到更新后的结果

  • B 窗口执行
SELECT * from test_innodb_lock;

6BFnMfE.png!mobile

我们可以看到 B 窗口不能看到更新后的结果,看到的还是老数据,这是因为 a = 1 的这行记录被 A 窗口执行的 SQL 语句抢到了锁,并且没有执行 commit 提交操作。所以窗口 B 看到的还是老数据。这就是 MySQL 隔离级别中的"读已提交"。

  • 窗口 A 执行 commit 操作
COMMIT;
  • 窗口 B 查询
SELECT * from test_innodb_lock;

RNfuQvJ.png!mobile

这个时候我们发现窗口 B 已经读取到最新数据了

行锁(写&写)

  • 窗口 A 执行更新 a = 1 的记录
update test_innodb_lock set b='a2' where a=1;

这时候并没有 commit 提交,锁是窗口 A 持有。

  • 窗口 B 也执行更新 a = 1 的记录
update test_innodb_lock set b='a3' where a=1;

VRNFZ3.png!mobile

可以看到,窗口 B 一直处于阻塞状态,因为窗口 A 还没有执行 commit,还持有锁。窗口 B 抢不到 a = 1 这行记录的锁,所以一直阻塞等待。

  • 窗口 A 执行 commit 操作
COMMIT;
  • 窗口 B 的变化

fuuaaya.png!mobile

可以看到这个时候窗口 B 已经执行成功了

表锁

当索引失效的时候,行锁会升级成表锁,索引失效的其中一个方法是对索引自动 or 手动的换型。a 字段本身是 integer,我们加上引号,就变成了 String,这个时候索引就会失效了。

  • 窗口 A 更新 a = 1 的记录
update test_innodb_lock set b='a4' where a=1 or a=2;
  • 窗口 B 更新 a = 2 的记录
update test_innodb_lock set b='b1' where a=3;

BJZJVbR.png!mobile

这个时候发现,虽然窗口 A 和 B 更新的行不一样,但是窗口 B 还是被阻塞住了,就是因为窗口 A 的索引失效,导致行锁升级成了表锁,把整个表锁住了,索引窗口 B 被阻塞了。

  • 窗口 A 执行 commit 操作
COMMIT;
  • 窗口 B 的变化

vmu2ean.png!mobile

可以看到这个时候窗口 B 已经执行成功了

间隙锁

  • 什么是间隙锁

当我们采用范围条件查询数据时,InnoDB 会对这个范围内的数据进行加锁。比如有 id 为:1、3、5、7 的 4 条数据,我们查找 1-7 范围的数据。那么 1-7 都会被加上锁。2、4、6 也在 1-7 的范围中,但是不存在这些数据记录,这些 2、4、6 就被称为间隙。

  • 间隙锁的危害

范围查找时,会把整个范围的数据全部锁定住,即便这个范围内不存在的一些数据,也会被无辜的锁定住,比如我要在 1、3、5、7 中插入 2,这个时候 1-7 都被锁定住了,根本无法插入 2。在某些场景下会对性能产生很大的影响

  • 间隙锁演示

我们先把字段 a 的值修改成 1、3、5、7、9

  • 窗口 A 更新 a = 1~7 范围的数据
update test_innodb_lock set b='b5' where a>1 and a<7;
  • 窗口 B 在 a = 2 的位置插入数据
insert into test_innodb_lock values(2, "b6");

EZjiUnf.png!mobile

这个时候发现窗口 B 更新 a = 2 的操作一直在等待,因为 1~7 范围的数据被间隙锁,锁住了。只有等窗口 A 执行 commit,窗口 B 的 a = 2 才能更新成功

行锁分析

  • 执行 SQL 分析命令
show status like 'innodb_row_lock%';

RZFfam.png!mobile

  • Variable_name 说明

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

结语

大家可以根据 Variable_name 这几个参数考虑是否要进行优化,如果锁定时间,锁定次数过大,那就该考虑优化了。优化手段可以参考之前索引优化的文章。

IT 老哥

一个通过自学,在大厂做高级Java开发的程序员, 关注我 ,每天分享技术干货


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK