交易¶
當使用資料庫時,通常需要將操作分組,以便在一個操作失敗時,可以回到最近的安全狀態。此解決方案在交易範式中描述,並由大多數資料庫引擎實作,因為它需要滿足 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
outer_tx
區塊中的回滾會回滾所有變更,包括 inner_tx
區塊中的變更(如果我們使用顯式回滾也會發生相同的情況)。
如果回滾是由 inner_tx
區塊中的例外觸發,則包括 outer_tx
中的所有變更都會被回滾。
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-transaction
中的變更,但保留 outer-transaction
中的變更。在 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
如果在 inner-transaction
區塊中引發 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