4.5 事务与并发控制
本篇定位:事务与并发控制是数据库的"正确性保障层"——9 年真题直接命中 8 题(2018 单28/51/多86、2019 单49、2020 单53、2021 单53/多90、2022 单53、2023 单49/51、2024 单48),是数据库篇出题最稳定的知识块(几乎年年考)。题型集中在:①ACID 判定(哪种特性被破坏);②并发不一致性(丢失修改/不可重复读/脏数据);③锁与封锁协议;④死锁。2027 预测 1–2 题:ACID 对应、封锁协议、并发不一致辨析。 数据依据:
01-真题/真题文本/+01-真题/考点汇总/;答案以专业判断为准。 阅读提示:死锁概念与 2.2 操作系统死锁同源(资源竞争),写作时呼应;锁与 4.6 优化(2023 单49 更新慢=锁定)衔接;事务日志与 4.6 恢复衔接。
一、考点定位(本章真题统计)
| 考点 | 真题锚点 | 频次 | 考法 |
|---|---|---|---|
| ACID 特性判定 | 2018 单28、2019 单49、2020 单53 | 3 年 3 题 | 一致性/隔离性被破坏的场景 |
| 并发不一致性 | 2018 多86 | 1 年 | 丢失修改/不可重复读/读脏数据(死锁不是) |
| 锁与封锁协议 | 2023 单51、2024 单48、2022 单53 | 3 年 3 题 | 一级封锁协议解决丢失修改;排他锁读写;活锁/死锁辨析 |
| 死锁 | 2018 单51 | 1 年 | 并发时才可能死锁 |
| 并发控制目的 | 2021 单53 | 1 年 | 防止同时更新同一数据致完整性问题 |
| 事务边界 | 2021 多90 | 1 年 | 开始/结束事务的语句 |
| 更新慢=锁定 | 2023 单49 | 1 年 | 场景题:CPU/I/O 低但更新无响应 |
命题规律:本章几乎年年出题,两大主线:①ACID 场景判定("正在访问的数据被修改→隔离性"、"读未提交数据→隔离性");②并发控制机制(锁类型、封锁协议、死锁)。2027 预测 1–2 题:ACID 判定或锁辨析。核心能力:四个特性各管什么 + 三类并发不一致分别由什么锁解决。
二、知识框架(先搭骨架)
事务(Transaction)= 用户定义的一个数据库操作序列,要么全做、要么全不做
├── 特性 ACID(必背四字母)
│ ├── A 原子性 Atomicity:操作要么全成功要么全失败(回滚)
│ ├── C 一致性 Consistency:事务前后数据库都处于一致状态
│ ├── I 隔离性 Isolation:并发事务互不干扰(2019 单49、2020 单53)
│ └── D 持久性 Durability:提交后修改永久保存
├── 事务边界(2021 多90)
│ ├── 开始:INSERT/UPDATE/DELETE(第一个操作自动开始)
│ ├── 结束:COMMIT(提交)/ ROLLBACK(回滚)
│ └── DDL 也隐含结束事务;可用 SET AUTOCOMMIT 控制
└── 并发控制(解决三类不一致,2018 多86)
├── 丢失修改:两个事务同时改一个数据,后写的覆盖先写的
├── 不可重复读:一个事务两次读同一数据,结果不同
├── 读脏数据:读到另一个事务未提交(后回滚)的数据
└── 手段:锁(封锁协议)、时间戳、多版本
封锁(Locking)
├── 锁类型
│ ├── 排他锁 X(写锁):加锁者能读能写,其他事务不能加任何锁(2024 单48)
│ └── 共享锁 S(读锁):加锁者能读,其他事务可加 S 锁但不能加 X 锁
├── 封锁协议(三级)
│ ├── 一级:写前加 X 锁、事务结束释放 → 解决丢失修改(2023 单51)
│ ├── 二级:一级 + 读前加 S 锁、读完即释放 → 还解决读脏数据
│ └── 三级:一级 + 读前加 S 锁、事务结束释放 → 还解决不可重复读
└── 活锁 vs 死锁
├── 活锁:一直等不到锁(饿死)→ 先来先服务解决
├── 死锁:互相等对方释放(2022 单53:资源分配图检测死锁)
└── 死锁只在并发操作时可能出现(2018 单51)
一句话记忆:事务 ACID(原子/一致/隔离/持久)→ 并发三乱(丢失修改/不可重复读/脏数据)→ 锁(X 排他/S 共享)→ 三级封锁(一锁防丢失、二锁加防脏读、三锁再防不可重复读)→ 活锁先来先服务、死锁资源分配图。
三、考点详解
3.1 事务与 ACID(2018 单28、2019 单49、2020 单53,必考)
ACID 的英文全称(一次记牢,选择题爱考"看到英文认特性")
| 字母 | 中文 | 英文全称 | 一句话管什么 |
|---|---|---|---|
| A | 原子性 | Atomicity | 全做或全不做(改一半崩溃要回滚) |
| C | 一致性 | Consistency | 事务把数据库从一个一致状态带到另一个一致状态(账要平) |
| I | 隔离性 | Isolation | 并发事务互不干扰,看不见对方未提交的数据 |
| D | 持久性 | Durability | 提交后永久保存,故障也不丢 |
记忆钩子:A 原子(Atomic,不可再分)/C 一致(Consistent,账平)/I 隔离(Isolated,互不看)/D 持久(Durable,落地不丢)。四个英文词都是形容词,中文后缀都带"性"——看到 -ity/-cy/-tion 结尾的英文,就是 ACID 四兄弟之一。
精确定义:事务 = 用户定义的一个数据库操作序列(如"转账"= 扣款+入账两条 SQL),这些操作要么全部执行成功,要么全部不执行(中间失败自动回滚)。
四个特性(必背):
- 原子性(Atomicity,A):操作不可分割,全做或全不做(回滚);
- 一致性(Consistency,C):事务执行前后数据库都处于一致状态(如转账前后总金额不变);
- 隔离性(Isolation,I):并发执行的事务互相隔离,一个事务看不到另一个事务未提交的中间状态;
- 持久性(Durability,D):事务提交后的修改永久保存(即使系统故障也不丢失)。
2018 单28:事务执行成功则提交全部更新、失败则恢复原状——保持数据库一致性(ACID 的 C)。 2019 单49:事务执行过程中,正在访问的数据被其他事务修改,导致结果不正确——违背隔离性(I)。 2020 单53:读脏数据(依赖未提交更新)——隔离性(I)遭破坏(读到了别的事务未提交、随后可能回滚的数据)。
辨析口诀:改一半失败回滚=原子性;账不平=一致性;看见别人没提交的=隔离性;提交后丢了=持久性。
3.2 事务边界(2021 多90)
2021 多90(选 BD):
- INSERT/UPDATE/DELETE 开始事务(第一个 DML 语句自动开启事务);
- COMMIT(提交)/ ROLLBACK(回滚)结束事务;
- DDL(CREATE/ALTER/DROP)隐含结束事务(执行即提交);
- 可用 SET AUTOCOMMIT 改变自动提交状态。
一句话口径(2021 多90 的常错项):"DDL 语句时事务开始"是错的——DDL 结束事务(隐式提交);开始事务的是第一条 DML。
⚠️ 易错点两连(2021 多90): ① 事务"什么时候开始、怎么结束"(教材口径):第一条 DML(INSERT/UPDATE/DELETE)语句开始事务;SELECT 是只读,不启动事务;DDL 不是"开始"、而是"隐含结束/隐式提交"事务——选项 A 把"执行 DDL 时事务开始"说成对的,正是这个坑;"事务只能用 COMMIT 结束"错误,ROLLBACK 也是结束方式(选 D)。 ② "读也要提交"到底对不对?——先看"提交"是哪一层,两层别混:
- 提交修改(把改动永久化):只读事务没有修改,无物可提交 → 这个意义上"读也要提交"不成立;
- 结束事务(COMMIT 作为事务结束标志,顺带释放锁 / 一致性快照):这个意义上,工程实现里确实"只读也要提交"——MySQL/InnoDB 里一条 SELECT 也会开启事务并建立一致性快照(读视图),不 COMMIT 就不释放,长只读事务会占资源、拖住 undo 回收。这与"二级封锁协议读完即释放 S 锁"是同一类道理:锁 / 快照不用了就得还回去;
- 本考试怎么答:题干问"事务从哪开始 / 怎么结束" → 按教材口径(DML 开始、COMMIT/ROLLBACK 结束、DDL 隐式提交);"只读也要提交"是工程实践的说法,不是本章考点,别拿它改 2021 多90 的答案(该题选 BD)。
3.3 并发操作的三类不一致(2018 多86,必考)
| 不一致性 | 发生场景 | 解决协议 |
|---|---|---|
| 丢失修改 | 两个事务同时读同一数据并先后写,后写覆盖先写 | 一级封锁协议 |
| 不可重复读 | 一个事务两次读同一数据,期间被其他事务修改 | 三级封锁协议 |
| 读脏数据 | 读到其他事务未提交(随后回滚)的数据 | 二级封锁协议 |
2018 多86:并发操作带来的数据不一致性 = 丢失修改、不可重复读、读脏数据(选 ABC);死锁不是(D 是并发控制的另一类问题,不是"数据不一致性"本身)。
❓ "别人改数据不是正常的吗?为什么'不可重复读'算问题?"——关键限定词是"同一个事务内、还没结束"。
| 场景 | 是否问题 |
|---|---|
| 我的事务已提交,别人随后改 | ✅ 正常,没有任何问题 |
| 我的事务还没结束,同一事务内读了两次,中间别人改并提交 | ⚠️ 两次结果不一样 → 不可重复读 |
为什么算问题:一个事务是一个逻辑单元,它内部的多次读取应反映同一个时间点的数据库状态。两次读之间世界变了,事务内部就会自相矛盾——而它往往正是拿第一次读的结果做决策:
- 超卖:T1 读库存 10 → 决定卖 5;T2 卖掉 8 件提交(剩 2);T1 仍按"10"继续卖 5 → 超卖。
- 对账不平:T1 先读 A 账户 100 万,中途 T2 转走 10 万,T1 再读 B 账户 100 万,最后报"A+B=200 万"(实际 190 万)。
- 校验失效:T1 读余额 1000 判定"够扣 800",中途 T2 扣走 700,T1 再扣 800 → 透支。
它也不是"绝对的错",而是"分级取舍":读已提交(RC)允许不可重复读(看最新值,适合单条查询);可重复读(RR)禁止(要求事务内快照稳定,适合对账/汇总)。MySQL InnoDB 默认 RR,Oracle / PostgreSQL 默认 RC。 一句话:不是"别人改了"有问题,是"我读了两遍、结果不一样"有问题——同一事务的两次读,看到了两个不同的世界。
❓ 为什么"没提交"的数据别人还能读到?(最常被问倒的一点)
一句话:"未提交"只等于"还不算永久",不等于"还没写进去"。 修改早就落在内存缓冲区里了,而缓冲区是全体事务共享的。
| 步骤 | 发生什么 | 谁看得见 |
|---|---|---|
| ① T1 执行 UPDATE | 改的是缓冲区里的数据页(可能还没刷盘) | T1 自己,以及任何通过缓冲区读这一页的事务 |
| ② T2 此时来读同一行 | 缓存管理直接给它新值 | T2 读到了"未提交"的值 ← 脏读就发生在这里 |
| ③ T1 之后 ROLLBACK | 新值作废、恢复旧值 | T2 此前读到的值,从未真实存在过 = 脏数据 |
所以脏读能不能发生,取决于"锁什么时候放": · 若 T1 的写锁(X 锁)一直持有到事务结束(一级封锁协议的做法),T2 想读就被阻塞 → 读不到脏数据; · 若 T1 写完就把锁放了(或读之前不加锁),T2 立刻就能读到 → 脏读。 这就是"二级封锁协议才防脏读"的原因:它在"一级"的基础上要求读之前也加 S 锁,从而保证"读不到别人正在改、且还没提交的值"。
⚠️ 别把判据搞成"是否落盘":脏数据的判据是提交状态(COMMIT / ROLLBACK),与"是否写进磁盘"无关——已经落盘但没提交,照样是脏数据。
❓ 没提交的数据也会被写到磁盘吗?——会,而且必须会。
- 刷盘看的是"这个数据页脏不脏",不看"改它的事务提没提交":缓冲区空间不够(LRU 淘汰脏页)、检查点(checkpoint)、一次事务改了太多页,都会把未提交的修改写到磁盘。
- 为什么不怕:数据库遵循 WAL(Write-Ahead Logging,日志先行)——日志先落盘,数据页后落盘。每条修改在日志里都有记录,崩溃恢复时按日志撤销(undo)未提交的、重做(redo)已提交的。
- 所以"是否落盘"和"是否提交"是两个独立维度:
| 修改已写到磁盘? | 已提交? | 崩溃恢复后 | 这值算不算脏 |
|---|---|---|---|
| 否(还在内存) | 否 | undo 撤销 | 脏 |
| 是(已刷盘) | 否 | 照样 undo 撤销 | 仍是脏数据 ← 本条的要点 |
| 是 | 是 | 保留 | 有效 |
| 否(在内存,但日志已落盘) | 是 | redo 重做 → 保留 | 有效 |
最后一行就是持久性(Durability)的真正实现:提交时只要保证日志落盘,数据页晚一点写也不要紧——所以 D 靠的是日志,不是"立刻把数据页刷盘"。(日志/检查点/redo·undo 的细节见 4.6 恢复技术。)
3.4 锁与封锁协议(2023 单51、2024 单48、2022 单53,必考)
锁的两种类型(2024 单48):
- 排他锁(X 锁,写锁):事务 T 对数据 Q 加 X 锁后,T 对 Q 既能读又能写,其他事务既不能加 S 锁也不能加 X 锁(独占);
- 共享锁(S 锁,读锁):加 S 锁后可读,其他事务可再加 S 锁(共享读)但不能加 X 锁。
2024 单48:事务 T 对 Q 加了排他锁,则 T 对 Q 既能读又能写(选 C)——"只能读""只能写""都不能"都是干扰项。
两把锁的"持有者 vs 他人"速查(最不容易记混的写法):
| 锁类型 | 持有者 | 其他事务 |
|---|---|---|
| X 锁(排他 / 写锁) | 读 ✅ 写 ✅ | 读 ❌ 写 ❌ |
| S 锁(共享 / 读锁) | 读 ✅ 写 ❌(要写得升级成 X 锁) | 读 ✅(也加 S 锁) 写 ❌ |
⚠️ X 锁下"别人不可读"的原因:不是"禁止读",而是读操作本身要申请 S 锁,而 S 与 X 冲突——这也正是 S 锁存在的意义(若读不需加锁,X 锁根本拦不住别人读)。 ⚠️ 锁升级会招来死锁:T1、T2 都先加 S 锁再想升级为 X 锁,就会互相等对方放 S 锁 → 死锁的经典来源。反过来 X → S 叫锁降级。 ⚠️ "可读可写"≠"必须读写":持 X 锁也可以只读不写,X 锁是独占许可,不是"强制写"。
❓ 既然"读"是共享的,为什么还要加 S 锁?——S 锁拦的不是"读",是"写"。
- "共享"只说明"多个读者可以并存",不说明"读不需要任何约束"。S 锁的作用是声明"我正在读,别改它"。
- 不给读加锁会出三种乱子:
| 时序 | 后果 | 名称 |
|---|---|---|
| T1 读到一半 → T2 改 → T1 接着读 | 读到一半新一半旧 | 不一致读 |
| T1 读第一次 → T2 改并提交 → T1 再读 | 两次结果变了 | 不可重复读 |
| T1 读"全表"期间 → T2 插入并提交 → T1 再读 | 凭空多出一行 | 幻读 |
锁的兼容矩阵(记住这张表,比背概念牢):
| 请求加锁 \ 已持有 | 无锁 | S 锁 | X 锁 |
|---|---|---|---|
| S 锁(读) | ✓ | ✓ 共享读 | ✗ 阻塞 |
| X 锁(写) | ✓ | ✗ 阻塞 | ✗ 阻塞 |
读读兼容、读写互斥、写写互斥——"共享"就体现在第一行中间那个 ✓。
❓ 那"第一个读者加锁、后面的直接读"行不行?——不行。S 锁是"登记每个读者"的计数器,不是"第一个人替大家锁上"。
- 加 S 锁=在自己名下登记"我也在读"(计数 +1),释放=减 1。锁管理器看不见"实际有几个人在读",它只看登记簿。
- 若后面的读者不登记:
| 时刻 | 发生什么 | 锁管理器以为 | 实际 |
|---|---|---|---|
| t1 | T1 加 S 锁读 | 1 个读者 | T1 读 |
| t2 | T2 直接读(没登记) | 仍是 1 个读者 | T1、T2 读 |
| t3 | T1 读完释放 | 0 个读者 | T2 还在读 |
| t4 | T3 申请 X 锁想写 | 没人读 → 批准 | 与 T2 撞车 → T2 读到改一半的数据 |
- "多个读者加 S 锁不排队"≠"可以不加":S 与 S 兼容,所以加锁动作立刻成功、看不出停顿,但内部确实记了一笔。
- ⚠️ MVCC 的"读不加锁"是换机制,不是省登记:它靠版本号/快照判断该看哪个版本,不需要引用计数;考纲口径仍是"每个读者各自加锁"。
三级封锁协议(2023 单51):
| 级别 | 要求 | 解决的不一致 |
|---|---|---|
| 一级 | 修改前加 X 锁,事务结束才释放 | 丢失修改 |
| 二级 | 一级 + 读前加 S 锁,读完即释放 | 丢失修改 + 读脏数据 |
| 三级 | 一级 + 读前加 S 锁,事务结束才释放 | 丢失修改 + 读脏数据 + 不可重复读 |
2023 单51:一级封锁协议解决了事务并发操作带来的数据丢失修改(选 A)。 记忆口诀:一级防丢失、二级防脏读、三级防不可重复读(逐级升级)。
关键延伸:S 锁"什么时候放"决定能防到哪一级——读完立即放(二级协议)只防脏读,仍会不可重复读;持有到事务结束(三级协议)才防不可重复读。锁定"加不加锁"只是第一步,"持有多久"才是分水岭。
❓ "读的期间别人改了,我会立刻读到吗?"——不会"推送给你",只有"下次读才看到"。
| 情形 | A 读到什么 | 属于哪种问题 |
|---|---|---|
| A 正在执行这条 SELECT 时别人想改(二级协议:A 已持 S 锁) | 别人加不上 X 锁、被阻塞 → A 读到稳定的旧值 | 这正是二级协议防脏读的机制 |
| A 读完并释放 S 锁之后,别人改并提交 | A 不会收到任何通知;但 A 下一次读就能读到新值 | 不可重复读(二级防不住,三级才防) |
| 读时完全不加 S 锁(一级协议 / 读未提交),别人改到还没提交 | A 可能读到未提交的新值 | 脏读 |
一句话记住:数据库的读是"取值快照",不是"实时订阅"。 一条 SELECT 执行完就定格了,之后别人怎么改都不会"追着改"它——想看到新值,必须再读一次。
⚠️ 上表只说了"谁解决什么",但没解释"为什么"——这里是最容易读成死记硬背的地方,必须讲透:
- 二级为什么能防脏读?不是"把数据锁住",而是"逼读者排队"。 一级协议已经规定:写者加 X 锁后要持有到事务结束。所以 T1 改了 R 又没提交时,R 上的 X 锁一直挂着。二级要求读者读前先申请 S 锁,而 S 锁与 X 锁互斥 → 申请被挡 → 读者必须等 T1 提交(或回滚)释放 X 锁后才能读 → 读到的一定是已提交的值。"加 S 锁"的真实作用,是让"读"这件事和"写者未提交的那段时间"发生冲突,从而迫使读者等待;如果读不加锁,读者会毫无阻碍地直接看见未提交的值。
- 为什么"读完即释放"对脏读够用、对不可重复读不够?——两次异常的"出问题窗口"根本不同:
- 脏读是"单点"问题:危险只在读的那一瞬间(读到的可能是别人未提交、随后回滚的幻影值)。一旦这次读顺利完成,T1 拿到的是已提交的干净值,这次读就已经安全了,锁再留着没有意义 → 可以立刻放。
- 不可重复读是"区间"问题:危险在两次读之间的整段时间。T2 第一次读时值也是干净的(第三者的修改此时还没提交),但 T2 读完就把 S 锁放了 → T3 立刻拿到 X 锁改 R 并提交 → T2 再读就变了。
- 所以两者的解法差异不在"加不加锁",而在"锁持有多久":
| 异常 | 出问题的窗口 | 需要的锁行为 |
|---|---|---|
| 丢失修改 | 两个写者同时写 | 写锁持到事务结束(一级) |
| 读脏数据 | 读的那一刻撞上未提交的写 | 读前加 S 锁(与未提交的 X 锁冲突,被迫等待);读完即可放 |
| 不可重复读 | 两次读之间别人提交了修改 | S 锁持到事务结束 |
- 一句话:脏读是"读到了不该读的未提交值",靠"挡住未提交的写者"解决;不可重复读是"读过的值在两次读之间被合法改掉了",靠"不让别人中途改"解决——前者是"点"的保护,后者是"区间"的保护。
- ⚠️ "脏"的判据是"未提交",不是"未落盘":脏数据 = 事务已修改但尚未 COMMIT 的数据,判据是提交状态(COMMIT / ROLLBACK),与是否写进磁盘无关。①已写入缓冲区甚至已落盘、但没提交 → 仍是脏数据;②已提交但修改还留在内存、尚未落盘 → 不脏——那属于持久性(D)要保证的事(靠日志/检查点,崩溃后重做)。对应关系:脏读破坏隔离性(I);"已提交但没落盘"是原子性/持久性(A/D) 的事,两件不同的事别混。
- ⚠️ 边界:三级协议防不住幻读:三级锁挡的是"同一条已存在的数据被改",挡不住别人 INSERT / DELETE 造成"同一条件查出的行数变了"(幻读)——幻读要靠范围锁 / 间隙锁,或直接上串行化隔离级别。
3.4.1 两段锁协议(2PL)——"加锁能不能保证可串行化"的答案
先回答一个问题:只要"读写前都加锁",就一定能保证可串行化吗?——不能。 必须再加一条加锁/放锁的顺序规则,这就是两段锁协议。
精确定义(两阶段):每个事务的加锁/放锁必须分成两个阶段:
| 阶段 | 允许的动作 | 禁止的动作 |
|---|---|---|
| ① 扩展阶段(growing phase) | 可以申请加锁(S 或 X) | 不能释放任何锁 |
| ② 收缩阶段(shrinking phase) | 只可以释放锁 | 不能再申请新锁 |
分界点:事务一旦释放了第一个锁,就进入收缩阶段,此后不许再申请新锁。
它保证什么:任何遵守 2PL 的并发调度,都是可串行化的——这是它存在的全部理由。(证明思路:所有冲突操作都被锁强制成"先来后到",冲突图不会成环。)
它不保证什么:⚠️ 2PL 不能避免死锁。因为两个事务可以各自进入扩展阶段、各持一把锁、再互相申请对方的锁 → 互相等待。"保证可串行化"与"保证不死锁"是两件事。
两个加强版(实际系统用的):
| 变体 | 规则 | 解决什么 |
|---|---|---|
| 严格两段锁(Strict 2PL) | 所有 X 锁持有到事务结束才释放 | 避免级联回滚(别人读不到未提交数据) |
| 强两段锁(Rigorous 2PL) | 所有锁(含 S 锁)都持有到事务结束 | 最严;等价于可串行化级别 |
2PL 与三级封锁协议的区别(最容易被问混):
| 管的是什么 | 谁包含谁 | |
|---|---|---|
| 三级封锁协议 | 只管锁持有多久(S 锁持到事务结束),用来防具体异常 | 与 2PL 不是包含关系 |
| 两段锁协议(2PL) | 管加锁/放锁的阶段顺序,用来保证可串行化 | 遵守三级协议 不一定遵守 2PL;反之亦然 |
一句话:三级协议防的是"具体三种异常",2PL 防的是"调度不可串行化"——前者是"对症下药",后者是"整体正确性"。
死锁的两种预防法(与 2PL 配套考):
| 方法 | 做法 | 代价 |
|---|---|---|
| 一次封锁法 | 事务开始前一次性申请全部要用的锁 | 并发度低、难以预知要哪些锁 |
| 顺序封锁法 | 所有事务按同一固定次序申请锁(如按表名/主键排序) | 需要全局约定;实际难完全做到 |
易错:2PL 保证可串行化,但不保证不死锁(这是它最常考的"缺点");"遵守 2PL 就一定不会死锁"是错误说法。
3.5 活锁与死锁(2018 单51、2022 单53)
⚠️ 先分清两层门:权限 ≠ 锁(最容易混的一件事)
| 层面 | 工具 | 管什么 | 生命周期 | 谁授予 |
|---|---|---|---|---|
| 权限(访问控制) | GRANT / REVOKE(DCL,见 4.2) | 这个用户能不能访问这张表/这一列 | 持久(用户级) | DBA 授予 |
| 锁(并发控制) | S 锁 / X 锁、封锁协议 | 此刻能不能访问、几个人能同时访问 | 临时(事务级,事务结束释放) | DBMS 自动管理 |
两句分清:①"这张表我能查吗" → 权限;②"我现在能查吗、有没有人在改" → 锁。 顺序:先过权限,再谈锁——没有
SELECT权限,拿到 S 锁也读不到;有权限但数据被 X 锁占着,同样得等。 类比:权限=门禁卡(有没有资格进楼);锁=会议室门口的"使用中/独占"牌(现在能不能进、能不能多人同时进)。 ⚠️ "访问前必须加锁"不是唯一方案:教材主线(也是考纲口径)是基于封锁的并发控制;此外还有时间戳排序、乐观并发控制、MVCC——它们不加锁也能保证一致性,但答题按"封锁"答。 精确定义:
- 活锁:事务一直等不到锁(总是被其他事务抢先)——"饿死";解决:先来先服务(排队);
- 死锁:两个(多个)事务互相等待对方释放资源,谁也进行不下去。
2022 单53(选错误的说法):"避免活锁用资源分配图"是错误说法(选 B)——避免活锁用先来先服务;资源分配图用于检测死锁(不是避免活锁)。
- 死锁检测:资源分配图有环 → 死锁;死锁解除:撤销(某些)事务——即选一个"牺牲者"回滚它、释放它持有的锁(不是"撤销某个事务的一半",回滚单位是整个事务)。
- 解除死锁的三条办法:①选牺牲者、撤销它的事务(最常用,按"代价最小"选:已运行时间短、改动数据少、持锁少);②回滚牺牲者的全部工作;③超时中止(等锁超过阈值即中止,实现里作兜底)。
- 🔴 牺牲者必须从"死锁环"里选——撤销环外事务没用:死锁的本质是等待图(资源分配图)中存在环;只有撤销环上的事务、释放它持有的"正被环内另一个事务等待的那把锁",环才断得开。撤销一个与环无关的事务,环上的等待关系一条都没变,死锁照旧。
例:A→B→C→A 已成环(A 等 B、B 等 C、C 等 A),另有一个无关事务 D 持有无关表的锁。 · 撤销 D → A/B/C 的等待关系不变 → 死锁依旧 ❌ · 撤销 B → 释放 B 的锁 → A 拿到锁继续跑 → 环断开 ✅ ⇒ 正确规则是两条:①必须在环内;②环内候选中代价最小。教材简写"选代价最小的牺牲者",隐含了"在环内选"这个前提。
- ⚠️ 防"饿死":若同一事务总被选为牺牲者,它永远做不完 → 记录每个事务被回滚的次数,优先不选已被多次回滚的事务。
2018 单51:死锁只有在并发操作时才有可能出现(选 D)——A"数据库不存在死锁"错误;B"禁止两个用户同时访问同一资源"是过度做法(封锁不是禁止并发);C"竞争相同资源不会死锁"错误(竞争正是死锁前提)。
3.5.1 并发控制的其他手段(锁只是其中一种)
先立住目标:并发控制要保证的是"并发执行的结果,等价于某个串行执行的结果"(可串行化)。达成这个目标有很多手段,封锁只是其中最常见的一种。
| 手段 | 基本思想 | 冲突怎么处理 | 优点 | 代价 / 典型问题 |
|---|---|---|---|---|
| ① 封锁(Locking) | 访问前申请 S/X 锁,用完(或事务结束)释放 | 等(阻塞,排队) | 实现直观;教材主线、真题默认口径 | 会死锁;锁管理有开销;并发度受锁粒度影响 |
| ② 时间戳排序(TO,Timestamp Ordering) | 每个事务发一个时间戳,按时间戳先后决定读写的合法顺序 | 杀(冲突就回滚重来) | 不会死锁(不等待) | 高冲突场景大量回滚,可能"回滚—重来"循环(活锁风险) |
| ③ 乐观并发控制(OCC) | 分三阶段:读阶段 → 校验阶段 → 写阶段;先干,提交前校验 | 校验后重做 | 冲突少时几乎无开销,读不加锁 | 冲突多时反复重做;长事务易失败 |
| ④ 多版本并发控制(MVCC) | 每次修改生成新版本,读按快照取合适版本 | 隔离(读写互不阻塞) | 读不阻塞写、写不阻塞读,并发度最高 | 旧版本要回收(purge);空间开销;长事务导致版本堆积 |
| ⑤ 可串行化判定(不是控制手段) | 用优先图(前趋图)判断一个调度是否可串行化 | — | 判定/分析工具,用于理论题 | 只回答"对不对",不负责"怎么做" |
一句话把四类手段串起来:面对冲突,一共三种态度——等(封锁)、杀(时间戳、乐观)、躲(MVCC 用版本躲开)。可串行化判定不参与执行,只做裁判。 考试口径:真题考的都是封锁(S/X 锁、三级封锁协议、两段锁、活锁死锁)。其他方法知道名字 + 一句思想即可;若出"下列属于并发控制方法的是"这类多选,封锁、时间戳、乐观、MVCC 都算。
3.6 场景题:更新操作慢(2023 单49)
2023 单49:某更新语句长时间无响应,但 CPU 使用率 <1%、磁盘 I/O 非常小 → 原因是更新操作涉及的数据被锁定(选 D)。
- 辨析:CPU 低+I/O 低说明不是性能瓶颈,而是在等待——等锁释放(别的长事务持有锁)。
- 这类场景题的思路:CPU/I/O 都不忙却卡住 = 锁等待。
3.7 隔离级别四级对照(补强)
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(Read Uncommitted) | 可能 | 可能 | 可能 |
| 读已提交(Read Committed) | 否 | 可能 | 可能 |
| 可重复读(Repeatable Read) | 否 | 否 | 可能(标准口径) |
| 可串行化(Serializable) | 否 | 否 | 否 |
- 隔离级别越高 → 数据越安全、并发度越低;读已提交是多数数据库的常用默认。
- MySQL/InnoDB 默认可重复读:用 MVCC 快照解决不可重复读、用**临键锁/间隙锁(Next-Key)**避免幻读——所以"标准说可重复读允许幻读"与"InnoDB 实测不出幻读"并不矛盾,答题按"标准口径"、理解按"实现偏差"。
- MVCC 的边界:快照读不阻塞写、写不阻塞读;但当前读(
SELECT … FOR UPDATE)与写写冲突仍靠锁。 - 可串行化可用**两阶段封锁(2PL)**或乐观并发实现;坑:级别高低与"加锁多少"同向。
四、易错坑汇总(命题人惯用陷阱)
- 隔离性 vs 一致性(2018 单28、2019 单49、2020 单53):"正在访问的数据被其他事务修改""读到未提交的值(脏数据)"= 隔离性破坏;一致性破坏 = 事务结束后数据库不再满足完整性约束/业务规则,典型就是"账不平":转账 A−200 成功了、B+200 没做 → A+B 的总和不再是原来的数。
- ⚠️ 别写成"事务前后状态不一致":事务的本职就是改数据,正常做完数据全变了(但仍满足约束,所以仍是一致的)。准确表述是:一致性 = 事务把数据库从一个一致状态带到另一个一致状态;破坏 = 停在了违反约束的"不一致状态"上。
- ⚠️ 判特性看"原因",不看"后果":2019 单49 题干带一句"导致处理结果不正确"——看着像一致性,但那是后果;"正在访问的数据被其他事务修改"才是原因 → 隔离性。
- 两个问法要对上号:问"破坏了/违背了哪个特性"→ 看原因(并发干扰 → 隔离性);问"保持了数据库处于什么状态"→ 答一致性(2018 单28 问的就是这个,选 B)。
四特性判据速查:原子性=多个操作全做或全不做(改一半崩溃);一致性=事务结束后数据库仍满足完整性约束(账不平);隔离性=并发事务互不干扰、看不见对方未提交的数据(读脏数据、访问的数据被别人改);持久性=已提交的修改永不丢失(提交后断电丢了)。 ⚠️ 四者不是并列关系——A / I / D 是手段/机制,C 是目的。 2018 单28 就是活证据:题干描述的是"全部提交 / 失败恢复原状"这个原子性机制,但问的是"数据库处于什么状态",答的是一致性。
- 死锁不是并发不一致性(2018 多86):并发不一致 = 丢失修改/不可重复读/脏数据;死锁是另一类并发问题。
- 排他锁能读能写(2024 单48):X 锁是"独占",加锁者读+写都行;"X 锁只能写不能读"错误。
- 一级封锁协议解决丢失修改(2023 单51):"一级协议解决读脏数据"错误(那是二级);"解决不可重复读"错误(那是三级)。
- 避免活锁用先来先服务(2022 单53):资源分配图是检测死锁的;"用资源分配图避免活锁"错误。
- 死锁只在并发时出现(2018 单51):单用户串行操作不会死锁;"禁止并发访问就能防死锁"是错误做法(封锁是并发控制,不是禁止并发)。
- DDL 隐含提交事务(2021 多90):CREATE/ALTER/DROP 执行即提交;"DDL 也在事务内可回滚"错误(MySQL 中 DDL 自动提交)。
- UPDATE 慢不一定是性能问题(2023 单49):CPU/I/O 都不忙却卡住 → 锁等待;别往"索引失效/磁盘慢"方向猜。
五、横向扩展(防漏网:本章边界内、真题尚未考但可能补考的点)
说明:横向扩展覆盖"大纲范围内但 9 年真题未考过"的知识,标注【新增概率】供取舍。
- 两段锁协议(2PL)【中】:加锁分两个阶段(扩展阶段只加锁、收缩阶段只释放)——保证并发调度的可串行性;名词辨析题。
- 可串行化调度【低】:并发调度的结果与串行执行相同——调度正确性概念。
- 时间戳排序协议【低】:另一种并发控制方法(不用锁)——与锁对比。
- 多版本并发控制(MVCC)【中】:读不阻塞写、写不阻塞读(快照读)——MySQL InnoDB 的实现,金融科技/数据库前沿题可能提及。
- 死锁的四个必要条件【中】:互斥/占有并等待/不可剥夺/循环等待——与 2.2 操作系统死锁同源(2019 单23 考过 OS 死锁成因),数据库死锁场景可能引用。
- 隔离级别(读未提交/读已提交/可重复读/串行化)【高】:四个隔离级别与三类不一致的对应——高概率补考点(读未提交→可能全乱;读已提交→防脏读;可重复读→防不可重复读;串行化→全防)。
- 事务日志与恢复的关系【高】:UNDO(回滚未提交)/REDO(重做已提交)——衔接 4.6 恢复。
- 乐观锁 vs 悲观锁【中】:乐观锁(版本号/CAS,提交时检查)vs 悲观锁(先加锁)——应用场景辨析。
六、真题演练(代表性真题 + 解析)
例 1(2019 单49) 数据库一个事务执行过程中,其正在访问的数据被其他事务所修改,导致处理结果不正确,这是由于违背了事务的(口径已核)而引起的。 A. 原子性 B. 一致性 C. 隔离性 D. 持久性
答案:C。事务看到其他事务未提交的修改 → **隔离性(I)**遭破坏(并发事务应互相隔离)。原子性=全做或全不做、一致性=前后状态一致、持久性=提交后不丢。口诀:看见别人的中间状态=隔离性。
例 2(2020 单53) 数据库中对未提交更新的依赖(读脏数据)是事务的(口径已核)遭到了破坏。 A. 原子性 B. 一致性 C. 隔离性 D. 持久性
答案:C。读脏数据 = 读到其他事务未提交(可能回滚)的数据 → 隔离性破坏。与例 1 同族(2020 与 2019 跨年呼应)。口诀:脏数据=隔离性的锅。
例 3(2024 单48) 若事务 T 对数据对象 Q 加了排他锁,则 T 对 Q(口径已核)。 A. 只能读不能写 B. 只能写不能读 C. 既能读又能写 D. 既不能读又不能写
答案:C。排他锁(X 锁):加锁者独占——能读能写;其他事务不能加任何锁。口诀:X 锁独占读写,S 锁共享只读。
例 4(2023 单51) 一级封锁协议解决了事务的并发操作带来的(口径已核)不一致性的问题。 A. 数据丢失修改 B. 数据不可重复读 C. 读脏数据 D. 数据重复修改
答案:A。一级封锁协议(写前加 X 锁、事务结束释放)→ 防止两个事务同时写 → 解决丢失修改;二级再加防读脏数据、三级再加防不可重复读。口诀:一级防丢失、二级防脏读、三级防不可重复读。
例 5(2022 单53) 下列关于数据库锁的说法错误的是(口径已核)。 A. 避免活锁可采用先来先服务 B. 避免活锁可采用资源分配图 C. 资源分配图可用于死锁检测 D. 数据库操作中存在死锁
答案:B。避免活锁 = 先来先服务(A 对);资源分配图 = 检测死锁(C 对,B 错——张冠李戴);数据库并发存在死锁(D 对)。口诀:活锁排队解决,死锁画图检测。
例 6(2018 多86) 在数据库中,并发操作会带来的数据不一致性有(口径已核)。 A. 丢失修改 B. 不可重复读 C. 读脏数据 D. 死锁
答案:ABC。并发不一致三兄弟 = 丢失修改、不可重复读、读脏数据;死锁不是数据不一致性(是并发控制的另一类问题)。口诀:并发三乱=丢修、不可重复、脏读。
七、本章小结
- 一条主线:事务(ACID)→ 事务边界(DML 开始、COMMIT/ROLLBACK 结束)→ 并发三乱(丢失修改/不可重复读/脏数据)→ 锁(X/S)→ 三级封锁协议(一防丢失、二防脏读、三防不可重复读)→ 活锁/死锁。
- 必背判定:读未提交数据=隔离性破坏(2019 单49、2020 单53);一级封锁协议=防丢失修改(2023 单51);排他锁=能读能写(2024 单48);避免活锁=先来先服务(2022 单53)。
- 必背场景:CPU/I/O 都低但更新卡住=锁等待(2023 单49);死锁只在并发时出现(2018 单51)。
- 衔接预告:死锁与 2.2 OS 死锁同源;事务日志(UNDO/REDO)→4.6 恢复;隔离级别与锁 → 4.6 优化。