这一章在做什么
进程会崩,网络会断,几个客户端会同时改同一份数据。事务把一组读写打成一个包:要么全部生效,要么全部撤销,出了错应用重试就行。这一章的大头是隔离:为了性能,数据库常用读已提交、快照隔离这类弱隔离,各挡住一部分竞争,丢失更新和写偏斜却可能漏过去;真正的可串行化有串行执行、两阶段锁、SSI 三种做法,代价各不相同。最后讲跨多个节点时怎样用两阶段提交让大家一起提交,以及它在出故障时的麻烦。
读之前:先预测
第 6、7 章把数据复制、分片到多台机器上;这一章回到一个更老的问题:几个客户端同时读写、任何一步都可能崩溃时,数据库能替应用担下多少。第 4 章的预写日志在这里是原子性和持久性的底座,第 6 章「最后写入胜出」会丢掉的更新,这里正面讲怎么防。往后,第 9 章讲网络和时钟为什么靠不住,第 10 章用共识算法替换两阶段提交里那个单点的协调者,第 12、13 章接着讲「恰好一次」和幂等。
几个客户端同时读写同一份数据,任何一步都可能失败。数据库能把多少种竞争和故障替应用挡掉?挡得越多,要付出多少等待和中止?
- 读已提交已经保证了不脏读、不脏写,为什么两个人同时给计数器加一还会少加一次?§2.1§2.3
- 写偏斜和丢失更新差在哪?为什么快照隔离自带的丢失更新检测抓不住它?§2.4
- 两阶段锁「先等再说」,SSI「先做再查」。它们各在什么样的负载下吃亏?§3.2§3.3
- 两阶段提交里,协调者一崩溃,参与者为什么只能干等?§4.1
这一章的骨架
这一章按「事务要对付什么」分三块:出错(§1),并发(§2–§3),跨节点(§4)。篇幅最大、最容易乱的是并发:§2 一级一级讲弱隔离,每讲一级就冒出一种它挡不住的竞争,到 §3 才给出能全挡住的三种实现。读的时候在纸上画一张表:横着是隔离级别,竖着是竞争的种类,每读到一处就填一格。
边读边填:每行一个问题,读到答案就写一两句。只存在你这台设备的浏览器里,别人看不到。
| 要找的 | 带着这个问题读,找到了就写下来 |
|---|---|
| A 到底管什么 | ACID 的原子性不管并发,那它管什么?作者觉得换成哪个词更贴切?§1.1 |
| C 归谁 | 为什么说一致性常常不是数据库自己的属性?§1.1 |
| 快照怎么做 | 一行存几个版本?一个事务能看到哪个版本,由哪几条规则决定?§2.2 |
| 写偏斜的套路 | 书里几个写偏斜的例子都按哪几步走?为什么有的能靠 SELECT FOR UPDATE 补救、有的不能?§2.4 |
| 两阶段锁的代价 | 它慢在哪?为什么高百分位的延迟特别难看?§3.2 |
| SSI 何时中止 | 它检测哪两种情况?为什么要等到提交时才决定中止?§3.3 |
| 不能反悔的点 | 两阶段提交里,哪两个时刻一过就不能回头?§4.1 |
读完了?对照我找到的
- A 到底管什么:出错时把这个事务已经做的写全部丢掉,让应用可以放心重试;作者说叫「可中止性」更贴切。
- C 归谁:不变式是应用定义的,数据库只能替你检查声明成约束的那部分,其余要靠应用把事务写对。
- 快照怎么做:每行可以有好几个版本,标着创建它和删除它的事务号;看开始时哪些事务还在进行、谁的事务号更大、谁中止了。
- 写偏斜的套路:先查条件,按结果做决定,再写入,而写入改变了刚才的前提;查的是「不存在的行」时没有行可锁。
- 两阶段锁的代价:读写互相阻塞,并发度低,死锁更多;一个慢事务或一个大读事务就能让别人全排队。
- SSI 何时中止:读快照时忽略了别人未提交的写、读完之后别人又写了;只读事务不必中止,对方也可能回滚。
- 不能反悔的点:参与者回答「是」的那一刻,协调者把决定写进日志的那一刻。
分节地图
全章约 2.2 万词(不算代码和图注),是全书最长的一章,建议分六次读。第二、三次是全章的重心,最好连着读;第四、五次讲可串行化的三种实现;第六次的 XA 部分细节多,抓住「存疑时一直持锁」这一条就够。
- 第一次坐下§0–§1.2 · 约 4300 词 · 事务是什么,ACID 四个字母各管什么
- In the harsh reality of…约 500 词开篇会出错的种种;事务是为简化编程而发明的,不是每个应用都离不开它。
- What Exactly Is a Transaction?约 300 词事务到底是什么从 System R 到 NoSQL 丢掉事务,再到 NewSQL 把它找回来。
- The Meaning of ACID约 1900 词ACID 的含义四个字母逐个拆开。抓住两点:原子性不管并发,一致性属于应用。方框讲持久性的种种不可靠。
- Single-Object and Multi-Object Operations约 1600 词单对象与多对象操作什么时候非得一次改好几个对象;单对象也要原子性;中止后重试的几个坑。
- 第二次坐下§2–§2.2 · 约 3700 词 · 读已提交和快照隔离
- Weak Isolation Levels约 500 词弱隔离级别数据库为什么不默认可串行化;弱隔离造成的 bug 有多真实。
- Read Committed约 1000 词读已提交两条保证:不脏读、不脏写。各自通常怎么实现,为什么读不加锁。
- Snapshot Isolation and Repeatable Read约 2200 词快照隔离与可重复读读偏斜是什么;多版本和可见性规则怎么给出一致的快照;名字为什么一团乱。
- 第三次坐下§2.3–§2.4 · 约 3100 词 · 两个事务同时写
- Preventing Lost Updates约 1400 词防止丢失更新读-改-写循环的几种对策,各自要应用做什么;复制的数据库里为什么又不一样。
- Write Skew and Phantoms约 1700 词写偏斜与幻读两人改的不是同一行,照样出错。抓住几个例子共同的四步套路。
- 第四次坐下§3–§3.2 · 约 3700 词 · 可串行化:真串行和两阶段锁
- Serializability约 400 词可串行化弱隔离的困境;实现可串行化的三条路。
- Actual Serial Execution约 1600 词真正的串行执行单线程一次跑一个事务为什么又行得通了;存储过程和分片各有什么代价。
- Two-Phase Locking约 1700 词两阶段锁读写互斥的锁、它的性能问题,以及谓词锁、索引范围锁怎么挡住幻读。
- 第五次坐下§3.3–§4.1 · 约 3900 词 · 乐观的 SSI,然后进入分布式提交
- Serializable Snapshot Isolation约 1800 词可串行化快照隔离先放行、提交时检查前提有没有过时。留意两种检测情形的区别。
- Distributed Transactions约 700 词分布式事务单机靠一条提交记录做到原子提交,多个节点为什么不能各提交各的。
- Two-Phase Commit约 1500 词两阶段提交两轮请求、两次承诺,协调者崩溃时参与者的处境,三阶段提交为什么不现实。
- 第六次坐下§4.2–§5 · 约 3400 词 · XA 的麻烦、数据库内部的事务、恰好一次
- Distributed Transactions Across Different Systems约 1800 词跨不同系统的分布式事务XA 怎么工作;存疑的事务为什么一直持锁;协调者坏了谁来收拾。
- Database-Internal Distributed Transactions约 300 词数据库内部的分布式事务同一套软件内部的两阶段提交,为什么没有 XA 那些毛病。
- Exactly-Once Message Processing Revisited约 500 词再谈恰好一次的消息处理只用数据库自己的事务,怎样让每条消息的效果恰好出现一次。
- Summary约 800 词小结全章回顾。先自己把几种竞争和隔离级别对一遍,再读它。
先用中文过一遍
读已提交和快照隔离:读到的是哪个版本
隔离要解决的是:几个事务同时跑,彼此别踩到对方。最弱的一档实用级别是读已提交,它保证两件事:只读得到别人已经提交的数据(不脏读),只覆盖别人已经提交的数据(不脏写)。不脏写靠行锁:要改一行先拿锁,拿到提交或中止才放。不脏读通常不用锁,而是让数据库同时记着这一行的旧值(已提交的)和新值(还没提交的),别的事务来读,就给它旧值。
读已提交挡不住一种情况:一个事务先后读两行,两次之间别人改了这两行并提交了,它看到的是一半旧、一半新。只读一下,过几秒刷新就好;可要是备份或对账这种要跑几个小时的读,就会读出一个从来没存在过的状态。快照隔离的办法是把「旧值和新值」推广成「好几个已提交的版本」:每个版本标着是哪个事务创建的、哪个事务删除的(更新就是删一个旧版本、加一个新版本)。事务开始时记下当时还有哪些事务没提交,之后读任何一行,都只挑「开始那一刻已经提交」的那个版本。这就是多版本并发控制(MVCC)。读的人不挡写的人,写的人也不挡读的人。
两个事务同时写:丢失更新和写偏斜
前面两级主要管「读的人看到什么」。两个事务都要写时,问题更多。最常见的是丢失更新:两个事务都先读一个值、在程序里改、再整个写回去,后写的那个把先写的冲掉了。读已提交挡不住它:后一次写发生在前一个事务提交之后,算不上脏写。对策有好几种:让数据库做原子的「在原值上加减」,显式给要改的行加锁,让数据库自动检测并中止后写的那个,或者用「值没变才写」的条件写。
写偏斜更隐蔽:两个事务读的是同一批数据,改的却是不同的行。每个事务都先查一个条件(「还有空位吗」),按结果决定写入,而它的写入恰好让对方的条件不再成立。因为两人改的不是同一行,行锁、丢失更新检测统统看不到冲突。更麻烦的是,如果条件查的是「没有符合条件的行」,就连一行可以加锁的东西都没有,这就是幻读引起的写偏斜。
要全挡住:可串行化的三种做法
可串行化的意思是:不管事务实际上怎么交错执行,结果都和某个「一个接一个跑」的顺序一样。这样只要每个事务单独跑是对的,一起跑也是对的。书里讲了三种实现。第一种最直接:真的只用一个线程,一次跑一个事务。前提是事务都很短、数据都在内存里,而且事务要写成存储过程整个交给数据库,不能和应用来回聊。第二种是两阶段锁:读要拿共享锁,写要拿排他锁,读写互相等,拿到的锁一直留到事务结束;为了挡住幻读,还要锁住「查询条件」或者索引上的一段范围。第三种是可串行化快照隔离(SSI):照快照隔离那样放手让事务跑,只是暗中记下谁读了什么、谁写了什么,到提交时检查自己当初读到的前提有没有被别人改掉,改掉了就中止。前两种是悲观的,有风险就等;第三种是乐观的,先做,出了问题再撤。
跨节点提交:两阶段提交的两次承诺
单机上,「提交」落在一个确定的时刻:提交记录写进磁盘上的日志的那一刻。事务涉及好几个节点时,不能简单地给每个节点发一句「提交」:有的节点可能提交了,有的因为冲突、超时或崩溃回滚了,而已经提交的数据别人可能已经读走,收不回来。两阶段提交加了一个协调者,把提交拆成两轮。第一轮问每个参与者「你能提交吗」,参与者答「是」之前,要先把数据写盘、查过约束,答了就不能反悔。第二轮,协调者收齐答复,把决定写进自己的日志,然后通知所有人。这个决定一旦写下,就要不停重试直到人人照办。代价也在这里:参与者答了「是」之后,如果协调者崩溃,它既不能自己提交,也不能自己回滚,只能拿着锁等。
这一章点名的系统
| 英文 | 中文 | 一句话 |
|---|---|---|
| transaction | 事务 | 把一组读写当成一个整体:全部提交(commit),或者全部中止(abort)(§0)。 |
| ACID | ACID | 原子性、一致性、隔离性、持久性。作者说它如今多半是营销用语(§1.1)。 |
| dirty read / dirty write | 脏读 / 脏写 | 读到、或者覆盖了别人还没提交的数据(§2.1)。 |
| read committed | 读已提交 | 不脏读、不脏写。很多数据库的默认隔离级别(§2.1)。 |
| snapshot isolation | 快照隔离 | 每个事务从开始那一刻的一致快照里读(§2.2)。 |
| MVCC | 多版本并发控制 | 一行同时保留几个版本,按事务号决定谁看到哪个(§2.2)。 |
| read skew | 读偏斜 | 一个事务在不同时刻读到数据库的不同部分,凑出一个不存在的状态(§2.2)。 |
| lost update | 丢失更新 | 两个读-改-写循环同时跑,后写的冲掉了先写的(§2.3)。 |
| write skew | 写偏斜 | 按读到的前提做决定并写入,写的时候前提已被别人改掉(§2.4)。 |
| phantom | 幻读(幻影) | 一个事务的写,改变了另一个事务某个查询条件的结果(§2.4)。 |
| serializability | 可串行化 | 并发执行的结果和某种一个接一个执行的结果相同(§3)。 |
| two-phase locking (2PL) | 两阶段锁 | 读写都加锁、锁拿到事务结束才放的可串行化实现(§3.2)。 |
| serializable snapshot isolation (SSI) | 可串行化快照隔离 | 在快照隔离上加冲突检测,提交时中止前提过时的事务(§3.3)。 |
| two-phase commit (2PC) | 两阶段提交 | 让多个节点要么都提交、要么都中止的协议(§4.1)。 |
| idempotent | 幂等 | 做一次和做好几次效果一样,因而可以放心重试(§4.4)。 |
动手:模拟器
先猜:库存 10 件,A 买 1 件、B 买 2 件,都按「读库存 → 写回减过的数 → 提交」来,顺序是 A 读、B 读、A 写、B 写、A 提交、B 提交。读已提交下最后剩几件?换成「快照隔离(检测丢失更新)」,谁会被中止、在哪一步?换成两阶段锁呢?猜好了再点「坏交错:自动走」验证。
两个事务交错执行
选一个场景和隔离级别,用「A 走一步」「B 走一步」自己安排两个事务的交错,看谁要等锁、谁被中止、最后的数据对不对。数据和场景都是本站编的。
- 场景「丢失更新」,五种隔离级别各点一次「坏交错:自动走」。哪几种最后剩 8 件?其余几种里,B 是在哪一步、因为什么被中止的?点「重试被中止的事务」再把 B 走完,库存对了吗?
- 场景「写偏斜」,在「快照隔离(检测丢失更新)」下自动走:检测为什么没起作用?换两阶段锁:谁先停下来等、谁被中止,为什么算死锁?换 SSI:两个事务都顺利走到提交,谁在提交那一刻被拒?
- 场景「读偏斜」,手动走:A 开始、A 读东仓,然后一直点「B 走一步」,直到 B 走完或者走不动,再把 A 走完,最后把 B 走完。读已提交下 A 算出的合计是多少?快照隔离和两阶段锁都给出 100,但 B 的经历完全不同,差在哪?
词与短语
词条按在书里出现的顺序排,按节分组;读一节之前先把这一节的过一遍,读的时候就不用停。斜体小字是原句里连续的几个词,在电子书里搜它就能找到那一句。标着「每个字都认识」的,是字面意思会把你带偏的。行话(compaction、quorum 这类)不在这里,在上面的术语表里。
§0 In the harsh reality of…
§1 What Exactly Is a Transaction?
§1.1 The Meaning of ACID
§1.2 Single-Object and Multi-Object Operations
§2 Weak Isolation Levels
§2.1 Read Committed
§2.2 Snapshot Isolation and Repeatable Read
§2.3 Preventing Lost Updates
§2.4 Write Skew and Phantoms
§3 Serializability
§3.1 Actual Serial Execution
§3.2 Two-Phase Locking
§3.3 Serializable Snapshot Isolation
§4 Distributed Transactions
§4.1 Two-Phase Commit
§4.2 Distributed Transactions Across Different Systems
§4.3 Database-Internal Distributed Transactions
§4.4 Exactly-Once Message Processing Revisited
§5 Summary
下面是读完再看的:你的预测对不对、几道换成你来选的判断题、值得细读的句子和理解检查。先看了,读书时就只是在找答案。
读完:回到你的预测
答案是第三项(§2.4):两个都成功,31 人。这正是写偏斜:两个事务读的是同一组行,各自插入的却是不同的新行。丢失更新检测只管「同一行被两个事务改了」,这里没有同一行;行锁也锁不到还不存在的行;快照隔离下谁也看不到对方没提交的插入,所以不会读到 30。书里说,PostgreSQL 的 repeatable read、MySQL 的 repeatable read、Oracle 的 serializable、SQL Server 的快照隔离都不会自动检测写偏斜,要挡住它需要真正的可串行化(§3)。想一想:如果把「已选人数」存成课程表上的一个计数字段,两个事务都去改它,答案会变吗?
答案是第四项(§4.1)。库存库答了「是」,就交出了单方面中止的权利;它又不知道协调者的决定,自己提交也不安全,因为可能有别的参与者投了反对。问订单库听起来可行,但那不是两阶段提交协议的一部分。它只能等协调者恢复、从日志里读出决定再通知它,这段时间里这笔事务改过的行一直锁着(§4.2 讲了这为什么会把系统的其他部分也卡住)。想一想:如果协调者自己用共识算法复制成几台、一台坏了另一台接手(§4.3,第 10 章),库存库还要等多久?
判断题:换成你来选
场景都是本站编的。先选,再看解释;解释里写了对应的节号,拿不准就回原书看那一节。
场景:会员积分服务用 PostgreSQL 的默认隔离级别(读已提交)。代码通过 ORM 先把用户记录查出来,在程序里给积分加上这次的奖励,再整条保存回去。大促期间偶尔有用户反映同一时刻的两笔奖励只到账一笔,日志里没有任何报错。
这是丢失更新:两个读-改-写循环交错,后写的冲掉了先写的。读已提交不防它,因为后一次写发生在前一个事务提交之后。原子更新让数据库自己在原值上加,是书里说的首选;显式加锁(SELECT FOR UPDATE)也行。读未提交只能降低概率,挡不住;缓存与此无关;这里根本没有报错,重试无从谈起。
场景:新用户可以领一张首单券,每人限一张。领券接口在一个事务里先查「这个用户领过没有」,没领过就插入一条领券记录。数据库用快照隔离。日志显示偶尔有人领到两张,都是用户连点了两下。
这是幻读造成的写偏斜:两个事务都查到「没有」,各插入一条。查的是不存在的行,SELECT FOR UPDATE 没东西可锁;两条插入是不同的行,丢失更新检测也看不见。唯一约束让第二条插入直接失败,简单可靠。前端置灰挡不住重放的请求,书里提醒过攻击者会故意并发地打你的接口。
场景:订单库用两阶段锁实现可串行化。每晚的对账任务要在一个事务里扫一遍整张订单表,跑 40 分钟;这 40 分钟里,下单接口大面积超时。
两阶段锁下,读整张表要拿整张表的共享锁,在它提交之前所有写都得等。SSI 里读不挡写、写不挡读,只读事务在快照上跑,不用拿锁,书里说长时间的只读事务在 SSI 下没问题。连接池大了也只是更多人排队;拆成并行扫描照样要拿共享锁;真串行执行下这 40 分钟的事务会让所有事务都停下。
场景:支付成功的消息从消息队列进来,消费者在数据库里给商家余额加钱,提交后向队列确认。偶尔消费者在提交之后、确认之前崩溃,队列把消息重投一次,钱就加了两次。有人提议上 XA,把消息确认和数据库写放进同一个两阶段提交。
书里 §4.4 的做法:只用数据库自己的事务,把消息 ID 和业务写一起提交,处理就变成幂等的,重投也只生效一次。XA 理论上可行,但要求两边都支持,协调者一出问题,存疑的事务就一直持锁,书里花了整整一节讲这些麻烦。先确认再处理,崩溃时会丢消息;调超时与崩溃无关。
场景:一个抢购系统想用「单线程一次跑一个事务」换取可串行化:热数据全在内存,下单事务写成存储过程,平均 50 微秒跑完。运营后台还有一个「导出全部订单」的功能,也作为一个事务在这个线程上跑,要 3 秒。
串行执行要求每个事务都又小又快,一个慢事务就能让整条流水线停下。书里的办法是让只读的长查询在快照上跑,放到串行执行的循环之外。存储过程难管理是真问题,但不是最先爆的;题目说了热数据在内存里;只有一个线程,谈不上死锁。
和本站其他书对照
值得停下来的句子
Transactions are not a law of nature; they were created with a purpose—namely, to simplify the programming model for applications accessing a database.— §0。本站译:事务不是自然规律;它是带着目的被造出来的,那就是让访问数据库的应用编程起来更简单。
这句话定下了全章的读法。既然事务是为了简化编程而存在,评价一个隔离级别、一个提交协议,就要问它替应用挡掉了哪些情况、还剩哪些要应用自己操心。后面讲弱隔离时作者一再抱怨,正是因为它们把一部分复杂度又推回给了应用,而且推得不明不白。
the database guarantees that if the transactions behave correctly when run individually, they continue to do so when run concurrently— §3。本站译:数据库保证,如果事务单独运行时行为正确,那么并发运行时也依然正确。
这是可串行化最有用的一种说法。它把「并发对不对」这个难问题,换成了「单个事务对不对」这个容易的问题,应用开发者只需要想后者。弱隔离级别给不了这句保证,所以每用一个弱隔离级别,你都得回到 §2 那几种竞争,一个一个排查自己的代码。
检查一下理解
1.(§1.1)作者为什么说「可中止性」比「原子性」更贴切?
ACID 的 A 讲的不是并发时别人看不到半成品,那是 I 的事。它讲的是出错时把事务已经做的写全部丢掉,应用因此可以放心重试。
2.(§1.2)重试一个被中止的事务,什么情况下反而坏事?
事务其实已提交、只是确认在网络上丢了,重试会做两遍;错误来自过载或高竞争时,重试让情况更糟;违反约束这类永久错误,重试没用;发邮件这种数据库之外的副作用会重复发生。
3.(§2.1)防脏读,大多数数据库为什么不让读去拿锁?
一个长时间的写事务会让所有读它那几行的请求一起等,只读请求的响应时间被拖垮,一处慢还会连累别处。所以数据库记下旧的已提交值,写事务提交之前,别人读到的都是旧值。
4.(§2.2)备份和完整性检查为什么特别需要快照隔离?
它们要读很大一片数据、跑很久,期间写入照常进行。没有一致的快照,读到的是不同时刻的拼凑;备份恢复后,这种不一致(比如钱凭空少了)就永久留下了。
5.(§2.3)PostgreSQL 和 MySQL 的 repeatable read 在丢失更新上有什么不同?
PostgreSQL 会自动发现丢失更新,中止后写的那个事务;MySQL 的 InnoDB 不检测。所以有作者认为,按「必须防住丢失更新」的定义,MySQL 提供的不算快照隔离。
6.(§2.4)什么是「把冲突物化」?作者为什么只把它当最后的手段?
幻读的问题是没有行可锁,那就人为造一张表,比如把每个资源、每个时间段预先建成一行,事务先锁住相应的行再检查和写入。但怎么物化很难想对、容易出错,而且让并发控制的细节漏进了业务数据模型,能用可串行化就用可串行化。
7.(§3.1)单线程串行执行为什么要求事务写成存储过程?
交互式事务的大部分时间花在应用和数据库之间来回传语句上,单线程干等就太慢了。把整段事务代码事先交给数据库,数据又都在内存里,它就能不等网络和磁盘一口气跑完。
8.(§4.4)不用跨系统的分布式事务,怎样做到每条消息恰好处理一次?
在数据库事务里先记下消息 ID,再做业务写,一起提交,之后才向消息代理确认。重投时看到 ID 已在库里,就直接确认丢弃;ID 表上的唯一约束挡住两个并发的重复处理。
用一句话说这一章(写给一个没读过的朋友):
精彩回顾
一句话:事务把一大类故障和并发问题变成「中止,再试一次」;隔离级别越弱,留给应用自己防的竞争越多。
ACID 的 A 不管并发,管的是出错时把已做的写全部撤销。这样应用才敢放心重试。
§1.1一致性指应用定义的不变式。数据库只能检查你声明成约束的那部分,其余靠你把事务写对。
§1.1快照隔离靠多版本:每个版本标着谁建的、谁删的,读的人按开始那一刻挑版本。读不挡写,写不挡读。
§2.2repeatable read 在 PostgreSQL 里是快照隔离,在 MySQL 里更弱,在 Db2 里是可串行化。看文档,别看名字。
§2.2写偏斜:两个事务各自查条件、各自写不同的行,写完后对方的前提都不成立了。只有可串行化能自动挡住。
§2.4两阶段锁有风险就等,一个慢事务能卡住一片;SSI 先放行,提交时发现前提过时再中止,只读查询在快照上跑,不用拿锁。
§3.3两阶段提交靠两次承诺成立;协调者崩溃时,答过「是」的参与者只能持锁干等。
§4.1XA 的「启发式决定」让参与者自己拍板,作者直说:这多半意味着破坏原子性,只留给灾难时用。
§4.2