文章出處

 

前言應用場景

事務必須滿足傳統事務的特性,即原子性,一致性,分離性和持久性。但是分布式事務處理過程中,

某些場地比如在電商系統中,當有用戶下單后,除了在訂單表插入一條記錄外,對應商品表的這個商品數量必須減1吧,怎么保證?

在搜索廣告系統中,當用戶點擊某廣告后,除了在點擊事件表中增加一條記錄外,
還得去商家賬戶表中找到這個商家并扣除廣告費吧,怎么保證?

一 本地事務
以用戶A轉賬用戶B為例,假設有

  用戶A賬戶表:A(id,userId,amount)  

  用戶B賬戶表:B(id,userId,amount)

  用戶的userId=1;

從用戶A轉賬1萬塊錢到用戶B的動作分為兩步:

  1)用戶A表扣除1萬:update A set amount=amount-10000 where userId=1;

  2)用戶B表增加1萬:update B set amount=amount+10000 where userId=1;

  如何確保用戶A用戶B收支平衡呢?有人說這個很簡單嘛,可以用事務解決。

Begin transaction
update A set amount=amount-10000 where userId=1
update B set amount=amount+10000 where userId=1
End transaction
commit;

 

 
非常正確!如果你使用spring的話一個注解就能搞定上述事務功能。
1
2
3
4
5
@Transactional(rollbackFor=Exception.class)
public void update() {
updateATable(); //更新A表
updateBTable(); //更新B表
}

 如果系統規模較小,數據表都在一個數據庫實例上,上述本地事務方式可以很好地運行,但是如果系統規模較大,
比如用戶A賬戶表和用戶B賬戶表顯然不會在同一個數據庫實例上,他們往往分布在不同的物理節點上,這時本地事務已經失去用武之地。

既然本地事務失效,分布式事務自然就登上舞臺。
二 XA

XA是由X/Open組織提出的分布式事務的規范。XA規范主要 定義了(全局)事務管理器(Transaction Manager)和(局部)資源管理器(Resource Manager)之間的接口。
XA接口是雙向的系統接口,在事務管理器(Transaction Manager)以及一個或多個資源管理器(Resource Manager)之間形成通信橋梁。
XA之所以需要引入事務管理器是因為,在分布式系統中,從理論上講(參考Fischer等的論文),兩臺機器理論上無 法達到一致的狀態,需要引入一個單點進行協調。
事務管理器控制著全局事務,管理事務生命周期,并協調資源。資源管理器負責控制和管理實際資源(如數據庫或 JMS隊列)。
下圖說明了事務管理器、資源管理器,與應用程序之間的關系:

三 兩階段提交協議

分布式事務必須滿足傳統事務的特性,即原子性,一致性,分離性和持久性。但是分布式事務處理過程中,某些場地(Server)可能發生故障,
或 者由于網絡發生故障而無法訪問到某些場地。為了防止分布式系統部分失敗時產生數據的不一致性。
在分布式事務的控制中采用了兩階段提交協議(Two- Phase Commit Protocol)。即事務的提交分為兩個階段:

  預提交階段(Pre-Commit Phase)
  決策后階段(Post-Decision Phase)

  兩階段提交用來協調參與一個更新中的多個服務器的活動,以防止分布式系統部分失敗時產生數據的不一致性。例如,如果一個更新操作要求位于三個不同結點上的記錄被改變,且其中只要有一個結點失敗,另外兩個結點必須檢測到這個失敗并取消它們所做的改變。

  為了支持兩階段提交,一個分布式更新事務中涉及到的服務器必須能夠相互通信。一般來說一個服務器會被指定為"控制"或"提交"服務器并監控來自其它服務器的信息。

   在分布式更新期間,各服務器首先標志它們已經完成(但未提交)指定給它們的分布式事務的那一部分,并準備提交(以使它們的更新部分成為永久性的)。這是 兩階段提交的第一階段。如果有一結點不能響應,那么控制服務器要指示其它結點撤消分布式事務的各個部分的影響。如果所有結點都回答準備好提交,控制服務器 則指示它們提交并等待它們的響應。等待確認信息階段是第二階段。
在接收到可以提交指示后,每個服務器提交分布式事務中屬于自己的那一部分,并給控制服務器 發回提交完成信息。

  在一個分布式事務中,必須有一個場地的Server作為協調者(coordinator),它能向 其它場地的Server發出請求,并對它們的回答作出響應,由它來控制一個分布式事務的提交或撤消。該分布式事務中涉及到的其它場地的Server稱為參 與者(Participant)。

事務兩階段提交的過程如下:
  ● 兩階段提交在應用程序向協調者發出一個提交命令時被啟動。這時提交進入第一階段,即預提交階段。在這一階段中:
  (1) 協調者準備局部(即在本地)提交并在日志中寫入"預提交"日志項,并包含有該事務的所有參與者的名字。
  (2) 協調者詢問參與者能否提交該事務。一個參與者可能由于多種原因不能提交。例如,該Server提供的約束條件(Constraints)的延遲檢查不符合 限制條件時,不能提交;參與者本身的Server進程或硬件發生故障,不能提交;或者協調者訪問不到某參與者(網絡故障),這時協調者都認為是收到了一個 否定的回答。
  (3) 如果參與者能夠提交,則在其本身的日志中寫入"準備提交"日志項,該日志項立即寫入硬盤,然后給協調者發回,已準備好提交"的回答。
  (4) 協調者等待所有參與者的回答,如果有參與者發回否定的回答,則協調者撤消該事務并給所有參與者發出一個"撤消該事務"的消息,結束該分布式事務,撤消該事務的所有影響。

  ● 如果所有的參與者都送回"已準備好提交"的消息,則該事務的提交進入第二階段,即決策后提交階段。在這一階段中:
  (1) 協調者在日志中寫入"提交"日志項,并立即寫入硬盤。
  (2) 協調者向參與者發出"提交該事務"的命令。各參與者接到該命令后,在各自的日志中寫入"提交"日志項,并立即寫入硬盤。然后送回"已提交"的消息,釋放該事務占用的資源。 
  (3) 當所有的參與者都送回"已提交"的消息后,協調者在日志中寫入"事務提交完成"日志項,釋放協調者占用的資源 。這樣,完成了該分布式事務的提交。

     現如今實現基于兩階段提交的分布式事務也沒那么困難了,如果使用java,那么可以使用開源軟件atomikos來快速實現。

     缺點

  不過但凡使用過的上述兩階段提交的同學都可以發現性能實在是太差,根本不適合高并發的系統。為什么?

  1)兩階段提交涉及多次節點間的網絡通信,通信時間太長!

  2)事務時間相對于變長了,鎖定的資源的時間也變長了,造成資源等待時間也增加好多。

四 使用消息隊列來避免分布式事務
  如果仔細觀察生活的話,生活的很多場景已經給了我們提示。
  比如在北京很有名的姚記炒肝點了炒肝并付了錢后,他們并不會直接把你點的炒肝給你,往往是給你一張小票,然后讓你拿著小票到出貨區排隊去取。
為什么他們要將付錢和取貨兩個動作分開呢?原因很多,其中一個很重要的原因是為了使他們接待能力增強(并發量更高)。

還是回到我們的問題,只要這張小票在,你最終是能拿到炒肝的。同理轉賬服務也是如此,當用戶A賬戶扣除1萬后,
我們只要生成一個憑證(消息)即可,這個憑證(消息)上寫著“讓用戶B賬戶增加 1萬”,只要這個憑證(消息)能可靠保存,
我們最終是可以拿著這個憑證(消息)讓用戶B賬戶增加1萬的,即我們能依靠這個憑證(消息)完成最終一致性。

4.1 如何可靠保存憑證(消息)

  有兩種方法:

4.1.1 業務與消息耦合的方式

  用戶A在完成扣款的同時,同時記錄消息數據,這個消息數據與業務數據保存在同一數據庫實例里(消息記錄表表名為message);

1
2
3
4
5
<span style="color: #000000;">Begin transaction
update A set amount</span>=amount-10000 where userId=1<span style="color: #000000;">;
insert into message(userId, amount,status) values(</span>1, 10000, 1<span style="color: #000000;">);
End transaction
commit;</span>

  上述事務能保證只要用戶A賬戶里被扣了錢,消息一定能保存下來。

  當上述事務提交成功后,我們通過實時消息服務將此消息通知用戶B,用戶B處理成功后發送回復成功消息,用戶A收到回復后刪除該條消息數據。

4.1.2 業務與消息解耦方式

  上述保存消息的方式使得消息數據和業務數據緊耦合在一起,從架構上看不夠優雅,而且容易誘發其他問題。為了解耦,可以采用以下方式。

  1)用戶A在扣款事務提交之前,向實時消息服務請求發送消息,實時消息服務只記錄消息數據,而不真正發送,只有消息發送成功后才會提交事務;

  2)當用戶A扣款事務被提交成功后,向實時消息服務確認發送。只有在得到確認發送指令后,實時消息服務才真正發送該消息;

  3)當用戶A扣款事務提交失敗回滾后,向實時消息服務取消發送。在得到取消發送指令后,該消息將不會被發送;

  4)對于那些未確認的消息或者取消的消息,需要有一個消息狀態確認系統定時去用戶A系統查詢這個消息的狀態并進行更新。為什么需要這一步驟,
舉個例子:假設在第2步用戶A扣款事務被成功提交后,系統掛了,此時消息狀態并未被更新為“確認發送”,從而導致消息不能被發送。

  優點:消息數據獨立存儲,降低業務系統與消息系統間的耦合;

  缺點:一次消息發送需要兩次請求;業務處理服務需要實現消息狀態回查接口。

4.2 如何解決消息重復投遞的問題

  還有一個很嚴重的問題就是消息重復投遞,以我們用戶A轉賬到用戶B為例,如果相同的消息被重復投遞兩次,那么我們用戶B賬戶將會增加2萬而不是1萬了。

  為什么相同的消息會被重復投遞?比如用戶B處理完消息msg后,發送了處理成功的消息給用戶A,正常情況下用戶A應該要刪除消息msg,但如果用戶A這時候悲劇的掛了,
重啟后一看消息msg還在,就會繼續發送消息msg。

  解決方法很簡單,在用戶B這邊增加消息應用狀態表(message_apply),通俗來說就是個賬本,用于記錄消息的消費情況,每次來一個消息,
在真正執行之前,先去消息應用狀態表中查詢一遍,如果找到說明是重復消息,丟棄即可,如果沒找到才執行,同時插入到消息應用狀態表(同一事務)。

1
2
3
4
5
6
7
8
for each msg in queue
Begin transaction
select count(*) as cnt from message_apply where msg_id=msg.msg_id;
if cnt==0 then
update B set amount=amount+10000 where userId=1;
insert into message_apply(msg_id) values(msg.msg_id);
End transaction
commit;

文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()