事务¶
在处理数据库时,通常需要将操作分组,以便如果一个操作失败,我们可以回滚到最新的安全状态。此解决方案在事务范式中有所描述,并且由大多数数据库引擎实现,因为它对于满足 ACID 属性(原子性、一致性、隔离性、持久性)[^ACID] 是必要的。
考虑到这一点,我们给出以下示例:
我们有两个账户(每个账户由姓名和金额表示)。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
在某个时刻,从一个账户到另一个账户进行了一次转账。例如,John 向 Sarah 转账 50 美元
我们有两个账户(每个账户由姓名和金额表示)。
deposit db, "Sarah", 50
withdraw db, "John", 50
重要的是要记住,如果其中一个操作失败,则最终状态将不一致。因此,我们需要将两个操作(存款和取款)作为一个操作执行。如果发生错误,我们希望回滚到该操作从未执行过的状态。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
db.transaction do |tx|
cnn = tx.connection
transfer_amount = 1000
deposit cnn, "Sarah", transfer_amount
withdraw cnn, "John", transfer_amount
end
在上面的示例中,我们通过简单地调用 Database#transaction
方法来启动一个事务(我们如何获得 database
对象封装在 get_bank_db
方法中,超出了本文档的范围)。block
是事务的主体。当block
执行(没有任何错误)时,最终会执行一个隐式提交,将更改持久化到数据库中。如果其中一个操作引发异常,则会执行一个隐式回滚,将数据库恢复到事务开始之前的状态。
异常处理和回滚¶
正如我们之前提到的,当引发异常时,会执行一个隐式回滚,值得一提的是,我们可以捕获该异常。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
begin
db.transaction do |tx|
cnn = tx.connection
transfer_amount = 1000
deposit(cnn, "Sarah", transfer_amount)
# John does not have enough money in his account!
withdraw(cnn, "John", transfer_amount)
end
rescue ex
puts "Transfer has been rolled back due to: #{ex}"
end
我们也可以在事务主体中引发异常:
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
begin
db.transaction do |tx|
cnn = tx.connection
transfer_amount = 50
deposit(cnn, "Sarah", transfer_amount)
withdraw(cnn, "John", transfer_amount)
raise Exception.new "Because ..."
end
rescue ex
puts "Transfer has been rolled back due to: #{ex}"
end
与之前的示例一样,异常会导致事务回滚,然后由我们捕获。
有一个exception
的行为有所不同。如果在块中引发了 DB::Rollback
,则会发生隐式回滚,但异常不会在块之外引发。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
begin
db.transaction do |tx|
cnn = tx.connection
transfer_amount = 50
deposit(cnn, "Sarah", transfer_amount)
withdraw(cnn, "John", transfer_amount)
# rollback exception
raise DB::Rollback.new
end
rescue ex
# ex is never a DB::Rollback
end
显式提交和回滚¶
在所有前面的示例中,回滚都是隐式的,但我们也可以告诉事务回滚:
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
begin
db.transaction do |tx|
cnn = tx.connection
transfer_amount = 50
deposit(cnn, "Sarah", transfer_amount)
withdraw(cnn, "John", transfer_amount)
tx.rollback
puts "Rolling Back the changes!"
end
rescue ex
# Notice that no exception is used in this case.
end
我们也可以使用 commit
方法:
db = get_bank_db
db.transaction do |tx|
cnn = tx.connection
transfer_amount = 50
deposit(cnn, "Sarah", transfer_amount)
withdraw(cnn, "John", transfer_amount)
tx.commit
end
注意:
在使用 commit
或 rollback
之后,事务将不再可用。连接仍然处于打开状态,但任何语句都将在终止事务的上下文中之外执行。
嵌套事务¶
顾名思义,嵌套事务是在另一个事务的范围内创建的事务。这是一个示例:
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
create_account db, "Jack", amount: 0
begin
db.transaction do |outer_tx|
outer_cnn = outer_tx.connection
transfer_amount = 50
deposit(outer_cnn, "Sarah", transfer_amount)
withdraw(outer_cnn, "John", transfer_amount)
outer_tx.transaction do |inner_tx|
inner_cnn = inner_tx.connection
# John => 50 (pending commit)
# Sarah => 150 (pending commit)
# Jack => 0
another_transfer_amount = 150
deposit(inner_cnn, "Jack", another_transfer_amount)
withdraw(inner_cnn, "Sarah", another_transfer_amount)
end
end
rescue ex
puts "Exception raised due to: #{ex}"
end
从上面的示例中可以观察到:即使 outer_tx
正在等待提交,inner_tx
也可以使用更新后的值。outer_tx
和 inner_tx
使用的连接是同一个连接。这是因为 inner_tx
在创建时从 outer_tx
继承了连接。
回滚嵌套事务¶
正如我们已经看到的那样,回滚可以随时被触发(通过异常或显式发送 rollback
消息)
因此,让我们来看一个由外层事务中的异常触发的回滚示例:
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
create_account db, "Jack", amount: 0
begin
db.transaction do |outer_tx|
outer_cnn = outer_tx.connection
transfer_amount = 50
deposit(outer_cnn, "Sarah", transfer_amount)
withdraw(outer_cnn, "John", transfer_amount)
outer_tx.transaction do |inner_tx|
inner_cnn = inner_tx.connection
# John => 50 (pending commit)
# Sarah => 150 (pending commit)
# Jack => 0
another_transfer_amount = 150
deposit(inner_cnn, "Jack", another_transfer_amount)
withdraw(inner_cnn, "Sarah", another_transfer_amount)
end
raise Exception.new("Rollback all the things!")
end
rescue ex
puts "Exception raised due to: #{ex}"
end
在外层事务块中触发的回滚,回滚了所有更改,包括内层事务块中的更改(如果我们使用显式回滚,也是如此)。
如果回滚是由内层事务块中的异常触发的,则会回滚所有更改,包括外层事务中的更改。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
create_account db, "Jack", amount: 0
begin
db.transaction do |outer_tx|
outer_cnn = outer_tx.connection
transfer_amount = 50
deposit(outer_cnn, "Sarah", transfer_amount)
withdraw(outer_cnn, "John", transfer_amount)
outer_tx.transaction do |inner_tx|
inner_cnn = inner_tx.connection
# John => 50 (pending commit)
# Sarah => 150 (pending commit)
# Jack => 0
another_transfer_amount = 150
deposit(inner_cnn, "Jack", another_transfer_amount)
withdraw(inner_cnn, "Sarah", another_transfer_amount)
raise Exception.new("Rollback all the things!")
end
end
rescue ex
puts "Exception raised due to: #{ex}"
end
有一种方法可以回滚内层事务中的更改,但保留外层事务中的更改。在 inner_tx
对象中使用 rollback
。这将只回滚内层事务。这是一个示例:
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
create_account db, "Jack", amount: 0
begin
db.transaction do |outer_tx|
outer_cnn = outer_tx.connection
transfer_amount = 50
deposit(outer_cnn, "Sarah", transfer_amount)
withdraw(outer_cnn, "John", transfer_amount)
outer_tx.transaction do |inner_tx|
inner_cnn = inner_tx.connection
# John => 50 (pending commit)
# Sarah => 150 (pending commit)
# Jack => 0
another_transfer_amount = 150
deposit(inner_cnn, "Jack", another_transfer_amount)
withdraw(inner_cnn, "Sarah", another_transfer_amount)
inner_tx.rollback
end
end
rescue ex
puts "Exception raised due to: #{ex}"
end
如果在内层事务块中引发了 DB::Rollback
异常,也是如此。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
create_account db, "Jack", amount: 0
begin
db.transaction do |outer_tx|
outer_cnn = outer_tx.connection
transfer_amount = 50
deposit(outer_cnn, "Sarah", transfer_amount)
withdraw(outer_cnn, "John", transfer_amount)
outer_tx.transaction do |inner_tx|
inner_cnn = inner_tx.connection
# John => 50 (pending commit)
# Sarah => 150 (pending commit)
# Jack => 0
another_transfer_amount = 150
deposit(inner_cnn, "Jack", another_transfer_amount)
withdraw(inner_cnn, "Sarah", another_transfer_amount)
# Rollback exception
raise DB::Rollback.new
end
end
rescue ex
puts "Exception raised due to: #{ex}"
end
[^ACID]: Theo Haerder 和 Andreas Reuter。1983。面向事务的数据库恢复原则。ACM Comput. Surv. 15, 4 (1983 年 12 月),287-317。DOI=http://dx.doi.org/10.1145/289.291