消息隊列的事務(wù)消息
1 MQ事務(wù)的意義
“發(fā)消息”過程,往往是為通知另外一個系統(tǒng)更新數(shù)據(jù),MQ的“事務(wù)”,主要解決Pro和Con的消息數(shù)據(jù)一致性問題。
用戶在電商APP上購物時
先把商品加到購物車
然后幾件商品一起下單
最后支付
完成購物流程,就可以愉快地等待收貨
該過程中有個需用MQ。
訂單系統(tǒng)創(chuàng)建訂單后,發(fā)消息給購物車模塊,將已下單商品從購物車刪除。
從購物車刪除已下單商品步驟,并非用戶下單支付這個主要流程的必需步驟,所以使用MQ異步清理購物車。
訂單模塊創(chuàng)建訂單的過程執(zhí)行了如下操作:
在訂單DB插一條訂單數(shù)據(jù),以創(chuàng)建訂單
發(fā)消息給MQ,消息內(nèi)容即剛創(chuàng)建的訂單
購物車模塊訂閱相應(yīng)Topic,接收訂單創(chuàng)建的消息,然后清理購物車,在購物車中刪除訂單中的商品。分布式下的這些步驟都有失敗可能性,若不做處理,就可能導(dǎo)致訂單數(shù)據(jù)與購物車數(shù)據(jù)不一致:
創(chuàng)建了訂單,沒有清理購物車
訂單沒創(chuàng)建成功,購物車里面的商品卻被清了
因此在任意步驟都可能失敗時,要保證訂單DB和購物車DB的數(shù)據(jù)一致性。購物車系統(tǒng)收到訂單創(chuàng)建成功消息清理購物車操作,只要成功執(zhí)行購物車清理后再提交消費確認即可。
如果失敗,由于沒有提交消費確認,MQ會自動重試。
問題關(guān)鍵點在訂單系統(tǒng),創(chuàng)建訂單和發(fā)送消息不允許一個成功而另一個失敗。
這就是事務(wù)問題。
2 分布式事務(wù)
單體關(guān)系型數(shù)據(jù)庫都完整的實現(xiàn)ACID,但對分布式系統(tǒng)
嚴格實現(xiàn)ACID,幾乎不可能
或?qū)崿F(xiàn)代價太大,無法接受
分布式系統(tǒng)在保證可用性和不嚴重犧牲性能前提下,要實現(xiàn)數(shù)據(jù)一致性非常困難,所以出現(xiàn)很多“殘血版”一致性,如順序一致性、最終一致性。
所以分布式事務(wù)更多是在分布式系統(tǒng)中事務(wù)的不完整實現(xiàn)。在不同場景有不同實現(xiàn),都是通過一些妥協(xié)解決問題。常見分布式事務(wù)實現(xiàn)有2PC、TCC和事務(wù)消息。每種實現(xiàn)都有其特定的使用場景,也有各自問題,沒有完美無缺的方案。
3 事務(wù)消息適用場景
主要是那些需要異步更新數(shù)據(jù),并且對數(shù)據(jù)實時性要求不高。
比如在創(chuàng)建訂單后,如果出現(xiàn)短暫幾s,購物車商品沒被及時清空,也不是完全不可接受,只要最終購物車的數(shù)據(jù)和訂單數(shù)據(jù)保持一致。
4 MQ實現(xiàn)分布式事務(wù)
事務(wù)消息需要MQ提供相應(yīng)功能才能實現(xiàn),Kafka和RocketMQ都支持事務(wù)功能:
注意:第二步發(fā)送半消息第三步創(chuàng)建訂單,這2個順序反一下是等價的,即先創(chuàng)建訂單在發(fā)送半消息。
半消息并非消息內(nèi)容不完整,包含的就是完整的消息內(nèi)容,和普通消息的唯一區(qū)別:
在事務(wù)提交前,對消費者,該消息不可見。
半消息發(fā)成功后,訂單系統(tǒng)即可執(zhí)行本地事務(wù)(創(chuàng)建訂單這個數(shù)據(jù)庫事務(wù)):
在訂單庫創(chuàng)建一條訂單記錄,并提交訂單庫的數(shù)據(jù)庫事務(wù)
然后根據(jù)本地事務(wù)執(zhí)行結(jié)果,決定提交或回滾事務(wù)消息
訂單創(chuàng)建成功,提交事務(wù)消息,購物車系統(tǒng)即可消費到該消息,繼續(xù)后續(xù)流程
訂單創(chuàng)建失敗,回滾事務(wù)消息,購物車系統(tǒng)不會收到該消息
但有問題:若在第4步提交事務(wù)消息時失敗了咋辦?對此,Kafka和RocketMQ給出不同解決方案:
Kafka直接拋異常,讓用戶自行處理,我們能:
在業(yè)務(wù)代碼反復(fù)重試提交,直到提交成功
或刪除之前創(chuàng)建的訂單進行補償
下面詳解 rocketmq 的方案。
5 RocketMQ分布式事務(wù)
RocketMQ的事務(wù)實現(xiàn)增加了事務(wù)反查機制,來解決事務(wù)消息提交失敗的問題。
若Pro(訂單模塊)在提交或回滾事務(wù)消息時發(fā)生網(wǎng)絡(luò)異常,導(dǎo)致Broker沒收到提交或回滾請求,Broker會定期去Pro反查該事務(wù)對應(yīng)的本地事務(wù)的狀態(tài),然后根據(jù)反查結(jié)果決定提交或回滾該事務(wù)。
要支持事務(wù)反查,業(yè)務(wù)代碼需實現(xiàn)一個反查本地事務(wù)狀態(tài)的接口,告知RocketMQ本地事務(wù)是成功還是失敗。
若反查的服務(wù)器數(shù)據(jù)不一致,它是認為本地事務(wù)失敗還是繼續(xù)多次反查呢?
反查接口的定義,它檢查的是本地事務(wù)(在我們這個例子里面就是數(shù)據(jù)庫事務(wù))有沒有執(zhí)行成功,并不會去比較數(shù)據(jù)是否一致。
根據(jù)消息中的訂單ID,在訂單庫中查詢該訂單是否存在:
存在,則返回成功
否則,返回失敗
RocketMQ會自動根據(jù)事務(wù)反查的結(jié)果提交或回滾事務(wù)消息。
反查本地事務(wù)的實現(xiàn)并不依賴消息的發(fā)送方,即訂單服務(wù)的某節(jié)點的任何數(shù)據(jù)。
這種情況下,即使發(fā)送事務(wù)消息的訂單服務(wù)節(jié)點宕機,RocketMQ依然可通過其他訂單服務(wù)節(jié)點執(zhí)行反查,確保事務(wù)完整性。
5.1 RocketMQ事務(wù)消息流程圖
若本地事務(wù)提交失敗,已發(fā)出去的消息又是無法撤回的,這就會導(dǎo)致數(shù)據(jù)不一致。
5.2 FAQ
因為消費失敗,會自動重試,所以不會丟消息,但可能重復(fù)消費。
若Pro本地事務(wù)執(zhí)行太久還沒執(zhí)行完,消息中心就來反查是否有問題,所以可以將發(fā)消息放本地事務(wù)的后面吧,另外次數(shù)定義也是經(jīng)驗值。反查一般指定一個事務(wù)超時時間,超時之前會不定期反查。
5.3 RocketMQ事務(wù)消息代碼實現(xiàn)案例
訂單下單。
在該方法里進行訂單創(chuàng)建,并提交本地事務(wù):
若commit成功,則返回COMMIT狀態(tài)
否則ROLLBACK狀態(tài)
若正常返回COMMIT或ROLLBACK,不會存在第3步的反查情況。
此方法會根據(jù)消息中的訂單號去數(shù)據(jù)庫確認訂單是否存在,存在就返回COMMIT狀態(tài),否則是ROLLBACK狀態(tài)。
只要收到MQ消息就將本次訂單的商品從購物車中刪除即可。
6 RocketMQ事務(wù)消息完整實現(xiàn)ACID了嗎
A:本地事務(wù)的操作1,與往MQ中生產(chǎn)消息的操作2,是兩個分離操作,不具備原子性
C:由于操作MQ屬異步,在數(shù)據(jù)一致性上,只能保證最終一致性。
對時效性要求很高系統(tǒng),事務(wù)消息并非數(shù)據(jù)一致。但對時效性要求不高系統(tǒng),就是數(shù)據(jù)一致的。需要結(jié)合業(yè)務(wù)需要看問題
I:由于事務(wù)消息分兩步操作,本地事務(wù)提交后,別的事務(wù)消息就已經(jīng)能看到提交的消息。所以,不符合隔離性
D:rocketMq上支持事務(wù)的反查機制,但“半消息”存儲在磁盤還是內(nèi)存?
若存儲在磁盤,那就支持持久性,即使事務(wù)消息提交后,發(fā)生服務(wù)突然宕機也不受影響
若存儲在內(nèi)存,則無法保證持久性
rocketmq實現(xiàn)分布式事務(wù),使用兩階段提交,和mysql寫redo log和binlog日志的兩階段提交類似,以訂單為例:
提交訂單消息到mq中,等待mq回復(fù)ack,消息提交成功,但是此時的消息對消費組不可見,即half消息
此階段像mysql的引擎層寫redo log的prepare階段。
執(zhí)行本地事務(wù),執(zhí)行本地事務(wù)成功
此階段像mysql的service層寫binlog的階段,寫binlog成功,最后提交或者回滾隊列事務(wù)。
rocketmq為防止commit和rollback超時或者失敗,采取回查的補償機制,回查次數(shù)默認15次(感覺這個會不會導(dǎo)致服務(wù)超時了),超過會rollback,有點像mysql宕機重啟根據(jù)redo log中的xid找binlog的xid事務(wù),如果binlog日志也已經(jīng)寫成功,mysql這個事務(wù)也會提交,因為redo log和binlog這個事務(wù)都寫完整。
消息對消費者不可見,將其消息的主題topic和隊列id修改為half topic,原先的主題和隊列id也做為消息的屬性,如果事務(wù)提交或者回滾會將其消息的隊列改為原先的隊列。rocketMq開啟任務(wù),從half topic中獲取消息,調(diào)用其中的生產(chǎn)者的監(jiān)聽進行回查是否提交回滾。
rocketmq采用commitlog存放消息,消費者使用consumeQueue二級索引從commitlog獲取消息實體內(nèi)容。
理解Index File:indexFile的作用就是給commitlog做的索引,提升讀取消息時的查詢效率。
回查借助OP topic進行獲取到Half消息進行后續(xù)的回查操作。
7 若MQ不支持半消息,是否有其他的解決方案
利用數(shù)據(jù)庫的事務(wù)消息表。
把消息信息的快照和對業(yè)務(wù)數(shù)據(jù)的操作作為數(shù)據(jù)庫事務(wù)操作數(shù)據(jù)庫,操作成功后從數(shù)據(jù)庫讀取消息信息發(fā)送給broker,收到發(fā)送成功的回執(zhí)后刪除數(shù)據(jù)庫中的消息快照。我個人覺得這種方案在不支持半消息的隊列方案里也是一種選擇,不知道您覺得這種實現(xiàn)方案有沒有什么問題。
如果有個生產(chǎn)者和消費者都可訪問,并且性能還不錯的數(shù)據(jù)庫,肯定使用這個數(shù)據(jù)庫實現(xiàn)事務(wù)較好。
然而大部分事務(wù)消息使用的場景是
沒有這樣的數(shù)據(jù)庫
或由于設(shè)計、安全或者網(wǎng)絡(luò)原因,生產(chǎn)者消費者不能共享數(shù)據(jù)庫
或數(shù)據(jù)庫的性能達不到要求
如果先創(chuàng)建訂單,當前服務(wù)由于不可抗拒因素不能正常工作,沒給購物車系統(tǒng)發(fā)送消息,這種情況加就會出現(xiàn):訂單已創(chuàng)建且購物車沒有清空。
而發(fā)送半消息,可通過定期查詢事務(wù)狀態(tài)然后根據(jù)然后具體的業(yè)務(wù)回滾操作或者重新發(fā)送消息(保持業(yè)務(wù)的冪等性)。
消費端做冪等處理來保障消息不會重復(fù)消費
可以采用狀態(tài)機的方式
消息數(shù)據(jù)唯一鍵+redis setnx來保障
本地消息表,要確保插入本地消息表和執(zhí)行消息消費業(yè)務(wù)在同一事務(wù)里
8 總結(jié)
RocketMQ事務(wù)反查機制通過定期反查事務(wù)狀態(tài),來補償提交事務(wù)消息可能出現(xiàn)的通信失敗。
在Kafka的事務(wù)功能中,并沒有類似的反查機制,需要用戶自行去解決這個問題。
但不代表RocketMQ的事務(wù)功能比Kafka更好,只能說在該例場景,RocketMQ更適合。
Kafka對事務(wù)的定義、實現(xiàn)和適用場景,和RocketMQ有較大差異。
參考
https://rocketmq.apache.org/docs/transaction-example/
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。