為什麼Actor模型是高併發事務的終極解決方案?

banq發表於2013-09-12
首先看看道友提出的一個問題:
使用者甲的操作
1.開始事務
2.訪問表A
3.訪問表B
4.提交事務
乙使用者在操作
1.開始事務
2.訪問表B
3.訪問表A
4.提交事務

如果甲使用者和乙使用者的兩個事務同時發生,甲事務鎖住了表A未釋放(因為整個事務未完成),正在準備訪問B表,而乙事務鎖住了表B未釋放(因為整個事務未完成),正在準備訪問A表,可是A表被甲事務鎖住了,等甲事務釋放,而甲事務真正等待乙事務釋放B表,陷入了無限等待,也就是死鎖Dead Lock。

也有道友使用多執行緒來模擬儲存過程:http://www.jdon.com/45727,每個執行緒裡開啟一個事務,類似上述問題也會出現死鎖。

問題出在哪裡?

是我們的思路方向出現問題:

其實無論是使用資料庫鎖 還是多執行緒,這裡有一個共同思路,就是將資料餵給執行緒,就如同計算機是一套加工流水線,資料作為原材料投入這個流水線的開始,流水線出來後就是成品,這套模式的前提是資料是被動的,自身不復雜,沒有自身業務邏輯要求。適合大資料處理或網際網路網站應用等等。

但是如果資料自身要求有嚴格的一致性,也就是事務機制,資料就不能被動被加工,要讓資料自己有行為能力保護實現自己的一致性,就像孩子小的時候可以任由爸媽怎麼照顧關心都可以,但是如果孩子長大有自己的思想和要求,他就可能不喜歡被爸媽照顧,他要求自己透過行動實現自己的要求。

資料也是如此。

只有我們改變思路,讓資料自己有行為維護自己的一致性,才能真正安全實現真正的事務。

資料+行為=物件,有人問了,物件不是也要被執行緒呼叫嗎?

例如下述程式碼,因為物件的行為要被執行緒呼叫,我們要使用同步鎖synchronized :

public class A { 
        private volatile int lower, upper; //兩個狀態值
        public int getLower() { return lower; } 
        public int getUpper() { return upper; }
        public synchronized void setAUpper(int value){
             if (value < a.getUpper()) 
                    a.setLower(value);
        }

        public asynchronization void setALower(int value){
            if (value > a.getLower()) 
                   a.setUpper(value);
         }
   }
<p class="indent">


上面這段程式碼業務邏輯是想實現lower<upper:

1. lower和upper的初始值是(0, 5),
2.一個客戶端請求執行緒A: setLower(4)
一個客戶端請求執行緒B: setUpper(3)
3. lower和upper是 (4, 3)

這個結果破壞了lower<upper這個邏輯一致性,所以,用鎖並不能保證邏輯一致性,而且還帶來了堵塞。鎖用錯了地方,不但沒有得到想要的,而且還失去更多。

下圖展示了鎖帶來堵塞,每個時刻只能允許一個執行緒工作,如同只能允許一個人蹲馬桶一樣。

[img index=1]

從歷史上看,鎖的問題如鬼魂一直伴隨著我們:
1.用資料表一個欄位來表示狀態,比如1表示已付款未發貨,2表示已付款已發貨,然後使用者來一個請求用SQL或儲存過程修改,這時使用的資料庫鎖。

2.用ORM實現,比如Hibernate JPA來修改狀態,雖然不用SQL了,但是Hibernate的悲觀鎖和樂觀鎖也讓人抓狂。

3.徹底拋棄資料庫,直接在記憶體快取中進行修改,使用Java的同步鎖,效能還是不夠,吞吐量上不去。如上圖提示,只能一個廁所蹲位一個人用,其他人必須排隊。

4.Actor模型。

Actor模型原理
Actor模型=資料+行為+訊息。

Actor模型內部的狀態由自己的行為維護,外部執行緒不能直接呼叫物件的行為,必須透過訊息才能激發行為,這樣就保證Actor內部資料只有被自己修改。

Actor模型如何實現?

Scala或ErLang的程式信箱都是一種Actor模型,也有Java的專門的Actor模型,這裡是幾種Actor模型比較

明白了Actor模型原理,使用Disruptor這樣無鎖佇列也可以自己實現Actor模型,讓一個普通物件與外界的互動呼叫透過Disruptor訊息佇列實現,比如LMAX架構就是這樣實現高頻交易,從2009年成功執行至今,被Martin Fowler推崇。

回到本帖最初問題,如何使用Actor模型解決高併發事務呢?

轉賬是典型的符合該問題的案例,轉賬是將A帳號到B帳號轉賬,使用Actor模型解決如下:

發出是否可轉出訊息--->訊息佇列--->A

A作為一個物件,注意不是資料表,物件是有行為的,檢查自己餘額是否可轉賬,如果可以,凍結這部分金額,比如轉賬100元,凍結100元,從餘額中扣除。因為外部命令是透過訊息順序進來的,所以下一個訊息如果也是扣除,再次檢查餘額是否足夠......

具體詳細流程可見:REST和DDD

那麼,既然Actor模型如此巧妙,而解決方向與我們習慣的資料喂機器的方式如此不同,那麼如何在實戰中能明顯發現某個資料修改應該用Actor模型解決呢?因為我們習慣將資料喂機器的思路啊?

使用DDD領域驅動設計或CQRS架構就能明顯發現這些特殊情況,CQRS是讀寫分離,其中寫操作是應領域專家要求編寫的功能,在這類方向,我們都有必要使用Actor模型實現,因為在這個方向上,領域專家的要求都表達為聚合根實體,聚合根就是用Actor模型實現最合適不過了。而讀方向,比如大資料處理,報表查詢,OLTP等等都是資料喂機器的方式。

有的道友會疑問,我們經常使用SSH,也就是Spring + Hibernate架構,這個預設是哪種方向呢?很顯然,預設是資料喂機器的方向,所以在實現寫操作時,特別警惕高併發發生死鎖等影響效能問題,當然也包括EJB架構。

有一種togaf架構,將企業軟體架構分為資料架構和應用架構等,實際是EJB或SSH的變相描述,這種架構的問題我們已經一目瞭然了,特別這樣的系統如果從面向內部管理轉向到SaaS模型時,這類高併發死鎖問題就特別容易發生,幾乎不具備可用性。前期12306火車票系統是這類問題的典型體現。

[該貼被banq於2013-09-12 08:09修改過]

[該貼被admin於2013-09-15 07:13修改過]

[該貼被admin於2013-09-15 07:29修改過]

[該貼被admin於2013-09-15 07:30修改過]

相關文章