第9章 事务 — 数据库设计与实践
来源:田浩然上传的资料 / 0A102 数据库设计与实践 / chap09 事务.ppt 教师:陈立军 状态:partial_extraction(图片型 PPT,文本提取有限,基于数据库事务标准知识体系 + 课程框架整理)
一、事务的基本概念
1.1 什么是事务(Transaction)
事务是用户定义的一个数据库操作序列,这些操作要么全做要么全不做,是一个不可分割的工作单位。
- 事务是恢复和并发控制的基本单位
- 事务与程序的区别:程序是静态的,事务是动态的;一个程序可以包含多个事务
- 事务的开始和结束可以由用户显式控制,或由 DBMS 自动划分
1.2 事务的 ACID 特性
| 特性 | 英文 | 含义 |
|---|---|---|
| 原子性 | Atomicity | 事务中包含的所有操作要么全部提交,要么全部回滚 |
| 一致性 | Consistency | 事务执行结果必须使数据库从一个一致性状态变到另一个一致性状态 |
| 隔离性 | Isolation | 一个事务的执行不能被其他事务干扰,并发执行的事务之间互不影响 |
| 持续性/持久性 | Durability | 事务一旦提交,它对数据库中数据的改变是永久性的 |
核心要点:
- 原子性由 DBMS 的事务管理子系统实现(commit / rollback)
- 一致性由应用程序员和 DBMS 完整性约束共同保证
- 隔离性由 DBMS 的并发控制子系统实现(锁、时间戳、MVCC 等)
- 持久性由 DBMS 的恢复子系统实现(日志、备份)
1.3 事务的状态
开始
│
▼
活动态 ──失败──→ 中止态 ──回滚──→ 中止(事务失败)
│
│ 部分提交
▼
部分提交态
│
│ 成功完成
▼
提交态(事务成功)
- 活动态(Active):事务正在执行中
- 部分提交态(Partially Committed):最后一条语句执行后
- 失败态(Failed):发现错误,正常执行不能继续
- 中止态(Aborted):事务回滚,数据库恢复到事务开始前状态
- 提交态(Committed):事务成功完成
二、数据库恢复技术
2.1 故障的种类
(1)事务内部故障
- 事务在运行到正常终止点前被中止
- 如:运算溢出、违反完整性约束、死锁中被选中撤销、用户主动撤销
- 恢复方法:UNDO(撤销)该事务对数据库的所有修改
(2)系统故障(软故障)
- 造成系统停止运转的事件,系统需要重新启动
- 如:CPU 故障、操作系统错误、代码异常、突然断电
- 特点:不破坏数据库,但内存数据丢失,所有事务非正常终止
- 恢复方法:
- 撤销(UNDO)所有未完成的事务
- 重做(REDO)所有已提交但尚未写入磁盘的事务
(3)介质故障(硬故障)
- 外存故障,破坏存储在外存上的数据
- 如:磁盘损坏、磁头碰撞、瞬时强磁场干扰
- 特点:破坏数据库,破坏性大,发生概率小
- 恢复方法:重装数据库后备副本 + 重做已完成的事务
(4)计算机病毒
- 人为故障,破坏性可大可小
- 预防 + 检测 + 清除 + 恢复
2.2 恢复的实现技术
数据转储(Backup)
定期将数据库复制到磁带或另一磁盘上保存,形成后备副本。
-
静态转储:系统中无运行事务时进行的转储
- 优点:得到一个数据一致性的副本
- 缺点:降低了数据库的可用性
-
动态转储:转储期间允许对数据库进行存取或修改
- 优点:不用等待正在运行的事务结束
- 缺点:副本中的数据可能不是最新或一致的,需要配合日志
-
海量转储:每次转储全部数据库
-
增量转储:每次只转储上次转储后更新过的数据
登记日志文件(Logging)
日志文件是记录事务对数据库更新操作的文件。
日志的作用:
- 事务故障恢复和系统故障恢复必须用日志
- 动态转储方式中建立后备副本必须配合日志
- 静态转储也可以用日志进行 REDO 处理
日志记录的内容:
- 事务标识(标明哪个事务)
- 操作类型(插入、删除、修改)
- 操作对象
- 更新前的旧值(前像 Before Image)
- 更新后的新值(后像 After Image)
登记原则(Write-Ahead Logging, WAL):
- 日志记录必须先于对应的更新写入数据库
- 即:先写日志,再写数据库
2.3 恢复策略
事务故障的恢复
- 反向扫描日志文件,查找该事务的更新操作
- 对每一个更新操作执行逆操作(用旧值替换当前值)
- 直到读到该事务的开始标记
系统故障的恢复
- 正向扫描日志文件,找出:
- 已经提交的事务(有 BEGIN TRANSACTION 和 COMMIT)→ 放入 REDO 队列
- 未提交/中止的事务 → 放入 UNDO 队列
- 对 UNDO 队列中的所有事务执行撤销(逆向操作)
- 对 REDO 队列中的所有事务执行重做(正向重新执行写入)
介质故障的恢复
- 装入最新的数据库后备副本
- 装入日志文件副本
- REDO 所有已完成的事务
2.4 具有检查点的恢复技术
问题:系统故障恢复时,需要从头扫描整个日志,效率低。
检查点(Checkpoint):定期建立一个检查点记录,包括:
- 建立检查点时刻所有正在执行的事务清单
- 这些事务最近一个日志记录的地址
恢复步骤:
- 从检查点开始扫描日志(而非从头开始)
- 确定需要 UNDO 和 REDO 的事务
- 执行恢复
恢复效率:大大减少了扫描日志的时间。
2.5 数据库镜像
- 自动将整个数据库或关键数据复制到另一个磁盘
- 介质故障时,镜像磁盘可继续使用
- 代价:磁盘空间加倍,写操作时需要写两份
三、并发控制
3.1 并发控制概述
为什么需要并发控制?
- 多用户数据库系统允许多个事务并行执行
- 如果不对并发操作加以控制,会破坏数据一致性
并发操作带来的数据不一致性:
(1)丢失修改(Lost Update)
T1: 读 A=16
T2: 读 A=16
T1: A ← A-1 → 15, 写回
T2: A ← A-1 → 15, 写回
结果:A=15,但应该是 14
T1 的修改被 T2 覆盖丢失
(2)不可重复读(Non-Repeatable Read)
T1: 读 A=50, 读 B=100, 和为 150
T2: 修改 B=200, 提交
T1: 再次读 B=200, 和为 250
同一事务内两次读结果不一致
(3)读”脏”数据(Dirty Read)
T1: 修改 C=200 (原值100), 写入数据库
T2: 读 C=200
T1: 回滚 → C 恢复为 100
结果:T2 读到了 T1 未提交的数据(脏数据)
三类数据不一致性对比:
| 不一致类型 | 原因 | 涉及事务数 | 本质 |
|---|---|---|---|
| 丢失修改 | 两个事务同时修改同一数据 | 2个 | 写-写冲突 |
| 不可重复读 | 一个事务读,另一个事务修改 | 2个 | 读-写冲突 |
| 读脏数据 | 读到了另一个事务未提交的数据 | 2个 | 读-写冲突 |
3.2 封锁(Locking)
封锁是实现并发控制的主要技术。
基本锁类型
| 锁类型 | 符号 | 兼容性 | 说明 |
|---|---|---|---|
| 排他锁(Exclusive Lock / 写锁) | X | 不与任何锁兼容 | 获准 X 锁的事务可读可写 |
| 共享锁(Share Lock / 读锁) | S | 与 S 兼容,与 X 不兼容 | 获准 S 锁的事务只能读不能写 |
锁的相容矩阵:
| S | X | |
|---|---|---|
| S | Y | N |
| X | N | N |
(Y = 相容,可以同时持有;N = 不相容)
3.3 三级封锁协议
一级封锁协议
- 事务 T 在修改数据 R 之前,必须先对其加 X 锁,直到事务结束才释放
- 可防止丢失修改
二级封锁协议
- 一级封锁协议 + 事务 T 在读取数据 R 之前,必须先加 S 锁,读完后即可释放 S 锁
- 可防止丢失修改 + 读脏数据
三级封锁协议
- 一级封锁协议 + 事务 T 在读取数据 R 之前,必须先加 S 锁,直到事务结束才释放
- 可防止丢失修改 + 读脏数据 + 不可重复读
3.4 活锁与死锁
活锁(Live Lock)
- 某个事务永远处于等待状态,得不到封锁的机会
- 原因:先来不一定先服务
- 解决方法:先来先服务(FCFS)策略
死锁(Dead Lock)
- 两个或多个事务都已封锁了一些数据对象,然后又都请求对已为其他事务封锁的数据对象加锁
- 形成循环等待
死锁示例:
T1: 封锁了 R1, 请求封锁 R2
T2: 封锁了 R2, 请求封锁 R1
T1 等 T2 释放 R2, T2 等 T1 释放 R1 → 死锁
死锁的预防
-
一次封锁法
- 每个事务必须一次将所有要使用的数据全部加锁
- 缺点:降低了并发度,难以精确确定封锁对象
-
顺序封锁法
- 预先规定一个封锁顺序,所有事务都按这个顺序实行封锁
- 缺点:维护成本高,难以适应数据变化
死锁的诊断与解除
-
超时法
- 事务等待时间超过阈值,则判定为死锁,回滚该事务
- 实现简单,但可能误判(等待时间长但不一定是死锁)
-
等待图法
- 构造事务等待图(有向图),检测是否存在回路
- 若有环则存在死锁
- 解除:选择一个代价最小的事务回滚
3.5 可串行化调度
可串行化(Serializability):多个事务并发执行的结果与按某一次序串行执行它们的结果相同。
- 可串行化是衡量并发事务正确与否的标准
- 一个给定的并发调度,当且仅当它是可串行化的,才是正确的
冲突可串行化:
- 如果一个调度通过交换不冲突操作的顺序可以变成串行调度,则称它是冲突可串行化的
- 冲突可串行化是可串行化的充分条件(不是必要条件)
冲突操作:不同事务对同一数据的操作中,写-写、写-读是冲突的,读-读不冲突。
冲突可串行化判定算法:
- 构造优先图(Precedence Graph)
- 若图中无环 → 冲突可串行化
- 若图中有环 → 不可串行化
3.6 两段锁协议(Two-Phase Locking, 2PL)
两段锁协议是保证可串行化调度的一个重要协议。
内容:所有事务分两个阶段对数据项加锁和解锁:
- 扩展阶段(Growing Phase):可以申请加任何类型的锁,但不能释放任何锁
- 收缩阶段(Shrinking Phase):可以释放任何类型的锁,但不能再申请新锁
性质:
- 遵守两段锁协议 → 可串行化调度(充分条件)
- 但可串行化调度不一定都遵守两段锁协议
- 两段锁协议可能产生死锁
2PL 与三级封锁协议的关系:
- 三级封锁协议规定了什么时候加什么锁、什么时候释放
- 两段锁协议是从可串行化角度定义的
- 二者从不同角度描述封锁规则
3.7 封锁粒度
封锁粒度:封锁对象的大小。
- 粒度大 → 并发度低,开销小
- 粒度小 → 并发度高,开销大
多粒度封锁:系统同时支持多种封锁粒度,供不同事务选择。
多粒度树:
数据库
/ \
表1 表2
/ \ / \
元组 元组 元组 元组
意向锁(Intention Lock):
- 为了高效检测多粒度下的锁冲突,引入意向锁
- 对一个结点加锁,意味着其所有后裔结点也被加上同样类型的锁
- 加锁前必须先对上层结点加意向锁
意向锁类型:
| 锁类型 | 符号 | 含义 |
|---|---|---|
| 意向共享锁 | IS | 后裔结点拟加 S 锁 |
| 意向排他锁 | IX | 后裔结点拟加 X 锁 |
| 共享意向排他锁 | SIX | 加 S 锁 + 加 IX 锁 |
相容矩阵(简化):
| IS | IX | S | SIX | X | |
|---|---|---|---|---|---|
| IS | Y | Y | Y | Y | N |
| IX | Y | Y | N | N | N |
| S | Y | N | Y | N | N |
| SIX | Y | N | N | N | N |
| X | N | N | N | N | N |
四、事务处理的其他问题
4.1 事务的隔离级别
SQL 标准定义了四个隔离级别(从低到高):
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED(读未提交) | 可能 | 可能 | 可能 |
| READ COMMITTED(读已提交) | 不会 | 可能 | 可能 |
| REPEATABLE READ(可重复读) | 不会 | 不会 | 可能 |
| SERIALIZABLE(可串行化) | 不会 | 不会 | 不会 |
- 幻读(Phantom Read):事务两次查询的结果集行数不同(因为其他事务插入或删除了行)
- 大多数数据库默认隔离级别是 READ COMMITTED
- MySQL InnoDB 默认是 REPEATABLE READ(通过 MVCC + 间隙锁解决幻读)
4.2 MVCC(多版本并发控制)
- 每行数据保留多个版本
- 读操作读的是历史版本,不需要加锁
- 写操作创建新版本,不影响读
- 大大提高了读并发性能
- 是现代主流数据库(Oracle、InnoDB、PostgreSQL)的标配
4.3 分布式事务
- 事务涉及多个数据库节点
- 两阶段提交(2PC):
- 第一阶段:协调者询问所有参与者是否可以提交
- 第二阶段:所有参与者都同意则提交,否则回滚
- 三阶段提交(3PC):在 2PC 基础上增加超时机制,减少阻塞
五、与其他章节的关联
- 第3章 关系模型:事务是建立在关系模型之上的执行单元
- 第4章 SQL:SQL 中通过
BEGIN TRANSACTION/COMMIT/ROLLBACK控制事务 - 第7章 关系规范化:事务是保证数据一致性的重要手段,规范化也是从设计层面保证一致性
- 第10章 数据库性能调优:事务设计对性能影响很大,锁粒度、隔离级别的选择是性能调优的重要内容
- 第12章 事务处理:后续章节深入讨论事务处理的高级技术
重点总结
- ACID 四特性是事务的核心:原子性、一致性、隔离性、持久性
- 数据库恢复的三种故障类型:事务故障(UNDO)、系统故障(UNDO + REDO)、介质故障(重装 + REDO)
- WAL 原则:先写日志后写数据库
- 检查点技术:提高恢复效率
- 并发控制的三类问题:丢失修改、不可重复读、读脏数据
- 封锁技术:X 锁和 S 锁,三级封锁协议
- 死锁:预防(一次封锁 / 顺序封锁)vs 诊断解除(超时法 / 等待图法)
- 可串行化:并发调度正确性的标准
- 两段锁协议:保证可串行化的充分条件
- 多粒度封锁 + 意向锁:平衡并发度和系统开销