锁与死锁——数据库为什么会发生事务等待?

前面我们了解了事务与隔离级别,以及 MVCC,理解数据库如何通过多个数据版本,让不同事务读取各自应该看到的数据。

但 MVCC 并不能解决所有并发问题。假设两个事务同时修改同一条记录,数据库必须决定哪个事务先修改、后来的事务是否等待,以及互相等待时如何处理。这就是锁(Lock)机制要解决的问题。


一、为什么数据库需要锁?

假设一条记录的余额为 1000,事务 A 扣除 200,事务 B 扣除 300。如果两个事务都先读取 1000,再分别写入 800 和 700,后写入的值可能覆盖前一个事务的修改,造成丢失更新(Lost Update)。

锁通过协调对冲突资源的访问,避免不受控制的并发修改。典型过程是:

  1. 事务获取锁;
  2. 执行操作;
  3. 在适当的事务结束点释放锁;
  4. 其他事务请求冲突锁时,需要等待或按数据库规则中止。

二、共享锁与排他锁

Shared Lock(S Lock,共享锁)

共享锁通常用于锁定读取。多个事务可以同时持有同一资源的兼容共享锁。

Exclusive Lock(X Lock,排他锁)

排他锁通常用于修改数据。它与同一资源上的共享锁和排他锁一般不兼容,因此其他事务请求冲突锁时需要等待。

已持有锁 请求 S Lock 请求 X Lock
S Lock 兼容 冲突
X Lock 冲突 冲突

这是基础兼容矩阵。真实数据库还会有意向锁、记录锁、范围锁等类型,具体规则也取决于数据库实现。


三、锁粒度

锁粒度(Lock Granularity) 指锁保护的资源范围。

粒度 保护范围 典型权衡
表锁 整张表 锁管理相对简单,但可能造成较多不必要阻塞
页锁 一个数据页 介于表锁与行锁之间
行锁 特定记录或索引记录 并发更灵活,但锁管理成本可能更高

锁越粗,管理通常越简单,但并发灵活性越低;锁越细,冲突范围通常越小,但系统需要管理更多锁状态。不同数据库支持的锁粒度和内部实现并不完全相同。例如,InnoDB 的常规数据锁主要围绕索引记录及范围工作。


四、两阶段锁(2PL)

两阶段锁(Two-Phase Locking,2PL) 约束事务获取和释放锁的顺序,分为两个阶段:

  • 增长阶段(Growing Phase):事务可以获取新锁,但不能释放已有锁。
  • 收缩阶段(Shrinking Phase):事务可以释放锁,但不能再获取新锁。

2PL 能保证冲突可串行化(Conflict Serializability) 。严格两阶段锁(Strict 2PL) 进一步要求排他锁等关键锁保持到事务提交或回滚,从而避免其他事务过早依赖尚未最终确定的修改。

锁保持时间越长,隔离与恢复语义通常越容易管理,但事务等待也可能增加。


五、死锁(Deadlock)

假设事务 A 持有 X,事务 B 持有 Y;随后 A 请求 Y,B 请求 X。A 等待 B,B 又等待 A,双方都无法继续,这就是死锁。

等待关系可以表示为等待图(Wait-for Graph):

text 复制代码
T1 → T2
↑     ↓
└─────┘

在常见的锁等待模型中,等待图出现环是死锁的重要判据。

死锁处理方式

  1. 检测(Detection) :数据库检查等待关系,发现循环等待后选择一个事务作为牺牲者(Victim),中止或回滚它,释放锁。
  2. 超时(Timeout):等待超过配置阈值后取消等待或中止事务。超时不一定代表死锁,也可能只是事务执行时间较长。
  3. 预防(Prevention):例如规定事务按统一顺序获取资源,减少循环等待形成的机会,但不能覆盖所有死锁场景。

六、锁管理器在数据库内核中的职责

锁管理器不负责查找记录本身,而是管理资源锁状态和请求协调。概念上,它需要维护:

  • 锁定资源,例如表、记录或索引范围;
  • 锁模式与兼容关系;
  • 当前持有锁的事务;
  • 等待队列;
  • 事务状态、取消与唤醒流程。

简化的请求流程如下:

text 复制代码
事务请求锁
    ↓
查找资源锁状态
    ↓
检查兼容性
    ├── 兼容 → 授予锁,事务继续
    └── 冲突 → 加入等待队列
                    ↓
              冲突锁释放
                    ↓
              重新检查并授予

事务被唤醒并不意味着一定立即获得锁;数据库仍需重新检查兼容性和队列调度规则。并发实现还必须妥善处理线程同步、事务取消、回滚和异常路径。


七、MVCC 与锁如何配合?

  • MVCC 主要处理版本可见性:事务应读取哪个版本。
  • 锁 主要处理冲突协调:哪些访问需要互斥、哪些修改需要等待。

例如,MVCC 一致性读可能让读事务读取旧版本,而不必等待另一个事务完成更新;但两个事务同时修改同一记录时,数据库仍然需要处理写冲突。显式锁定读取、唯一性约束、外键检查和某些范围保护场景,也可能需要锁或其他并发控制机制。

因此不能把 MVCC 理解为"不需要锁"。实际数据库通常组合使用 MVCC、锁、事务状态和日志机制。


八、PostgreSQL 与 InnoDB 的典型差异

维度 PostgreSQL InnoDB
普通一致性读 通常通过 MVCC Snapshot 通常通过 Read View
行级并发控制 行级锁与 Tuple 版本机制协作 锁通常围绕索引记录及范围
范围保护 Serializable 下使用 SSI 等机制 适用场景下使用 Gap Lock、Next-Key Lock
死锁处理 检测后中止事务 检测后选择事务回滚
历史版本回收 VACUUM 等 Purge 等

PostgreSQL 支持多种表锁、行锁及咨询锁。普通 MVCC 一致性读取通常不需要对每一行获取传统共享行锁;SELECT ... FOR UPDATE 等锁定读取则会请求相应锁。在 Serializable 隔离级别下,PostgreSQL 使用 Serializable Snapshot Isolation(SSI) 跟踪可能导致不可串行化结果的依赖,必要时中止事务。

InnoDB 的记录锁和范围锁与索引密切相关。Gap Lock 保护索引记录之间的间隙;Next-Key Lock 组合记录锁与间隙锁,在适用场景下防止范围内的冲突插入或修改。实际锁定范围取决于索引、查询计划、隔离级别和具体数据库版本。


九、锁的成本与设计原则

锁带来的成本主要包括:

  • 管理开销:维护锁对象、状态、等待队列和兼容性检查;
  • 等待开销:冲突事务可能阻塞,降低吞吐量;
  • 死锁处理成本:检测、选择牺牲者和回滚;
  • 并发度损失:锁范围过大时,互不冲突的操作也可能被阻塞。

常见的优化方向包括:

  • 缩短事务持续时间;
  • 减少不必要的锁范围;
  • 合理设计索引;
  • 避免事务以不一致的顺序访问资源;
  • 在适合的场景利用 MVCC 一致性读。

目标不是消灭锁,而是以合理成本保护真正存在冲突的操作。


十、把锁放回数据库内核全景

text 复制代码
SQL / Query Executor
        ↓
Transaction Manager
        ↓
Lock Manager + MVCC
        ↓
Index / Page / Buffer Pool
        ↓
WAL
        ↓
Commit / Recovery

这是概念上的职责关系,不代表真实数据库严格按单一线性顺序执行。执行器、事务管理、锁与版本控制、数据页修改和 WAL 记录会在事务过程中相互协作。

需要区分三件事:

  • 索引与存储层定位数据;
  • 锁管理器与 MVCC协调并发访问和版本可见性;
  • WAL 与恢复机制支持崩溃后的持久性恢复。

小结

  • S Lock 与 X Lock 描述常见的锁模式及兼容关系。
  • 锁粒度决定锁保护资源的范围,是并发灵活性与管理成本之间的权衡。
  • 2PL 约束锁获取和释放顺序;Strict 2PL 会将关键锁保持到事务结束。
  • 死锁是循环等待,可通过检测、超时或预防策略处理。
  • MVCC 负责版本可见性,锁负责冲突协调,两者通常协同工作。
  • PostgreSQL 与 InnoDB 都支持 MVCC 和锁,但锁对象、范围保护与内部实现存在差异。
相关推荐
赛博守夜人1 小时前
跨境业务与出海合规之八:面向海外合规监管突击搜查(Dawn Raid)的信息安全应急响应(IR)与合规取证
大数据·数据库
冰暮流星2 小时前
mysql之索引创建,查看,删除
数据库·mysql
溪语流沙2 小时前
【Web全栈进阶】Alembic数据库迁移:改表结构不再删库
前端·数据库·python·elasticsearch·postgresql
杨云龙UP2 小时前
Oracle 19c 单机到单机 Active Data Guard 搭建与巡检操作手册
数据库·oracle·adg·操作指南·data guard·dg搭建
念越2 小时前
初中语数英答题与竞赛平台
java·数据库·spring boot
落日归屿2 小时前
AI 商品图做完后,为什么还要管图层?
数据库
拾贰_C2 小时前
【English | conversation 】call | 抖音AI短句情景:打电话---come over
数据库·redis·缓存
2601_962885723 小时前
AlphaFeed 支持哪些 K 线周期?period参数怎么传?
前端·数据库·python
麦壳饼3 小时前
架构深度解析:SonnetDB 的写入与查询路径
数据库·sonnetdb