3.6 关系数据库的规范化
数据库规范化与并发控制:一份修正后的完整笔记
本篇整理自常见教材(如王珊《数据库系统概论》)的核心知识点,修正了若干易错表述,并为每条关键概念配了通俗样例。可直接作为复习提纲或博客正文使用。
一、函数依赖(Functional Dependency)
1. 定义
设关系模式 R(U),X、Y 是 U 的子集。若 R 中任意两个元组在 X 上的值相等时,它们在 Y 上的值也必然相等,则称 "X 函数确定 Y" 或 "Y 函数依赖于 X",记作 X -> Y。
注意:函数依赖是语义层面的约束(由现实世界规则决定),而不是某个具体的数学公式。像 "Y = X * X" 这种算术表达式并不是函数依赖的例子;正确的例子是「学号 -> 姓名」——因为同一个学号只能对应一个学生姓名。
更多通俗样例:
- 「身份证号 -> 姓名」:同一个人不可能有两个名字。
- 「手机号 -> 用户名」:一个手机号只注册一个账号。
- 「商品编号 -> 商品名称」:同一款商品只有一个名字。
- 「车牌号 -> 车主姓名」:一辆车登记在一个人名下。
反例(不成立的情况):
- 「姓名 -> 学号」不成立,因为可能有两个同名学生。
- 「成绩 -> 学生姓名」不成立,因为不同学生可能考相同分数。
2. 平凡与非平凡
- 平凡函数依赖:若 Y 包含于 X,则 X -> Y 必然成立(例如 (学号, 姓名) -> 学号)。这类依赖不提供任何新信息。
- 非平凡函数依赖:Y 不包含于 X。我们讨论的依赖基本都是这种。
通俗理解:平凡依赖就像说"我的身份证号码+姓名 能确定我的身份证号码"——这是废话,因为左边已经包含了右边。真正有价值的是非平凡依赖,比如"学号能确定姓名"。
3. 完全(直接)函数依赖 vs 部分函数依赖
设 X -> Y,且 X 是复合属性集:
- 若存在 X 的真子集 X' 使得 X' -> Y 成立,则称 Y 对 X 部分函数依赖;
- 若对 X 的任何真子集都不成立,则称 Y 对 X 完全函数依赖。
样例:选课表 SC(学号, 课程号, 姓名, 成绩),候选键为 (学号, 课程号)。
- (学号, 课程号) -> 成绩 是完全依赖(缺一门就定不下成绩,必须知道是哪门课);
- (学号, 课程号) -> 姓名 是部分依赖(光靠 学号 就能定姓名,课程号 是多余的)。
更多通俗样例:
| 场景 | 复合键 | 完全依赖 | 部分依赖 |
|---|---|---|---|
| 仓库出货 | (仓库号, 商品号) -> 出货数量 | 出货数量(需同时知道哪个仓库、哪个商品) | 商品名称(知道商品号就能确定名称) |
| 员工考勤 | (员工号, 日期) -> 打卡时间 | 打卡时间(需同时知道谁、哪天) | 员工部门(知道员工号就能确定部门) |
| 订单明细 | (订单号, 商品编号) -> 单价 | 单价(需同时知道哪个订单、哪种商品) | 商品名称(知道商品编号就能确定名称) |
4. 传递函数依赖
若 X -> Y,Y -> Z,且满足 Y 不包含于 X 以及 Y 不能决定 X(即 X 与 Y 不等价),则称 Z 对 X 存在传递函数依赖。
样例:学生(学号, 系名, 系主任),其中 学号 -> 系名,系名 -> 系主任,而 系名 不能决定 学号(一个系有很多学生)。因此 学号 -> 系主任 是传递依赖。
更多通俗样例:
- 「员工号 -> 部门号 -> 部门经理」:通过员工号找到部门,再通过部门找到经理。员工号不能通过部门号反推(一个部门多个员工),所以这是传递依赖。
- 「快递单号 -> 物流站点 -> 站点所在城市」:通过单号找到经手站点,再通过站点找到所在城市。这是传递依赖。
补充:如果 X 与 Y 等价(互相决定),那么 X -> Z 属于直接决定,不算传递。例如若 学号 <-> 工号(一人一号,一号一人),则 学号 -> 姓名 和 工号 -> 姓名 是直接关系,不存在传递。
二、Armstrong 公理系统与推理规则
设 U 为属性全集,F 是 R 上的函数依赖集。
1. 三条基本公理
| 规则 | 内容 | 通俗样例 |
|---|---|---|
| 自反律 | 若 Y 包含于 X,则 X -> Y | 知道"学号+姓名"就能知道"学号"——这很明显,因为右边本来就是左边的一部分 |
| 增广律 | 若 X -> Y 成立,且 Z 是任意属性集,则 XZ -> YZ 成立 | 已知"学号 -> 姓名",两边同时加上"课程号",得到"学号+课程号 -> 姓名+课程号" |
| 传递律 | 若 X -> Y、Y -> Z 成立,则 X -> Z 成立 | 已知"学号 -> 系名"且"系名 -> 系主任",所以"学号 -> 系主任" |
2. 三条常用推论(可由上面三条推出)
| 规则 | 内容 | 通俗样例 |
|---|---|---|
| 合并律 | 若 X -> Y、X -> Z 成立,则 X -> YZ 成立 | 已知"学号 -> 姓名"和"学号 -> 系名",可以合并为"学号 -> 姓名+系名" |
| 分解律 | 若 X -> YZ 成立,则 X -> Y、X -> Z 都成立 | 已知"学号 -> 姓名+系名",可以拆成"学号 -> 姓名"和"学号 -> 系名" |
| 伪传递律 | 若 X -> Y、WY -> Z 成立,则 XW -> Z 成立 | 已知"学号 -> 系名"和"系名+课程号 -> 成绩",可得"学号+课程号 -> 成绩" |
3. 属性集闭包
X 关于 F 的闭包 X+:从 X 出发,利用 F 中的依赖能推导出的所有属性的集合。
用途:判断 X -> Y 是否成立,只需验证 Y 包含于 X+。
样例:F = {A->B, B->C, C->D},则 A+ = {A,B,C,D},故 A -> D 成立。
更多通俗样例:
设学生表属性 U = {学号, 姓名, 系名, 系主任, 课程号, 成绩},依赖集 F = {学号->姓名, 学号->系名, 系名->系主任, (学号,课程号)->成绩}。
- 求 {学号}+:从学号出发,能推出姓名、系名,再由系名推出系主任。所以 {学号}+ = {学号, 姓名, 系名, 系主任}。注意:成绩推不出来,因为还需要课程号。
- 求 {(学号,课程号)}+:从学号和课程号出发,能推出成绩,再由学号推出姓名、系名、系主任。所以 {(学号,课程号)}+ = 全部属性 = 候选键。
- 判断"学号 -> 系主任"是否成立:{学号}+ 包含系主任,所以成立。(这也验证了传递依赖)
4. 最小函数依赖集(最小覆盖)
满足三个条件:① 右部均为单属性;② 无冗余依赖(删掉任一依赖,闭包会变小);③ 左部无冗余属性。它是规范化分解前的常用预处理步骤。
通俗样例:
假设 F = {学号->姓名, 学号->系名, 学号->系主任, 系名->系主任}。
- 第一步(右部分解):已经都是单属性,不用改。
- 第二步(去冗余依赖):学号->系主任 可以通过 学号->系名 + 系名->系主任 推导出来,是冗余的,删掉。
- 第三步(去左部冗余属性):假设有一条 (学号,课程号)->成绩,检查学号能否单独决定成绩——不能,所以左部无冗余。
最终最小覆盖:F' = {学号->姓名, 学号->系名, 系名->系主任}。
三、键(Key)与属性分类
| 概念 | 定义 | 通俗样例 |
|---|---|---|
| 超键 | 能唯一标识元组的属性集(不要求最小) | (学号, 姓名) 能唯一确定一个学生,是超键,但学号本身就够了,姓名是多余的 |
| 候选键 | 极小超键:去掉任一属性就不再唯一 | 学号 是候选键(去掉就什么都不是了);(学号, 手机号) 也是候选键(两个合起来唯一,但每个单独不一定唯一) |
| 主键 | 从候选键中选定一个作为主键 | 实际建表时选 学号 做 PRIMARY KEY |
| 外键 | 本表中的一个(组)属性,其取值必须匹配另一张表(或自身)的候选键的值,或者为空 | 选课表中的 学号 参照学生表的 学号;员工表中的 上级编号 参照同表的 员工编号 |
| 主属性 | 属于任一候选键的属性 | 若候选键有 学号 和 手机号,则 学号 和 手机号 都是主属性 |
| 非主属性 | 不属于任何候选键的属性(也叫非码属性) | 姓名、成绩、系名 等 |
更多通俗样例(以公司系统为例):
假设有一张员工表 Employee(工号, 邮箱, 姓名, 部门号, 部门名称, 上级工号):
- 超键:{工号, 邮箱}、{工号, 姓名}、{工号, 邮箱, 姓名} 等都是超键(能唯一标识一个员工)
- 候选键:{工号} 和 {邮箱}(假设邮箱唯一),因为它们极小——去掉任何一个属性就不能唯一标识了
- 主键:选 {工号} 作为主键(工程上通常选自然键或代理键)
- 外键:部门号 参照 Department 表的 部门号;上级工号 参照本表的 工号
- 主属性:工号、邮箱(它们属于候选键)
- 非主属性:姓名、部门名称、上级工号
求候选键的方法:
方法一:闭包枚举法(通用)
- 只在依赖左部出现、不在右部出现的属性,一定属于每个候选键;
- 只在右部出现的属性,一定不属于任何候选键;
- 两边都出现的属性,逐个组合试闭包,若闭包 = U 且极小,即为候选键。
方法二:有向图法(直观但有局限)
把属性当节点,按 X -> Y 画有向边。入度为 0 的节点必属候选键;从它们出发能遍历全图的极小组合即为候选键。
局限:当依赖右部为多属性、或存在组合决定的情况时,图法可能漏解,建议用闭包法复核。
样例:R(A,B,C,D),F={A->B, B->C, C->D, D->A}。每个属性闭包都是 {A,B,C,D},故候选键有四个:A、B、C、D,全部是主属性。
四、完整性约束
| 类型 | 含义 | 通俗样例 |
|---|---|---|
| 实体完整性 | 主键的各属性不能取空值,且不能重复 | 学号不能为 NULL、不能有两个学生学号都是 2024001 |
| 参照完整性 | 外键的值必须是参照表中已存在的候选键值,或者为 NULL | 选课表里填的学号,必须在学生表里能找到对应的人 |
| 用户自定义完整性 | 根据应用语义自定义的约束条件 | 年龄 BETWEEN 0 AND 150;性别 IN ('男','女');邮箱必须包含 @ 符号 |
| 触发器(Trigger) | 通过编写过程代码,在 INSERT/UPDATE/DELETE 前后自动执行,可实现复杂约束或业务逻辑 | 删除某商品前,自动检查库存表中是否还有该商品的记录,若有则阻止删除并提示 |
更多通俗样例:
- 实体完整性:电商系统中,订单号不能为空也不能重复——否则系统不知道这笔订单是谁的。
- 参照完整性:订单表中的 客户ID 必须是客户表中已存在的客户,不能给一个不存在的客户下单。
- 用户自定义完整性:注册时密码长度不少于 8 位;日期不能是未来时间(如出生日期不能是 2030 年)。
- 触发器:
- 每次修改库存后,自动在"操作日志表"中记录谁在什么时候改了什么;
- 员工离职时,自动将其名下所有项目转移给部门经理;
- 订单金额超过 1 万元时,自动触发"风控审核"流程。
说明:触发器常用于实现用户自定义完整性,但它还能做审计、同步、日志等工作,所以更准确的说法是"实现复杂约束的手段之一",而不宜断言"本质属于用户自定义完整性约束"。
五、范式(Normalization)
范式层级:1NF ⊂ 2NF ⊂ 3NF ⊂ BCNF ⊂ 4NF ⊂ 5NF
1NF:属性不可再分
每个分量都必须是原子值,表中不能再套表(不允许"组合项"、"重复组")。
通俗理解:1NF 就是"表格不能嵌套表格"。每一格只能填一个值,不能填一堆值。
违反 1NF 的例子:
| 学号 | 姓名 | 选修课程 |
|---|---|---|
| 001 | 张三 | 数学, 英语, 物理 |
修正后(拆成多行):
| 学号 | 姓名 | 课程 |
|---|---|---|
| 001 | 张三 | 数学 |
| 001 | 张三 | 英语 |
| 001 | 张三 | 物理 |
1NF 仍存在四大问题(以 学生(学号, 姓名, 系名, 系主任, 课程号, 成绩) 为例):
- 数据冗余:同一系的学生反复存储相同的系主任——就像全班同学的档案里都抄了一遍系主任名字,浪费空间还容易不一致。
- 更新异常:换系主任要改该系所有学生的记录——如果改了其中一个忘了改另一个,数据就不一致了。
- 插入异常:新系成立但还没学生时,系信息无法录入——因为主键是学号,没有学生就没有学号,系的信息就无处安放。
- 删除异常:某系学生全部毕业被删后,该系信息也随之丢失——就像把整个班级删了,系主任信息也跟着没了。
2NF:消除非主属性对候选键的部分依赖
若 R 属于 1NF,且每个非主属性都完全函数依赖于每一个候选键,则 R 属于 2NF。
通俗理解:2NF 就是"非主属性必须跟整个主键有关系,不能只跟主键的一部分有关系"。
接上例:候选键为 (学号, 课程号),而 学号 -> 姓名、学号 -> 系名 是部分依赖(只依赖主键的一部分——学号),故不满足 2NF。
分解为:
- 学生(学号, 姓名, 系名, 系主任) —— 主键 学号
- 选课(学号, 课程号, 成绩) —— 主键 (学号, 课程号)
两张表均满足 2NF(选课 中成绩完全依赖于整个主键——必须同时知道哪个学生、哪门课才能确定成绩)。
更多通俗样例(电商订单场景):
原始表 OrderInfo(订单号, 商品号, 商品名称, 商品单价, 数量, 总金额, 客户姓名, 客户地址):
- 候选键 = (订单号, 商品号)
- 部分依赖:商品号 -> 商品名称、商品号 -> 商品单价(不需要订单号就能确定)
- 完全依赖:(订单号, 商品号) -> 数量、(订单号, 商品号) -> 总金额
分解后:
- 订单明细(订单号, 商品号, 数量, 总金额) —— 主键 (订单号, 商品号)
- 商品信息(商品号, 商品名称, 商品单价) —— 主键 商品号
- 客户信息(订单号, 客户姓名, 客户地址) —— 主键 订单号
3NF:消除非主属性对候选键的传递依赖
若 R 属于 2NF,且不存在非主属性对候选键的传递依赖,则 R 属于 3NF。
等价定义:对每个非平凡函数依赖 X -> A,要么 X 是超键,要么 A 是主属性。
通俗理解:3NF 就是"非主属性之间也不能通过其他非主属性间接挂钩"。
接上例:学生 表中 学号 -> 系名 -> 系主任,存在传递依赖(系主任通过系名间接依赖于学号),故不满足 3NF。
再分解为:
- 学生(学号, 姓名, 系名) —— 主键 学号
- 系(系名, 系主任) —— 主键 系名
- 选课(学号, 课程号, 成绩) —— 主键 (学号, 课程号)
三张表均满足 3NF。
更多通俗样例(员工场景):
原始表 Employee(工号, 姓名, 部门号, 部门名称, 部门经理, 工资):
- 候选键 = 工号
- 传递依赖:工号 -> 部门号 -> 部门名称 -> 部门经理
分解后:
- 员工(工号, 姓名, 部门号, 工资) —— 主键 工号
- 部门(部门号, 部门名称, 部门经理) —— 主键 部门号
BCNF:消除主属性对键的部分/传递依赖
若 R 属于 1NF,且对每个非平凡函数依赖 X -> Y,X 必为超键(决定因素都包含候选键),则 R 属于 BCNF。
通俗理解:BCNF 比 3NF 更严格——不仅非主属性不能有传递依赖,主属性也不能有。换句话说,"谁能决定其他东西"这个权力,必须只有候选键(或超键)才配拥有。
经典反例:授课(学生S, 课程C, 教师T),F = {(S,C)->T, T->C}。
- 候选键:(S,C) 和 (S,T) → 三个属性全是主属性 → 已满足 3NF(因为没有非主属性,3NF 的条件自动满足);
- 但 T -> C 中 T 不是超键 → 不满足 BCNF(一个非超键决定了另一个属性)。
通俗理解这个反例:一个教师只能教一门课(T->C),但一门课可以有多个老师教。学生选某门课的某个老师就能确定成绩。这里"教师"这个主属性(因为它出现在候选键(S,T)中)决定了"课程",但教师本身不是候选键——这就违反了 BCNF。
分解为 BCNF:教课(S, T) 与 任课(T, C)。
更多通俗样例:
假设关系模式 R(学生, 课程, 教室),语义规则:
- 一个学生在同一时间只能上一门课,且每门课固定在一个教室 → (学生, 课程) -> 教室
- 每门课固定在同一个教室 → 课程 -> 教室
候选键有 (学生, 课程) 和 (学生, 教室)。课程是主属性(出现在候选键(学生,教室)中),但 课程 -> 教室 中 课程 不是超键,所以不满足 BCNF。
分解为:选课(学生, 课程) 和 排课(课程, 教室)。
4NF:消除非平凡的多值依赖
多值依赖:设 X, Y, Z 是 U 的子集,Z = U - X - Y。若给定一对 (x, z) 值,对应的 Y 值只由 x 决定、与 z 无关,则称 X ->-> Y(Y 多值依赖于 X)。
通俗理解:多值依赖描述的是"一对多"且"相互独立"的关系。比如一门课有多个老师和多本参考书,老师的选择和参考书的选择互不影响——选哪个老师不影响有哪些参考书,反之亦然。
性质:对称性(X ->-> Y 则 X ->-> Z)、传递性、函数依赖是多值依赖的特例(X -> Y 则 X ->-> Y)。
R 属于 1NF,若对每个非平凡多值依赖 X ->-> Y(Y 不包含于 X,且 X 并 Y 不等于全属性集),都有 X 是超键,则 R 属于 4NF。
样例:课程信息(课程号, 教师, 参考书),一门课有多位教师和多本参考书,两者互不相干。
- 存在 课程号 ->-> 教师、课程号 ->-> 参考书;
- 候选键是 (课程号, 教师, 参考书),全主属性 → 已达 BCNF;
- 但 课程号 不是超键却决定了多值 → 不满足 4NF。
分解为:开课(课程号, 教师) 与 用书(课程号, 参考书)。
更多通俗样例:
假设关系模式 项目(项目编号, 开发人员, 测试人员):
- 一个项目有多个开发和多个测试人员,且开发人选和测试人选互不影响。
- 存在 项目编号 ->-> 开发人员、项目编号 ->-> 测试人员。
- 候选键 = (项目编号, 开发人员, 测试人员),全主属性 → 达 BCNF。
- 但 项目编号 不是超键却决定多值 → 不满足 4NF。
- 分解为:项目开发(项目编号, 开发人员) 和 项目测试(项目编号, 测试人员)。
小结:
- 只考虑函数依赖时,最高到 BCNF;
- 考虑多值依赖时,可到 4NF;进一步还有处理连接依赖的 5NF(投影-连接范式)。因此"关系模式最高规范化程度是 4NF"的说法并不准确。
六、模式分解
规范化的手段是模式分解:把一个大关系拆成多个小关系,剥离部分依赖与传递依赖。评价分解好坏看两条标准:
1. 无损连接分解(Lossless Join)
分解后的表自然连接能精确还原原表(不丢信息、不产生假元组)。
通俗理解:无损分解就像把一本相册按主题分成两本,然后把两本放在一起对照,能完美还原原来的相册——不会多出一张照片,也不会少一张。
二元分解判定定理:ρ = {R1, R2} 是无损分解 当且仅当
(R1 交 R2) -> (R1 - R2) 或 (R1 交 R2) -> (R2 - R1)
在 F+ 中成立(公共属性能函数决定某一侧的私有属性)。
样例:学生(学号, 系名, 系主任) 拆成 学生(学号, 系名) 和 系(系名, 系主任),公共属性 系名 满足 系名 -> 系主任,故为无损分解。
更多通俗样例:
假设 R(A, B, C),F = {A -> B},分解为 R1(A, B) 和 R2(B, C):
- R1 交 R2 = {B},R1 - R2 = {A},R2 - R1 =
- 检查:B -> A?不成立。B -> C?不成立。
- 结论:有损分解!自然连接后会产生假元组。
假设 R(A, B, C),F = {A -> B},分解为 R1(A, B) 和 R2(A, C):
- R1 交 R2 = {A},R1 - R2 = {B},R2 - R1 =
- 检查:A -> B?成立!
- 结论:无损分解!
多于两个子模式时用 Chase(追踪)算法 / 矩阵法 判定。
2. 保持函数依赖(Dependency Preserving)
把 F 投影到各子模式上得到 Gi,若 Gi 的并集与 F 等价(能覆盖 F 中每个依赖),则分解保持依赖。
通俗理解:保持依赖就是"拆表后,原来所有的规则(依赖)仍然能在某一张子表里被检查到"。如果拆完后某条规则分散在两张表里,没法单独检查,那就是丢失了依赖。
充分条件:若 F 中每个依赖都完整地落在某个子模式中,则一定保持依赖;反之不成立,需用闭包进一步检验。
3. 两者的取舍
| 分解目标 | 无损连接 | 保持依赖 | 说明 |
|---|---|---|---|
| 分解到 3NF | 总能做到 | 总能做到 | 有合成算法保证 |
| 分解到 BCNF | 一定能 | 未必能 | 有时为了消除主属性的传递依赖,不得不牺牲依赖保持 |
通俗理解:3NF 是"既要又要"的折中方案——既能无损还原,又能保持所有规则;BCNF 更彻底地消除了冗余,但偶尔会"拆得太碎"导致某些规则没法在单表中检查。
工程实践中常选 3NF 作为平衡点;若对冗余零容忍,则选 BCNF 并在应用层补偿丢失的依赖检查。
七、事务与并发控制
1. 事务的 ACID 特性
| 特性 | 含义 | 通俗样例 |
|---|---|---|
| 原子性 Atomicity | 操作要么全做,要么全不做 | 转账就像"过独木桥"——要么成功过去,要么原地不动,不可能卡在中间 |
| 一致性 Consistency | 事务执行前后数据库都处于合法状态 | 转账前后两个账户的总金额不变,不会凭空多出或消失钱 |
| 隔离性 Isolation | 并发事务互不干扰,中间状态对外不可见 | 你在转账过程中,别人看不到你"只扣了A的钱还没加到B"的中间状态 |
| 持久性 Durability | 一旦提交,修改永久保存(即使宕机) | 转账显示"成功"后就算立刻断电,钱也已经安全转过去了 |
注:第四个性质的标准译名是持久性(Durability),不宜写作"持续性"或"改变持续性"。
基本操作:COMMIT(提交,相当于"确认执行")、ROLLBACK(回滚,相当于"撤销重来")。
ACID 综合样例(银行转账):
场景:A 账户转 1000 元到 B 账户。
- 原子性:这笔转账包含两个操作——A 扣 1000、B 加 1000。这两个操作必须作为一个整体:要么都成功,要么都失败。不可能出现 A 扣了但 B 没加上。
- 一致性:转账前 A+B 的总额是 X,转账后 A-1000 + B+1000 的总额仍然是 X。数据库的总额约束始终成立。
- 隔离性:如果在转账过程中有人查看 A 的余额,他要么看到转账前的值,要么看到转账后的值,不会看到一个"已经扣了但还没加上"的中间值。
- 持久性:转账成功后(COMMIT),即使数据库立刻崩溃重启,这笔交易也不会消失。
2. 并发带来的三类问题
| 问题 | 描述 | 通俗样例 |
|---|---|---|
| 丢失修改(丢失更新) | 两个事务同读同写,后写回的覆盖了先写回的 | 两个人同时改同一份文档的同一处,后保存的人把先保存的人的修改覆盖了 |
| 读脏数据 | 读到别的事务尚未提交(后来还被回滚)的数据 | A 改了余额但还没保存,B 看到了这个未保存的值,然后 A 取消了修改——B 看到的就是"脏"数据 |
| 不可重复读 | 同一事务内两次读取同一数据,结果不一致 | 你第一次看银行账户是 1000 元,期间别人转了一笔钱进来,你再一看变成 1500 元了 |
(另有"幻读":同一查询条件两次查出的行数不同,通常由插入/删除引起。比如你查"工资大于 5000 的员工",第一次查到 10 个人,期间有人升职工资超过 5000 了,你再查就变成 11 个人了——好像"幻化"出一个人一样。)
并发问题综合样例:
假设有两个事务 T1 和 T2 同时操作库存表中的商品 A:
| 时间线 | 事务 T1 | 事务 T2 | 问题 |
|---|---|---|---|
| T1 | 读取库存 = 100 | ||
| T2 | 读取库存 = 100 | ||
| T1 | 库存 = 100 - 10 = 90,写回 | ||
| T2 | 库存 = 100 - 20 = 80,写回 | 丢失更新:T1 的 -10 被覆盖了,实际应该减 30 却只减了 20 | |
| T1 | 读取库存 = 100 | ||
| T2 | 库存 = 100 + 50 = 150,写回(未提交) | ||
| T1 | 读到库存 = 150 | 读脏数据:T2 后来回滚了,T1 读到的 150 是无效数据 | |
| T1 | 读取库存 = 100 | ||
| T2 | 库存 = 100 + 50 = 150,写回并提交 | ||
| T1 | 再次读取库存 = 150 | 不可重复读:同一次事务中两次读,结果不同 |
3. 锁的类型
- X 锁(排他锁 / 写锁):加锁事务可读可写,其他事务不能加任何锁——就像你给文件加了"只读"以外的锁,别人既不能读也不能改,等你改完释放锁才行。
- S 锁(共享锁 / 读锁):加锁事务只能读,其他事务只能再加 S 锁(可读不可写)——就像文件的"只读"属性,多人可以同时看,但没人能改。
通俗理解:
- X 锁 = 独占模式:你在会议室开会(写数据),别人不能进(不能读也不能写)。
- S 锁 = 共享模式:大家在图书馆看书(读数据),多人可以同时看,但想改内容(写数据)不行,得等所有人都看完。
4. 三级封锁协议
| 协议 | 规定 | 解决的问题 | 通俗理解 |
|---|---|---|---|
| 一级 | 写数据前加 X 锁,直到事务结束才释放 | 丢失修改 | 要改东西先独占,改完才放人 |
| 二级 | 一级 + 读前加 S 锁,读完即可释放 | 丢失修改、读脏数据 | 看完就放手,不会一直占着不让别人改 |
| 三级 | 一级 + 读前加 S 锁,直到事务结束才释放 | 丢失修改、读脏数据、不可重复读 | 看完也不放手,直到你事务结束,保证你不会再读到老数据 |
规律:读锁握得越久,一致性越强,并发性越差。 就像上厕所——门链子挂得越久(锁释放越晚),别人越等不到位置(并发度低),但你的隐私和体验越好(一致性高)。
5. 两段锁协议(2PL)
事务执行分为两个阶段:
- 增长阶段(加锁阶段):只能申请锁(读申请 S,写申请 X),申请不到就等待,不得解锁——就像进货阶段,只能往仓库里放东西,不能往外拿。
- 收缩阶段(解锁阶段):只能释放锁,不得再加锁——就像出货阶段,只能往外拿东西,不能再往仓库里放。
实务做法:从第一条语句起一直加锁,到执行 COMMIT / ROLLBACK 时一次性进入解锁阶段释放全部锁。
遵守 2PL 的事务调度一定是可串行化的;但 2PL 不能避免死锁(双方互相等待对方的锁)。
死锁通俗样例:
两个人去吃饭,桌上只有一双筷子。A 拿到了左筷子,B 拿到了右筷子。A 等 B 放下右筷子,B 等 A 放下左筷子——谁也吃不了,死锁了。
6. 死锁的处理
- 预防:一次封锁法(一次性申请全部锁,要么全拿到要么全不拿,避免部分持有)、顺序封锁法(约定全局加锁顺序,比如永远先锁 A 再锁 B,避免环路)。
- 检测与解除:构造等待图(有环即死锁)或设置超时;解除时通常撤销代价最小(已做工作量最少)的事务,让其回滚并释放锁。
通俗理解:
- 一次封锁法 = "贪心法":做事之前先把所有需要的资源全部拿到手,拿不齐就不开始。不会出现"拿到一半等另一半"的情况。
- 顺序封锁法 = "排队法":约定所有人按固定顺序拿资源,比如永远先拿筷子再拿碗。不会出现 A 等 B、B 等 A 的循环等待。
- 超时法 = "等太久就放弃":等锁超过一定时间还没拿到,就主动放弃回滚,过一会儿再试。
八、一张速查总表
| 概念 | 一句话要点 | 通俗记忆法 |
|---|---|---|
| 函数依赖 | X 定则 Y 必定的语义约束 | " Knowing X means you know Y" |
| 部分/传递依赖 | 真子集能定 / 经第三者中转且不等价 | "部分"=只用一部分就能定;"传递"=绕路通过第三者 |
| Armstrong 公理 | 自反、增广、传递 → 合并、分解、伪传递 | 三条公理是"根",三条推论是"枝叶" |
| 候选键 | 极小超键;用闭包法最稳妥 | "最小的能唯一标识的组合" |
| 外键 | 本表属性,参照他表(或自身)候选键,可为空 | "本表的引用列,指向别人的主键" |
| 1NF | 属性不可再分,不能有"小表" | "一格只能填一个值" |
| 2NF | 砍掉非主属性的部分依赖 | "非主属性必须跟整个主键有关" |
| 3NF | 再砍掉非主属性的传递依赖 | "非主属性不能通过其他非主属性间接挂钩" |
| BCNF | 每个决定因素都是超键(主属性也不能例外) | "谁能决定别人?只有候选键才有这个权力" |
| 4NF | 再砍掉非平凡多值依赖 | "一对多且互相独立的属性要拆开" |
| 无损分解 | 公共属性能函数决定某一侧私有属性 | "拆开后能完美拼回去" |
| 保持依赖 | 所有依赖都能在某张子表里被检验 | "拆表后规则没丢" |
| ACID | 原子、一致、隔离、持久 | "要么全做要么全不做,做完就永久保存" |
| 三级封锁 | 一级防丢失修改,三级再加不可重复读 | "锁得越久越安全但也越慢" |
| 两段锁 | 保可串行化,但不保不死锁 | "先加锁后解锁,两段不能混" |
整理完毕。若需配套习题(如求候选键、判断范式级别、Chase 算法演练、调度可串行化判定),可在此基础上继续扩展。

浙公网安备 33010602011771号