十、数据库规范化理论
10.1、规范化理论基本概念
函数依赖定义 :设 R(U,F)R(U, F)R(U,F) 是属性 UUU 上的一个关系模式,XXX 和 YYY 是 UUU 的子集,rrr 为 RRR 的任一关系。如果对于 rrr 中的任意两个元组 uuu、vvv,只要有 u[X]=v[X]u[X]=v[X]u[X]=v[X],就有 u[Y]=v[Y]u[Y]=v[Y]u[Y]=v[Y],则称 XXX 函数决定 YYY ,或称 YYY 函数依赖于 XXX ,记为 X→YX \rightarrow YX→Y。
示例 :学号 -> 成绩。学号是决定因素。
10.2、非规范化带来的问题
数据冗余:数据重复存储。
更新异常:修改一个数据需要修改多行,易导致数据不一致。
插入异常:应该插入的数据无法插入。
删除异常:不该删除的数据被删除了。
10.3、Armstrong 公理系统
对关系模式 R<U,F>R<U, F>R<U,F> 来说有以下的推理规则
10.3.1、基础规则
自反律 (Reflexivity) :若 Y⊆X⊆UY \subseteq X \subseteq UY⊆X⊆U,则 X→YX \rightarrow YX→Y 成立。
增广律 (Augmentation) :若 Z⊆UZ \subseteq UZ⊆U 且 X→YX \rightarrow YX→Y,则 XZ→YZXZ \rightarrow YZXZ→YZ 成立。
传递律 (Transitivity) :若 X→YX \rightarrow YX→Y 且 Y→ZY \rightarrow ZY→Z,则 X→ZX \rightarrow ZX→Z 成立。
10.3.2、推导规则
合并规则 :由 X→Y,X→ZX \rightarrow Y, X \rightarrow ZX→Y,X→Z,有 X→YZX \rightarrow YZX→YZ。
伪传递规则 :由 X→Y,WY→ZX \rightarrow Y, WY \rightarrow ZX→Y,WY→Z,有 XW→ZXW \rightarrow ZXW→Z。
分解规则 :由 X→YX \rightarrow YX→Y 及 Z⊆YZ \subseteq YZ⊆Y,有 X→ZX \rightarrow ZX→Z。
10.4、键(Key)的概念
候选键 (Candidate Key) :唯一标识元组,且无冗余(最小性)。
主键 (Primary Key) :从候选键中任选一个。
外键 (Foreign Key) :其他关系的主键。
主属性与非主属性 :组成候选码的属性就是主属性 ,其他的就是非主属性。
全码 (All-Key):关系模式的所有属性组是这个关系的候选码。
消除冗余属性
DBA选定
组成部分
不包含在候选键中
属性集合
超键/Super Key
唯一标识元组
候选键/Candidate Key
唯一 + 最小
主键/Primary Key
被选中的那个
主属性
非主属性
10.5、求候选键的方法(图论法)
能
不能
能
不能
开始求解候选键
绘制函数依赖有向图
寻找入度为0的属性集合 Set_A
Set_A 能否遍历
图中所有节点?
Set_A 即为候选键
选取中间节点 Node_M
(既有入度也有出度)
Set_B = Set_A + Node_M
Set_B 能否遍历
图中所有节点?
Set_B 为候选键
尝试其他中间节点组合
1)将关系模式的函数依赖关系用"有向图"的方式表示。
2)找入度为 0 的属性,并以该属性集合为起点,尝试遍历有向图。
3)若能正常遍历图中所有节点,则该属性集即为关系模式的候选键。
4)若入度为 0 的属性集不能遍历图中所有节点,则需要尝试性地将一些中间节点(既有入度,也有出度的节点)并入入度为 0 的属性集中,直至该集合能遍历所有节点,集合为候选键。
10.6、属性闭包 (X+X^+X+) 计算法
定义 :设 FFF 为函数依赖集,XF+{X}^+_FXF+ 是指所有被 XXX 逻辑蕴含的属性集合。
算法:
- 设 result=Xresult = Xresult=X。
- 遍历 FFF 中的依赖 A→BA \rightarrow BA→B,如果 A⊆resultA \subseteq resultA⊆result,则 result=result∪Bresult = result \cup Bresult=result∪B。
- 重复直到 resultresultresult 不再增长。
10.7、快速求候选键(分类法)
L 类 (Left) :只在函数依赖左边 出现的属性。→\rightarrow→ 必是候选键的成员。
R 类 (Right) :只在函数依赖右边 出现的属性。→\rightarrow→ 不可能是候选键的成员(除非是全码)。
N 类 (None) :在左右两边都没 出现的属性。→\rightarrow→ 必是候选键的成员(通常是独立的无关属性)。
LR 类 (Left & Right) :在左右两边都 出现的属性。→\rightarrow→ 可能是候选键的成员。
解题步骤:
- 令 K=L∪NK = L \cup NK=L∪N。
- 计算 KKK 的闭包 K+K^+K+。
- 若 K+K^+K+ 包含所有属性,则 KKK 是唯一候选键。
- 若 K+K^+K+ 不全,则依次尝试将 LRLRLR 类属性加入 KKK 中计算闭包,直至覆盖所有属性。
10.8、规范化与性能的权衡
"异常" (冗余、插入、删除、更新)是我们在逻辑设计阶段力求避免的,所以我们要拆分表(规范化)。
反规范化 :但是,作为架构师,在海量数据查询场景下(如数据仓库、高并发读),为了减少 Join 操作,我们可能会故意违反规范化,保留冗余数据(如订单表存一份商家名称)。这是"空间换时间"的经典架构决策。
十一、数据库范式
11.1、范式升级路线图
规范化过程
No
Yes
No
Yes
No
Yes
No
Yes
初始关系模式
是否所有属性
均为原子值?
非 1NF
第一范式 1NF
是否消除了非主属性
对码的部分依赖?
存在部分依赖
第二范式 2NF
是否消除了非主属性
对码的传递依赖?
存在传递依赖
第三范式 3NF
是否消除了主属性
对码的部分/传递依赖?
主属性内部存在依赖
BC范式
第一范式 (1NF) :属性值都是不可再分的原子值。
第二范式 (2NF) :在 1NF 基础上,消除非主属性对候选键的部分依赖。
第三范式 (3NF) :在 2NF 基础上,消除非主属性对候选键的传递依赖。
BC范式 (BCNF) :在 3NF 基础上,消除主属性对候选键的部分和传递依赖(即:每个依赖的决定因素必定包含某个候选码)。
注:4NF 是限制关系模式的属性间不允许有非平凡且非函数依赖的多值依赖。
11.2、第一范式 (1NF)
定义:所有域只包含原子值,即每个属性都是不可再分的数据项。
反例 :{系名称, 高级职称人数: {教授: x, 副教授: y}}。这里"高级职称人数"包含了两个子属性,违反 1NF。
11.3、第二范式 (2NF)
成绩表
联合决定
联合决定
单独决定
学号
成绩
课程号
学分
红色箭头表示部分依赖
非主属性'学分'只依赖于主键的一部分
定义 :当且仅当实体 E 是 1NF,且每一个非主属性完全依赖主键(不存在部分依赖)时。
反例模型 :(学号, 课程号, 成绩, 学分)。主键是 (学号, 课程号)。
问题 :学分 只依赖于 课程号(主键的一部分),属于部分依赖。
解决方案 :拆分为 R1(课程号, 学分) 和 R2(学号, 课程号, 成绩)。
11.4、第三范式 (3NF)
决定
决定
决定
学号
系号
系名
系位置
传递依赖:
学号 -> 系号 -> 系名
定义 :当且仅当实体 E 是 2NF,且 E 中没有非主属性传递依赖于码时。
反例模型 :(学号, 姓名, 系号, 系名, 系位置)。主键是 学号。
问题 :学号 -> 系号,系号 -> 系名。存在 学号 -> 系名 的传递依赖。
解决方案 :拆分为 R1(学号, 姓名, 系号) 和 R2(系号, 系名, 系位置)。
11.5、BC范式 (BCNF)
定义 :设 R 是一个关系模式,F 是它的依赖集,R 属于 BCNF 当且仅当其 F 中每个依赖的决定因素必定包含 R 的某个候选码。
BCNF 定律 :"所有的决定因素,必须是候选键(一把完整的钥匙)。"
反例模型 :STJ (Student, Teacher, Subject)。
- 规则:每个老师只教一门课 (T→JT \rightarrow JT→J);每门课有若干老师;学生选定某门课就对应一个固定老师。
- 候选键:(S,T)(S, T)(S,T) 和 (S,J)(S, J)(S,J)。
- 问题 :存在依赖 T→JT \rightarrow JT→J。这里 TTT(老师)决定了 JJJ(课程),但 TTT 不是候选键(它只是候选键的一部分)。
- 插入异常 :如果新来了一个老师,还没排课(没学生选),他的信息(老师-课程)插不进去,因为缺 SSS(主键不能为空)。
- 删除异常:如果选这门课的学生都退课了,删掉学生记录的同时,把"这个老师教这门课"的信息也误删了。
- 结论:虽然它是 3NF(没有非主属性),但它不是 BCNF。
关系模式 STJ (学生, 老师, 课程)
联合组成候选键
联合组成候选键
决定
❌ 决定因素 T 不是候选键
学生 S
课程 J
老师 T
候选键: S+J
11.6、概念辨析
11.6.1、部分依赖
A,B→CA, B \rightarrow CA,B→C,同时 A→CA \rightarrow CA→C。说明 CCC 只需要 AAA 就够了,不需要 BBB。即 CCC 部分依赖于 (A,B)(A, B)(A,B)。这是 2NF 要解决的问题。
识别技巧 :只要主键是组合键(由多个属性构成),就极大概率存在部分依赖,需要检查 2NF。
11.6.2、传递依赖
A→B,B→CA \rightarrow B, B \rightarrow CA→B,B→C。说明 CCC 是通过 BBB 间接依赖于 AAA 的。这是 3NF 要解决的问题。
识别技巧:只要表中存在"非主属性"决定"非主属性"的情况,就违反了 3NF。
11.6.3、3NF 与 BCNF 的区别
3NF :只盯着**"非主属性"看。只要非主属性都很规矩(既不部分依赖,也不传递依赖),那就是 3NF。它允许主属性**之间存在依赖混乱。
BCNF :是 3NF 的加强版 。它不仅要求非主属性规矩,还要求主属性也很规矩。
11.7、模式分解
11.7.1、保持函数依赖分解
定义 :设数据库模式 ρ={R1,...,Rk}\rho=\{R_1, ..., R_k\}ρ={R1,...,Rk} 是 R 的一个分解,F 是 R 上的函数依赖集。如果 {F1,F2,...,Fk}\{F_1, F_2, ..., F_k\}{F1,F2,...,Fk} 的并集与 F 是等价 的(即相互逻辑蕴涵),那么称分解 ρ\rhoρ 保持 FD。
检查过程
是
否
输入: 原模式 R, 依赖集 F
分解为 R1, R2
投影: 找出 R1 中的依赖 F1
投影: 找出 R2 中的依赖 F2
合并: F' = F1 ∪ F2
是否等价于 F?
保持函数依赖
不保持函数依赖
(丢失了约束)
实例分析:
- 例1 (保持) :F={A→B,B→C}F=\{A \rightarrow B, B \rightarrow C\}F={A→B,B→C}。分解为 (AB),(BC)(AB), (BC)(AB),(BC)。
- R1(AB)R_1(AB)R1(AB) 包含 A→BA \rightarrow BA→B。
- R2(BC)R_2(BC)R2(BC) 包含 B→CB \rightarrow CB→C。
- 原有的依赖都还在,保持。
- 例2 (不保持) :F={A→B,B→C,A→C}F=\{A \rightarrow B, B \rightarrow C, A \rightarrow C\}F={A→B,B→C,A→C}。分解为 (AB),(AC)(AB), (AC)(AB),(AC)。
- R1(AB)R_1(AB)R1(AB) 包含 A→BA \rightarrow BA→B。
- R2(AC)R_2(AC)R2(AC) 包含 A→CA \rightarrow CA→C。
- 丢失 :B→CB \rightarrow CB→C 丢失了(因为 B 和 C 不在同一张表里,且无法通过其他依赖推导出来)。
- 结果:不保持。
11.7.2、无损分解
有损:不能还原。
无损:可以还原。
定义 :将一个关系模式分解成若干个关系模式后,通过自然连接 和投影等运算仍能还原到原来的关系模式。
11.7.2.1、公式法
前提 :将关系模式 R 分解为 ρ={R1,R2}\rho = \{R_1, R_2\}ρ={R1,R2}。
判定公式:分解 ρ\rhoρ 具有无损连接性的充分必要条件是:
(R1∩R2)→(R1−R2)(R_1 \cap R_2) \rightarrow (R_1 - R_2)(R1∩R2)→(R1−R2)
或者
(R1∩R2)→(R2−R1)(R_1 \cap R_2) \rightarrow (R_2 - R_1)(R1∩R2)→(R2−R1)
I -> D1 成立
I -> D2 成立
都不成立
开始: 分解 R 为 R1 和 R2
计算交集 I = R1 ∩ R2
计算差集
D1 = R1 - R2
D2 = R2 - R1
判定:
公共属性 I 能否决定
D1 或 D2 ?
无损分解
(Lossless)
有损分解
(Lossy)
例题 :设 R=ABC,F={A→B}R=ABC, F=\{A \rightarrow B\}R=ABC,F={A→B}。
分解1 :ρ1={R1(AB),R2(AC)}\rho_1 = \{R_1(AB), R_2(AC)\}ρ1={R1(AB),R2(AC)}
- 交集:AAA。
- R1−R2=BR_1 - R_2 = BR1−R2=B。
- 判断:A→BA \rightarrow BA→B 成立吗?成立。
- 结果:无损分解。
分解2 :ρ2={R1(AB),R3(BC)}\rho_2 = \{R_1(AB), R_3(BC)\}ρ2={R1(AB),R3(BC)}
- 交集:BBB。
- R1−R3=AR_1 - R_3 = AR1−R3=A;R3−R1=CR_3 - R_1 = CR3−R1=C。
- 判断:B→AB \rightarrow AB→A?不成立。B→CB \rightarrow CB→C?不成立。
- 结果:有损分解(无法还原)。
11.7.2.2、表格法
题目背景:
给定关系模式 成绩 (学号, 姓名, 课程号, 课程名, 分数)。
函数依赖集 FFF:
- 学号→姓名学号 \rightarrow 姓名学号→姓名
- 课程号→课程名课程号 \rightarrow 课程名课程号→课程名
- (学号,课程号)→分数(学号, 课程号) \rightarrow 分数(学号,课程号)→分数
将其分解为 3 个子模式:
- R1 成绩:(学号, 课程号, 分数)
- R2 学生:(学号, 姓名)
- R3 课程:(课程号, 课程名)
问题:判断该分解是否为无损分解?
第一步:初始化表格
画一张表,列 代表所有属性,行代表分解后的各个子模式。
- 如果某个子模式包含该属性,填入 ✔。
- 如果不包含,填入 ×。
| 学号 | 姓名 | 课程号 | 课程名 | 分数 | |
|---|---|---|---|---|---|
| 成绩 (R1) | ✔ | ×\times× | ✔ | ×\times× | ✔ |
| 学生 (R2) | ✔ | ✔ | ×\times× | ×\times× | ×\times× |
| 课程 (R3) | ×\times× | ×\times× | ✔ | ✔ | ×\times× |
第二步:根据函数依赖"填空"
依次扫描函数依赖集 FFF,如果在某个属性列上,多行都打钩了(即值相等),那么它们在依赖推导出的那一列上的值也必须相等(把 ×\times× 变成 ✔)。
1. 应用依赖:学号→姓名学号 \rightarrow 姓名学号→姓名
- 观察 :"学号"列,R1 和 R2 都有 ✔✔(说明它们针对同一个学号)。
- 推导:既然学号相同,那么"姓名"也必须相同。
- 操作 :R2 在"姓名"列是 ✔,所以我们要把 R1 的"姓名"列也改成 ✔。
2. 应用依赖:课程号→课程名课程号 \rightarrow 课程名课程号→课程名
- 观察 :"课程号"列,R1 和 R3 都有 ✔。
- 推导:既然课程号相同,那么"课程名"也必须相同。
- 操作 :R3 在"课程名"列是 ✔,所以我们要把 R1 的"课程名"列也改成 ✔。
| 学号 | 姓名 | 课程号 | 课程名 | 分数 | |
|---|---|---|---|---|---|
| 成绩 (R1) | ✔ | ✔ (新补) | ✔ | ✔ (新补) | ✔ |
| 学生 (R2) | ✔ | ✔ | ×\times× | ×\times× | ×\times× |
| 课程 (R3) | ×\times× | ×\times× | ✔ | ✔ | ×\times× |
判定标准:
只要表格中有一行全部变成了 ✔(即该行不再有 ×\times×),则证明该分解是无损连接。
本题结果:
- 观察上表,第一行(成绩 R1)的所有列现在都是 ✔ 了。
- 结论:该分解是无损分解。
11.8、反规范化
11.8.1、核心概念
反规范化 (又称逆规范化、违规化)是指在数据库设计中,为了提高查询性能 ,有意违反规范化原则(如 3NF),通过增加数据冗余 、合并表等手段,以减少连接操作(Join)的过程。
- 本质 :空间换时间。
- 适用场景 :读多写少、数据量大、对查询响应速度要求极高的系统(如数据仓库、报表系统、高并发互联网应用)。
- 所属阶段 :属于逻辑结构设计阶段(因为它改变了数据的Schema结构)。
反规范化 (读优化)
合并
合并
解决
解决
解决
规范化 (写优化)
拆分
拆分
拆分
大宽表
表A
表B
表C
目标: 消除冗余
避免异常
节省空间
表A
冗余大表
表B
目标: 减少Join
提高查询速度
空间换时间
增加冗余列
增加派生列
表分割
风险: 数据不一致
触发器
应用层同步
批处理
11.8.2、常见的反规范化技术手段
| 技术手段 | 说明 | 典型场景 |
|---|---|---|
| 增加冗余列 | 在一个表中复制另一个表的列。 | 在"订单表"中存储"客户姓名",避免每次查询订单都 Join "客户表"。 |
| 增加派生列 | 增加可以通过其他列计算得出的列。 | 在"订单明细"中有"单价"和"数量",额外增加"总价"列,避免查询时实时计算。 |
| 重新组表 | 把拆分过于细碎的表合并。 | 将 1:1 扩展表合并回主表,减少 Join。 |
| 分割表 | 水平分割 :按行分(如按年份分表、按地区分表)。 垂直分割:按列分(将常用列和冷门大字段分开)。 | 长沙的用户存在长沙表,上海的用户存在上海表(分库分表的基础)。 |
11.8.3、优缺点权衡
| 维度 | 优点 (Pros) | 缺点 (Cons) |
|---|---|---|
| 查询性能 | 连接操作(Join)少,检索容易,检索快,统计快。 | - |
| 写入性能 | - | 插入、更新、删除开销更大(因为要修改多份数据)。 |
| 存储成本 | - | 数据冗余,需要更大的存储空间。 |
| 数据质量 | - | 数据不一致风险(修改了A处,忘了改B处)。 |
| 开发维护 | 只需要查单表,SQL简单。 | 更新和插入的代码更难写(需要保证同步)。 |
11.8.4、数据不一致的解决方案
11.8.4.1、触发器 (Triggers)
利用数据库自身的触发器机制,当源数据修改时,同步更新冗余数据。
优点:实时性强。
缺点:影响写性能,逻辑隐藏在库中不易维护。
11.8.4.2、应用程序同步
在应用代码(Service层)中,在一个事务里同时更新多张表。
优点:逻辑清晰。
缺点:代码复杂度增加。
11.8.4.3、批处理 (Batch Processing)
定期(如每天凌晨)运行脚本同步数据。
适用:对实时性要求不高的统计报表。
11.8.4.4、物化视图 (Materialized View)
数据库提供的特性,自动维护查询结果的物理副本。
十二、数据库安全性控制
12.1、**"洋葱模型"**的安全防御体系
数据库安全防御体系
(1) 用户标识与鉴定
(门禁: 账号/密码)
(2) 存取控制
(权限: DAC/MAC)
(3) 视图与加密
(隐蔽: 隐藏敏感数据)
核心数据
(5) 审计 Audit
(监控摄像机: 记录所有操作)
用户
12.2、用户标识和鉴定
地位:最外层的安全保护措施。
手段:用户账户、口令(Password)、随机数检验等。
通俗理解:类似于公司的"门禁卡",证明你是谁。
12.3、存取控制
定义:对用户进行授权。
要素:
- 操作类型 :如
SELECT,INSERT,DELETE,UPDATE。 - 数据对象:数据范围(表、列、行)。
SQL (对应 GRANT / REVOKE 语句)。
12.3.1、自主存取控制 (DAC, Discretionary Access Control)
用户对自己创建的数据有控制权,并且可以将权限转授给别人。
SQL实现 :GRANT SELECT ON TableA TO UserB WITH GRANT OPTION。
缺点:权限容易扩散,安全性较低。
12.3.2、强制存取控制 (MAC, Mandatory Access Control)
系统强制规定权限,用户不能转授。
机制 :给数据打标签(绝密、机密、公开),给用户打标签。只有"用户级别 ≥\ge≥ 数据级别"才能读。
优点:安全性极高(军用级)。
12.4、密码存储和传输
对象:对远程终端信息、敏感数据进行加密。
通俗理解:防止数据在传输过程中被截获(中间人攻击)。
12.5、视图的保护
机制:对视图进行授权,而不是直接对基表授权。
作用:隐藏敏感字段(例如:只给财务看"工资列",不给其他部门看)。
12.6、审计
机制 :使用专用文件或数据库,自动将用户对数据库的所有操作记录下来。
作用:事后追责,"谁在什么时间做了什么坏事"。
审计虽然能追踪责任,但会严重降低数据库性能(因为每一步操作都要写日志)。
架构决策:通常只开启关键业务(如资金变动、权限变更)的审计,而不是全量审计。
12.7、可靠
持久性、可靠性、备份 :这其实属于数据库恢复技术的范畴,但常与安全性一起考。安全性防"坏人",可靠性/备份防"故障"。
12.8、SQL 注入与参数化查询
防御:使用**预编译语句(PreparedStatement)**或存储过程,避免拼接 SQL 字符串。
十三、数据库并发控制
13.1、核心基础:事务
| 特性 | 英文 | 含义 | 关键保障机制 |
|---|---|---|---|
| 原子性 | Atomicity | 要么全做,要么全不做(All or Nothing)。 | Undo Log (回滚日志) |
| 一致性 | Consistency | 事务前后,数据必须保持一致状态(如转账总额不变)。 | 完整性约束 + 其他三个特性 |
| 隔离性 | Isolation | 并发事务之间互不干扰。 | 锁 (Locking) / MVCC |
| 持久性 | Durability | 一旦提交,修改就是永久的(即使断电)。 | Redo Log (重做日志) |
13.2、并发产生的三大问题
13.2.1、丢失更新 (Lost Update)
场景:T1 和 T2 同时读入 A=10,T1 改为 11 提交,T2 改为 12 提交。T1 的修改被 T2 覆盖了。
后果:严重的数据错误,必须绝对避免。
13.2.2、读"脏"数据 (Dirty Read)
场景:T1 修改 A=20(未提交),T2 读到了 A=20。结果 T1 回滚了(A变回10),T2 读到的 20 就是脏数据。
后果:使用了无效数据。
13.2.3、不可重复读 (Non-repeatable Read)
场景:T1 读 A=10。此时 T2 将 A 修改为 20 并提交。T1 再次读 A,变成了 20。
后果 :同一个事务内,两次读取结果不一致(针对修改操作)。
13.2.4、幻读 (Phantom Read)
T1 读只有3行记录,T2 插入了一行,T1 再读发现变成了4行(针对新增/删除操作)。
13.3、解决方案:封锁协议
13.3.1、锁的类型
X 锁 (Exclusive Lock, 排他锁/写锁):
- 如果 T 对 A 加了 X 锁,T 可以读写 A。
- 其他人 :不能加任何锁(不能读也不能写)。
S 锁 (Shared Lock, 共享锁/读锁):
- 如果 T 对 A 加了 S 锁,T 只能读 A。
- 其他人 :只能加 S 锁(可以一起读),不能加 X 锁(不能写)。
13.3.2、三级封锁协议
| 协议级别 | 规则 (怎么加锁) | 解决的问题 | 遗留的问题 |
|---|---|---|---|
| 一级封锁 | 修改数据前加 X锁,直到事务结束才释放。 | ✅ 丢失更新 | 脏读、不可重复读 |
| 二级封锁 | 一级 + 读取数据前加 S锁 ,读完立刻释放。 | ✅ 丢失更新 ✅ 读脏数据 | 不可重复读 |
| 三级封锁 | 一级 + 读取数据前加 S锁 ,直到事务结束才释放。 | ✅ 丢失更新 ✅ 读脏数据 ✅ 不可重复读 | - |
13.4、一级封锁协议:解决"丢失更新"
规则 :事务 T 在修改数据 前必须加 X锁 ,直到事务结束才释放。
【场景还原】 假设 T1 和 T2 都要把 A (原本是10) 加 1。
- 如果没有协议:T1 读 10,T2 读 10。T1 改为 11 提交。T2 改为 11 提交。结果是 11(T1 的更新丢了)。
- 有一级协议后 :
- T1 想改 A,先申请 X锁。成功。
- T2 也想改 A,申请 X锁。
- 冲突! 因为 T1 拿着 X 锁,T2 的申请被拒绝,T2 进入等待队列。
- T1 改完 A=11,提交事务,释放 X 锁。
- T2 终于拿到了 X 锁,读取 A(此时已经是11了),改为 12,提交。
- 结果 :A = 12。丢失更新被解决。
13.5、二级封锁协议:解决"读脏数据"
规则 :在一级基础上,读取数据 前必须加 S锁 ,读完立刻释放。
【场景还原】
假设 T1 修改了 A=20(还没提交),T2 想读 A。
- 如果没有协议:T2 读到了 20。结果 T1 后来回滚了,A 变回 10。T2 读了个寂寞(脏数据)。
- 有二级协议后 :
- T1 修改 A,根据一级协议,T1 必须加 X锁。
- T2 想读 A,根据二级协议,T2 必须申请 S锁。
- 冲突! T1 的 X 锁排斥 T2 的 S 锁。
- 机制生效 :T2 读不了!只能等着 T1 完事。
- 只有等 T1 提交(A确实变成20)或者回滚(A变回10)并释放 X 锁后,T2 才能拿到 S 锁去读。
- 结果 :T2 永远读不到"未提交"的数据。脏读被解决。
【可重复读无法避免的场景还原】
目标:T1 想读两次 A,希望两次读到的结果一样(即可重复读)。
协议背景 :二级封锁协议 (写加 X 锁长持,读加 S 锁,读完立刻释放)。
- 第一步(T1 读数据):
- T1 发起读取 A(假设 A=10)。
- 遵守二级协议 :T1 申请 S锁。
- 读操作:T1 读到了 A=10。
- 关键动作 :读完了,T1 立刻释放 S锁。(注意!这时候 A 身上已经没有任何锁了,变成"裸奔"状态)。
- 第二步(T2 趁虚而入):
- T1 的事务还没结束(可能在处理其他逻辑),此时 T2 进来了。
- T2 想把 A 改成 20。
- 遵守一级协议 :T2 申请 X锁。
- 能申请到吗? 能! 因为刚才第一步里,T1 已经把 S 锁扔掉了。A 身上没锁,T2 顺利加上 X 锁。
- T2 修改 A=20,提交事务,释放 X 锁。
- 第三步(T1 再次读数据):
- T1 的事务还在进行,它想确认一下 A 的值。
- 遵守二级协议 :T1 再次申请 S锁。
- 能申请到吗? 能,因为 T2 已经搞完跑路了。
- 读操作 :T1 读到了 A=20。
结局 : T1 在同一个事务里,第一次读是 10,第二次读是 20。数据变了! 这就是"不可重复读"。
13.6、三级封锁协议:解决"不可重复读"
规则 :在一级基础上,读取数据 前必须加 S锁 ,直到事务结束才释放。
【场景还原】 假设 T1 先读 A=10,想过一会儿再读一次验证;T2 想把 A 改成 20。
- 有三级协议后 :
- T1 第一次读 A,申请 S锁。
- 关键点 :读完后,T1 不释放 S锁,一直攥在手里。
- T2 想改 A,申请 X锁。
- 冲突! 虽然 S 锁允许别人读,但绝对不允许别人写(X锁)。T2 发现 A 上面有 S 锁,只能等待。
- T1 第二次读 A,因为没人能改动 A,读出来肯定还是 10。
- T1 事务结束,释放 S 锁。T2 这才拿到 X 锁去修改。
- 结果 :在 T1 整个事务期间,没人能修改它读过的数据。不可重复读被解决。
13.7、问题 和协议复现图
封锁协议解决方案
并发产生的问题
解决
解决
解决
解决
解决
解决
重要结论
保证
两段锁协议 2PL
可串行化调度
(1) 丢失更新
(2) 读脏数据
(3) 不可重复读
一级封锁协议
(写加X锁, 事务结束放)
二级封锁协议
(一级 + 读加S锁, 读完马上放)
三级封锁协议
(一级 + 读加S锁, 事务结束放)
13.8、两段锁协议 (2PL)
定义:所有事务必须分两个阶段对数据项加锁和解锁。
扩展阶段:获得锁,不能释放锁。
收缩阶段:释放锁,不能获得锁。
结论 :若并发执行的所有事务均遵守两段锁协议,则对这些事务的任何并发调度策略都是"可串行化"的。
13.9、死锁 (Deadlock)
当两个事务相互等待对方释放锁时,就会形成死锁。
- 预防死锁 :
- 一次封锁法(一次性申请所有锁,效率低)。
- 顺序封锁法(按固定顺序申请锁)。
- 死锁的诊断与解除 :
- 超时法。
- 等待图法 (Wait-for Graph) :如果图中出现回路,说明有死锁。
- 解除 :选择一个处理代价最小的事务进行撤销 (Rollback),释放它的锁给别人用。
十四、分布式事务管理
14.1、核心概念
在分布式系统中,为了保证跨多个节点的操作要么全部成功,要么全部失败(ACID特性),引入了协调者 (Coordinator) 来统一调度所有参与者 (Participant) 的执行逻辑。
最经典的一致性协议就是 2PC (Two-Phase Commit)。
14.2、2PC协议流程
14.2.1、第一阶段:表决阶段 (Voting Phase)
- 目的:形成一个共同的决定。
- 动作:协调者问参与者:"你们准备好提交了吗?"(Prepare)。
- 参与者行为:执行事务操作,将 Undo/Redo 信息写入日志,但不提交。如果成功写盘则返回"Ready/Yes",否则返回"Abort/No"。
14.2.2、第二阶段:执行阶段 (Execution Phase)
- 目的:实现协调者的决定。
- 动作 :
- 如果所有人都说 Yes →\rightarrow→ 发送 Global Commit。
- 只要有一个人说 No →\rightarrow→ 发送 Global Abort。
- 参与者行为:根据指令正式提交或回滚,并释放资源(锁)。
14.3、2PC全局提交规则(铁律)
一票否决权 :只要有一个 参与者撤销事务(或超时未响应),协调者就必须做出全局撤销 (Global Abort) 的决定。
全票通过权 :只有所有 参与者都同意提交事务,协调者才能做出全局提交 (Global Commit) 的决定。
14.4、2PC详细交互与日志机制
在 2PC 中,写日志 (Write Log) 的时机非常关键,这是为了在系统崩溃后能恢复状态。
14.4.1、协调者 (Coordinator) 的逻辑
- Start :写入
begin_commit日志,发送"准备提交"请求,进入等待 (Wait) 状态。 - Decision :
- 若收集齐所有"Yes" →\rightarrow→ 写入
commit日志 →\rightarrow→ 发送"全局提交"。 - 若收到任一"No"或超时 →\rightarrow→ 写入
abort日志 →\rightarrow→ 发送"全局撤销"。
- 若收集齐所有"Yes" →\rightarrow→ 写入
- End :收到所有参与者的确认 (Ack) 后,写入
end_trans日志,事务结束。
14.4.2、参与者 (Participant) 的逻辑
- Receive:收到"准备提交"请求。
- Check :
- 若无法执行 →\rightarrow→ 写入
abort日志 →\rightarrow→ 发送"建议撤销 (No)"。 - 若执行成功 →\rightarrow→ 写入
ready日志 →\rightarrow→ 发送"建议提交 (Yes)" →\rightarrow→ 进入就绪 (Ready) 状态(注意:此时参与者处于阻塞状态,必须等待协调者的命令)。
- 若无法执行 →\rightarrow→ 写入
- Action :
- 收到 Commit →\rightarrow→ 写入
commit日志 →\rightarrow→ 提交事务 →\rightarrow→ 发送 Ack。 - 收到 Abort →\rightarrow→ 写入
abort日志 →\rightarrow→ 回滚事务 →\rightarrow→ 发送 Ack。
- 收到 Commit →\rightarrow→ 写入
参与者 (Participant) 协调者 (Coordinator) 参与者 (Participant) 协调者 (Coordinator) 1. 写 begin_commit 日志 进入 WAIT 状态 写 abort 日志 写 ready 日志 进入 READY 状态 (阻塞等待) alt [参与者执行失败] [参与者执行成功] 决策阶段 (一票否决) 4. 写 commit 日志 写 commit 日志 ->> 提交 ->> 释放资源 4. 写 abort 日志 写 abort 日志 ->> 回滚 ->> 释放资源 alt [决定提交 (全票Yes)] [决定回滚 (有No或超时)] 收到所有Ack ->> 写 end_trans 日志 2. 发送: 准备提交? (Prepare) 3. 回复: 不行 (No) 3. 回复: 可以 (Yes) 5. 发送: 全局提交 (Global Commit) 6. 回复: 完成 (Ack) 5. 发送: 全局回滚 (Global Abort) 6. 回复: 完成 (Ack)
十五、数据备份
15.1、备份方式分类
| 特性 | 冷备份 (Cold/Static Backup) | 热备份 (Hot/Dynamic Backup) |
|---|---|---|
| 别名 | 静态备份 | 动态备份 |
| 定义 | 关闭数据库,在停止状态下复制文件。 | 数据库正常运行状态下,利用备份软件进行备份。 |
| 优点 | 1. 简单 :只需复制文件。 2. 快速 :直接文件拷贝速度快。 3. 安全:容易归档,低度维护。 | 1. 高可用 :无需停机 ,业务不中断。 2. 精度 :可达到秒级恢复。 3. 范围:可对几乎所有数据库实体恢复。 |
| 缺点 | 1. 需停机 :业务必须中断。 2. 只能恢复到某个时间点。 3. 速度受限于磁盘IO。 | 1. 维护复杂:不能出错,否则备份无效。 2. 若失败,后果严重。 |
| 场景 | 对业务连续性要求不高的系统(如内部统计系统)。 | 7x24小时核心业务系统(如电商、银行)。 |
15.2、备份策略分类
| 备份类型 | 定义 (关键区别) | 备份速度/空间 | 恢复速度 |
|---|---|---|---|
| 完全备份 (Full Backup) | 备份所有数据。 | 🐢 最慢 / 最大 | 🚀 最快 (只需导入这一份) |
| 差量备份 (Differential) | 备份上一次完全备份之后变化的数据。 | ⚖️ 中等 | ⚖️ 中等 (需导入:完全备份 + 最新的一份差量) |
| 增量备份 (Incremental) | 备份上一次备份 (无论是全备、差备还是增备) 之后变化的数据。 | 🚀 最快 / 最小 | 🐢 最慢 (需导入:完全备份 + 所有中间的增量备份) |
15.3、备份结构概览
备份策略_按数据量
备份方式_按状态
冷备份: 关机, 简单, 有宕机
热备份: 开机, 复杂, 无宕机
完全备份: 所有数据
差量备份: 差异自上一次'全备'
增量备份: 差异自上一次'任何备份'
备份最慢, 恢复最快
备份中等, 恢复中等
备份最快, 恢复最慢
数据库
日志文件
15.4、日志文件
定义:针对数据库改变所作的记录,独立保存在文件中。
作用:是数据库恢复的核心依据(配合 Checkpoint、Redo/Undo 使用)。
考点 :备份不仅要备数据文件 (.mdf/.dbf),必须同时备份日志文件,否则只能恢复到过去的时间点,无法恢复到故障前一秒的状态。
15.5、备份结论
15.5.1、恢复速度排序
完全备份 > 差量备份 > 增量备份。
理由:完全备份恢复时文件最少;增量备份恢复时像"拼图",要按顺序一份份拼回去,缺一不可。
15.5.2、备份速度排序
增量备份 > 差量备份 > 完全备份。
理由:增量备份的数据量通常最小。
15.5.3、推荐策略
通常建议 "每周一次全备 + 每日一次增量/差量"。
如果空间紧张:选增量。
如果恢复时间要求高 (RTO短):选差量(因为恢复时不需要重放那么多份日志)。
十六、数据库故障与恢复体系
16.1、故障分类与恢复策略表
| 故障类型 | 典型原因 | 谁来恢复? | 恢复策略 (核心考点) |
|---|---|---|---|
| 事务内部故障 (可预期) | 逻辑判断(如余额不足)。 | 程序员 | 在代码中预先设置 ROLLBACK 语句回滚。 |
| 事务内部故障 (不可预期) | 算术溢出、死锁、违反约束。 | DBMS | 自动撤销 (UNDO) 该事务对数据库的修改,回退到初始状态。 |
| 系统故障 (Soft Crash) | CPU断电、操作系统崩溃。 (内存数据丢,磁盘还在) | DBMS | 重启时自动进行。 1. 撤销 (UNDO) 未提交的事务。 2. 重做 (REDO) 已提交的事务。 *通常配合**检查点 (Checkpoint)*使用。 |
| 介质故障 (Hard Crash) | 磁盘损坏、磁头碰撞。 (物理存储坏了) | DBA | 人工介入 。 1. 装入最新的数据库备份 (Backup) 。 2. 装入日志文件,重做 (REDO) 备份后的所有已提交业务。 |
16.2、恢复流程逻辑图
有 Commit 记录
无 Commit 记录
系统故障重启 / 恢复开始
正向扫描日志文件
检查事务状态
加入 REDO 队列
(重做)
加入 UNDO 队列
(撤销)
执行重做:
将日志的新值写入库
执行撤销:
将日志的旧值写回库
系统恢复完成
对外开放服务
16.3、UNDO (撤销)
对象 :故障发生时,尚未 Commit 的活跃事务。
逻辑:原子性要求"要么全做,要么全不做"。既然没做完就被打断了,那就必须抹去它所有的痕迹。
16.4、REDO (重做)
对象 :故障发生时,已经 Commit 的事务。
逻辑:持久性要求"一旦提交,永久生效"。即使数据还在内存里没来得及刷到磁盘就断电了,重启后也要根据日志把它重新写进去。
16.5、DBA (数据库管理员)
只有 介质故障 (硬盘坏了) 才需要 DBA 物理出现去换硬盘、导备份。其他故障都是 DBMS 自动完成的。
十七、数据库性能优化
17.1、优化体系总览
数据库优化不仅仅是改改 SQL 语句,而是一个从硬件到软件的系统工程 。课件将其分为两大类:集中式数据库优化 和 分布式数据库优化。
17.2、集中式数据库优化
17.2.1、硬件与系统级优化
硬件系统:
- CPU:计算密集型操作(如复杂计算、聚合)。
- 内存:数据库缓存(Buffer Pool)的大小直接影响性能。
- I/O (硬盘/阵列):数据库的瓶颈通常在磁盘 I/O。使用 SSD、RAID 10 等技术。
- 网络:带宽和延迟。
系统软件:
- 调整操作系统参数,如进程优先级 、CPU 使用权 、内存分配策略(避免 Swap 交换分区的使用)。
17.2.2、数据库设计优化
表与视图:
- 表的规划:合理范式与反规范化。
- 物化视图 (Materialized View) : 与普通视图不同,物化视图是实际存储数据 的。它预先计算并保存复杂查询的结果,以空间换时间,特别适合用于 OLAP(报表)系统。
索引 (Index):
- 原则 : 常查询 的列 →\rightarrow→ 建索引 ;常修改 的列 →\rightarrow→ 避免建索引(因为维护索引有开销)。
SQL 优化:(代码级优化的黄金法则)
- 子查询优化 :用不相关 子查询替代相关子查询(相关子查询会执行多次)。
- 列的筛选 :只检索需要的列,避免
SELECT \*。 - 条件转换 :用带
IN的条件子句等价替换OR子句(OR往往会导致索引失效)。 - 事务控制 :经常提交 (COMMIT),以尽早释放锁,提高并发性。
- 减少连接:尽可能减少多表查询(Join)。
17.2.3、应用软件优化
数据库连接池 (Connection Pool):避免频繁创建和销毁数据库连接(握手开销大),实现连接复用。
17.3、分布式数据库优化
分布式环境下,优化的核心矛盾发生了转移:从"磁盘I/O"变成了"网络通信"。
17.3.1、核心目标:降低通信代价
分布式查询的主要时间开销在数据在不同节点间的传输。
- 全局查询树的变换:优化执行计划,减少跨节点数据传输。
- 多副本策略:将数据复制到多个节点,实现"就近读取"。
- 查询树的分解:将大查询拆解分发。
17.3.2、关键技术:半连接 (Semi-Join)
场景:表 A 在节点 1,表 B 在节点 2,要做 Join。
笨办法:把表 B 全传给节点 1 进行 Join。
半连接优化:
- 节点 1 只把表 A 的 Join Key (连接键) 传给节点 2。
- 节点 2 筛选出匹配的行。
- 节点 2 只把匹配成功的数据传回节点 1。
效果:大幅减少了网络传输的数据量。
17.4、完整优化链条
数据库性能优化
集中式优化
分布式优化
硬件: CPU, 内存, I/O
系统: 参数调整
应用: 连接池
数据库设计 (重点)
表/物化视图
索引: 读多建, 写多避
SQL优化
不相关子查询
只查需要的列
用IN代替OR
尽早Commit释放锁
通信代价 (核心瓶颈)
全局查询树变换
多副本策略
半连接 Semi-Join
十八、NoSQL
18.1、核心概念与对比
NoSQL 泛指非关系型数据库 ,它的出现不是为了取代 SQL,而是为了解决传统关系型数据库在海量数据、高并发、多变结构场景下的瓶颈。
架构师视角:RDBMS 追求的是"规矩和准确",NoSQL 追求的是"速度和灵活"。
| 维度 | 关系型数据库 (RDBMS) | NoSQL 数据库 |
|---|---|---|
| 应用领域 | 面向通用领域 (ERP, OA, 银行)。 | 特定应用领域 (社交、电商秒杀、日志)。 |
| 数据结构 | 结构化 (二维表),Schema 必须预定义。 | 非结构化 (Key-Value, 文档, 图),Schema 灵活。 |
| 扩展方式 | 向上扩展 (Scale-up):买更贵的服务器。 | 向外扩展 (Scale-out):加更多的普通机器 (集群)。 |
| 并发性能 | 支持并发,但性能受限于锁机制。 | 高并发,读写性能极高。 |
| 事务支持 | 高事务性 (ACID),强一致性。 | 弱事务性 (BASE),最终一致性。 |
| 数据量 | 有限数据 (TB级)。 | 海量数据 (PB级)。 |
18.2、NoSQL 的四大分类
| 分类 | 数据模型 | 典型代表 | 优缺点 | 适用场景 |
|---|---|---|---|---|
| 键值存储 (Key-Value) | Map<Key, Value> 哈希表 |
Redis, Memcached | 优 :查找速度最快 (O(1)O(1)O(1))。 缺:数据无结构,只适合简单的存取。 | 内容缓存 (Session, 购物车)、高访问负载、日志系统。 |
| 列式存储 (Column-Family) | 以列簇为单位存储。 | HBase, Cassandra | 优 :查找速度快,扩展性强,适合分布式。 缺:功能相对局限。 | 海量数据分布式存储 (如 Hadoop 生态)、分布式文件系统。 |
| 文档型 (Document) | Key-Value 的变种。 Value 是 JSON/XML。 | MongoDB, CouchDB | 优 :结构灵活 (Schema-free),表结构可变。 缺:缺乏统一查询语法。 | Web应用 (日志, 评论, 用户画像),数据结构经常变化的业务。 |
| 图形数据库 (Graph) | 图结构 (节点+边)。 | Neo4J | 优 :擅长处理复杂关系 (如最短路径)。 缺:难以做分布式集群。 | 社交网络 (关注关系)、推荐系统、知识图谱。 |
18.3、NoSQL图谱
NoSQL 四大金刚
SQL vs NoSQL
RDBMS: 强一致, 结构化
垂直扩展
NoSQL: 高并发, 非结构
水平扩展
(1) 键值 Key-Value
(2) 文档 Document
(3) 列族 Column
(4) 图 Graph
Redis
缓存/秒杀
MongoDB
Web应用/用户画像
HBase
海量数据/日志分析
Neo4j
社交关系/推荐
18.4、决策逻辑
缓存层选型 :只要看到"提高读性能"、"Session共享"、"排行榜",首选 Redis (Key-Value)。
大数据存储选型 :只要看到"海量日志"、"PB级数据"、"Hadoop",首选 HBase (列式)。
灵活业务选型 :只要看到"字段不固定"、"快速迭代的互联网应用",首选 MongoDB (文档型)。
复杂关系选型 :只要看到"好友推荐"、"六度人脉",首选 Neo4J (图库)。
十九、联邦数据库系统 (FDBS)
19.1、核心定义
联邦数据库系统 (FDBS) :是一个彼此协作却又相互独立的成员数据库 (CDBS) 的集合。
成员数据库 (CDBS, Component Database System):构成联邦的各个子节点数据库。
联邦数据库管理系统 (FDBMS):对该系统整体提供控制和协同操作的软件。
本质:将成员数据库系统按不同程度进行集成,对外提供统一的访问接口,但内部各成员依然保持独立。
19.2、四大核心特征
19.2.1、分布性 (Distribution)
数据在物理上是分布在不同网络节点上的。
19.2.2、异构性 (Heterogeneity)
成员数据库可以是不同的类型(有的存文本文件 ,有的是 MySQL ,有的是 Oracle)。
硬件平台、操作系统、DBMS 都可以不同。
19.2.3、自治性 (Autonomy)
成员数据库虽然加入联邦,但依然保留对自己数据的控制权。它们可以独立运行,服务于原本的本地应用,不受联邦系统的完全支配。
19.2.4、透明性 (Transparency)
用户在使用联邦数据库时,感觉不到数据的分布、异构和底层实现细节,就像在使用一个集中式数据库一样。
19.3、分类
根据耦合程度的不同,FDBS 分为:
- 紧耦合 (Tightly Coupled):有统一的全局模式,管理较严格。
- 松耦合 (Loosely Coupled):没有统一的全局模式,各成员更自由,通常通过多数据库语言进行互操作。
19.4、FDBMS 统一管理底层异构数据源的架构
底层异构数据源 (CDBS)
统一查询
协同操作
协同操作
协同操作
协同操作
用户/应用程序
联邦数据库管理系统 FDBMS
MySQL
Oracle
文本文件
...