
离考试只剩三天数据库教材翻到“并发控制”这一章你发现前面事务那章还能看懂一到锁、协议、隔离级别就像在看天书。这不是某一个人的问题。我见过很多考生在考场上能把“丢失修改”“不可重复读”的定义默写出来但一遇到“为什么两段锁协议能保证可串行化”“死锁怎么预防”这种需要推理的题目就卡住了。其实并发控制这一章是整本数据库教材里最接近真实工程的考点——你在生产环境日志里看到的Deadlock found when trying to get lock和试卷上的“死锁四个必要条件”本质是同一件事。这篇文章要把这章讲透。不靠死记靠建立起一条因果链并发为什么会出问题锁怎么解决问题协议怎么兜底隔离级别怎么落地。把这根链条走通再上考场你会发现大部分题目不是考背诵而是考你有没有真正理解数据库为什么非要“控制”并发不可。1. 先搞清楚并发控制到底在解决什么问题1.1 并发控制是事务的“后半场”任何数据库教材都会先讲事务的 ACID 特性。原子性、一致性、隔离性、持久性四个词背得滚瓜烂熟但很多人没意识到隔离性I就是并发控制要负责的事。事务在串行环境下很容易保证正确性——一个事务跑完下一个再跑结果不会乱。但数据库设计的目标是吞吐量和响应速度不可能让所有用户排队执行。多个事务同时执行时就要同时读写同一份数据于是隔离性被打破各种错误就来了。换句话说如果数据库不允许并发就根本不需要“并发控制”这一章。这一章所有的概念、锁、协议、隔离级别都是为了回答一个问题——多个事务同时操作同一份数据时怎么样让最终结果等价于某个串行顺序执行的结果。这个理解很关键。考试里如果出“什么是并发控制”这类简答题你把这句话写上去再展开讲锁机制就比单纯背定义得分高。1.2 三类经典并发异常考点核心并发事务如果完全不加控制会出现三类典型异常。这三类异常是几乎所有并发控制考题的地基必须先想明白。异常类型发生条件通俗解释后果丢失修改丢失更新两个事务都先读再写同一数据两个人都基于余额 100 各自操作后提交的覆盖了先提交的一次修改被丢掉脏读读脏数据一个事务读到另一事务未提交的数据读到对方还在修改、还没确定的数据使用了可能回滚的数据不可重复读一个事务读两次中间被另一事务修改同一个事务内第一次读到 100第二次读到 90事务内前后读数不一致这里面最容易丢分的是“丢失修改”。很多同学不理解为什么“两个事务都把余额从 100 改成 90”会是异常——答案是两个事务本来都应该基于原始的 100 去做计算各自加 10正确结果应该是 120。但因为并发执行互相覆盖最后变成 110等于丢了一次修改。提醒考试写答案时不要只写名词。要写清楚“哪个事务读了什么、改了什么时候被覆盖”。阅卷老师看的是因果关系不是术语堆砌。2. 封锁机制并发控制的底层骨架2.1 两种基本锁X 锁和 S 锁解决三类异常最直接的办法就是让冲突操作不能同时进行。这就是“锁”。锁分两种排他锁X 锁写锁事务加了 X 锁之后其他事务既不能读也不能写。共享锁S 锁读锁事务加了 S 锁之后其他事务还可以加 S 锁来读但不能加 X 锁来写。为什么要有两种锁而不是只有一把锁因为“读和读”之间其实不冲突。两个事务同时读一条数据谁也不会改坏它。如果只有一把互斥锁连读都要排队数据库的并发能力会低得没法用。S 锁和 X 锁的兼容矩阵是必背的当前持有的锁请求 S 锁请求 X 锁S 锁兼容不兼容X 锁不兼容不兼容这个矩阵不用死记理解一句话就行只有“读读”兼容其他组合都冲突。2.2 三级封锁协议从解决一个异常到解决全部异常光有锁还不够还要规定什么时候加锁、什么时候解锁。这组规定就是封锁协议。教材里常考的三级封锁协议本质是逐步加码一级封锁协议事务在修改数据前必须加 X 锁并持有到事务结束。它解决的是丢失修改。二级封锁协议在一级基础上读数据前必须加 S 锁读完立即释放。它额外解决脏读。三级封锁协议在二级基础上S 锁也要持有到事务结束。它额外解决不可重复读。这里最关键的判断是为什么一级协议能解决丢失修改因为两个事务都要改同一条数据谁先拿到 X 锁谁先改另一个事务必须等锁释放。等第一个事务提交了第二个事务再基于新值去改两次修改就不会互相覆盖。还有一个高频考点三级封锁协议是“可串行化”的充分条件但不是必要条件。这个表述很多同学看不懂。简单理解就是遵守三级封锁协议结果一定可串行化但结果可串行化不一定非要遵守三级封锁协议。考试如果考这个判断题要把这两层关系写清楚。2.3 两段锁协议真正保证可串行化的规则三级封锁协议保证的是隔离性但要保证并发调度的正确性还要引入两段锁协议2PLTwo-Phase Locking。两段锁协议规定事务分成两个阶段。第一个阶段只能加锁、不能解锁叫扩展阶段第二个阶段只能解锁、不能加锁叫收缩阶段。也就是说一个事务一旦开始释放锁就不能再申请新锁了。为什么这个规定能保证可串行化原理可以简化理解每个事务的加锁和解锁之间存在一个“分界点”这个分界点会把所有事务的锁操作排成某种先后顺序。事务之间一旦在锁上发生等待执行顺序就有了一条逻辑链条最终效果等价于按某个顺序串行执行。考试里有个经典判断题“遵守两段锁协议一定能避免死锁吗”答案是不能。两段锁协议能保证可串行化但事务之间仍然可能互相持锁、互相等待形成死锁。这完全是两件事千万别混。2.4 锁粒度与意向锁简答题的加分项锁粒度是指锁定数据范围的大小从大到小依次为数据库级、表级、页级、行级元组级。粒度越大开销越小但并发度越低粒度越小并发度越高但锁管理开销越大。多粒度封锁带来一个新问题一个事务给某一行加了 X 锁另一个事务想给整个表加 X 锁数据库怎么快速判断“表上能不能加锁”如果每一行都扫描一遍代价太大了。于是有了意向锁意向共享锁IS事务准备在某个数据行上加 S 锁先向表一级“预告”。意向排他锁IX事务准备在某个数据行上加 X 锁先向表一级“预告”。意向锁的核心价值是“通过表级标记快速判断行级锁的存在”不用逐行扫描。考试里如果写“锁粒度越细越好”这是不对的。粒度细代表并发度高但锁管理开销也高。正确的答题思路是“根据事务规模和冲突概率选择合适的粒度”。3. 从考点到答题死锁与可串行化3.1 死锁的四个必要条件死锁是并发控制里必考的知识点。四个必要条件要背但更要理解互斥条件至少有一个资源只能被一个事务独占。占有并等待事务已经持有某个锁又在等待另一个锁。不可剥夺已获得的锁不能被强行抢走只能自己释放。循环等待存在一个事务—资源的循环等待环。四个条件缺一不可所以预防死锁的思路就是破坏其中一个。考试常考的预防措施有一次封锁法一个事务一次性申请它需要的所有锁破坏“占有并等待”。顺序封锁法所有事务按固定顺序加锁破坏“循环等待”。死锁的处理还包括检测和恢复。检测的常见手段是等待图有向图图中出现回路就说明存在死锁。恢复时通常选择一个代价最小的事务回滚释放它持有的锁。3.2 可串行化调度怎么判断一个调度对不对考试里有一种题给出一组事务的读写操作序列问这个调度是否可串行化如果是写出一个等价的串行顺序。做题步骤可以固定为以下五步列出每个事务的读、写操作序列。找出冲突操作不同事务对同一数据项的操作且至少有一个是写操作。根据冲突操作的先后顺序画有向边如果 T1 的冲突操作发生在 T2 之前画 T1 → T2。检查有向图是否有环无环则可串行化有环则不可串行化。无环时按拓扑排序得到一个等价串行顺序。这个方法叫冲突可串行化判断核心依据是优先图precedence graph。它的本质是只要冲突操作的发生顺序能和一个串行执行顺序一致调度就是正确的。注意这里讨论的是“冲突可串行化”它不等于“视图可串行化”考试要看清楚题目问的是哪一种。3.3 一份 10 分简答题模板如果考“什么是两段锁协议为什么它能保证可串行化”建议答案分三层第一层写出定义。两段锁协议要求事务分为扩展阶段和收缩阶段扩展阶段只能加锁、不能解锁收缩阶段只能解锁、不能加锁。第二层写出关键点。加锁要一次性获得够用一旦事务开始释放锁就进入收缩阶段不能再加锁。第三层写出原因。由于每个事务存在一个加锁/解锁的分界点锁操作的先后顺序决定了冲突操作的相对顺序从而让整个调度等价于某个串行调度因此能保证可串行化。这三层写下来比只背一句话定义能多拿 4 到 5 分。阅卷老师想看到的不是“你知道这个概念”而是“你能证明你真的懂”。4. 隔离级别把并发控制串成一张表4.1 四档隔离级别与三类异常的关系SQL 标准定义了四个隔离级别它们和三类异常的关系几乎是每年必考隔离级别脏读不可重复读幻读读未提交READ UNCOMMITTED可能可能可能读已提交READ COMMITTED不会可能可能可重复读REPEATABLE READ不会不会可能标准定义下可串行化SERIALIZABLE不会不会不会注意一个细节点在 SQL 标准里可重复读仍然可能发生幻读。幻读和不可重复读的区别在于——不可重复读是针对一条已存在记录的修改导致的两次读取结果不同幻读是另一个事务插入或删除了新记录导致同一查询条件两次执行返回的记录集合不同。很多工程资料会补充MySQL 的 InnoDB 引擎在可重复读隔离级别下通过间隙锁等机制实际上也避免了幻读。这是工程实现和 SQL 标准的差异。考试时一定要先看清题目问的是“SQL 标准”还是“某个具体数据库”。4.2 隔离级别和封锁协议本质上是同一套逻辑隔离级别不是凭空出现的四档配置它本质上就是封锁协议的工程化套餐读未提交基本不读加锁相当于不用二级、三级封锁协议的内容。读已提交读前加 S 锁读完立刻释放相当于二级封锁协议。可重复读S 锁持有到事务结束相当于三级封锁协议。可串行化在可重复读基础上进一步限制操作顺序实际实现中通常用更强的锁或强制串行。把隔离级别表背下来之前先把三级封锁协议想明白这张表就能推导出来不用死记。4.3 工程里的真实选择不能盲目追求“绝对安全”做数据库课程设计或者实际开发时常见一个误区把隔离级别直接设成可串行化觉得最安全。但可串行化是以牺牲并发度为代价的高并发场景下大量事务排队等待吞吐量可能急剧下降。更合理的做法是分场景选档对一致性要求不高的展示类查询用读已提交甚至读未提交来降低成本对资金、订单、库存这类强一致数据至少用可重复读并配合业务层校验对需要完全互斥的关键操作再考虑可串行化同时把事务范围尽量缩短。工程和考试的差别就在这考试会问“哪个隔离级别能避免哪些异常”落地时你要问的是“我能接受哪类异常出现在我的业务里”。5. 时间戳与乐观/悲观别只会写“锁”5.1 时间戳排序机制不用锁也能保证正确性锁是并发控制的经典方案但不是唯一方案。时间戳机制是一种不用锁也能实现正确并发控制的方法。核心思路是每个事务进入系统时获得一个全局唯一的时间戳越早开始的事务时间戳越小、优先级越高。数据库为每个数据项记录“读它的最大时间戳”和“写它的最大时间戳”。当一个事务要读写某数据项时比较自己的时间戳和数据项上的时间戳如果自己的时间戳更小说明有更晚的事务已经抢先读写过了自己应该回滚重新开始。如果自己的时间戳更大说明自己是最新的允许操作并更新数据项上的时间戳。时间戳机制的优点是不会死锁——事务不需要等待锁冲突直接回滚。缺点是可能出现事务被反复回滚尤其是长事务浪费大量计算。所以它更适合冲突较少、读多写少的场景。5.2 悲观控制与乐观控制一对底层思路把并发控制的知识体系收拢一下你会发现所有方案其实分成两大类悲观控制假设冲突一定会发生先加锁、再操作。典型代表就是 X 锁/S 锁、两段锁协议。乐观控制假设冲突很少发生先正常操作提交时再检查是否有冲突有冲突就回滚重试。典型代表就是基于版本号的校验机制。一个重要的判断冲突率低时乐观控制比悲观控制效率高冲突率高时乐观控制会导致大量回滚反而是悲观控制更稳定。这个判断写进应用题的答案里比单纯罗列概念更能体现理解深度。数据库课程设计里常见的乐观锁实现本质上就是给表加一个版本号字段。更新时在 WHERE 条件里带上预期版本号-- 乐观锁示例先查出版本号 version 3 UPDATE account SET balance balance - 10, version version 1 WHERE id 1 AND version 3;如果影响行数为 0说明版本号被其他事务改过了需要重新查询再重试。同样的场景悲观锁的做法是显式加行锁-- 悲观锁示例先锁住这行再更新 SELECT * FROM account WHERE id 1 FOR UPDATE;这两段代码不是考点但它们能帮你把“悲观/乐观”从抽象概念变成看得见的行为。6. 零基础 3 小时怎么分配从背考点到上考场6.1 前 90 分钟概念和锁机制第一优先级永远是三类异常 X 锁/S 锁 锁兼容矩阵。这三块是基础中的基础必须达到“闭卷能默写”的程度。建议用一张 A4 纸画三栏表异常名称 / 场景描述 / 用哪种锁或协议解决。不要抄书要合上书自己凭记忆写写不出来的再看书。这一轮输出的过程比输入重要得多。6.2 中间 60 分钟协议、死锁、隔离级别接下来集中攻三级封锁协议、两段锁协议、死锁四个条件、隔离级别表。这几块彼此关联建议画一张从低到高的“安全性阶梯”读未提交 → 读已提交 → 可重复读 → 可串行化每一级标注三件事能解决什么异常、不能解决什么异常、付出的代价是什么。画完这张阶梯你就可以应付大部分选择题和判断题。6.3 最后 30 分钟刷两种必考题如果时间有限至少要学会做两类题给定并发调度判断是否可串行化——按 3.2 的优先图五步法。给定场景选择隔离级别或封锁协议——按“异常类型 → 用哪级封锁协议 → 对应隔离级别”的链条写。这两种题千万不要只背结论一定要亲手画一次优先图、亲手列一次锁请求顺序。画过之后你才会发现之前看不懂的“调度”“冲突”到底长什么样。6.4 考场上最容易丢分的三个点第一把不可重复读和幻读混为一谈。第二答“两段锁协议能避免死锁”。第三只默写定义不写“为什么”。这三个坑几乎是每年重灾区。另外如果题目考到国产数据库比如达梦DM、人大金仓这类环境下的并发控制题目不要慌。它们的并发控制机制遵循同样的关系数据库理论和 SQL 标准考的还是锁、隔离级别、死锁那套底层逻辑不会跳出这章的知识边界。7. 把考点翻译成工程理解才算真正学透7.1 从教材概念到真实日志如果你以后做开发会发现教材里的概念会和真实系统日志一一对应。以 MySQL 为例Deadlock found when trying to get lock; try restarting transaction—— 数据库检测到死锁自动回滚其中一个事务。这就是“死锁的检测与恢复”。Lock wait timeout exceeded—— 事务等待锁超时被数据库主动终止。这就是“锁等待超时策略”。所以学这一章时最好带着“将来我要在日志里看到它”的心态。比死背“死锁四个条件”更有价值的是知道事务写得太长锁持有时间过久互相等待的概率就会大幅上升——这才是工程里死锁最常见的原因。7.2 遇到死锁或锁等待按这个顺序排查真实环境里遇到锁相关报错不要急着改代码。按下面的顺序逐层排查看现象是报错、卡住、还是刚发版后出现错误码是死锁还是锁等待超时看事务事务是不是太长一条 SQL 后面是不是还跟着大量业务逻辑没提交多个事务访问表的顺序是不是不一致看索引相关表有没有合适的索引没有索引时数据库可能扫描大量行把行锁变成表锁锁的范围被放大。看隔离级别隔离级别是不是设置得过高可重复读和可串行化在高并发下更容易积累锁。看并发量同一时间点是不是有大量事务集中访问同一批热点数据如果是光改隔离级别没用要从业务上拆分热点。这个排查链路对应到考点上就是“死锁条件 封锁协议 隔离级别”的组合应用。7.3 给开发者的三条落地建议第一事务越短越好。锁的持有时间和事务运行时间绑定长事务等于长时间占着锁一定会拖垮并发度。能用一条 UPDATE 完成的事情不要拆成“查出来—Java 里算—再写回去”三步。第二访问数据顺序尽量固定。多个事务都先更新表 A 再更新表 B而不是有的先 A 后 B、有的先 B 后 A可以从源头减少循环等待。第三出现死锁优先让程序重试而不是人工干预。数据库的死锁恢复策略会主动回滚其中一个事务应用层要做好异常捕获和重试机制。把重试次数控制在合理范围比如 3 次以内同时保留日志方便复盘。这三条不是考试内容但它们都是从“死锁四个条件”和“两段锁协议”里推出来的工程版结论。你如果能把教材里每一个抽象概念都翻译成一个工程行为这一章才算真正学透了。回到最开始的问题3 小时能不能速成 90我的答案是能但前提是你不要把这章当成“需要背的考点”而要当成“数据库为什么非要这么设计”的逻辑链。先通读一遍这篇文章然后把书合上自己画一张锁兼容矩阵和三级封锁协议对照表。画得出来你就已经超过大多数只会背定义的考生了。