天天看點

軟體事務記憶體導論(五)建立嵌套事務

在之前的示例中,每個用到事務的方法都是各自在其内部單獨建立事務,并且事務所涉及的變動也都是各自獨立送出的。但如果我們想要将多個方法裡的事務調整成一個統一的原子操作的時候,上述做法就無能為力了,是以我們需要使用嵌套事務來實作這一目标。

通過使用嵌套事務,所有被主要函數調用的那些函數所建立的事務都會預設被整合到主要函數的事務中。除此之外,Akka/Multiverse還提供了很多其他配置選項,如新隔離事務(new isolated transactions)等。總之,使用了嵌套事務之後,隻有位于最外層的主要函數事務送出時,其内部所做的變更才會被送出。在具體使用時,為了保證所有嵌套事務能夠作為一個整體成功完成,我們需要保證所有函數都必須在一個可配置的逾時範圍内做完。

我們在4.6節中通過加鎖方式實作的AccountService的transfer()函數将會受益于嵌套事務。因為這個版本的transfer()函數需要按自然順序對所有賬戶排序并顯式地對鎖進行管理。STM将為我們消除所有這些負擔。下面我們會首先在Java中用嵌套事務重新實作這一示例,然後再來看一下該示例在Scala中是如何實作的。

<b>在Java</b><b>中使用嵌套事務</b>

現在讓我們開始對Account類進行事務化的改造吧。首先我們需要把儲存賬戶餘額的變量balance改成托管引用,下面我們就來定義這個字段以及該字段的getter函數。

<code>1</code>

<code>public</code> <code>class</code> <code>Account {</code>

<code>2</code>

<code>final</code> <code>private</code> <code>Ref&lt;Integer&gt; balance =</code><code>new</code> <code>Ref&lt;Integer&gt;();</code>

<code>3</code>

<code>public</code> <code>Account(</code><code>int</code> <code>initialBalance) { balance.swap(initialBalance); }</code>

<code>4</code>

<code>public</code> <code>int</code> <code>getBalance() {</code><code>return</code> <code>balance.get(); }</code>

在構造函數中,我們用Ref的swap()函數将給定的數量設定成balance的初始值。由于swap()函數運作在自己獨立的事務中,是以我們就無需再建立額外的事務了(同時我們假設調用者也不會為這個操作建立額外的事務)。getBalance()函數的情況與之類似,就不再贅述了。

由于deposit()函數需要對balance進行先讀後寫的操作,是以該函數内的所有操作需要整體封裝到一個事務裡運作。下面的代碼為我們展示了如何将這兩個操作封裝到一個獨立事務中的方法。

<code>01</code>

<code>public</code> <code>void</code> <code>deposit(</code><code>final</code> <code>int</code> <code>amount) {</code>

<code>02</code>

<code>new</code> <code>Atomic&lt;Boolean&gt;() {</code>

<code>03</code>

<code>public</code> <code>Boolean atomically() {</code>

<code>04</code>

<code>System.out.println(</code><code>"Deposit "</code> <code>+ amount);</code>

<code>05</code>

<code>if</code> <code>(amount &gt;</code><code>0</code><code>) {</code>

<code>06</code>

<code>balance.swap(balance.get() + amount);</code>

<code>07</code>

<code>return</code> <code>true</code><code>;</code>

<code>08</code>

<code>}</code>

<code>09</code>

<code>throw</code> <code>new</code> <code>AccountOperationFailedException();</code>

<code>10</code>

<code>11</code>

<code>}.</code>

基于同樣的理由,我們需要把withdraw()函數裡的所有操作也封裝到一個獨立的事務中。

<code>public</code> <code>void</code> <code>withdraw(</code><code>final</code> <code>int</code> <code>amount) {</code>

<code>int</code> <code>currentBalance = balance.get();</code>

<code>112</code> <code>• Chapter</code><code>6</code><code>. Introduction to Software Transactional Memory</code>

<code>if</code> <code>(amount &gt;</code><code>0</code> <code>&amp;&amp; currentBalance &gt;= amount) {</code>

<code>balance.swap(currentBalance - amount);</code>

<code>12</code>

<code>}.execute();</code>

<code>13</code>

<code>14</code>

如果運作過程中有異常抛出,則事務将會強制失敗。是以當賬戶内餘額不足或存款/取款操作輸入了非法參數時,我們就可以利用這一點來表示操作失敗。相當簡單,是吧?從此我們就可以不用再擔心同步、加鎖、死鎖等令人煩惱的問題了。

現在到了該浏覽一下執行轉賬操作的AccountService類的時候了,讓我們首先來看一下其中的transfer()函數(校注:java中應該叫方法)

<code>public</code> <code>class</code> <code>AccountService {</code>

<code>public</code> <code>void</code> <code>transfer(</code>

<code>final</code> <code>Account from,</code><code>final</code> <code>Account to,</code><code>final</code> <code>int</code> <code>amount) {</code>

<code>System.out.println(</code><code>"Attempting transfer..."</code><code>);</code>

<code>to.deposit(amount);</code>

<code>System.out.println(</code><code>"Simulating a delay in transfer..."</code><code>);</code>

<code>try</code> <code>{ Thread.sleep(</code><code>5000</code><code>); }</code><code>catch</code><code>(Exception ex) {}</code>

<code>System.out.println(</code><code>"Uncommitted balance after deposit $"</code> <code>+</code>

<code>to.getBalance());</code>

<code>from.withdraw(amount);</code>

<code>15</code>

<code>16</code>

在這個示例中,我們會将多個事務置于互相沖突的環境中,以此來示範嵌套事務的行為并幫助你加深對嵌套事務的了解。Transfer()函數中的所有操作都是在同一個事務中完成的。作為轉賬過程的一部分,我們首先将錢存到目标賬戶中。緊接着,在經過一個為引入事務沖突而專門設定的延時之後,我們将錢從源賬戶中劃走。我們希望當且僅當從源帳戶劃款成功之後,向目标賬戶存款的操作才能夠成功,這也是我們這個事務所要完成的目标。

我們可以通過列印balance的值來觀察轉賬操作是否成功。如果有一個友善的函數來調用transfer()函數,處理下異常,并在最後列印一下balance的值就更好了,下面就讓我們動手寫一個吧:

<code>public</code> <code>static</code> <code>void</code> <code>transferAndPrintBalance(</code>

<code>boolean</code> <code>result =</code><code>true</code><code>;</code>

<code>try</code> <code>{</code>

<code>new</code> <code>AccountService().transfer(from, to, amount);</code>

<code>}</code><code>catch</code><code>(AccountOperationFailedException ex) {</code>

<code>result =</code><code>false</code><code>;</code>

<code>System.out.println(</code><code>"Result of transfer is "</code> <code>+ (result ?</code><code>"Pass"</code> <code>:</code><code>"Fail"</code><code>));</code>

<code>System.out.println(</code><code>"From account has $"</code> <code>+ from.getBalance());</code>

<code>System.out.println(</code><code>"To account has $"</code> <code>+ to.getBalance());</code>

最後我們還需要一個main()函數來讓整個示例運轉起來。

<code>public</code> <code>static</code> <code>void</code> <code>main(</code><code>final</code> <code>String[] args)</code><code>throws</code> <code>Exception {</code>

<code>final</code> <code>Account account1 =</code><code>new</code> <code>Account(</code><code>2000</code><code>);</code>

<code>final</code> <code>Account account2 =</code><code>new</code> <code>Account(</code><code>100</code><code>);</code>

<code>final</code> <code>ExecutorService service = Executors.newSingleThreadExecutor();</code>

<code>service.submit(</code><code>new</code> <code>Runnable() {</code>

<code>public</code> <code>void</code> <code>run() {</code>

<code>try</code> <code>{ Thread.sleep(</code><code>1000</code><code>); }</code><code>catch</code><code>(Exception ex) {}</code>

<code>account2.deposit(</code><code>20</code><code>);</code>

<code>});</code>

<code>service.shutdown();</code>

<code>transferAndPrintBalance(account1, account2,</code><code>500</code><code>);</code>

<code>System.out.println(</code><code>"Making large transfer..."</code><code>);</code>

<code>transferAndPrintBalance(account1, account2,</code><code>5000</code><code>);</code>

在main函數中,我們建立了兩個賬戶,并在一個單獨的線程中從第二個賬戶裡取走$20。與此同時,我們還啟動了一個在賬戶之間轉賬的事務。由于這些操作都會影響到公共執行個體(即兩個賬戶——譯者注),是以這種做法将導緻兩個事務(存$20的事務和轉賬$500的事務——譯者注)産生沖突。于是隻有一個事務能夠順利完成,而另一個将會重做。最後,我們會啟動一個超出源賬戶餘額的轉賬操作,以此來示範存款和取款這兩個互相關聯的事務通過嵌套事務的方式在轉賬過程中實作了原子性的操作。下面讓我們通過輸出結果來觀察事務的行為:

<code>Attempting transfer...</code>

<code>Deposit 500</code>

<code>Simulating a delay in transfer...</code>

<code>Deposit 20</code>

<code>Uncommitted balance after deposit $600</code>

<code>Uncommitted balance after deposit $620</code>

<code>Result of transfer is Pass</code>

<code>From account has $1500</code>

<code>To account has $620</code>

<code>Making large transfer...</code>

<code>17</code>

<code>Deposit 5000</code>

<code>18</code>

<code>19</code>

<code>Uncommitted balance after deposit $5620</code>

<code>20</code>

<code>Result of transfer is Fail</code>

<code>21</code>

<code>22</code>

輸出結果起始處的重試操作讓人看起來有些摸不着頭腦。這個非預期的重試是由Multiverse對于單個對象上的隻讀事務的預設優化造成的。雖然有兩種方法可以重新配置這一行為,但修改了之後可能會對性能造成影響。請參閱Akka/Multiverse文檔來進一步了解變更這一配置所造成的影響。

在本例中,向帳戶2存$20的操作會先完成。而與此同時,從賬戶1向賬戶2的轉賬事務則處于模拟的延遲當中。當轉賬事務重新恢複運作并察覺到其涉及的對象發生了變化時,該事務将悄悄地復原并重做。如果事務在運作過程中一直出現内部資料有變化的情況,則該事務會不斷重做直至成功或逾時退出為止。本例中的轉賬事務是最終成功了的,帳戶餘額的變化充分地反映了這一結果——賬戶1轉出了$500,而賬戶2則從并發的存款和轉賬操作中總共擷取了$520。

本例的最後一個操作是從賬戶1向賬戶2轉$5000。在這個事務中,存款操作順利完成了,但事務能否最終成功還是要看取款操作的結果。不出所料,取款動作由于賬戶餘額不足而失敗并抛了異常。随後,之前的存款動作被復原,系統最終保證了賬戶餘額資料不受事務失敗的影響。

再次聲明,在事務中列印資訊和插入延時都不是好習慣,我在本例中這樣用是為了使你能夠更好地觀察事務的運作順序和重做行為,在實際工作中請最好不要在事務代碼裡列印消息或打日志。請記住,事務是不應該有任何副作用的。如果事務中确實需要包含有副作用的操作,我們可以将這些代碼放到後面将會提到的後置送出(post-commit)handler裡面去。

我可以拍胸脯向你保證,使用事務絕對可以替你分擔大部分并發程式設計方面的煩惱。下面就讓我們通過一組對比來看看事務到底效用幾何。讓我們回顧一下4.6節中我們用加鎖方式實作的轉賬函數transfer(),為友善起見我将代碼列在下面:

<code>public</code> <code>boolean</code> <code>transfer(</code>

<code>final</code> <code>Account from,</code><code>final</code> <code>Account to,</code><code>final</code> <code>int</code> <code>amount)</code>

<code>throws</code> <code>LockException, InterruptedException {</code>

<code>final</code> <code>Account[] accounts =</code><code>new</code> <code>Account[] {from, to};</code>

<code>Arrays.sort(accounts);</code>

<code>if</code><code>(accounts[</code><code>0</code><code>].monitor.tryLock(</code><code>1</code><code>, TimeUnit.SECONDS)) {</code>

<code>if</code> <code>(accounts[</code><code>1</code><code>].monitor.tryLock(</code><code>1</code><code>, TimeUnit.SECONDS)) {</code>

<code>if</code><code>(from.withdraw(amount)) {</code>

<code>}</code><code>else</code> <code>{</code>

<code>return</code> <code>false</code><code>;</code>

<code>}</code><code>finally</code> <code>{</code>

<code>accounts[</code><code>1</code><code>].monitor.unlock();</code>

<code>accounts[</code><code>0</code><code>].monitor.unlock();</code>

<code>23</code>

<code>24</code>

<code>throw</code> <code>new</code> <code>LockException(</code><code>"Unable to acquire locks on the accounts"</code><code>);</code>

<code>25</code>

你可以将上述代碼與去掉了延時和log輸出的事務版本進行比較:

舊版本的代碼既要考慮加鎖的問題又要顧及加鎖的順序,是以很容易出錯。代碼越多越容易出問題,這是顯而易見的道理。在新版本中,我們顯著地降低了代碼量和複雜度。這讓我想起了C.A.R.Hoare的名言:“這世界上有兩種建構軟體設計的方法。一種方法是使其足夠簡單以至于不存在明顯的缺陷。而另一種方法是使其足夠複雜以至于無法看出有什麼毛病” 。隻有讓代碼更少、結構更簡單,我們才能将更多的時間投入到程式邏輯的設計開發中去。

<b>在Scala</b><b>中使用嵌套事務</b>

從上例中我們可以看到,使用了嵌套事務的Java版轉賬函數是非常簡潔的。然而,雖然事務的使用讓我們得以去除Java中那些用于同步的備援代碼,但還是會有一些由于Java文法需要而存在的一些額外代碼。正如我們下面所看到的那樣,Scala的優雅和強大的表達能力使其在代碼清晰簡潔方面更勝一籌。下面就是Scala版的Account類:

<code>class</code> <code>Account(</code><code>val</code> <code>initialBalance</code><code>:</code> <code>Int) {</code>

<code>val</code> <code>balance</code><code>=</code> <code>Ref(initialBalance)</code>

<code>def</code> <code>getBalance()</code><code>=</code> <code>balance.get()</code>

<code>def</code> <code>deposit(amount</code><code>:</code> <code>Int)</code><code>=</code> <code>{</code>

<code>atomic {</code>

<code>println(</code><code>"Deposit "</code> <code>+ amount)</code>

<code>if</code><code>(amount &gt;</code><code>0</code><code>)</code>

<code>balance.swap(balance.get() + amount)</code>

<code>else</code>

<code>throw</code> <code>new</code> <code>AccountOperationFailedException()</code>

<code>def</code> <code>withdraw(amount</code><code>:</code> <code>Int)</code><code>=</code> <code>{</code>

<code>val</code> <code>currentBalance</code><code>=</code> <code>balance.get()</code>

<code>if</code><code>(amount &gt;</code><code>0</code> <code>&amp;&amp; currentBalance &gt;</code><code>=</code> <code>amount)</code>

<code>balance.swap(currentBalance - amount)</code>

Scala版本的Account是邏輯直接從Java版本翻譯過來的、但代碼風格又帶有Scala和Akka簡潔優雅特征的一種實作。在Scala版本的AccountService中我們也可以看到同樣的優點

<code>object AccountService {</code>

<code>def transfer(from : Account, to : Account, amount : Int) = {</code>

<code>println("Attempting transfer...")</code>

<code>to.deposit(amount)</code>

<code>println("Simulating a delay in transfer...")</code>

<code>Thread.sleep(5000)</code>

<code>println("Uncommitted balance after deposit $" + to.getBalance())</code>

<code>from.withdraw(amount)</code>

<code>def transferAndPrintBalance(</code>

<code>from : Account, to : Account, amount : Int) = {</code>

<code>var result = "Pass"</code>

<code>try {</code>

<code>AccountService.transfer(from, to, amount)</code>

<code>} catch {</code>

<code>case ex =&gt; result = "Fail"</code>

<code>println("Result of transfer is " + result)</code>

<code>println("From account has $" + from.getBalance())</code>

<code>println("To account has $" + to.getBalance())</code>

<code>def main(args : Array[String]) = {</code>

<code>val account1 = new Account(2000)</code>

<code>26</code>

<code>val account2 = new Account(100)</code>

<code>27</code>

<code>actor {</code>

<code>28</code>

<code>Thread.sleep(1000)</code>

<code>29</code>

<code>account2.deposit(20)</code>

<code>30</code>

<code>31</code>

<code>transferAndPrintBalance(account1, account2, 500)</code>

<code>32</code>

<code>println("Making large transfer...")</code>

<code>33</code>

<code>transferAndPrintBalance(account1, account2, 5000)</code>

<code>34</code>

<code>35</code>

與Java版本一樣,Scala版本的AccountService同樣會将事務置于互相沖突的環境之下。是以毫無懸念,其輸出結果也與Java版本完全相同:

<code>118 • Chapter 6. Introduction to Software Transactional Memory</code>

前面我們已經比較過用Java實作的加鎖同步版本和嵌套事務版本(如下所示)的轉賬函數

現在讓我們将之與Scala版本進行一下比較:

<code>def</code> <code>transfer(from</code><code>:</code> <code>Account, to</code><code>:</code> <code>Account, amount</code><code>:</code> <code>Int)</code><code>=</code> <code>{</code>

<code>5</code>

<code>6</code>

從上面的對比中我們可以清晰地看到,Scala版本的代碼除了核心邏輯之外沒有任何備援。這又讓我想起了Alan Perlis的名言:“如果用某種程式設計語言寫代碼時還需要注意一些與核心邏輯無關的東西,那麼這個語言就是低級語言。”

截至目前,我們已經學習了如何用Akka建立事務以及如何組合嵌套事務,但我們才剛上路呢。下面我們将一起了解一下在Akka中如何對事務進行配置。