面试必备之MySQL有几种锁?

读书笔记:自卑这个词听起来不太好听,我更倾向于是个人对现实的清醒认识以及坦然面对。自卑和自卑情结有着本质区别。比如学历差,如果可以坦然接受面对学历背景不好,并能认识到需要更加努力才能弥补差距,自卑将被划为动力。如果总是以自身条件差为借口和原因,觉得自己做不到或无法实现xx。或者认为如果我学历好,我也可以做到或者实现xx。这样就是自卑情结。

一、前言背景

谈及数据库锁,我们脑海中最先浮现且最为常见的当属行锁和表锁。在 MySQL 数据库中,不同的存储引擎对锁的支持有所差异。其中,InnoDB 存储引擎支持行级锁,而 MYISAM 存储引擎仅支持表级锁。这种锁粒度上的显著区别,使得 InnoDB 在互联网海量数据高并发的时代背景下脱颖而出,成为了 MySQL 默认的存储引擎。与之相对,MYISAM 更适用于先进行一次性大批量更新导入,后续日常操作以查询为主、写入较少的应用场景。

二、MySQL InnoDB 的锁类型

在 MySQL 5.7 版本中,InnoDB 存储引擎提供了 8 种不同类型的锁,具体包括读锁、写锁、意向锁、记录锁、间隙锁、临键锁、写入意向锁以及自增主键锁。不过,在实际应用中,我们最为关注的通常是读锁和写锁。意向锁属于表级别的锁,而记录锁、间隙锁和临键锁则是不同锁范围的具体表现形式。今天,我们将重点深入探讨在并发事务场景下,对读写锁的实践理解。

2.1 读锁和写锁(S 锁和 X 锁)

读锁,即 S 锁,其全称为 shared (S) locks;写锁,也被称为排他锁,全称为 exclusive (X) locks。此外,它们还有其他常见的表述,如共享锁和独占锁。在 MySQL 数据库中,共享锁实际上就是读锁,而写锁则等同于排他锁和独占锁。

在 MySQL 的并发事务场景下,读锁和写锁具有不同的加锁规则。若一行数据已被加上读锁,其他事务仍可对其进行读取操作,也能够为这行数据添加读锁,但不允许添加写锁。相反,若一行数据被加上了写锁,其他事务既不能为其添加读锁,也不能添加写锁。这表明,数据的读锁之间不会相互排斥,而写锁与写锁、读锁之间是相互排斥的。

2.2 意向锁

在 MySQL 5.7 版本中,意向锁(Intention Locks)属于表级别的锁。当表中的某些数据被加上读锁或者写锁后,系统会同时为该表添加意向读锁或意向写锁标识。当系统需要为这张表添加表锁时,只需查看这个标识,就能判断表中是否已有数据被加锁,而无需对全表进行扫描,从而显著提高了表加锁的效率。

在日常开发中,表级意向锁的使用频率极低,除非是数据库管理员(DBA),研发人员几乎无需了解其具体应用。这是因为意向读锁和意向写锁之间并不会相互排斥。实际上,意向锁真正产生影响的是表锁。例如,当执行数据定义语言(DDL)操作,如为表新增、修改或删除某个字段属性时,系统会自动触发表锁。一旦表被加上排他锁,表内的数据就无法直接申请更新数据的排他锁,必须等待表锁释放后,才能申请行级别的排他锁。

2.3 记录锁

记录锁的全称为 Record Locks。需要说明的是,记录锁、间隙锁和临键锁主要是针对锁的范围而言。例如,当 id 为主键或者索引时,执行如下 SQL 语句:

select * from user_mvcc_demo where id = 3 for update;

此操作会触发行的排他锁,且锁的范围为记录级锁,即会精确地为 id = 3 的这行数据加上锁。

2.4 间隙锁

间隙锁,全称是Gap Locks。间隙锁的作用是锁住某个范围。比如事务A把id范围【1,4】的数据都加了排它锁,那事务B再尝试给id=4的数据加锁,就会失败。

这里锁的范围就是间隙锁。作用是可以避免该范围被新增或者删除,以及其他事务尝试修改该范围的数据。比如表里有id=1,4,9三条数据。如果对select * from user_mvcc_demo where id>=1 and id<=4 for update;加锁,那么就可以有效阻塞,新增id=2,3的数据。可以有效避免幻读。

总结起来:间隙锁,用于锁住目标范围数据的【间隙】或者说空隙,可以避免幻读。

2.5 临键锁

临键锁全称Next-Key Locks。实际上就是临键锁=记录锁+间隙锁。看起来不太好理解,容易和间隙锁搞混。可以这么理解: 临键锁,不仅锁住范围数据的间隙,也锁住了相关行前后间隙。

比如事务A把id范围【1,7)的数据都加了排它锁,实际因为触发了临键锁,事务B尝试给id=7的数据加锁,就会需要等待,无法直接加锁。

三、MySQL加锁、释放锁有多少种方式

3.1 读锁-S锁,加锁和释放锁方式

可以在sql 末尾增加 lock in share mode的方式去加读S锁。比如:

begin;

select \* from user\_mvcc\_demo where id=1 lock in share mode;

释放读锁,当事务结束后会自动释放。比如commit,或者rollback,或者会话超时,也都会自动释放。

3.2 写锁-X锁

3.2.1 主动加锁

可以在sql 末尾增加for update的方式去加写X锁。比如:

begin;

select \* from user\_mvcc\_demo where id=1 for update;

3.2.2 自动加锁

在事务里,insert、update、delete语句都是会自动加写锁。

四、事务并发场景实战验证

4.1 并发事务同时读和写,不冲突demo实战

场景设计:事务A查询id=1的数据,但是不加读锁。另一个事务B尝试加锁更新,是否会被卡住?

事务1- 读id=1数据。

begin;

select \* from user\_mvcc\_demo where id=1;

事务2-直接尝试加独占锁

select \* from user\_mvcc\_demo where id=1 for update;

预期效果:事务B直接加锁成功,并可以执行更新。

具体示例:

首先,事务AB、并发开启并执行查询id=1的数据,结果互不干扰。

然后事务B继续对name值进行更新并查询。

update user\_mvcc\_demo set name='拉丁解牛说技术002' where id=1;

select \* from user\_mvcc\_demo where id=1;

结论:

在MySQL InnoDB存储引擎下,事务并发读写,两个事务没有受写锁的影响。底层就是系列五,之前说的MVCC机制,undoLog版本链支持严格按照事务隔离级别去读数据、更新数据。MVCC的设计,大幅提升MySQL InnoDB存储引擎的事务并发能力。

4.2 并发事务加锁读和加锁写,锁冲突demo实战

场景设计:事务A查询id=1的数据,并加了读锁,另一个事务B尝试加锁更新,是否会被卡住?

事务1- 读id=1数据,并加读锁。

begin;

select \* from user\_mvcc\_demo where id=1 lock in share mode;

事务2-直接尝试加独占锁

select \* from user\_mvcc\_demo where id=1 for update;

预期效果:事务B会被卡住,直到超时,或者事务1提交释放锁。

具体示例:

demo结果:事务1一直未提交事务释放锁,最后事务B,加锁超时失败。

4.3 事务里的select 查询默认是否加读锁?实战验证

事务A里,查询id=1的数据;事务B对id=1的数据进行加写锁,是否会读事务A的重复读有影响?

如下图:

结论:MySQL的select查询,默认是不加读锁。

而且并发事务下,事务B虽然对id=1加了写锁,事务A里仍然可以继续查id=1的数据。底层就是基于readView机制去读事务隔离级别下对应的数据。如下:事务A可以重复读,没有被写锁阻塞。

4.4 如何一句话说清多事务并发时读写加锁与不加锁的场景?

我们很容易理解这样一种常见表述:“数据若被加上读锁(S 锁),多个事务可以共享读取该数据,但不能再对其加写锁(X 锁);若数据被加上写锁,则不能被读取。”

然而,这种表述存在一定的误导性。实际上,当数据被加上写锁(X 锁)时,数据本身是可以被读取的,只是其他事务不能再对其加读锁。也就是说,正确的表述应该是:数据加了 X 锁后,数据本身可读,但不能再加读锁。

若数据已经被加上写锁,此时其他事务若尝试加读锁(例如执行 select * from t1 where id = 1 lock in share mode 语句),那么该事务肯定无法读取数据。具体情况如下:

首先事务B,加了写锁:

mysql> begin;

Query OK, 0 rows affected (0.00 sec)

mysql> select \* from user\_mvcc\_demo where id=1 for update;

然后事务A尝试加读锁,就会被阻塞卡住。

mysql> begin;

Query OK, 0 rows affected (0.00 sec)

mysql> select \* from user\_mvcc\_demo where id=1 lock in share mode;

结果如下图:

最后,事务 A 因加读锁超时,操作以失败告终,系统抛出错误信息:

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

在这个示例中,还有另一种情况值得关注。即便事务 B 在读取数据时添加了排他锁,若事务 A 不添加读锁,而是直接读取数据,是能够顺利获取到数据的。

简而言之,在并发事务场景下,若数据已被加写锁,其他事务便无法再对其加读锁;若数据已被加读锁,多个事务可以共享读取该数据,但不能再对其加写锁。

此外,当不主动进行加锁操作(如使用 xxx for updatexxx lock in share mode 这类语句)时,各个事务能够自由地并发进行读写操作,彼此互不干扰,并发效率极高。

五、MySQL 的锁,锁定的是索引吗?

在日常操作中,当我们要对特定 id = xx 的数据进行加锁时,无论是使用 update xx 语句,还是采用 select xx for update 的方式,如果 id 列并非主键,那么操作将会触发表锁,从而锁住整张表。而当 id 是主键,也就是索引时,操作则只会锁定一行数据。

下面通过一个具体的案例来进行说明。假设表中存在 3 条数据,其 id 分别为 1、2、3。

在事务 A 中,对 id = 1 的数据添加了排他锁。在此情况下,事务 B 依旧能够对 id = 2id = 3 的数据添加排他锁。

之后,当事务 A 尝试对已经被事务 B 加锁的 id = 2 的数据进行加锁时,就必须等待,进入排队状态。

效果如下图: