• 锁的粒度划分,可分为全局锁、表级锁、行级锁
  • 锁的级别划分,可分为共享锁、排他锁
画板

全局锁

影响整个 MySQL 实例的锁,例如,FLUSH TABLES WITH READ LOCK 命令会锁定整个数据库实例的所有表,主要用于全局备份等操作。这个命令是全局读锁定,执行了命令之后库实例中的所有表都被锁定为只读。(包括 DDL 语句也会被限制)

既然使用全局锁会影响业务,那有什么方式可以避免呢?
如果数据库支持 RR 的隔离级别,在备份之前开启事务,整个事务执行期间均基于开始时创建的 Read View,配合 MVCC,备份期间仍可进行更新操作。

备份数据库的工具是 mysqldump,在使用 mysqldump 时加上 <font style="color:#000000;">–single-transaction</font> 参数的时候,就会在备份数据库之前先开启事务。这种方法只适用于支持「可重复读隔离级别的事务」的存储引擎。

表级锁

表锁
//表级别的共享锁,也就是读锁;
lock tables t_student read;

//表级别的独占锁,也就是写锁;
lock tables t_stuent write;

也就是说如果本线程对表加了「共享表锁」,那么本线程接下来如果要对表执行写操作的语句,是会被阻塞的,当然其他线程对表进行写操作时也会被阻塞,直到锁被释放。

元数据锁

我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL:

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

注意以下这种情况:当 MDL 读锁被持有时,若某线程对当前表发起 DDL 操作,试图获取 MDL 写锁,读写锁冲突,此线程之后到来的所有请求都会被阻塞。

意向锁

MySQL的Innodb引擎中,支持多种锁级别,包括了行级锁和表级锁。当多个事务想要访问一个共享资源的时候,如果每个事务都直接请求获取锁,那么就可能会导致互相阻塞,甚至导致死锁。意向锁是数据库管理系统中用于实现锁协议的一种锁机制,旨在快速判断表内是否有记录被被加锁。

  • 意向共享锁(Intention Shared Lock,IS 锁):表示事务打算在资源上设置共享锁(读锁)。这通常用于表示事务计划读取资源,并不希望在读取时有其他事务设置排它锁。
  • 意向排他锁(Intention Exclusive Lock,IX 锁):表示事务打算在资源上设置排它锁(写锁)。这表示事务计划修改资源,并不希望有其他事务同时设置共享或排它锁。

意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。

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

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

自增锁

AUTO-INC 锁是一种特殊的表级锁,由插入带有 AUTO_INCREMENT 列的表的事务获取。在最简单的情况下,如果一个事务正在向表中插入值,任何其他事务都必须等待,以便执行它们自己的插入操作,这样第一个事务插入的行就会接收到连续的主键值。

innodb_autoinc_lock_mode 变量控制用于自增锁定的算法。它允许你选择如何在可预测的自增值序列和插入操作的最大并发性之间进行权衡。

:::info
如果并发量太大,会存在锁竞争问题。另外,由于数据库的连接是有限的,当并发量特别大的时候,可能会因为连接数不够而导致阻塞。这是 mysql 获取主键 id 的瓶颈。

:::

InnoDB行级锁

行级锁是存储引擎实现的,像 MyISAM 这类引擎就不支持行级锁。表级锁的实现与存储引擎无关。行锁的释放时机是事务提交后。

  • 记录锁(Record Lock):也被称为记录锁,锁的是索引记录。
  • 间隙锁(Gap Lock):锁定一个范围,不包括记录本身。
  • 邻键锁(Next-Key Lock):Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
共享锁与排他锁
  • 共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。<font style="color:#000000;">SELECT ... LOCK IN SHARE MODE;</font>
  • 排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。<font style="color:#000000;">SELECT ... FOR UPDATE;</font><font style="color:#000000;">UPDATE</font><font style="color:#000000;">DELETE</font>等。

共享锁(S锁)满足读读共享,读写互斥。独占锁(X锁)满足写写互斥、读写互斥。

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

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

间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的

临键锁(左开右闭)

next-key lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。

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

:::info
在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。

:::

:::info
insert 语句如何加锁?

加插入意向锁<font style="color:#000000;">LOCK_MODE:INSERT_INTENTION</font>

每插入一条新记录,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁,如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态

尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。所以,插入意向锁和间隙锁之间是冲突的

MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁

以下这种情况,就会因为等待对方事务释放间隙锁而导致死锁:

:::

死锁与死锁检测

我们知道,死锁存在的四个必要因素是:互斥、占有且等待、不可强占用、循环等待。

死锁的产生一般是以下原因:

  1. 不同事务执行速度不同:如果一个事务在获取资源后执行速度很慢,而其他事务需要等待该事务释放资源,那么可能会导致其他事务超时,从而发生死锁
  2. 操作数据量过大:在持有锁的同时,又请求获取更多的锁,导致互相等待

数据库层面的死锁解决:

  1. 大部分现代 DBMS 在检测到死锁时会自动干预。它们通常选择回滚一个或多个事务来打破死锁。
  2. 除了自动干预外,很多 DBMS 也支持手动强制回滚某些事务来解决死锁。比如 navicat 解决死锁的办法:https://www.cnblogs.com/xbdeng/p/16541111.html
  3. 还有就是如果你什么都不做,MySQL 自己也可以解决死锁,一种是立刻解决,一种是延迟解决。(打破循环等待条件
  • 如果MySQL开启了死锁检测(<font style="color:rgb(34, 36, 38);">innodb_deadlock_detect = on</font> ),那么他会定时的检测死锁,在检测到死锁后,MySQL将自动选择并终止事务中的一个或多个事务来解决死锁。
  • 如何设置事务等待锁的超时时间(<font style="color:rgb(34, 36, 38);">innodb_lock_wait_timeout</font>)。当一个事务的等待获取锁的时长超过这个阈值的时候,会对这个事务进行回滚,这样也能解决死锁。
  • 通常采用的是第 1 种方案,但是针对热点行并发更新时的死锁检测的成本是极高的,1000 个并发线程同时更新一行数据,死锁的检测次数就达到了百万量级。针对这种问题可以考虑采用控制数据库服务端的并发度,比如设置等待队列等。

避免死锁:

  1. 降低隔离级别,使用 RC 代替 RR 避免因 next-key 锁而带来的死锁情况
  2. 减少操作的数据量

MySQL 加锁原则

:::info
加锁的对象是索引,基本单位是 next-key Lock,在记录锁和间隙锁能够保证不发生幻读的情况下,next-key Lock 会发生退化。

:::

唯一索引等值查询

通过select * from performance_schema.data_locks;语句可以查看加锁情况,针对主键操作的更新语句一般会出现以下结果:

可以看到,对当前主键索引加的是记录锁。之前提到,在满足幻读不发生的前提下,临键锁会发生退化。在当前情况下,要避免幻读就是要防止某一条记录被其他事务删除或新插入一条记录,在主键索引的约束下,本身就无法插入相同 id 的新记录,又由于对当前记录加了记录锁,所以不会发生记录被删除的情况。

当所查询的主键不存在时,加锁情况如下:

也就是加的间隙锁。

非唯一索引等值查询

当查询的记录「存在」时,由于不是唯一索引,所以肯定存在索引值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描,然后在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁,而对于第一个不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁

当查询的记录「不存在」时,扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁

:::info
当在数据量非常大的数据库表执行 update 语句时,如果没有使用索引,就会给全表的加上 next-key 锁, 锁会持续很长一段时间,直到事务结束,而这期间除了select ... from语句,其他语句都会被锁住不能执行,业务会因此停滞。想要避免这类问题发生,可以考虑开启 MySQL 的安全更新模式,将<font style="color:rgb(44, 62, 80);">sql_safe_updates</font>参数设置为 1,

:::

乐观锁与悲观锁实现

悲观锁
  • 尝试对数据加上排他锁
  • 若成功加锁,对记录做修改,事务完成后解锁
  • 期间若有其他事务想对数据做修改,都会等待锁释放或抛出异常
//0.开始事务
begin; 
//1.查询出商品信息
select quantity from items where id=1 for update;
//2.修改商品quantity为2
update items set quantity=2 where id = 1;
//3.提交事务
commit;
乐观锁

主要通过CAS的机制来实现,一般通过version版本号来实现

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

//查询出商品信息,quantity = 3
select quantity from items where id=1
//根据商品信息生成订单
//修改商品quantity为2
update items set quantity=2 where id=1 and quantity = 3;

显著的不同是,乐观锁不是一来就加锁,而是执行到更新语句时,才对记录加锁。

:::info
高并发场景下,悲观锁适用于写场景多的情况,乐观锁适用于写场景多的情况

:::

此作者没有提供个人介绍
最后更新于 2024-10-26