场景一:单个事务更新一条存在的数据
假设有表 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
型行锁。”
- 这个标记是:“
- 查找
- 事务 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; // 持有这个锁的事务IDint 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 高效地查找、管理、冲突检测和死锁检测。它们是实时变化的内存数据,支撑着数据库的并发控制。