用消息隊列和消息應用狀態表來消除分佈式事務

由於數據量的巨大,大部分Web應用都需要部署很多個數據庫實例。這樣,有些用戶操作就可能需要去修改多個數據庫實例中的數據。傳統的解決方法是使用分佈式事務保證數據的全局一致性,經典的方法是使用兩階段提交協議。

長期以來,分佈式事務提供的優雅的全局ACID保證麻醉了應用開發者的心靈,很多人都不敢越雷池一步,想像沒有分佈式事務的世界會是怎樣。如今就如MySQL和PostgreSQL這類面向低端用戶的開源數據庫都支持分佈式事務了,開發者更是沉醉其中,不去考慮分佈式事務是否給系統帶來了傷害。

事實上,有所得必有所失,分佈式事務提供的ACID保證是以損害系統的可用性、性能與可伸縮性爲代價的。只有在參與分佈式事務的各個數據庫實例都能夠正常工作的前提下,分佈式事務才能夠順利完成,只要有一個工作不正常,整個事務就不能完成。這樣,系統的可用性就相當於參加分佈式事務的各實例的可用性之積,實例越多,可用性下降越明顯。從性能和可伸縮性角度看,首先是事務的總持續時間通常是各實例操作時間之和,因爲一個事務中的各個操作通常是順序執行的,這樣事務的響應時間就會增加很多;其次是一般Web應用的事務都不大,單機操作時間也就幾毫秒甚至不到1毫秒,一但涉及到分佈式事務,提交時節點間的網絡通信往返過程也爲毫秒級別,對事務響應時間的影響也不可忽視。由於事務持續時間延長,事務對相關資源的鎖定時間也相應增加,從而可能嚴重增加了併發衝突,影響到系統吞吐率和可伸縮性。

正是由於分佈式事務有以上問題,eBay在設計上就不採用分佈式事務,而是通過其它途徑來解決數據一致性問題。其中使用的最重要的技術就是消息隊列和消息應用狀態表。

舉個例子。假設系統中有以下兩個表
user(id, name, amt_sold, amt_bought)
transaction(xid, seller_id, buyer_id, amount)
其中user表記錄用戶交易彙總信息,transaction表記錄每個交易的詳細信息。

這樣,在進行一筆交易時,若使用事務,就需要對數據庫進行以下操作:
begin;
INSERT INTO transaction VALUES(xid, $seller_id, $buyer_id, $amount);
UPDATE user SET amt_sold = amt_sold + $amount WHERE id = $seller_id;
UPDATE user SET amt_bought = amt_bought + $amount WHERE id = $buyer_id;
commit;
即在transaction表中記錄交易信息,然後更新賣家和買家的狀態。

假設transaction表和user表存儲在不同的節點上,那麼上述事務就是一個分佈式事務。要消除這一分佈式事務,將它拆分成兩個子事務,一個更新transaction表,一個更新user表是不行的,因爲有可能transaction表更新成功後,更新user失敗,系統將不能恢復到一致狀態。

解決方案是使用消息隊列。如下所示,先啓動一個事務,更新transaction表後,並不直接去更新user表,而是將要對user表進行的更新插入到消息隊列中。另外有一個異步任務輪詢隊列內容進行處理。
begin;
INSERT INTO transaction VALUES(xid, $seller_id, $buyer_id, $amount);
put_to_queue “update user(“seller”, $seller_id, amount);
put_to_queue “update user(“buyer”, $buyer_id, amount);
commit;
for each message in queue
begin;
dequeue message;
if message.type = “seller” then
UPDATE user SET amt_sold = amt_sold + message.amount WHERE id = message.user_id;
else
UPDATE user SET amt_bought = amt_bought + message.amount WHERE id = message.user_id;
end
commit;
end

上述解決方案看似完美,實際上還沒有解決分佈式問題。爲了使第一個事務不涉及分佈式操作,消息隊列必須與transaction表使用同一套存儲資源,但爲了使第二個事務是本地的,消息隊列存儲又必須與user表在一起。這兩者是不可能同時滿足的。

如果消息具有操作冪等性,也就是一個消息被應用多次與應用一次產生的效果是一樣的話,上述問題是很好解決的,只要將消息隊列放到transaction表一起,然後在第二個事務中,先應用消息,再從消息隊列中刪除。由於消息隊列存儲與user表不在一起,應用消息後,可能還沒來得及將應用過的消息從隊列中刪除時系統就出故障了。這時系統恢復後會重新應用一次這一消息,由於冪等性,應用多次也能產生正確的結果。

但實際情況下,消息很難具有冪等性,比如上述的UPDATE操作,執行一次和執行多次的結束顯然是不一樣的。解決這一問題的方法是使用另一個表記錄已經被成功應用的消息,並且這個表使用與user表相同的存儲。假設增加以下表 message_applied(msg_id)記錄被成功應用的消息,則產生最終的解決方案如下:
begin;
INSERT INTO transaction VALUES(xid, $seller_id, $buyer_id, $amount);
put_to_queue “update user(“seller”, $seller_id, amount);
put_to_queue “update user(“buyer”, $buyer_id, amount);
commit;
for each message in queue
begin;
SELECT count(*) as cnt FROM message_applied WHERE msg_id = message.id;
if cnt = 0 then
if message.type = “seller” then
UPDATE user SET amt_sold = amt_sold + message.amount WHERE id = message.user_id;
else
UPDATE user SET amt_bought = amt_bought + message.amount WHERE id = message.user_id;
end
INSERT INTO message_applied VALUES(message.id);
end
commit;
if 上述事務成功
dequeue message
DELETE FROM message_applied WHERE msg_id = message.id;
end
end

我們來仔細分析一下:
1、消息隊列與transaction使用同一實例,因此第一個事務不涉及分佈式操作;
2、message_applied與user表在同一個實例中,也能保證一致性;
3、第二個事務結束後,dequeue message之前系統可能出故障,出故障後系統會重新從消息隊列中取出這一消息,但通過message_applied表可以檢查出來這一消息已經被應用過,跳過這一消息實現正確的行爲;
4、最後將已經成功應用,且已經從消息隊列中刪除的消息從message_applied表中刪除,可以將message_applied表保證在很小的狀態(不清除也是可以的,不影響系統正確性)。由於消息隊列與message_applied在不同實例上,dequeue message之後,將對應message_applied記錄刪除之前可能出故障。一但這時出現故障,message_applied表中會留下一些垃圾內容,但不影響系統正確性,另外這些垃圾內容也是可以正確清理的。

雖然由於沒有分佈式事務的強一致性保證,使用上述方案在系統發生故障時,系統將短時間內處於不一致狀態。但基於消息隊列和消息應用狀態表,最終可以將系統恢復到一致。使用消息隊列方案,解除了兩個數據庫實例之間的緊密耦合,其性能和可伸縮性是分佈式事務不可比擬的。

當然,使用分佈式事務有助於簡化應用開發,使用消息隊列明顯需要更多的工作量,兩者各有優缺點。個人觀點是,對於時間緊迫或者對性能要求不高的系統,應採用分佈式事務加快開發效率,對於時間需求不是很緊,對性能要求很高的系統,應考慮使用消息隊列方案。對於原使用分佈式事務,且系統已趨於穩定,性能要求高的系統,則可以使用消息隊列方案進行重構來優化性能。

注: 本文取材於eBay的工程師Dan Pritchet寫的這篇文章 ,並轉載至http://wangyuanzju.blog.163.com/blog/static/1302920086424341932

轉載於淘寶核心團隊blog: http://rdc.taobao.com/blog/cs/?p=671

ps:好記性不如爛筆頭,在此mark供以後查看。

發佈了86 篇原創文章 · 獲贊 23 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章