MySQL数据库——常见的几种锁分类

MySQL数据库——常见的几种锁分类

大家好,这里是编程Cookbook。本文详细介绍MySQL的几种常见锁分类,如:表级锁、行级锁、页面锁、悲观锁、乐观锁、共享锁、排他锁、Gap-锁等。

按锁粒度分

表级锁

  • 开销小,加锁快,不会出现死锁,锁粒度大(整张表)。
  • 并发度低,发生锁竞争概率大,适合查询。

行级锁

  • 开销大,加锁慢,会出现死锁,锁粒度最小(一行数据)。
  • 并发度高,发生锁竞争概率小,适合并发写,事务控制。

页面锁

  • 开销、加锁速度、锁粒度、并发度都介于表级锁和行级锁之间,会出现死锁。

总结

很难说哪种锁更好,只能根据具体应用程序的特点选择合适的锁。

  • 对于查询远大于修改的场景,表级锁是合适的,因为锁粒度大但管理简单
  • 对于并发查询并发更新少量数据的应用,行级锁是更合适的选择,它提供更高的并发性和灵活性

InnoDB的默认锁是行级锁,但通过意向锁机制实现了多粒度锁的协同工作。


锁与索引关系

行级锁并不是直接对记录行加锁,而是对行对应的索引加锁

  • 如果 SQL 语句操作了主键索引,MySQL 会锁定这条主键索引。
  • 如果 SQL 语句操作了非主键索引,MySQL 会先锁定该非主键索引,再锁定相关的主键索引。
  • 在 InnoDB 中,如果 SQL 语句不涉及索引,则会通过隐藏的聚簇索引对记录加锁。
  • 对聚簇索引加锁,实际效果和表锁一样,因为找到某一条记录就得扫描全表,锁定表的行为和表锁一致。

按加锁机制分【逻辑上的锁】

悲观锁

  • 悲观锁思想认为并发问题极易发生,因此每次操作时,无论读写,都会先对记录加锁,以防止其他线程对数据进行修改。
  • 实现方式:数据库的行锁、读锁和写锁。

例如,使用 select...for update

select * from User where name='jay' for update;

注意:如果没有索引/主键,悲观锁会是表锁;否则是行锁。

以上 SQL 会锁定 User 表中所有符合检索条件(name='jay')的记录,在本次事务提交之前,其他线程无法修改这些记录。

乐观锁

  • 乐观锁认为多个线程操作不会频繁冲突,有线程过来,先放过去修改,如果看到别的线程没修改过,就可以修改成功,如果别的线程修改过,就修改失败或者重试。
    • 如果没有修改,事务成功;如果其他线程修改过,操作失败或重试。
    • 实现方式:乐观锁一般会使用 版本号机制CAS(Compare and Swap)算法

步骤:
乐观锁假设并发冲突的概率较低,因此在数据读取时不会加锁。每次数据修改时,都会检查数据的版本号来保证数据一致性。具体步骤如下:

版本号机制
  1. 读取数据:读取数据时,获取当前数据的值和对应的版本号(version1)。
  2. 修改数据:在修改数据时,检查当前数据的版本号(version2)是否与读取时的版本号(version1)一致。
  3. 版本号一致:如果一致,说明没有其他事务修改数据,可以进行更新,并将版本号自增。
  4. 版本号不一致:如果不一致,说明数据已被其他事务修改,需要回滚或重试。
CAS(Compare and Swap)
  1. 读取数据:读取数据时,获取当前值(value1)。
  2. 修改数据:在修改数据时,检查当前值(value2)是否与读取时的值(value1)一致。
  3. 值一致:如果一致,执行更新操作,并将数据值修改为新的值(value2)。
  4. 值不一致:如果不一致,说明数据已被其他线程修改,需要回滚或重试。
区别

CAS更加底层,通常直接通过原子操作实现,而版本号机制则依赖于显式的版本号字段,并且需要依赖外部锁或同步机制来保证原子性。

  • 版本号机制依赖显式的版本号字段来跟踪数据修改。
  • CAS通过底层原子操作直接对内存中的数据进行比较和更新。

按兼容性分

共享锁(读锁)

  • 共享锁允许当前线程对共享资源加共享锁,其他线程可以读取该资源,可以继续追加共享锁,但不能修改此资源,也不能追加排他锁。
  • 语法:
    select id from t_table in share mode;
    
  • 多个共享锁可以共存,但共享锁与排他锁不能共存

排他锁(写锁)

  • 排他锁会锁住共享资源,其他线程既不能读取/更新此资源,也不能追加共享锁或排他锁。
  • 语法:
    update t_table set a=1;	// 数据库的增删改操作默认都会加排他锁
    select * from t_table for update;	// for update会对查询的记录加排他锁,确保其他事务无法修改这些记录,但它本身并不进行增、删、改操作。
    
  • 排他锁是独占的,不会与其他锁共存

按可见性划分

  • 隐式锁 是数据库根据事务隔离级别和执行操作 自动加的锁,不需要用户干预。
  • 显式锁 是用户显式指定的锁,通过 SQL 语句来控制。

隐式锁

  • 隐式锁:InnoDB 会根据事务的隔离级别和操作自动加锁,这些锁是在数据库内部隐式管理的,用户不需要显式指定。
  • InnoDB 使用 两阶段锁定协议(2PL)
    • 扩展阶段:事务可以获取锁但不能释放锁,避免了数据在事务执行期间被其他事务意外修改等情况。
    • 收缩阶段:事务可以释放锁但不能再获取锁,这标志着事务已经基本完成了对数据的操作。

显式锁

  • 显式锁:显式锁是指由用户通过 SQL 语句明确指定的锁,如 SELECT FOR UPDATELOCK IN SHARE MODE 等,用户可以通过这些语句手动加锁。

注意:按照不同维度划分的锁,相互之间没有任何联系。例如,悲观锁可以是行锁,也可以是表锁。

按锁模式划分

记录锁(Record Lock)

  • 定义:记录锁是锁定单个数据行的锁类型。它是最精细的锁类型,作用于表中单独的行。使用记录锁时,MySQL 会锁定某一行数据,防止其他事务修改该行数据。

  • 适用场景:通常用于 行级锁,记录锁是实现行锁的重要组成部分。例如在执行 SELECT FOR UPDATE 时,当满足查询条件的行被返回时,会加上记录锁。记录锁是 InnoDB 行级锁的一部分,是 InnoDB 实现行级锁的基础。

  • 特点:当一个事务修改某一行时,其他事务无法修改这行数据,从而避免了并发修改同一行数据时的冲突。

  • 示例
    假设有一个名为 users 的表,其中包含字段 idname

    -- 假设事务 A 执行了以下查询并加上了记录锁
    START TRANSACTION;
    SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- 锁定 id=1 的记录
    -- 其他事务不能修改 id=1 的记录,直到事务 A 提交或回滚
    

    在这个例子中,事务 A 对 id=1 的记录加了 记录锁,直到事务 A 提交或回滚,其他事务无法修改该记录。

    记录锁是行级锁的一种具体实现形式,行级锁是一个更宽泛的概念,可以通过记录锁、间隙锁等多种锁类型实现。


Gap 锁(Gap Lock)

  • 定义:Gap 锁锁住的不是具体的数据行,而是数据行之间的间隙。它阻止其他事务在该间隙中插入新行,防止幻读 (Phantom Read)

  • 适用场景:常用于范围查询时,防止其他事务在当前事务的查询范围内插入新记录。Gap 锁不仅适用于 SELECT ... LOCK IN SHARE MODE,还可能在 DELETEINSERT(唯一索引冲突时)等情况下被触发。

  • 特点

    • Gap 锁本身不会锁住某一行,而是锁住了索引中的两个数据之间的“空隙”,防止其他事务在这个空隙插入新数据。
    • Next-Key 锁 中也会涉及到 Gap 锁。
    • Gap 锁基于索引,如果列没有索引,可能会退化为表锁。
  • 示例
    假设有一个 users 表,其中按 id 列建立了索引。

    START TRANSACTION;
    SELECT * FROM users WHERE id > 10 AND id < 20 LOCK IN SHARE MODE;
    
    • LOCK IN SHARE MODE 只会加 Gap 锁,不会加 Next-Key 锁
    • 由于 id > 10 AND id < 20,查询不会锁住 id=10id=20 本身,只会在 (10,20) 之间加 Gap 锁
    • 这个锁的作用是防止 (10,20) 范围内的插入,但不影响已有数据的修改或删除。

Next-Key 锁(Next-Key Lock)

  • 定义:Next-Key 锁是 记录锁Gap 锁 的结合,它锁定某一行以及该行与下一行之间的间隙。它防止其他事务对当前记录该记录的范围进行修改或插入,防止幻读 (Phantom Read) 和对已存在记录的修改

  • 适用场景:Next-Key 锁通常用于范围查询时,尤其是在对某一范围内的数据进行查询并进行加锁的场景。它既锁住了当前行,又锁住了该行后面的间隙,以确保查询结果的稳定性。

  • 特点

    • Next-Key 锁比单独的记录锁更加复杂,它既能防止数据行的并发修改,也防止其他事务在查询的范围内插入新的数据。
    • Next-Key 锁仅在 InnoDB 的 REPEATABLE READ 级别下默认启用。在 READ COMMITTED 级别下,InnoDB 会退化为行锁(Record Lock)+ 间隙锁(Gap Lock),不使用 Next-Key 锁。
    • Next-Key 锁基于索引,如果查询列没有索引,可能会锁住全表。
  • 示例
    假设 users 表按 id 列建立了索引,且表中已有 {5, 10, 15, 20, 25}

    START TRANSACTION;
    SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;
    
    • FOR UPDATE 会加 Next-Key 锁,即锁住查询范围内的所有记录 + 记录之间的间隙。
    • BETWEEN 10 AND 20 会锁住 id=10id=20 本身,同时锁住 id=10 之前和 id=20 之后的间隙。InnoDB 的索引组织表在 FOR UPDATE 下会锁住:
      1. (5,10]锁住 id=10 及其前一个间隙
      2. (10,15]锁住 id=15(10,15) 之间的间隙
      3. (15,20]锁住 id=20(15,20) 之间的间隙
      4. (20,25]锁住 id=20 之后的间隙
    • 这个锁的作用是防止 (5,25) 范围内的插入,同时也不能对已有数据的修改或删除。

仅加 Gap 锁(间隙锁)时,不会锁定索引边界的前后间隙。 加 Next-Key
锁的情况下,索引边界的前后间隙可能会被锁定,具体取决于查询范围。

Record 锁|Gap 锁|Next-Key的区别

锁类型 锁定范围 作用
Record 锁 仅锁住某一条具体的记录 阻止该记录的更新或删除,但不影响其他记录
Gap 锁 只锁住索引记录之间的间隙 防止插入新记录,但不影响已有记录的修改/删除
Next-Key 锁 锁住索引记录本身 + 该记录后的间隙 防止插入新记录 + 阻止修改/删除已有记录

意向锁(Intention Lock)

  • 定义意向锁是表级锁的一种特殊类型,关键作用是为更细粒度的锁提供一个标识,用于标示当前事务计划在更细粒度的资源(如行、页等)上加锁。这种锁并不会直接阻塞对具体行或页的访问,而是告诉数据库系统当前事务计划在某些数据上加锁,从而避免其他事务在相同的粒度上加锁,避免冲突。

  • 适用场景:意向锁主要用于行锁与表锁的协调。它主要用来优化锁的冲突检测机制。具体来说,当一个事务想在行级别加锁时,必须先获取意向锁。如果有意向锁,数据库会知道该事务正在尝试对某些行加锁,从而避免了与其他事务加锁的冲突。

  • 意向锁的种类

    • 意向共享锁(IS 锁):表示事务意图在某些行上加共享锁
    • 意向排他锁(IX 锁):表示事务意图在某些行上加排他锁
  • 示例
    假设有一个 users 表,事务 A 计划在某些行上加行锁。

    -- 假设事务 A 执行了以下操作
    START TRANSACTION;
    -- 首先加上意向排他锁
    SELECT * FROM users WHERE id=1 FOR UPDATE; -- 事务 A 计划对 id=1 行加排他锁
    -- 由于已经有意向锁存在,InnoDB 会允许事务 B 在表上加锁
    -- 但是,事务 B 不能在 id=1 上加行锁,避免冲突
    

    在这个例子中,事务 A 对 id=1 的行加上了 意向排他锁(IX 锁),表示事务 A 将要在该行加上排他锁,其他事务无法对该行加锁或修改。

插入意向锁(Insert Intention Lock)

  • 定义:插入意向锁是一种特殊的意向锁,表示事务打算在某个间隙插入一行数据插入意向锁通常与 Gap 锁配合使用,以便在插入新行之前对相应的间隙进行锁定

  • 适用场景:在执行 INSERT 操作时,如果事务打算插入的行位于某个索引的间隙中,MySQL 会加上插入意向锁。这种锁不会实际锁定某行数据,而是标示事务意图插入数据。

  • 特点:插入意向锁是为了保证插入操作的顺序和一致性,避免其他事务同时在相同的间隙中插入数据。

  • 示例
    假设有一个 users 表,其中按 id 列建立了索引。

    -- 假设事务 A 执行了以下操作
    START TRANSACTION;
    INSERT INTO users (id, name) VALUES (10, 'Alice');  -- 事务 A 准备在 id=10 的位置插入数据
    -- 这会在 id=10 的位置加上插入意向锁,表示事务 A 会在该位置插入数据
    

    在这个例子中,事务 A 打算在 id=10 的位置插入数据,MySQL 会为该位置加上 插入意向锁,这与 Gap 锁 一起确保事务 A 在插入数据之前不会有其他事务在该位置插入数据。

意向锁和插入意向锁区别:

  • 意向锁 是为了协调 行级锁表级锁 之间的关系,标示当前事务将要在某些行上加锁。
  • 插入意向锁 是用来标示事务计划在某个空隙插入数据,并防止其他事务在该位置插入数据。它是与并发插入操作相关的一种机制。
    • Gap 锁 用于防止幻读,确保事务执行过程中不会有新数据插入到指定索引间隙中,因此主动阻塞其他事务的插入
    • 插入意向锁 用于防止插入冲突,表示事务意图在某个间隙插入数据,它本身不会阻塞其他事务,但如果多个事务同时插入相同间隙,则可能冲突

总结

  • 记录锁:最细粒度的锁,锁定单一数据行
  • Gap 锁锁住数据行之间的“间隙”,防止其他事务在这个空隙插入数据。
  • Next-Key 锁结合了记录锁和 Gap 锁,锁定数据行及其后面的间隙,常用于范围查询。
  • 意向锁:用于标示事务意图加锁的粒度(行级或页级),帮助协调不同级别的锁。
  • 插入意向锁:表示事务准备在某个间隙插入数据,通常与 Gap 锁一起使用。

这些锁模式在 MySQL 的 InnoDB 存储引擎 中用于实现精细化的锁定机制,确保数据一致性和并发性。