淺談分佈式事務

淺談分佈式事務

前言應用場景

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

某些場地比如在電商系統中,當有用戶下單後,除了在訂單表插入一條記錄外,對應商品表的這個商品數量必須減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收支平衡呢?有人說這個很簡單嘛,可以用事務解決。

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;">;
update B set amount</span>=amount+10000 where userId=1<span style="color: #000000;">;
End transaction
commit;</span>

非常正確!如果你使用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;
發佈了23 篇原創文章 · 獲贊 8 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章