Mysql进行操作时锁的具体行为示例详解

 更新时间:2025年09月01日 11:48:21   作者:@Jackasher  
MySQL锁的使用需要根据具体的应用场景来选择,例如,如果应用以查询为主,少量更新,表锁可能是更合适的选择,这篇文章主要介绍了Mysql进行操作时锁的具体行为,需要的朋友可以参考下

场景一:单个事务更新一条存在的数据

假设有表 user (id PK, name, age),数据:[id=1, name='Alice', age=25]

你的 SQL: UPDATE user SET age = 26 WHERE id = 1;

底层动作:

  1. 事务 A (主动方) 发起更新请求。
  2. Lock Manager 介入:
    • 查找 id=1 的索引记录: Lock Manager 根据 id=1(主键)找到对应的主键索引树叶子节点中的那条索引记录
    • 检查这条索引记录上的锁状态: 发现 id=1 这条索引记录此刻是无锁状态
    • 在索引记录上“粘贴”一个锁标记: Lock Manager 会在 id=1 这条具体的索引记录上,打上一个**“X 锁 (Exclusive Lock)”**的标记。
      • 这个标记就是一条内部的内存数据结构,记录着:“id=1 这条索引记录,现在被事务 AX 模式锁住,并且引用计数+1”。
    • 在对应的表头部“粘贴”一个意向锁标记: 同时,Lock Manager 还会顺手在 user 表的内部元数据结构上,打上一个**“IX 锁 (Intention Exclusive Lock)”**的标记。
      • 这个标记是:“user 表的某个地方,有事务正在尝试或已经持有 X 型行锁。”
  3. 事务 A 执行更新: 事务 A 获得锁,可以安全地修改 id=1 这条索引记录的 age 值。
  4. 事务 A 提交/回滚: 事务 A 结束时,Lock Manager 会根据之前的记录,移除 id=1 上的 X 锁标记,同时检查 user 表是否还有其他 IX 锁持有者,如果没有,也移除 user 表上的 IX 锁标记。

场景二:事务 A 更新数据,事务 B 随后读取同一条数据

数据:[id=1, name='Alice', age=25]

你的 SQL (事务 A): UPDATE user SET name = 'Alicia' WHERE id = 1;
你的 SQL (事务 B): SELECT * FROM user WHERE id = 1;

底层动作:

  1. 事务 A 获得 id=1 的 X 锁 (如场景一所述)。
    • id=1 索引记录上:X 锁,持有者 事务 A
    • user 表元数据上:IX 锁,持有者 事务 A
  2. 事务 B 发起读取请求。
  3. Lock Manager 介入:
    • 查找 id=1 的索引记录。
    • 检查这条索引记录上的锁状态: 发现 id=1 这条索引记录上有一个 X 锁,并且持有者是**事务 A**。
    • 判断冲突: 事务 B 尝试读取,但 事务 A 持有的是 X 锁 (排他锁)。X 锁会阻止所有其他事务的读写。
    • 不授予锁,并等待: Lock Manager 不授予事务 B 任何锁,而是把事务 B 放入一个等待队列,同时启动事务 B 的等待计时器
      • “事务 B 正在等待 id=1 这条索引记录上的锁。”
  4. 事务 A 提交: 释放 id=1 上的 X 锁,也释放 user 表的 IX 锁。
  5. Lock Manager 通知: id=1 上的锁被移除,Lock Manager 发现等待队列中有事务 B。
  6. 事务 B 被唤醒: 事务 B 获得执行权限,读取 id=1 这条记录的新数据(比如 name='Alicia')。

场景三:间隙锁 (Gap Lock) 的具体行为 (防止幻读)

数据:user (id PK),记录只有 [id=10], [id=30] (没有 id=20)

你的 SQL (事务 A): SELECT * FROM user WHERE id BETWEEN 15 AND 25 FOR UPDATE; (注意这是范围查询且带 FOR UPDATE)

底层动作:

  1. 事务 A 发起请求。
  2. Lock Manager 介入:
    • 查找索引: Lock Manager 根据条件 id BETWEEN 15 AND 25,在主键索引树上进行查找。
    • 发现没有符合条件的记录 (这是一个空区间/间隙)。
    • 在“间隙”上打锁标记: 尽管没有找到具体的数据行,Lock Manager 依然会在索引结构中,针对 id=10id=30 之间的**“范围”(即 (10, 30) 这个间隙),打上一个“间隙锁 (Gap Lock)”**的标记。
      • 这个标记就是:“索引中 id 值在 1030 之间的空地,现在被事务 A 锁住,禁止插入新数据。”
      • 通常,这个间隙锁是 X 类型的,因为它阻止其他事务在这个间隙中进行 INSERT 操作。
    • 在对应的表头部“粘贴”一个意向锁标记 (IX)
  3. 事务 B 尝试插入数据: INSERT INTO user (id) VALUES (20);
  4. Lock Manager 介入:
    • 判断插入位置: 发现 id=20 应该插入到 id=10id=30 之间。
    • 检查该间隙的锁状态: 发现这个 (10, 30) 的间隙上有一个间隙锁,持有者是**事务 A**。
    • 不授予锁,并等待: Lock Manager 不授予事务 B 任何锁,将事务 B 放入等待队列
  5. 事务 A 提交: 释放 (10, 30) 上的间隙锁,以及 user 表的 IX 锁。
  6. Lock Manager 通知: 间隙锁被移除,事务 B 被唤醒,可以成功插入 id=20

这些“锁标记”本质上都是数据库系统内部维护的内存数据结构,它们记录着:哪个事务在哪个资源(索引记录或间隙或表)上持有哪种类型的锁。当其他事务请求时,Lock Manager 就去查这些标记,进行兼容性判断,决定是立即授予、等待还是死锁。
内存中的锁管理数据结构,它们并不是简单的“标记”那么纯粹,而是一系列精巧组织的对象。

要理解这个,我们得从 Lock Manager (锁管理器) 的核心工作开始。Lock Manager 维护着一张“活的地图”,这张地图记录了哪些资源被锁了被谁锁了锁的类型是什么,以及谁在等待这些锁

最底层数据结构模拟:Lock Manager 的“活地图”

想象 Lock Manager 就好比一个大型交通控制中心,它有几块巨大的显示屏和一些重要的记录本。

核心数据结构 1:锁哈希表 (Lock Hash Table) 或 锁链表 (Lock List)

这是所有正在活动的锁及其等待者的“索引”。

  • 目的快速查找某个资源(比如某行数据)上是否有锁,以及有哪些事务在等待。
  • 实现:通常是一个哈希表std::unordered_map 类似),键是资源标识符,值是一个链表或队列,里面包含了所有作用在该资源上的锁对象和等待者。因为哈希表的查找速度快,能迅速定位到某个资源。

模拟其内部结构:

// 这是一个高度简化的伪代码,模拟内存中的核心结构

// 1. 资源标识符 (Resource Identifier) - 锁住哪个具体的“东西”
//    这是锁的“粒度”所在,可以是一个Page ID + Index ID + Record ID,也可以是表ID
struct LockResource {
    enum ResourceType {
        TABLE_LOCK,    // 表级
        RECORD_LOCK,   // 行级
        GAP_LOCK       // 间隙
    };
    ResourceType type;
    long long tableId; // 表的唯一标识
    long long pageId;  // 数据页的唯一标识 (行锁和间隙锁可能需要)
    long long indexId; // 索引的唯一标识 (行锁和间隙锁需要)
    // 对于Record Lock,可能还需要存储记录的在页面内的具体位置或哈希值
    // 对于Gap Lock,可能需要存储间隙的起始和结束点(如索引键值,或其他内部指针)
    std::string recordKeyHash; // 简化表示:实际是索引键值的hash或物理位置
  
    // 确保 LockResource 可以作为哈希表的键
    bool operator==(const LockResource& other) const { /* 比较所有成员 */ }
    size_t operator()(const LockResource& res) const { /* 计算哈希值 */ }
};

// 2. 具体的锁对象 (Lock Object) - 锁本身的信息
struct LockObject {
    enum LockMode {
        IS_LOCK,   // Intention Shared (表级意向共享)
        IX_LOCK,   // Intention Exclusive (表级意向排他)
        S_LOCK,    // Shared (读锁,共享锁)
        X_LOCK     // Exclusive (写锁,排他锁)
    };
    LockMode mode;
    long long transactionId; // 持有这个锁的事务ID
    int lockCount;           // 锁计数 (用于可重入性), 比如 SELECT ...FOR UPDATE 两次
    bool isWaiting;          // 这个事务是否正在等待这个锁?
  
    // 指向下一个等待这个资源的锁对象(如果存在的话)
    // 或者指向下一个被该事务持有的锁对象
    LockObject* nextLockInResourceList; // 针对同一资源的所有锁和等待者链表
    LockObject* nextLockInTxList;       // 某个事务持有的所有锁链表
};

// 3. 锁哈希表 - 核心的数据结构
// Key: LockResource (哪个资源被锁)
// Value: 一个链表/队列,包含所有作用在该资源上的 LockObject
std::unordered_map<LockResource, std::list<LockObject>> globalLockHashTable;

理解 globalLockHashTable 里的“东西”:

  • 每个节点上
    • 没有独立的“锁标记”。相反,数据库管理着一个集中的 Lock Manager
    • 当你说的“节点”是时,表上会有意向锁ISIX)的记录,这些记录也会被存放在 globalLockHashTable 中。LockResourcetype 会是 TABLE_LOCK
    • 当你说的“节点”是时,它指的就是索引记录 (Index Record)。这才是 InnoDB 行级锁的真正目标。LockResourcetype 会是 RECORD_LOCKGAP_LOCK

核心数据结构 2:事务持有的锁列表 (Transaction’s Lock List)

除了按资源查找锁,Lock Manager 还需要知道一个事务到底持有哪些锁,以便在事务提交或回滚时能迅速释放它们。

  • 目的快速释放一个事务持有的所有锁。
  • 实现:每个活跃事务内部,或者 Lock Manager 维护一个映射Transaction ID -> List of LockObject

模拟其内部结构:

// 4. Per-Transaction Lock List - 每个活跃事务会有一个这样的内部列表
//   一个事务 A 内部可能有一个指针指向它所持有的第一个 LockObject
//   或者 Lock Manager 维护一个 map:
std::unordered_map<long long, std::list<LockObject*>> transactionLocksMap;
// 这个 list 里面的 LockObject* 都是上面 globalLockHashTable 里的指针

可视化模拟:

假设有表 user (id PK, name),数据:[id=1], [id=5], [id=10]

事务 A 操作:UPDATE user SET name='New' WHERE id=1;
事务 B 操作:SELECT * FROM user WHERE id BETWEEN 3 AND 7 FOR UPDATE;

Lock Manager 内部状态(简化):

{
  "globalLockHashTable": {
    // 资源1: 用户表, TABLE_LOCK类型
    "Resource_Table_user": [
      {
        "mode": "IX_LOCK",          // 意向排他锁
        "transactionId": "TxA",
        "lockCount": 1,
        "isWaiting": false
      },
      {
        "mode": "IX_LOCK",          // 意向排他锁 (TxB也会加IX)
        "transactionId": "TxB",
        "lockCount": 1,
        "isWaiting": false
      }
    ],
    // 资源2: id=1 的索引记录, RECORD_LOCK类型
    "Resource_Record_user_id_1": [
      {
        "mode": "X_LOCK",           // 排他锁
        "transactionId": "TxA",
        "lockCount": 1,
        "isWaiting": false
      }
    ],
    // 资源3: "(1,5)" 间隙(id=5前面),GAP_LOCK类型
    "Resource_Gap_user_(1,5)": [
      {
        "mode": "X_LOCK",           // 间隙锁是排他的
        "transactionId": "TxB",
        "lockCount": 1,
        "isWaiting": false
      }
    ],
    // 资源4: "id=5" 记录,RECORD_LOCK类型
    "Resource_Record_user_id_5": [
      {
        "mode": "X_LOCK",           // Next-key lock会包含记录本身
        "transactionId": "TxB",
        "lockCount": 1,
        "isWaiting": false
      }
    ],
    // 资源5: "(5,10)" 间隙,GAP_LOCK类型
    "Resource_Gap_user_(5,10)": [
      {
        "mode": "X_LOCK",           // 间隙锁是排他的
        "transactionId": "TxB",
        "lockCount": 1,
        "isWaiting": false
      }
    ]
    // ... 其他资源
  },
  "transactionLocksMap": {
    "TxA": [
      "Resource_Table_user[IX_LOCK_TxA]",
      "Resource_Record_user_id_1[X_LOCK_TxA]"
    ],
    "TxB": [
      "Resource_Table_user[IX_LOCK_TxB]",
      "Resource_Gap_user_(1,5)[X_LOCK_TxB]",
      "Resource_Record_user_id_5[X_LOCK_TxB]",
      "Resource_Gap_user_(5,10)[X_LOCK_TxB]"
    ]
  }
}

死锁检测器的“行为”:

死锁检测器会定期(或在每次等待发生时)遍历 globalLockHashTable 中的等待链表,并结合 transactionLocksMap 来构建一个**“等待图 (Waits-for Graph)”**。

等待图:

  • 节点:事务 ID (TxA, TxB)。
  • 边:如果 TxA 在等待 TxB 释放某个锁,则从 TxA 指向 TxB。

伪算法:

  1. “老铁,数据库里现在谁在等谁啊?”
  2. 遍历 globalLockHashTable 里的每一个 LockObject
  3. 如果 LockObject.isWaitingtrue
    • 找出这个 LockObject 对应的 LockResource
    • 找出目前正在持有这个 LockResource 上的冲突锁的那个 LockObjecttransactionId (假设是 TxC)。
    • 那么,LockObject.transactionId 正在等待 TxC
    • 在内存的**“等待图”**中,就画一条边:LockObject.transactionId --> TxC
  4. “图画好了!现在我们看看有没有循环
  5. 在“等待图”中进行深度优先搜索 (DFS)拓扑排序等算法来检测是否存在
    • 如果发现 TxA --> TxB --> TxC --> TxA 这样的循环,警报!死锁!
  6. “有了循环!挑选一个受害者,把它回滚,让它释放所有锁,打破这个循环!”

“每个节点上每个表上都有锁标记吗?”

  • 不是每个表“节点”上都有独立的锁标记,而是统一由 Lock Manager 在内存中管理这些 LockObject 实例。
  • 表上:会有 IS/IX 意向锁的 LockObject
  • 行上特指索引记录 (Index Record) 上,会有 S/X 共享/排他锁的 LockObject
  • 间隙上特指索引的空闲区域 (Gap) 上,会有 Gap LockLockObject

所有这些 LockObject 都被组织在 globalLockHashTable 中(按资源分类)以及 transactionLocksMap 中(按事务分类),供 Lock Manager 高效地查找、管理、冲突检测和死锁检测。它们是实时变化的内存数据,支撑着数据库的并发控制。

总结

到此这篇关于Mysql进行操作时锁的具体行为的文章就介绍到这了,更多相关Mysql操作时锁的行为内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 深入浅析MySQL COLUMNS分区

    深入浅析MySQL COLUMNS分区

    COLUMN分区是5.5开始引入的分区功能,只有RANGE COLUMN和LIST COLUMN这两种分区;支持整形、日期、字符串;RANGE和LIST的分区方式非常的相似。下面就两者的区别给大家介绍下,对mysql columns知识感兴趣的朋友一起看看吧
    2016-11-11
  • mysql存储过程之if语句用法实例详解

    mysql存储过程之if语句用法实例详解

    这篇文章主要介绍了mysql存储过程之if语句用法,结合实例形式详细分析了mysql存储过程中if语句相关原理、使用技巧与操作注意事项,需要的朋友可以参考下
    2019-12-12
  • Java连接mysql数据库并进行内容查询的方法

    Java连接mysql数据库并进行内容查询的方法

    下面小编就为大家带来一篇Java连接mysql数据库并进行内容查询的方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-10-10
  • 使用Dify访问mysql数据库详细代码示例

    使用Dify访问mysql数据库详细代码示例

    这篇文章主要介绍了使用Dify访问mysql数据库的相关资料,并详细讲解了如何在本地搭建数据库访问服务,使用ngrok暴露到公网,并创建知识库、数据库访问工作流和智能体,需要的朋友可以参考下
    2025-03-03
  • SQL查询至少连续n天登录的用户

    SQL查询至少连续n天登录的用户

    这篇文章介绍了SQL查询至少连续n天登录用户的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-01-01
  • MySQL脚本批量自动插入数据及数据可按条件插入实现

    MySQL脚本批量自动插入数据及数据可按条件插入实现

    在初始化数据库或者导入一些数据时,常常会用到批量的操作,本文主要介绍了MySQL脚本批量自动插入数据及数据可按条件插入实现,具有一定的参考价值,感兴趣的可以了解一下
    2024-01-01
  • Mysql limit 优化,百万至千万级快速分页 复合索引的引用并应用于轻量级框架

    Mysql limit 优化,百万至千万级快速分页 复合索引的引用并应用于轻量级框架

    MySql 性能到底能有多高?用了php半年多,真正如此深入的去思考这个问题还是从前天开始。有过痛苦有过绝望,到现在充满信心!
    2011-06-06
  • MySQL 创建索引(Create Index)的方法和语法结构及例子

    MySQL 创建索引(Create Index)的方法和语法结构及例子

    MySQL 创建索引(Create Index)的方法和语法结构及例子
    2009-07-07
  • MySQL外键级联的实现

    MySQL外键级联的实现

    本文主要介绍了MySQL外键级联的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07
  • Mysql中一千万条数据怎么快速查询

    Mysql中一千万条数据怎么快速查询

    很多人在使用Mysql时没有考虑到优化问题,如果遇到上千万数据量的表,查询上千万数据量的时候会发生什么问题,本文就来介绍一下如何快速查询一千万条数据,感兴趣的可以了解一下
    2021-12-12

最新评论