首先看看一個問題:
用戶甲的操作 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); } }
上面這段代碼業務邏輯是想實現lower<upper:
1. lower和upper的初始值是(0, 5), 2.一個客戶端請求線程A: setLower(4) 一個客戶端請求線程B: setUpper(3) 3. lower和upper是 (4, 3)
這個結果破壞了lower<upper這個邏輯一致性,所以,用鎖並不能保證邏輯一致性,而且還帶來了堵塞。鎖用錯了地方,不但沒有得到想要的,而且還失去更多。
從歷史上看,鎖的問題如鬼魂一直伴隨着我們:
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火車票系統是這類問題的典型體現。
我們可以將目前企業軟件分為兩大類:license軟件和雲計算SaaS。
賣license的軟件要過渡到Saas幾乎不可能,需要重新編寫,因為過去 企業軟件架構存在致命問題:高並發事務處理。
賣license的軟件一般是SOA架構,我認為由於SOA太強調服務,而服務是一種機器處理,如果在數據寫方向使用SOA,這是一種數據喂機器的方式,必然會遭遇高並發事務和死鎖問題。在這個寫方向使用REST要比SOA更合適一些,因為REST側重資源與狀態,與DDD強調的實體與行為是一脈相承的。
函數式編程前段時間很火,但是純FP的語言逐漸被對象和函數混合式的語言如Scala趕超,關鍵還是可變的狀態是現實中無法回避的問題,可變的使用對象式的Actror是最合適的,而不變性使用函數這種機器處理方式也是最合適的。可變性對應寫操作,不變性對應讀操作,如大數據處理,報表BI等。
有人說,為什么大多數互聯網軟件都是數據喂機器,或函數式即可,比如twitter或facebook,因為他們沒有事務要求,一般涉及到錢等重要交易都需要事務,而發個言,寫段話等等都是非高事務要求,也可以這么說,非結構化的數據一般都沒有事務要求,結構化聚合的數據才有事務要求。
