三大核心问题:数据库如何保证操作逻辑完整?并发执行如何正确?故障后数据如何恢复? 所有答案围绕 ACID 展开。
一、事务(Transaction)
1.1 是什么?
事务 = 具有完整逻辑意义的数据库操作序列,是数据库执行的最小工作单位。
一个事务包含一条或多条 SQL(如查询、插入、更新、删除),这些操作要么全部成功,要么全部失败,不能拆半。
例子:飞机订票 =「更新售票点已售票数」+「更新航班余票数」,两个操作必须打包成一个事务。
1.2 为什么要有事务?
现实中一个业务往往需要多条 SQL 配合。如果只执行一半就停了,数据库会处于逻辑混乱的"半成品"状态。
| 没有事务的后果 | 例子 |
|---|---|
| 数据不一致 | 转账只扣 A 不增 B,钱凭空消失 |
| 无法回滚 | 操作到一半出错,已经改的数据赖在数据库里 |
| 无法保证业务规则 | 已售票数 + 余票数 ≠ 总座位数 |
1.3 事务怎么用?
基本语法(SQL):
BEGIN TRANSACTION; -- 开启事务
UPDATE 账户表 SET 余额 = 余额 - 1000 WHERE 账户 = 'A'; -- 操作1
UPDATE 账户表 SET 余额 = 余额 + 1000 WHERE 账户 = 'B'; -- 操作2
COMMIT; -- 全部成功,提交生效
-- 或
ROLLBACK; -- 任何一步出错,全部撤销
单条 SQL 默认也是事务------隐式自动提交,只是你看不到 BEGIN/COMMIT。
二、事务的 ACID 特性
2.1 ACID 是什么?
| 特性 | 英文 | 含义 |
|---|---|---|
| A | Atomicity(原子性) | 要么全做,要么全不做 |
| C | Consistency(一致性) | 事务执行前后,数据库都处于合法状态(满足所有约束) |
| I | Isolation(隔离性) | 多个事务并发执行时互不干扰 |
| D | Durability(持久性) | 事务提交后,修改永久生效,故障不丢失 |
2.2 为什么要有 ACID?
四根支柱撑起数据库可靠性:
| 少了哪个 | 会出现什么问题 |
|---|---|
| 没有原子性 | 操作做一半停了,数据成"半成品" |
| 没有一致性 | 转账后总金额变了,违反业务规则 |
| 没有隔离性 | 多人同时订票,同一张票卖给两个人 |
| 没有持久性 | 提交后断电,数据没了,用户白操作 |
2.3 由谁保证?怎么用?
| 特性 | 由谁保证 | 怎么用 |
|---|---|---|
| 原子性 | 恢复管理模块(UNDO 操作) | 事务失败时自动回滚所有已做更新 |
| 一致性 | 应用开发人员 + DBMS 完整性约束 | 写正确的业务逻辑,DBMS 用 CHECK/FOREIGN KEY 辅助校验 |
| 隔离性 | 并发控制模块(封锁协议) | 通过加锁机制控制并发事务的执行顺序 |
| 持久性 | 恢复管理模块(REDO 操作 + 日志) | 提交先写日志,故障后通过日志重做 |
三、并发控制
3.1 是什么?
并发控制 = 管理多个事务同时执行,保证结果正确
DBMS 允许多个事务"穿插"执行(宏观并行、微观串行),但需要用机制防止它们互相干扰。
3.2 为什么要并发?
| 不并发的问题 | 并发的好处 |
|---|---|
| 事务排队执行,后面的苦等 | 提高吞吐量,减少平均响应时间 |
| CPU 等磁盘时闲着浪费 | 事务 A 等 I/O 时,事务 B 用 CPU 计算 |
| 用户体验差 | 多人同时操作时系统不卡顿 |
3.3 并发带来的三类问题
| 问题 | 是什么 | 例子 | 危害 |
|---|---|---|---|
| 读脏数据 | 读到别人未提交的数据,之后对方回滚 | T1 把余票 10→8(未提交),T2 读了 8;T1 回滚,实际余票还是 10 | 基于错误数据做决策 |
| 不可重复读 | 同一事务两次读同一数据,结果不同 | T3 第一次读余票 10,T1 改成 8 并提交,T3 第二次读变成 8 | 同一事务内数据不一致 |
| 丢失更新 | 两个事务都基于旧值修改,后者覆盖前者 | T1、T2 都读余票 10,T1 改成 8 提交,T2 基于 10 改成 7 提交,最终=7 | T1 的更新丢了 |
3.4 调度与串行化
是什么?
-
调度(Schedule):多个事务操作的执行序列。
-
串行调度 :一个事务全跑完再跑下一个,一定正确但效率低。
-
冲突可串行化:可以通过交换非冲突操作,等价于某个串行调度的并发调度。
为什么需要?
并发执行是随机的,不是所有随机调度都正确。需要一个判断标准来衡量并发调度是否安全。
怎么用?
判断方法:优先图法
-
找出所有冲突操作对(不同事务、同一数据、至少一个写)。
-
冲突操作 Ti 在前 → Tj 在后,画边 Ti → Tj。
-
无环 = 冲突可串行化 (正确);有环 = 不正确。
例子:调度 S = r1(A) w2(A) r2(B) w1(B) 冲突:r1(A)↔w2(A) 得 T1→T2;r2(B)↔w1(B) 得 T2→T1 优先图成环(T1→T2→T1),不是冲突可串行化。
3.5 封锁机制
是什么?
封锁 = 访问数据前先加锁,阻止其他事务以冲突方式同时访问。
两种基本锁:
| 锁类型 | 符号 | 特点 | 相容性 |
|---|---|---|---|
| 共享锁 | S 锁 | 可读不可写 | 多个 S 锁可以共存 |
| 排他锁 | X 锁 | 可读可写 | X 锁与任何锁都不相容 |
为什么需要封锁?
防止并发冲突的根本手段------"占着茅坑不让人乱来"。
怎么用?
两阶段封锁协议(2PL):
增长阶段:只能加锁,不能解锁
↓
封锁点(最后申请锁的时刻)
↓
缩减阶段:只能解锁,不能加新锁
核心规则:所有锁必须在事务结束前申请完毕,之后只能释放。
为什么能保证冲突可串行化?封锁点定义了一个全序,按封锁点排序执行等价于某个串行调度。
改进协议:
| 协议 | 改进点 | 解决什么问题 |
|---|---|---|
| 严格两阶段封锁 | X 锁必须等事务提交后才释放 | 避免读脏数据、避免级联回滚 |
| 强两阶段封锁 | 所有锁(S锁和X锁)提交后才释放 | 按提交顺序串行化,商用数据库广泛使用 |
3.6 实际数据库中的并发控制
| 数据库 | 方法 |
|---|---|
| SQL Server | 严格两阶段封锁 + 行级/页级/表级锁 + 意向锁 |
| MySQL (InnoDB) | MVCC(多版本并发控制)+ 两阶段锁 |
| Oracle | MVCC + 行级锁 |
MVCC 核心思想:保存数据的多个版本,读不加锁,写加锁,读操作读历史版本快照。大幅降低锁竞争。
四、恢复与备份
4.1 数据库故障分类
| 故障类型 | 是什么 | 例子 |
|---|---|---|
| 事务故障 | 事务没跑完就停了 | 用户取消、代码错误(除以零、约束违反) |
| 系统故障 | 系统停机,内存丢了,磁盘完好 | 断电、操作系统崩溃、CPU 故障 |
| 介质故障 | 存储设备物理损坏 | 硬盘坏道、磁盘损坏、自然灾害 |
| 其他故障 | 人为或外部破坏 | 黑客入侵、误删表、病毒篡改 |
4.2 恢复机制
是什么?
恢复 = 数据库故障后,把数据恢复到正确状态的过程。
核心手段是两个操作:
| 操作 | 是什么 | 针对什么场景 |
|---|---|---|
| UNDO | 撤销事务已做的所有更新 | 事务未完成(未提交),需要回滚到开始前状态 |
| REDO | 重做事务已提交的更新 | 事务已提交,但更新可能还在内存没写磁盘 |
为什么需要?
保证原子性和持久性:
-
原子性要求:没完成的事务,已经改的数据必须"吐出来"(UNDO)。
-
持久性要求:已完成的交易,就算天塌了也得保留(REDO)。
怎么用?
基于日志的恢复:
日志是记录所有事务操作的追加文件,存在稳固存储器 (如磁盘),遵循先写日志规则(WAL)。
日志记录格式:
<Ti, START> -- 事务开始
<Ti, X, V1, V2> -- 事务Ti把数据X从V1改成V2(V1=前映像,V2=后映像)
<Ti, COMMIT> -- 事务提交
<Ti, ROLLBACK> -- 事务回滚
<Checkpoint L> -- 检查点,L=活跃事务列表
恢复策略按故障类型:
| 故障类型 | 恢复策略 | 为什么 |
|---|---|---|
| 事务故障 | UNDO 该事务所有更新 | 没提交的事务不能留 |
| 系统故障 | UNDO 所有未完成事务 + REDO 所有已提交事务 | 内存丢了,已提交的要做持久,未完成的要撤销 |
| 介质故障 | 装最新备份 + REDO 备份后已提交事务 | 磁盘坏了,先恢复到备份点,再用日志补后续 |
恢复流程(无检查点):
1. 扫描日志,确定:
- REDO集 = 日志中有<COMMIT>的事务
- UNDO集 = 日志中只有<START>没有<COMMIT>的事务
2. UNDO阶段:从日志尾反向扫描,用前映像(V1)恢复
3. REDO阶段:从日志头正向扫描,用后映像(V2)重做
关键规则 :必须先 UNDO,再 REDO。如果顺序反过来,REDO 的结果可能被 UNDO 的前映像覆盖掉。
4.3 检查点(Checkpoint)
是什么?
检查点 = 周期性执行的一个"快照"操作,目的是缩短故障恢复时的日志扫描范围。
为什么需要?
没有检查点,每次恢复都要从头到尾扫描整个日志,日志越长恢复越慢。检查点把日志切成段,只需扫最近一段。
怎么用?
写检查点时做的三件事:
-
把日志缓冲区的所有日志记录写到磁盘。
-
把数据缓冲区的所有更新块写到磁盘。
-
在日志中写入
<Checkpoint L>,L 是当前所有未提交事务列表。
引入检查点后的恢复策略:
[检查点] ---- [T1提交] ---- [T2开始] ---- [故障点]
↑ ↑
扫描起点 恢复终点
| 事务情况 | 恢复操作 |
|---|---|
| 检查点前已提交 | 无需恢复(检查点已刷盘) |
| 检查点前开始,故障前已提交 | REDO |
| 检查点前开始,故障时未结束 | UNDO |
| 检查点后开始,故障前已提交 | REDO |
| 检查点后开始,故障时未结束 | UNDO |
4.4 备份
是什么?
备份 = 数据库的副本,用于介质故障后恢复。
为什么需要?
介质故障(硬盘损坏)时,日志和数据库文件可能一起没了,单靠日志不够,必须有备份副本作为恢复的起点。
怎么用?
备份类型对比:
| 类型 | 是什么 | 特点 |
|---|---|---|
| 静态备份 | 无事务运行时备份 | 副本一致,但需暂停业务 |
| 动态备份 | 和事务并发时备份 | 不影响业务,但必须配合日志 |
| 全备份 | 备份整个数据库 | 恢复简单,耗时长、占空间 |
| 增量备份 | 只备份上次备份后更新的数据 | 省时间省空间,恢复需按顺序应用 |
介质故障恢复步骤:
-
装入最新备份副本。
-
从备份时刻起扫描日志。
-
REDO 备份后所有已提交事务。
-
数据库恢复到故障前一致状态。
五、知识脉络图
事务管理与恢复
│
├── 事务(Transaction)
│ ├── 是什么:完整逻辑的操作序列
│ ├── 为什么:保证业务操作不"半成品"
│ └── 怎么用:BEGIN → 操作 → COMMIT/ROLLBACK
│
├── ACID 特性
│ ├── A(原子性)→ 恢复管理(UNDO)保证
│ ├── C(一致性)→ 应用逻辑 + 完整性约束保证
│ ├── I(隔离性)→ 并发控制(封锁协议)保证
│ └── D(持久性)→ 恢复管理(REDO + 日志)保证
│
├── 并发控制
│ ├── 为什么:提高吞吐量、减少响应时间
│ ├── 问题:读脏数据、不可重复读、丢失更新
│ ├── 标准:冲突可串行化(优先图判断)
│ └── 手段:封锁(S锁/X锁)+ 两阶段封锁协议
│
└── 恢复与备份
├── 故障分类:事务故障、系统故障、介质故障
├── 核心操作:UNDO(撤销)+ REDO(重做)
├── 依据:日志 + 先写日志规则(WAL)
├── 优化:检查点(缩短恢复扫描范围)
└── 兜底:备份(全量/增量,静态/动态)
六、一句话记忆
| 概念 | 一句话 |
|---|---|
| 事务 | 打包操作,要么全成,要么全撤 |
| 原子性 | 做一半出事了,把已经改的吐出来(UNDO) |
| 持久性 | 提交了就必须永远留着,断电也不怕(REDO + 日志) |
| 隔离性 | 多人同时干活,互相别打扰(封锁协议) |
| 并发控制 | 让大家一起干活,但结果要像排队干的一样正确 |
| 冲突可串行化 | 并发调度的结果等价于某个排队顺序 |
| 两阶段封锁 | 前半程只能加锁,后半程只能解锁 |
| 检查点 | 定期存档,故障后不用从头再来 |
| 先写日志 | 先记账,再改数据,改砸了有账本可查 |
总结基于《数据库系统概论》第10章导学材料整理