MySQL 的 InnoDB 存储引擎中的 排他锁 详解

news2024/10/10 9:14:08

        在 MySQL 的 InnoDB 存储引擎中,排他锁(Exclusive Lock , 也常写作X锁)是控制并发访问的核心机制之一。排他锁允许事务对记录进行修改操作时,防止其他事务同时读取或修改这些记录。为了深入了解其工作机制,我们需要从底层原理、锁管理流程以及源码实现等多个方面进行详细的解释。

1. 排他锁的概念与特点

1.1 排他锁的定义

        X 锁(排他锁)是一种严密的锁类型,当一个事务对一条记录(或多个记录)加上排他锁时,其他事务不能再对该记录进行任何操作,直到锁被释放为止。

这意味着:

  • 只能有一个事务持有排他锁 :其他事务无法获取同样的记录上的 X 锁。
  • 阻止其他事务获取 S 锁:防止共享读,其他事务无法读取被加 X 锁的记录。
  • 事务级隔离:事务持有 排他锁的记录只对持有该锁的事务可见,其他事务不能访问。

1.2 排他锁的适用场景

排他锁主要在执行修改操作时使用,例如:

  • INSERT:插入新记录时,必须对新插入的记录加 X 锁。
  • UPDATE:修改现有记录时,必须对被修改的记录加 X 锁。
  • DELETE:删除记录时,必须对被删除的记录加 X 锁。
  • SELECT ... FOR UPDATE:显示加锁查询时,会对所查询的记录加上 X 锁,防止其他事务同时修改这些记录。

1.3 两阶段锁协议

InnoDB 中的排他锁遵循两阶段锁协议(Two-Phase Locking Protocol,2PL):

  • 第一阶段:事务执行时,系统会动态地获取所需的 X 锁。
  • 第二阶段:在事务结束时(提交或回滚),系统会释放事务所持有的所有 X 锁。这意味着锁会一直持有到事务结束,以确保操作的原子性和一致性。

1.4 排他锁为 行锁 和 表锁的场景

        在 MySQL 的 InnoDB 存储引擎中,排他锁(X 锁)可以是 行锁 也可以是 表锁,这取决于查询语句的执行方式和查询所涉及的数据结构。为了全面理解排他锁在何种情况下是行锁或表锁,需要从以下几个方面进行详细解释,包括 InnoDB 的锁实现原理、索引机制、SQL 执行场景、隔离级别,以及 底层源码实现

1.4.1. 行锁 vs 表锁的概念
  • 行锁(Row Lock):只锁定特定的行,也就是某条或某几条记录。行锁粒度更小,能够提高并发性。InnoDB 通过索引来实现行级锁。
  • 表锁(Table Lock):锁定整个表,阻止任何其他事务对该表进行操作。表锁粒度较大,通常并发性能较低。

行锁是 InnoDB 引擎的默认行为,但在某些情况下,InnoDB 可能会退化为 表锁。以下从原理层面详细讨论不同场景下 X 锁的行为。


1.4.2. 基于索引的行锁实现

        InnoDB 中的行锁是通过 索引 来实现的。只有当 SQL 查询使用了索引(无论是聚簇索引还是辅助索引),InnoDB 才能精准定位到具体的行,并对这些行加锁。

  • 聚簇索引(Clustered Index):InnoDB 中表的物理存储是基于聚簇索引的。每条记录都通过主键保存在聚簇索引中,行锁实际上锁定的是该记录在聚簇索引上的位置。
  • 辅助索引(Secondary Index):如果查询使用了辅助索引,InnoDB 会首先锁定辅助索引中的条目,然后通过辅助索引定位到对应的聚簇索引条目,进一步加锁。
示例:使用主键进行查询
UPDATE users SET age = 30 WHERE id = 1;

        在此查询中,id 是主键(聚簇索引)。InnoDB 只会对 id = 1 的记录加 X 锁,属于行锁。

示例:使用辅助索引进行查询
UPDATE users SET age = 30 WHERE email = 'example@example.com';

        在此查询中,email 是辅助索引。InnoDB 会首先锁定 email 索引,然后通过它找到对应的聚簇索引条目并加锁,仍然是行锁。


1.4.3. 在以下情况下,X 锁会退化为表锁
1.4.3.1 查询未使用索引

        如果查询没有使用索引(即全表扫描),InnoDB 无法精准定位到某条记录,X 锁会退化为 表锁。这是因为 InnoDB 必须扫描整个表来寻找满足条件的记录,因此只能对整个表加锁。

示例:不使用索引的更新语句
UPDATE users SET age = 30 WHERE name = 'John Doe';

        假设 name 字段没有索引,InnoDB 需要扫描整个表寻找 name = 'John Doe' 的记录,X 锁会锁定整个表。

1.4.3.2 没有主键或唯一索引

        如果表中没有主键或者没有唯一索引,InnoDB 不能精确定位单条记录,锁也会退化为 表锁。例如,如果表没有主键,InnoDB 在内部使用一个隐式的主键 _rowid,此时可能导致锁的粒度变大。

示例:没有主键的表
UPDATE users SET age = 30 WHERE name = 'John Doe';

        如果 users 表没有主键且 name 字段没有索引,那么 InnoDB 无法精准定位到具体的行,因此可能会将整个表加锁。

1.4.3.3 使用 LOCK IN SHARE MODE 或 FOR UPDATE 时全表扫描

        在使用类似 SELECT ... FOR UPDATE 或 LOCK IN SHARE MODE 的查询时,如果没有索引或者索引无法有效利用,InnoDB 可能也会将锁扩展为表锁。此时,X 锁会锁定整个表的记录,防止其他事务插入或修改。

示例:无索引的锁定查询
SELECT * FROM users WHERE age = 30 FOR UPDATE;

如果 age 字段上没有索引,InnoDB 会执行全表扫描,并可能对整张表加锁。


1.4.4. 隔离级别对锁行为的影响

        不同的事务隔离级别也会影响 X 锁的粒度和范围,尤其是在涉及到幻读和插入操作时。

1.4.4.1 可重复读(REPEATABLE READ)与 Next-Key 锁

        在 可重复读 隔离级别下,InnoDB 使用 Next-Key 锁 来防止幻读。Next-Key 锁是行锁和 Gap 锁 的结合,它不仅锁定当前的记录,还会锁定索引项之间的“间隙”,以防止其他事务在这些间隙中插入新记录。

        由于 Next-Key 锁的存在,即使是行锁,也会因为 Gap 锁而锁定索引间的范围,这在某种程度上可能导致锁定的范围比预期要大。

示例:可重复读中的 Next-Key 锁
SELECT * FROM users WHERE age > 30 FOR UPDATE;

        在可重复读隔离级别下,InnoDB 会对满足条件的记录加上 X 锁,同时会锁定这些记录之间的“间隙”,防止其他事务在这些范围内插入新的记录。虽然是行锁,但锁定的范围变得更大。

1.4.4.2 读已提交(READ COMMITTED)

        在 读已提交 隔离级别下,InnoDB 不使用 Gap 锁,因此 X 锁只会锁定具体的行,而不会锁定“间隙”。这种隔离级别下,锁的范围较小,并且只在实际修改的行上加锁。


1.4.5. 源码层面的实现
1.4.5.1 InnoDB 行锁的管理结构

        InnoDB 中的行锁主要由 lock_t 结构体管理,该结构体中记录了锁的类型、锁定的对象(行或表)、事务等信息。

struct lock_t {
    trx_t*      trx;            // 持有锁的事务
    ulint       type_mode;      // 锁的类型 (X 锁或 S 锁)
    dict_index_t* index;        // 被锁定的索引
    rec_t*      rec;            // 锁定的记录
    ...
};

        其中 type_mode 标识锁的类型,包括 X 锁、S 锁等。而 index 和 rec 字段则分别表示锁定的索引和具体的记录。如果 index 为 NULL 或锁定整个表时,InnoDB 会直接将锁加在表级别上。

1.4.5.2 锁的申请与表锁的退化

        当事务试图获取锁时,InnoDB 会根据查询条件决定是加行锁还是表锁。锁的申请逻辑在 lock_rec_lock() 函数中实现:

void lock_rec_lock(
    ulint      lock_type,       // 锁类型 (S 锁或 X 锁)
    rec_t*     rec,             // 被锁的记录
    buf_block_t* block,         // 被锁记录所在的页
    dict_index_t* index,        // 被锁定的索引
    trx_t*     trx)             // 持有锁的事务
{
    if (index == NULL || no_index_used) {
        // 如果没有索引或没有使用索引,则锁定整个表
        lock_table(trx->table, lock_type);
    } else {
        // 否则锁定特定的行
        lock_row(trx, rec, index, lock_type);
    }
}
  • 索引为空或未使用时,InnoDB 会调用 lock_table() 对整个表加锁,锁的粒度扩大。
  • 使用了索引时,InnoDB 会调用 lock_row() 对具体的行加锁。
1.4.5.3 全表扫描的判断

        no_index_used 是判断查询是否使用了索引的标志。如果查询使用了索引(包括主键和辅助索引),InnoDB 能够将锁精准加在行上。如果没有使用索引(比如没有索引的字段或者 EXPLAIN 显示全表扫描),InnoDB 则会将锁扩展到整个表。


1.4.6. 总结:排他锁何时是行锁,何时是表锁
  • 行锁:当 SQL 查询使用了索引(无论是主键或辅助索引),InnoDB 可以精准定位到具体的记录,加上行锁。
  • 表锁:在以下情况下,InnoDB 会将行锁退化为表锁:
    • 查询没有使用索引,导致全表扫描。
    • 表中没有主键或唯一索引,无法精准定位记录。
    • 在 SELECT ... FOR UPDATE 或 LOCK IN SHARE MODE 查询中,未使用索引时。
    • 某些隔离级别(如可重复读)下,由于 Next-Key 锁可能会锁定更大范围。

        InnoDB 通过索引来管理行锁和表锁,并根据 SQL 查询的具体情况动态调整锁的粒度,从而在性能和数据一致性之间做出平衡。

2. 排他锁的底层原理

        在底层,InnoDB 的 X 锁是基于行级别实现的。与表锁相比,行级锁的粒度更小,因此能够提供更高的并发性。排他锁的底层原理主要依赖 InnoDB 的索引机制、两阶段锁协议以及锁等待队列来保证数据的原子性和一致性。

2.1 基于索引的行级锁

        InnoDB 的 X 锁是基于索引项进行加锁的,而不是直接加在记录上。这是 InnoDB 高效实现行锁的关键。

  • 聚簇索引 (Clustered Index):InnoDB 表的物理存储是基于聚簇索引的,每一条记录实际上是聚簇索引的一部分。因此,当事务需要加排他锁时,InnoDB 会锁定该记录的聚簇索引项。
  • 辅助索引 (Secondary Index):如果查询使用了辅助索引,那么 InnoDB 会先在辅助索引上加锁,然后通过辅助索引回表找到相应的聚簇索引项,并对其加锁。

        当事务对记录加 X 锁时,InnoDB 实际上锁定的是与该记录相关的索引项。这意味着如果查询未使用索引,InnoDB 会锁住整个表,因为没有精确的索引定位。

2.2 Next-Key 锁与 排他锁 的结合

        InnoDB 的 X 锁与 Next-Key 锁 机制紧密结合,尤其是在 可重复读 隔离级别下,InnoDB 使用 Next-Key 锁来防止幻读。Next-Key 锁是行锁与 GAP 锁的结合:

  • Next-Key 锁:不仅锁定当前的索引项(即记录本身),还锁定该记录与下一条记录之间的“间隙”,防止其他事务在该间隙中插入新记录,从而避免幻读问题。
  • GAP 锁:仅锁定索引项之间的“间隙”,不会锁定现有的记录。GAP 锁的目的是防止插入操作,确保查询范围内的数据不会被其他事务改变。

        因此,在某些场景下,当事务对一条记录加 排他锁 时,InnoDB 可能会同时锁定该记录的索引项及其相邻的“间隙”,以确保数据一致性。

2.3 锁的生命周期

        InnoDB 的 X 锁遵循两阶段锁协议,这意味着锁的生命周期与事务的生命周期保持一致。锁的生命周期可以分为以下几个阶段:

  1. 锁的申请:当事务执行修改操作时,InnoDB 会为受影响的记录申请 排他锁。
  2. 锁的持有:一旦获取 X 锁,事务将一直持有该锁,直到事务结束(提交或回滚)。
  3. 锁的释放:当事务提交或回滚时,InnoDB 会释放该事务所持有的所有锁,包括 排他锁。

3. 排他锁的实现细节(源码层面)

        InnoDB 的 X 锁实现分布在多个源码文件中,包括 lock0lock.ccrow0mysql.cc 和 trx0trx.cc。我们从源码的角度深入分析排他锁的申请、等待和释放机制。

3.1 锁结构体 lock_t

        在 InnoDB 中,锁的管理通过 lock_t 结构体来实现。这个结构体包含了锁的类型、所锁定的索引项、持有该锁的事务等信息。我们先来看 lock_t 结构体中的相关字段:

struct lock_t {
    trx_t*      trx;            // 持有锁的事务
    ulint       type_mode;      // 锁的类型 (X 锁或 S 锁)
    dict_index_t* index;        // 被锁定的索引
    rec_t*      rec;            // 锁定的记录
    ulint       n_bits;         // 锁定的位
    lock_t*     next;           // 指向下一个锁
    ...
};

        在 lock_t 中,type_mode 表示锁的类型,可以是 排他锁 X 锁 或 共享锁也称为S 锁,trx 是持有该锁的事务,rec 是被锁定的记录,index 表示被锁定的索引。

3.2 锁的申请

        锁的申请逻辑位于 lock_rec_lock() 函数中。当事务需要对某个记录加锁时,InnoDB 会调用此函数:

void lock_rec_lock(
    ulint      lock_type,       // 锁类型 (S 锁或 X 锁)
    rec_t*     rec,             // 被锁的记录
    buf_block_t* block,         // 被锁记录所在的页
    dict_index_t* index,        // 被锁定的索引
    trx_t*     trx)             // 持有锁的事务
{
    // 创建并初始化锁对象
    lock_t* lock = lock_create();
    lock->trx = trx;
    lock->type_mode = lock_type; // 设置为 X 锁
    lock->rec = rec;
    lock->index = index;

    // 将锁对象添加到事务持有的锁列表中
    lock_add_to_trx(trx, lock);

    // 试图获取锁,如果锁被占用,则进入等待队列
    lock_attempt(trx, lock);
}

这个函数实现了锁的申请逻辑:

  1. 创建锁对象:为锁申请内存,并初始化它的相关字段。
  2. 初始化锁属性:设置锁类型为排他锁 X 锁,并将锁关联到具体的事务、记录和索引。
  3. 锁的管理:将锁对象添加到事务的锁链表中,并通过 lock_attempt() 函数尝试获取锁。如果当前锁被其他事务持有,当前事务将进入等待状态。

3.3 锁的等待

        当事务试图获取一条记录的排他锁 X 锁,但该记录已经被其他事务锁定时,InnoDB 会让事务进入锁等待状态。这一部分逻辑在 lock_wait() 函数中实现:

void lock_wait(trx_t* trx, lock_t* lock)
{
    trx->state = TRX_STATE_WAIT;  // 将事务标记为等待状态
    trx->lock_wait = lock;        // 设置事务等待的锁

    // 将事务加入锁的等待队列中
    lock_add_to_wait_queue(lock, trx);

    // 等待事件的通知,可能是锁可用或超时
    trx_wait_for_lock(trx);
}
  • trx_wait_for_lock() 是一个等待函数,事务在这里会等待锁的释放。如果锁被释放或检测到死锁,事务会被唤醒,否则它会继续等待直到超时。

3.4 锁的释放

        当事务提交或回滚时,InnoDB 会调用 lock_release() 函数释放事务持有的所有锁,包括排他锁 :

void lock_release(trx_t* trx)
{
    lock_t* lock = trx->lock_list;

    // 逐个释放事务持有的所有锁
    while (lock != NULL) {
        lock_remove(lock);  // 从锁管理器中移除该锁
        lock = lock->next;
    }

    // 清空事务的锁链表
    trx->lock_list = NULL;
}

当事务结束时,InnoDB 会遍历事务的锁链表,逐个释放所有持有的锁。

4. 排他锁的优化与性能考虑

       排他锁在保证数据一致性的同时,也对系统性能有较大影响,因此 InnoDB 进行了多方面的优化:

4.1 行级锁粒度

        InnoDB 采用行级锁而非表级锁,极大地提高了并发性能。通过精细粒度的行级锁定,InnoDB 可以在处理大量事务时仍保持高效的并发控制。

4.2 索引优化

        InnoDB 通过索引实现行锁。加锁时,InnoDB 首先查找记录所在的索引位置,然后基于索引项加锁,这样可以避免对整个表加锁,进一步提升并发性能。

4.3 死锁检测机制

        InnoDB 内置了死锁检测机制,当系统检测到死锁时,会主动回滚优先级较低的事务,解除死锁。死锁检测的实现通过等待图来跟踪事务间的依赖关系。

5. 总结

        InnoDB 的排他锁在实现上基于行级锁,结合了索引机制和两阶段锁协议,能够高效地控制事务的并发访问。X 锁的源码实现分布在多个模块中,涵盖了锁的申请、等待、释放以及死锁检测等机制。通过对锁粒度的精细化控制和对死锁的智能处理,InnoDB 既保证了数据的一致性,又最大限度提升了并发性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2201618.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java类和对象_成员变量方法修饰符局部变量this关键字-cnblog

java类和对象 成员变量 权限修饰符 变量类型 变量名; 成员变量可以是任意类型,整个类是成员变量的作用范围 成员变量 成员方法 权限修饰符 返回值类型 方法名() 成员方法可以有参数,也可以有返回值,用return声明 权限修饰符 private 只能在本类…

IDEA必装的插件:Spring Boot Helper的使用与功能特点

在IntelliJ IDEA中,Spring Boot Helper插件是一个非常实用的工具,可以帮助我们更快速地创建和管理Spring Boot项目。以下是Spring Boot Helper插件的详细介绍和使用方法: 激活码地址: 点击获取 一、安装Spring Boot Helper插件 1 打开Intell…

如何设计三极管放大电路?

设计放大电路 分压式串联负反馈放大电路 可以看下面这个视频 , 讲得更加详细 366-单管放大电路偏置电阻的计算,看完自己也会设计一个_哔哩哔哩_bilibili 计算流过电阻Rb2的电流过程中,工程当中常发现Rb2上的电流是Ib的5倍 , 因此由基尔霍夫电流定律也能知道流过R…

Java的基础概念和常识(二)

什么是字节码?采用字节码的好处是什么? 字节码(Byte-code)是一种中间形式的代码,是源代码编译后生成的一种低级表示,通常是在编译阶段生成的。在 Java 中,JVM 可以理解的代码就叫做字节码&…

K8s(学习笔记)

swap分区是什么呀? 什么是ipvs呀? yaml是什么呀??? p20看不下去了!!!

ansible 流程控制

目录 1.流程控制 2.handlers触发器 2.1使用handlers案例 3.when 判断 3.1 案例1 用于给task设置条件 满足或者不满足运行对应模块 3.2 案例2 如果系统是centos则安装sl,cowsay 如果是unbantu则安装cmatrix 4.循环 4.1案例 1.流程控制 hand…

飞腾CPU技术发展分析

飞腾CPU剖析 CPU:信创根基,国之重器 国产CPU市场呈现三大领军阵营:x86、ARM以及其他创新架构。鲲鹏与飞腾在ARM阵营中引领风潮,依托ARM技术授权研发高性能处理器;海光与兆芯则以x86架构为基石,深入挖掘其潜…

图论day56|广度优先搜索理论基础 、bfs与dfs的对比(思维导图)、 99.岛屿数量(卡码网)、100.岛屿的最大面积(卡码网)

图论day56|广度优先搜索理论基础 、bfs与dfs的对比(思维导图)、 99.岛屿数量(卡码网)、100.岛屿的最大面积(卡码网)) 广度优先搜索理论基础bfs与dfs的对比(思维导图)&…

Spring Boot Starter Parent介绍

引言 spring-boot-starter-parent 是一个特殊的项目,为基于 Spring Boot 的应用程序提供默认配置和默认依赖。 在本 Spring Boot 教程中,我们将深入了解所有 Spring Boot 项目内部使用的 spring-boot-starter-parent 依赖项。我们将探讨此依赖项所提供…

基于jmeter+perfmon的稳定性测试记录

1. 引子 最近承接了项目中一些性能测试的任务,因此决定记录一下,将测试的过程和一些心得收录下来。 说起来性能测试算是软件测试行业内,有些特殊的部分。这部分的测试活动,与传统的测试任务差别是比较大的,也比较依赖…

用Python实现运筹学——Day 12: 线性规划在物流优化中的应用

一、学习内容 线性规划在物流优化中可以用于解决诸如配送路径优化、货物运输调度等问题。配送中心的路径优化问题本质上是寻找一条最优路径,在满足需求点的需求条件下,最小化配送的总运输成本或时间。常见的物流优化问题包括: 配送中心的货…

集师知识付费小程序:打造培训机构在线教育的金字招牌 集师知识付费系统 集师知识付费小程序 集师知识服务系统 集师线上培训系统 集师线上卖课小程序

在数字化浪潮的推动下,在线教育已成为教育领域的热门话题。而在众多在线教育平台中,集师知识付费小程序凭借其独特的定位和创新的模式,成功为培训机构打造了一张闪亮的在线教育金字招牌。 集师知识付费小程序,是一个集课程展示、…

Python 如何使用 Matplotlib 和 Seaborn 可视化数据

Python 如何使用 Matplotlib 和 Seaborn 可视化数据 一、简介 数据可视化是数据分析过程中非常重要的步骤。通过可视化,复杂的数据变得更直观,数据中的模式、趋势和异常可以更容易被识别。Python 提供了多个强大的库来进行数据可视化,其中最…

ChatGPT:引领人工智能新潮流!

一、ChatGPT 是什么? 1. ChatGPT 的强大功能和广泛应用。 ChatGPT 作为一款先进的 AI 语言模型,拥有众多强大功能。它可以进行文本生成、文本分类、情感分析、机器翻译等多种自然语言处理任务。同时,ChatGPT 还能进行对话式交互,…

python之详解集合

一种无序且不重复的数据容器,集合用大括号{}表示。 1、集合的查找访问 集合是不能通过 集合名[index] 这种方式访问的,其作用在于快速读取,而不是针对某个元素。 但,可将集合转为列表,再由列表访问元素。不过&#…

Laravel Filament 如何配置多语言支持

演示 一、安装拓展包outerweb/filament-translatable-fields composer require outerweb/filament-translatable-fields配置模型 该套件包含一个名为 HasTranslations 的特性,用于使 Eloquent 模型具备多语言功能。翻译值以 JSON 格式存储,并不需要额外…

叙说 OSI 七层网络模型 | 你在第几层

引言 开放系统互联(OSI,Open Systems Interconnection)模型,这一国际标准化组织(ISO)提出的理论框架,是计算机网络通信领域内不可或缺的基础工具。如同语法和句法对于构建和解析语言的重要性一…

Python对PDF文件页面的旋转和切割

Python对PDF文件页面的旋转和切割 利用Python的.rotate()方法和.mediabox属性对PDF页面进行旋转和切割,最终生成一个PDF。下面结合案例进行说明,本示例中的名为split_and_rotate.pdf文件在practice_files文件夹中, 示例(1&#…

ShardingSphere分库分表产品介绍

目录 一、ShardingSphere分库分表产品介绍 二、客户端分库分表与服务端分库分表 1、ShardingJDBC客户端分库分表 2、ShardingProxy服务端分库分表 3、ShardingSphere混合部署架构 三、分库分表,能不分就不分! 1、为什么要分库分表? 2、…

基于SpringBoot点餐系统【附源码】

基于SpringBoot点餐系统 效果如下: 系统首页界面 用户注册界面 美食信息页面 促销活动页面 管理员登录主页面 系统管理界面 订单管理界面 研究背景 随着互联网的迅猛发展和人们生活节奏的加快,传统的点餐方式已经无法满足现代消费者对快速、便捷服务的…