【MySQL系列5】深入分析MySQL中锁并详解锁解决幻读问题

    • 间隙是如何确定的
  • 临键锁(Next-Key Lock)

    • 临键锁加锁规则
  • 临键锁为何能解决幻读问题

  • 死锁(Dead Lock)

    • 什么是死锁
  • 死锁的检测

  • 死锁的避免

  • 锁信息查询

    • INNODB_TRX
  • INNODB_LOCKS

  • INNODB_LOCK_WAITS

MySQL系列文章汇总

========================================================================

| MySQL系列文章 | 飞机票 |

| — | — |

| MySQL索引为何选择B+树 | 登机入口 |

| 深入分析MySQL索引 | 登机入口 |

| MySQL执行计划EXPLAIN详细说明和举例 | 登机入口 |

| MySQL中事务以及MVCC的实现原理 | 登机入口 |

| 深入分析MySQL中锁并详解幻读问题 | 登机入口 |

| select语句和update语句的执行流程 | 登机入口 |

| InnoDB引擎存储结构及Change Buffer和Double Writer分析 | 登机入口 |

| InnoDB中四种行格式分析及对varchar和char的限制 | 登机入口 |

前言

===============================================================

上一篇,我们分析了MySQL中事务以及MVCC相关知识,想要详细了解的请点击这里。那么今天就为大家介绍一下MySQL中锁相关的知识。

本文在没有特别声明的情况下,均是默认InnoDB引擎,如涉及到其他引擎或者数据库则会特别指出。

什么是锁

=================================================================

锁是一种用于保证在并发场景下每个事务仍能以一致性的方式读取和修改数据的方式,当一个事务对某一条数据上锁之后,其他事务就不能修改或者只能阻塞等待锁的释放,所以锁的粒度大小一定程度上可以影响到访问数据库的性能。

锁的分类

=================================================================

从锁的粒度上来说,我们可以将锁分为全局锁,表锁和行锁

全局锁


全局锁,也被简称为FTWRL,通过以下语句执行:

FLUSH TABLE WITH READ LOCK;–加全局锁

UNLOCK TABLES;-- 解锁

执行这个语句之后整个数据库都只允许读,不允许写。

当然,也可以使用以下语句:

SET GLOBAL READ_ONLY=TRUE;

当然,如果需要加全局锁的话,一般我们推荐使用FTWRL,主要是因为如果客户端因为异常断开了,FTWRL会自动释放锁,而使用set语句则会一直保持read-only状态。

表锁


顾名思议,表锁就是直接锁表,在MyISAM引擎中就只有表锁。

表锁也可以分为两种:一种是通过LOCK命令加锁,另一种是MDL锁

  • 1、通过LOCK命令加锁

LOCK TABLE t1 READ;-- 此时本线程只能读t1表,其他线程也只能读t1表

LOCK TABLE t2 WRITE;-- 此时本线程只能读写t2表,其他线程不能读写t2表

UNLOCK TABLE; --解锁

  • 2、MDL锁

MDL(metadata lock)即元数据锁,是MySQL5.5版本引入的一种表级锁。MDL锁主要是用来保证读写的正确性。

  • 当对一个表做增删改查操作的时候,加MDL读锁(事务提交后自动释放)。

  • 当对一个表结构做变更操作时,加MDL写锁(事务提交后自动释放)。

MDL读锁之间不互斥,可以对一张表加多个MDL读锁,读写和写锁之间互斥。

PS:MDL锁是在Server层实现的,所以所有引擎都具有MDL锁。

行锁


行锁,从名字上来看,就是锁住一行数据,然而,行锁的实际实现算法会相对复杂,有时候并不仅仅只是锁住某一条数据,这个后面再展开。

正常的思路是:锁住一行数据之后,其他事务就不能来访问这条数据了,那么我们想象,假如事务A访问了一条数据,只是拿出来读一下,并不想去修改,正好事务B也来访问这条数据,也仅仅只是想拿出来读一下,并不想去修改,这时候如果因此阻塞了,就有点浪费性能了。所以为了优化这种读数据的场景,我们又把行锁分为了两大类型:共享锁和排他锁

共享锁


共享锁,Shared Lock,又称之为读锁,S锁,就是说一条数据被加了S锁之后,其他事务也能来读数据,可以共享一把锁。

我们可以通过如下语句加共享锁:

select * from test where id=1 LOCK IN SHARE MODE;

加锁之后,直到加锁的事务结束(提交或者回滚)就会释放锁。

排他锁

排他锁,Exclusive Lock,又称之为写锁,X锁。就是说一条数据被加了X锁之后,其他事务想来访问这条数据只能阻塞等待锁的释放,具有排他性。

当我们在修改数据,如:insert,update,delete的时候MySQL就会自动加上排他锁,同样的,我们可以通过如下sql语句手动加上排他锁:

select * from test where id=1 for update;

在InnoDB引擎中,是允许行锁和表锁共存的。

但是这样就会有一个问题,假如事务A给t表其中一行数据上锁了,这时候事务B想给t表上一个表锁,这时候怎么办呢?事务B怎么知道t表有没有行锁的存在,如果采用全表遍历的情况,当表中的数据很大的话,加锁都要加半天,所以MySQL中就又引入了意向锁

意向锁


意向锁为表锁,分为两种类型,分为:意向共享锁(Intention Shared Lock)和意向排他锁(Intention Exclusive Lock),这两种锁又分别可以简称为IS锁和IX锁。

意向锁是MySQL自己维护的,用户无法手动加意向。

意向锁有两大加锁规则:

  • 当需要给一行数据加上S锁的时候,MySQL会先给这张表加上IS锁。

  • 当需要给一行数据加上X锁的时候,MySQL会先给这张表加上IX锁。

这样的话上面的问题就迎刃而解了,当需要给一张表上表锁的时候,只需要看这张表是否有对应的意向锁就可以了,无需遍历整张表。

各种锁的兼容关系

=====================================================================

下面这张图是InnoDB引擎中各种锁的兼容关系,参考自官网:

| | X | IX | S | IS |

| — | — | — | — | — |

| X | 互斥 | 互斥 | 互斥 | 互斥 |

| IX | 互斥 | 共享 | 冲突 | 共享 |

| S | 互斥 | 互斥 | 共享 | 共享 |

| IS | 互斥 | 共享 | 共享 | 共享 |

锁到底锁的是什么

=====================================================================

建立以下两张表,并初始化5条数据,注意test表有2个索引而test2没有索引:

CREATE TABLE test (

id int(11) NOT NULL,

name varchar(50) DEFAULT NULL,

PRIMARY KEY (id),

KEY NAME_INDEX (name)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test VALUE(1,‘张1’);

INSERT INTO test VALUE(5,‘张5’);

INSERT INTO test VALUE(8,‘张8’);

INSERT INTO test VALUE(10,‘张10’);

INSERT INTO test VALUE(20,‘张20’);

CREATE TABLE test2 (

id varchar(32) NOT NULL,

name varchar(32) DEFAULT NULL

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test2 VALUE(1,‘张1’);

INSERT INTO test2 VALUE(5,‘张5’);

INSERT INTO test2 VALUE(8,‘张8’);

INSERT INTO test2 VALUE(10,‘张10’);

INSERT INTO test2 VALUE(20,‘张20’);

举例猜测


在行锁中,假如我们对一行记录加锁,那么到底是把什么东西锁住了,我们来看下面两个例子:

举例1(操作test表):

| 事务A | 事务B |

| — | — |

| BEGIN; | |

| SELECT * FROM test WHERE id=1 FOR UPDATE; | |

| | SELECT * FROM test WHERE id=1 FOR UPDATE;

阻塞 |

| | SELECT * FROM test WHERE id=5 FOR UPDATE;

加锁成功 |

| COMMIT;(释放锁) | |

| | SELECT * FROM test WHERE id=1 FOR UPDATE;

加锁成功 |

举例2(操作test2表):

| 事务A | 事务B |

| — | — |

| BEGIN; | |

| SELECT * FROM test2 WHERE id=1 FOR UPDATE; | |

| | SELECT * FROM test2 WHERE id=1 FOR UPDATE;

阻塞 |

| | SELECT * FROM test2 WHERE id=5 FOR UPDATE;

阻塞 |

| COMMIT;(释放锁) | |

| | SELECT * FROM test2 WHERE id=1 FOR UPDATE;

加锁成功 |

从上面两个例子我们可以发现,test表好像确实是锁住了id=1这一行的记录,而test2表好像不仅仅是锁住了id=1这一行记录,实际上经过尝试我们就知道,test2表是被锁表了,所以其实MySQL中InnoDB锁住的是索引,当没有索引的时候就会锁表

接下来再看一个场景:

| 事务A | 事务B |

| — | — |

| BEGIN; | |

| SELECT * FROM test WHERE name=‘张1’ FOR UPDATE; | |

| | SELECT name FROM test WHERE name=‘张1’ FOR UPDATE;

阻塞 |

| | SELECT id FROM test WHERE id=1 FOR UPDATE;

阻塞 |

| COMMIT;(释放锁) | |

| | SELECT id FROM test WHERE id=1 FOR UPDATE;

加锁成功 |

这个例子中我们是把name索引锁住了,然后我们在事务B中通过主键索引只查id,这样就用到name索引了,但是最后发现也被阻塞了。所以我们又可以得出下面的结论,MySQL索引不但锁住了辅助索引,还会把辅助索引对应的主键索引一起锁住

到这里,可能有人会有怀疑,那就是我把辅助索引锁住了,但是假如加锁的时候,只用到了覆盖索引(关于覆盖索引的相关知识,想要详细了解的,可以点击这里),然后我再去查主键会怎么样呢?

接下来让我们再验证一下:

| 事务A | 事务B |

| — | — |

| BEGIN; | |

| SELECT name FROM test WHERE name=‘张1’ FOR UPDATE; | |

| | SELECT name FROM test WHERE name=‘张1’ FOR UPDATE;

阻塞 |

| | SELECT * FROM test WHERE id=1 FOR UPDATE;

阻塞 |

| | SELECT id FROM test WHERE id=1 FOR UPDATE;

阻塞 |

| COMMIT;(释放锁) | |

| | SELECT id FROM test WHERE id=1 FOR UPDATE;

加锁成功 |

我们可以看到,就算只是用到了辅助索引加锁,MySQL还是会把主键索引锁住,而主键索引的B+树叶子节点中,又存储了整条数据,所以查询任何字段都会被锁定。

到这里,我们可以明确的给锁到底锁住了什么下结论了:

结论


InnoDB引擎中,锁锁的是索引:

  • 假如一张表没有索引,MySQL会进行锁表(其实锁住的是隐藏列ROWID的主键索引)

  • 假如我们对辅助索引加锁,那么辅助索引所对应的主键索引也会被锁住

  • 主键索引被锁住,实际上就等于是整条记录都被锁住了(主键索引叶子节点存储了整条数据)

行锁的算法

==================================================================

上一篇介绍事务的时候我们提到了,MySQL通过加锁来防止了幻读,但是如果行锁只是锁住一行记录,好像并不能防止幻读,所以行锁锁住一条记录的话只是其中一种情况,实际上行锁有三种算法:记录锁(Record Lock),间隙锁(Gap Lock)和临键锁(Next-Key Lock),而之所以能做到防止幻读,正是临键锁起的作用。

记录锁(Record Lock)


记录锁就是上面介绍的,当我们的查询能命中一条记录的时候,InnoDB就会使用记录锁,锁住所命中的这一行记录。

间隙锁(Gap Lock)


当我们的查询没有命中记录的时候,这时候InnoDB就会加上一个间隙锁。

| 事务A | 事务B |

| — | — |

| BEGIN; | |

| SELECT * FROM test WHERE id=1 FOR UPDATE; | |

| | INSERT INTO test VALUE (2,‘张2’);

阻塞 |

| | INSERT INTO test VALUE (3,‘张3’);

阻塞 |

| | SELECT * FROM test WHERE id=2 FOR UPDATE;

加锁成功 |

| COMMIT;(释放锁) | |

总结

虽然面试套路众多,但对于技术面试来说,主要还是考察一个人的技术能力和沟通能力。不同类型的面试官根据自身的理解问的问题也不尽相同,没有规律可循。

上面提到的关于这些JAVA基础、三大框架、项目经验、并发编程、JVM及调优、网络、设计模式、spring+mybatis源码解读、Mysql调优、分布式监控、消息队列、分布式存储等等面试题笔记及资料

有些面试官喜欢问自己擅长的问题,比如在实际编程中遇到的或者他自己一直在琢磨的这方面的问题,还有些面试官,尤其是大厂的比如 BAT 的面试官喜欢问面试者认为自己擅长的,然后通过提问的方式深挖细节,刨根到底。

OR UPDATE; | |

| | INSERT INTO test VALUE (2,‘张2’);

阻塞 |

| | INSERT INTO test VALUE (3,‘张3’);

阻塞 |

| | SELECT * FROM test WHERE id=2 FOR UPDATE;

加锁成功 |

| COMMIT;(释放锁) | |

总结

虽然面试套路众多,但对于技术面试来说,主要还是考察一个人的技术能力和沟通能力。不同类型的面试官根据自身的理解问的问题也不尽相同,没有规律可循。

[外链图片转存中…(img-draJmmdW-1714517426875)]

[外链图片转存中…(img-nRoYRCvF-1714517426876)]

上面提到的关于这些JAVA基础、三大框架、项目经验、并发编程、JVM及调优、网络、设计模式、spring+mybatis源码解读、Mysql调优、分布式监控、消息队列、分布式存储等等面试题笔记及资料

有些面试官喜欢问自己擅长的问题,比如在实际编程中遇到的或者他自己一直在琢磨的这方面的问题,还有些面试官,尤其是大厂的比如 BAT 的面试官喜欢问面试者认为自己擅长的,然后通过提问的方式深挖细节,刨根到底。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 10
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值