2. MongoDB存储引擎与事务底层
要真正理解MongoDB事务的提交与回滚,不能停留在commitTransaction()和abortTransaction()这两个API层面,而必须下沉到WiredTiger存储引擎。MongoDB从3.0起将WiredTiger作为默认存储引擎,多文档事务的原子性、隔离性最终都由WiredTiger的MVCC、快照、日志(Journal)与检查点(Checkpoint)四套机制协同实现。本节围绕这四者展开,说明一次事务从写入到持久化、从回滚到数据可见性变化的物理过程。
一、MVCC:用版本链代替锁
WiredTiger采用多版本并发控制(MVCC)。核心思想是:更新一条记录时不原地覆盖旧值,而是生成一个新版本,旧版本仍保留在内存中供其他读事务访问。每个数据页(page)内部维护一条按时间排序的版本链,每个版本带有自己的事务ID(transaction ID,简称txnid)。
当事务T1修改文档{_id:1, qty:10}为qty:8时,WiredTiger并不会抹掉qty:10,而是追加一个qty:8的新版本,并标记其txnid为T1。此时若另一个事务T2以较早的快照读取该文档,它看到的仍是qty:10。这就是“读不阻塞写、写不阻塞读”的物理基础——读操作沿版本链找到自己快照可见的版本,写操作只追加新版本,二者互不等待。
版本链不会无限增长。当某个版本对所有活跃快照都不可见时,它就成为“过期版本”,由后台的淘汰(eviction)线程回收。因此长事务会拖住版本回收,导致内存膨胀,这是生产中必须避免长事务的直接原因。
二、快照隔离:事务看到的是哪个时间点
WiredTiger为每个事务在开始时分配一个快照(snapshot)。快照本质上是一组“可见事务ID集合”的边界:事务只能看到在它开始之前已提交的数据,看不到开始之后才提交的数据,也看不到未提交的数据。MongoDB默认在WiredTiger之上提供的是快照隔离(Snapshot Isolation),它比读已提交更强,能避免脏读、不可重复读和幻读,但可能出现写偏斜(write skew)。
快照隔离下,两个事务若修改同一文档,后提交者会因写冲突(WriteConflict)失败。MongoDB捕获该错误后,默认在事务内部自动重试一次;若仍冲突,则向应用抛出WriteConflict,由应用决定重试或放弃。理解这一点,就能解释为什么高并发下事务偶发失败并非Bug,而是隔离级别的正常表现。
三、Journal:提交的持久化承诺
WiredTiger的写入默认先落在内存,若此时进程崩溃,内存数据会丢失。Journal(预写日志,Write-Ahead Log)就是防止这种丢失的机制。事务提交时,WiredTiger把该事务的修改记录写入Journal缓冲区,再按策略刷到磁盘上的journal/WiredTigerLog.*文件。
MongoDB的写关注(writeConcern)中的j参数直接控制这一行为:j:true表示必须等Journal落盘后才向客户端返回成功;j:false则允许先返回、后落盘。因此{w:1, j:true}是“单节点持久化”的保证,而{w:"majority", j:true}才是“多数节点持久化”的保证。回滚时,事务的修改从未写入Journal,所以崩溃恢复不会重放它——这就是回滚在物理上“干净”的原因。
Journal文件默认每100ms刷一次(可通过storage.journal.commitIntervalMs调整),单个文件达到100MB后轮转。恢复时,WiredTiger从最近一次Checkpoint开始,顺序重放Journal中所有已提交事务,直到日志末尾。
四、Checkpoint:把内存状态固化成磁盘基线
如果只有Journal,恢复时间会随日志增长而无限拉长。Checkpoint的作用是定期把内存中的脏页(dirty page)刷到数据文件,形成一个一致的磁盘基线,并记录该基线对应的Journal位置。此后,这个位置之前的Journal就可以安全删除。
WiredTiger默认每60秒或Journal写入达到2GB时触发一次Checkpoint。Checkpoint本身是“一致性快照”:它记录所有数据文件的根页位置,保证恢复时能找到一个完整、自洽的起点。Checkpoint期间,新事务照常写入内存和Journal,不会被阻塞。
可以用以下命令观察Checkpoint与Journal状态:
db.serverStatus().wiredTiger.log
// 关注 "log bytes of payload"、"log bytes written"
db.serverStatus().wiredTiger.transaction
// 关注 "transaction checkpoint most recent time (msecs)"
// 以及 "transaction checkpoint total time (msecs)"
若transaction checkpoint most recent time持续偏高(例如超过1秒),说明脏页过多或磁盘写入能力不足,应检查写入负载与磁盘IO。
五、提交与回滚的完整物理过程
把四套机制串起来,一次事务的物理路径如下:
- 开始:分配txnid,获取快照,确定可见事务边界。
- 写入:每次修改在内存页的版本链上追加新版本,标记txnid,不覆盖旧值。
- 提交:将修改写入Journal缓冲;若
j:true则同步刷盘;更新提交时间戳,使新版本对后续快照可见。 - 回滚:将本事务产生的版本标记为废弃,不写Journal;后续快照不再看到这些版本,后台淘汰线程最终回收它们。
- Checkpoint:定期把已提交的脏页刷入数据文件,记录基线,截断Journal。
- 崩溃恢复:从最近Checkpoint开始重放Journal,恢复已提交但未落盘的事务;未提交事务因不在Journal中而自然消失。
WriteConflict频繁出现,说明多个事务在争抢同一文档的版本链;第三,j:false下宕机可能丢失最近约100ms的已提交事务;第四,Checkpoint时间过长会拖慢整体写入,需要关注磁盘吞吐。
总结来看,MVCC提供并发读写的版本基础,快照隔离定义事务的可见性边界,Journal保证提交的持久性,Checkpoint控制恢复的起点与日志的回收。四者共同构成WiredTiger事务的物理闭环:提交是“写日志+更新可见性”,回滚是“标记废弃+不写日志”,而崩溃恢复则依赖Checkpoint与Journal的配合。掌握这条链路,才能在面对事务失败、性能抖动和数据恢复时做出正确判断。