DDS事务与读写关注
事务
- 事务简介
在DDS中,对单个文档的操作具有原子性。由于您可以使用嵌入式文档和数组在单个文档结构中捕获数据之间的关系,而不是在多个文档和集合中进行规范化,因此这种单文档原子性避免了许多实际使用案例中对多文档事务的需求。
对于需要多个文档更新的原子性或多个文档读取之间的一致性的适用场景:从4.0版本开始,DDS提供了针对副本集执行多文档事务的能力。
多文档事务可以跨多个操作、集合、数据库和文档使用。多文档事务提供了一个“全或无”的命题。当一个事务提交时,所有在事务中做的数据更改都会被保存。如果事务中的任何操作失败,事务将中止,并且在事务中进行的所有数据更改都将被丢弃,而不会变得可见。在事务提交之前,事务中的写操作在事务之外是不可见的。
在大多数情况下,多文档事务比单文档写入要付出更大的性能代价,多文档事务的可用性不应该取代有效的模式设计。对于许多场景,非规范化数据模型(嵌入文档和数组)将继续适合您的数据和用例。也就是说,对于许多场景,适当地对数据进行建模将最大限度地减少对多文档事务的需求。
- 事务API
下面的mongo shell代码示例展示了使用DDS事务的关键API:
// Runs the txnFunc and retries if TransientTransactionError encountered function runTransactionWithRetry(txnFunc, session) { while (true) { try { txnFunc(session); // performs transaction break; } catch (error) { // If transient error, retry the whole transaction if ( error.hasOwnProperty("errorLabels") && error.errorLabels.includes("TransientTransactionError") ) { print("TransientTransactionError, retrying transaction ..."); continue; } else { throw error; } } } } // Retries commit if UnknownTransactionCommitResult encountered function commitWithRetry(session) { while (true) { try { session.commitTransaction(); // Uses write concern set at transaction start. print("Transaction committed."); break; } catch (error) { // Can retry commit if (error.hasOwnProperty("errorLabels") && error.errorLabels.includes("UnknownTransactionCommitResult") ) { print("UnknownTransactionCommitResult, retrying commit operation ..."); continue; } else { print("Error during commit ..."); throw error; } } } } // Updates two collections in a transactions function updateEmployeeInfo(session) { employeesCollection = session.getDatabase("hr").employees; eventsCollection = session.getDatabase("reporting").events; session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } ); try{ employeesCollection.updateOne( { employee: 3 }, { $set: { status: "Inactive" } } ); eventsCollection.insertOne( { employee: 3, status: { new: "Inactive", old: "Active" } } ); print(tojson(employeesCollection.find({ employee: 3 }).toArray())); print(tojson(eventsCollection.find({ employee: 3 }).toArray())); print("countDocuments = " + eventsCollection.countDocuments({})); } catch (error) { print("Caught exception during transaction, aborting.", error); session.abortTransaction(); throw error; } commitWithRetry(session); } // insert data employeesCollection = db.getSiblingDB("hr").employees; eventsCollection = db.getSiblingDB("reporting").events; employeesCollection.drop(); eventsCollection.drop(); for (var i = 0; i < 10; ++i) { employeesCollection.insertOne({employee: i}); eventsCollection.insertOne({employee: i}); } // Start a session. session = db.getMongo().startSession( { readPreference: { mode: "primary" } } ); try{ runTransactionWithRetry(updateEmployeeInfo, session); } catch (error) { // Do something with error } finally { session.endSession(); }
事务与会话相关联。当启动一个事务的时候,需要先启动一个会话。在任何给定的时间,一个会话只能有一个打开的事务。如果会话结束并且它有一个打开的事务,则该事务将中止。
使用驱动程序时,必须将会话传递给事务中的每个操作。
- 事务和原子性
- 当事务提交时,在事务中进行的所有数据更改都将被保存并在事务外部可见。在事务提交之前,事务中所做的数据更改在事务外部是不可见的。
- 当事务中止时,在事务中进行的所有数据更改都将被丢弃,而不会变得可见。例如,如果事务中的任何操作失败,事务将中止,并且在事务中进行的所有数据更改都将被丢弃,而不会变得可见。
- 事务的操作限制
在事务中:
- 您可以在已经存在的集合上指定读/写(CRUD)操作。集合可以在不同的数据库中。
- 您无法读取/写入config、admin或local数据库中的集合。
- 您无法写入system.*集合。
- 不支持explain方法。
- 对于在事务外部创建的游标,不能在事务内部调用getMore。
- 对于在事务中创建的游标,不能在事务外部调用getMore。
- 无法执行非CRUD的命令,包括listCollections/listIndexes/createUser/getParameter/count等。
- 要在事务中执行计数操作,请使用$count聚合阶段或$group(带有$sum表达式)聚合阶段。从DDS4.0开始,mongo shell提供了db.collection.countDocuments()方法,该方法使用带有$sum表达式的$group来执行计数。
- 不能包含会导致创建新集合的插入操作,即隐式创建集合的操作。不能包含影响数据库目录的操作,例如创建或删除集合或索引。
- 事务使用注意事项
- 运行时间限制。
默认情况下,事务的运行时间必须小于一分钟。您可以使用transactionLifetimeLimitSeconds参数修改此限制。超过此限制的事务将被视为已过期,并将由定期清理过程中止。
- Oplog大小限制
当事务提交时,如果事务包含任何写操作,则创建单个oplog(操作日志)条目。也就是说,事务中的各个操作没有对应的oplog条目。相反,单个oplog条目包含一个事务中的所有写操作。事务的oplog条目必须在16MB的BSON文档大小限制内。
- 缓存
- 当您放弃一个事务时,中止该事务。
- 当您在事务中的单个操作过程中遇到错误时,请中止事务并重试事务。
transactionLifetimeLimitSeconds参数可以确保定期中止过期的事务,以缓解存储缓存压力。
- 事务和锁
默认情况下,事务最多等待5毫秒来获取事务中的操作所需的锁。如果事务无法在5毫秒内获得所需的锁,则事务中止。事务在中止或提交时会释放所有持有的锁。
获取锁请求超时时间,您可以使用maxTransactionLockRequestTimeoutMillis参数来调整事务等待获取锁的时间。增大maxTransactionLockRequestTimeoutMillis可以让事务中的操作等待指定的时间来获取所需的锁。这有助于避免在瞬时并发锁获取(如快速运行的元数据操作)时发生事务中止。但是,这可能会延迟发生死锁的事务中止。
- 挂起的DDL操作和事务
如果多文档事务正在进行,则影响相同数据库的新DDL操作会在事务之后等待。当这些挂起的DDL操作存在时,与挂起的DDL操作访问同一数据库的新事务无法获得所需的锁,并将在等待maxTransactionLockRequestTimeoutMillis后中止。此外,访问同一数据库的新的非事务性操作将阻塞,直到它们达到其maxTimeMS限制。
为了解释说明,请比较以下两种情况:
考虑这样一种情况:一个正在进行的事务对hr数据库中的employees集合执行各种CRUD操作。当该事务正在进行时,可以启动并完成访问hr数据库中foobar集合的单独事务。
考虑另一种情况:正在进行的事务对hr数据库中的employees集合执行各种CRUD操作,并发出单独的DDL操作以在hr数据库中的employees 集合上创建索引。DDL操作需等待事务完成。
当DDL操作挂起时,一个新的事务尝试访问hr数据库中的foobar集合。如果DDL操作在maxTransactionLockRequestTimeoutMillis之前一直挂起,则新事务将中止。
- 正在进行的事务和写入冲突
如果多文档事务正在进行中,并且事务外部的写操作修改了该事务中的操作稍后将会尝试修改的文档,则事务会因为写冲突而中止。 如果一个多文档事务正在进行,并且已经获取了一个锁来修改文档,那么当事务外部的写操作尝试修改同一个文档时,该写操作会一直等待,直到事务结束。
- 正在进行的事务和过时读取
事务内的读操作可以返回陈旧的数据。也就是说,事务内的读操作不能保证看到其他已提交事务或非事务性写操作执行的写操作。
例如,以下序列:
- 事务正在进行中。
- 事务外部的写入删除文档。
- 事务内部的读取操作能够读取现在删除的文档,因为该操作使用的是写入之前的快照。
为了避免单个文档的事务内部的陈旧读取,可以使用db.collection.findOneAndUpdate()方法。例如:// insert data employeesCollection = db.getSiblingDB("hr").employees; employeesCollection.drop(); employeesCollection.insertOne({ _id: 1, employee: 1, status: "Active" }); // Start a session. session = db.getMongo().startSession( { readPreference: { mode: "primary" } } ); session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } ); employeesCollection = session.getDatabase("hr").employees; employeeDoc = employeesCollection.findOneAndUpdate( { _id: 1, employee: 1, status: "Active" }, { $set: { employee: 1 } }, { returnNewDocument: true } ); print(tojson(employeeDoc));
- 如果employee文档在事务之外发生了更改,则事务中止。
- 如果employee文档没有更改,事务将返回文档并锁定文档。
- 正在进行的事务和写入冲突
- 运行时间限制。
- 事务使用最佳实践
- 默认情况下,DDS将自动中止任何运行超过60秒的多文档事务。请注意,如果服务器的写入量很低,您可以灵活地调整事务以获得更长的执行时间。为了解决超时问题,应该将事务分成更小的部分,以便在配置的时间限制内执行。您还应该确保使用适当的索引覆盖来正确优化您的查询模式,以允许在事务中快速访问数据。
- 对于一个事务中可以读取的文档数量没有硬性限制。作为最佳实践,在一个事务中修改的文档不应超过1000个。对于需要修改超过1000个文档的操作,开发人员应该将事务分解为单独的部分,这些部分可以批量处理文档。
- 在DDS4.0中,事务在单个oplog条目中表示,因此必须在16MB文档大小限制内。更新操作只存储更新的增量(即已更改的内容),而插入操作将存储整个文档。因此,事务中所有语句的oplog描述的总和必须小于16MB。如果超过此限制,事务将被中止并完全回滚。因此,事务应该被分解成一个更小的操作集,这些操作集可以用16MB或更小的空间表示。
- 当事务中止时,将异常返回给驱动程序,并将事务完全回滚。开发人员可以添加捕获并重试由于临时异常(例如暂时的网络故障或主副本选举)而中止的事务的应用逻辑。对于可重试写入,DDS驱动程序将自动重试事务的提交语句。
- DDL操作(如创建索引或删除数据库)会阻塞命名空间上正在运行的事务。在DDL操作挂起期间尝试新访问命名空间的所有事务将无法获得锁,从而中止新事务。
事务和读关注
事务中的操作使用事务级别的读关注。也就是说,在事务内部,在集合和数据库级别设置的任何读关注都会被忽略。可以在事务开始时设置事务级别的读关注。
如果未设置事务级别的读关注,则事务级别的读关注默认为会话级别的读关注。
如果未设置事务级别和会话级别的读关注,则事务级别的读关注默认为客户端级别的读关注。默认情况下,对于针对主节点的读取,客户端级别的读关注为"local"
- 多文档事务支持以下读关注级别:
事务和写关注
事务使用事务级别的写操作来提交写操作。事务内部的操作设置的写关注将被忽略。不要对事务内的单个写操作显式设置写关注。您可以在事务开始时设置事务级别的写关注:
如果未设置事务级别的写关注,则事务级别的写关注默认为提交的会话级别的写关注。
如果未设置事务级写关注和会话级写关注,则事务级写关注默认为客户端级写关注。默认情况下,客户端级别的写关注为w: 1。
- 多文档事务支持以下写关注w值:
- w: 1
写关注w: 1:在提交应用到主节点后返回确认。当使用w: 1提交时,如果发生故障,事务会被回滚。
当使用写关注w: 1进行提交时,事务级别的读关注"majority"并不能保证事务中的读操作能读取到大多数节点已提交的数据。
当您使用写关注w: 1进行提交时,事务级别的读关注"snapshot"无法保证事务中的读操作使用了大多数节点已提交数据的快照。
- w: "majority"
写关注w: "majority"在提交已应用于大多数(M个)投票成员之后返回确认;即,该提交已应用于主节点和(M-1个)有投票权的从节点。
当使用写关注w: "majority"进行提交时,事务级别的读关注"majority"保证能读取到大多数节点已提交的数据。
当使用写关注w: "majority"进行提交时,事务级别的读关注"snapshot"保证能读取来自大多数节点已提交数据的同步快照。
- w: 1
- 事务和写关注使用建议
- 在大多数情况下,推荐使用"majority"级别的写关注·。
这种设置能够确保写入操作得到副本集中大多数节点的确认,从而即使在节点故障或异常切换的情况下,也能避免数据丢失或回滚的风险。
- 在需要高写入性能的场景中,可以考虑使用w: 1的写关注,并密切关注从节点的复制延迟。
w: 1的写关注通常能提供更优的写入性能,适用于写入密集型的场景。同时,必须合理监控从节点的复制延迟,因为延迟过大可能导致主节点异常回滚。此外,如果复制延迟超过了oplog的保留时间,从节点可能会进入异常的RECOVERING状态且无法自动恢复,从而降低实例的可用性,因此应优先关注并处理此类问题。
- 根据不同的操作需求,设置最适合的写关注。
业务侧可以根据实际需求灵活调整写关注,以满足多样化的业务场景。例如,金融交易数据可以使用带写关注的事务来保证原子性;游戏业务核心玩家数据可以使用w: "majority"级别的写关注来确保数据不会被回滚;日志数据则可以使用默认或w: 1的写关注获得更优的写入性能。
- 在大多数情况下,推荐使用"majority"级别的写关注·。