事务
MongoDB 服务器 4.0 版本引入了多文档事务。(在单个文档内更新多个字段在所有 MongoDB 版本中都是原子的。)Ruby 驱动程序版本 2.6.0 添加了对事务的支持。
使用事务
为了开始一个事务,应用程序必须有一个会话.
使用事务的推荐方法是利用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
不会尝试提交或中止(但仍然可能在块外传播的临时事务错误上重试块)。
如果事务的提交结果未知,则也会重试块。这可能发生在例如在提交期间集群进行选举的情况下。在这种情况下,当块被重试时,拓扑的主服务器可能会发生变化。
目前,with_transaction
会在其执行开始后的120秒内停止重试块和提交。此时间不可配置,并且可能在未来的驱动程序版本中更改。请注意,这并不保证 with_transactions
的总体运行时间将是120秒或更少——只是当壁钟时间过去120秒后,不会发起进一步的尝试。
如果需要更多对事务的控制,也提供了一组低级API。
with_transaction
使用与 start_transaction
相同的选项,包括读取关注点、写入关注点和读取偏好
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
在开始事务时,也可以指定读取关注点、写入关注点和读取偏好
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
注意:未解决的事务可能持有对服务器上各种对象的锁定,例如数据库。例如,以下片段中的drop调用将在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
将会导致错误。