MongoDB 4.x——高级特性(Change Stream和事务)

发布时间:2026/7/26 6:17:34
MongoDB 4.x——高级特性(Change Stream和事务) MongoDB 4.x高级特性Change Stream和事务1、Change Stream介绍2、Change Stream案例数据迁移2.1、关键点2.2、实战使用Change Stream实现增量迁移2.2.1、准备工作2.2.2、记录增量日志2.2.3、全量迁移2.2.4、增量迁移2.2.5、验证程序3、多文档事务3.1、事务简介3.2、MongoDB中的事务3.2.1、在mongo shell中使用事务3.2.2、验证隔离性3.2.3、事务超时4、基于Spring开发事务4.1、在驱动中实现事务4.2、使用Spring Data实现事务4.2.1、事务管理器4.2.2、使用TransactionTemplate5、事务实现原理5.1、MVCC与快照的一致性5.2、事务持久性5.3、读写隔离设定6、写冲突模式7、使用事务的限制1、Change Stream介绍Change Stream指数据的变化事件流MongoDB从3.6版本开始提供订阅数据变更的功能。在此之前MongoDB 3.4及以下版本​为了实时获得集合文档的变化事件我们不得不使用tailing oplog复制集日志这一方案来完成这样的功能。众所周知oplog是实现副本集数据同步的核心手段为了持续获得oplog的内容可执行如下的命令db.oplog.rs.find({fromMigrate:{$exists:false}}).addOption(DBQuery.Option.tailable).addOption(DBQuery.Option.awaitData)其中fromMigrate是分片迁移的标记我们需要对这些数据均衡产生的oplog进行过滤除此之外对查询的Cursor还设置了tailable自动滚动和awaitData自动等待新数据两个选项。可见构造这样的查询是比较复杂的而且tailing oplog这种方式还存在很多弊端。解析oplog的工作相对复杂实现者需要探究MongoDB复制的一些非公开的细节。oplog是全局性的这意味着为了订阅某个集合的变更需要对整个系统的oplog进行过滤效率太低。另外获得oplog意味着对所有集合都有变更读的权限安全风险增加了。当主备节点发生切换时oplog存在被回滚的风险应用可能会获取到“脏”的变更。在分片集群环境中你需要对每个分片shard进行oplog拉取此时由于存在数据均衡很可能会出现乱序问题。Change Stream特性的出现恰恰解决了这些问题使用db.collection.watch命令你可以轻松地获得实时且顺序一致的数据变化。而且Change Stream会采用readConcernmajority这样的一致性级别保证写入的变更不会被回滚。在监听范围方面Change Stream支持3种不同的级别见表。不同的监听级别提升了数据处理的灵活性而且你还可以在watch命令中添加聚合过滤器来满足自己的需求。那么Change Stream的实现机制和oplog有什么本质的不同吗答案是并没有Change Stream仍然是基于底层的oplog机制实现的为了正常使用Change Stream你必须使用副本集或者分片副本集架构的MongoDB集群。执行下面的代码即可对集合开启监听。varwatchCursordb.getSiblingDB(data).sensors.watch()while(!watchCursor.isExhausted()){if(watchCursor.hasNext()){printjson(watchCursor.next());}}此时对于sensors集合的一些数据的增加、删除、修改、查询操作会触发相应的Change Event。通常的Change Event的形式如下字段说明见表。其中对于变更类型operationType的支持见表。下面是一些样例可以基本了解一下。insert事件的代码如下delete事件的代码如下replace事件的代码如下update事件的代码如下drop事件的代码如下rename事件的代码如下dropDatabase事件的代码如下invalidate事件的代码如下2、Change Stream案例数据迁移Change Stream具有诸多优点在项目实战中很容易找到一些合适的应用场景例如数据迁移。尽管现有的微服务架构带来了许多新的理念但也带来了许多“旧改”的繁杂事情。所谓“旧改”​往往是把现有的系统架构重构拆分成多个细粒度的服务然后找合适的时间进行割接升级。这其中保证数据的平滑迁移往往会成为一个非常重要且复杂的工作。那么服务化改造中的数据迁移的问题有哪些呢首先是难度大做一个迁移方案需要了解项目的“前世今生”​以评估迁移方案、技术工具等。其次是成本高。由于新旧系统数据结构是不一样的因此需要定制开发迁移转化功能。很难有一个通用工具能一键迁移。再次对于一些容量大、可靠性要求高的系统要做到不影响业务出了问题能追溯因此在设计方案时要全面、细致。按照数据迁移的方案及流程一般可以采取停机迁移、业务双写、追日志增量迁移等几种方式。其中增量迁移对业务侵入性最低能实现较为完美的平滑迁移这也是本次重点介绍的方案。增量迁移的基本思路是先进行全量的迁移转换待完成后持续进行增量数据的处理直到数据追平后切换系统如图所示。2.1、关键点1系统需要支持增量数据的记录保存而且在全量迁移过程开始之前就应该开始监听。尽管Change Stream支持断点恢复resumeAfter的功能但其本质是基于oplog实现的。假设全量迁移需要12个小时而oplog窗口却只有10个小时则意味全量迁移完成时会有两个小时的增量数据被丢弃。为了保证数据完整有必要事先保存这些增量数据。2增量数据的回放是持续进行的。在所有的增量数据回放转换过程中系统仍然会产生新的增量数据这要求迁移工具能做到将增量数据持续回放并将之追平之后才能进行系统切换。当然对于一些业务吞吐量较大的场景还应该保证迁移程序的写入速度足够块。2.2、实战使用Change Stream实现增量迁移本次设计了一个简单的论坛帖子迁移样例用于演示如何利用Change Stream实现完美的增量迁移方案。背景现有的系统中有一批帖子每个帖子都属于一个频道channel​见表。新系统中频道字段将切换为英文简称相关的转换如图所示。原理说明topic是帖子原表在迁移开始前将开启watch任务持续获得增量数据并记录到topic_incr表中接着执行全量的迁移转换之后持续对增量表数据进行迁移直到无新的增量为止。下面我们使用Java程序来完成相关代码mongodb-java–driver在MongoDB 3.6版本后才支持watch功能需要确保升级到对应版本代码如下2.2.1、准备工作1定义Channel枚举用于转换频道信息代码如下2为topic表预置一部分数据用于模拟存量数据代码如下可见在这段代码的实现中每个帖子都分配了随机的频道channel​。2.2.2、记录增量日志1对topic表开启监听任务将所有变更写入增量表topic_incr代码如下上述代码中通过watch命令获得一个MongoCursor对象用于遍历所有的变更。FullDocument.UPDATE_LOOKUP选项启用后在update变更事件中将携带完整的文档数据FullDocument​。尽管如此FullDocument可能仍然会产生空值原因在于Change Stream只保证事件产生的顺序一致性在产生update事件后会尝试对源文档进行查询如果此时文档已经被删除就查询不到。watch命令提交后mongos会与分片上的mongod主节点建立订阅通道这可能需要花费一点时间。为了模拟线上业务的真实情况启用几个线程对topic表进行持续写操作代码如下ChangeTask的实现逻辑如下每一个变更任务会不断对topic表产生写操作触发一系列ChangeEvent产生。doInsert生成随机频道的topic表后执行insert操作。doUpdate随机取得一个topic表将其channel字段改为随机值执行update操作。doReplace随机取得一个topic表将其channel字段改为随机值执行replace操作。doDelete随机取得一个topic表执行delete操作。以doUpdate为例实现代码如下2.2.3、全量迁移在开启监听之后就可以执行全量的迁移任务将topic表中的数据迁移到topic_new新表代码如下在全量迁移开始前先获得当前时刻的最大_id值可以将此值记录下来作为终点。随后逐步完成数据转换和写入。2.2.4、增量迁移在全量迁移完成后便可以开始执行增量迁移任务。需要注意的是在增量迁移过程中变更操作仍然在进行。相关的代码如下增量迁移的实现是一个不断拉取的过程利用_id字段的有序特性进行分段迁移即记录下当前处理的_id值循环拉取在该_id值之后的记录进行处理。一般情况下增量表topic_incr中除了delete事件变更其余的类型都保留了整个文档因此可直接利用replaceupsert操作追加到新表。更新事件中的fullDocument可能为空需规避处理。2.2.5、验证程序最后让我们来梳理一下整个案例的过程预置存量数据。启动监听将增量写入日志表。模拟并发的变更任务。启动全量迁移。全量迁移结束启动增量迁移。停止变更任务停止向日志表追加等待增量迁移完成。好了现在基本上是完整的了启动任务后输出如下此时若查看topic表和topic_new表可以发现两者数量是相同的。而为了进一步确认一致性我们对两个表分别做一次聚合统计。topic表的代码如下输出结果如图所示。topic_new表的代码如下输出结果如图所示。可见前后对比的结果是一致的3、多文档事务3.1、事务简介事务transaction是传统数据库所具备的一项基本能力其根本目的是为数据的可靠性与一致性提供保障。而在通常的实现中事务包含了一个系列的数据库读写操作这些操作要么全部完成要么全部撤销。例如在电子商城场景中当顾客下单购买某件商品时除了生成订单还应该同时扣减商品的库存这些操作应该被作为一个整体的执行单元进行处理否则就会产生不一致的情况。数据库事务需要包含4个基本特性即常说的ACID具体如下。原子性atomicity​事务作为一个整体被执行包含在其中的对数据库的操作要么全部被执行要么都不执行。一致性consistency​事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。隔离性isolation​多个事务并发执行时一个事务的执行不应影响其他事务的执行。持久性durability​已被提交的事务对数据库的修改应该是永久性的。隔离级别在隔离性方面事务机制需要确保在多个事务并发执行时其数据的中间状态是彼此不可见的。如果不考虑事务的隔离性则可能会发生如下几个问题。脏读即事务中读取了“脏”的数据这些数据可能是未提交的或者是在将来发生了回滚。不可重复读在一个事务中同一条数据的状态是不稳定的例如第一次查询和第二次查询获得的结果不同可能是读到了其他事务提交的结果。幻读与不可重复读类似但幻读所对应的现象是数据的“有无”发生变化。例如第一个事务执行了范围修改操作之后而第二个事务插入了新增的数据在同一范围内​此后第一个事务将会发现还存在没有修改的数据行就好像出现了幻觉一样。针对这些问题标准的SQL规范为事务隔离性定义了4种级别。Read Uncommitted读未提交​事务在执行过程中可能访问到其他事务未经提交的修改这种级别是最弱的无法避免“脏读”​。Read Committed读已提交​事务在执行时可以读取另一个事务已经提交到数据库的结果。该级别可以避免“脏读”​但事务中多次读取可能产生不一样的结果因此会存在无法重复读的问题。Repeatable Read可重复读​在同一个事务内数据所呈现的状态将能持续保持一致从事务的起始时间点开始​当前事务只能读取到本事务所做出的修改。但是该级别所定义的隔离范围并不包括插入操作即事务还是会读取到其他事务提交的新增数据。Serializable串行化​在该级别下规定了事务只能串行化执行而不能并发执行。该隔离级别可以有效防止“脏读”​、不可重复读和幻读的问题但实际应用中很少使用因为会带来性能问题。下表整理了各个级别所应对的问题。通常事务的隔离级别越高越能保证数据库的完整性和一致性。另外隔离级别越高对并发性能的影响也更加明显应用上通常的选择是Read Committed读已提交​、Repeatable Read可重复读这两种级别。而解决幻读问题的手段一般是采用MVCC或者锁机制来实现。3.2、MongoDB中的事务如果此前对WiredTiger引擎有所了解就不难理解为什么MongoDB特意将4.0版本的事务称之为多文档事务multi document transaction了。WiredTiger引擎本身是支持事务的而MongoDB在内部实现中则使用了该引擎所提供的事务性API从MongoDB 3.0版本开始便对单文档的操作提供了事务原子性的保证。在经过多个版本的迭代之后MongoDB 4.0版本开始支持真正意义的多文档事务基于副本集​如此命名只是便于区分。而从MongoDB 4.2版本开始提供了跨分片的分布式事务事务能力得到了进一步完善。MongoDB的事务是基于逻辑会话session的MongoDB 3.6版本便开始支持会话特性会话提供了因果一致性的保证。对于事务来说必须先创建会话才能使用事务系统允许在任何时刻运行多个会话但对于每个会话来说同一时刻只能执行一个事务。这点可以类比多线程任务的场景把会话看作一个线程而事务则是绑定到线程上的一个任务单元。3.2.1、在mongo shell中使用事务在使用事务之前需要先创建相关的集合代码如下use datadb.createCollection(goods)多文档事务内部不允许执行createCollection这样的DDL操作包括由insert事件触发的DDL行为都将导致报错。创建一个会话用于执行事务代码如下sessiondb.getMongo().startSession()session{id:UUID(7a586278-2b42-4572-92ae-f8f9c94418d4)}接下来我们启动事务并向goods集合插入一些文档代码如下session.startTransaction()collectionsession.getDatabase(data).goods data.goodscollection.insert({_id:0,name:football,price:79})collection.insert({_id:1,name:basketball,price:128})在事务提交之前会发现只有在当前会话中事务内才能查到写入的数据而在会话外查询则会得到空的结果如果我们在会话的外部执行查询会发现仍然无法找到写入的数据。具体代码如下//会话内collection.find(){_id:0,name:football,price:79}{_id:1,name:basketball,price:128}//会话外db.goods.find()//结果为空执行事务提交之后在会话外部成功查到了写入的数据代码如下session.commitTransaction()db.goods.find(){_id:0,name:football,price:79}{_id:1,name:basketball,price:128}如果希望回滚事务则可以使用session.abortTransaction方法这样一来事务中的所有修改都会被永久撤销。3.2.2、验证隔离性MongoDB的事务采用了快照snapshot一致性的隔离级别即事务之间基于自身的快照上下文实施读写操作。分别开启两个事务在第一个事务中执行修改代码如下//事务一sessiondb.getMongo().startSession()session.startTransaction()collectionsession.getDatabase(data).goods//修改文档collection.update({_id:1},{$set:{price:99}})//插入文档collection.insert({_id:2,name:pingpong,price:31})//删除文档collection.remove({_id:0})此时第一个事务还未提交我们在第二个事务窗口中进行查询代码如下//事务二sessiondb.getMongo().startSession()session.startTransaction()collectionsession.getDatabase(data).goodscollection.find(){_id:0,name:football,price:79}{_id:1,name:basketball,price:128}此时事务二看到的仍然是原来的状态事务一启动之前​。将第一个事务进行提交并确认已经生效代码如下//事务一session.commitTransaction()db.goods.find(){_id:0,name:football,price:79}{_id:1,name:basketball,price:128}再次在事务二中进行查询代码如下//事务二db.goods.find(){_id:0,name:football,price:79}{_id:1,name:basketball,price:128}结果是尽管事务一已经提交了相关修改但这些修改对事务二仍然是不可见的。MongoDB的快照隔离级别是比可重复读更严谨的一种级别除了解决不可重复读的问题还避免了幻读如上述过程中事务一的提交中尽管插入了新的记录但在事务二中仍然无法读取出来。3.2.3、事务超时在执行事务的过程中如果操作太多或者存在一些长时间的等待则可能会产生如下异常原因在于默认情况下MongoDB会为每个事务设置1分钟的超时时间如果在该时间内没有提交就会强制将其终止。该超时时间可以通过transactionLifetimeLimitSecond变量设定。4、基于Spring开发事务4.1、在驱动中实现事务MongoDB的客户端驱动已经全面支持事务功能对于使用MongoDB Java Driver的应用来说必须升级到3.11.0版本以上代码如下在编程模型上MongoDB事务的开发与关系型事务比较相似实现事务的代码片段如下事务需要绑定在会话中运行因此第一步总是需要启动一个ClientSession对象。ClientSession实现了Java中的AutoClose接口这是JDK9提供的try-with-resources特性即只需要在try语句中初始化资源对象后编译器会在资源使用完毕后自动调用close方法进行释放。例子中的事务包含了插入文档、更新文档的操作且在事务过程发生异常时调用session.abortTransaction命令进行回滚。MongoDB为事务异常定义了两种类型。TransientTransactionError指事务中的操作所产生的临时错误如果发生了该异常则应用程序应该尝试进行重试处理。UnknownTransactionCommitResult指事务在提交时产生的未知错误在发生该异常时应用同样应该尝试重新提交。上述例子中使用的是Core API调用方式这需要开发者自行实现事务中发生异常时的重试逻辑。如果希望获得简化则可以使用Callback API调用方式代码如下这里的session.withTransaction方法的入参是一个TransactionBody对象我们只需要提供其execute方法的实现即可此时事务的启动、停止、异常处理则直接交给驱动处理。在Callback API这种风格的实现上驱动会自动捕获TransientTransactionError、UnknownTransactionCommitResult这两种错误并在有限的时间内进行重试该时间一般是120s这大约是事务超时时间的两倍。为了理解这种区别你可以在另外一个事务中对文档_id0进行更新这样可以产生一个WriteConfilict错误。此时如果使用Callback API驱动会自动重试并最终返回成功。4.2、使用Spring Data实现事务随着MongoDB多文档事务特性的推出Spring Data MongoDB也在2.1.0版本之后开始支持事务功能。通过前面的介绍我们已了解如何使用Spring DataMongoDB实现数据库的读写而一般应用的编程会基于两种风格实现基于MongoRepository接口实现标准的CRUD。基于MongoTemplate实现自定义的操作。4.2.1、事务管理器为了尽可能保证编程风格的统一Spring DataMongoDB可以使用Spring传统的事务管理器。而且事务的加入并不需要改变之前操作数据库的方式。下面来看一个例子在电子商城中平台方通常会根据用户的消费情况给予 一些福利例如用户可以使用积分来换取一定的优惠券。对于兑换优惠券这一操作来说会涉及用户积分表、优惠券表的操作实现代码如下doExchangeCoupon方法用于完成积分的扣减以及优惠券的生成。如我们所看到的BonusPointsRepository、CouponRepository都是标准的CRUD接口分别提供了BonusPoints用户积分​、Coupon优惠券实体的操作功能。接下来我们将为这段业务逻辑添加事务的支持。1声明一个MongoTransactionManager事务管理器Bean对象代码如下2添加事务注解Transactional代码如下这里我们新增了一个exchangeCoupon方法Transactional注解表示将该方法作为一个事务执行。Transactional是Spring Data的声明式事务注解通过这种注解的方式Spring Data会以AOP的方式织入事务处理的逻辑依赖于事务管理器​从而避免业务代码的侵入式修改。此时可以在SpringBoot程序中对事务功能进行测试代码如下一定要记住事务不支持隐式的createCollection操作某些写入操作导致建表​在操作事务之前务必确保所读写的集合已经创建。如上述代码中使用mongoTemplate.createCollection确保了这点。在启动程序后通过日志可以观察到事务的行为4.2.2、使用TransactionTemplate除了注解的方式我们还可以使用TransactionTemplate来完成事务的操作这在使用风格上很像MongoTemplate。具体实现代码如下需要注意的是Spring Data MongoDB是基于Java驱动的在事务处理上采用的仍然是Core API方式。这意味着应用需要自行处理事务产生的错误如TransientTransactionError​。对此官方建议使用Spring Retry组件来解决此类问题。5、事务实现原理5.1、MVCC与快照的一致性快照snapshot指的是系统在瞬时间的一致性状态这保证了事务之间的状态彼此隔离。我们曾经提及过WiredTiger基于MVCC实现了数据的并发读写控制而这正是隐藏在事务隔离级别背后的原理。在MVCC机制中数据会在内存中同时保存多个版本这为事务的快照读提供了基础。如果事务对数据产生了修改则将会在MVCC链表头上追加一个元素记录的内容为写事务的编号transaction_id。时间戳。修改的数据。而事务的读取会从MVCC链表的头部开始查找根据当前读事务的快照和在元素中修改事务的编号transaction_id来判断是否可读如果不可读则向链表尾部方向移动直到找到当前事务可读的版本如图所示。事务T0最早发生而事务T4发生的时刻最晚。由于T1/T2/T3对数据做了修改那么在MVCC链表中会相应增加3个版本。在快照隔离级别下读事务T0只能看到T0之前提交的值10而对于读事务T4来说由于事务T3并未提交且事务T2因为回滚而失效因此它只能读取到12这个版本的值事务T1的提交​。既然事务需要根据自己的快照来确定什么是可见的那么快照具体都包含了什么呢在事务开启或首次进行操作时数据库需要对内部正在执行或将要执行的事务做一次快照用于保存当时所有事务的状态并以此来区分哪些事务对当前事务可见而哪些事务又是不可见的。一个快照对象包含的信息如下具体的例子如图所示。假设在T5时刻数据库对正在进行的事务创建一个快照那么产生的结果如下此时对于事务T5能访问的范围包括3个区间所有小于T1事务的修改[0T1​。在T1和T4区间内已经提交的事务的修改这里只有事务T2。也就是说在事务T5建立快照的那一刻起凡是大于snap_maxT4或者在snap_arrayT1T4中出现的事务的修改都是不可见的。这个约束将贯穿整个事务过程譬如事务T1在后面产生了提交其对于事务T5仍然是不可见的。当然了事务在读取数据时除了快照还应该包含自身事务内的修改。实际上WiredTiger对事务的支持同时包含了未提交读、提交读、快照一致性读。而MongoDB事务采用的是快照一致性读。5.2、事务持久性ACID的一个重要特性就是保证事务的持久性即事务一旦提交成功其修改就是永久性的。然而WiredTiger的写模型是缓冲式的其为了避免频繁的I/O操作会将数据的修改先存储在内存中后面再一并刷到磁盘。因此事务中的修改只有在执行CheckPoint操作之后才算是真正落盘。那么是不是意味着事务的持久性就无法保证了呢并非如此MongoDB保证事务持久性的手段是重新操作日志redo log​也就是之前所说的journal日志。事务在开启时会向日志缓冲区redo log buffer预写入一条日志记录而随后的一些操作也会写入该记录中当事务提交时也一并将该日志变更为已提交状态。随后多个并发提交的事务日志会被合并写入磁盘的文件每100ms刷新一次中。重新操作日志保证了已提交事务在系统宕机时仍然可以恢复MongoDB在重启后会检查日志并执行日志回放。关于事务的持久性流程如图所示。5.3、读写隔离设定在读写级别方面多文档事务会产生一些不同的约束。1readPreference在多文档事务中readPreference被强制约束为Primary即客户端对事务的读操作只能通过主节点完成。2readConcern(rc)包括本地local读、大多数majority读、集群快照snapshot读。readConcernlocal默认级别该级别无法保证脏读。readConcernmajority只有在事务使用writeConcernmajority时才能保证读大多数提交。readConcernsnapshot保证从大多数提交的一致性快照上读取该级别可实现多个分片上的一致性快照。该级别同样只有在writeConcernmajority时才能保证效果。需要注意的是readConcern级别是针对事务读取的快照对于事务内部则始终保持快照的隔离级别。3writeConcern(wc)事务可选择writeConcern1或者writeConcernmajority默认的选项是writeConcern1。在writeConcernmajority级别下事务的提交可以实现大多数写。也只有在writeConcernmajority这样的设定下事务读操作可以保证从大多数提交的一致性快照中读取不会存在脏读或幻读等问题。而writeConcern1的设定则意味着读取操作仅来自本地快照这可能会导致脏读但带来的好处是性能的提升。对于MongoDB集群来说writeConcern级别对事务的隔离级别存在一定的影响例如是否存在脏读问题。可参考下面的两种场景。场景1writeConcert1、readConcernsnapshot见图在wc1级别下事务2在事务1提交到主节点之后启动可能读取到“暂态”数据产生脏读。场景2writeConcernmajority、readConcernsnapshot见图在wcmajority级别下对于rcsnapshot的快照读级别事务2只会读取事务1启动之前的状态。在这种模式下可以获得集群内的一致性保证。6、写冲突模式对于MongoDB事务一个令人感兴趣的问题是当多个事务尝试更新同一个文档时会发生什么一种可能的结果是相互覆盖即以最终执行的事务为准。但这并不是大多数人希望看到的因为这会带来一些不确定的风险。下面让我们来完成一个实验。打开两个mongo shell窗口分别启动事务代码如下在第一个窗口事务中执行修改代码如下在第二个窗口事务中对同一文档执行修改代码如下结果表明尽管事务一并未提交但事务二在执行过程中已经提前检测到了冲突并产生了异常。实际上在事务中对一个文档进行更新时MongoDB需要获取该文档的一个排它锁如果在5ms内无法获取则会产生写冲突WriteConflict​并导致事务中止。导致事务内写冲突的原因通常是文档已经被其他事务锁定或者在产生快照之后被其他非事务性写操作所篡改如图所示。同样对于非事务场景文档写操作也会尝试获取锁如果该文档被锁定可能来自未提交事务的修改​那么同样会产生冲突。但不同之处在于非事务性的写操作会自动重试直到成功或者产生了超时OvermaxTimeMs​如图所示。实现资源锁定通过事务中的冲突检测我们可以知道当前正在修改的文档是否正在被其他人修改。借由这样的机制我们就能在事务中实现某种资源的锁定。下面介绍一个例子。在最开始时创建锁对应的集合及初始文档如下在事务启动后执行文档更新以锁定资源代码如下注意我们在每次更新锁记录时都会使用一个新的ObjectId对象这是为了保证每次更新都会产生新的值。MongoDB只有当存在真实变更的update操作时才会产生写锁而ObjectId天生避免了重复问题由时间戳和计数器所组成​因此非常适合这样的场景。7、使用事务的限制MongoDB的多文档事务特性存在诸多限制在使用时仍然需要注意主要有如下几点。不允许在事务中对不存在的集合进行操作执行集合的创建、删除都是禁止的。这同时也包括一些导致集合级联创建的insert、upsert命令。不允许在事务中对索引进行创建、删除。事务中不支持对固定集合进行写入。不允许存在对config、admin、local数据库的读写操作包括不允许向system.*命名的集合写入数据。不允许执行一些非常规读写的命令如listCollection、listIndexes、explain等操作。事务中不支持collection.count命令需要使用聚合框架的$count操作来替代。事务中无法调用事务外部所创建的游标对象进行getMore遍历而反过来亦是如此。除此之外事务在性能方面的一些制约因素主要如下。对于MongoDB 4.0版本一个事务最多只能包含16MB的修改原因在于4.0版本将同一个事务写入了一条oplog中而BSON文档存在不能超过16MB的限制。在MongoDB 4.2版本中该限制已经被解除但建议应尽量减少超大的事务通常一个事务内不要超过1000条。一个事务的最大执行时间不超过60s超过该时间后事务会被自动淘汰。MongoDB的事务严重依赖于WiredTiger的快照能力长时间运行的事务会导致WiredTiger的缓存中积压大量未被持久化的数据进而加大内存使用的压力。业务上应当避免长时间运行的事务。应小心出现一些长时间运行的DDL操作如创建索引等可能会对事务产生阻塞。分布式事务是基于二阶段提交的相比之前的单文档事务模式来说性能有一定的降级。在业务表设计上建议尽可能利用单文档模型来保证数据的一致性和完整性。