Mysql进行操作时锁的具体行为示例详解
作者:@Jackasher
场景一:单个事务更新一条存在的数据
假设有表 user (id PK, name, age),数据:[id=1, name='Alice', age=25]
你的 SQL: UPDATE user SET age = 26 WHERE id = 1;
底层动作:
- 事务 A (主动方) 发起更新请求。
- Lock Manager 介入:
- 查找 id=1 的索引记录: Lock Manager 根据
id=1(主键)找到对应的主键索引树叶子节点中的那条索引记录。 - 检查这条索引记录上的锁状态: 发现
id=1这条索引记录此刻是无锁状态。 - 在索引记录上“粘贴”一个锁标记: Lock Manager 会在
id=1这条具体的索引记录上,打上一个**“X 锁 (Exclusive Lock)”**的标记。- 这个标记就是一条内部的内存数据结构,记录着:“
id=1这条索引记录,现在被事务 A 以X模式锁住,并且引用计数+1”。
- 这个标记就是一条内部的内存数据结构,记录着:“
- 在对应的表头部“粘贴”一个意向锁标记: 同时,Lock Manager 还会顺手在
user表的内部元数据结构上,打上一个**“IX 锁 (Intention Exclusive Lock)”**的标记。- 这个标记是:“
user表的某个地方,有事务正在尝试或已经持有X型行锁。”
- 这个标记是:“
- 查找 id=1 的索引记录: Lock Manager 根据
- 事务 A 执行更新: 事务 A 获得锁,可以安全地修改
id=1这条索引记录的age值。 - 事务 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;
底层动作:
- 事务 A 获得 id=1 的 X 锁 (如场景一所述)。
id=1索引记录上:X锁,持有者事务 A。user表元数据上:IX锁,持有者事务 A。
- 事务 B 发起读取请求。
- Lock Manager 介入:
- 查找 id=1 的索引记录。
- 检查这条索引记录上的锁状态: 发现
id=1这条索引记录上有一个X锁,并且持有者是**事务 A**。 - 判断冲突: 事务 B 尝试读取,但
事务 A持有的是X锁 (排他锁)。X锁会阻止所有其他事务的读写。 - 不授予锁,并等待: Lock Manager 不授予事务 B 任何锁,而是把事务 B 放入一个等待队列,同时启动事务 B 的等待计时器。
- “事务 B 正在等待
id=1这条索引记录上的锁。”
- “事务 B 正在等待
- 事务 A 提交: 释放
id=1上的X锁,也释放user表的IX锁。 - Lock Manager 通知:
id=1上的锁被移除,Lock Manager 发现等待队列中有事务 B。 - 事务 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)
底层动作:
- 事务 A 发起请求。
- Lock Manager 介入:
- 查找索引: Lock Manager 根据条件
id BETWEEN 15 AND 25,在主键索引树上进行查找。 - 发现没有符合条件的记录 (这是一个空区间/间隙)。
- 在“间隙”上打锁标记: 尽管没有找到具体的数据行,Lock Manager 依然会在索引结构中,针对
id=10和id=30之间的**“范围”(即(10, 30)这个间隙),打上一个“间隙锁 (Gap Lock)”**的标记。- 这个标记就是:“索引中
id值在10和30之间的空地,现在被事务 A 锁住,禁止插入新数据。” - 通常,这个间隙锁是
X类型的,因为它阻止其他事务在这个间隙中进行INSERT操作。
- 这个标记就是:“索引中
- 在对应的表头部“粘贴”一个意向锁标记 (IX)。
- 查找索引: Lock Manager 根据条件
- 事务 B 尝试插入数据:
INSERT INTO user (id) VALUES (20); - Lock Manager 介入:
- 判断插入位置: 发现
id=20应该插入到id=10和id=30之间。 - 检查该间隙的锁状态: 发现这个
(10, 30)的间隙上有一个间隙锁,持有者是**事务 A**。 - 不授予锁,并等待: Lock Manager 不授予事务 B 任何锁,将事务 B 放入等待队列。
- 判断插入位置: 发现
- 事务 A 提交: 释放
(10, 30)上的间隙锁,以及user表的IX锁。 - 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。
- 当你说的“节点”是表时,表上会有意向锁(
IS或IX)的记录,这些记录也会被存放在globalLockHashTable中。LockResource的type会是TABLE_LOCK。 - 当你说的“节点”是行时,它指的就是索引记录 (Index Record)。这才是 InnoDB 行级锁的真正目标。
LockResource的type会是RECORD_LOCK或GAP_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。
伪算法:
- “老铁,数据库里现在谁在等谁啊?”
- 遍历
globalLockHashTable里的每一个LockObject。 - 如果
LockObject.isWaiting是true:- 找出这个
LockObject对应的LockResource。 - 找出目前正在持有这个
LockResource上的冲突锁的那个LockObject的transactionId(假设是TxC)。 - 那么,
LockObject.transactionId正在等待TxC。 - 在内存的**“等待图”**中,就画一条边:
LockObject.transactionId-->TxC。
- 找出这个
- “图画好了!现在我们看看有没有循环”
- 在“等待图”中进行深度优先搜索 (DFS) 或拓扑排序等算法来检测是否存在环。
- 如果发现
TxA --> TxB --> TxC --> TxA这样的循环,警报!死锁!
- 如果发现
- “有了循环!挑选一个受害者,把它回滚,让它释放所有锁,打破这个循环!”
“每个节点上每个表上都有锁标记吗?”
- 不是每个表“节点”上都有独立的锁标记,而是统一由 Lock Manager 在内存中管理这些 LockObject 实例。
- 表上:会有
IS/IX意向锁的LockObject。 - 行上:特指索引记录 (Index Record) 上,会有
S/X共享/排他锁的LockObject。 - 间隙上:特指索引的空闲区域 (Gap) 上,会有
Gap Lock的LockObject。
所有这些 LockObject 都被组织在 globalLockHashTable 中(按资源分类)以及 transactionLocksMap 中(按事务分类),供 Lock Manager 高效地查找、管理、冲突检测和死锁检测。它们是实时变化的内存数据,支撑着数据库的并发控制。
总结
到此这篇关于Mysql进行操作时锁的具体行为的文章就介绍到这了,更多相关Mysql操作时锁的行为内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
