分佈式事務就是爲了保證不一樣數據庫的數據一致性。 TCChtml
CAP 定理,又被叫做布魯爾定理。對於設計分佈式系統(不只僅是分佈式事務)的架構師來講,CAP 就是你的入門理論。java
C (一致性) A (可用性) P (分區容錯性) web
對於 CP 來講,放棄可用性,追求一致性和分區容錯性,咱們的 ZooKeeper 其實就是追求的強一致。sql
BASE 是 Basically Available(基本可用)、Soft state(軟狀態)和 Eventually consistent (最終一致性)三個短語的縮寫,是對 CAP 中 AP 的一個擴展。數據庫
有這樣一個需求:網絡
小明有兩個帳戶,分別位於A、B兩個數據庫中,小明須要將A中的資金轉到B中。多線程
咱們如何實現?架構
按照下面的方式實現看看有沒有問題。併發
上面操做,正常狀況是沒有問題。負載均衡
第7步執行成功以後,網絡出問題了,第8步會提交失敗,此時的結果是:A庫資金減小了100,B庫資金卻沒有增長;這是一個網絡問題致使了咱們業務失敗了,網絡因素是程序不可控的一些因素,還有其餘的好比運行到7以後,系統忽然斷電了,也會出現一樣的結果。形成了數據錯誤,對業務影響也是比較大的。
分佈式事務能夠這麼理解:一個業務操做中,會包含不少子業務的,每一個子業務都是獨立的事務,咱們須要考慮的是如何保證這些子業務都成功,或者都失敗。
就拿上面的轉帳來講,A庫的資金減小了,因爲網絡問題,操做B庫的connB鏈接斷開了,致使B庫資金沒有增長;網絡問題是能夠恢復了,若是網絡恢復了,系統可以給B中資金加上,這樣最終數據也是正確的;這中間有段時間AB庫的資金是不一致的(A庫減小了100,B庫應該增長100卻沒有增長,數據是不一致的),可是最終某個時間點數據變爲一致了。可以將不一致的時間降到最低是系統須要考慮的問題。
分佈式事務中,咱們能夠接受數據在某個時間段以內不一致,可是數據最終在某個時間點是一致的。
分佈式事務系列中主要講這2種方案,這兩種方案基本上能夠解決大多數常見的分佈式事務的問題,因此我們必須把這兩種方式拿下。
下面咱們介紹一下使用可靠消息如何實現?
對於 TCC 的解釋:
Try 階段:嘗試執行,完成全部業務檢查(一致性),預留必需業務資源(準隔離性)。
Confirm 階段:確認真正執行業務,不做任何業務檢查,只使用 Try 階段預留的業務資源,Confirm 操做知足冪等性。要求具有冪等設計,Confirm 失敗後須要進行重試。
Cancel 階段:取消執行,釋放 Try 階段預留的業務資源,Cancel 操做知足冪等性。Cancel 階段的異常和 Confirm 階段異常處理方案基本上一致。
舉個簡單的例子:若是你用 100 元買了一瓶水, Try 階段:你須要向你的錢包檢查是否夠 100 元並鎖住這 100 元,水也是同樣的。
若是有一個失敗,則進行 Cancel(釋放這 100 元和這一瓶水),若是 Cancel 失敗不論什麼失敗都進行重試 Cancel,因此須要保持冪等。
若是都成功,則進行 Confirm,確認這 100 元被扣,和這一瓶水被賣,若是 Confirm 失敗不管什麼失敗則重試(會依靠活動日誌進行重試)。
對於 TCC 來講適合一些:
強隔離性,嚴格一致性要求的活動業務。
執行時間較短的業務。
對於同一筆業務操做,無論調用多少次,獲得的結果都是同樣的。
select * from t_order where order_id = trade_no for update;
重點在於for update,對for update,作一下說明:
1.當線程A執行for update,數據會對當前記錄加鎖,其餘線程執行到此行代碼的時候,會等待線程A釋放鎖以後,才能夠獲取鎖,繼續後續操做。
2.事物提交時,for update獲取的鎖會自動釋放。
CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較並替換。
CAS機制當中使用了3個基本操做數:內存地址V,舊的預期值A,要修改的新值B。
更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改成B。
1.CPU開銷較大
在併發量比較高的狀況下,若是許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很大的壓力。
2.不能保證代碼塊的原子性
CAS機制所保證的只是一個變量的原子性操做,而不能保證整個代碼塊的原子性。好比須要保證3個變量共同進行原子性的更新,就不得不使用Synchronized了。
3.ABA問題
這是CAS機制最大的問題所在。
什麼是ABA問題?怎麼解決?咱們下一期來詳細介紹。
對於同一筆業務操做,無論調用多少次,獲得的結果都是同樣的。
普通方式:
過程以下:
1.接收到支付寶支付成功請求
2.根據trade_no查詢當前訂單是否處理過
3.若是訂單已處理直接返回,若未處理,繼續向下執行
4.開啓本地事務
5.本地系統給用戶加錢
6.將訂單狀態置爲成功
7.提交本地事務
jvm加鎖方式
方式1中因爲併發出現了問題,此時咱們使用java中的Lock加鎖,來防止併發操做,過程以下:
分析問題:
Lock只能在一個jvm中起效,若是多個請求都被同一套系統處理,上面這種使用Lock的方式是沒有問題的,不過互聯網系統中,多數是採用集羣方式部署系統,同一套代碼後面會部署多套,若是支付寶同時發來多個通知通過負載均衡轉發到不一樣的機器,上面的鎖就不起效了。此時對於多個請求至關於無鎖處理了,又會出現方式1中的結果。此時咱們須要分佈式鎖來作處理。
依靠數據庫中的樂觀鎖來實現。
1.接收到支付寶支付成功請求
2.查詢訂單信息
select * from t_order where order_id = trade_no;
3.判斷訂單是已處理
4.若是訂單已處理直接返回,若未處理,繼續向下執行
5.打開本地事物
6.給本地系統給用戶加錢
7.將訂單狀態置爲成功,注意這塊是重點,僞代碼:
update t_order set status = 1 where order_id = trade_no where status = 0; //上面的update操做會返回影響的行數num if(num==1){ //表示更新成功 提交事務; }else{ //表示更新失敗 回滾事務; }
注意:
update t_order set status = 1 where order_id = trade_no where status = 0; 是依靠樂觀鎖來實現的,status=0做爲條件去更新,相似於java中的cas操做;關於什麼是cas操做,能夠移步:什麼是 CAS 機制?
執行這條sql的時候,若是有多個線程同時到達這條代碼,數據內部會保證update同一條記錄會排隊執行,最終最有一條update會執行成功,其餘未成功的,他們的num爲0,而後根據num來進行提交或者回滾操做。
使用數據庫中悲觀鎖實現。悲觀鎖相似於方式二中的Lock,只不過是依靠數據庫來實現的。數據中悲觀鎖使用for update來實現,過程以下:
1.接收到支付寶支付成功請求
2.打開本地事物
3.查詢訂單信息並加悲觀鎖
select * from t_order where order_id = trade_no for update;
4.判斷訂單是已處理
5.若是訂單已處理直接返回,若未處理,繼續向下執行
6.給本地系統給用戶加錢
7.將訂單狀態置爲成功
8.提交本地事物
重點在於for update,對for update,作一下說明:
1.當線程A執行for update,數據會對當前記錄加鎖,其餘線程執行到此行代碼的時候,會等待線程A釋放鎖以後,才能夠獲取鎖,繼續後續操做。
2.事物提交時,for update獲取的鎖會自動釋放。
方式3能夠正常實現咱們須要的效果,能保證接口的冪等性,不過存在一些缺點:
1.若是業務處理比較耗時,併發狀況下,後面線程會長期處於等待狀態,佔用了不少線程,讓這些線程處於無效等待狀態,咱們的web服務中的線程數量通常都是有限的,若是大量線程因爲獲取for update鎖處於等待狀態,不利於系統併發操做。
依賴數據庫中惟一約束來實現。
咱們能夠建立一個表:
CREATE TABLE `t_uq_dipose` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `ref_type` varchar(32) NOT NULL DEFAULT '' COMMENT '關聯對象類型', `ref_id` varchar(64) NOT NULL DEFAULT '' COMMENT '關聯對象id', PRIMARY KEY (`id`), UNIQUE KEY `uq_1` (`ref_type`,`ref_id`) COMMENT '保證業務惟一性' ) ENGINE=InnoDB;
對於任何一個業務,有一個業務類型(ref_type),業務有一個全局惟一的訂單號,業務來的時候,先查詢t_uq_dipose表中是否存在相關記錄,若不存在,繼續放行。
過程以下:
1.接收到支付寶支付成功請求
2.查詢t_uq_dipose(條件ref_id,ref_type),能夠判斷訂單是否已處理
select * from t_uq_dipose where ref_type = '充值訂單' and ref_id = trade_no;
3.判斷訂單是已處理
4.若是訂單已處理直接返回,若未處理,繼續向下執行
5.打開本地事物
6.給本地系統給用戶加錢
7.將訂單狀態置爲成功
8.向t_uq_dipose插入數據,插入成功,提交本地事務,插入失敗,回滾本地事務,僞代碼:
try{ insert into t_uq_dipose (ref_type,ref_id) values ('充值訂單',trade_no); 提交本地事務: }catch(Exception e){ 回滾本地事務; }
說明:
對於同一個業務,ref_type是同樣的,當併發時,插入數據只會有一條成功,其餘的會違法惟一約束,進入catch邏輯,當前事務會被回滾,最終最有一個操做會成功,從而保證了冪等性操做。
關於這種方式能夠寫成通用的方式,不過業務量大的狀況下,t_uq_dipose插入數據會成爲系統的瓶頸,須要考慮分表操做,解決性能問題。
上面的過程當中向t_uq_dipose插入記錄,最好放在最後執行,緣由:插入操做會鎖表,放在最後能讓鎖表的時間降到最低,提高系統的併發性。
關於消息服務中,消費者如何保證消息處理的冪等性?
每條消息都有一個惟一的消息id,相似於上面業務中的trade_no,使用上面的方式便可實現消息消費的冪等性。
1.實現冪等性常見的方式有:悲觀鎖(for update)、樂觀鎖、惟一約束
2.幾種方式,按照最優排序:樂觀鎖 > 惟一約束 > 悲觀鎖
一個事務在執行的過程當中讀取到了其餘事務尚未提交的數據。
即一個事務操做過程當中能夠讀取到其餘事務已經提交的數據。
一個事務操做中對於一個讀取操做無論多少次,讀取到的結果都是同樣的。
髒讀、不可重複讀、可重複讀、幻讀,其中最難理解的是幻讀
幻讀在可重複讀的模式下才會出現,其餘隔離級別中不會出現
事務A從新執行一個查詢,返回一系列符合查詢條件的行,發現其中插入了被事務B提交的行
http://www.itsoku.com/article/77