軟體事務記憶體導論(八)提交和回滾事件
宣告:本文是《Java虛擬機器併發程式設計》的第六章,感謝華章出版社授權併發程式設計網站釋出此文,禁止以任何形式轉載此文。
提交和回滾事件
Java的try-catch-finally語法結構不但使我們可以安全地處理異常,還能夠在程式丟擲異常時選擇性地執行一些程式碼。同樣地,我們也可以控制程式在事務成功提交之後去執行某段程式碼,而當事務回滾時則去執行另一段程式碼。StmUtils中的deferred()和compensatiing()這兩個函式分別提供了上述功能。特別地,在實現事務的過程中,為保證事務能順利完成,我們通常會加入一些帶副作用的邏輯,而deferred()函式則是一個執行所有這部分邏輯的絕佳地點。
Java中的提交和回滾事件
我們可以把想要在事務成功完成之後執行的程式碼放在Runnable介面實現部分的程式碼塊中,並將其作為引數傳給StmUtils的deferred()函式。同樣地,我們也可以把想要在事務失敗之後執行的程式碼封裝在Runnable介面中傳給compensating()函式。由於這兩個函式必須在事務的環境下執行,所以我們只有在automically()函式的函式體中才能呼叫他們。
public class Counter { private final Ref<Integer> value = new Ref<Integer>(1); public void decrement() { new Atomic<Integer>() { public Integer atomically() { deferred(new Runnable() { public void run() { System.out.println( "Transaction completed...send email, log, etc."); } }); compensating(new Runnable() { public void run() { System.out.println("Transaction aborted...hold the phone"); } }); if(value.get() <= 0) throw new RuntimeException("Operation not allowed"); value.swap(value.get() - 1); return value.get(); } }.execute(); } }
在Counter類的定義程式碼中我們看到,Counter類僅含有一個名為decrement()的例項方法。在這個方法中,我們繼承了Atomic類並實現了atomically()函式。在前面的例子中,我們都僅僅是簡單地把事務的邏輯程式碼放在這個位置。而現在,除了原有的邏輯程式碼之外,我們把事務成功和事務回滾之後要執行的程式碼也放到了atomically()裡面。下面讓我們構建一個簡單的測試用例來驗證一下Counter的功能:
package com.agiledeveloper.pcj; public class UseCounter { public static void main(final String[] args) { Counter counter = new Counter(); counter.decrement(); System.out.println("Let's try again..."); try { counter.decrement(); } catch(Exception ex) { System.out.println(ex.getMessage()); } } }
通過執行UseCounter,我們可以清楚地觀察到事務成功完成和失敗時程式的執行邏輯:
Transaction aborted...hold the phone Transaction completed...send email, log, etc. Let's try again... Transaction aborted...hold the phone Operation not allowed
當第一次呼叫decrement()函式併成功完成事務之後,封裝在deferred()函式內的程式碼邏輯將被執行。而當我們第二次呼叫decrement()時,由於事務執行過程中丟擲了異常,所以事務將被回滾,而封裝在compensating()函式內的程式碼也將被執行。最後我們需要注意的是,輸出結果中最頂部的那個非預期的重試是由我們之前在6.9節中曾討論過的預設優化設定所導致的。
deferred()函式是一個執行事務收尾工作以便使其效果固化的絕佳地點,所以我們可以在裡面隨便進行列印、顯示訊息、釋出通知以及提交資料庫事務等操作。如果我們在事務之外有什麼遺留的工作待完成,那麼這個函式無疑是最好的完成地點。與deferred()類似的是,compensating()函式是記錄事務失敗資訊的好地方。此外,如果我們之前已經將非託管物件(即那些沒有使用Akka Ref進行管理的物件)與託管物件混雜在一起的話,那麼這裡也是糾正這一錯誤的合適地點——但是由於這種做法太容易出錯,所以請你最好避免採用這樣的設計思路。
Scala中的提交和回滾事件
在Scala中,我們處理提交和回滾事件的方式與Java基本相同,唯一區別就是在Scala中我們可以將閉包/函式值直接傳遞給deferred()和compensating()。下面讓我們將Counter類由Java轉譯成Scala。
class Counter { private val value = Ref(1) def decrement() = { atomic { deferred { println("Transaction completed...send email, log, etc.") } compensating { println("Transaction aborted...hold the phone") } if(value.get() <= 0) throw new RuntimeException("Operation not allowed") value.swap(value.get() - 1) value.get() } } }
在上面的程式碼中,我們將事務執行成功時所要執行的那部分程式碼封裝在一個閉包中,然後將其作為引數傳遞給deferred()函式。類似地,事務回滾時所要執行的程式碼也被作為一個閉包賦給了compensating()函式。與此同時,這兩個函式又與事務邏輯程式碼一起被置於表示atomic()函式的閉包當中。這段程式碼再次彰顯了Scala在語法上簡潔明瞭的特徵。下面讓我們將UseConuter類也從Java轉譯成Scala:
package com.agiledeveloper.pcj object UseCounter { def main(args : Array[String]) : Unit = { val counter = new Counter() counter.decrement() println("Let's try again...") try { counter.decrement() } catch { case ex => println(ex.getMessage()) } } }
如下所示,Scala版程式碼的執行結果與Java版的結果是完全相同的:
Transaction aborted...hold the phone Transaction completed...send email, log, etc. Let's try again... Transaction aborted...hold the phone Operation not allowed