乐趣区

关于前端:MySQL学习之聊聊锁及分类

对于读,在 RR 级别的 MVCC 下,当一个事务开启的时候会产生一个 ReadView,而后通过 ReadView 找到符合条件的历史版本,而这个版本则是由 undo 日志构建的,而在生成 ReadView 的时候,其实就是生成了一个快照,所以此时的 SELECT 查问也就是快照读(或者一致性读),咱们晓得在 RR 下,一个事务在执行过程中只有第一次执行 SELECT 操作才会生成一个 ReadView,之后的 SELECT 操作都复用这个 ReadView,这样就防止了不可反复读和很大水平上防止了幻读的问题。

对于写,因为在快照读或一致性读的过程中并不会对表中的任何记录做加锁操作并且 ReadView 的事务是历史版本,而对于写操作的最新版本两者并不会抵触,所以其余事务能够自在的对表中的记录做改变。

2️⃣ 读写操作都加锁

如果咱们的一些业务场景不容许读取记录的旧版本,而是每次都必须去读取记录的最新版本,比如在银行存款的事务中,你须要先把账户的余额读出来,而后将其加上本次贷款的数额,最初再写到数据库中。在将账户余额读取进去后,就不想让别的事务再拜访该余额,直到本次贷款事务执行实现,其余事务才能够拜访账户的余额。这样在读取记录的时候也就须要对其进行加锁操作,这样也就意味着读操作和写操作也像写 - 写操作那样排队执行。

对于脏读,是因为以后事务读取了另一个未提交事务写的一条记录,但如果另一个事务在写记录的时候就给这条记录加锁,那么以后事务就无奈持续读取该记录了,所以也就不会有脏读问题的产生了。

对于不可反复读,是因为以后事务先读取一条记录,另外一个事务对该记录做了改变之后并提交之后,以后事务再次读取时会取得不同的值,如果在以后事务读取记录时就给该记录加锁,那么另一个事务就无奈批改该记录,天然也不会产生不可反复读了。

对于幻读,是因为以后事务读取了一个范畴的记录,而后另外的事务向该范畴内插入了新记录,以后事务再次读取该范畴的记录时发现了新插入的新记录,咱们把新插入的那些记录称之为幻影记录。

怎么了解这个范畴?如下:

如果表 user 中只有一条 id= 1 的数据。

当事务 A 执行一个 id = 1 的查问操作,能查问进去数据,如果是一个范畴查问,如 id in(1,2),必然只会查问进去一条数据。

此时事务 B 执行一个 id = 2 的新增操作,并且提交。

此时事务 A 再次执行 id in(1,2)的查问,就会读取出 2 条记录,因而产生了幻读。

注:因为 RR 可反复读的起因,其实是查不出 id = 2 的记录的,所以如果执行一次 update … where id = 2,再去范畴查问就能查出来了。

采纳加锁的形式解决幻读问题就有不太容易了,因为以后事务在第一次读取记录时那些幻影记录并不存在,所以读取的时候加锁就有点麻烦,因为并不知道给谁加锁。

那么 InnoDB 是如何解决的呢?咱们先来看看 InnoDB 存储引擎有哪些锁。

  1. MySQL 中的锁及分类
    在 MySQL 官网文档 中,InnoDB 存储引擎介绍了以下几种锁:

同样,看起来依然一头雾水,但咱们能够依照学习 JDK 中锁的形式来进行分类:

  1. 锁的粒度分类
    什么是锁的粒度?所谓锁的粒度就是你要锁住的范畴是多大。

比方你在家上卫生间,你只有锁住卫生间就能够了,不须要将整个家都锁起来不让家人进门吧,卫生间就是你的加锁粒度。

怎么才算正当的加锁粒度呢?

其实卫生间并不只是用来上厕所的,还能够洗澡,洗手。这里就波及到优化加锁粒度的问题。

你在卫生间里洗澡,其实他人也能够同时去外面洗手,只有做到隔离起来就能够,如果马桶,浴缸,洗漱台都是隔开绝对独立的(干湿拆散了属于是),实际上卫生间能够同时给三个人应用,当然三个人做的事儿不能一样。这样就细化了加锁粒度,你在洗澡的时候只有关上浴室的门,他人还是能够进去洗手的。如果当初设计卫生间的时候没有将不同的性能区域划分隔离开,就不能实现卫生间资源的最大化应用。

同样,在 MySQL 中也存在锁的粒度。通常分为三种,行锁,表锁和页锁。

3.1 行锁
在共享锁和独占锁的介绍中其实都是针对某一行记录的,所以也能够称之为行锁。

对一条记录加锁影响的也只是这条记录而已,所以行锁的锁定粒度在 MySQL 中是最细的。InnoDB 存储引擎默认锁就是行锁。

它具备以下特点:

锁抵触概率最低,并发性高

因为行锁的粒度小,所以产生锁定资源争用的概率也最小,从而锁抵触的概率就低,并发性越高。

开销大,加锁慢

锁是十分耗费性能的,试想一下,如果对数据库的多条数据加锁,必然会占用很多资源,而对于加锁须要期待之前的锁开释能力加锁。

会产生死锁

对于什么是死锁,能够往下看。

3.2 表锁
表级锁为表级别的锁定,会锁定整张表,能够很好的防止死锁,也是 MySQL 中最大颗粒度的锁定机制。

MyISAM 存储引擎的默认锁就是表锁。

它具备以下特点:

开销小,加锁快

因为是对整张表加锁,速度必然快于单条数据加锁。

不会产生死锁

都对整张表加锁了,其余事务基本拿不到锁,天然也不会产生死锁。

锁粒度大,产生锁抵触概率大,并发性低

3.3 页锁
页级锁是 MySQL 中比拟独特的一种锁定级别,在其余数据库管理软件中并不常见。

页级锁的颗粒度介于行级锁与表级锁之间,所以获取锁定所须要的资源开销,以及所能提供的并发解决能力同样也是介于下面二者之间。另外,页级锁和行级锁一样,会产生死锁。

行锁 表锁 页锁
锁的粒度 小 大 两者之间
加锁效率 慢 快 两者之间
抵触概率 低 高 –
并发性能 高 低 个别
性能开销 大 小 两者之间
是否死锁 是 否 是

  1. 锁的兼容性分类
    在 MySQL 中数据的读取次要分为以后读和快照读:

快照读

快照读,读取的是快照数据,不加锁的一般 SELECT 都属于快照读。

1

SELECT * FROM table WHERE …

以后读

以后读就是读的是最新数据,而不是历史的数据,加锁的 SELECT,或者对数据进行增删改都会进行以后读。

1

2

3

4

5

SELECT * FROM table LOCK IN SHARE MODE;

SELECT FROM table FOR UPDATE;

INSERT INTO table values …

DELETE FROM table WHERE …

UPDATE table SET …

而在大多数状况下,咱们操作数据库都是以后读的情景,而在并发场景下,既要容许读 - 读状况不受影响,又要使写 - 写、读 - 写或写 - 读状况中的操作互相阻塞,就须要用到 MySQL 中的共享锁和独占锁。

4.1 共享锁和独占锁
共享锁(Shared Locks),也能够叫做读锁,简称 S 锁。能够并发的读取数据,然而任何事务都不能对数据进行批改。

独占锁(Exclusive Locks),也能够叫做排他锁或者写锁,简称 X 锁。若某个事物对某一行加上了排他锁,只能这个事务对其进行读写,在此事务完结之前,其余事务不能对其进行加任何锁,其余过程能够读取,不能进行写操作,需期待其开释。

来剖析一下获取锁的情景:如果存在事务 A 和事务 B

事务 A 获取了一条记录的 S 锁,此时事务 B 也想获取该条记录的 S 锁,那么事务 B 也能获取到该锁,也就是说事务 A 和事务 B 同时持有该条记录的 S 锁。

如果事务 B 想要获取该记录的 X 锁,则此操作会被阻塞,直到事务 A 提交之后将 S 锁开释。

如果事务 A 首先获取的是 X 锁,则不论事务 B 想获取该记录的 S 锁还是 X 锁都会被阻塞,直到事务 A 提交。

因而,咱们能够说 S 锁和 S 锁是兼容的,S 锁和 X 锁是不兼容的,X 锁和 X 锁也是不兼容的。

4.2 意向锁
动向共享锁(Intention Shared Lock),简称 IS 锁。当事务筹备在某条记录上加 S 锁时,须要先在表级别加一个 IS 锁。

动向独占锁(Intention Exclusive Lock),简称 IX 锁。当事务筹备在某条记录上加 X 锁时,须要先在表级别加一个 IX 锁。

意向锁是表级锁,它们的提出仅仅为了在之后加表级别的 S 锁和 X 锁时能够疾速判断表中的记录是否被上锁,以防止用遍历的形式来查看表中有没有上锁的记录。就是说其实 IS 锁和 IS 锁是兼容的,IX 锁和 IX 锁是兼容的。

为什么须要意向锁?

InnoDB 的意向锁次要用户多粒度的锁并存的状况。比方事务 A 要在一个表上加 S 锁,如果表中的一行已被事务 B 加了 X 锁,那么该锁的申请也应被阻塞。如果表中的数据很多,逐行查看锁标记的开销将很大,零碎的性能将会受到影响。

举个例子,如果表中记录 1 亿,事务 A 把其中有几条记录上了行锁了,这时事务 B 须要给这个表加表级锁,如果没有意向锁的话,那就要去表中查找这一亿条记录是否上锁了。如果存在意向锁,那么如果事务A在更新一条记录之前,先加意向锁,再加X锁,事务 B 先查看该表上是否存在意向锁,存在的意向锁是否与本人筹备加的锁抵触,如果有抵触,则期待直到事务A开释,而无须逐条记录去检测。事务B更新表时,其实毋庸晓得到底哪一行被锁了,它只有晓得反正有一行被锁了就行了。

说白了意向锁的次要作用是解决行锁和表锁之间的矛盾,可能显示某个事务正在某一行上持有了锁,或者筹备去持有锁。

表级别的各种锁的兼容性:

S IS X IX
S 兼容 兼容 不兼容 不兼容
IS 兼容 兼容 不兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容
IS 兼容 兼容 不兼容 不兼容
4.3 读操作的锁
对于 MySQL 的读操作,有两种形式加锁。

1️⃣ SELECT * FROM table LOCK IN SHARE MODE

如果以后事务执行了该语句,那么它会为读取到的记录加 S 锁,这样容许别的事务持续获取这些记录的 S 锁(比方说别的事务也应用 SELECT … LOCK IN SHARE MODE 语句来读取这些记录),然而不能获取这些记录的 X 锁(比方说应用 SELECT … FOR UPDATE 语句来读取这些记录,或者间接批改这些记录)。

如果别的事务想要获取这些记录的 X 锁,那么它们会阻塞,直到以后事务提交之后将这些记录上的 S 锁开释掉

2️⃣ SELECT FROM table FOR UPDATE

如果以后事务执行了该语句,那么它会为读取到的记录加 X 锁,这样既不容许别的事务获取这些记录的 S 锁(比方说别的事务应用 SELECT … LOCK IN SHARE MODE 语句来读取这些记录),也不容许获取这些记录的 X 锁(比如说应用 SELECT … FOR UPDATE 语句来读取这些记录,或者间接批改这些记录)。

如果别的事务想要获取这些记录的 S 锁或者 X 锁,那么它们会阻塞,直到以后事务提交之后将这些记录上的 X 锁开释掉。

4.4 写操作的锁
对于 MySQL 的写操作,罕用的就是 DELETE、UPDATE、INSERT。隐式上锁,主动加锁,解锁。

1️⃣ DELETE

对一条记录做 DELETE 操作的过程其实是先在 B+ 树中定位到这条记录的地位,而后获取一下这条记录的 X 锁,而后再执行 delete mark 操作。咱们也能够把这个定位待删除记录在 B+ 树中地位的过程看成是一个获取 X 锁的锁定读。

2️⃣ INSERT

个别状况下,新插入一条记录的操作并不加锁,InnoDB 通过一种称之为隐式锁来爱护这条新插入的记录在本事务提交前不被别的事务拜访。

3️⃣ UPDATE

在对一条记录做 UPDATE 操作时分为三种状况:

① 如果未修改该记录的键值并且被更新的列占用的存储空间在批改前后未发生变化,则先在 B+ 树中定位到这条记录的地位,而后再获取一下记录的 X 锁,最初在原记录的地位进行批改操作。其实咱们也能够把这个定位待批改记录在 B+ 树中地位的过程看成是一个获取 X 锁的锁定读。

② 如果未修改该记录的键值并且至多有一个被更新的列占用的存储空间在批改前后发生变化,则先在 B+ 树中定位到这条记录的地位,而后获取一下记录的 X 锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最初再插入一条新记录。这个定位待批改记录在 B+ 树中地位的过程看成是一个获取 X 锁的锁定读,新插入的记录由 INSERT 操作提供的隐式锁进行爱护。

③ 如果批改了该记录的键值,则相当于在原记录上做 DELETE 操作之后再来一次 INSERT 操作,加锁操作就须要依照 DELETE 和 INSERT 的规定进行了。

PS:为什么上了写锁,别的事务还能够读操作?

因为 InnoDB 有 MVCC 机制(多版本并发管制),能够应用快照读,而不会被阻塞。

  1. 锁的粒度分类
    什么是锁的粒度?所谓锁的粒度就是你要锁住的范畴是多大。

比方你在家上卫生间,你只有锁住卫生间就能够了,不须要将整个家都锁起来不让家人进门吧,卫生间就是你的加锁粒度。

怎么才算正当的加锁粒度呢?

其实卫生间并不只是用来上厕所的,还能够洗澡,洗手。这里就波及到优化加锁粒度的问题。

你在卫生间里洗澡,其实他人也能够同时去外面洗手,只有做到隔离起来就能够,如果马桶,浴缸,洗漱台都是隔开绝对独立的(干湿拆散了属于是),实际上卫生间能够同时给三个人应用,当然三个人做的事儿不能一样。这样就细化了加锁粒度,你在洗澡的时候只有关上浴室的门,他人还是能够进去洗手的。如果当初设计卫生间的时候没有将不同的性能区域划分隔离开,就不能实现卫生间资源的最大化应用。

同样,在 MySQL 中也存在锁的粒度。通常分为三种,行锁,表锁和页锁。

4.1 行锁
在共享锁和独占锁的介绍中其实都是针对某一行记录的,所以也能够称之为行锁。

对一条记录加锁影响的也只是这条记录而已,所以行锁的锁定粒度在 MySQL 中是最细的。InnoDB 存储引擎默认锁就是行锁。

它具备以下特点:

锁抵触概率最低,并发性高

因为行锁的粒度小,所以产生锁定资源争用的概率也最小,从而锁抵触的概率就低,并发性越高。

开销大,加锁慢

锁是十分耗费性能的,试想一下,如果对数据库的多条数据加锁,必然会占用很多资源,而对于加锁须要期待之前的锁开释能力加锁。

会产生死锁

对于什么是死锁,能够往下看。

4.2 表锁
表级锁为表级别的锁定,会锁定整张表,能够很好的防止死锁,也是 MySQL 中最大颗粒度的锁定机制。

MyISAM 存储引擎的默认锁就是表锁。

它具备以下特点:

开销小,加锁快

因为是对整张表加锁,速度必然快于单条数据加锁。

不会产生死锁

都对整张表加锁了,其余事务基本拿不到锁,天然也不会产生死锁。

锁粒度大,产生锁抵触概率大,并发性低

4.3 页锁
页级锁是 MySQL 中比拟独特的一种锁定级别,在其余数据库管理软件中并不常见。

页级锁的颗粒度介于行级锁与表级锁之间,所以获取锁定所须要的资源开销,以及所能提供的并发解决能力同样也是介于下面二者之间。另外,页级锁和行级锁一样,会产生死锁。

行锁 表锁 页锁
锁的粒度 小 大 两者之间
加锁效率 慢 快 两者之间
抵触概率 低 高 –
并发性能 高 低 个别
性能开销 大 小 两者之间
是否死锁 是 否 是

  1. 算法实现分类
    对于下面的锁的介绍,咱们实际上能够晓得,次要辨别就是在锁的粒度下面,而 InnoDB 中用的锁就是行锁,也叫记录锁,然而要留神,这个记录指的是通过给索引上的索引项加锁。

InnoDB 这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB 才应用行级锁,否则,InnoDB 将应用表锁。

不论是应用主键索引、惟一索引或一般索引,InnoDB 都会应用行锁来对数据加锁。

只有执行打算真正应用了索引,能力应用行锁:即使在条件中应用了索引字段,但是否应用索引来检索数据是由 MySQL 通过判断不同执行打算的代价来决 定的,如果 MySQL 认为全表扫描效率更高,比方对一些很小的表,它就不会应用索引,这种状况下 InnoDB 将应用表锁,而不是行锁。

同时当咱们用范畴条件而不是相等条件检索数据,并申请锁时,InnoDB 会给符合条件的已有数据记录的索引项加锁。

不过即便是行锁,InnoDB 里也是分成了各种类型的。换句话说即便对同一条记录加行锁,如果类型不同,起到的效用也是不同的。通常有以下几种罕用的行锁类型。

5.1 Record Lock
记录锁,单条索引记录上加锁。

Record Lock 锁住的永远是索引,不包含记录自身,即便该表上没有任何索引,那么 innodb 会在后盾创立一个暗藏的汇集主键索引,那么锁住的就是这个暗藏的汇集主键索引。

记录锁是有 S 锁和 X 锁之分的,当一个事务获取了一条记录的 S 型记录锁后,其余事务也能够持续获取该记录的 S 型记录锁,但不能够持续获取 X 型记录锁;当一个事务获取了一条记录的 X 型记录锁后,其余事务既不能够持续获取该记录的 S 型记录锁,也不能够持续获取 X 型记录锁。

5.2 Gap Locks
间隙锁,对索引前后的间隙上锁,不对索引自身上锁。

MySQL 在 REPEATABLE READ 隔离级别下是能够解决幻读问题的,解决方案有两种,能够应用 MVCC 计划解决,也能够采纳加锁计划解决。然而在应用加锁计划解决时有问题,就是事务在第一次执行读取操作时,那些幻影记录尚 不存在,咱们无奈给这些幻影记录加上记录锁。所以咱们能够应用间隙锁对其上锁。

如存在这样一张表:

1

2

3

4

5

6

7

8

9

10

11

12

CREATE TABLE test (

id INT (1) NOT NULL AUTO_INCREMENT,

number INT (1) NOT NULL COMMENT '数字',

PRIMARY KEY (id),

KEY number (number) USING BTREE

) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;

插入以下数据

INSERT INTO test VALUES (1, 1);

INSERT INTO test VALUES (5, 3);

INSERT INTO test VALUES (7, 8);

INSERT INTO test VALUES (11, 12);

如下:

开启一个事务 A:

1

2

3

BEGIN;

SELECT * FROM test WHERE number = 3 FOR UPDATE;

此时,会对 ((1,1),(5,3)) 和((5,3),(7,8))之间上锁。

如果此时在开启一个事务 B 进行插入数据,如下:

1

2

3

4

BEGIN;

阻塞

INSERT INTO test (id, number) VALUES (2,2);

后果如下:

为什么不能插入?因为记录 (2,2) 要 插入的话,在索引 number 上,刚好落在 ((1,1),(5,3)) 和((5,3),(7,8))之间,是有锁的,所以不容许插入。如果在范畴外,当然是能够插入的,如:

1

INSERT INTO test (id, number) VALUES (8,8);

5.3 Next-Key Locks
next-key locks 是索引记录上的记录锁和索引记录之前的间隙上的间隙锁的组合,包含记录自身,每个 next-key locks 是前开后闭区间,也就是说间隙锁只是锁的间隙,没有锁住记录行,next-key locks 就是间隙锁根底上锁住右边界行。

默认状况下,InnoDB 以 REPEATABLE READ 隔离级别运行。在这种状况下,InnoDB 应用 Next-Key Locks 锁进行搜寻和索引扫描,这能够避免幻读的产生。

  1. 乐观锁和乐观锁
    乐观锁和乐观锁其实不算是具体的锁,而是一种锁的思维,不仅仅是在 MySQL 中体现,常见的 Redis 等中间件都能够利用这种思维。
    https://zhuanlan.zhihu.com/p/…
    https://zhuanlan.zhihu.com/p/…
    https://zhuanlan.zhihu.com/p/…
    https://zhuanlan.zhihu.com/p/…
    https://zhuanlan.zhihu.com/p/…
    https://zhuanlan.zhihu.com/p/…
    https://zhuanlan.zhihu.com/p/…
    https://zhuanlan.zhihu.com/p/…

6.1 乐观锁
所谓乐观锁,就是持有乐观的态度,当咱们更新一条记录时,假如这段时间没有其他人来操作这条数据。

实现乐观锁常见的形式

常见的实现形式就是在表中增加 version 字段,管制版本号,每次批改数据后 +1。

在每次更新数据之前,先查问出该条数据的 version 版本号,再执行业务操作,而后在更新数据之前在把查到的版本号和以后数据库中的版本号作比照,若雷同,则阐明没有其余线程批改过该数据,否则作相应的异样解决。

退出移动版