事务1、事务简介1.1、ACID的定义2、如何使用事务3、对应用程序的事务限制进行调优3.1、时间和oplog大小限制3.1.1、时间限制3.1.2、oplog 大小限制1、事务简介如上所述事务是数据库中处理的逻辑单元包括一个或多个数据库操作既可以是读操作也可以是写操作。在某些情况下作为这个逻辑处理单元的一部分应用程序可能需要对多个文档在一个或多个集合中进行读写。事务的一个重要方面是它永远不会只完成一部分——它要么成功要么失败。要使用事务你所部署的 MongoDB 必须是MongoDB 4.2 或更高版本MongoDB 的驱动程序必须更新为 MongoDB 4.2 或更高版本。MongoDB 提供了一个驱动程序兼容性参考页面可以用来确保 MongoDB驱动程序版本的兼容性。1.1、ACID的定义ACID 是一个“真正”事务所需要具备的一组属性集合。ACID 是原子性atomicity​、一致性consistency​、隔离性isolation和持久性durability的缩写。ACID 事务可以保证数据和数据库状态的有效性即使在出现断电或其他错误的情况下也是如此。原子性确保了事务中的所有操作要么都被应用要么都不被应用。事务永远不能应用部分操作要么被提交要么被中止。一致性确保了如果事务成功那么数据库将从一个一致性状态转移到下一个一致性状态。隔离性是允许多个事务同时在数据库中运行的属性。它保证了一个事务不会查看到任何其他事务的部分结果这意味着多个事务并行运行与依次运行每个事务所获得的结果相同。当数据库满足所有这些属性并且只有成功的事务才会被处理时它就被称为是符合 ACID 的数据库。如果在事务完成之前发生故障那么 ACID 确保不会更改任何数据。MongoDB 是一个分布式数据库它支持跨副本集和/或分片的 ACID 事务。网络层增加了额外的复杂性。MongoDB 的工程团队提供了一些记录和谈话视频描述了他们是如何实现支持 ACID 事务的必要特性的。2、如何使用事务MongoDB 提供了两种 API 来使用事务。第一种是与关系数据库类似的语法如 start_transaction 和commit_transaction​称为核心 API第二种称为回调 API这是使用事务的推荐方法。核心 API 不为大多数错误提供重试逻辑它要求开发人员为操作、事务提交函数以及所需的任何重试和错误逻辑手动编写代码。与核心 API 不同回调 API 提供了一个单独的函数该函数封装了大量功能包括启动与指定逻辑会话关联的事务、执行作为回调函数提供的函数以及提交事务或在出现错误时中止​。此函数还包含了处理提交错误的重试逻辑。在 MongoDB 4.2 中添加回调 API 是为了简化使用事务的应用程序开发也便于添加处理事务错误的应用程序重试逻辑。在这两种 API 中都是由开发人员负责启动事务使用的逻辑会话。这两种 API 都要求事务中的操作与特定的逻辑会话相关联例如将会话传递给每个操作​。MongoDB 中的逻辑会话会在整个 MongoDB 部署的上下文中跟踪操作的时间和顺序。逻辑会话或服务器端会话是底层框架的一部分其被客户端会话用于支持MongoDB 中的可重试写入和因果一致性——这两个特性都是作为支持事务所需基础的一部分在 MongoDB 3.6中添加的。在 MongoDB 中一个特定的读写操作序列被定义为因果一致的客户端会话它们的顺序反映了因果关系。客户端会话由应用程序启动并用于与服务器端会话进行交互。2019 年MongoDB 的 6 位高级工程师在 SIGMOD 2019 会议上发表了一篇论文即“Implementation ofCluster-wide Logical Clock and Causal Consistencyin MongoDB”​。该论文为 MongoDB 中的逻辑会话和因果一致性背后的机制提供了更深层次的技术解释记录了一个跨越多个团队历时多年的工程项目的成果。这项工作包括更改存储层的多个方面、添加新的复制共识协议、修改分片架构、重构分片集群元数据以及添加一个全局逻辑时钟。这些更改为数据库添加符合 ACID 的事务特性提供了必要的基础。作者是负责分片的软件工程师 Misha Tyulenev、分布式系统副总裁 Andy Schwerin、分布式系统首席产品经理Asya Kamsky、负责分片的高级软件工程师 RandolphTan、分布式系统产品经理 Alyson Cabral以及负责分片的软件工程师 Jack Mulrow。应用程序中的复杂性和所需的额外编码是推荐使用回调 API 而不使用核心 API 的主要原因。下表对它们之间的差异进行了简单总结。为了理解这两种 API 之间的差异可以使用一个电子商务网站的简单事务示例来进行比较。在这个示例中需要在该电子商务网站上下订单并且相应的产品在销售时应从可用库存中删除。这会涉及单个事务中不同集合的两个文档。这两个操作将是该事务示例的核心orders.insert_one({sku:abc123,qty:100},sessionsession)inventory.update_one({sku:abc123,qty:{$gte:100}},{$inc:{qty:-100}},sessionsession)首先了解如何在这个事务示例中使用 Python 中的核心API。事务的两个操作显示在了如下程序的步骤 1 中# 使用DNS种子列表连接格式为连接定义uriString urimongodbsrv://server.example.com/clientMongoClient(uriString)my_wc_majorityWriteConcern(majority,wtimeout1000)# 前提条件/步骤0如果集合不存在就创建集合 # 事务中的CRUD操作必须在已存在的集合中进行 client.get_database(webshop,write_concernmy_wc_majority).orders.insert_one({sku:abc123,qty:0})client.get_database(webshop,write_concernmy_wc_majority).inventory.insert_one({sku:abc123,qty:1000})# 步骤1在事务中定义操作及其顺序 defupdate_orders_and_inventory(my_session):ordersmy_session.client.webshop.orders inventorymy_session.client.webshop.inventorywithmy_session.start_transaction(read_concernReadConcern(snapshot),write_concernWriteConcern(wmajority),read_preferenceReadPreference.PRIMARY):orders.insert_one({sku:abc123,qty:100},sessionmy_session)inventory.update_one({sku:abc123,qty:{$gte:100}},{$inc:{qty:-100}},sessionmy_session)commit_with_retry(my_session)# 步骤2尝试使用重试逻辑运行并提交事务 defcommit_with_retry(session):whileTrue:try:# 提交操作会使用事务开始时设置的写关注 session.commit_transaction()print(Transaction committed.)breakexcept(ConnectionFailure,OperationFailure)asexc:# 可以重试提交ifexc.has_error_label(UnknownTransactionCommitResult):print(UnknownTransactionCommitResult, retrying commit operation ...)continueelse:print(Error during commit ...)raise # 步骤3尝试使用重试逻辑运行事务函数txn_func defrun_transaction_with_retry(txn_func,session):whileTrue:try:txn_func(session)# 运行事务breakexcept(ConnectionFailure,OperationFailure)asexc:# 如果出现暂时性错误则重试整个事务ifexc.has_error_label(TransientTransactionError):print(TransientTransactionError, retrying transaction ...)continueelse:raise # 步骤4开启一个会话withclient.start_session()asmy_session:# 步骤5调用函数run_transaction_with_retry并向其传递update_orders_and_inventory 函数的调用和my_session会话以关联此事务try:run_transaction_with_retry(update_orders_and_inventory,my_session)except Exceptionasexc:# 错误处理。在核心API中没有提供错误处理代码 raise现在再看看如何在 Python 中为这个事务示例使用回调API。事务的两个操作显示在了如下程序的步骤 中。# 使用DNS种子列表连接格式为连接定义uriString uriStringmongodbsrv://server.example.com/clientMongoClient(uriString)my_wc_majorityWriteConcern(majority,wtimeout1000)# 前提条件/步骤0如果集合不存在就创建集合 # 事务中的CRUD操作必须在已存在的集合中进行 client.get_database(webshop,write_concernmy_wc_majority).orders.insert_one({sku:abc123,qty:0})client.get_database(webshop,write_concernmy_wc_majority).inventory.insert_one({sku:abc123,qty:1000})# 步骤1定义回调方法以指定在事务内部执行的操作序列 defcallback(my_session):ordersmy_session.client.webshop.orders inventorymy_session.client.webshop.inventory # 重要必须将会话变量my_session传递给操作 orders.insert_one({sku:abc123,qty:100},sessionmy_session)inventory.update_one({sku:abc123,qty:{$gte:100}},{$inc:{qty:-100}},sessionmy_session)# 步骤2启动客户端会话withclient.start_session()assession:# 步骤3使用with_transaction启动事务、执行回调并提交或在发生错误时中止 session.with_transaction(callback,read_concernReadConcern(local),write_concernmy_write_concern_majority,read_preferenceReadPreference.PRIMARY)在 MongoDB 的多文档事务中只能对已存在的集合或数据库执行读写CRUD操作。如示例所示如果希望将操作加入事务中则必须首先在事务之外创建集合。在事务中不允许创建、删除集合或者进行索引操作。3、对应用程序的事务限制进行调优在使用事务时有几个重要的参数需要注意。可以对它们进行调整以确保应用程序能够最佳地使用事务。3.1、时间和oplog大小限制在 MongoDB 事务中有两类主要的限制。第一类与事务的时间限制有关控制特定事务可以运行多长时间、事务等待获取锁的时间以及所有事务将运行的最大长度。第二类与 MongoDB 的 oplog 条目和单个条目的大小限制有关。3.1.1、时间限制事务的默认最大运行时间是 1 分钟。可以通过在mongod 实例级别上修改transactionLifetimeLimitSeconds的限制来增加。对于分片集群必须在所有分片副本集成员上设置该参数。超过此时间后事务将被视为已过期并由定期运行的清理进程中止。清理进程每 60 秒或每transactionLifetimeLimitSeconds/2运行一次以较小的值为准。要显式设置事务的时间限制建议在提交事务时指定maxTimeMS参数。如果maxTimeMS没有设置那么将使用transactionLifetimeLimitSeconds如果设置了maxTimeMS但这个值超过了transactionLifetimeLimitSeconds那么还是会使用transactionLifetimeLimitSeconds。事务等待获取其操作所需锁的默认最大时间是 5 毫秒。可以通过修改由maxTransactionLockRequestTimeoutMillis参数控制的限制来增加。如果事务在此期间无法获得锁则该事务会被中止。maxTransactionLockRequestTimeoutMillis可以设置为 0、-1 或大于 0 的数字。将其设置为 0 意味着如果事务无法立即获得所需的所有锁则该事务会被中止。设置为 -1 将使用由 maxTimeMS 参数所指定的特定于操作的超时时间。任何大于 0 的数字都将等待时间配置为该时间以秒为单位以作为事务尝试获取所需锁的指定时间段。3.1.2、oplog 大小限制MongoDB 会创建出与事务中写操作数量相同的oplog 条目。但是每个 oplog 条目必须在 16MB 的BSON 文档大小限制之内。事务为 MongoDB 的一致性提供了一个有用的特性但是它们应该与富文档模型结合使用。该模型的灵活性以 及使用设计模式等最佳实践有助于避免在大多数情况下使用事务。事务是一个强大的特性最好在应用程序中谨慎使用。