type
status
date
slug
summary
tags
category
password

1、概述

MySQL 的锁机制是数据库并发控制的核心,从以下角度分类:
  1. 按锁的粒度分类:
      • 全局锁:用于锁住整个数据库,整个数据库就处于只读状态了。
      • 表级锁:MySQL 里面表级别的锁有这几种:
        • 表锁:显式锁定整个表
        • 元数据锁(MDL):自动加锁,无需显式操作。例如访问表时自动加 MDL 读锁,结构变更时加 MDL 写锁。
        • 意向锁:特殊的表级锁,作用是为了快速判断表里是否有记录被加锁。当另一个事务想对整个表加锁时,如果没有意向锁,它必须检查表中的每一行是否已被锁定。有了意向锁,只需要检查表级别的意向锁就可以快速判断是否可以加表锁。
        • AUTO-INC 锁:特殊的表级锁,用于自增列,在 INSERT 自动获取。
      • 行锁:只有 InnoDB 引擎是支持行级锁的,MyISAM 引擎不支持行级锁。注意行锁锁的是索引而不是用户数据。行锁分为:
        • Record Lock:锁定单条索引记录。
        • Gap Lock:锁定索引记录之间的间隙,目的是防止幻读问题。
        • Next-Key Lock:记录锁 + 间隙锁的组合,锁定一个索引记录及其前面的间隙。
        • 插入意向锁 (Insert Intention Lock):特殊的间隙锁,表示有事务想在某个间隙插入记录。
  1. 按锁的性质分类:
      • 共享锁 (S 锁/读锁):允许多个事务同时获取共享锁,但不允许其他事务获取排他锁。适用于需要读取数据但不希望其他事务修改该数据的场景。通过 SELECT ... LOCK IN SHARE MODE (MySQL 8.0 中改为 SELECT ... FOR SHARE)实现。
      • 排他锁 (X 锁/写锁):一次只能有一个事务获取资源的排他锁,不允许其他事务获取任何锁(包括排他锁和共享锁)。适用于修改数据的场景。通过 SELECT ... FOR UPDATE 实现。
  1. 按锁的实现方式分类:
      • 悲观锁:默认认为并发冲突会发生,先加锁再访问,通过 SELECT ... FOR UPDATE 实现。
      • 乐观锁:认为并发冲突较少,通过版本号或时间戳实现。

    2、全局锁、表锁、行锁

    2.1 全局锁

    全局锁的作用是锁定整个数据库实例。应用场景是数据库备份等需要保证数据一致性的场景。
    💡
    全局锁会导致整个数据库不可用,严重影响数据库可用性,生产环境谨慎使用。
    全局锁的相关命令:
    1、开启全局锁。
    执行后,整个数据库就处于只读状态了,不会阻塞读操作,但会阻塞所有写操作和大部分 DDL 操作:
    • 对数据的增删改操作,比如 insert、delete、update等语句;
    • 对表结构的更改操作,比如 alter table、drop table 等语句。
    2、释放全局锁。
    有什么办法可以降低全局锁对数据库整体的影响?
    使用备份数据库工具 mysqldump,在使用时加上 –single-transaction 参数的时候,就会在备份数据库之前先开启一个单一的事务,在这个事务中执行导出操作,确保导出的数据是某个时间点的一致快照。具体工作过程:
    1. 开始一个事务(START TRANSACTION WITH CONSISTENT SNAPSHOT
    1. 在这个事务中读取所有表的数据
    1. 由于 InnoDB 的 MVCC 特性,可以读取到事务开始时的一致数据视图
    1. 其他事务的修改不会影响这个快照
    这种方法只适用于 InnoDB 引擎,因为 InnoDB 支持事务和 MVCC。对于 MyISAM 这种不支持事务的引擎,在备份数据库时就要使用全局锁的方法。

    2.2 表级锁

    2.2.1 表锁

    表锁会锁住整个表而不是表中的特定行,分为读锁(共享锁)和写锁(排他锁)两种类型。
    • 读锁:多个会话可以同时获取同一表的读锁,持有读锁的会话可以读取表数据,但不能修改。其他会话也可以获取读锁,但不能获取写锁。
      • 写锁:一次只有一个会话能获取表的写锁,持有写锁的会话可以读取和修改表数据,其他会话不能获取读锁或写锁。
        要释放表锁,可以调用UNLOCK TABLES释放锁,或者会话结束时自动释放。
        注意:
        • MyISAM 只支持表级锁,InnoDB 支持行级锁和表级锁。
        • 在高并发环境中优先考虑使用 InnoDB 的行级锁。
        • 使用 SHOW PROCESSLISTSHOW OPEN TABLES 监控锁情况。

        2.2.2 元数据锁

        元数据锁(MDL)是MySQL中用于保护数据库对象结构(如表结构)的一种表级锁,防止当用户对表执行 CRUD 操作时,其他线程对这个表结构做了变更。
        我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,MySQL 会自动给这个表加上 MDL
        • 对一张表进行 CRUD 操作时,加的是 MDL 读锁。
        • 对一张表做结构变更操作的时候,加的是 MDL 写锁。
        这意味着:
        • 当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)。
        • 当有线程对表结构进行变更( 加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成( 释放 MDL 写锁)。
        MDL 不需要显示调用,那它是在什么时候释放的?
        • DML语句:语句执行期间持有 MDL 锁
        • DDL语句:整个事务期间持有 MDL 锁
        • 事务:直到事务结束才释放 MDL 锁
        那如果数据库有一个长事务(所谓的长事务,就是开启了事务,但是一直还没提交),那在对表结构做变更操作的时候,可能会发生意想不到的事情,比如下面这个顺序的场景:
        1. 首先,线程 A 先启用了事务(但是一直不提交),然后执行一条 select 语句,此时就先对该表加上 MDL 读锁;
        1. 然后,线程 B 也执行了同样的 select 语句,此时并不会阻塞,因为「读读」并不冲突;
        1. 接着,线程 C 修改了表字段,此时由于线程 A 的事务并没有提交,也就是 MDL 读锁还在占用着,这时线程 C 就无法申请到 MDL 写锁,就会被阻塞,
        1. 那么在线程 C 阻塞后,后续有对该表的 select 语句,就都会被阻塞,如果此时有大量该表的 select 语句的请求到来,就会有大量的线程被阻塞住,这时数据库的线程很快就会爆满了。
        为什么线程 C 因为申请不到 MDL 写锁,而导致后续的申请读锁的查询操作也会被阻塞?
        这是因为申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。
        所以为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉这个长事务,然后再做表结构的变更。

        2.2.3 意向锁

        意向锁是一种特殊的表级锁,作用是为了快速判断表里是否有记录被加锁。当另一个事务想对整个表加锁时:
        • 如果没有意向锁,它遍历表里所有记录,检查表中的每一行是否有独占锁。
        • 有了意向锁,直接查该表是否有意向独占锁,,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。
        意向锁有两种类型:
        • 意向共享锁(IS锁):先在表级别加上一个「意向共享锁」,再对表的某些记录加上「共享锁」。例如:
          • 意向独占锁(IX锁):先在表级别加上一个「意向独占锁」,再对表的某些纪录加上「独占锁」。例如执行插入、更新、删除等操作。
            注意普通的 select 是不会加行级锁的,普通的 select 语句是利用 MVCC 实现一致性读,是无锁的。
            意向锁、行锁、表锁的兼容性:
            • 意向共享锁(IS锁)和意向独占锁(IX锁)之间互相兼容,不会发生冲突。
            • 意向锁不会和行级的共享锁和独占锁发生冲突。
            • 意向锁和共享表锁(S锁)(lock tables ... read)和独占表锁(X锁)(lock tables ... write)发生冲突。
              • 请求锁类型
                IS
                IX
                S
                X
                IS
                兼容
                兼容
                兼容
                不兼容
                IX
                兼容
                兼容
                不兼容
                不兼容
                S
                兼容
                不兼容
                兼容
                不兼容
                X
                不兼容
                不兼容
                不兼容
                不兼容

            2.2.4 AUTO-INC 锁

            AUTO-INC 锁是一种特殊的表级锁,用于处理自增列(如主键ID)的表插入操作,确保自增值的唯一性。
            他的工作原理是:
            1. 表主键 ID 通常都会设置AUTO_INCREMENT 属性来实现自增。
            1. 在插入数据时,如果不指定主键的值,会自动加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值。
            1. 一个事务在持有 AUTO-INC 锁的过程中,其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时,被 AUTO_INCREMENT 修饰的字段的值是连续递增的。
            1. 等插入语句执行完成后,就会把 AUTO-INC 锁释放掉,而不是等事务提交后。
            AUTO-INC 锁在对大量数据进行插入的时候,会影响插入性能,因为其他事务的插入都会被阻塞。因此, 在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种轻量级的锁来实现自增。InnoDB 存储引擎提供了个 innodb_autoinc_lock_mode 的系统变量,是用来控制选择用 AUTO-INC 锁,还是轻量级的锁。
            • innodb_autoinc_lock_mode = 0,就采用 AUTO-INC 锁,语句执行结束后才释放锁;
            • innodb_autoinc_lock_mode = 2,就采用轻量级锁,申请自增主键后就释放锁,并不需要等语句执行后才释放。
            • innodb_autoinc_lock_mode = 1
              • 普通 insert 语句,自增锁在申请之后就马上释放;
              • 类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;
            轻量级锁的工作过程和 AUTO-INC 锁类似,在插入数据的时候,会为被 AUTO_INCREMENT 修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁。

            2.3 行级锁

            InnoDB 引擎支持行级锁的,这是它区别于 MyISAM 等存储引擎的重要特性之一。注意 InnoDB 行级锁是对索引项(聚簇索引或二级索引)加锁,而不是对具体的用户数据加锁。行级锁有三种类型:
            • Record Lock:记录锁,锁定单条索引项。例如 SELECT * FROM table WHERE id = 1 FOR UPDATE 会对 id=1 的记录加记录锁
            • Gap Lock:间隙锁,锁定索引记录之间的间隙,但不包括记录本身,是一个左开右开的区间。例如 SELECT * FROM table WHERE id BETWEEN 10 AND 20 FOR UPDATE 会对 (10, 20) 范围加锁。
            • Next-Key Lock:Record Lock + Gap Lock 的组合,锁定索引记录及其前面的间隙,是一个左开右闭的区间。例如 SELECT * FROM table WHERE id BETWEEN 10 AND 20 FOR UPDATE 会对 (10, 20] 范围加锁。
            InnoDB 引入间隙锁主要是为了解决幻读问题。比如表中加入 6 个记录,0,5,10,15,20,25。则产生 7 个间隙:
            notion image
            产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。即使把所有的记录都加上锁,还是阻止不了新插入的记录。为了解决幻读问题,InnoDB 只好引入间隙锁的概念来锁住一个区间,其他事务不能在锁定的范围内插入新数据。
            上面这个例子,可以被 Gap Lock 锁住的区间有:
            能被 Next-Key Lock 锁住的区间有:
            使用注意:
            1. 只有通过索引条件检索数据时才会使用行锁,否则会使用表锁
            1. 即使是访问不同行的查询,如果使用了相同的索引键,也可能出现锁冲突
            1. 当表有多个索引时,不同的事务可以使用不同的索引锁定不同的行

            2.3.1 记录锁(Record Lock)

            Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的:
            • 当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);
            • 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。
            举个例子,当一个事务执行了下面这条语句:
            就是对 t_test 表中主键 id 为 1 的这条记录加上 X 型的记录锁,这样其他事务就无法对这条记录进行修改了。
            notion image
            当事务执行 commit 后,事务过程中生成的锁都会被释放。

            2.3.2 间隙锁(Gap Lock)

            Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
            假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。
            notion image
            间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的
            Innodb 下自动使用间隙锁的条件:
            1. 必须在 RR 级别下。RC隔离级别下是没有间隙锁
            1. 检索的字段必须有索引。没有索引的话会全表扫描,那样会锁定整张表所有的记录,包括不存在的记录,此时其他事务不能修改不能删除不能添加)
            1. 检索条件必须是扫描一个区间。如果扫描条件是等值索引的话,那 InnoDB 只会采用行锁方式来加锁,而不会使用间隙锁的方式。

            2.3.3 Next-Key Lock

            Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
            假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。
            notion image
            所以,next-key lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。
            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 型的记录锁是冲突的。

            2.3.4 插入意向锁

            插入意向锁是一种特殊类型的间隙锁,主要用于提高并发插入的效率。一个事务在插入一条记录的时候,会先生成一个插入意向锁,表明有事务想在某个区间插入新记录,但不会实际锁定区间。
            1. 如果插入位置被其他事务加了插入意向锁,互相之间无需等待,即不会阻塞。
            1. 如果插入位置是否已被其他事务加了间隙锁(Gap Lock 或者 Next-Key Lock),插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止。
            例如:
            从这里可以看出插入意向锁的特性:
            • 插入意向锁之间互相兼容:多个事务可以在同一间隙中同时申请插入意向锁。
            • 与普通的间隙锁不兼容:如果一个事务已经持有了间隙锁,另一个事务不能获取插入意向锁。
            插入意向锁的作用是提高并发插入的效率:多个插入事务可以在同一间隙上同时持有插入意向锁,而不会相互阻塞。
            可以通过以下命令查看锁状态:
            在输出中查找"TRANSACTIONS"部分,可以看到锁的类型包括"insert intention"。

            3、共享锁和排他锁

            3.1 共享锁

            共享锁也被称为读锁或 S 锁,多个事务可以同时持有同一资源的共享锁,但会阻止其他事务获取排他锁。适用于当多个事务需要同时读取同一数据,并保证读取过程不会被其他事务修改的场景。
            获取共享锁:
            共享锁的适用场景:
            • 报表生成:需要生成一致性快照的报表
            • 数据分析:查询需要长时间运行且需要数据保持一致性

              3.2 排他锁

              排他锁也称为写锁或 X 锁,同一时间只有一个事务能持有某一资源的排他锁,排他锁会阻止其他事务获取任何类型的锁(共享锁和排他锁)。适用于需要修改数据并防止其他事务读取或修改的场景。
              获取排他锁:执行 DML 语句(INSERT/UPDATE/DELETE)会自动获取,也可以通过以下方式显示获取
              排他锁的适用场景:
              • 数据修改:数据的增删改场景。
              • 银行转账
                • 库存管理
                  共享锁和排他锁的兼容性:
                  当前锁 \ 请求锁
                  共享锁(S)
                  排他锁(X)
                  共享锁(S)
                  兼容
                  不兼容
                  排他锁(X)
                  不兼容
                  不兼容

                  4、悲观锁和乐观锁

                  Mysql 中的悲观锁和乐观锁更偏向于一种锁的设计思想,以上提到的所有类型的锁,本质上都属于悲观锁。乐观锁在 Mysql 里面没有具体的锁机制实现,而是通过版本号来实现。

                  4.1 悲观锁

                  悲观锁假设并发冲突经常发生,因此在数据被访问前就加锁。其他事务在访问数据之前需要先获取锁,获取到锁才有权限访问数据。以上提到的所有类型的锁(包括全局锁/表锁/行锁、共享锁/排他锁),本质上都属于悲观锁
                  悲观锁适用于适合写多读少,冲突概率高的场景。

                  4.2 乐观锁

                  乐观锁假设并发冲突很少发生,只在提交更新时检查是否有冲突。乐观锁在 MySQL 中不直接使用数据库的锁机制,而是通过版本控制实现:
                  1. 表中增加 version 字段。
                  1. 在更新前先查询 version 值。
                  1. 在更新时比较更新前的 version 值与当前的 version 值是否相等,是的话才更新成功。
                  悲观锁和乐观锁对比:
                  特性
                  悲观锁
                  乐观锁
                  并发性能
                  实现复杂度
                  数据库原生支持,简单
                  需应用层实现,较复杂
                  适用场景
                  写多读少,冲突概率高
                  读多写少,冲突概率低
                  锁机制
                  行锁、表锁等
                  无锁
                  冲突处理
                  等待锁释放
                  回滚或重试
                   
                  Mysql锁篇:死锁问题Mysql事务篇:事务汇总(事务隔离级别、MVCC)
                  Loading...