老城梧桐街上有家瑞丰钱庄,掌柜姓陈。小时候,陈掌柜的账房只有一本总账,每一笔生意在账本上就是一行字——落笔即成。这行字要么完整地写下了,要么压根没写,绝不会出现“半个字”的尴尬。这就是我们常说的“单文档原子性”:一行记录天然完整、无法切开。可随着生意越做越大,钱庄与布庄、粮行、药铺开始联营。今天布庄要给粮行转三百两银子,账房必须同时减布庄的账、加粮行的账——两行字必须同时落笔。只减不加,账目就乱了。陈掌柜为此愁得直捋胡子。 后来,掌柜翻出一本蒙尘的旧册子,名叫“联营对账手册”——它就是 MongoDB 事务。这本手册规定:凡是牵动多本账目的事,都得走一套“发函确认、统一落账”的流程(两阶段提交),全程看着同一版账本(快照隔离),绝不能出现记一半的乱账。翻完这本手册,你会理解它是什么、如何演变、怎么运转、有哪些坑,以及什么时候压根不该用它。一句话:MongoDB 事务不是把文档数据库变成关系型数据库,而是给文档模型添上一把“多账本一致”的锁。

岗位关联

事务能力是你从“会写增删改查”走向“能设计可靠数据流”的关键一步。理解 MongoDB 事务,才能在订单、支付、风控等真实业务中给出既一致又高效的存储方案——这是数据工程师与后端架构师的硬通货。

目录
  1. 可视化演示
  2. 事务基础概念:一张票据,一笔记录
  3. 开发支持历程:掌柜的工具箱升级
  4. 副本集一致性:每本账都有复本
  5. 快照隔离机制:看同一版老账本
  6. 两阶段提交:发函确认与统一落账
  7. 分片事务限制:跨分号的麻烦
  8. 事务实践指南:掌柜的操作手册
  9. 设计原则权衡:能一笔记清就别啰嗦
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

事务基础概念:一张票据,一笔记录

陈掌柜当年在账本上记录银两进出,就是一行字。笔墨落纸,这行字要么清清楚楚,要么根本没写,绝不可能出现“笔画写了一半”的账目。这就是钱庄的童子功:字要一气呵成,账才能一笔不乱。MongoDB 保存文档也是这个道理——一个文档在存储引擎中作为整体写入,单次操作要么完全生效,要么完全不生效。这个特性有一个专门的名字:单文档原子性,它是 MongoDB 一切一致性保障的地基。

可联营以后,一笔生意要同时改两本账。这时候,一行“字”就不够了,需要一套把多笔操作打包成“一单”的机制——这就是事务。事务(Transaction)把一组操作捆在一起,要么全部成功,要么像墨迹未干就被擦掉一样全部回滚。MongoDB 从 4.0 版本起正式支持多文档事务,并完整遵循 ACID:原子性(Atomicity)保证操作整体提交或回滚;一致性(Consistency)保证事务前后数据满足所有约束;隔离性(Isolation)让并发事务互不干扰;持久性(Durability)保证提交后数据不丢失。

所以陈掌柜把多笔联营账装进一个“对账单”里:要么分铺都认这一单,要么全盘抹掉。MongoDB 事务就是这张对账单,它把“多行字”变成“一单字”,保住了钱庄“落笔即成、涂改即净”的老规矩。

T={op1,op2,…,opn},Result(T)∈{Committed,Aborted}\displaystyle T = \{op_1, op_2, \ldots, op_n\}, \quad \text{Result}(T) \in \{\text{Committed}, \text{Aborted}\}
这个公式说:事务是若干操作的集合,最终结果要么整体提交(Committed),要么整体中止(Aborted),没有第三种终态。
  1. 操作集合T 代表事务,花括号里是从 op₁ 到 opₙ 的所有操作,例如“布庄减银”和“粮行加银”是两个操作
    T={op1,op2,…,opn}\displaystyle T = \{op_1, op_2, \ldots, op_n\}
  2. 二元结果Result(T) 表示事务的最终结果,它只可能是两个值中的一个
    Result(T)∈{Committed,Aborted}\displaystyle \text{Result}(T) \in \{\text{Committed}, \text{Aborted}\}
  3. 不再有中间态Committed 是提交,Aborted 是回滚,事务不存在“进行了一半”的永久状态
    Committed∨Aborted\displaystyle \text{Committed} \vee \text{Aborted}

开发支持历程:掌柜的工具箱升级

陈掌柜的账房并不是一夜之间学会跨铺对账的。早年间联营生意少,一本总账打天下。后来联营多了,掌柜先在自家几个分号之间试行了“对账手册”;再后来分号遍布各镇,手册又升级出“总号分号统一结算”的版本;如今手册多次改版,越用越顺手。工具是慢慢换的,账房先生的胆子也是慢慢练出来的——MongoDB 对事务的支持同样是分阶段的。

2018 年 6 月,MongoDB 4.0 正式发布,首先在副本集(Replica Set)上支持多文档 ACID 事务,让单副本集内的多文档操作具备了完整的原子性与隔离性。2019 年 8 月,4.2 版本把事务扩展到分片集群(Sharded Cluster),事务可以跨越多个分片协调执行,成为真正意义上的分布式事务。此后 8.0 及更高版本持续演进:扩大事务写入大小上限、优化协调开销、完善快照和可重试写入的支持,让事务在更重的业务场景下也能落地。

这就好比掌柜的“对账手册”先是自家分号用,后来扩展成联营各号通用。手册越写越厚,但对账的底气也越来越足。MongoDB 的版本演进,正是这本手册的“修修补补”。

S(v)={∅v<4.0{ReplicaSet}4.0≤v<4.2{ReplicaSet,Sharded}v≥4.2\displaystyle S(v) = \begin{cases} \varnothing & v < 4.0 \\ \{\text{ReplicaSet}\} & 4.0 \le v < 4.2 \\ \{\text{ReplicaSet}, \text{Sharded}\} & v \ge 4.2 \end{cases}
能力集合 S(v) 随版本号 v 增长而扩充:4.0 之前没有事务能力,4.0 起支持副本集事务,4.2 起分片集群也纳入事务支持范围。
  1. 空能力集当版本号小于 4.0 时,事务能力集合为空,即不支持多文档事务
    S(v)=∅,v<4.0\displaystyle S(v) = \varnothing, \quad v < 4.0
  2. 副本集阶段4.0 到 4.2 之间,能力集合只有副本集一项,分片集群还未覆盖
    S(v)={ReplicaSet},4.0≤v<4.2\displaystyle S(v) = \{\text{ReplicaSet}\}, \quad 4.0 \le v < 4.2
  3. 分片阶段从 4.2 起,能力集合同时包含副本集与分片集群,事务能力完整
    S(v)={ReplicaSet,Sharded},v≥4.2\displaystyle S(v) = \{\text{ReplicaSet}, \text{Sharded}\}, \quad v \ge 4.2

副本集一致性:每本账都有复本

陈掌柜最怕两件事:火灾和盗贼。账本一旦烧了,生意全完。所以他让账房先生每本总账都抄出两三份复本,分放在不同的钱匣里——这就是钱庄的“副本集”。凡是重要的入账,先在主账本上落笔,再由账房先生誊抄到各复本上。万一主账本被毁,任取一份复本就能重建账目,生意不至于断档。

MongoDB 的副本集(Replica Set)也是同样思路:由一个主节点(Primary)承担全部写入,其他从节点(Secondary)通过复制操作日志(oplog)保持同步。如果主节点故障,剩余节点会发起选举,选出一个新主节点继续服务。三个要点值得牢记:主节点写入——所有写操作(包括事务)都必须经过主节点,它唯一决定写入顺序;从节点复制——从节点异步回放 oplog,不断追赶主节点的状态;读偏好设置——默认读主节点能拿到最新数据,而设置 readPreference 为 secondary 可以分散读压力,但可能读到稍旧版本。

有了复本,掌柜再也不怕火烧账本。但他也明白:读账的人若图快去看复本,有时会看到稍微旧一点的数字——这就是“从节点读”的取舍。想要永远最新,就得多花一点代价去读主账本。

Lsync=treplica−tprimary,0≤Lsync≤Δ\displaystyle L_{sync} = t_{replica} - t_{primary}, \quad 0 \le L_{sync} \le \Delta
同步延迟 L_sync 表示从节点比主节点落后的时间差,它总是非负的,并且有一个上界 Δ,超过上界会触发重新选主等处理。
  1. 复制延迟L_sync 表示某个从节点相对主节点落后了多少时间
    Lsync=treplica−tprimary\displaystyle L_{sync} = t_{replica} - t_{primary}
  2. 非负约束从节点永远不可能比主节点“更新”,所以延迟不小于零
    0≤Lsync\displaystyle 0 \le L_{sync}
  3. 上界约束Δ 是延迟容忍上界(如心跳超时阈值),超过它说明副本已失联
    Lsync≤Δ\displaystyle L_{sync} \le \Delta

快照隔离机制:看同一版老账本

联营对账最怕账目一边对一边变。陈掌柜订了一条铁规矩:一旦开启对账,所有分铺都必须照着“开账那一刻的账本副本”核对,谁也不许看中途改动的数字。这就像读今天的晨报——哪怕下午出了增刊,你手里那份晨报的内容也不会变。看报的人从不会因为增刊而困惑,因为报纸是某个时间点的“快照”。

MongoDB 事务也遵循同样的快照隔离(Snapshot Isolation):事务开始时,存储引擎为事务生成一个一致性快照,事务内的所有读操作都基于这个快照,而不是实时数据。其他事务的写入,在这个事务看来仿佛不存在——直到它自己提交。底层支撑来自 WiredTiger 存储引擎的多版本并发控制(MVCC):每条记录维护多个版本,读操作按快照时间点选取对应版本;写操作之间则通过版本号检测冲突,两个事务同时改同一文档时,后提交者会收到 WriteConflict 错误,需要捕获后重试。

有了“晨报”式快照,各分铺对账时再也不用担心隔壁突然改了数。谁要改账,就等这一单对完再改——改早了就会撞上“写冲突”这堵墙,无功而返。

Vtx=Snapshot(τ0),τ0≤tstart(tx)\displaystyle V_{tx} = \text{Snapshot}(\tau_0), \quad \tau_0 \le t_{start}(tx)
事务可见的数据版本 V_tx 是某个开始时刻之前生成的快照,整个事务期间这个快照保持不变。
  1. 快照版本V_tx 表示事务可见的数据版本,它来自存储引擎生成的快照
    Vtx=Snapshot(τ0)\displaystyle V_{tx} = \text{Snapshot}(\tau_0)
  2. 快照时间点τ₀ 是生成快照的那个具体时刻,比如开账的一瞬间
    τ0\displaystyle \tau_0
  3. 不晚于事务起点T_start(tx) 是事务开始时间,τ₀ 不能晚于它
    τ0≤tstart(tx)\displaystyle \tau_0 \le t_{start}(tx)

两阶段提交:发函确认与统一落账

跨铺转账最难的不是算数,而是协同。陈掌柜的解法是“先问后写”:第一步,掌柜给涉及的每家分铺发一封确认函,写明“明日开市,布庄减银三百,粮行加银三百,贵号能否办妥?”各分铺收到后认真验账,回函“可办”或“不可办”。如果所有分铺都回“可办”,掌柜才发第二封函:“约定已定,即刻落账!”所有分铺同时下笔。但凡有一家回“不可办”,所有回函作废,一切恢复原样——这就是两阶段提交的雏形。

MongoDB 的分布式事务协调者正是这么做的。整个流程分为两个阶段:准备阶段(prepare),协调者向所有参与节点发送准备请求,各节点执行写操作并持有锁,但不提交,回复“已准备”或“不能准备”;提交阶段(commit),若全部回复“已准备”,协调者广播提交指令,各节点正式提交;否则广播中止指令,所有节点回滚。协调者(Coordinator)是核心角色:它记录事务状态,决定提交或中止,并且会把状态写入事务集合持久化,防止协调者自身崩溃后丢失决定。

陈掌柜这套“先问后写”的规矩,保证了联营各号步调一致。两阶段提交的智慧正在于此:把“能不能办”和“一起办”分成两步。宁可事前多问一句,也不要事后扯皮。

Commit(T)  ⟺  ⋀i=1nPrepared(Pi)\displaystyle \text{Commit}(T) \iff \bigwedge_{i=1}^{n} \text{Prepared}(P_i)
事务 T 能够提交,当且仅当所有参与者 P₁ 到 Pₙ 都回复“已准备”。这是一个全票通过机制。
  1. 充要条件⟺ 表示前后双向等价,左边成立右边必成立,反之亦然
    Commit(T)  ⟺  …\displaystyle \text{Commit}(T) \iff \ldots
  2. 全称逻辑与⋀ 表示“并且”要遍历所有参与者,下标 i 从 1 到 n
    ⋀i=1n\displaystyle \bigwedge_{i=1}^{n}
  3. 参与者就绪Prepared(Pᵢ) 表示第 i 个参与者已经确认“已准备”
    Prepared(Pi)\displaystyle \text{Prepared}(P_i)

分片事务限制:跨分号的麻烦

分号开多了,麻烦也来了。陈掌柜发现,跨分号对账时,每封信都要送到不同的镇子,信使往返的脚程(网络延迟)累积起来相当可观;有的分号规矩不同,有些账目那边根本不接(功能限制);对账时间拖久了,账本一直锁着,别的新生意都做不了(超时与锁)。跨铺对账虽好,代价却实实在在。

当 MongoDB 集群做了分片(Sharding),数据被切分到多个分片上。一旦事务涉及多个分片,每个分片都要完整参与 prepare 与 commit,协调成本随分片数量增长,网络往返、锁等待、日志同步的成本叠加起来,事务性能明显下降。分片事务的典型限制包括:性能开销——涉及分片越多,两阶段协调的往返次数越多,延迟越高;功能限制——某些聚合命令和跨分片操作受限,对分片键的查询约束也更严格;超时机制——事务默认 60 秒超时(由 transactionLifetimeLimitSeconds 控制),超时自动中止;锁等待时间过长也会快速失败,保证不会无限占用资源。

陈掌柜于是定下规矩:能在一家分号办完的事,绝不劳烦第二条街。同样的道理,能设计成单分片事务的应用,就不要让 MongoDB 为“跨分片一致”多付三倍脚程钱。

Cost(T)=Costcoord(T)+∑s∈Shards(T)Costshard(T,s)\displaystyle \text{Cost}(T) = \text{Cost}_{coord}(T) + \sum_{s \in \text{Shards}(T)} \text{Cost}_{shard}(T, s)
跨分片事务的总成本等于协调成本加上每个相关分片各自付出的成本之和。分片越多,总成本越高。
  1. 总成本Cost(T) 表示事务 T 的整体开销,包含时间与资源消耗
    Cost(T)\displaystyle \text{Cost}(T)
  2. 协调者开销Cost_coord(T) 是协调者调度、状态持久化的固定成本
    Costcoord(T)\displaystyle \text{Cost}_{coord}(T)
  3. 分片求和对事务涉及的每个分片 s 累加执行成本,∑ 表示求和
    ∑s∈Shards(T)Costshard(T,s)\displaystyle \sum_{s \in \text{Shards}(T)} \text{Cost}_{shard}(T, s)

事务实践指南:掌柜的操作手册

陈掌柜把多年对账心得整理成一本《联营操作手册》。扉页写着三条训诫:一、回函没等到就重新发,别干等(重试);二、收到“不可办”就当即作废,从头再议(错误处理);三、对账要有专属房间,账房先生用固定桌案(会话 Session),用完马上收拾,别占着桌子(缩短事务范围)。

落到代码层面,MongoDB 事务也有同样的实践规范。第一,事务必须绑定会话(Session):通过 client.start_session() 开启会话,再调用 session.start_transaction() 开启事务,读写操作都要显式传入 session。第二,捕获可重试错误:遇到 TransientTransactionError(如写冲突、瞬态网络故障)时,应该重试的是整个事务,而不是单个操作。第三,保持事务短小:事务内只放必要的读写,杜绝耗时的外部 API 调用,避免长期占用锁和 oplog 空间。官方驱动普遍提供 withTransaction 回调,内部自动处理瞬态错误重试,开发者只需把业务操作封装进回调。

手册的道理朴素:“别干等、早作废、快收拾。”现代数据库事务也一样——代码写得越短平快,重试得越果断,系统就越稳。

Psuccess(k)=1−(1−p)k\displaystyle P_{\text{success}}(k) = 1 - (1 - p)^k
若单次事务成功率为 p,最多尝试 k 次后,至少成功一次的概率就是 1 减去“连续失败 k 次”的概率。
  1. 单次失败率(1 − p) 表示单次事务失败的概率,与成功率互补
    (1−p)\displaystyle (1 - p)
  2. 连续失败 k 次指数 k 表示连续失败 k 次的概率,前提是各次尝试相互独立
    (1−p)k\displaystyle (1 - p)^k
  3. 成功为补集用 1 减去全失败概率,就得到至少成功一次的概率
    1−(1−p)k\displaystyle 1 - (1 - p)^k

设计原则权衡:能一笔记清就别啰嗦

陈掌柜的祖师爷留下一句训话:“能在一页账上记清的事,绝不牵连第二本账。”单行记录天然完整、修改快、不锁别人的账;一旦动用跨铺对账,就要发函、等回音、锁账本,劳师动众。账房先生们把这句话刻在算盘上,每次想开新账本前都要先问自己:这一页真装不下吗?

这条训话对应 MongoDB 的核心设计原则:优先设计单文档模型,把相关字段内嵌进同一个文档,利用单文档原子性完成大多数业务操作;只有真正需要跨文档、跨集合甚至跨库保持一致时,才引入事务。典型的单文档场景包括:订单与订单明细——把明细作为数组内嵌进订单文档,一次写完成;用户与偏好设置——同一文档承载全部资料。必须上事务的场景包括:转账(账户 A 扣款 + 账户 B 入账)、下单(订单创建 + 库存扣减 + 积分增加)等跨集合、跨文档的一致操作。反模式警示则是“为了事务而事务”——本来可以内嵌的建模被强行拆散,再靠事务拼回去,只会平添复杂度与性能损失。

陈掌柜把祖训刻在账房门楣上,也刻进了 MongoDB 的设计哲学:单文档原子性永远是最快的路,事务是备用的桥。能走桥是本事,能不走桥才是智慧。

Normalized(doc)  ⟺  Embedded(fields,doc)∧Atomic(update(doc))\displaystyle \text{Normalized}(doc) \iff \text{Embedded}(fields, doc) \wedge \text{Atomic}(\text{update}(doc))
当且仅当相关字段都能内嵌进同一个文档,且单文档更新天然原子时,这个设计就是理想的归一化设计,不需要引入事务。
  1. 归一化设计Normalized(doc) 表示这个文档模型是理想化、简洁的
    Normalized(doc)\displaystyle \text{Normalized}(doc)
  2. 字段内嵌Embedded(fields, doc) 表示所有相关字段都装在同一个文档 doc 里
    Embedded(fields,doc)\displaystyle \text{Embedded}(fields, doc)
  3. 原子更新Atomic(update(doc)) 表示对 doc 的单文档更新天然具备原子性
    Atomic(update(doc))\displaystyle \text{Atomic}(\text{update}(doc))

AI 应用场景

AI 风控决策与权益的原子生效

阶段:模型推理结果落库

解决的问题:传统做法是模型回传分数后,业务系统分多条写操作更新“用户风险等级”“授信额度调整”“风控处置记录”三个独立文档。任一条失败都会造成“等级改了但额度没改”的不一致,风控等于白做。用 MongoDB 事务把三条更新包进同一事务,任何一个失败全部回滚,保证处置动作原子生效。

具体案例:某信贷平台调用反欺诈模型后,在单个 MongoDB 事务内同时更新用户的 risk_level 文档、credit_limit 文档和 audit_log 文档。模型判定为高风险时,如果额度调整写入失败,事务整体回滚,避免出现“已标注高风险却仍保留高额度”的危险状态。

AI 推荐系统的配额扣减与行为日志

阶段:推荐服务调用后的计费与数据沉淀

解决的问题:AI 推荐调用往往涉及“扣减用户配额 + 写入调用流水 + 更新 A/B 实验曝光计数”三个独立文档。单文档写入无法保证三处一致,一旦中途失败就会出现超扣或多记。事务把三者绑定,即便接口被并发请求轰炸,配额也只会扣一次,日志与计数始终匹配。

具体案例:某国民级 App 的推荐接口每次调用扣 1 积分,并写一条推荐日志,同时更新服务端 A/B 实验的曝光计数。三个操作放入同一事务。接口重试时,配合幂等去重键与事务回滚语义,保证“扣分、记日志、计曝光”三者同生共死。

AI 模型版本热切换的一致发布

阶段:模型上线与灰度发布

解决的问题:新模型上线需要同时更新模型元数据、特征映射表、监控报警配置。只改模型权重而特征映射没切过去,新旧流量会算出不一致的结果,监控也会告警失真。事务让“模型 + 特征 + 监控”三份配置在同一刻完成切换,发布过程对线上完全无感。

具体案例:某推荐团队的模型发布流程在同一个事务内完成三步写入:models 集合新增 v2 记录、features 集合切换 activeFeatureVersion、alerts 集合更新监控阈值。事务提交后,所有新请求立即使用 v2 特征流水线,老版本记录完整保留便于回滚。

Python 演示

python40 行
from pymongo import MongoClient client = MongoClient("mongodb://localhost:27017")db = client["bank"]accounts = db["accounts"]  def transfer(sender, receiver, amount):    with client.start_session() as session:        with session.start_transaction():            sender_doc = accounts.find_one({"_id": sender}, session=session)            if not sender_doc or sender_doc["balance"] < amount:                raise ValueError("insufficient balance")            accounts.update_one(                {"_id": sender},                {"$inc": {"balance": -amount}},                session=session            )            accounts.update_one(                {"_id": receiver},                {"$inc": {"balance": amount}},                session=session            )  if __name__ == "__main__":    accounts.delete_many({})    accounts.insert_many([        {"_id": "alice", "balance": 1000},        {"_id": "bob", "balance": 500}    ])     transfer("alice", "bob", 200)    print("normal:", [(a["_id"], a["balance"]) for a in accounts.find()])     try:        transfer("alice", "bob", 99999)    except ValueError as exc:        print("rollback demo:", exc)    print("after rollback:", [(a["_id"], a["balance"]) for a in accounts.find()])
目的:用 pymongo 驱动演示 MongoDB 多文档事务的转账操作,先展示正常转账成功,再演示余额不足导致事务回滚、数据保持原样。运行前提:MongoDB 4.0+,且以副本集模式启动(单机可先执行 rs.initiate() 初始化为单节点副本集)。
  1. with client.start_session() as session:开启会话,事务必须绑定会话,如同掌柜为对账单分配专属房间
  2. with session.start_transaction():在会话内开启事务,之后的读写都在“同一张对账单”里
  3. accounts.find_one({"_id": sender}, session=session)事务内的读必须传入 session,才能读取事务快照,看到开账那一刻的余额
  4. {"$inc": {"balance": -amount}}原子自减操作,配合事务保证扣款与入账同生共死
  5. raise ValueError("insufficient balance")抛出业务异常,触发 with 块自动回滚,相当于掌柜喊了一声“作废!”

常见误区

误区网上很多资料说 NoSQL 放弃 ACID 来换性能和扩展性,所以 MongoDB 也做不了事务,只能靠应用层自己补偿。

正解MongoDB 从 4.0 起就在副本集上支持多文档 ACID 事务,4.2 起支持分片集群上的分布式事务。NoSQL 是一个宽泛的分类,不是“完全没有事务”的同义词。这就好比钱庄虽不用朝廷银票结算中心那套制度,但联营分号之间照样有自己的对账章程,章程同样严谨。

为何常见:NoSQL 早期宣传确实以“放松事务换取扩展性”为主打,这类旧印象在社区里流传太广,很多人的知识还停留在 3.x 时代。

误区一个订单文档要同时改状态、改金额、改物流信息,这么多字段一起变,是不是必须开事务?

正解这属于“多字段更新”,但仍是单文档操作。MongoDB 在存储引擎层面保证单文档更新天然原子,不需要事务。事务只用在跨文档、跨集合或多个文档必须同生共死的场景。就像在一行账目上改数字,不管改几栏,都还是那一行,一支笔就能写完。

为何常见:把“多字段”和“多文档”混为一谈,因为关系型数据库里一行多列由行锁保证,而 JSON 文档的嵌套字段容易让人误以为各自独立。

误区既然数据库支持事务了,那所有读取不都应该能看到最新的值吗?

正解事务提供的是快照隔离——事务内看到的是开始时刻的快照,而不是实时最新值。事务外的普通读默认读主节点,但如果设置了 readPreference 为 secondary,仍可能读到落后数据。事务保证一致性,不代表全局实时可见。这就像你手里那份晨报是“那一刻的新闻”,增刊出了也与你无关。

为何常见:把“一致性”误解为“实时性”,这是分布式系统里最经典的概念混淆,尤其容易被关系型数据库的事务经验带偏。

误区反正都是数据库操作,跨分片事务无非多走几步消息,用户根本感知不到差异。

正解跨分片事务的每次写入都要在多个分片上完整参与 prepare/commit,网络往返、锁等待、协调者状态落盘的累积开销很大,事务涉及的文档还会产生额外的路由与调度限制。实际生产中,跨分片事务的延迟可能是单分片事务的数倍甚至更高。一分钱一分货,跨铺对账当然比单本记账贵得多。

为何常见:本地单机事务很快,让人误以为分布式协调只是“多走两步”,低估了网络与共识的工程代价。

误区代码里 catch 到异常,把同一段代码重新跑一遍就能解决所有问题,反正驱动会自动重试。

正解重试只对一部分错误有效。TransientTransactionError(写冲突、瞬态网络故障)值得重试;而“余额不足”这类业务异常重试一万次也没用,甚至可能把一次成功的操作重复执行变成重复扣款。重试必须配合幂等键或业务校验,否则会把“部分成功”变成“重复操作”。就像回函说“不可办”,你重发一百遍确认函,人家还是不可办。

为何常见:看到官方驱动有 withTransaction 自动重试,就以为所有异常都能自动恢复。实际上自动重试只帮助瞬态错误,业务语义错误永远要靠人判断。

面试题

Q1. 在 MongoDB 中引入多文档 ACID 事务的版本是?

单选题 · easy

  • A.3.6 版本
  • B.4.0 版本← 正确答案
  • C.4.2 版本
  • D.5.0 版本

答案:B。MongoDB 4.0(2018 年 6 月)首次在副本集上支持多文档 ACID 事务;4.2 是把事务扩展到分片集群,而不是首次引入。就像对账手册先在自己分号试行,后推广到全部分号——4.0 落地,4.2 放大,记忆时抓住“先副本后分片”的顺序。

Q2. 单文档更新操作具备天然原子性的根本原因是?

单选题 · easy

  • A.文档经过分布式锁保护
  • B.文档被存储引擎单点写入← 正确答案
  • C.副本集同步保证一致性
  • D.客户端请求串行到达

答案:B。单文档原子性来自存储引擎(WiredTiger)把整个文档作为一次写入单元,要么完全写入要么完全不写入,无需任何分布式协调。分布式锁和副本集同步都发生在多节点场景,不是“单文档原子”的根源;客户端请求串行到达也只是并发模型,与原子性无关。

Q3. 关于 MongoDB 事务的快照隔离,正确的说法是?

单选题 · mid

  • A.事务每次读都读取最新提交的数据
  • B.事务开始时获取一致性快照← 正确答案
  • C.事务提交时才生成数据快照
  • D.快照对所有并发事务共享

答案:B。事务开始时,存储引擎为它生成一致性快照,整个事务期间的读都基于这份快照,这也是“晨报”式体验的来源。A 是实时读而非快照读;C 颠倒了时序;D 错在快照按事务隔离,互不共享。

Q4. 两阶段提交中,协调者允许广播提交的充要条件是?

单选题 · mid

  • A.有半数以上参与者回复已准备
  • B.所有参与者都回复已准备← 正确答案
  • C.至少一个参与者回复已准备
  • D.由主节点单方面作出决定

答案:B。两阶段提交是“全票通过”机制:所有参与者都回复“已准备”才可提交,任何一个说“不可办”就整体中止。这就像陈掌柜发确认函,必须所有分号都回“可办”,才能统一落账;半数通过或单方面决定都会破坏跨铺一致性。

Q5. MongoDB 4.2 版本在事务方面的里程碑是?

单选题 · mid

  • A.首次引入多文档事务机制
  • B.支持分片集群上的事务← 正确答案
  • C.取消事务默认超时时间
  • D.移除事务的协调者角色

答案:B。4.2 把事务从副本集扩展到分片集群,使跨分片分布式事务成为生产可用能力。首次引入多文档事务是 4.0;取消超时和移除协调者都不符合事实——超时与协调者恰恰是分布式事务的基石,不可能被移除。

Q6. 两阶段提交中,若某个参与者回复“无法准备”,协调者会?

单选题 · mid

  • A.继续等待其他参与者回复
  • B.向全部参与者广播提交指令
  • C.广播中止指令并回滚事务← 正确答案
  • D.只提交已回复准备好的节点

答案:C。只要有一家分号说“不可办”,整单生意就作废。协调者会广播中止指令,所有参与者回滚已做的写入,恢复到事务开始前的状态。继续等待毫无意义,提交指令更不可能发出,因为全票通过的前提已经不成立了。

Q7. 以下关于跨分片事务性能的描述,错误的是?

单选题 · hard

  • A.事务协调涉及更多网络往返
  • B.锁的持有时间通常更长更久
  • C.性能与单分片事务基本一致← 正确答案
  • D.协调者的调度开销明显增加

答案:C。跨分片事务的成本公式是协调成本加各分片成本之和,分片越多开销越大,性能绝不可能与单分片事务“基本一致”。A、B、D 都是跨分片协调的真实代价。用钱庄比喻:跨三镇对账的脚程和锁账时间,一定比自家一间分号核算贵得多。

Q8. MongoDB 事务遵循的 ACID 特性包含哪些?

多选题 · easy

  • A.原子性(Atomicity)← 正确答案
  • B.一致性(Consistency)← 正确答案
  • C.可用性(Availability)
  • D.持久性(Durability)← 正确答案

答案:ABD。ACID 是原子性、一致性、隔离性、持久性四个英文首字母的缩写,对应 A、B、D。可用性(Availability)属于 CAP 定理中的概念,不是 ACID 的组成部分。ACID 关心单事务的可靠性,CAP 关心分布式系统的取舍,二者常被混淆。

Q9. 以下属于 MongoDB 事务限制的是?

多选题 · hard

  • A.事务默认存在 60 秒超时← 正确答案
  • B.单事务写入限制为 16MB← 正确答案
  • C.事务内可以做耗时的外部请求
  • D.事务只能操作单个集合文档

答案:AB。MongoDB 事务默认 60 秒超时(可调),且单事务的写操作量默认上限为 16MB,这两项都是硬限制。事务内做耗时的外部请求违背“短事务”最佳实践,并非支持的能力;事务可以跨多个集合操作,D 是错误的说法。

Q10. 编写健壮的 MongoDB 事务代码,推荐的实践有?

多选题 · hard

  • A.尽量缩短事务的运行时间← 正确答案
  • B.事务内放入最少必要操作← 正确答案
  • C.忽略可重试的错误直接返回
  • D.捕获瞬态错误后重试整个事务← 正确答案

答案:ABD。事务越短,锁占用越少,写冲突概率越低,所以 A、B 都正确;遇到 TransientTransactionError(写冲突、瞬态网络故障)应该重试整个事务,D 正确。C 忽略错误会导致业务不一致,是明显的反模式。