本章介绍数据库并发控制的基本概念,详细阐述事务的 ACID 特性、并发操作引发的数据不一致问题,以及通过 S 锁与 X 锁构建的三级封锁协议。
9.1 并发控制与事务基础
9.1.1 什么是并发控制
在多用户数据库系统中,多个用户或应用程序可能会同时(并发地)访问或修改同一个数据。并发控制 (Concurrency Control) 就是通过特定的技术和机制,协调这些并发操作的执行,保证数据库的 一致性 (Consistency) 和 隔离性 (Isolation),防止因并发冲突导致数据被破坏。
9.1.2 什么是事务 (Transaction)
数据库中的 事务 (Transaction) 是用户定义的一个数据库操作序列。这些操作要么全做,要么全不做,是一个不可分割的逻辑工作单位。 在部分基础介绍中,事务有时也会被称为“事件”,其核心和本质即数据库的事务。
9.1.3 事务的 ACID 特性
事务是并发控制的基本单位。一个合法的事务必须满足 ACID 四大特性:
- 原子性 (Atomicity):事务中的操作是不可分割的。事务中包含的所有操作要么全部成功提交,要么全部失败回滚 (Rollback)。
- 一致性 (Consistency):事务必须使数据库从一个一致性状态过渡到另一个一致性状态。例如,转账操作中,双方的总金额在转账前后必须保持守恒。
- 隔离性 (Isolation):一个事务的执行不能被其他并发事务干扰。并发执行的事务之间必须相互隔离,事务的中间状态对其他事务不可见。
- 持久性 (Durability):一旦事务提交 (Commit),它对数据库中数据的改变应该是永久性的,即使数据库随后发生系统崩溃或停电,修改结果也不会丢失。
9.2 并发操作带来的数据不一致问题
如果不对并发操作进行控制,当多个事务并发读写相同的数据对象时,可能会导致以下 3 种典型的数据不一致问题:
9.2.1 丢失修改 (Lost Update)
当两个事务 和 同时读入同一数据并进行修改时, 的提交结果覆盖了 的提交结果,导致 的修改丢失了。
- 示例流程:
- 事务 读出机票余额 。
- 事务 也读出机票余额 。
- 事务 售出一张票,修改 并写入磁盘。
- 事务 售出一张票,修改 并写入磁盘。此时 的修改被覆盖,机票本应剩 98 张,实际却显示 99 张。
9.2.2 读脏数据 (Dirty Read)
当事务 修改了某个数据并将其写回磁盘,但在事务结束前由于某种原因被撤销 (Rollback) 回滚。在此期间,另一个并发事务 读取了被修改后的值。此时 读取到的数据就是 脏数据 (即与磁盘实际状态不符的不正确数据)。
- 示例流程:
- 事务 将 的值修改为 200 并写回。
- 事务 读取了 。
- 事务 由于出错执行回滚,将 的值恢复为原来的 100。
- 此时事务 持有的 就是脏数据。
9.2.3 不可重复读 (Unrepeatable Read)
不可重复读是指事务 读取数据后,并发事务 对该数据进行了修改、删除或插入,当事务 再次尝试读取该数据或该范围的记录时,得到了与前一次不同的结果。
- 具体表现形式:
- 值修改: 读取 , 将其修改为 200, 再次读取得到 200,与第一次不同。
- 记录删除: 按条件读取了某些记录,随后 删除了其中部分记录, 再次读取发现记录减少。
- 幻读 (Phantom Read): 按条件读取了某些记录,随后 插入了符合条件的新记录, 再次读取发现多了记录 (如产生“幻影”般的新数据)。
9.3 封锁机制 (Locking)
封锁是实现并发控制最常用的技术。所谓封锁,就是事务在对某个数据对象 (如数据项、记录、关系表) 操作之前,向系统发出请求,对其加锁。在释放锁之前,其他事务不能对该数据对象进行不兼容的操作。
常用的基本封锁类型有两种:共享锁 (S 锁) 和 排他锁 (X 锁)。
9.3.1 共享锁 (S 锁 - Shared Lock)
又称 读锁。如果事务 对数据对象 加上了 S 锁,则事务 可以读取 但不能修改 。
- 兼容性:其他事务可以对 再次加 S 锁以并发读取,但不能加 X 锁,直到事务 释放了 上的 S 锁。这保证了多个事务可以并发读取同一数据,但读期间不允许任何写操作。
9.3.2 排他锁 (X 锁 - Exclusive Lock)
又称 写锁。如果事务 对数据对象 加上了 X 锁,则事务 既可以读取 也可以修改 。
- 兼容性:其他任何事务都不能再对 加任何类型的锁 (无论是 S 锁还是 X 锁),直到事务 释放了 上的 X 锁。这保证了在写操作期间,其他事务既不能读也不能写该数据。
9.3.3 锁的相容矩阵
| 锁请求 \ 已持锁 | 无锁 | S 锁 (读锁) | X 锁 (写锁) |
|---|---|---|---|
| S 锁 (读锁) | 允许 | 允许 | 拒绝 |
| X 锁 (写锁) | 允许 | 拒绝 | 拒绝 |
9.4 三种封锁协议 (Locking Protocols)
在运用 X 锁与 S 锁对数据对象加锁时,需要约定一些规则 (如:何时申请、持锁多久、何时释放)。这些规则被称为 封锁协议 (Locking Protocols)。不同级别的封锁协议可以提供不同强度的并发控制保护。
9.4.1 一级封锁协议
- 规则:事务 在修改数据 之前必须先对其加 X 锁,直到事务结束 (
COMMIT或ROLLBACK) 才释放。如果是读取数据,则不需要加锁。 - 保护效果:可以防止 丢失修改 (因为其他修改事务无法在同一时间对该数据加写锁)。
- 缺陷:由于读操作不加锁,因此无法防范 读脏数据 和 不可重复读。
9.4.2 二级封锁协议
- 规则:在一级封锁协议的基础上,增加对读操作的加锁限制:事务 在读取数据 之前必须先对其加 S 锁,读完后即可释放 S 锁。
- 保护效果:可以防止 丢失修改 和 读脏数据 (因为读数据前要加 S 锁,而如果其他事务正在修改该数据并持有 X 锁,读事务必须等待写事务结束释放锁后才能加 S 锁,从而避免了读到未提交的脏数据)。
- 缺陷:由于读完数据后立即释放了 S 锁,如果在同一个事务中后面再次读取,数据可能已被其他事务修改,因此无法防范 不可重复读。
9.4.3 三级封锁协议
- 规则:在一级封锁协议的基础上,增加更严格的读锁持有期限制:事务 在读取数据 之前必须先对其加 S 锁,并且 必须持续到事务结束 才能释放。
- 保护效果:可以防止 丢失修改、读脏数据 以及 不可重复读 (因为 S 锁持续到事务结束,在这期间其他并发事务无法申请 X 锁来修改该数据,从而保证了可重复读)。
9.4.4 封锁协议对比与总结
| 封锁协议级别 | 写锁 (X 锁) 释放时机 | 读锁 (S 锁) 释放时机 | 防丢失修改 | 防读脏数据 | 防不可重复读 |
|---|---|---|---|---|---|
| 一级封锁协议 | 事务结束时 | 不加锁 | ✅ | ❌ | ❌ |
| 二级封锁协议 | 事务结束时 | 读完即释放 | ✅ | ✅ | ❌ |
| 三级封锁协议 | 事务结束时 | 事务结束时 | ✅ | ✅ | ✅ |
两段锁协议 (2PL) 与三级协议的区别需要特别区分 三级封锁协议 和 两段锁协议 (Two-Phase Locking Protocol, 2PL):
- 三级协议:侧重于解决特定的并发不一致问题(如脏读、不可重复读),它定义的是不同锁的持有生命周期。
- 2PL 协议:规定所有事务的加锁操作必须在解锁操作之前进行(加锁阶段和解锁阶段)。它是保证并发事务 可串行化 (Serializability) 执行(即并发执行效果等同于某种串行执行效果)的充分条件。遵循 2PL 的事务并不一定是三级协议,三级协议也不一定完全遵循 2PL。
参考视频:数据库系统概论 陈红 老师主讲 第11章 并发控制
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时

