第 20 章 数据库事务——1970年代,为什么一笔转账绝不能只成功一半?标记本章已读大字阅读打印本章回到上次阅读位置交互模块加载失败。正文仍可阅读,请刷新重试。正在加载交互实验…第一季 · 人类如何驯服数据历史主线:1970—1992年 · Codd、IBM System R、Jim Gray、ACID、WAL、ARIES 与数据库恢复1970年代,美国加利福尼亚州圣何塞。IBM 的研究人员正在尝试把一种崭新的数据模型变成真正可以工作的数据库系统。他们面对的,不只是如何高效查询一张表。而是一个比查询更危险的问题:如果数据库正在修改数据,计算机却在最关键的时刻突然断电,怎么办?假设银行有两个账户:账户 A¥1,000账户 B¥500现在,A 向 B 转账100元。程序需要完成两件事:第一步,从 A 扣除100元。第二步,向 B 增加100元。正常情况下,最终状态应当是:A:900元。B:600元。总金额仍然是1500元。可是,如果执行完第一步,机器突然断电呢?数据库可能留下:A:900元。B:500元。100元凭空消失。反过来,如果先向 B 加钱,随后宕机,也可能凭空多出100元。这看起来像一个简单的程序问题。其实,它是数据库系统最核心的挑战之一。因为在一个真实系统中,程序不可能保证自己永远不会在两条指令之间停止。磁盘可能故障。进程可能崩溃。操作系统可能重启。网络可能中断。而多个用户还可能同时修改同一份数据。于是,数据库研究者必须设计一种机制:把多次操作组合成一个不可随意拆开的逻辑整体。这就是:Transaction——事务。第一幕:1970年,Edgar Codd 改变了数据库的思考方式来源Edgar F. Codd1923—2003 · 英国出生的 IBM 研究员 · 1981年度图灵奖得主他提出关系数据模型,为现代关系数据库奠定了数学基础。1970年6月,Codd 在 Communications of the ACM 发表:A Relational Model of Data for Large Shared Data Banks《大型共享数据库的关系数据模型》。这篇论文提出,用户不应该被迫了解数据究竟存放在哪块磁盘、使用什么指针结构或者以什么物理顺序组织。数据库应该提供更加独立、抽象的数据表示和操作方式。[Communications of the ACM][IBM]这听起来像是在讨论 SQL,但当时现代 SQL 还没有完全形成。Codd 的研究目标更深:让数据的逻辑结构与物理存储方式相互分离。例如,我们可以把账户表示为一张关系表:AccountIDBalanceA1000B500用户只关心:账户 A 的余额是什么?如何更新账户 B?两张表如何关联?不应该需要知道每条记录保存在磁盘的哪个物理页面。但一个新的问题随即出现了:既然允许多个用户共享这些数据,数据库如何保证多人同时修改时不会互相破坏?而如果系统执行到一半崩溃,又如何恢复?这将成为 System R 的重要研究任务。第二幕:1976年,IBM System R 把关系模型带入真实系统来源1973年前后,IBM 圣何塞研究实验室启动了 System R 项目。目标是验证:关系数据库能否不仅在数学上优雅,还能在实际环境中达到商业应用所要求的性能和可靠性?1976年,14位作者共同发表论文:System R: Relational Approach to Database Management。研究内容包括关系查询、授权、完整性约束、共享数据更新,以及日志与恢复子系统。其中一位作者,就是后来在事务处理领域作出重要贡献的:Jim Gray。[IBM Research][国家科学院]请注意一个历史区别。System R 不是第一套拥有事务概念的系统。可靠更新、恢复和并发控制的思想在更早的文件系统、事务处理和数据库研究中已经存在。System R 的意义,是在新兴的关系数据库环境中,把这些能力与查询、数据独立性、多用户访问结合起来,推动其走向实用。后来,System R 的研究还深刻影响了 SQL 和商用关系数据库的发展。[IBM Research]研究者面临的核心矛盾为了让数据库运行得快,不能每修改一个数字,就立即同步重写全部相关磁盘页面。但是,为了保证崩溃后能够恢复,又必须留下足够可靠的信息,知道哪些修改应该保留、哪些修改应该撤销。与此同时,多名用户还可能并发更新同一账户。这意味着数据库至少要同时面对三个不同的问题:事务完整性:一笔转账不能只执行一半。并发正确性:不同事务之间不能产生不允许的相互干扰。崩溃恢复:系统停止后,必须能够识别并恢复已提交与未完成的操作。这三个问题共同塑造了现代事务处理系统。第三幕:ACID——数据库事务的四项核心承诺1983年,Theo Haerder 与 Andreas Reuter 发表著名综述:Principles of Transaction-Oriented Database Recovery。这篇论文系统整理了数据库事务与恢复中的概念,成为 ACID 特性历史上的重要文献。[IBM Research]ACID 由四个英语单词组成。AAtomicity — 原子性事务中的操作作为一个逻辑整体生效。转账要么全部成功,要么全部撤销。CConsistency — 一致性一个正确执行的事务应当使数据库从满足约束的状态转移到另一个满足约束的状态。数据库约束与应用逻辑共同决定哪些状态合法。IIsolation — 隔离性并发事务的相互影响受到约束。具体保证取决于所采用的隔离级别,严格的可串行化是最强的常见事务隔离目标之一。DDurability — 持久性在规定的故障与存储假设下,成功提交的事务应当能够在系统崩溃重启后恢复。这些概念很容易背下来。但在工程实践中,每一个字母都对应一整套复杂机制。尤其要注意:ACID 不是一个自动保证业务永不出错的魔法开关。例如,数据库可以正确执行一条把余额增加1000元的 SQL。但如果业务本来只应该增加100元,那仍然是应用程序的错误。事务机制负责执行一致的状态转换,不负责猜测业务开发者的真实意图。第四幕:亲手让转账在最危险的地方突然停止我们先只研究原子性。假设 A 有1000元,B 有500元。现在转账100元。你可以点击执行每一步,也可以在任意步骤触发故障。转账崩溃实验室观察部分修改为什么必须能够撤销执行中账户 A¥1,000账户 B¥500当前显示总额¥1,500启用事务回滚保护(概念模拟)准备从 A 向 B 转账 ¥100。执行下一步 →模拟故障↶本实验仅模拟可见账户状态和事务回滚结果,尚未模拟真实数据库的 WAL 持久化、脏页刷盘及崩溃恢复步骤。这个实验有一个值得反复思考的地方。程序已经执行过的指令,不代表数据库就必须永久接受这些修改。如果事务尚未提交,数据库需要有办法撤销这些操作。而如果事务已经确认成功,它又需要在崩溃之后有办法重新恢复这些操作。于是,现代数据库必须同时具备两种能力:UNDO:撤销不应当生效的修改。REDO:重做应该生效、却尚未完整写入数据页的修改。这两个能力,将把我们带到数据库事务最精彩的技术——日志恢复。第五幕:Jim Gray——把数据库事务变成一套可证明的工程体系来源Jim Gray1944—2007 · 数据库与事务处理研究先驱1998年度 ACM 图灵奖得主,在数据库恢复、并发控制与可靠事务处理方面具有重要贡献。在 System R 的研究团队中,Jim Gray 特别关注两件事。第一,多名用户同时修改数据时,怎样防止相互干扰。第二,系统在任何可能发生的故障点停止后,怎样恢复到合法状态。同事 Bruce Lindsay 在2008年的历史回顾中指出,Gray 在1970年代 IBM 圣何塞实验室工作期间,帮助建立了现代在线事务处理所依赖的基础概念和实现技术。[IBM Research]1980年2月,Gray 完成一份技术报告:A Transaction Model。报告已经系统讨论事务恢复、UNDO、REDO、锁、死锁以及两阶段提交。1981年,他又发表:The Transaction Concept: Virtues and Limitations。《事务概念:优点与局限》。Gray 在论文中强调:事务是一种状态转换,其价值在于让复杂的数据更新能够作为可靠的单元进行管理。[infolab.usc.edu][71E79A74.doc]这里有个很重要的历史认识:事务并不是从 SQL 的 BEGIN 和 COMMIT 两条语句开始的。SQL 只是提供了使用事务的一种接口。真正困难的是底层的理论与机制:系统怎样定义合法提交?怎样限制并发操作?怎样知道磁盘里哪些修改可以信任?怎样在重启后恢复?这些问题远比写下 COMMIT 六个字母复杂。第六幕:数据库如何在断电后恢复?秘密是先写日志现在我们来到最值得深入理解的技术:Write-Ahead Logging,预写式日志,简称 WAL。想象一下。一张账户数据页已经被数据库加载到内存。A 当前余额1000元。程序准备将它修改为900元。如果立刻修改内存,没有问题。但如果内存内容尚未写回磁盘,计算机就断电,修改可能消失。你可能说:“那就每次修改,都立即把数据库页面写入磁盘。”这种方法当然可以提供一部分持久化保障。但代价非常高。一个事务可能修改几十个不同的数据页。如果每次提交都要求把这些页面全部同步写回磁盘,吞吐量会受到严重影响。WAL 提出另一种顺序:先持久化描述修改的日志,再允许相关数据页以适当时机写回磁盘。一个简化的 WAL 记录假设事务 T100 正在转账。日志可以概念性地写成:text复制代码BEGIN T100 UPDATE T100 A: 1000 → 900 UPDATE T100 B: 500 → 600 COMMIT T100真实数据库的日志格式远比这复杂,可能记录物理页面变化、逻辑操作或二者的某种组合。但核心顺序是一样的。数据库必须确保:在相应的数据页修改被持久化之前,必要的日志记录已经先被可靠写入。而在采用相应的同步提交保证时,系统不能在相关提交信息尚未安全持久化的情况下,就把事务当成已达到该级别的持久提交。PostgreSQL 官方文档明确介绍了 WAL 的这一基本原则,并指出:通过日志恢复,数据库不需要在每个事务提交时都同步刷写所有被修改的数据页。[PostgreSQL]为什么先写日志会更高效?原因之一,是日志通常具有顺序追加的特性。数据库的数据页则可能位于不同存储位置。相对而言,把若干修改的信息连续写入日志,往往比逐一同步刷新大量分散的数据页更加高效。数据库还可以把多个事务的日志刷盘合并处理。这就是 Group Commit(组提交)的基本思路。一个常见误解:有 WAL 就绝对不会丢数据?并非如此。WAL 必须结合正确的持久化配置和可靠的存储行为。例如,PostgreSQL 的 synchronous_commit、fsync 等配置会影响提交确认与持久化语义。某些为了性能而放宽持久化要求的配置,允许最近确认的事务在崩溃后丢失。存储设备如果虚假报告写入已经完成,也可能破坏数据库对持久化的假设。因此,准确的说法是:在规定的提交、刷盘和存储可靠性条件得到满足时,WAL 能帮助数据库恢复已持久提交的事务。第七幕:把数据库断电的每一个瞬间拆开现在我们做一个真正接近故障恢复机制的实验。事务 T100 需要完成:A:1000 → 900。B:500 → 600。我们把“内存状态”和“磁盘状态”分开。同时记录 WAL 是否已经持久化。WAL 崩溃恢复实验室故障注入① 修改刚发生,还没有可靠日志② 更新日志已持久化,尚未提交③ COMMIT 日志已持久化,数据页未必刷盘④ COMMIT 与数据页都已持久化事务状态未提交WAL:没有可靠的事务修改日志内存:A=900,B=500(部分内存修改)磁盘:A=1000,B=500◇系统重启后重启后使用原有磁盘状态。事务未提交。这是概念模型,结合了常见 WAL/UNDO/REDO 思想。不同数据库引擎对未提交版本的恢复、提交记录及缓存页处理方式并不相同;不能把图中模拟步骤视为某个特定数据库的完整内部流程。最关键的时刻,是第三种情况。数据库的数据页可能还没有写完。但只要提交所需的 WAL 已经按规定持久化,就可以在重启时利用日志重建必要的结果。这就把两个原本矛盾的目标联系起来了:不必每次提交都写完全部数据页,却仍然能够在符合持久化假设时保证已提交事务可恢复。第八幕:1992年,ARIES 把恢复算法推向系统化来源C. MohanIBM 数据库研究者长期研究数据库恢复、并发控制、日志管理与存储系统。1992年,C. Mohan 与 Don Haderle、Bruce Lindsay、Hamid Pirahesh、Peter Schwarz 在 ACM Transactions on Database Systems 发表:ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging。这篇论文提出并系统描述了 ARIES 恢复方法。ARIES 的名称来自:Algorithm for Recovery and Isolation Exploiting Semantics。论文讨论如何在支持细粒度并发与部分回滚的同时,使用预写式日志可靠地进行故障恢复。[IBM Research]ARIES 的三个经典恢复阶段1. Analysis — 分析找出崩溃时的事务状态、脏页信息以及恢复起点↓2. Redo — 重做重新执行恢复所需的历史修改↓3. Undo — 撤销回滚崩溃时尚未提交、需要撤销的事务效果这里值得注意的是:ARIES 并非简单地“只重做已提交事务”。它采用一种常称为 repeating history 的恢复思想:先恢复到足以反映崩溃前历史的状态,再撤销未完成事务的相应操作。它还使用补偿日志记录(Compensation Log Records,CLRs)等机制,帮助正确处理恢复过程中再次崩溃的情况。这个设计极其重要。因为恢复程序本身也可能运行到一半再次断电。可靠的恢复算法,必须允许恢复过程中再次发生故障。这听起来似乎有点过分谨慎。但对于银行、库存、订单和财务系统而言,正是这种严格性,构成了数据可信的基础。第九幕:事务原子性解决了,两个用户同时修改怎么办?现在,假设只有一个数据库,也有可靠的 WAL。系统已经能够保证一笔转账不会只成功一半。那么并发问题是不是也解决了?并没有。假设某个商品库存只有10件。两个客户同时下单。每人购买7件。程序逻辑是:text复制代码读取库存 判断是否足够 库存减7现在出现如下执行顺序:时刻事务 T1事务 T21读取库存102读取库存103判断10≥7,通过4判断10≥7,通过5写入库存36写入库存37提交提交两个事务都认为自己成功购买了7件。但是最终库存却是3。实际上已经售出14件,超过了原有的10件库存。这是一种典型的并发更新问题。具体可表现为 Lost Update(丢失更新)或业务层面的并发校验失效,取决于 SQL 形式与数据库隔离机制。注意:即使每个事务单独来看都是原子的,两个事务并发执行时仍然可能产生不正确的结果。这就是 ACID 中的 I:Isolation,隔离性。实验:两次购买如何造成超卖?并发购买实验室Lost Update启用数据库原子条件更新策略数据库库存10 件初始库存10件,每笔请求购买7件。事务 T1尚未读取事务 T2尚未读取库存为10。两名客户各想购买7件。下一次数据库操作↶一种有效的数据库级防护方式,是把检查与修改放入同一条原子更新语句:sql复制代码UPDATE inventory SET quantity = quantity - 7 WHERE product_id = 1001 AND quantity >= 7;然后必须检查实际影响行数。如果返回0行,说明更新条件没有成立。这样可以避免“先 SELECT,然后在应用程序中计算,再无条件 UPDATE”造成的典型竞争问题。不过,真实订单还可能涉及订单记录、支付状态和库存预留,仍然需要正确的事务设计。第十幕:MVCC——让读取和写入不必总是互相等待来源如果我们为保证正确性,把所有数据库操作都串行化,性能可能非常糟糕。例如:报表系统正在查询100万条订单。与此同时,业务系统希望新增一笔订单。难道必须等待报表读完?数据库研究者长期寻找一种方法:既能让不同事务看到合理一致的数据,又能够允许更多并发操作。这就是多版本并发控制:MVCC,Multi-Version Concurrency Control。核心思想:一个数据可以存在多个版本假设账户 A 最初余额1000元。事务 T1 在某个快照中开始读取。后来 T2 把余额修改成900元并提交。此时数据库内部可以保留与事务可见性有关的多个版本。一个账户的两个版本较早版本¥1,000旧快照可能可见较新版本¥900新快照可能可见这使数据库可以根据事务快照及可见性规则,决定每个事务应当看到哪个版本。例如,在 PostgreSQL 的 REPEATABLE READ 隔离级别下,事务通常会保持一个稳定的事务快照。而默认的 READ COMMITTED 隔离级别,则通常为不同语句取得新的快照,因此同一事务内两次查询可能看到不同的已提交结果。PostgreSQL 官方文档对此有明确说明。[PostgreSQL][PostgreSQL]MVCC 不是“完全不需要锁”这是一个非常常见的误解。MVCC 可以减少读写之间某些不必要的相互阻塞。但它不能自动解决所有写写冲突,也不能让任意并发事务都具有可串行化结果。某些操作仍然需要行锁、表锁、冲突检查或其他同步机制。四种常见隔离级别隔离级别核心意义READ UNCOMMITTEDSQL 标准允许脏读;具体数据库可能提供更强保证READ COMMITTED不读取未提交事务的修改REPEATABLE READ提供更稳定的重复读取语义SERIALIZABLE已成功提交事务的结果等效于某种串行执行顺序注意,各数据库对这些隔离级别的具体实现并不完全相同。例如 PostgreSQL 的 READ UNCOMMITTED 实际按 READ COMMITTED 行为处理;其 REPEATABLE READ 还比 SQL 标准最低要求更强,但仍可能出现串行化异常。[PostgreSQL]这意味着:仅仅知道隔离级别的名称还不够,还需要理解所使用数据库的实际实现和保证。第十一幕:一笔事务跨越两台数据库怎么办?现在,我们来到一个更难的问题。账户 A 位于数据库服务器 S1。账户 B 位于数据库服务器 S2。需要转账100元。但单台数据库的 BEGIN TRANSACTION,通常只能直接管理它所控制的事务资源。如果涉及两个独立数据库实例,我们如何保证双方共同提交或共同回滚?这就引出了:Two-Phase Commit,2PC,两阶段提交。注意,它与并发控制里的 Two-Phase Locking(两阶段锁)不是同一概念。第一阶段:Prepare协调者分别询问参与者:“这笔事务能否提交?请先准备好,并保留必要的恢复信息。”数据库 S1:已经准备好从 A 扣款。数据库 S2:已经准备好向 B 加款。它们将事务进入可恢复的准备状态。但尚未执行最终提交决定。第二阶段:Commit 或 Rollback如果所有参与者都回答可以提交,协调者作出提交决定。然后通知各方正式提交。如果有参与者无法准备成功,协调者决定回滚。两阶段提交:事务协调过程协调者 Coordinator要求所有参与者准备事务↓S1PREPAREDS2PREPARED↓协调者发出最终决定COMMIT 或 ROLLBACK这项机制能够协调多个支持相应事务协议的资源管理者。但它也有一个很重要的弱点:某些故障情况下,已经进入准备状态的参与者可能需要等待协调者恢复,才能得知最终决定。这就是经典两阶段提交的阻塞问题。如果协调者崩溃,而部分参与者已经进入 Prepared 状态,它们可能必须保留锁和其他事务资源。这会给系统带来很大压力。因此,2PC 不是分布式事务的万能答案。在不同业务里,还可能采用 Saga、补偿事务、Outbox、消息驱动流程等方式,但这些方式提供的保证和失败处理语义也不同,不能简单替代严格的原子提交。PostgreSQL 是否支持2PC?支持。PostgreSQL 提供:sql复制代码PREPARE TRANSACTION 'tx_100';以及:sql复制代码COMMIT PREPARED 'tx_100';或者:sql复制代码ROLLBACK PREPARED 'tx_100';但官方文档明确强调,这类事务主要面向外部事务管理系统协调的场景,不建议普通应用随意使用。因为长期处于准备状态的事务可能持有锁,并阻碍数据库清理等操作。[PostgreSQL]第十二幕:把这些理论变成一段真正可靠的 SQL我们以 PostgreSQL 为例。假设账户表:sql复制代码CREATE TABLE accounts ( account_id BIGINT PRIMARY KEY, balance NUMERIC(18, 2) NOT NULL CHECK (balance >= 0) );初始化:sql复制代码INSERT INTO accounts (account_id, balance) VALUES (1, 1000.00), (2, 500.00);现在进行一笔100元转账。这里使用显式事务与行级锁。sql复制代码BEGIN; -- 锁定涉及的账户,避免并发修改干扰 SELECT account_id, balance FROM accounts WHERE account_id IN (1, 2) ORDER BY account_id FOR UPDATE; -- 应用程序确认两账户存在、余额充足 UPDATE accounts SET balance = balance - 100 WHERE account_id = 1 AND balance >= 100; -- 必须检查影响行数为1,否则 ROLLBACK UPDATE accounts SET balance = balance + 100 WHERE account_id = 2; -- 必须检查影响行数为1,否则 ROLLBACK COMMIT;这段 SQL 展示的是核心事务流程,不是完整的资金清算系统。生产环境还必须处理:唯一业务请求编号与幂等性。死锁与事务重试。错误及连接中断。账户权限校验。审计流水。对账。以及不同业务状态的约束。尤其是:数据库提交结果不确定时,不能无条件重新发送同一笔转账。例如客户端发送 COMMIT 后网络断开。客户端不知道:事务到底提交成功了,只是响应丢失?还是数据库还没提交就发生了故障?正确办法通常是通过唯一交易编号检查业务最终状态,并设计可安全重试的请求处理机制。这也是 Jim Gray 所强调的事务技术价值:它把复杂的失败与并发问题,变成可以系统分析、恢复和验证的状态转换。[71E79A74.doc]终章挑战:COMMIT 返回了,数据页还没有写盘,可以断电吗?假设数据库使用正确配置的 WAL,并满足相应的持久化保证。事务已经提交。提交日志也已经可靠写盘。但是,包含账户余额的数据页还没有全部写入数据库主数据文件。此时突然断电。数据库重新启动后,应该怎么办?你来判断正确的恢复行为数据页没写完,所以必须丢弃已经提交的事务根据可靠的 WAL 记录恢复必要的修改不需要恢复,磁盘中的数据必然已经是最新的允许一部分更新保留,另一部分更新丢失尾声:数据库为什么值得我们信任?1970年,Edgar Codd 提出了关系数据模型,让数据管理逐渐摆脱对物理存储结构的依赖。1970年代,IBM System R 证明,关系模型可以与实际的查询、并发控制、日志恢复结合起来。Jim Gray 推动事务处理形成系统的理论与工程方法。1983年,Haerder 与 Reuter 对事务恢复概念进行重要整理。1992年,Mohan 等研究者发表 ARIES,让复杂数据库的恢复机制得到更加系统化的表达。这段历史揭示了数据库工程最重要的一条原则:数据正确性不能建立在“程序不会崩溃、网络不会中断、用户不会同时操作”的假设之上。相反,一个可靠的系统必须主动假设:程序会在任何时刻停止。写入可能只完成一部分。多个事务会交错执行。恢复过程自身也可能再次故障。然后,通过事务、日志、并发控制和严格的不变量,让这些异常不至于破坏数据。这就是为什么数据库不只是一个保存数据的文件系统。它是一套在不可靠执行环境中,尽可能维持可靠状态转换的工程体系。第二十章知识总结概念核心思想Transaction多次数据库操作组成一个逻辑单元Atomicity全部生效或全部撤销Consistency维持数据库定义的合法约束Isolation约束并发事务之间的相互干扰Durability已按规定提交的数据能够在故障后恢复WAL先可靠记录修改,再按规则写入数据页UNDO / REDO撤销未完成的效果、重做必要修改ARIES分析、重做、撤销的经典恢复体系MVCC通过多个数据版本实现并发可见性管理2PC协调多个参与者的原子提交决定Idempotency避免重试导致业务操作重复生效本章历史原始资料Codd(1970):《A Relational Model of Data for Large Shared Data Banks》IBM(1976):《System R: Relational Approach to Database Management》Jim Gray(1981):《The Transaction Concept: Virtues and Limitations》Haerder、Reuter(1983):《Principles of Transaction-Oriented Database Recovery》Mohan 等(1992):《ARIES》原始论文PostgreSQL 官方文档:Write-Ahead LoggingPostgreSQL 官方文档:并发控制与 MVCC下一章预告第二十一章:死锁——1965年,为什么两个程序都没有出错,整个系统却永远无法继续?1960年代,计算机开始允许多个程序同时运行。每个程序都需要资源。打印机。磁带机。内存。文件。数据库锁。现在想象:程序 A 已经拿到了资源 X,却还在等待资源 Y。程序 B 已经拿到了资源 Y,却还在等待资源 X。于是:A 等 B。B 等 A。没有人违反程序规则。没有人发生硬件故障。但整个系统就这样停住了。这就是:Deadlock,死锁。下一章,我们将从 Edsger Dijkstra 的并发程序研究、资源分配图、银行家算法和数据库锁管理出发,研究一个非常尖锐的问题:为什么两个单独看都完全正确的程序,放在一起运行,却可能永远无法完成?上一章第十九章:缓存算法的战争——1969年,为什么给计算机增加内存,反而可能让它变慢?下一章第二十一章:死锁——1960年代,为什么两个程序都没有出错,整个计算机却停止了工作?