閱讀765 返回首頁    go 阿裏雲 go 技術社區[雲棲]


軟件事務內存導論(五)創建嵌套事務

1.1    創建嵌套事務

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

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

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

在Java中使用嵌套事務

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

public class Account {
final private Ref<Integer> balance = new Ref<Integer>();
public Account(int initialBalance) { balance.swap(initialBalance); }
public int getBalance() { return balance.get(); }

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

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

public void deposit(final int amount) {
new Atomic<Boolean>() {
public Boolean atomically() {
System.out.println("Deposit " + amount);
if (amount > 0) {
balance.swap(balance.get() + amount);
return true;
}
throw new AccountOperationFailedException();
}
}.

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

public void withdraw(final int amount) {
new Atomic<Boolean>() {
public Boolean atomically() {
int currentBalance = balance.get();
112 • Chapter 6. Introduction to Software Transactional Memory
if (amount > 0 && currentBalance >= amount) {
balance.swap(currentBalance - amount);
return true;
}
throw new AccountOperationFailedException();
}
}.execute();
}
}

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

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

public class AccountService {
public void transfer(
final Account from, final Account to, final int amount) {
new Atomic<Boolean>() {
public Boolean atomically() {
System.out.println("Attempting transfer...");
to.deposit(amount);
System.out.println("Simulating a delay in transfer...");
try { Thread.sleep(5000); } catch(Exception ex) {}
System.out.println("Uncommitted balance after deposit $" +
to.getBalance());
from.withdraw(amount);
return true;
}
}.execute();
}

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

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

public static void transferAndPrintBalance(
final Account from, final Account to, final int amount) {
boolean result = true;
try {
new AccountService().transfer(from, to, amount);
} catch(AccountOperationFailedException ex) {
result = false;
}
System.out.println("Result of transfer is " + (result ? "Pass" : "Fail"));
System.out.println("From account has $" + from.getBalance());
System.out.println("To account has $" + to.getBalance());
}

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

public static void main(final String[] args) throws Exception {
final Account account1 = new Account(2000);
final Account account2 = new Account(100);
final ExecutorService service = Executors.newSingleThreadExecutor();
service.submit(new Runnable() {
public void run() {
try { Thread.sleep(1000); } catch(Exception ex) {}
account2.deposit(20);
}
});
service.shutdown();
transferAndPrintBalance(account1, account2, 500);
System.out.println("Making large transfer...");
transferAndPrintBalance(account1, account2, 5000);
}
}

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

Attempting transfer...
Deposit 500
Attempting transfer...
Deposit 500
Simulating a delay in transfer...
Deposit 20
Uncommitted balance after deposit $600
Attempting transfer...
Deposit 500
Simulating a delay in transfer...
Uncommitted balance after deposit $620
Result of transfer is Pass
From account has $1500
To account has $620
Making large transfer...
Attempting transfer...
Deposit 5000
Simulating a delay in transfer...
Uncommitted balance after deposit $5620
Result of transfer is Fail
From account has $1500
To account has $620

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

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

本例的最後一個操作是從賬戶1向賬戶2轉$5000。在這個事務中,存款操作順利完成了,但事務能否最終成功還是要看取款操作的結果。不出所料,取款動作由於賬戶餘額不足而失敗並拋了異常。隨後,之前的存款動作被回滾,係統最終保證了賬戶餘額數據不受事務失敗的影響。

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

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

public boolean transfer(
final Account from, final Account to, final int amount)
throws LockException, InterruptedException {
final Account[] accounts = new Account[] {from, to};
Arrays.sort(accounts);
if(accounts[0].monitor.tryLock(1, TimeUnit.SECONDS)) {
try {
if (accounts[1].monitor.tryLock(1, TimeUnit.SECONDS)) {
try {
if(from.withdraw(amount)) {
to.deposit(amount);
return true;
} else {
return false;
}
} finally {
accounts[1].monitor.unlock();
}
}
} finally {
accounts[0].monitor.unlock();
}
}
throw new LockException("Unable to acquire locks on the accounts");
}

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

public void transfer(
final Account from, final Account to, final int amount) {
new Atomic<Boolean>() {
public Boolean atomically() {
to.deposit(amount);
from.withdraw(amount);
return true;
}
}.execute();
}

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

在Scala中使用嵌套事務

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

class Account(val initialBalance : Int) {
val balance = Ref(initialBalance)
def getBalance() = balance.get()
def deposit(amount : Int) = {
atomic {
println("Deposit " + amount)
if(amount > 0)
balance.swap(balance.get() + amount)
else
throw new AccountOperationFailedException()
}
}
def withdraw(amount : Int) = {
atomic {
val currentBalance = balance.get()
if(amount > 0 && currentBalance >= amount)
balance.swap(currentBalance - amount)
else
throw new AccountOperationFailedException()
}
}
}

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

object AccountService {
def transfer(from : Account, to : Account, amount : Int) = {
atomic {
println("Attempting transfer...")
to.deposit(amount)
println("Simulating a delay in transfer...")
Thread.sleep(5000)
println("Uncommitted balance after deposit $" + to.getBalance())
from.withdraw(amount)
}
}
def transferAndPrintBalance(
from : Account, to : Account, amount : Int) = {
var result = "Pass"
try {
AccountService.transfer(from, to, amount)
} catch {
case ex => result = "Fail"
}
println("Result of transfer is " + result)
println("From account has $" + from.getBalance())
println("To account has $" + to.getBalance())
}
def main(args : Array[String]) = {
val account1 = new Account(2000)
val account2 = new Account(100)
actor {
Thread.sleep(1000)
account2.deposit(20)
}
transferAndPrintBalance(account1, account2, 500)
println("Making large transfer...")
transferAndPrintBalance(account1, account2, 5000)
}
}

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

Attempting transfer...
Deposit 500
Attempting transfer...
Deposit 500
Simulating a delay in transfer...
118 • Chapter 6. Introduction to Software Transactional Memory
Deposit 20
Uncommitted balance after deposit $600
Attempting transfer...
Deposit 500
Simulating a delay in transfer...
Uncommitted balance after deposit $620
Result of transfer is Pass
From account has $1500
To account has $620
Making large transfer...
Attempting transfer...
Deposit 5000
Simulating a delay in transfer...
Uncommitted balance after deposit $5620
Result of transfer is Fail
From account has $1500
To account has $620

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

public void transfer(
final Account from, final Account to, final int amount) {
new Atomic<Boolean>() {
public Boolean atomically() {
to.deposit(amount);
from.withdraw(amount);
return true;
}
}.execute();
}

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

def transfer(from : Account, to : Account, amount : Int) = {
atomic {
to.deposit(amount)
from.withdraw(amount)
}
}

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

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


文章轉自 並發編程網-ifeve.com

最後更新:2017-05-22 16:37:20

  上一篇:go  通過JVM日誌來進行安全點分析
  下一篇:go  為何從10開始到99連續相乘會得到0?