Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > Mysql操作时锁的行为

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

作者:@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)

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

模拟其内部结构:

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

// 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 里的“东西”:

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

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

模拟其内部结构:

// 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)”**。

等待图:

伪算法:

  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. “有了循环!挑选一个受害者,把它回滚,让它释放所有锁,打破这个循环!”

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

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

总结

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

您可能感兴趣的文章:
阅读全文