Mysql-深入理解Mysql中的锁


1. Mysql中有哪些锁?

Mysql中,根据加锁的范围可以分为全局锁,表级锁和行锁三种

表级锁:表锁,元数据锁,意向锁,AUTO-INC

全局锁

行级锁:Record LockGap LoclNew Key Lock

2. 全局锁是怎么用的?

要使用全局锁,那么可以使用内置的悲观锁,使用的指令如下

flush tables with read lock

执行完了之后,整个数据库的状态就处于只读的状态了,当执行以下几种指令的时候,就会导致阻塞

  • 对数据库表的结构的DDL语句:比如说alter tabledrop table等语句
  • 对数据库数据的CRUD的DML语句,比如说insertupdatedelete等语句

在加锁完成业务后,就可以执行解锁

unlock tables

当会话断开了,全局锁就会被自动释放

3. 什么时候要用全局锁?

锁的基本应用场景是在多线程并发操作下使用的,全局锁也是如此

Mysql需要定期做一个数据库的备份,由于备份的过程涉及到大量的I/O,因此在这种情况下,如果不对用户的访问加以限制,那么就很有可能产生并发的错误

我们假设一个网购的例子

A从B商店中购买了C商品,消耗了D元

那么这个过程可以分为对两张表的操作

  • 1.用户表中A的余额要减D
  • 2.商店中C商品的库存要减去1

假设现在数据库备份线程正在执行,假设没有加锁,允许用户线程并发执行

1在备份线程备份数据库之前执行了,2在备份线程数据库之后执行了

这样的话就产生了数据的不一致的问题,最终导致错误

当1和2的顺序调换的时候,会导致更加严重的问题,用户相当于买了一件商品而且余额没减!这是严重的生产事故

3. 加锁会带来什么问题?

加锁这个就好像JVM中的STW,但是它相对来说程度轻一点,因为它还能够接收select的请求,而STW会导致整个进程无法接收任何的请求。

如果数据库有很多数据,备份就会花费很多时间,关键是备份的期间会导致DML和DDL被阻塞。

4. 可以用不加锁的方式来保护备份数据库数据的过程吗?

如果数据库的引擎支持RR的隔离级别,那么在备份数据库之前会先开启事务,会先创建Read View,然后整个事务执行期间都会在用这个Read View,而且由于MVCC的支持,备份期间依然可以对数据进行更新的操作

因为在可重复读的隔离级别下,即使其他事务更新了表的数据,也不会影响备份数据库时的一个快照读,这就是事务四大特性中的隔离性,这样的话备份期间备份的数据一直是在开启事务时的数据。

备份数据库的工具是mysqldump,在使用mysqldump时加上-single-transaction参数的时候,就会在备份数据库之前先开启事务,这种方法只适用于支持可重复读隔离级别的事务的存储引擎

InnoDB存储引擎默认的事务隔离级别就是RR,因此采用这种方式来备份数据库

但是对于MyISAM这种不支持事务的引擎,在备份数据库的时候就要使用全局锁

5. Mysql的表级锁有哪些?具体怎么用?

表级锁具体来说有这几种

  • 表锁
  • 元数据锁(MDL)
  • 意向锁
  • AUTO-INC锁

6. 表锁是什么?有什么用?

表锁通常分为读锁和写锁,读锁是共享锁,意味着多个线程可以同时读这张表

写锁是独占锁,当有线程在写的时候,其他线程不能操作表

表锁的使用方法,如果想对table加锁,可以使用下面的命令

lock table t_t read;//加读锁
lock table t_t write;//加写锁

这是一个基本的读写锁的操作模型,它除了会限制其他线程的读写操作之外,还会限制本线程的读写操作

比如说本线程加了一个读锁,那么下面就不能写了,反之亦然

解锁的操作

unlock tables

7. 元数据锁是什么?有什么用?

元数据锁的存在是为了避免一种情况:

当有线程对表进行CRUD操作的时候,其他线程对表的结构进行了修改,元数据就是当前表的一些基本信息,比如字段类型,字段名称等

元数据锁的调用并不需要使用命令来显式的开启,而是在当命令满足特殊要求的时候自动开启

  • 当对一张表进行CRUD的操作的时候,加的就是MDL读锁
  • 当对一张表的表结构进行修改的时候,加的就是MDL写锁

8. 既然MDL锁不需要被显式调用,那么是在什么时候释放的?

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

如果数据库中有一个长事务,对表结构做一个变更操作的时候,就可能导致意外发生

比如说:当线程A做了一个更改表的操作的时候,这时候自动申请了一个MDL写锁,然后它是一个长事务,因此如果有其他事务是对这个表做CRUD操作的话,那么就会有其他大量的事务到来的时候被阻塞,从而导致Mysql崩溃

还有一种情况:线程A对表做CRUD,于是申请了MDL读锁,线程A是是一个长事务,因此短时间内不会释放锁,后来一个线程B到来对表做一个表的结构修改,因此申请了一个MDL写锁,这时候线程B就会被阻塞,这倒还好,如果后续突然并发量上来,来了1w个查询的线程,这时候就会导致这1w个线程都被阻塞了

为什么线程 C 因为申请不到 MDL 写锁,而导致后续的申请读锁的查询操作也会被阻塞?

这是为了避免线程饥饿而做的一个权衡,为了避免写锁长时间获取不到,因此Mysql在设计锁的时候,设计了一个申请锁的队列,要申请锁,必须排队申请。这样的话就形成了相对公平。

9. 意向锁是什么?有什么用?

  • InnoDB引擎表中的某些记录加上共享锁之前,需要先在表级别上加上一个意向共享锁
  • InnoDB引擎表中的某些记录加上独占锁之前,需要现在表级别上加上一个意向独占锁

当执行插入、更新、删除的时候,需要先对表加上意向独占锁,然后再对该记录加独占锁

为什么要这样做?直接加锁不就好了吗?

我们知道S锁X锁之间是存在一个读写冲突的,也就是说:

读读不冲突,读写冲突,写写冲突,因此在给表加锁之前,还需要判断表中的记录是否有冲突的锁

假设一个表中有100w条数据,然后为了要给这张表上一个锁,那么就必须遍历这张表,看一百万条记录是否被锁上了,这样的话十分耗时,那么意向锁的作用就是做一个标记,就是说当前的表中有记录被锁上了,而且还标记了锁的类型,这样的话通过1次判断,就能够知道这个表能否上锁了

加锁的操作是怎么样的?

//共享锁
select ... lock in share mode;
//独占锁
select ... for update

10. AUTO-INC锁是什么?有什么用?

通常来说,我们会将表的主键设置为自增的,而自增主键的id分配的这个过程并不是原子性的,因此可能会导致多个线程同时插入数据的时候,导致自增主键相同的问题。

那么在这样的情况下,就需要给分配自增主键的过程的加一个锁,让每个线程都能分配到唯一的ID

目前来说,自增主键分配过程的上锁策略有三种,可以通过调整innodb_autoinc_lock_mode这个变量进行实现

第一种,在innodb_autoinc_lock_mode = 0 的时候,采取的是最安全的方式,也就是只有当语句执行完毕的时候,才会释放这个锁,然后其他想要insert的线程才能够获取到自增的id,会造成线程的阻塞

第二种,在innodb_autoinc_lock_mode = 1的时候,采取了一种折中的方案,当能够提前知道需要多少个ID的时候,就会使用一种轻量级锁的机制,这种机制的工作原理就是对字段进行上锁,当有人在申请ID的时候,其他事务不能够插队,但是当不知道当前需要多少个ID的时候,就需要加这个表级锁了

第三种,在innodb_autoinc_lock_mode = 2的时候,一律采取轻量级锁的机制,也就是只对字段加锁,而不对整个表进行加锁,在这种情况会产生并发问题,这也就是为什么=1时在是不知道插入语句条数的时候要采用表锁的原因

考虑一个主从复制的场景

在这个情况下,有两个线程并发执行,具体的操作可以描述如下:

  • sessionB先插入了两条记录,内容是(1,1,1)、(2,2,2)
  • 并发执行,A插入了(3,5,5)
  • 然后sessionB插入了(4,3,3),(5,4,4)

于是在这种情况下,sessionB的插入的id是不连续的,然而在lock_mode = 1的时候是会是连续的。

讨论主从复制的情况:当主库发生了这种情况,bin log面对t2表的更新只会记录这两个sessioninsert的语句的原始逻辑,也就是主键全部为null,而在这样的情况下,我们首先要明白,记录日志的时候实际上是按照session的整体进行记录的,要么先记录 sessionA要么就先记录sessionB,于是并发执行的情况就被抹除掉了,最终导致了从库数据和主库的数据不一致

怎么解决呢?

要解决这个问题,binlog的日志格式应该要设置为binlog_foramt = row,这样设置能够使得bin log里面记录的是主库分配的自增值,到备库执行的时候,主库的自增值是啥,从库的就会是啥

11. 行级锁有哪些?有什么用?

行级锁根据范围包括有三种

  • Record Lock:记录锁,也就是仅仅将一条记录给锁上
  • Gap Lock:间隙锁,锁定一个范围,但是不包含记录本身
  • Next-key Lock:RecordLock+Gap Lock的组合,锁定一个范围,并且锁定记录本身

InnoDB是支持行级锁的,但是MyISAM引擎并不支持行级锁

InnoDB下,普通的select语句根据事务的隔离级别会读取到不同的内容,它是属于快照读,在MVCC的控制下实现并发安全的,但是可以使用以下的方面将快照读升级为锁定读

select ... lock in share mode;
select ... for update

当事务提交了,锁就会被释放

简单来说,行级锁实现了更小细粒度的锁,使得锁的作用范围更小了

12. RecordLock是什么?有什么用?

Record Lock叫做记录锁,它是用来对一条记录加锁的,锁的类型有共享锁和互斥锁

使用场景通常是要对这条记录做一个更新,防止其他事务篡改

通常用在较低级别的事务隔离级别下的并发安全。

13. GapLock是什么?有什么用?

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

假设,表中有一个范围id为(3,5)的间隙锁,那么其他事务就无法插入id = 4的这条记录了

为什么GapLock能够解决幻读问题

首先要理解什么是幻读问题,幻读问题就是当执行相同的查询语句的时候,先后查询出来的数据条数不一致,当我们使用GapLock的时候,它能够限制某一范围内的数据的插入/删除,从而使得数据条数不会发生变化

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

14. Next-Key Lock是什么?有什么用?

Next-Key Lock叫做临键锁,是Record LockGap Lock的一个组合,锁定一个范围,并且锁定记录本身

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

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

比如,一个事务持有了范围为 (1, 10] 的 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,就会被阻塞。

虽然相同范围的间隙锁是多个事务相互兼容的,但对于记录锁,我们是要考虑 X 型与 S 型关系,X 型的记录锁与 X 型的记录锁是冲突的。

这个锁能够可以保证可重复读并且能够保证幻读的问题被解决

15. 插入意向锁是什么?有什么用?

一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。

如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。

举个例子,假设事务 A 已经对表加了一个范围 id 为(3,5)间隙锁。

当事务 A 还没提交的时候,事务 B 向该表插入一条 id = 4 的新记录,这时会判断到插入的位置已经被事务 A 加了间隙锁,于是事物 B 会生成一个插入意向锁,然后将锁的状态设置为等待状态(PS:MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁),此时事务 B 就会发生阻塞,直到事务 A 提交了事务。

插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁

如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。

插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。

16. 什么是两阶段锁协议?

两阶段加锁协议在数据库原理一书中描述是:当一个事务流程是,前期只有加锁的过程,后期只有解锁的过程,这个协议使得事务之间可以按照它们提交的顺序串行化。

这给我们设计事务的流程做了一个启发:

当事务中需要锁多个行的时候,要把最可能造成锁冲突,造成并发问题的锁往后面放

假设你负责实现一个电影票在线交易业务,顾客 A 要在影院 B 购买电影票。我们简化一点,这个业务需要涉及到以下操作:1.从顾客 A 账户余额中扣除电影票价;2.给影院 B 的账户余额增加这张电影票价;3.记录一条交易日志。

那么如何来安排1 2 3的顺序呢,首先影院的业务大部分都是出售电影票,因此购买电影票的环节最有可能产生一个并发的问题,因此,你应该要将2 尽可能的往后面放,日志业务的话一般不会产生并发问题,而用户账户余额出现并发问题的概率比日志规的概率大得多,因此设计出来的加锁顺序是3 1 2

17. 死锁和死锁检测

死锁的四个基本条件:第一个条件是互斥,当一个线程持有锁的时候,另一个线程不能访问这个锁所对应的临界资源,第二条件是占有而且等待,说的是当线程申请了一部分资源后,如果还需要申请资源,它不会释放已经申请已经有的资源,而是等待新的资源被许可。第三个条件是不可剥夺,线程已经占有的资源不能够强行剥夺,第四个条件是循环等待,线程之间互相等待对方的资源被释放

InnoDB中,通常有两种策略来解决死锁的问题

  • 第一种策略:直接进入等待状态,不解决,直到超时后自动释放锁资源,innodb_lock_wait_timeout
  • 第二种策略:发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行,innodb_deadlock_detect

每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是 O(n) 的操作。假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是 100 万这个量级的。虽然最终检测的结果是没有死锁,但是这期间要消耗大量的 CPU 资源。因此,你就会看到 CPU 利用率很高,但是每秒却执行不了几个事务。

1个线程的行为是:检查自己持有锁的情况,然后再检查其他999个线程持有锁的情况


文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录