掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
作者:IT技術(shù)分享 2019-07-30 07:26:26
新聞
前端
分布式 在項目開發(fā)中,經(jīng)常會需要處理分布式事務。例如數(shù)據(jù)庫分庫分表之后,原來在一個單庫上的操作可能會跨越多個數(shù)據(jù)庫。

創(chuàng)新互聯(lián)于2013年創(chuàng)立,先為南關(guān)等服務建站,南關(guān)等地企業(yè),進行企業(yè)商務咨詢服務。為南關(guān)企業(yè)網(wǎng)站制作PC+手機+微官網(wǎng)三網(wǎng)同步一站式服務解決您的所有建站問題。
在項目開發(fā)中,經(jīng)常會需要處理分布式事務。例如數(shù)據(jù)庫分庫分表之后,原來在一個單庫上的操作可能會跨越多個數(shù)據(jù)庫。系統(tǒng)服務化拆分之后,原來的在一個系統(tǒng)上的操作可能會跨越多個系統(tǒng)。就連我們平時經(jīng)常使用到的緩存(如redis、memcache等)也可能涉及分布式事務,因為緩存和數(shù)據(jù)庫是兩個不同的實體,如何保證數(shù)據(jù)在緩存和數(shù)據(jù)庫間的一致性也是要重點考慮的。分布式事務就是指事務要處理的資源分別位于分布式系統(tǒng)中的不同節(jié)點之上的事務。
對于單機系統(tǒng),通常我們借助數(shù)據(jù)庫實現(xiàn)本地事務,例如下面JDBC代碼實現(xiàn)了一個事務:
- Connection con = datasource.getConnection();
- con.setAutoCommit(false);
- ...
- 執(zhí)行CRUD操作,可能會涉及到多個表
- ...
- con.commit()/con.rollback()
由于在分布式系統(tǒng)中,多個系統(tǒng)無法共用同一個數(shù)據(jù)庫鏈接,所以無法簡單借用上面的處理方式實現(xiàn)分布式事務。
下面將介紹幾種本人在實際開發(fā)中使用過的處理分布式事務的方式,最后再引出分布式事務的相關(guān)理論并進行總結(jié)。
避免出現(xiàn)分布式事務
由于分布式事務比較難于處理,所以應該盡量避免分布式事務的發(fā)生。例如對于一個客戶信息系統(tǒng),由于注冊用戶數(shù)太多導致存儲的數(shù)據(jù)量過大,所以對其進行分庫分表存儲。而客戶信息模型又分為多個子模型,對應數(shù)據(jù)庫中的多個表,例如客戶基本信息表、客戶登錄賬號表、客戶登錄密碼表、客戶聯(lián)系方式表等等。假設登錄賬號表和客戶基本信息表的關(guān)聯(lián)關(guān)系如下所示:
user_id和login_id分別是兩個表的主鍵,user_id還作為login_info表的外鍵使兩個表關(guān)聯(lián)。在用戶注冊時會自動生成user_id和login_id的值。 user_info和login_info兩個表分別采用user_id和login_id計算分庫分表規(guī)則 。假設我們對每個模型分十庫一百表存儲,即存在user_info_00 ~ user_info_99一百個表,其中user_info_00 ~ user_info_09屬于第一個庫,user_info_10 ~ user_info_19屬于第二個庫,依次類推。
在分庫分表之后,如果我們不仔細的考慮user_id和login_id的生成規(guī)則(例如隨意生成一個數(shù)字字符串或簡單使用遞增sequence),就可能導致同一個用戶的user_info信息和login_info信息被存儲到兩個不同的庫,這就會導致分布式事務發(fā)生。
面對這種問題,最好的解決思路就是考慮如何避免分布式事務的發(fā)生。只要想辦法讓跟一個用戶相關(guān)的所有模型數(shù)據(jù)全部存入到一個庫中,就可以避免分布式事務了。由于每個模型數(shù)據(jù)的分庫分表路由規(guī)則又是由各個表的主鍵id決定的(例如user_id、login_id),所以只要對各個表的主鍵生成規(guī)則進行定制,就可以保證一個用戶的所有模型數(shù)據(jù)全部存到同一個庫。假設有下面的id生成規(guī)則:
根據(jù)這個思想,我們可以在用戶注冊的時候先生成user_id,user_id的分庫分表位可以隨機生成。然后在為其它模型生成主鍵id時(例如login_id),必須讓這個模型的主鍵id的分庫分表位與user_id的分庫分表位相同。另外一點也要注意,一個表的查詢條件不一定只有主鍵id一個,如果有其它查詢條件列,那就要保證那一列的生成規(guī)則也要包含相同的分庫分表位,否則就不能使用該列進行查詢。
通過這種方式,就可以保證一個用戶的所有模型數(shù)據(jù)全部存儲到同一個庫中,有效的避免分布式事務的發(fā)生。
事務補償
通常情況下,應對高并發(fā)的一個主要手段就是增加分布式緩存(如redis)以提高查詢性能。增加分布式緩存后系統(tǒng)查詢數(shù)據(jù)的流程如下圖:
即先嘗試從緩存中查詢數(shù)據(jù),如果緩存命中就直接返回結(jié)果,否則嘗試從DB中查詢數(shù)據(jù)。如果查詢DB命中則將數(shù)據(jù)補充到緩存,以備下次查詢時可以命中緩存。
而在更新數(shù)據(jù)時,通常是先更新DB中的數(shù)據(jù),DB寫入成功后再更新緩存中的數(shù)據(jù)。那么就有一個問題, 如何保證緩存和DB間數(shù)據(jù)的一致性? 由于緩存和DB是兩個不同的實體,寫入DB成功后再去更新緩存,如果緩存更新失敗(例如網(wǎng)絡抖動造成短暫的緩存不可用)就會造成緩存和DB的不一致。此時按照上圖的查詢邏輯,先查緩存就會查詢到“臟”的數(shù)據(jù),就會嚴重影響業(yè)務。這也是一個典型的分布式事務問題——緩存和DB要嘛同時更新成功,要嘛同時更新失敗。解決這個問題的一個較好方式就是事務補償。
我們可以在DB中創(chuàng)建一張事務補償表transaction_log,transaction_log表可以和業(yè)務數(shù)據(jù)在一個庫中,也可以在不同的庫。在更新數(shù)據(jù)前,先將要更新的模型數(shù)據(jù)記錄到transaction_log中。例如我們更新user_info表中的數(shù)據(jù),就將userId記錄到transaction_log中。
transaction_log記錄成功后,再去更新業(yè)務數(shù)據(jù)表user_info中的內(nèi)容,最后更新緩存中的userInfo數(shù)據(jù)。緩存更新成功后,就可以刪除transaction_log表中對應的記錄。
假設在更新完user_info表之后,由于網(wǎng)絡抖動等原因?qū)е戮彺娓率?,則transaction_log表中對應的記錄就會一直存在,表示這個事務沒有完成的一種記錄。
應用會創(chuàng)建一個定時任務,周期性的掃描transaction_log表中的記錄(例如每隔2S掃描一次)。發(fā)現(xiàn)有符合條件的記錄,就嘗試執(zhí)行補償邏輯。例如更新用戶信息時,DB中的user_info表更新成功,但緩存更新失敗,定時任務發(fā)現(xiàn)transaction_log表中對應的記錄沒有刪除且已經(jīng)超過正常等待時間,就嘗試使緩存和DB一致(可以刪除緩存中對應的數(shù)據(jù),也可以根據(jù)userId重新查詢DB再補充的緩存)。補償任務執(zhí)行完成后,就可以刪除transaction_log表中對應的記錄。如果補償任務執(zhí)行再次失敗,就保留transaction_log表中的記錄,等待下個周期再次執(zhí)行。
事務補償這種方式保證的是事務的最終一致性,即如果發(fā)生意外,會存在一個時間窗口(例如2S),在這個窗口內(nèi)DB和緩存間是不一致的,但能保證最終兩者的數(shù)據(jù)是一致的。至于定時任務周期的設定,要結(jié)合業(yè)務對“臟”數(shù)據(jù)的敏感程度以及系統(tǒng)的負載。
事務型消息
對于一個金融系統(tǒng),假設有一個需求是用戶注冊成功后自動為用戶創(chuàng)建一個賬戶??蛻舻男畔⒕S護在客戶中心系統(tǒng),客戶的賬戶信息維護的賬務中心系統(tǒng),如果用戶注冊成功,必須保證客戶的賬戶在賬務系統(tǒng)創(chuàng)建成功。這顯然也是一個分布式事務問題。
處理這個問題,顯然也可以采用上一小節(jié)介紹的事務補償機制來處理。但注冊和開戶并不要求一定是同步完成,且需要感知用戶注冊成功事件的系統(tǒng)并不只有賬務系統(tǒng)一個(例如營銷系統(tǒng)可能也需要感知用戶注冊成功的事件,給用戶發(fā)優(yōu)惠券),所以使用消息機制異步通知更加合適。那么問題就變成了“如果用戶注冊成功,一定要保證消息發(fā)送成功”。
應對這種場景,可以使用事務型消息。但前提條件是使用的MQ中間件必須支持事務型消息,比如阿里的RocketMQ。目前市面上其它一些主流的MQ中間件都不支持事務型消息,比如Kafka和RabbitMQ都不支持。
下面的序列圖是事務型消息的執(zhí)行流程:
細心的小伙伴會發(fā)現(xiàn),如果在上圖中的第5步發(fā)生問題導致發(fā)送commit失敗,不還是會導致消息發(fā)布者和消息訂閱者間事務的不一致嗎?為了防止這種情況的發(fā)生,增加MQ超時回調(diào)機制。
下面的序列圖是事務型消息commit失敗時的執(zhí)行流程:
當MQ長時間收不到發(fā)布者的commit/rollback通知時,MQ會回調(diào)發(fā)布者應用詢問本地事務是否執(zhí)行成功,是commit還是rollback之前的消息。發(fā)布者需要提供對應的callback,在callback中判斷本地事務是否執(zhí)行成功。
TCC兩階段提交
在某些場景下,一個分布式事務可能會涉及到多個參與者,且每個參與者需要根據(jù)自己當時的狀態(tài)對事務進行響應。
假設這樣一個場景,一個電商網(wǎng)站可以允許用戶在支付時選擇多種支付方式。例如總共需要支付100元錢,用戶可以選擇積分支付10元,賬戶余額支付90元。用戶的積分由營銷系統(tǒng)負責,賬戶余額由賬務系統(tǒng)負責,訂單的狀態(tài)管理由訂單系統(tǒng)負責。
應對這種分布式事務場景,可以采用TCC兩階段提交的方式進行處理。
TCC將整個事務分成兩個階段——try和commit/cancel。TCC整個流程具有三種角色——事務發(fā)起者、事務參與者、事務協(xié)調(diào)者。以上面的訂單支付為例,采用TCC實現(xiàn)處理事務的流程如下:
但僅是這樣處理還是有一致性問題,例如在第二階段commit時如果發(fā)生宕機、網(wǎng)絡抖動等異常情況,就可能導致事務處于“非最終一致”狀態(tài)(參與者只執(zhí)行了try階段,沒有執(zhí)行第二階段?;虿糠謪⑴c者第二階段commit成功,部分參與者commit失敗)。為了應對這種情況,需要增加事務日志,以便發(fā)生異常時回復事務。
可以利用DB這種可靠存儲來記錄事務日志。日志中應包含事務執(zhí)行過程中的上下文、事務執(zhí)行狀態(tài)、事務的參與者等信息。事務日志可以由事務發(fā)起發(fā)負責記錄,也可以交由事務協(xié)調(diào)方進行記錄。
事務日志可以由主事務記錄日志和從事務記錄日志組成:
有了事務日志后,就可以周期性的不斷掃描事務日志,找到異常中斷的事務。根據(jù)事務日志中記錄的信息,推動剩余的參與者commit或者cancel,以便使整個分布式事務達到“最終一致性”。
下面是commit階段發(fā)生異常時的事務補償邏輯:
TCC兩階段提交的實現(xiàn)需要注意如下事項:
總結(jié)
傳統(tǒng)的單機事務應滿足A(原子性)、C(一致性)、I(隔離型)、D(持久性)四個特性,屬于剛性事務。由于分布式系統(tǒng)具有多個節(jié)點的特點,要求完全滿足ACID這四個規(guī)范會非常的困難。所以就誕生了柔性事務BASE理論(Basic availability、Soft state、Eventual consistency)。
相比于單機事務,分布式事務在A和D上仍能夠嚴格保證,但在C和I上就要有一定程度的限制放寬(允許看到中間狀態(tài)數(shù)據(jù)、最終一致性)。

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流