4.5 事务与并发控制

4.52026-11-03 11:34:00

本篇定位:事务与并发控制是数据库的"正确性保障层"——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 单533 年 3 题一致性/隔离性被破坏的场景
并发不一致性2018 多861 年丢失修改/不可重复读/读脏数据(死锁不是)
锁与封锁协议2023 单51、2024 单48、2022 单533 年 3 题一级封锁协议解决丢失修改;排他锁读写;活锁/死锁辨析
死锁2018 单511 年并发时才可能死锁
并发控制目的2021 单531 年防止同时更新同一数据致完整性问题
事务边界2021 多901 年开始/结束事务的语句
更新慢=锁定2023 单491 年场景题: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。锁管理器看不见"实际有几个人在读",它只看登记簿。
  • 若后面的读者不登记:
时刻发生什么锁管理器以为实际
t1T1 加 S 锁读1 个读者T1 读
t2T2 直接读(没登记)仍是 1 个读者T1、T2 读
t3T1 读完释放0 个读者T2 还在读
t4T3 申请 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)**或乐观并发实现;坑:级别高低与"加锁多少"同向。

四、易错坑汇总(命题人惯用陷阱)

  1. 隔离性 vs 一致性(2018 单28、2019 单49、2020 单53):"正在访问的数据被其他事务修改""读到未提交的值(脏数据)"= 隔离性破坏;一致性破坏 = 事务结束后数据库不再满足完整性约束/业务规则,典型就是"账不平":转账 A−200 成功了、B+200 没做 → A+B 的总和不再是原来的数。
    • ⚠️ 别写成"事务前后状态不一致":事务的本职就是改数据,正常做完数据全变了(但仍满足约束,所以仍是一致的)。准确表述是:一致性 = 事务把数据库从一个一致状态带到另一个一致状态;破坏 = 停在了违反约束的"不一致状态"上。
    • ⚠️ 判特性看"原因",不看"后果":2019 单49 题干带一句"导致处理结果不正确"——看着像一致性,但那是后果;"正在访问的数据被其他事务修改"才是原因 → 隔离性。
    • 两个问法要对上号:问"破坏了/违背了哪个特性"→ 看原因(并发干扰 → 隔离性);问"保持了数据库处于什么状态"→ 答一致性(2018 单28 问的就是这个,选 B)。

四特性判据速查:原子性=多个操作全做或全不做(改一半崩溃);一致性=事务结束后数据库仍满足完整性约束(账不平);隔离性=并发事务互不干扰、看不见对方未提交的数据(读脏数据、访问的数据被别人改);持久性=已提交的修改永不丢失(提交后断电丢了)。 ⚠️ 四者不是并列关系——A / I / D 是手段/机制,C 是目的。 2018 单28 就是活证据:题干描述的是"全部提交 / 失败恢复原状"这个原子性机制,但问的是"数据库处于什么状态",答的是一致性。

  1. 死锁不是并发不一致性(2018 多86):并发不一致 = 丢失修改/不可重复读/脏数据;死锁是另一类并发问题。
  2. 排他锁能读能写(2024 单48):X 锁是"独占",加锁者读+写都行;"X 锁只能写不能读"错误。
  3. 一级封锁协议解决丢失修改(2023 单51):"一级协议解决读脏数据"错误(那是二级);"解决不可重复读"错误(那是三级)。
  4. 避免活锁用先来先服务(2022 单53):资源分配图是检测死锁的;"用资源分配图避免活锁"错误。
  5. 死锁只在并发时出现(2018 单51):单用户串行操作不会死锁;"禁止并发访问就能防死锁"是错误做法(封锁是并发控制,不是禁止并发)。
  6. DDL 隐含提交事务(2021 多90):CREATE/ALTER/DROP 执行即提交;"DDL 也在事务内可回滚"错误(MySQL 中 DDL 自动提交)。
  7. UPDATE 慢不一定是性能问题(2023 单49):CPU/I/O 都不忙却卡住 → 锁等待;别往"索引失效/磁盘慢"方向猜。

五、横向扩展(防漏网:本章边界内、真题尚未考但可能补考的点)

说明:横向扩展覆盖"大纲范围内但 9 年真题未考过"的知识,标注【新增概率】供取舍。

  1. 两段锁协议(2PL)【中】:加锁分两个阶段(扩展阶段只加锁、收缩阶段只释放)——保证并发调度的可串行性;名词辨析题。
  2. 可串行化调度【低】:并发调度的结果与串行执行相同——调度正确性概念。
  3. 时间戳排序协议【低】:另一种并发控制方法(不用锁)——与锁对比。
  4. 多版本并发控制(MVCC)【中】:读不阻塞写、写不阻塞读(快照读)——MySQL InnoDB 的实现,金融科技/数据库前沿题可能提及。
  5. 死锁的四个必要条件【中】:互斥/占有并等待/不可剥夺/循环等待——与 2.2 操作系统死锁同源(2019 单23 考过 OS 死锁成因),数据库死锁场景可能引用。
  6. 隔离级别(读未提交/读已提交/可重复读/串行化)【高】:四个隔离级别与三类不一致的对应——高概率补考点(读未提交→可能全乱;读已提交→防脏读;可重复读→防不可重复读;串行化→全防)。
  7. 事务日志与恢复的关系【高】:UNDO(回滚未提交)/REDO(重做已提交)——衔接 4.6 恢复。
  8. 乐观锁 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 优化。