行级锁需与事务生命周期绑定,通过LockManager以事务ID为持有者、RID为粒度管理S/X锁,并在加锁等待时构建wait-for图实时检测死锁,事务提交/回滚时同步清理锁和图边。

行级锁怎么在 C++ 中与数据库事务联动
行级锁不能脱离事务生命周期存在,否则锁住的行在事务提交或回滚后无法被正确释放。C++ 里没有现成的「数据库锁管理器」,必须自己封装一个能和事务状态同步的 LockManager 类,并让它感知 Transaction 的 commit() 和 abort()。
关键点在于:锁的持有者必须是事务 ID(比如 txn_id_t),不是线程 ID;锁资源必须以「表名+主键值」或「page_id + slot_id」为粒度建索引;每次 SELECT ... FOR UPDATE 或 UPDATE 执行前,先调用 lock_manager_->LockRow(txn, table_name, rid)。
-
rid(record id)建议用结构体封装page_id和slot_num,避免字符串拼接做 key - 锁类型分
LOCK_SHARED和LOCK_EXCLUSIVE,读操作可申请 S 锁,写操作必须 X 锁;S 锁与 S 锁兼容,其余互斥 - 不要在锁管理器内部做 I/O,只管内存中的锁表(
std::unordered_map<rid lockrequestqueue></rid>)和事务等待关系
死锁检测图怎么构建和遍历
死锁检测不是靠定时轮询,而是在每次加锁失败、事务进入等待时,立即触发环路检查。核心是维护一张有向图:wait_for_graph_,其中边 A → B 表示「事务 A 正在等待事务 B 持有的锁」。
构建逻辑很简单:当事务 T1 因请求被 T2 占用的锁而阻塞,就插入一条边 T1 → T2;每次插入后,从 T1 出发做 DFS(或用 Floyd-Warshall 简化版),看能否回到自身。检测到环,就选一个牺牲者(通常选回滚代价最小的,比如修改行数最少的事务)。
立即学习“C++免费学习笔记(深入)”;
- 图节点用
txn_id_t,边用std::unordered_map<txn_id_t std::vector>></txn_id_t>存储 - DFS 必须限制深度(比如 ≤ 100),防止恶意构造长链导致栈溢出
- 检测过程中要加图结构的读写锁(
std::shared_mutex),但不能阻塞正在加锁的热路径 - 别忘了在事务
commit/abort时,从图中删掉它所有出边和入边,否则图会持续膨胀
为什么不能直接用 std::mutex 模拟行锁
因为 std::mutex 是进程内同步原语,不具备事务语义,也无法跨进程/崩溃恢复。数据库要求锁在 crash 后仍能重建(通过 WAL 和 checkpoint),且锁冲突必须反映真实数据依赖,而非线程调度顺序。
更实际的问题是:一个事务可能持有数百行锁,如果每行都 new 一个 std::mutex,内存开销大、初始化慢、且无法批量释放;而基于哈希表的锁管理器可以统一管理、支持超时、支持升级(S→X)、支持意向锁(IX/IS)扩展。
- 用
std::mutex会导致死锁检测失效——你根本不知道谁在等谁,图信息全丢失 - 事务回滚时,
std::mutex不会自动 unlock,必须显式调用,极易遗漏 - 调试时看不到锁等待链,只能看到线程卡在 mutex.lock(),无法定位是哪行记录被谁占着
常见错误:锁升级失败和图更新不同步
最隐蔽的 bug 是「锁升级」(S→X)时没检查等待队列是否已有其他事务在等这个 RID 的 X 锁,结果强行升级导致违反两阶段锁协议(2PL);另一个高频问题是图边未及时清理,导致已结束的事务仍出现在 wait-for 图中,引发误杀。
- 升级前必须调用
CanLockUpgrade(rid, txn_id):检查队列里有没有别的事务正等待 X 锁,且优先级更高 - 事务 abort 时,除了释放自己持有的所有锁,还要遍历
wait_for_graph_,删掉所有以该txn_id为起点或终点的边 - 日志里至少记下每次图变更(add edge / remove edge / choose victim),否则线上出现频繁 abort 时无法归因
- 测试时用两个事务交叉更新两行(T1: A→B, T2: B→A),必须稳定触发死锁并选出唯一 victim,否则图维护逻辑有缺陷
真正难的不是实现图算法,而是让锁状态、事务状态、日志落盘、恢复逻辑四者严格一致。哪怕只漏掉一次边删除,重启后死锁检测就会失灵。所以别省略单元测试里的 CrashRecoveryTest 用例。


















