深入浅出,简述MySql中的各种锁
其实对于一个开发人员来说,对数据库的掌握并不需要这么深入。
但本人亲身经历,面试的时候来一句“请你说说什么是间隙锁”,一瞬间将我排除在候选人名单外。
而我面试的仅仅是中级开发工程师,可见现在的内卷程度有多严重。
因此本文章整理了一番Mysql中各种锁的作用和原理,欢迎学习交流。
🍅你的关注和点赞对我很重要🍅
为什么需要锁?
我们知道Mysql数据库提供了4种隔离级别来预防我们操作数据的时候可能带来的并发事务问题。
上面的图片相信每个人都烂熟于心了,Mysql默认的隔离级别就是RR(REPEATABLE READ),避免了脏读和不可重复读,我们一般也将这个隔离级别成为:可重复读。
当面试进行到这里,你自我感觉还很良好的时候,面试官突然问你:你知道数据库的隔离级别的原理是什么吗?
好,这个时候,数据库锁的概念就出来了。
下面会通过这个脑图的结构依次简绍:
1 对数据操作的类型划分
1.1 读锁(共享/S锁)
读锁也称共享锁
,多个事务同时读取同一份数据,相互不会影响不会阻塞,数据安全。
给select语句加共享锁
:
Select * from test LOCK IN SHARE MODE;
或
Select * from test FOR SHARE;
1.2 写锁(排他/X锁)
写锁也称排他锁
,排他排他,顾名思义就是排挤他人。多个事务同时操作(有读有写或同时写)同一份数据,在一个事务还没完成(提交)前,它会阻断其他的事务,保证数据的安全性。
给select语句加排他锁
:
Select * from test FOR UPDATE;
update语句默认会加上排他锁
看到这里你会不会有点感觉了。脏读是不是就是通过排他锁
解决的呢?
案例
有一张user表,其中有这样一行初始数据:
现在模拟两个事务对这行数据进行操作:
- 事务1:
查询user表,加排他锁
- 事务2:
查询user表,加排他锁
2 锁粒度划分
2.1 表锁
表锁这里我们不展开解释了。顾名思义就是每次加锁就把整张表给锁了,一杆子打死。
我们知道Mysql有两种引擎,InnoDB和MyIsam。我们常用的是InnoDB,因为MyIsam不支持事务,并且MyIsam只能表锁,不能行锁。 而InnoDB支持事务,并且可以同时支持表锁
和行锁
。
2.2 行锁
为了提高数据库的并发吞吐量,每次锁定的范围越小越好,但这样的话加锁的数量就会变多,在锁的管理上也是很消耗资源的一件事。
所以,什么时候用表锁
,什么时候用行锁
,需要我们结合实际情况来考虑。
这里我们就先详细了解下我们平时用的最多的行锁
。
2.2.1 记录锁
记录锁
,记录锁
,顾名思义就是给某一条记录加锁。
对于记录锁
而言,它也分为共享锁
和排他锁
,功能参考1.1和1.2的描述。
案例
有一张user表,其中有这样一行初始数据:
现在模拟两个事务对这行数据进行操作:
- 事务1:
修改user表,把张三的年龄改为20,默认加记录锁
(排他锁
,行锁
)
- 事务2:
查询user表,加读锁
2.2.2 间隙锁
间隙锁
是面试种最常问到的东西,听名字感觉很高大上的,其实也是很简单的一个东西。
间隙锁
是防止幻读而产生的一种锁,是加在某一条不存在的数据上的一个锁。
幻读就是事务A第一次读到8条数据,这时候事务B插入了一条,事务A这时第二次读,发现读出来9条数据。多出来的这一条我们成为幻影数据。
案例
初始数据:
间隙锁
的案例我就不截图来演示,用文字描述更易懂。
- 事务A查询id =8的数据,发现查不到数据,并加了
读锁
,未提交。 - 事务B现在要插入一个id为6或7或8或9的数据,不能插入。
- 事务A提交了事务。
- 事务B现在才插入成功。
很明显,解决了幻读的问题。那到底是什么原理呢?为什么事务B不能插入呢?
事务A查询了一个不存在数据,例如id=8的数据。那么就会产生间隙锁
,间隙锁
的这个间隙就会是5——10,也就是当前查寻Id的前后两个数据,id为5和id为10之间不能插入任何数据。
如果事务A查询的id=15,那么根据间隙锁
的原理,插入的数据id如果大于当前表最大的id,则不能插入。这里就是如果事务B要插入id大于10的数据就插入失败。
间隙锁
的危害:间隙锁
是有可能会造成死锁的问题。
2.2.3 临键锁
临键锁
就是记录锁
和间隙锁
的结合,把间隙两头的数据也加上记录锁
。
结合上面的例子,就是:
事务A查询一个id=8的数据,那么5——10区间就会加上间隙锁
,不能插入数据。而5和10这两条数据也会加上记录锁
,不能读写操作。
具体操作就是在事务A进行范围查询的时候,加上for update。
2.2.4 插入意向锁
回顾之前我们的间隙锁,被间隙锁锁定的数据是阻塞、等待。
如果现在有多个插入事务都阻塞了,那他们之间会生成一个内部锁:插入意向锁
如:
- 事务A查询id =8的数据,发现查不到数据,并加了
读锁
,未提交。(5-10之间产生间隙锁
) - 事务B插入一个id为6的数据。阻塞。
- 事务C插入一个id为7的数据。阻塞。
- 事务B,C之间就会产生一个
插入意向锁
。如果B、C插入的数据不冲突,那待事务A释放后,B,C就会依次插入成功。
3 锁态度划分
3.1 悲观锁
每次一个线程进来操作数据的时候,总会觉得有其他线程会来同时操作,所以我一来就加锁,其他的线程全部阻塞,直到我释放锁。
Mysql中的写锁(for update)就是悲观锁。
这种锁安全性高,但吞吐量低,对数据库性能消耗大,属于数据库层面的锁,适用于写多于读的场景。
3.2 乐观锁
每次一个线程进来操作数据的时候,不会觉得有其他线程会来同时操作,不加锁,但是会在操作之前先判断一下当前的数据没有没被其他线程修改过。一般通过一个版本号字段来判断。
这种锁吞吐量高,属于代码层面的锁。适用于读多于写的场景。
4 全局锁和死锁
4.1 全局锁
全局锁
是对整个数据库进行加锁,让整个库都处于只读状态。 比如我们要备份数据库,以免其他人操作数据的时候我们可以加全局锁
Flush tables with read lock
4.2 死锁
两个事务都持有对方需要的锁,并且都在等待对方释放,形成死锁。