MySQL原理(五):锁

前言

上一篇介绍了 MySQL 的事务,这一篇将介绍锁相关的内容。

只要存在并发场景,就需要解决并发带来的问题,比如常见的脏写、脏读等等。锁是解决并发问题最常用的手段之一。

MySQL 的锁机制是由存储引擎实现的,不同的存储引擎支持的锁页不同,默认的 InnoDB 存储引擎中实现的锁,按照粒度可以划分为全局锁、表级锁、行级锁。

全局锁

使用全局锁的命令:Flush tables with read lock(FTWRL)。

使用全局锁后,整个数据库就处于只读状态了,其他线程操作(增删改数据、更改表结构)都会被阻塞。

主动释放全局锁的命令为:UNLOCK TABLES。另外,当会话断开时,全局锁会被自动释放。

全局锁主要用于全库逻辑备份

在可重复读的隔离级别下,全局锁不会影响业务。

表级锁

表锁
  • 表级别的读锁(共享锁):该锁可以被多个线程所持有,但是加了共享锁的文件不能加独占锁;

  • 表级别的写锁(独占锁):该锁一次只能被一个线程所持有,不能再加其他的共享锁和独占锁;

使用了表锁后,不仅其他线程的读写会被阻塞,本线程的操作也会被阻塞。

  • 加了读锁:其他线程写操作会被阻塞;本线程只能执行读该表的操作。
  • 加了写锁:其他线程读写操作会被阻塞;本线程只能读写该表。

用法:lock table t1 read, t2 write。

MDL

MDL,Meta Data Lock,元数据锁。

所有存储引擎的表都会存在一个 .frm 文件,这个文件中主要存储表的结构(DDL 语句),而 MDL 锁就是基于 .frm 文件中的元数据加锁的。

对数据库表进行操作时,会自动给这个表加上 MDL:

  • 对一张表进行 CRUD 操作时,加的是 MDL 读锁
  • 对一张表做结构变更操作的时候,加的是 MDL 写锁

MDL 是在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的

如果有一个长事务,那么在对表结构做变更操作的时候,可能会发生意想不到的事情:

  1. 首先,线程 A 先启用了事务(但是一直不提交),然后执行一条 select 语句,此时就先对该表加上 MDL 读锁;
  2. 然后,线程 B 也执行了同样的 select 语句,此时并不会阻塞,因为「读读」并不冲突;
  3. 接着,线程 C 修改了表字段,此时由于线程 A 的事务并没有提交,也就是 MDL 读锁还在占用着,这时线程 C 就无法申请到 MDL 写锁,就会被阻塞,

那么在线程 C 阻塞后,后续有对该表的 select 语句,就都会被阻塞,如果此时有大量该表的 select 语句的请求到来,就会有大量的线程被阻塞住,这时数据库的线程很快就会爆满了。

因为申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。

所以为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉这个长事务,然后再做表结构的变更。

意向锁
  • 在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
  • 在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」;

即当执行插入、更新、删除操作前,需要先对表加上「意向独占锁」,然后对该记录加独占锁。

普通的 select 是不会加行级锁的,普通的 select 语句是利用 MVCC 实现一致性读,是无锁的。

意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables … read)和独占表锁(lock tables … write)发生冲突。

如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。

有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。

意向锁的目的是为了快速判断表里是否有记录被加锁

AUTO-INC锁

自增锁。主键自动递增是通过 AUTO-INC 锁实现的,AUTO-INC 锁是特殊的表锁机制,不是在一个事务提交后才释放,而是在执行完插入语句后就会立即释放

但是, AUTO-INC 锁再对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞。

因此, 在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种轻量级的锁来实现自增。

一样也是在插入数据的时候,会为被 AUTO_INCREMENT 修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁

行级锁

InnoDB 引擎是支持行级锁的,而 MyISAM 等其他引擎并不支持行级锁。

Record Lock

Record Lock 称为记录锁,锁住的是一条记录。记录锁是有 S 锁和 X 锁之分的:

  • 当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);
  • 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。
Gap Lock

Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。

假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。

间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的

Next-Key Lock

Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。每个 next-key lock 都是前开后闭的。

假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。

next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的

插入意向锁

一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。

加锁规则

加锁的对象是索引,加锁的基本单位是 next-key lock

加锁的命令:

//对读取的记录加共享锁(S型锁)
select ... lock in share mode;
//对读取的记录加独占锁(X型锁)
select ... for update;

行锁的释放时机是在事务提交后,并不是一条语句执行完就释放行锁

具体的行级锁枷锁规则比较复杂,不同的场景会有不同的加锁形式,总的来说可以概括为两个原则、两个优化和一个特例:

  1. 原则1:加锁的基本单位是 next-key lock。
  2. 原则2:查找过程中访问到的对象都会加锁。
  3. 优化1:索引上的等值查询,给唯一索引加锁的时候(找到了),next-key lock 退化为记录锁。
  4. 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候(没找到),next-key lock 退化为间隙锁。
  5. 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。

加锁规则这一块特别复杂,我也只是通过资料学习,并没有通过源码和实验进行验证,所以只是一知半解。

推荐阅读小林coding的MySQL是怎么加锁的?和MySQL实战45讲的《21.为什么我只改一行的语句锁这么多?》,如果需要MySQL实战45讲的资料的话可以找我要。

二级索引查询

二级索引进行锁定读查询的时候,除了会对二级索引项加行级锁,而且还会对查询到的记录的主键索引项上加「记录锁」。

没有加索引的查询

如果锁定读查询语句,没有使用索引列作为查询条件,或者查询语句没有走索引查询,导致扫描是全表扫描。那么,每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表,这时如果其他事务对该表进行增、删、改操作的时候,都会被阻塞。

行锁的粒度粗化

行锁并不是一成不变的,行锁会在某些特殊情况下发生粗化,主要有两种情况:

  • 在内存中专门分配了一块空间存储锁对象,当该区域满了后,就会将行锁粗化为表锁。
  • 当做范围性写操作时,由于要加的行锁较多,此时行锁开销会较大,也会粗化成表锁。

死锁

死锁是指两个或两个以上的线程(或进程)在运行过程中,因为资源竞争而造成相互等待、相互僵持的现象。

MySQL 当出现死锁后,一般有两种策略:

  1. 直接进入等待,直到超时。超时时间可以通过参数 innodb_lock_wait_timeout 来设置,默认值是 50 秒。
  2. 发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事 务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑,默认是开启状态。

一般情况下会选用第二种策略,但是死锁检测会造成额外的开销。

最后

本文介绍了 MySQL 锁相关的内容,下一节将介绍 MySQL 的日志。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值