事务
MongoDB 服务器 4.0版本引入了多文档事务。 (在所有版本的MongoDB中,对单个文档中多个字段的更新都是原子性的。) Ruby驾驶员版本2.6.0 添加了对事务的支持。
使用事务
要启动ACID 事务,应用程序必须具有会话。
使用事务的推荐方法是利用 with_transaction
辅助方法:
session = client.start_session session.with_transaction do collection.insert_one({hello: 'world'}, session: session) end
with_transaction
助手会执行以下操作:
它在调用提供的区块之前启动事务,并在区块完成时提交事务。
如果区块中的任何操作或提交操作导致暂时性事务错误,则将再次执行区块和/或提交操作。
该区块应该是幂等的,因为它可能会被多次调用。
该区块可以通过调用commit_transaction
或abort_transaction
显式提交或中止事务;在这种情况下, with_transaction
不会尝试提交或中止(但仍可能在出现暂时性事务错误传播出区块时重试区块)。
如果事务的提交结果未知,也会重试该区块。 例如,如果集群在提交期间进行选举,则可能会发生这种情况。 在这种情况下,当重试区块时,拓扑结构的主服务器可能会发生变化。
目前,自开始执行起经过 120 秒后, with_transaction
将停止重试区块和提交。 该时间不可配置,并且可能在未来的驱动程序版本中发生变化。 请注意,这并不能保证with_transactions
的总运行时间为 120 秒或更短,只是保证一旦 120 秒的挂钟时间过去,就不会再启动重试尝试。
如果需要对事务进行更多控制,也可以使用低级 API。
with_transaction
采用与start_transaction
相同的选项,分别是读关注(read concern)、写关注(write concern)和读取偏好(read preference):
session = client.start_session session.with_transaction( read_concern: {level: :majority}, write_concern: {w: 3}, read: {mode: :primary} ) do collection.insert_one({hello: 'world'}, session: session) end
处理 区块内的错误with_transaction
如果with_transaction
区块内的命令失败,可能会导致服务器上的事务中止。 这种情况通常由驱动程序以透明方式处理。 但是,如果应用程序捕获此类错误且未重新引发该错误,则驱动程序将无法确定事务是否已中止。 然后,驱动程序将无限期地重试该区块。
为了避免这种情况,应用程序不得以静默方式处理with_transaction
区块中的错误。 如果应用程序需要处理区块内的错误,则必须重新引发错误。
session.with_transaction do collection.insert_one({hello: 'world'}, session: session) rescue Mongo::Error::OperationFailure => e # Do something in response to the error raise e end
如果应用程序需要以自定义方式处理错误,则应改用低级API 。
低级 API
可以通过在会话上调用start_transaction
方法来启动事务:
session = client.start_session session.start_transaction
还可以在启动事务(transaction)时指定读关注(read concern)、写关注(write concern)和读取偏好(read preference):
session = client.start_session session.start_transaction( read_concern: {level: :majority}, write_concern: {w: 3}, read: {mode: :primary})
要将事务中的更改持久保存到数据库,必须显式提交事务。 如果会话以打开事务结束,则该事务将中止。 事务也可以显式中止。
要提交或中止事务,请在会话实例上调用commit_transaction
或abort_transaction
:
session.commit_transaction session.abort_transaction
注意:未完成的事务可以锁定服务器中的各种对象,例如数据库。 例如,以下代码片段中的删除调用将挂起transactionLifetimeLimitSeconds秒(默认为60 ),直到服务器过期并中止事务:
c1 = Mongo::Client.new(['127.0.0.1:27017']).use(:test_db) session = c1.start_session c1['foo'].insert_one(test: 1) session.start_transaction c1['foo'].insert_one({test: 2}, session: session) c2 = Mongo::Client.new(['127.0.0.1:27017']).use(:test_db) # hangs c2.database.drop
由于事务与服务器端会话关联,因此关闭客户端不会中止此客户端启动的事务 — 应用程序必须调用abort_transaction
或等待服务器端事务超时。 除了提交或中止事务之外,应用程序还可以结束会话,这将中止此会话上正在进行的事务:
session.end_session c2 = Mongo::Client.new(['127.0.0.1:27017']).use(:test_db) # ok c2.database.drop
处理错误
如果事务中的命令失败,则服务器上的事务可能会中止。 中止事务的错误的错误标签中没有TransientTransactionError
。 提交此类事务的尝试将被拒绝,并显示NoSuchTransaction
错误。
重试提交
如果事务提交失败,可以重试。 以下是执行此操作的 Ruby 代码:
begin session.commit_transaction rescue Mongo::Error => e if e.label?('UnknownTransactionCommitResult') retry else raise end end
事务嵌套
MongoDB 不支持嵌套事务。 在事务已在进行中时尝试调用start_transaction
或with_transaction
将导致错误。