微服務(wù)架構(gòu)下啸罢,各個(gè)微服務(wù)間的通信方式是首先需要決定的事编检。微服務(wù)間的通信方式主要有REST、RPC和消息這三種扰才。這三種通信方式各有優(yōu)缺點(diǎn)允懂,各有其適合的場景,關(guān)于它們的比較及分析今天就先不講了衩匣。
今天主要講的是基于消息的通信方式下蕾总,先入庫在發(fā)送消息的問題±拍螅基于消息的通信方式下生百,各個(gè)微服務(wù)間通過消息驅(qū)動來完成業(yè)務(wù)邏輯。一個(gè)典型的例子如下:
上例中柄延,用戶服務(wù)處理用戶注冊請求蚀浆,先入庫,然后發(fā)送用戶注冊事件,郵件服務(wù)監(jiān)聽用戶注冊事件市俊,然后發(fā)送歡迎郵件杨凑。
那么就上述場景而言,對于用戶服務(wù)摆昧,我們的業(yè)務(wù)代碼該如何寫呢撩满?為什么我說先入庫再發(fā)消息,沒你想得這么簡單呢据忘?下面一起來看看。
1 先入庫搞糕,再發(fā)消息勇吊,最簡單又直接的方式
簡單又直接的入庫發(fā)消息偽代碼如下:
1. var content = processRequest(httpRequest);
2. var message = prepareMessage(httpRequest);
3. DB.insert(content);
4. Message.publish(message);
對于先入庫,再發(fā)消息的業(yè)務(wù)邏輯窍仰,最簡單直接的代碼如上汉规,那么上述代碼有什么問題嗎?
考慮以下場景:
(1) 數(shù)據(jù)庫入庫成功驹吮,發(fā)送消息失敗针史,即步驟3成功,步驟4失敗
數(shù)據(jù)可能不一致碟狞。因?yàn)椴襟E4失敗的原因總的來說有兩個(gè)啄枕,一是消息發(fā)送失敗(消息總線未接收到消息)族沃,此時(shí)數(shù)據(jù)不一致频祝,因?yàn)閿?shù)據(jù)庫中有數(shù)據(jù),但消息未發(fā)送脆淹。二是消息發(fā)送成功(消息總線接收到)常空,但回包的時(shí)候失敗,對于系統(tǒng)來說盖溺,此時(shí)數(shù)據(jù)反而是一致的漓糙,數(shù)據(jù)入庫,消息發(fā)送了烘嘱。
(2) 數(shù)據(jù)庫入庫失敗
數(shù)據(jù)可能不一致昆禽。有人可能會想數(shù)據(jù)庫操作失敗,數(shù)據(jù)庫的事務(wù)ACID特性可以保證數(shù)據(jù)一致性蝇庭。但實(shí)際這里也可能有兩種情況为狸,一是數(shù)據(jù)庫操作失敗,事務(wù)未提交遗契,此時(shí)數(shù)據(jù)一致辐棒,數(shù)據(jù)庫會回滾事務(wù)。二是事務(wù)已提交,數(shù)據(jù)庫回包失敗漾根,數(shù)據(jù)不一致(數(shù)據(jù)庫和消息總線中的數(shù)據(jù)不一致)泰涂,數(shù)據(jù)庫中有數(shù)據(jù),消息卻沒發(fā)送辐怕。
2 最簡單直接的方式并不好使逼蒙,那該怎么辦?
其實(shí)上述問題的本質(zhì)是分布式事務(wù)問題寄疏,數(shù)據(jù)庫和消息總線實(shí)際是兩個(gè)資源是牢。想要保持兩個(gè)或者多個(gè)資源間的數(shù)據(jù)一致性,以及操作的原子性陕截,這正是分布式事務(wù)要解決的問題驳棱。
讓我們嘗試解決此類問題。
首先要問的一個(gè)問題是农曲,我們的系統(tǒng)需要強(qiáng)一致性嗎社搅?在上述例子中如果數(shù)據(jù)庫和消息總線中的數(shù)據(jù)需要保持強(qiáng)一致,則在任一時(shí)刻數(shù)據(jù)庫與消息總線中的數(shù)據(jù)都需要保持一致乳规。
顯然并不需要形葬。
實(shí)際在分布式系統(tǒng)中,只要保持弱一致性就可以了暮的,也就是說最終一致性笙以,對應(yīng)上例,也就是說在任一時(shí)刻冻辩,數(shù)據(jù)庫與消息總線中的數(shù)據(jù)可以暫時(shí)不一致源织,但最終需要一致,只不過中間有一些間隔微猖。
所以現(xiàn)在我們只要讓系統(tǒng)具備最終一致性就可以了谈息,那么如何具備最終一致性呢?
在上例中凛剥,把問題具體化侠仇,其實(shí)就是處理完請求并且入庫后,必須發(fā)送消息犁珠,也就是數(shù)據(jù)庫中有的數(shù)據(jù)逻炊,消息總線中也必須有。問題進(jìn)一步抽象定義犁享,即解決數(shù)據(jù)庫入庫和發(fā)送消息的原子性問題余素,這兩個(gè)操作要么都成功,要么都失敗炊昆。并且現(xiàn)在我們的系統(tǒng)只需要滿足弱一致性就可以桨吊,所以問題可以更進(jìn)一步定義為這兩個(gè)操作要么最終都成功威根,要么最終都失敗。
看到這里视乐,有一個(gè)方案應(yīng)該能夠浮現(xiàn)出來——本地消息表洛搀。
本地消息表的偽代碼如下:
1. var content = processRequest(httpRequest);
2. var message = prepareMessage(httpRequest);
3. DB.begin();
4. DB.insert(content);
5. DB.insert(message);
6. DB.commit();
7. Message.publish(message);
8. DB.delete(message);
// 定時(shí)任務(wù),補(bǔ)償發(fā)送消息佑淀,這里查詢的消息注意時(shí)間存在過短的問題留美,避免重復(fù)發(fā)送
Executor.execute(new Task() {
public void run() {
while (true) {
var message = DB.selectMessage();
Message.publish(message);
DB.delete(message);}
}
});
該方案本質(zhì)上是利用了本地?cái)?shù)據(jù)庫事務(wù)的特性,將消息和業(yè)務(wù)邏輯處理放在一個(gè)事務(wù)里持久化伸刃,利用事務(wù)特性可以保證業(yè)務(wù)處理和消息能夠同時(shí)存儲成功或失敗谎砾,然后在發(fā)送消息。
同樣捧颅,讓我們考慮下述場景:
(1)數(shù)據(jù)庫入庫成功景图,消息發(fā)送失敗,即步驟3~6成功隘道,步驟7失敗
數(shù)據(jù)最終一致症歇。定時(shí)任務(wù)會補(bǔ)償消息投遞郎笆,當(dāng)然這里也可能會存在消息重復(fù)發(fā)送的問題谭梗。
(2)數(shù)據(jù)庫入庫失敗,即步驟3~6失敗宛蚓。
數(shù)據(jù)最終一致激捏。數(shù)據(jù)庫在事務(wù)提交前失敗,數(shù)據(jù)一致凄吏。數(shù)據(jù)庫在事務(wù)提交后远舅,但回包前失敗,數(shù)據(jù)最終一致痕钢,數(shù)據(jù)已存在图柏,定時(shí)任務(wù)會補(bǔ)償消息投遞。
(3)數(shù)據(jù)庫入庫成功任连,消息發(fā)送成功蚤吹,消息刪除失敗,即步驟3~7成功随抠,步驟8失敗裁着。
數(shù)據(jù)最終一致。消息未刪除拱她,定時(shí)任務(wù)補(bǔ)償發(fā)送消息二驰,會導(dǎo)致消息重復(fù)發(fā)送。消息已刪除秉沼,但數(shù)據(jù)庫回包前失敗桶雀,補(bǔ)償任務(wù)不做處理矿酵,數(shù)據(jù)最終一致。
可以看到本地消息表除了會導(dǎo)致消息重復(fù)投遞背犯,幾乎沒有別的問題坏瘩。
那么消息重復(fù)投遞怎么辦?
如果消息重復(fù)投遞漠魏,這里只看數(shù)據(jù)庫跟消息總線倔矾,其實(shí)數(shù)據(jù)是不一致的,消息總線中數(shù)據(jù)多了柱锹。但從整個(gè)系統(tǒng)的層面來看呢哪自?如果消費(fèi)端能夠?qū)崿F(xiàn)冪等,那么整個(gè)系統(tǒng)的數(shù)據(jù)還是最終一致的禁熏。所以采用本地消息表壤巷,下游消費(fèi)端需要實(shí)現(xiàn)冪等。而且現(xiàn)在有的消息中間件也能夠?qū)崿F(xiàn)發(fā)送消息的冪等(比如Kafka 0.11版本以上瞧毙,Broker可以通過發(fā)送的消息id進(jìn)行去重胧华,保證發(fā)送消息的冪等),即重復(fù)投遞的消息在消息中間件中只會存在一份宙彪,這樣系統(tǒng)也是沒有問題的矩动。
3 先入庫,再發(fā)消息原來不簡單释漆,那么符合上述場景的業(yè)務(wù)是不是都得這么做呢悲没?
有同學(xué)看到這里可能會覺得原來處理請求,先入庫再發(fā)消息的場景原來不是這么簡單呀男图,看來以前用錯(cuò)了示姿?
那是不是此場景下所有的應(yīng)用都需要按照此類方案來呢?
我這里的建議是看具體業(yè)務(wù)需求逊笆。
大流量大規(guī)模的分布式系統(tǒng)栈戳,從可靠性及可維護(hù)性來講,必須這么做难裆。至于那些用戶少子檀,規(guī)模小的應(yīng)用,從故障發(fā)生的概率差牛、發(fā)生故障后人員維護(hù)的成本來考慮命锄,你可以不遵守上述方案。
當(dāng)然偏化,能夠看到這些問題后然后選擇一個(gè)適合的方案脐恩,和不知道自己在做什么完全是兩碼事。
先知道規(guī)則侦讨,然后再知道什么時(shí)候可以打破規(guī)則驶冒。
寫在最后
對于請求處理苟翻,先入庫再發(fā)消息的場景并沒有看起來的這么簡單。該問題實(shí)際是一個(gè)分布式事務(wù)問題骗污,涉及到兩個(gè)資源間的數(shù)據(jù)一致性崇猫,入庫與發(fā)消息原子性問題。
對于大多數(shù)分布式應(yīng)用需忿,能夠滿足數(shù)據(jù)最終一致性就可以诅炉。
所以上述場景可以采用本地消息表的方案,本地消息表實(shí)質(zhì)上是利用了本地?cái)?shù)據(jù)庫的事務(wù)特性屋厘,保證業(yè)務(wù)處理與消息存儲的事務(wù)特性涕烧。
本地消息表可能會存在消息重復(fù)發(fā)送的問題,所以需要實(shí)現(xiàn)消費(fèi)端的冪等汗洒。
先知道規(guī)則议纯,然后再知道什么時(shí)候可以打破規(guī)則。
最后留一個(gè)問題溢谤,對于消費(fèi)端來說瞻凤,接收消息,然后處理入庫世杀,如何保持冪等阀参?如果是接收消息,然后處理入庫玫坛,再然后再發(fā)消息的場景呢结笨?如果是接收消息包晰,然后遠(yuǎn)程調(diào)用的場景呢湿镀?
如果能夠回答清楚上述問題,不光光是對這些場景有很深的理解伐憾,相信你對整個(gè)分布式系統(tǒng)的設(shè)計(jì)與實(shí)現(xiàn)都有很深的理解勉痴。
后面的文章,說一說我對上述場景以及分布式系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)的理解树肃。歡迎大家關(guān)注蒸矛。
希望今天的內(nèi)容對大家有所幫助,更多精彩文章歡迎關(guān)注微信公眾號:WU雙胸嘴。