MySQL学习之聊聊锁及分类

本篇文章带大家了解MySQL中的锁,介绍一下锁的粒度分类和锁的兼容性分类,希望对大家有所帮助。 1. 数据库并发场景 在高并发场景下,不考虑其他中间件的情况下,数据库会存在以下

    本篇文章带大家了解MySQL中的锁,介绍一下锁的粒度分类和锁的兼容性分类,希望对大家有所帮助。<p><img src="https://img.mryunwei.com/uploads/2023/04/20230416133900855.jpg"></p>

1. 数据库并发场景

在高并发场景下,不考虑其他中间件的情况下,数据库会存在以下场景:

MySQL 四大隔离级别:

那么有什么方式来解决呢?一般来说有两种方案:

1️⃣ 读操作 MVCC ,写操作加锁

对于读,在 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 存储引擎有哪些锁。

2. MySQL中的锁及分类

在 MySQL 官方文档 中,InnoDB 存储引擎介绍了以下几种锁:

1.png

同样,看起来仍然一头雾水,但我们可以按照学习 JDK 中锁的方式来进行分类:

2.png

3. 锁的粒度分类

什么是锁的粒度?所谓锁的粒度就是你要锁住的范围是多大。

比如你在家上卫生间,你只要锁住卫生间就可以了,不需要将整个家都锁起来不让家人进门吧,卫生间就是你的加锁粒度。

怎样才算合理的加锁粒度呢?

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

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

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

3.1 行锁

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

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

它具有以下特点:

锁冲突概率最低,并发性高

由于行锁的粒度小,所以发生锁定资源争用的概率也最小,从而锁冲突的概率就低,并发性越高。

开销大,加锁慢

锁是非常消耗性能的,试想一下,如果对数据库的多条数据加锁,必然会占用很多资源,而对于加锁需要等待之前的锁释放才能加锁。

会产生死锁

关于什么是死锁,可以往下看。

3.2 表锁

表级锁为表级别的锁定,会锁定整张表,可以很好的避免死锁,也是 MySQL 中最大颗粒度的锁定机制。

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

它具有以下特点:

开销小,加锁快

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

不会产生死锁

都对整张表加锁了,其他事务根本拿不到锁,自然也不会产生死锁。

锁粒度大,发生锁冲突概率大,并发性低

3.3 页锁

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

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