MySQL锁——面试工作中的老大难

112 阅读12分钟

本文是阅读《MySQL是怎么运行的》后的读书笔记,记录了我觉得比较重要的内容,为的是以后方便复习无需再翻看书本。

1.锁是什么?

就是内存中的一个结构,当事务要对一条记录做改动时,首先会看看内存中有没有一个锁结构与这条记录关联,如果没有就会在内存中生成一个锁结构与之关联。比方说事务T1 要对这条记录做改动,就需要生成一个锁结构与之关联:

其实在锁结构里有很多信息,但是我们目前只关注两个:

  • trx信息:代表这个锁结构是哪个事务生成的。
  • is_waiting :代表当前事务是否在等待。

因为没有别的事务为这条记录加锁,所以is_waiting就是false,表示当前事务无需等待,这个场景就是加锁成功获取锁成功,然后就可以执行改动操作了。

在事务T1提交之前,另一个事务T2也想对该记录做改动,那就先去看看有没有锁结构与这条记录关联,发现有一个锁结构与之关联后,也生成了一个锁结构与这条记录关联,只不过is_waiting是true,表示当前事务需要等待,这个场景就是加锁失败获取锁失败

在事务T1提交之后,就会把该事务生成的锁结构释放掉,然后再看看有没有别的事务在等待获取锁,发现事务T2在等待获取锁,所以吧事务T2对应的锁结构的is_waiting设置为false,然后把该事务对应的线程唤醒,让它继续执行,此时事务T2就算是获取到锁了。

总结:

  • 不加锁:就是不需要在内存中生成对应的锁结构,就可以直接执行事务操作
  • 加锁成功:在内存中生成了对应的锁结构,而且锁结构的is_waiting是false,事务可以继续执行
  • 加锁失败:在内存中生成了对应的锁结构,而且锁结构的is_waiting是true,事务需要等待

2.共享锁和独占锁(读锁和写锁)

2.1.概念

共享锁英文名: Shared Locks ,简称S锁,也叫读锁。在事务要读取一条记录时,需要先获取该记录的S锁。

独占锁也称排他锁,英文名: Exclusive Locks ,简称X锁,也叫写锁。在事务要改动一条记录时,需要先获取该记录的X锁。

  1. 假如事务T1 首先获取了一条记录的S锁之后,事务T2 接着也想再获取一个记录的S锁,那么事务T2 也会获得该锁,也就意味着事务T1 和T2 在该记录上同时持有S锁。
  2. 假如事务T1 首先获取了一条记录的S锁之后,事务T2 想要再获取一个记录的X锁,那么此操作会被阻塞,直到事务T1 提交之后将S锁释放掉。
  3. 如果事务T1 首先获取了一条记录的X锁之后,那么不管事务T2 接着想获取该记录的S锁还是X锁都会被阻塞,直到事务T1 提交。

所以我们说S锁和S锁是兼容的, S锁和X锁是不兼容的, X锁和X锁也是不兼容的。因此,共享锁和独占锁是以兼容性来给锁做分类的。

2.2.读取记录时的加锁语句

之前在mvcc提到的普通select是相对特殊select而言的,什么是特殊的select,就是加锁的select

2.2.1.对读取的记录加S锁

select ... lock in share mode;

如果当前事务执行了该语句,那么它会为读取到的记录加S锁,这样允许别的事务继续获取这些记录的S锁(比方说别的事务也使用SELECT ... LOCK IN SHARE MODE 语句来读取这些记录),但是不能获取这些记录的X锁(比方说使用SELECT ... FOR UPDATE 语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的S锁释放掉。

2.2.2.对读取的记录加X锁

select ... for update;

如果当前事务执行了该语句,那么它会为读取到的记录加X锁,这样既不允许别的事务获取这些记录的S锁(比方说别的事务使用SELECT ... LOCK IN SHARE MODE 语句来读取这些记录),也不允许获取这些记录的X锁(比方也说使用SELECT ... FOR UPDATE 语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的S锁或者X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的X锁释放掉。

注意: 为什么要等待事务结束才释放呢?我只是执行了这么一条语句,并没有把这个语句放到某个事务中呀?

在MySQL中,事务不一定需要显式地用START TRANSACTION语句来开始。那么任何单独的SQL语句也都会被视作一个隐式的事务。即使你没有显式地声明一个事务,执行任何修改数据的SQL语句(例如INSERT、UPDATE、DELETE以及携带FOR UPDATE或LOCK IN SHARE MODE的SELECT语句)都会自动开启一个事务,并在语句执行完成后等待提交(COMMIT)或回滚(ROLLBACK)。

因此,当你执行SELECT * FROM t1 WHERE id = 1 FOR UPDATE;这样的语句时,即便你没有显式地声明一个事务,一个事务依然被隐式地开启了。这个事务会持续到下一次的COMMIT或ROLLBACK。

要管理这种隐式事务的行为,你可以设置autocommit变量。当autocommit设为1(默认值),MySQL将在每条语句执行后自动提交事务。如果你将autocommit设置为0,你将需要显式地调用COMMIT来提交你的事务,或调用ROLLBACK来回滚事务。

对于默认情况而言(autocommit设为1),当我执行了这条语句的时候,隐式的开启了一个事务,事务中只有这么一条语句,当这条语句执行完成后事务就自动提交了,这个语句对应的独占锁也就释放了

3.行锁和表锁

3.1.什么是行锁、表锁

前边提到的锁都是针对记录的,也可以被称之为行级锁或者行锁,对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细。

其实一个事务也可以在表级别进行加锁,自然就被称之为表级锁或者表锁,对一个表加锁影响整个表中的记录,我们就说这个锁的粒度比较粗。给表加的锁也可以分为共享锁( S锁)和独占锁( X锁)

3.2.表锁的加锁规则

如果给一张表加了S锁,那么其他事务依然可以对这张表或表中的某些记录加S锁,但不能加X锁;

如果给一张表加了X锁,那么其他事务就不能对这张表或表中的某些记录加X锁或S锁;

但是这里面有个问题:

当我给这张表加S锁时,需要确保表中的所有记录都没有加X锁;

当我给这张表加X锁时,需要确保表中的所有记录都没有加X锁或S锁;

怎么解决这个问题?

当加表锁时总不能给表中每条记录都遍历一下吧,于是有了意向锁

3.2.1.意向共享锁(简称IS锁)

当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。I代表Intention:意图

所以,IS锁属于表级锁

3.2.2.意向独占锁(简称IX锁)

当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。

所以,IX锁属于表级锁

3.2.3.意向锁该怎么使用

当给某条记录加S锁时,先在表上加IS锁(不关心是否加了IX锁),当其他事务要给某条记录所在的这个表加S锁时,先看看这个表有没有IX锁,如果有,就等待IX释放掉才能加S锁;

当给某条记录加X锁时,先在表上加IX锁(不关心是否加了IX锁或IS锁),当其他事务要给某条记录所在的这个表加X锁时,先看看这个表有没有IS锁或IX锁,如果有,就等待IS锁和IX锁释放掉才能加X锁;

总结下意向锁:

IS锁和IX锁都是表级锁,他们的出现仅仅是为了在加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁了,以避免用遍历的方式来判断表中的记录是否被上锁了

S锁和X锁分表级别的锁和行级别的锁,但是意向锁只有表级别的,一般我们很少加表级别的锁,所以也很少关心意向锁

3.3.行锁有哪些

3.3.1.记录锁(Record Locks)

记录锁就是最普通的行锁,分S锁和X锁。一般我们说的行锁都是指记录锁。

3.3.2.间隙锁(Gap Locks)

MySQL 在REPEATABLE READ 隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC 方案解决,也可以采用加锁方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上记录锁。于是有了间隙锁。

如图中为number 值为8 的记录加了间隙锁,意味着不允许别的事务在number 值为8 的记录前边的间隙插入新记录,其实就是number 列的值(3, 8) 这个区间的新记录是不允许立即插入的。但是并没有锁住number 值为8 的这条记录本身。

比方说有另外一个事务再想插入一条number 值为4 的新记录,它定位到该条新记录的下一条记录的number 值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后, number 列的值在区间(3, 8) 中的新记录才可以被插入。

如何防止其他事务在(20, +∞) 这个区间插入新记录呢?

给数据页结构中的supermum这条记录加上一个gap锁,supermum是一条虚拟的最大记录,number=20的这条记录指向了supermum

这个gap锁的提出仅仅是为了防止插入幻影记录而提出的,如果你对一条记录加了gap锁,并不会限制其他事务对这条记录加记录锁或者继续加gap锁

3.3.3.临键锁(Next-Key Locks)

间隙锁只能锁住该记录的前边的间隙,并不能锁住该记录本身,有时候我们既想锁住某条记录,又想锁住该记录的前边的间隙,所以设计InnoDB 的大叔们就提出了一种称之为Next-Key Locks 的锁。next-key锁的本质就是一个记录锁和一个gap锁的合体,它既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的间隙。

3.3.4.插入意向锁

一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了所谓的gap锁( next-key

锁也包含gap锁),如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。

但是事务在等待的时候也需要在内存中生成一个锁结构(插入意向锁),表明有事务想在某个间隙中插入新记录,但是现在在等待。

比方说现在事务T1 为number 值为8 的记录加了一个gap锁,然后T2 和T3 分别想向hero 表中插入number 值分别为4 、5 的两条记录,所以现在为number 值为8 的记录加的锁的示意图就如下所示:

由于T1 持有gap锁,所以T2 和T3 需要生成一个插入意向锁的锁结构并且处于等待状态。当T1 提交后会把它获取到的锁都释放掉,这样T2 和T3 就能获取到对应的插入意向锁了,T2 和T3 之间也并不会相互阻塞,它们可

以同时获取到number 值为8的插入意向锁,然后执行插入操作。事实上插入意向锁并不会阻止别的事务继

续获取该记录上任何类型的锁( 插入意向锁就是这么鸡肋)。

4.锁的总结

锁按兼容性可分为独占锁和共享锁,按锁的范围可分为行锁和表锁

意向独占锁和意向共享锁属于表锁,独占锁和共享锁既有表锁也有行锁

行锁具体的有:记录锁,间隙锁,临键锁,插入意向锁,如果我们只说行锁,一般指的就是记录锁。还有一些不常用的锁没必要记

数据库中怎么会造成死锁,怎么解决