思维导图
前言
我们都知道,事务有ACID四大特性.前面讲Redis时提到过,Redis的隔离性是由单线程模型来实现的,每条命令都是串行化执行,保证彼此不相互干扰.
Redis运行在内存里,内存速度快,所以它可以这么玩,可是MySQL不行,硬盘的速度可比内存慢太多了.所以MySQL使用锁来实现隔离性.有了锁,一方面不仅能最大程度地利用数据库的并发访问,另外一方面还能确保每个用户能以一致的方式读取和修改数据.
事务与锁是MySQL中的重要内容,两者密切联系,互为因果.事务的实现离不开锁,锁的目的是保证事务.今天就来谈一谈MySQL的锁,同时也为下一篇事务专题做准备.
全局锁
根据锁的粒度不同,MySQL可以分为全局锁,表级锁和行级锁.
全局锁是粒度最粗的锁,顾名思义,就是对整个数据库实例加锁.
MySQL提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL).使用了这个命令后,整个库都处于只读状态, 任何试图修改数据库的命令都会被阻止.
全局锁的典型使用场景是,做全库逻辑备份.也就是把整库每个表都select出来存成文本.这听上去很危险,如果是主库,那么业务基本就停摆了;如果是从库,那么主从就暂时无法同步.
不过这基本是运维专属的活了,我们研发知道它的存在就行.
表级锁
MySQL的表级锁分为两类:
- lock tables … read/write
- MDL(metadata lock)元数据锁
lock tables … read,锁定整张表,只允许读,就连拿到这个锁的线程在unlock tables之前都只能读,不能写.
lock tables … write,锁定整张表,不允许其它线程访问.
MDL
- MDL分为MDL读锁和MDL写锁.
- MDL读锁之间不会相互阻塞.
- MDL写锁与写锁,写锁与读锁之间相互阻塞.
- MDL不需要显式使用,在访问一个表时mysql会自动帮我们加锁.
- DML -> MDL读锁. 对一个表进行增删查改等DML操作时,mysql自动帮我们加上MDL读锁.
- DDL -> MDL写锁. 对一个表进行结构性更改,例如增加字段等DDL操作时,mysql自动帮我们加上MDL写锁.
可以看到,MDL锁的设计是为了隔离开DML和DDL操作,但平时我们讨论mysql的锁时,注意力都在CURD的情景中,很容易就忽略了MDL元数据锁的存在.
忽视MDL也可能导致严重后果.来看一张图
session A和B拿到了MDL读锁,正常运行,而C拿到了写锁,因此被阻塞了.
C自己被阻塞了还不要紧,但它的加入会阻塞了原本不会阻塞的session D以及其它想读改表的事务–这就是锁表了.
如果这张表存的都是读的热点数据,那么由于这个阻塞可能都导致这个库的线程爆满.
如果这个C是个长事务,事务不提交,就会一直占着MDL锁.
所以在进行DDL的情况下,一定要注意MDL锁的存在.
- 尽量避免在长事务中使用.
- 如果一时半会也拿不到MDL锁,就考虑先kill掉这条命令,等表空闲的时候再来操作
除了以上两种,MySQL名义上的表级锁还包括两种意向锁IS和IX,这个放到下面与X/S锁一起介绍.
行级锁
行锁是InnoDB引擎的特点.也许你曾被问到InnoDB和MyISAM之间有何不同,在这里你就会明白锁粒度的不同(是否支持行锁)是它们最鲜明的差别.
InnoDB的行级锁包括:
- S锁 : 共享锁,也叫读锁.语法是SELECT … LOCK IN SHARE MODE.
- X锁 : 排他锁,也叫写锁.语法是SELECT … FOR UPDATE.
- 间隙锁 : Gap Lock,与S/X锁不同,锁定的是一个范围,开区间.
- Next-Key Lock : 行锁(X/S锁)与间隙锁的组合,左开右闭.
S锁
一般我们讲行锁,指的都是X锁或者S锁,它们锁定的是具体的某一行.
S锁的作用是锁定数据,只允许读,不允许修改.
mysql> select c from t where c =5 lock in share mode;
输入了上述的语句后,假如id=5的行存在,那么就会在这一行上加上S锁.
为什么说假如呢,因为如果没有符合条件的行,那么innodb加的不是S锁,而是间隙锁.
innodb加锁的逻辑是有点复杂的,这篇文章不会牵涉那么深,在介绍完事务和和索引之后,会专门去分析加锁的逻辑.
X锁
X锁的作用是锁定数据,只允许自己读写,不允许其他事务访问.
当执行update,insert,delete或者select … for update时,就会自动获取X锁.
S锁之间兼容,X锁阻塞别的X锁或者S锁.
间隙锁
间隙锁和Next-Key Lock锁定的都是一个范围,主要作用是锁定行与行之间的间隙,防止插入.
间隙锁是个开区间,Next-Key Lock左开右闭.它们可以是共享锁,也可以是排他锁.
也许你会问,怎么手动加上间隙锁啊?
其实,innodb在行上的加锁单位是Next-Key Lock,不过根据情况的不同,Next-Key Lock会退化成间隙锁,X锁或者S锁.
上面我们说select … lock in share mode加的是S锁,其实不详细.一开始加的是Next-Key Lock,在上述这个等值查询的例子中,刚好存在这么一条数据,所以它退化成了S锁.
假设表中只有id=5和id=10两条数据.
mysql> select * from t where id =7 for update;
上述sql的目的是想选择id=7的数据,可是没有这么一条数据,那么mysql就会把这个区间锁🔒住.
对于间隙锁,锁住的就是(5,10);对于Next-Key Lock,锁住的就是(5,10].它们的区别就在于右边界,间隙锁不会锁住id=10这一行,但是Next-Key Lock会.
不过,间隙锁并不是静态的,它会动态生长.在上述例子中,假如我们把id=10这一行删掉,那么间隙锁将从(5,10)变为(5,+∞]. +∞是个开区间.
你可以验证一下,在删掉id=10这一行后,试试能不能插入id=10或者更大的数据.
也许这里你还会有个疑问,为什么id=7没有对应的行也要上锁呢?而且还是把整个区间都锁上了.
这就涉及到不可重复读和幻读的问题了,得等到了事务篇再详细解释.
这里先记住,间隙锁最大的意义在于解决幻读的问题.
意向锁
InnoDB支持两种意向锁:
- 意向共享锁 : IS Lock.事务想要获得一个表中某几行的共享锁.
- 意向排他锁 : IX Lock.事务想要获得一个表中某几行的排他锁.
关于它们有以下协议:
- 在事务能够获取表中的行上的共享锁之前,它必须首先获取表上的IS锁或更强的锁。
- 在事务能够获取表中的行上的独占锁之前,它必须首先获取表上的IX锁。
你可以这么理解,获得了X锁,也一定同时持有IX锁;获得了S锁,也一定同时持有了IS锁.
IS锁和IX锁彼此兼容,而且和X/S锁也兼容,这体现了innodb支持不同粒度的锁共存的特性.
意向锁属于表级别的锁,但是实际上它们仅表意向,并没有阻塞功能.
也许你会有疑问,怎么会有不阻塞的锁?那么意向锁有什么作用呢?
根据《MySQL技术内幕:InnoDB引擎》的说法,意向锁只会阻塞全表扫描的请求.
那么我们就可以这样理解 : 意向锁的目的其实是加快能否扫全表时的判断效率.
全表扫描时,需要获得表中所有的S锁或者X锁,只要有一行获取失败,那么就无法进行扫描.
既然如此,与其试图获取全部全部的S锁或者X锁(一行一行扫描,不累吗),不如直接通过意向锁来进行判断.
如果你想读全表,本来你得一行行判断没有X锁,现在只用判断有没有IX锁即可.
如果你想写全表,本来得一行行判断有没有S或者X锁,现在只用判断有没有IX或者IS锁即可.
与其说它们是锁,倒不如说是个信号.
这样设计的好处不仅在于加快效率,而且也可以减少阻塞甚至是死锁的情况.假如我们全表扫之前不先做判断拿表锁,而是进去一行一行的获取行锁,如果我们在最后一行被阻塞了,那么我们前面获取的锁就暂时不会得到释放,同样会阻塞其他访问该表的请求,更糟糕的是如果拿到最后一行锁的事务又要拿前面的行锁,这样就会造成死锁了.
二阶段锁协议
在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议.
两段锁协议是指每个事务的执行可以分为两个阶段:生长阶段(加锁阶段)和衰退阶段(解锁阶段)。
- 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁,在进行写操作之前要申请并获得X锁。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
- 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。
MySQL的两段封锁法是这样实现的:
- 事务开始后就处于加锁阶段,一直到执行ROLLBACK和COMMIT之前都是加锁阶段。
- ROLLBACK和COMMIT使事务进入解锁阶段,即在ROLLBACK和COMMIT模块中DBMS释放所有封锁。
锁升级
锁升级是指将当前锁的粒度降低,也就是粒行锁变表锁,表锁变全局锁.
在某些数据库中,为了降低锁的开销,会在满足条件后,将多个锁合并升级到一个更粗粒度的锁.
可是在InnoDB中,1个锁的开销和1000000个锁是一样的,都没有开销.
所以InnoDB没有锁升级.