浅谈“微服务”
微服務概述
1.1 易于擴展
1.2 部署簡單
1.3 技術異構性
數(shù)據(jù)庫的服務化切分
2.1 什么是“分庫分表”?
2.2 數(shù)據(jù)庫擴展的幾種方式
2.3 分庫分表的幾種方式
2.4 引入分庫分表中間件后面臨的問題
2.5 現(xiàn)有分庫分表中間件的橫向?qū)Ρ?/p>
微服務架構中的分布式事務
3.1 什么是事務?
3.2 事務的四大特性 ACID
3.3 事務的隔離級別
3.4 什么是分布式事務?
3.5 CAP理論
3.6 BASE理論
3.7 酸堿平衡
3.8 分布式事務協(xié)議
3.9 分布式事務的解決方案
服務部署
4.1 持續(xù)集成、持續(xù)部署、持續(xù)交付
4.2 微服務與持續(xù)集成
4.3 微服務構建物
1. 什么是微服務?
我們首先給出微服務的定義,然后再對該定義給出詳細的解釋。
微服務就是一些可獨立運行、可協(xié)同工作的小的服務。
從概念中我們可以提取三個關鍵詞:可獨立運行、可協(xié)同工作、小。這三個詞高度概括了微服務的核心特性。下面我們就對這三個詞作詳細解釋。
可獨立運行
微服務是一個個可以獨立開發(fā)、獨立部署、獨立運行的系統(tǒng)或者進程。
可協(xié)同工作
采用了微服務架構后,整個系統(tǒng)被拆分成多個微服務,這些服務之間往往不是完全獨立的,在業(yè)務上存在一定的耦合,即一個服務可能需要使用另一個服務所提供的功能。這就是所謂的“可協(xié)同工作”。與單服務應用不同的是,多個微服務之間的調(diào)用時通過RPC通信來實現(xiàn),而非單服務的本地調(diào)用,所以通信的成本相對要高一些,但帶來的好處也是可觀的。
小而美
微服務的思想是,將一個擁有復雜功能的龐大系統(tǒng),按照業(yè)務功能,拆分成多個相互獨立的子系統(tǒng),這些子系統(tǒng)則被稱為“微服務”。每個微服務只承擔某一項職責,從而相對于單服務應用來說,微服務的體積是“小”的。小也就意味著每個服務承擔的職責變少,根據(jù)單一職責原則,我們在系統(tǒng)設計時,要盡量使得每一項服務只承擔一項職責,從而實現(xiàn)系統(tǒng)的“高內(nèi)聚”。
2. 微服務的優(yōu)點
1. 易于擴展
在單服務應用中,如果目前性能到達瓶頸,無法支撐目前的業(yè)務量,此時一般采用集群模式,即增加服務器集群的節(jié)點,并將這個單服務應用“復制”到所有的節(jié)點上,從而提升整體性能。然而這種擴展的粒度是比較粗糙的。如果只是系統(tǒng)中某一小部分存在性能問題,在單服務應用中,也要將整個應用進行擴展,這種方式簡單粗暴,無法對癥下藥。而當我們使用了微服務架構后,如果某一項服務的性能到達瓶頸,那么我們只需要增加該服務的節(jié)點數(shù)即可,其他服務無需變化。這種擴展更加具有針對性,能夠充分利用計算機硬件/軟件資源。而且只擴展單個服務影響的范圍較小,從而系統(tǒng)出錯的概率也就越低。
2. 部署簡單
對于單服務應用而言,所有代碼均在一個項目中,從而導致任何微小的改變都需要將整個項目打包、發(fā)布、部署,而這一系列操作的代價是高昂的。長此以往,團隊為了降低發(fā)布的頻率,會使得每次發(fā)布都伴隨著大量的修改,修改越多也就意味著出錯的概率也越大。 當我們采用微服務架構以后,每個服務只承擔少數(shù)職責,從而每次只需要發(fā)布發(fā)生修改的系統(tǒng),其他系統(tǒng)依然能夠正常運行,波及范圍較小。此外,相對于單服務應用而言,每個微服務系統(tǒng)修改的代碼相對較少,從而部署后出現(xiàn)錯誤的概率也相對較低。
3. 技術異構性
對于單服務應用而言,一個系統(tǒng)的所有模塊均整合在一個項目中,所以這些模塊只能選擇相同的技術。但有些時候,單一技術沒辦法滿足不同的業(yè)務需求。如對于項目的算法團隊而言,函數(shù)試編程語言可能更適合算法的開發(fā),而對于業(yè)務開發(fā)團隊而言,類似于Java的強類型語言具有更高的穩(wěn)定性。然而在單服務應用中只能互相權衡,選擇同一種語言,而當我們使用微服務結構后,這個問題就能夠引刃而解。我們將一個完整的系統(tǒng)拆分成了多個獨立的服務,從而每個服務都可以根據(jù)各自不同的特點,選擇最為合適的技術體系。
當然,并不是所有的微服務系統(tǒng)都具備技術異構性,要實現(xiàn)技術異構性,必須保證所有服務都提供通用接口。我們知道,在微服務系統(tǒng)中,服務之間采用RPC接口通信,而實現(xiàn)RPC通信的方式有很多。有一些RPC通信方式與語言強耦合,如Java的RMI技術,它就要求通信的雙方都必須采用Java語言開發(fā)。當然,也有一些RPC通信方式與語言無關,如基于HTTP協(xié)議的REST。這種通信方式對通信雙方所采用的語言沒有做任何限制,只要通信過程中傳輸?shù)臄?shù)據(jù)遵循REST規(guī)范即可。當然,與語言無關也就意味著通信雙方?jīng)]有類型檢查,從而會提高出錯的概率。所以,究竟選擇與語言無關的RPC通信方式,還是選擇與語言強耦合的RPC通信方式,需要我們根據(jù)實際的業(yè)務場景合理地分析。
2. 數(shù)據(jù)庫的服務化切分
2.1 什么是“分庫分表”?
隨著大數(shù)據(jù)時代的到來,業(yè)務系統(tǒng)的數(shù)據(jù)量日益增大,數(shù)據(jù)存儲能力逐漸成為影響系統(tǒng)性能的瓶頸。目前主流的關系型數(shù)據(jù)庫單表存儲上限為1000萬條記錄,而這一存儲能力顯然已經(jīng)無法滿足大數(shù)據(jù)背景下的業(yè)務系統(tǒng)存儲要求了。隨著微服務架構、分布式存儲等概念的出現(xiàn),數(shù)據(jù)存儲問題也漸漸迎來了轉(zhuǎn)機。而數(shù)據(jù)分片是目前解決海量數(shù)據(jù)持久化存儲與高效查詢的一種重要手段。數(shù)據(jù)分庫分表的過程在系統(tǒng)設計階段完成,要求系統(tǒng)設計人員根據(jù)系統(tǒng)預期的業(yè)務量,將未來可能出現(xiàn)瓶頸的數(shù)據(jù)庫、數(shù)據(jù)表按照一定規(guī)則拆分成多個庫、多張表。這些數(shù)據(jù)庫和數(shù)據(jù)表需要部署在不同的服務器上,從而將數(shù)據(jù)讀寫壓力分攤至集群中的各個節(jié)點,提升數(shù)據(jù)庫整體處理能力,避免出現(xiàn)讀寫瓶頸的現(xiàn)象。
目前數(shù)據(jù)分片的方式一共有兩種:離散分片和連續(xù)分片。
離散分片是按照數(shù)據(jù)的某一字段哈希取模后進行分片存儲。只要哈希算法選擇得當,數(shù)據(jù)就會均勻地分布在不同的分片中,從而將讀寫壓力平均分配給所有分片,整體上提升數(shù)據(jù)的讀寫能力。然而,離散存儲要求數(shù)據(jù)之間有較強的獨立性,但實際業(yè)務系統(tǒng)并非如此,不同分片之間的數(shù)據(jù)往往存在一定的關聯(lián)性,因此在某些場景下需要跨分片連接查詢。由于目前所有的關系型數(shù)據(jù)庫出于安全性考慮,均不支持跨庫連接。因此,跨庫操作需要由數(shù)據(jù)分庫分表中間件來完成,這極大影響數(shù)據(jù)的查詢效率。此外,當數(shù)據(jù)存儲能力出現(xiàn)瓶頸需要擴容時,離散分片規(guī)則需要將所有數(shù)據(jù)重新進行哈希取模運算,這無疑成為限制系統(tǒng)可擴展性的一個重要因素。雖然,一致性哈希能在一定程度上減少系統(tǒng)擴容時的數(shù)據(jù)遷移,但數(shù)據(jù)遷移問題仍然不可避免。對于一個已經(jīng)上線運行的系統(tǒng)而言,系統(tǒng)停止對外服務進行數(shù)據(jù)遷移的代價太大。
第二種數(shù)據(jù)分片的方式即為連續(xù)分片,它能解決系統(tǒng)擴容時產(chǎn)生的數(shù)據(jù)遷移問題。這種方式要求數(shù)據(jù)按照時間或連續(xù)自增主鍵連續(xù)存儲。從而一段時間內(nèi)的數(shù)據(jù)或相鄰主鍵的數(shù)據(jù)會被存儲在同一個分片中。當需要增加分片時,不會影響現(xiàn)有的分片。因此,連續(xù)分片能解決擴容所帶來的數(shù)據(jù)遷移問題。但是,數(shù)據(jù)的存儲時間和讀寫頻率往往呈正比,也就是大量的讀寫往往都集中在最新存儲的那一部分數(shù)據(jù),這就會導致熱點問題,并不能起到分攤讀寫壓力的初衷。
2.2 數(shù)據(jù)庫擴展的幾種方式
數(shù)據(jù)庫擴展一共有四種分配方式,分別是:垂直分庫、垂直分表、水平分表、水平數(shù)據(jù)分片。每一種策略都有各自的適用場景。
垂直分庫
垂直分庫即是將一個完整的數(shù)據(jù)庫根據(jù)業(yè)務功能拆分成多個獨立的數(shù)據(jù)庫,這些數(shù)據(jù)庫可以運行在不同的服務器上,從而提升數(shù)據(jù)庫整體的數(shù)據(jù)讀寫性能。這種方式在微服務架構中非常常用。微服務架構的核心思想是將一個完整的應用按照業(yè)務功能拆分成多個可獨立運行的子系統(tǒng),這些子系統(tǒng)稱為“微服務”,各個服務之間通過RPC接口通信,這樣的結構使得系統(tǒng)耦合度更低、更易于擴展。垂直分庫的理念與微服務的理念不謀而合,可以將原本完整的數(shù)據(jù)按照微服務拆分系統(tǒng)的方式,拆分成多個獨立的數(shù)據(jù)庫,使得每個微服務系統(tǒng)都有各自獨立的數(shù)據(jù)庫,從而可以避免單個數(shù)據(jù)庫節(jié)點壓力過大,影響系統(tǒng)的整體性能,如下圖所示。
?
垂直分表
垂直分表如果一張表的字段非常多,那么很有可能會引起數(shù)據(jù)的跨頁存儲,這會造成數(shù)據(jù)庫額外的性能開銷,而垂直分表可以解決這個問題。垂直分表就是將一張表中不常用的字段拆分到另一張表中,從而保證第一章表中的字段較少,避免出現(xiàn)數(shù)據(jù)庫跨頁存儲的問題,從而提升查詢效率。而另一張表中的數(shù)據(jù)通過外鍵與第一張表進行關聯(lián),如下圖所示。
?
水平分表
如果一張表中的記錄數(shù)過多(超過1000萬條記錄),那么會對數(shù)據(jù)庫的讀寫性能產(chǎn)生較大的影響,雖然此時仍然能夠正確地讀寫,但讀寫的速度已經(jīng)到了業(yè)務無法忍受的地步,此時就需要使用水平分表來解決這個問題。水平分表是將一張含有很多記錄數(shù)的表水平切分,拆分成幾張結構相同的表。舉個例子,假設一張訂單表目前存儲了2000萬條訂單的數(shù)據(jù),導致數(shù)據(jù)讀寫效率極低。此時可以采用水平分表的方式,將訂單表拆分成100張結構相同的訂單表,分別叫做order_1、order_2……、order_100。然后可以根據(jù)訂單所屬用戶的id進行哈希取模后均勻地存儲在這100張表中,從而每張表中只存儲了20萬條訂單記錄,極大提升了訂單的讀寫效率,如下圖所示。 當然,如果拆分出來的表都存儲在同一個數(shù)據(jù)庫節(jié)點上,那么當請求量過大的時候,畢竟單臺服務器的處理能力是有限的,數(shù)據(jù)庫仍然會成為系統(tǒng)的瓶頸,所以為了解決這個問題,就出現(xiàn)了水平數(shù)據(jù)分片的解決方案。
?
水平分庫分表
水平數(shù)據(jù)分片與數(shù)據(jù)分片區(qū)別在于:水平數(shù)據(jù)分片首先將數(shù)據(jù)表進行水平拆分,然后按照某一分片規(guī)則存儲在多臺數(shù)據(jù)庫服務器上。從而將單庫的壓力分攤到了多庫上,從而避免因為數(shù)據(jù)庫硬件資源有限導致的數(shù)據(jù)庫性能瓶頸,如下圖所示。
?
2.3 分庫分表的幾種方式
目前常用的數(shù)據(jù)分片策略有兩種,分別是連續(xù)分片和離散分片。
離散分片
離散分片是指將數(shù)據(jù)打散之后均勻地存儲在邏輯表的各個分片中,從而使的對同一張邏輯表的數(shù)據(jù)讀取操作均勻地落在不同庫的不同表上,從而提高讀寫速度。離散分片一般以哈希取模的方式實現(xiàn)。比如:一張邏輯表有4個分片,那么在讀寫數(shù)據(jù)的時候,中間件首先會取得分片字段的哈希值,然后再模以4,從而計算出該條記錄所在的分片。在這種方法中,只要哈希算法選的好,那么數(shù)據(jù)分片將會比較均勻,從而數(shù)據(jù)讀寫就會比較均勻地落在各個分片上,從而就有較高的讀寫效率。但是,這種方式也存在一個最大的缺陷——數(shù)據(jù)庫擴容成本較高。采用這種方式,如果需要再增加分片,原先的分片算法將失效,并且所有記錄都需要重新計算所在分片的位置。對于一個已經(jīng)上線的系統(tǒng)來說,行級別的數(shù)據(jù)遷移成本相當高,而且由于數(shù)據(jù)遷移期間系統(tǒng)仍在運行,仍有新數(shù)據(jù)產(chǎn)生,從而無法保證遷移過程數(shù)據(jù)的一致性。如果為了避免這個問題而停機遷移,那必然會對業(yè)務造成巨大影響。當然,如果為了避免數(shù)據(jù)遷移,在一開始的時候就分片較多的分片,那需要承擔較高的費用,這對于中小公司來說是無法承受的。
連續(xù)分片
連續(xù)分片指的是按照某一種分片規(guī)則,將某一個區(qū)間內(nèi)的數(shù)據(jù)存儲在同一個分片上。比如按照時間分片,每個月生成一張物理表。那么在讀寫數(shù)據(jù)時,直接根據(jù)當前時間就可以找到數(shù)據(jù)所在的分片。再比如可以按照記錄ID分片,這種分片方式要求ID需要連續(xù)遞增。由于Mysql數(shù)據(jù)庫單表支持最大的記錄數(shù)約為1000萬,因此我們可以根據(jù)記錄的ID,使得每個分片存儲1000萬條記錄,當目前的記錄數(shù)即將到達存儲上限時,我們只需增加分片即可,原有的數(shù)據(jù)無需遷移。連續(xù)分片的一個最大好處就是方便擴容,因為它不需要任何的數(shù)據(jù)遷移。但是,連續(xù)分片有個最大的缺點就是熱點問題。連續(xù)分片使得新插入的數(shù)據(jù)集中在同一個分片上,而往往新插入的數(shù)據(jù)讀寫頻率較高,因此,讀寫操作都會集中在最新的分片上,從而無法體現(xiàn)數(shù)據(jù)分片的優(yōu)勢。
2.4 引入分庫分表中間件后面臨的問題
跨庫操作
在關系型數(shù)據(jù)庫中,多張表之間往往存在關聯(lián),我們在開發(fā)過程中需要使用JOIN操作進行多表連接。但是當我們使用了分庫分表模式后,由于數(shù)據(jù)庫廠商處于安全考慮,不允許跨庫JOIN操作,從而如果需要連接的兩張表被分到不同的庫中后,就無法使用SQL提供的JOIN關鍵字來實現(xiàn)表連接,我們可能需要在業(yè)務系統(tǒng)層面,通過多次SQL查詢,完成數(shù)據(jù)的組裝和拼接。這一方面會增加業(yè)務系統(tǒng)的復雜度,另一方面會增加業(yè)務系統(tǒng)的負載。 因此,當我們使用分庫分表模式時,需要根據(jù)具體的業(yè)務場景,合理地設置分片策略、設置分片字段,這將會在本文的后續(xù)章節(jié)中介紹。
分布式事務
我們知道,數(shù)據(jù)庫提供了事務的功能,以保證數(shù)據(jù)一致性。然而,這種事務只是針對單數(shù)據(jù)庫而言的,數(shù)據(jù)庫廠商并未提供跨庫事務。因此,當我們使用了分庫分表之后,就需要我們在業(yè)務系統(tǒng)層面實現(xiàn)分布式事務。關于分布式事務的詳細內(nèi)容,可以參考筆者的另一篇文章《常用的分布式事務解決方案》。
2.5 現(xiàn)有分庫分表中間件的橫向?qū)Ρ?/h2> -
Cobar實現(xiàn)數(shù)據(jù)庫的透明分庫,讓開發(fā)人員能夠在無感知的情況下操縱數(shù)據(jù)庫集群,從而簡化數(shù)據(jù)庫的編程模型。然而Cobar僅實現(xiàn)了分庫功能,并未實現(xiàn)分表功能。分庫可以解決單庫IO、CPU、內(nèi)存的瓶頸,但無法解決單表數(shù)據(jù)量過大的問題。此外,Cobar是一個獨立運行的系統(tǒng),它處在應用系統(tǒng)與數(shù)據(jù)庫系統(tǒng)之間,因此增加了額外的部署復雜度,增加了運維成本。
-
為了解決上述問題,Cobar還推出了一個Cobar-Client項目,它只是一個安裝在應用程序的Jar包,并不是一個獨立運行的系統(tǒng),一定程度上降低了系統(tǒng)的復雜度。但和Cobar一樣,仍然只支持分庫,并不支持分表,也不支持讀寫分離。
-
MyCat是基于Cobar二次開發(fā)的數(shù)據(jù)庫中間件,和Cobar相比,它增加了讀寫分離的功能,并修復了Cobar的一些bug。但是,MyCat和Cobar一樣,都是一套需要獨立部署的系統(tǒng),因此會增加部署的復雜度,提高了后期系統(tǒng)運維的成本。
3. 微服務架構中的分布式事務
Cobar實現(xiàn)數(shù)據(jù)庫的透明分庫,讓開發(fā)人員能夠在無感知的情況下操縱數(shù)據(jù)庫集群,從而簡化數(shù)據(jù)庫的編程模型。然而Cobar僅實現(xiàn)了分庫功能,并未實現(xiàn)分表功能。分庫可以解決單庫IO、CPU、內(nèi)存的瓶頸,但無法解決單表數(shù)據(jù)量過大的問題。此外,Cobar是一個獨立運行的系統(tǒng),它處在應用系統(tǒng)與數(shù)據(jù)庫系統(tǒng)之間,因此增加了額外的部署復雜度,增加了運維成本。
為了解決上述問題,Cobar還推出了一個Cobar-Client項目,它只是一個安裝在應用程序的Jar包,并不是一個獨立運行的系統(tǒng),一定程度上降低了系統(tǒng)的復雜度。但和Cobar一樣,仍然只支持分庫,并不支持分表,也不支持讀寫分離。
MyCat是基于Cobar二次開發(fā)的數(shù)據(jù)庫中間件,和Cobar相比,它增加了讀寫分離的功能,并修復了Cobar的一些bug。但是,MyCat和Cobar一樣,都是一套需要獨立部署的系統(tǒng),因此會增加部署的復雜度,提高了后期系統(tǒng)運維的成本。
眾所周知,數(shù)據(jù)庫能實現(xiàn)本地事務,也就是在同一個數(shù)據(jù)庫中,你可以允許一組操作要么全都正確執(zhí)行,要么全都不執(zhí)行。這里特別強調(diào)了本地事務,也就是目前的數(shù)據(jù)庫只能支持同一個數(shù)據(jù)庫中的事務。但現(xiàn)在的系統(tǒng)往往采用微服務架構,業(yè)務系統(tǒng)擁有獨立的數(shù)據(jù)庫,因此就出現(xiàn)了跨多個數(shù)據(jù)庫的事務需求,這種事務即為“分布式事務”。那么在目前數(shù)據(jù)庫不支持跨庫事務的情況下,我們應該如何實現(xiàn)分布式事務呢?本文首先會為大家梳理分布式事務的基本概念和理論基礎,然后介紹幾種目前常用的分布式事務解決方案。廢話不多說,那就開始吧~
3.1 什么是事務?
事務由一組操作構成,我們希望這組操作能夠全部正確執(zhí)行,如果這一組操作中的任意一個步驟發(fā)生錯誤,那么就需要回滾之前已經(jīng)完成的操作。也就是同一個事務中的所有操作,要么全都正確執(zhí)行,要么全都不要執(zhí)行。
3.2 事務的四大特性 ACID
說到事務,就不得不提一下事務著名的四大特性。
-
原子性
原子性要求,事務是一個不可分割的執(zhí)行單元,事務中的所有操作要么全都執(zhí)行,要么全都不執(zhí)行。
-
一致性
一致性要求,事務在開始前和結束后,數(shù)據(jù)庫的完整性約束沒有被破壞。
-
隔離性
事務的執(zhí)行是相互獨立的,它們不會相互干擾,一個事務不會看到另一個正在運行過程中的事務的數(shù)據(jù)。
-
持久性
持久性要求,一個事務完成之后,事務的執(zhí)行結果必須是持久化保存的。即使數(shù)據(jù)庫發(fā)生崩潰,在數(shù)據(jù)庫恢復后事務提交的結果仍然不會丟失。
注意:事務只能保證數(shù)據(jù)庫的高可靠性,即數(shù)據(jù)庫本身發(fā)生問題后,事務提交后的數(shù)據(jù)仍然能恢復;而如果不是數(shù)據(jù)庫本身的故障,如硬盤損壞了,那么事務提交的數(shù)據(jù)可能就丟失了。這屬于『高可用性』的范疇。因此,事務只能保證數(shù)據(jù)庫的『高可靠性』,而『高可用性』需要整個系統(tǒng)共同配合實現(xiàn)。
3.3 事務的隔離級別
這里擴展一下,對事務的隔離性做一個詳細的解釋。
在事務的四大特性ACID中,要求的隔離性是一種嚴格意義上的隔離,也就是多個事務是串行執(zhí)行的,彼此之間不會受到任何干擾。這確實能夠完全保證數(shù)據(jù)的安全性,但在實際業(yè)務系統(tǒng)中,這種方式性能不高。因此,數(shù)據(jù)庫定義了四種隔離級別,隔離級別和數(shù)據(jù)庫的性能是呈反比的,隔離級別越低,數(shù)據(jù)庫性能越高,而隔離級別越高,數(shù)據(jù)庫性能越差。
3.3.1 事務并發(fā)執(zhí)行會出現(xiàn)的問題
我們先來看一下在不同的隔離級別下,數(shù)據(jù)庫可能會出現(xiàn)的問題:
更新丟失
當有兩個并發(fā)執(zhí)行的事務,更新同一行數(shù)據(jù),那么有可能一個事務會把另一個事務的更新覆蓋掉。 當數(shù)據(jù)庫沒有加任何鎖操作的情況下會發(fā)生。
臟讀
一個事務讀到另一個尚未提交的事務中的數(shù)據(jù)。 該數(shù)據(jù)可能會被回滾從而失效。 如果第一個事務拿著失效的數(shù)據(jù)去處理那就發(fā)生錯誤了。
不可重復讀
不可重復度的含義:一個事務對同一行數(shù)據(jù)讀了兩次,卻得到了不同的結果。它具體分為如下兩種情況:
- 虛讀:在事務1兩次讀取同一記錄的過程中,事務2對該記錄進行了修改,從而事務1第二次讀到了不一樣的記錄。
- 幻讀:事務1在兩次查詢的過程中,事務2對該表進行了插入、刪除操作,從而事務1第二次查詢的結果發(fā)生了變化。
不可重復讀 與 臟讀 的區(qū)別? 臟讀讀到的是尚未提交的數(shù)據(jù),而不可重復讀讀到的是已經(jīng)提交的數(shù)據(jù),只不過在兩次讀的過程中數(shù)據(jù)被另一個事務改過了。
3.3.2 數(shù)據(jù)庫的四種隔離級別
數(shù)據(jù)庫一共有如下四種隔離級別:
Read uncommitted 讀未提交
在該級別下,一個事務對一行數(shù)據(jù)修改的過程中,不允許另一個事務對該行數(shù)據(jù)進行修改,但允許另一個事務對該行數(shù)據(jù)讀。 因此本級別下,不會出現(xiàn)更新丟失,但會出現(xiàn)臟讀、不可重復讀。
Read committed 讀提交
在該級別下,未提交的寫事務不允許其他事務訪問該行,因此不會出現(xiàn)臟讀;但是讀取數(shù)據(jù)的事務允許其他事務的訪問該行數(shù)據(jù),因此會出現(xiàn)不可重復讀的情況。
Repeatable read 重復讀
在該級別下,讀事務禁止寫事務,但允許讀事務,因此不會出現(xiàn)同一事務兩次讀到不同的數(shù)據(jù)的情況(不可重復讀),且寫事務禁止其他一切事務。
Serializable 序列化
該級別要求所有事務都必須串行執(zhí)行,因此能避免一切因并發(fā)引起的問題,但效率很低。
隔離級別越高,越能保證數(shù)據(jù)的完整性和一致性,但是對并發(fā)性能的影響也越大。對于多數(shù)應用程序,可以優(yōu)先考慮把數(shù)據(jù)庫系統(tǒng)的隔離級別設為Read Committed。它能夠避免臟讀取,而且具有較好的并發(fā)性能。盡管它會導致不可重復讀、幻讀和第二類丟失更新這些并發(fā)問題,在可能出現(xiàn)這類問題的個別場合,可以由應用程序采用悲觀鎖或樂觀鎖來控制。
3.4 什么是分布式事務?
到此為止,所介紹的事務都是基于單數(shù)據(jù)庫的本地事務,目前的數(shù)據(jù)庫僅支持單庫事務,并不支持跨庫事務。而隨著微服務架構的普及,一個大型業(yè)務系統(tǒng)往往由若干個子系統(tǒng)構成,這些子系統(tǒng)又擁有各自獨立的數(shù)據(jù)庫。往往一個業(yè)務流程需要由多個子系統(tǒng)共同完成,而且這些操作可能需要在一個事務中完成。在微服務系統(tǒng)中,這些業(yè)務場景是普遍存在的。此時,我們就需要在數(shù)據(jù)庫之上通過某種手段,實現(xiàn)支持跨數(shù)據(jù)庫的事務支持,這也就是大家常說的“分布式事務”。
這里舉一個分布式事務的典型例子——用戶下單過程。 當我們的系統(tǒng)采用了微服務架構后,一個電商系統(tǒng)往往被拆分成如下幾個子系統(tǒng):商品系統(tǒng)、訂單系統(tǒng)、支付系統(tǒng)、積分系統(tǒng)等。整個下單的過程如下:
上述步驟2、3、4需要在一個事務中完成。對于傳統(tǒng)單體應用而言,實現(xiàn)事務非常簡單,只需將這三個步驟放在一個方法A中,再用Spring的@Transactional注解標識該方法即可。Spring通過數(shù)據(jù)庫的事務支持,保證這些步驟要么全都執(zhí)行完成,要么全都不執(zhí)行。但在這個微服務架構中,這三個步驟涉及三個系統(tǒng),涉及三個數(shù)據(jù)庫,此時我們必須在數(shù)據(jù)庫和應用系統(tǒng)之間,通過某項黑科技,實現(xiàn)分布式事務的支持。
3.5 CAP理論
CAP理論說的是:在一個分布式系統(tǒng)中,最多只能滿足C、A、P中的兩個需求。
CAP的含義:
-
C:Consistency 一致性
同一數(shù)據(jù)的多個副本是否實時相同。
-
A:Availability 可用性
可用性:一定時間內(nèi) & 系統(tǒng)返回一個明確的結果 則稱為該系統(tǒng)可用。
-
P:Partition tolerance 分區(qū)容錯性
將同一服務分布在多個系統(tǒng)中,從而保證某一個系統(tǒng)宕機,仍然有其他系統(tǒng)提供相同的服務。
CAP理論告訴我們,在分布式系統(tǒng)中,C、A、P三個條件中我們最多只能選擇兩個。那么問題來了,究竟選擇哪兩個條件較為合適呢?
對于一個業(yè)務系統(tǒng)來說,可用性和分區(qū)容錯性是必須要滿足的兩個條件,并且這兩者是相輔相成的。業(yè)務系統(tǒng)之所以使用分布式系統(tǒng),主要原因有兩個:
-
提升整體性能
當業(yè)務量猛增,單個服務器已經(jīng)無法滿足我們的業(yè)務需求的時候,就需要使用分布式系統(tǒng),使用多個節(jié)點提供相同的功能,從而整體上提升系統(tǒng)的性能,這就是使用分布式系統(tǒng)的第一個原因。
-
實現(xiàn)分區(qū)容錯性
單一節(jié)點 或 多個節(jié)點處于相同的網(wǎng)絡環(huán)境下,那么會存在一定的風險,萬一該機房斷電、該地區(qū)發(fā)生自然災害,那么業(yè)務系統(tǒng)就全面癱瘓了。為了防止這一問題,采用分布式系統(tǒng),將多個子系統(tǒng)分布在不同的地域、不同的機房中,從而保證系統(tǒng)高可用性。
這說明分區(qū)容錯性是分布式系統(tǒng)的根本,如果分區(qū)容錯性不能滿足,那使用分布式系統(tǒng)將失去意義。
此外,可用性對業(yè)務系統(tǒng)也尤為重要。在大談用戶體驗的今天,如果業(yè)務系統(tǒng)時常出現(xiàn)“系統(tǒng)異常”、響應時間過長等情況,這使得用戶對系統(tǒng)的好感度大打折扣,在互聯(lián)網(wǎng)行業(yè)競爭激烈的今天,相同領域的競爭者不甚枚舉,系統(tǒng)的間歇性不可用會立馬導致用戶流向競爭對手。因此,我們只能通過犧牲一致性來換取系統(tǒng)的可用性和分區(qū)容錯性。這也就是下面要介紹的BASE理論。
3.6 BASE理論
CAP理論告訴我們一個悲慘但不得不接受的事實——我們只能在C、A、P中選擇兩個條件。而對于業(yè)務系統(tǒng)而言,我們往往選擇犧牲一致性來換取系統(tǒng)的可用性和分區(qū)容錯性。不過這里要指出的是,所謂的“犧牲一致性”并不是完全放棄數(shù)據(jù)一致性,而是犧牲強一致性換取弱一致性。下面來介紹下BASE理論。
- BA:Basic Available 基本可用
- 整個系統(tǒng)在某些不可抗力的情況下,仍然能夠保證“可用性”,即一定時間內(nèi)仍然能夠返回一個明確的結果。只不過“基本可用”和“高可用”的區(qū)別是:
- “一定時間”可以適當延長 當舉行大促時,響應時間可以適當延長
- 給部分用戶返回一個降級頁面 給部分用戶直接返回一個降級頁面,從而緩解服務器壓力。但要注意,返回降級頁面仍然是返回明確結果。
- 整個系統(tǒng)在某些不可抗力的情況下,仍然能夠保證“可用性”,即一定時間內(nèi)仍然能夠返回一個明確的結果。只不過“基本可用”和“高可用”的區(qū)別是:
- S:Soft State:柔性狀態(tài) 同一數(shù)據(jù)的不同副本的狀態(tài),可以不需要實時一致。
- E:Eventual Consisstency:最終一致性 同一數(shù)據(jù)的不同副本的狀態(tài),可以不需要實時一致,但一定要保證經(jīng)過一定時間后仍然是一致的。
3.7 酸堿平衡
ACID能夠保證事務的強一致性,即數(shù)據(jù)是實時一致的。這在本地事務中是沒有問題的,在分布式事務中,強一致性會極大影響分布式系統(tǒng)的性能,因此分布式系統(tǒng)中遵循BASE理論即可。但分布式系統(tǒng)的不同業(yè)務場景對一致性的要求也不同。如交易場景下,就要求強一致性,此時就需要遵循ACID理論,而在注冊成功后發(fā)送短信驗證碼等場景下,并不需要實時一致,因此遵循BASE理論即可。因此要根據(jù)具體業(yè)務場景,在ACID和BASE之間尋求平衡。
3.8 分布式事務協(xié)議
下面介紹幾種實現(xiàn)分布式事務的協(xié)議。
3.8.1 兩階段提交協(xié)議 2PC
分布式系統(tǒng)的一個難點是如何保證架構下多個節(jié)點在進行事務性操作的時候保持一致性。為實現(xiàn)這個目的,二階段提交算法的成立基于以下假設:
- 該分布式系統(tǒng)中,存在一個節(jié)點作為協(xié)調(diào)者(Coordinator),其他節(jié)點作為參與者(Cohorts)。且節(jié)點之間可以進行網(wǎng)絡通信。
- 所有節(jié)點都采用預寫式日志,且日志被寫入后即被保持在可靠的存儲設備上,即使節(jié)點損壞不會導致日志數(shù)據(jù)的消失。
- 所有節(jié)點不會永久性損壞,即使損壞后仍然可以恢復。
1. 第一階段(投票階段):
2. 第二階段(提交執(zhí)行階段):
當協(xié)調(diào)者節(jié)點從所有參與者節(jié)點獲得的相應消息都為"同意"時:
如果任一參與者節(jié)點在第一階段返回的響應消息為"中止",或者 協(xié)調(diào)者節(jié)點在第一階段的詢問超時之前無法獲取所有參與者節(jié)點的響應消息時:
不管最后結果如何,第二階段都會結束當前事務。
二階段提交看起來確實能夠提供原子性的操作,但是不幸的事,二階段提交還是有幾個缺點的:
為此,Dale Skeen和Michael Stonebraker在“A Formal Model of Crash Recovery in a Distributed System”中提出了三階段提交協(xié)議(3PC)。
3.8.2 三階段提交協(xié)議 3PC
與兩階段提交不同的是,三階段提交有兩個改動點。
- 引入超時機制。同時在協(xié)調(diào)者和參與者中都引入超時機制。
- 在第一階段和第二階段中插入一個準備階段。保證了在最后提交階段之前各參與節(jié)點的狀態(tài)是一致的。
也就是說,除了引入超時機制之外,3PC把2PC的準備階段再次一分為二,這樣三階段提交就有CanCommit、PreCommit、DoCommit三個階段。
1. CanCommit階段
3PC的CanCommit階段其實和2PC的準備階段很像。協(xié)調(diào)者向參與者發(fā)送commit請求,參與者如果可以提交就返回Yes響應,否則返回No響應。
事務詢問
協(xié)調(diào)者向參與者發(fā)送CanCommit請求。詢問是否可以執(zhí)行事務提交操作。然后開始等待參與者的響應。
響應反饋
參與者接到CanCommit請求之后,正常情況下,如果其自身認為可以順利執(zhí)行事務,則返回Yes響應,并進入預備狀態(tài)。否則反饋No
2. PreCommit階段
協(xié)調(diào)者根據(jù)參與者的反應情況來決定是否可以記性事務的PreCommit操作。根據(jù)響應情況,有以下兩種可能。 假如協(xié)調(diào)者從所有的參與者獲得的反饋都是Yes響應,那么就會執(zhí)行事務的預執(zhí)行。
發(fā)送預提交請求
協(xié)調(diào)者向參與者發(fā)送PreCommit請求,并進入Prepared階段。
事務預提交
參與者接收到PreCommit請求后,會執(zhí)行事務操作,并將undo和redo信息記錄到事務日志中。
響應反饋
如果參與者成功的執(zhí)行了事務操作,則返回ACK響應,同時開始等待最終指令。
假如有任何一個參與者向協(xié)調(diào)者發(fā)送了No響應,或者等待超時之后,協(xié)調(diào)者都沒有接到參與者的響應,那么就執(zhí)行事務的中斷。
發(fā)送中斷請求
協(xié)調(diào)者向所有參與者發(fā)送abort請求。
中斷事務
參與者收到來自協(xié)調(diào)者的abort請求之后(或超時之后,仍未收到協(xié)調(diào)者的請求),執(zhí)行事務的中斷。
3. doCommit階段 該階段進行真正的事務提交,也可以分為以下兩種情況。
該階段進行真正的事務提交,也可以分為以下兩種情況。
3.1 執(zhí)行提交
發(fā)送提交請求
協(xié)調(diào)接收到參與者發(fā)送的ACK響應,那么他將從預提交狀態(tài)進入到提交狀態(tài)。并向所有參與者發(fā)送doCommit請求。
事務提交
參與者接收到doCommit請求之后,執(zhí)行正式的事務提交。并在完成事務提交之后釋放所有事務資源。
響應反饋
事務提交完之后,向協(xié)調(diào)者發(fā)送Ack響應。
完成事務
協(xié)調(diào)者接收到所有參與者的ack響應之后,完成事務。
3.2 中斷事務 協(xié)調(diào)者沒有接收到參與者發(fā)送的ACK響應(可能是接受者發(fā)送的不是ACK響應,也可能響應超時),那么就會執(zhí)行中斷事務。
發(fā)送中斷請求
協(xié)調(diào)者向所有參與者發(fā)送abort請求
事務回滾
參與者接收到abort請求之后,利用其在階段二記錄的undo信息來執(zhí)行事務的回滾操作,并在完成回滾之后釋放所有的事務資源。
反饋結果
參與者完成事務回滾之后,向協(xié)調(diào)者發(fā)送ACK消息
中斷事務
協(xié)調(diào)者接收到參與者反饋的ACK消息之后,執(zhí)行事務的中斷。
3.9 分布式事務的解決方案
分布式事務的解決方案有如下幾種:
- 全局消息
- 基于可靠消息服務的分布式事務
- TCC
- 最大努力通知
3.9.1 方案1:全局事務(DTP模型)
全局事務基于DTP模型實現(xiàn)。DTP是由X/Open組織提出的一種分布式事務模型——X/Open Distributed Transaction Processing Reference Model。它規(guī)定了要實現(xiàn)分布式事務,需要三種角色:
-
AP:Application 應用系統(tǒng)
它就是我們開發(fā)的業(yè)務系統(tǒng),在我們開發(fā)的過程中,可以使用資源管理器提供的事務接口來實現(xiàn)分布式事務。
-
TM:Transaction Manager 事務管理器
- 分布式事務的實現(xiàn)由事務管理器來完成,它會提供分布式事務的操作接口供我們的業(yè)務系統(tǒng)調(diào)用。這些接口稱為TX接口。
- 事務管理器還管理著所有的資源管理器,通過它們提供的XA接口來同一調(diào)度這些資源管理器,以實現(xiàn)分布式事務。
- DTP只是一套實現(xiàn)分布式事務的規(guī)范,并沒有定義具體如何實現(xiàn)分布式事務,TM可以采用2PC、3PC、Paxos等協(xié)議實現(xiàn)分布式事務。
-
RM:Resource Manager 資源管理器
- 能夠提供數(shù)據(jù)服務的對象都可以是資源管理器,比如:數(shù)據(jù)庫、消息中間件、緩存等。大部分場景下,數(shù)據(jù)庫即為分布式事務中的資源管理器。
- 資源管理器能夠提供單數(shù)據(jù)庫的事務能力,它們通過XA接口,將本數(shù)據(jù)庫的提交、回滾等能力提供給事務管理器調(diào)用,以幫助事務管理器實現(xiàn)分布式的事務管理。
- XA是DTP模型定義的接口,用于向事務管理器提供該資源管理器(該數(shù)據(jù)庫)的提交、回滾等能力。
- DTP只是一套實現(xiàn)分布式事務的規(guī)范,RM具體的實現(xiàn)是由數(shù)據(jù)庫廠商來完成的。
3.9.2 方案2:基于可靠消息服務的分布式事務
這種實現(xiàn)分布式事務的方式需要通過消息中間件來實現(xiàn)。假設有A和B兩個系統(tǒng),分別可以處理任務A和任務B。此時系統(tǒng)A中存在一個業(yè)務流程,需要將任務A和任務B在同一個事務中處理。下面來介紹基于消息中間件來實現(xiàn)這種分布式事務。
?
?
- 在系統(tǒng)A處理任務A前,首先向消息中間件發(fā)送一條消息
- 消息中間件收到后將該條消息持久化,但并不投遞。此時下游系統(tǒng)B仍然不知道該條消息的存在。
- 消息中間件持久化成功后,便向系統(tǒng)A返回一個確認應答;
- 系統(tǒng)A收到確認應答后,則可以開始處理任務A;
- 任務A處理完成后,向消息中間件發(fā)送Commit請求。該請求發(fā)送完成后,對系統(tǒng)A而言,該事務的處理過程就結束了,此時它可以處理別的任務了。 但commit消息可能會在傳輸途中丟失,從而消息中間件并不會向系統(tǒng)B投遞這條消息,從而系統(tǒng)就會出現(xiàn)不一致性。這個問題由消息中間件的事務回查機制完成,下文會介紹。
- 消息中間件收到Commit指令后,便向系統(tǒng)B投遞該消息,從而觸發(fā)任務B的執(zhí)行;
- 當任務B執(zhí)行完成后,系統(tǒng)B向消息中間件返回一個確認應答,告訴消息中間件該消息已經(jīng)成功消費,此時,這個分布式事務完成。
上述過程可以得出如下幾個結論:
上述過程中,如果任務A處理失敗,那么需要進入回滾流程,如下圖所示:
?
- 若系統(tǒng)A在處理任務A時失敗,那么就會向消息中間件發(fā)送Rollback請求。和發(fā)送Commit請求一樣,系統(tǒng)A發(fā)完之后便可以認為回滾已經(jīng)完成,它便可以去做其他的事情。
- 消息中間件收到回滾請求后,直接將該消息丟棄,而不投遞給系統(tǒng)B,從而不會觸發(fā)系統(tǒng)B的任務B。
此時系統(tǒng)又處于一致性狀態(tài),因為任務A和任務B都沒有執(zhí)行。
上面所介紹的Commit和Rollback都屬于理想情況,但在實際系統(tǒng)中,Commit和Rollback指令都有可能在傳輸途中丟失。那么當出現(xiàn)這種情況的時候,消息中間件是如何保證數(shù)據(jù)一致性呢?——答案就是超時詢問機制。
?
?
系統(tǒng)A除了實現(xiàn)正常的業(yè)務流程外,還需提供一個事務詢問的接口,供消息中間件調(diào)用。當消息中間件收到一條事務型消息后便開始計時,如果到了超時時間也沒收到系統(tǒng)A發(fā)來的Commit或Rollback指令的話,就會主動調(diào)用系統(tǒng)A提供的事務詢問接口詢問該系統(tǒng)目前的狀態(tài)。該接口會返回三種結果:
-
提交
若獲得的狀態(tài)是“提交”,則將該消息投遞給系統(tǒng)B。
-
回滾
若獲得的狀態(tài)是“回滾”,則直接將條消息丟棄。
-
處理中
若獲得的狀態(tài)是“處理中”,則繼續(xù)等待。
消息中間件的超時詢問機制能夠防止上游系統(tǒng)因在傳輸過程中丟失Commit/Rollback指令而導致的系統(tǒng)不一致情況,而且能降低上游系統(tǒng)的阻塞時間,上游系統(tǒng)只要發(fā)出Commit/Rollback指令后便可以處理其他任務,無需等待確認應答。而Commit/Rollback指令丟失的情況通過超時詢問機制來彌補,這樣大大降低上游系統(tǒng)的阻塞時間,提升系統(tǒng)的并發(fā)度。
下面來說一說消息投遞過程的可靠性保證。 當上游系統(tǒng)執(zhí)行完任務并向消息中間件提交了Commit指令后,便可以處理其他任務了,此時它可以認為事務已經(jīng)完成,接下來消息中間件**一定會保證消息被下游系統(tǒng)成功消費掉!**那么這是怎么做到的呢?這由消息中間件的投遞流程來保證。
消息中間件向下游系統(tǒng)投遞完消息后便進入阻塞等待狀態(tài),下游系統(tǒng)便立即進行任務的處理,任務處理完成后便向消息中間件返回應答。消息中間件收到確認應答后便認為該事務處理完畢!
如果消息在投遞過程中丟失,或消息的確認應答在返回途中丟失,那么消息中間件在等待確認應答超時之后就會重新投遞,直到下游消費者返回消費成功響應為止。當然,一般消息中間件可以設置消息重試的次數(shù)和時間間隔,比如:當?shù)谝淮瓮哆f失敗后,每隔五分鐘重試一次,一共重試3次。如果重試3次之后仍然投遞失敗,那么這條消息就需要人工干預。
?
?
?
有的同學可能要問:消息投遞失敗后為什么不回滾消息,而是不斷嘗試重新投遞?
這就涉及到整套分布式事務系統(tǒng)的實現(xiàn)成本問題。 我們知道,當系統(tǒng)A將向消息中間件發(fā)送Commit指令后,它便去做別的事情了。如果此時消息投遞失敗,需要回滾的話,就需要讓系統(tǒng)A事先提供回滾接口,這無疑增加了額外的開發(fā)成本,業(yè)務系統(tǒng)的復雜度也將提高。對于一個業(yè)務系統(tǒng)的設計目標是,在保證性能的前提下,最大限度地降低系統(tǒng)復雜度,從而能夠降低系統(tǒng)的運維成本。
不知大家是否發(fā)現(xiàn),上游系統(tǒng)A向消息中間件提交Commit/Rollback消息采用的是異步方式,也就是當上游系統(tǒng)提交完消息后便可以去做別的事情,接下來提交、回滾就完全交給消息中間件來完成,并且完全信任消息中間件,認為它一定能正確地完成事務的提交或回滾。然而,消息中間件向下游系統(tǒng)投遞消息的過程是同步的。也就是消息中間件將消息投遞給下游系統(tǒng)后,它會阻塞等待,等下游系統(tǒng)成功處理完任務返回確認應答后才取消阻塞等待。為什么這兩者在設計上是不一致的呢?
首先,上游系統(tǒng)和消息中間件之間采用異步通信是為了提高系統(tǒng)并發(fā)度。業(yè)務系統(tǒng)直接和用戶打交道,用戶體驗尤為重要,因此這種異步通信方式能夠極大程度地降低用戶等待時間。此外,異步通信相對于同步通信而言,沒有了長時間的阻塞等待,因此系統(tǒng)的并發(fā)性也大大增加。但異步通信可能會引起Commit/Rollback指令丟失的問題,這就由消息中間件的超時詢問機制來彌補。
那么,消息中間件和下游系統(tǒng)之間為什么要采用同步通信呢?
異步能提升系統(tǒng)性能,但隨之會增加系統(tǒng)復雜度;而同步雖然降低系統(tǒng)并發(fā)度,但實現(xiàn)成本較低。因此,在對并發(fā)度要求不是很高的情況下,或者服務器資源較為充裕的情況下,我們可以選擇同步來降低系統(tǒng)的復雜度。 我們知道,消息中間件是一個獨立于業(yè)務系統(tǒng)的第三方中間件,它不和任何業(yè)務系統(tǒng)產(chǎn)生直接的耦合,它也不和用戶產(chǎn)生直接的關聯(lián),它一般部署在獨立的服務器集群上,具有良好的可擴展性,所以不必太過于擔心它的性能,如果處理速度無法滿足我們的要求,可以增加機器來解決。而且,即使消息中間件處理速度有一定的延遲那也是可以接受的,因為前面所介紹的BASE理論就告訴我們了,我們追求的是最終一致性,而非實時一致性,因此消息中間件產(chǎn)生的時延導致事務短暫的不一致是可以接受的。
3.9.3 方案3:最大努力通知(定期校對)
最大努力通知也被稱為定期校對,其實在方案二中已經(jīng)包含,這里再單獨介紹,主要是為了知識體系的完整性。這種方案也需要消息中間件的參與,其過程如下:
?
?
- 上游系統(tǒng)在完成任務后,向消息中間件同步地發(fā)送一條消息,確保消息中間件成功持久化這條消息,然后上游系統(tǒng)可以去做別的事情了;
- 消息中間件收到消息后負責將該消息同步投遞給相應的下游系統(tǒng),并觸發(fā)下游系統(tǒng)的任務執(zhí)行;
- 當下游系統(tǒng)處理成功后,向消息中間件反饋確認應答,消息中間件便可以將該條消息刪除,從而該事務完成。
上面是一個理想化的過程,但在實際場景中,往往會出現(xiàn)如下幾種意外情況:
對于第一種情況,消息中間件具有重試機制,我們可以在消息中間件中設置消息的重試次數(shù)和重試時間間隔,對于網(wǎng)絡不穩(wěn)定導致的消息投遞失敗的情況,往往重試幾次后消息便可以成功投遞,如果超過了重試的上限仍然投遞失敗,那么消息中間件不再投遞該消息,而是記錄在失敗消息表中,消息中間件需要提供失敗消息的查詢接口,下游系統(tǒng)會定期查詢失敗消息,并將其消費,這就是所謂的“定期校對”。
如果重復投遞和定期校對都不能解決問題,往往是因為下游系統(tǒng)出現(xiàn)了嚴重的錯誤,此時就需要人工干預。
對于第二種情況,需要在上游系統(tǒng)中建立消息重發(fā)機制。可以在上游系統(tǒng)建立一張本地消息表,并將 任務處理過程 和 向本地消息表中插入消息 這兩個步驟放在一個本地事務中完成。如果向本地消息表插入消息失敗,那么就會觸發(fā)回滾,之前的任務處理結果就會被取消。如果這量步都執(zhí)行成功,那么該本地事務就完成了。接下來會有一個專門的消息發(fā)送者不斷地發(fā)送本地消息表中的消息,如果發(fā)送失敗它會返回重試。當然,也要給消息發(fā)送者設置重試的上限,一般而言,達到重試上限仍然發(fā)送失敗,那就意味著消息中間件出現(xiàn)嚴重的問題,此時也只有人工干預才能解決問題。
對于不支持事務型消息的消息中間件,如果要實現(xiàn)分布式事務的話,就可以采用這種方式。它能夠通過重試機制+定期校對實現(xiàn)分布式事務,但相比于第二種方案,它達到數(shù)據(jù)一致性的周期較長,而且還需要在上游系統(tǒng)中實現(xiàn)消息重試發(fā)布機制,以確保消息成功發(fā)布給消息中間件,這無疑增加了業(yè)務系統(tǒng)的開發(fā)成本,使得業(yè)務系統(tǒng)不夠純粹,并且這些額外的業(yè)務邏輯無疑會占用業(yè)務系統(tǒng)的硬件資源,從而影響性能。
因此,盡量選擇支持事務型消息的消息中間件來實現(xiàn)分布式事務,如RocketMQ。
3.9.4 方案4:TCC(兩階段型、補償型)
TCC即為Try Confirm Cancel,它屬于補償型分布式事務。顧名思義,TCC實現(xiàn)分布式事務一共有三個步驟:
- Try:嘗試待執(zhí)行的業(yè)務
- 這個過程并未執(zhí)行業(yè)務,只是完成所有業(yè)務的一致性檢查,并預留好執(zhí)行所需的全部資源
- Confirm:執(zhí)行業(yè)務
- 這個過程真正開始執(zhí)行業(yè)務,由于Try階段已經(jīng)完成了一致性檢查,因此本過程直接執(zhí)行,而不做任何檢查。并且在執(zhí)行的過程中,會使用到Try階段預留的業(yè)務資源。
- Cancel:取消執(zhí)行的業(yè)務
- 若業(yè)務執(zhí)行失敗,則進入Cancel階段,它會釋放所有占用的業(yè)務資源,并回滾Confirm階段執(zhí)行的操作。
下面以一個轉(zhuǎn)賬的例子來解釋下TCC實現(xiàn)分布式事務的過程。
假設用戶A用他的賬戶余額給用戶B發(fā)一個100元的紅包,并且余額系統(tǒng)和紅包系統(tǒng)是兩個獨立的系統(tǒng)。
-
Try
- 創(chuàng)建一條轉(zhuǎn)賬流水,并將流水的狀態(tài)設為交易中
- 將用戶A的賬戶中扣除100元(預留業(yè)務資源)
- Try成功之后,便進入Confirm階段
- Try過程發(fā)生任何異常,均進入Cancel階段
-
Confirm
- 向B用戶的紅包賬戶中增加100元
- 將流水的狀態(tài)設為交易已完成
- Confirm過程發(fā)生任何異常,均進入Cancel階段
- Confirm過程執(zhí)行成功,則該事務結束
-
Cancel
- 將用戶A的賬戶增加100元
- 將流水的狀態(tài)設為交易失敗
在傳統(tǒng)事務機制中,業(yè)務邏輯的執(zhí)行和事務的處理,是在不同的階段由不同的部件來完成的:業(yè)務邏輯部分訪問資源實現(xiàn)數(shù)據(jù)存儲,其處理是由業(yè)務系統(tǒng)負責;事務處理部分通過協(xié)調(diào)資源管理器以實現(xiàn)事務管理,其處理由事務管理器來負責。二者沒有太多交互的地方,所以,傳統(tǒng)事務管理器的事務處理邏輯,僅需要著眼于事務完成(commit/rollback)階段,而不必關注業(yè)務執(zhí)行階段。
TCC全局事務必須基于RM本地事務來實現(xiàn)全局事務
TCC服務是由Try/Confirm/Cancel業(yè)務構成的, 其Try/Confirm/Cancel業(yè)務在執(zhí)行時,會訪問資源管理器(Resource Manager,下文簡稱RM)來存取數(shù)據(jù)。這些存取操作,必須要參與RM本地事務,以使其更改的數(shù)據(jù)要么都commit,要么都rollback。
這一點不難理解,考慮一下如下場景:
?
?
假設圖中的服務B沒有基于RM本地事務(以RDBS為例,可通過設置auto-commit為true來模擬),那么一旦[B:Try]操作中途執(zhí)行失敗,TCC事務框架后續(xù)決定回滾全局事務時,該[B:Cancel]則需要判斷[B:Try]中哪些操作已經(jīng)寫到DB、哪些操作還沒有寫到DB:假設[B:Try]業(yè)務有5個寫庫操作,[B:Cancel]業(yè)務則需要逐個判斷這5個操作是否生效,并將生效的操作執(zhí)行反向操作。
不幸的是,由于[B:Cancel]業(yè)務也有n(0<=n<=5)個反向的寫庫操作,此時一旦[B:Cancel]也中途出錯,則后續(xù)的[B:Cancel]執(zhí)行任務更加繁重。因為,相比第一次[B:Cancel]操作,后續(xù)的[B:Cancel]操作還需要判斷先前的[B:Cancel]操作的n(0<=n<=5)個寫庫中哪幾個已經(jīng)執(zhí)行、哪幾個還沒有執(zhí)行,這就涉及到了冪等性問題。而對冪等性的保障,又很可能還需要涉及額外的寫庫操作,該寫庫操作又會因為沒有RM本地事務的支持而存在類似問題。。。可想而知,如果不基于RM本地事務,TCC事務框架是無法有效的管理TCC全局事務的。
反之,基于RM本地事務的TCC事務,這種情況則會很容易處理:[B:Try]操作中途執(zhí)行失敗,TCC事務框架將其參與RM本地事務直接rollback即可。后續(xù)TCC事務框架決定回滾全局事務時,在知道“[B:Try]操作涉及的RM本地事務已經(jīng)rollback”的情況下,根本無需執(zhí)行[B:Cancel]操作。
換句話說,基于RM本地事務實現(xiàn)TCC事務框架時,一個TCC型服務的cancel業(yè)務要么執(zhí)行,要么不執(zhí)行,不需要考慮部分執(zhí)行的情況。
TCC事務框架應該提供Confirm/Cancel服務的冪等性保障
一般認為,服務的冪等性,是指針對同一個服務的多次(n>1)請求和對它的單次(n=1)請求,二者具有相同的副作用。
在TCC事務模型中,Confirm/Cancel業(yè)務可能會被重復調(diào)用,其原因很多。比如,全局事務在提交/回滾時會調(diào)用各TCC服務的Confirm/Cancel業(yè)務邏輯。執(zhí)行這些Confirm/Cancel業(yè)務時,可能會出現(xiàn)如網(wǎng)絡中斷的故障而使得全局事務不能完成。因此,故障恢復機制后續(xù)仍然會重新提交/回滾這些未完成的全局事務,這樣就會再次調(diào)用參與該全局事務的各TCC服務的Confirm/Cancel業(yè)務邏輯。
既然Confirm/Cancel業(yè)務可能會被多次調(diào)用,就需要保障其冪等性。 那么,應該由TCC事務框架來提供冪等性保障?還是應該由業(yè)務系統(tǒng)自行來保障冪等性呢? 個人認為,應該是由TCC事務框架來提供冪等性保障。如果僅僅只是極個別服務存在這個問題的話,那么由業(yè)務系統(tǒng)來負責也是可以的;然而,這是一類公共問題,毫無疑問,所有TCC服務的Confirm/Cancel業(yè)務存在冪等性問題。TCC服務的公共問題應該由TCC事務框架來解決;而且,考慮一下由業(yè)務系統(tǒng)來負責冪等性需要考慮的問題,就會發(fā)現(xiàn),這無疑增大了業(yè)務系統(tǒng)的復雜度。
4. 服務部署
當我們完成業(yè)務代碼的開發(fā)后,就需要進入部署階段。在部署過程中,我們將會引入持續(xù)集成、持續(xù)交付、持續(xù)部署,并且闡述如何在微服務中使用他們。
4.1 持續(xù)集成、持續(xù)部署、持續(xù)交付
在介紹這三個概念之前,我們首先來了解下使用了這三個概念之后的軟件開發(fā)流程,如下圖所示:
?
首先是代碼的開發(fā)階段,當代碼完成開發(fā)后需要提交至代碼倉庫,此時需要對代碼進行編譯、打包,打包后的產(chǎn)物被稱為“構建物”,如:對Web項目打包之后生成的war包、jar包就是一種構建物。此時的構建物雖然沒有語法錯誤,但其質(zhì)量是無法保證的,必須經(jīng)過一系列嚴格的測試之后才能具有部署到生產(chǎn)環(huán)境的資格。我們一般會給系統(tǒng)分配多套環(huán)境,如開發(fā)環(huán)境、測試環(huán)境、預發(fā)環(huán)境、生產(chǎn)環(huán)境。每套環(huán)境都有它測試標準,當構建物完成了一套環(huán)境的測試,并達到交付標準時,就會自動進入下一個環(huán)境。構建物依次會經(jīng)過這四套環(huán)境,構建物每完成一套環(huán)境的驗證,就具備交付給下一套環(huán)境的資格。當完成預發(fā)環(huán)境的驗證后,就具備的上線的資格。
測試和交付過程是相互伴隨的,每一套環(huán)境都有各自的測試標準。如在開發(fā)環(huán)境中,當代碼提交后需要通過編譯、打包生成構建物,在編譯的過程中會對代碼進行單元測試,如果有任何測試用例沒通過,整個構建流程就會被中止。此時開發(fā)人員需要立即修復問題,并重新提交代碼、重新編譯打包。
當單元測試通過之后,構建物就具備了進入測試環(huán)境的資格,此時它會被自動部署到測試環(huán)境,進行新一輪的測試。在測試環(huán)境中,一般需要完成接口測試和人工測試。接口測試由自動化腳本完成,這個過程完成后還需要人工進行功能性測試。人工測試完成后,需要手動觸發(fā)進入下一個階段。
此時構建物將會被部署到預發(fā)環(huán)境。預發(fā)環(huán)境是一種“類生產(chǎn)環(huán)境”,它和生產(chǎn)環(huán)境的服務器配置需要保持高度一致。在預發(fā)環(huán)境中,一般需要對構建物進行性能測試,了解其性能指標是否能滿足上線的要求。當通過預發(fā)驗證后,構建物已經(jīng)具備了上線的資格,此時它可以隨時上線。
上述過程涵蓋了持續(xù)集成、持續(xù)交付、持續(xù)部署,那么下面我們就從理論角度來介紹這三個概念。
4.1.1 持續(xù)集成
“集成”指的是修改后/新增的代碼向代碼倉庫合并的過程,而“持續(xù)集成”指的是代碼高頻率合并。這樣有什么好處呢?大家不妨想一想,如果我們集成代碼的頻率變高了,那么每次集成的代碼量就會變少,由于每次集成的時候都會進行單元測試,從而當出現(xiàn)問題的時候問題出現(xiàn)的范圍就被縮小的,這樣就能快速定位到出錯的地方,尋找問題就更容易了。此外,頻繁集成能夠使問題盡早地暴露,這樣解決問題的成本也就越低。因為在軟件測試中有這樣一條定律,時間和bug修復的成本成正比,也就是時間越長,bug修復的成本也就越大。所以持續(xù)集成能夠盡早發(fā)現(xiàn)問題,并能夠及時修復問題,這對于軟件的質(zhì)量是非常重要的。
4.1.2 持續(xù)部署
“持續(xù)部署”指的是當存在多套環(huán)境時,當構建物成完上一套環(huán)境的測試后,自動部署到下一套環(huán)境并進行一系列的測試,直到構建物滿足上線的要求為止。
4.1.3 持續(xù)交付
當系統(tǒng)通過了所有的測試之后,就具備了部署到生產(chǎn)環(huán)境的資格,這個過程也就被稱為“交付”。“持續(xù)交付”指的是每個版本的構建物都具有上線的資格,這就要求每當代碼庫中有新的版本后,都需要自動觸發(fā)構建、測試、部署、交付等一系列流程,當構建物在某個階段的測試未通過時,就需要開發(fā)人員立即解決這個問題,并重新構建,從而保證每個版本的構建物都具備上線的資格,可以隨時部署到生產(chǎn)環(huán)境中。
4.2 微服務與持續(xù)集成
當我們了解了持續(xù)集成后,下面來介紹微服務如何與持續(xù)集成相整合。當我們對系統(tǒng)進行了微服務化后,原本單一的系統(tǒng)被拆分成多個課獨立運行的微服務。單服務系統(tǒng)的持續(xù)集成較為簡單,代碼庫、構建和構建物之間都是一對一的關系。然而,當我們將系統(tǒng)微服務化后,持續(xù)集成就變得復雜了。下面介紹兩種在微服務中使用持續(xù)集成的方法,分別是單庫多構建和多庫多構建,并依次介紹這兩種方式的優(yōu)缺點及使用場景。
4.2.1 單庫多構建
“單庫”指的是單個代碼倉庫,即整個系統(tǒng)的多個模塊的代碼均由一個代碼倉庫維護。“多構建”指的是持續(xù)集成平臺中的構建項目會有多個,每個構建都會生成一個構建物,如下如所示:
?
?
在這種持續(xù)集成的模式中,整個項目的所有代碼均在同一個代碼倉庫中維護。但在持續(xù)集成平臺中,每一項服務都有各自獨立的構建,從而持續(xù)集成平臺能夠為每一項服務產(chǎn)出各自的構建物。
這種持續(xù)集成的模式在微服務架構中顯然是不合理的。首先,一個系統(tǒng)的可能會有很多服務構成,如果將這些服務的代碼均在同一個代碼倉庫中維護,那么一個程序員在開發(fā)服務A代碼的時候很有可能會因為疏忽,修改了服務B的代碼,此時服務B構建之后就會存在安全隱患,如果這個問題在服務B上線前被發(fā)現(xiàn),那么還好,但無疑增加了額外的工作量;但如果這個問題及其隱諱,導致之前的測試用例沒有覆蓋到,從而服務B會帶著這個問題進入生產(chǎn)環(huán)境,這可能會給企業(yè)帶來巨大的損失。所以,在微服務架構中,盡量選擇多庫多構建模式來實現(xiàn)持續(xù)集成,它將帶來更大的安全性。
雖然這種模式不合理,但它也有存在的必要性,當我們在項目建設初期的時候,這種模式會給我們帶來更多的便利性。因為項目在建設初期,服務之間的邊界往往是比較模糊的,而且需要經(jīng)過一段時間的演化才能夠構建出穩(wěn)定的邊界。所以如果在項目建設初期直接使用微服務架構,那么服務邊界頻繁地調(diào)整會極大增加系統(tǒng)開發(fā)的復雜度,你要知道,在多個系統(tǒng)之間調(diào)整邊界比在單個系統(tǒng)的多個模塊之間調(diào)整邊界的成本要高很多。所以在項目建設初期,我們可以使用單服務結構,服務內(nèi)部采用模塊作為未來各個微服務的邊界,當系統(tǒng)演化出較為清晰、穩(wěn)定的邊界后再將系統(tǒng)拆分成多個微服務。此時代碼在同一個代碼倉庫中維護是合理的,這也符合敏捷開發(fā)中快速迭代的理念。
4.2.2 多庫多構建
?
?
當系我們的系統(tǒng)擁有了穩(wěn)定、清晰的邊界后,就可以將系統(tǒng)向微服務架構演進。與此同時,持續(xù)集成模式也可以從單庫多構建向多庫多構建演進。
在多庫多構建模式中,每項服務都有各自獨立的代碼倉庫,代碼倉庫之間互不干擾。開發(fā)團隊只需關注屬于自己的某幾項服務的代碼倉庫即可。每一項服務都有各自獨立的構建。這種方式邏輯清晰,維護成本較低,而且能避免單庫多構建模式中出現(xiàn)的影響其他服務的問題。
4.3 微服務構建物
持續(xù)集成平臺對源碼編譯、大包后生成的產(chǎn)物稱為“構建物”。根據(jù)打包的粒度不同,可以將構建物分為如下三種:平臺構建物、操作系統(tǒng)構建物和鏡像構建物。
4.3.1 平臺構建物
平臺構建物指的是由某一特定平臺生成的構建物,比如JVM平臺生成的Jar包、War包,Python生成的egg等都屬于平臺構建物。但平臺構建物運行需要部署在特定的容器中,如war需要運行在Servlet容器中,而Servlet容器又依賴的JVM環(huán)境。所以若要部署平臺構建物,則需要先給它們提供好運行所需的環(huán)境。
4.3.2 操作系統(tǒng)構建物
操作系統(tǒng)構建物是將系統(tǒng)打包成一個操作系統(tǒng)可執(zhí)行程序,,如CentOS的RPM包、Windows的MSI包等。這些安裝包可以在操作系統(tǒng)上直接安裝運行。但和平臺構建物相同的是,操作系統(tǒng)構建物往往也需要依賴于其他環(huán)境,所以也需要在部署之前搭建好安裝包所需的依賴。此外,配置操作系統(tǒng)構建物的復雜度較大,構建的成本較高,所以一般不使用這種方式,這里僅作介紹。
4.3.3 鏡像構建物
平臺構建物和操作系統(tǒng)構建物都有一個共同的缺點就是需要安裝構建物運行的額外依賴,增加部署復雜度,而鏡像構建物能很好地解決這個問題。
我們可以把鏡像理解成一個小型操作系統(tǒng),這個操作系統(tǒng)中包含了系統(tǒng)運行所需的所有依賴,并將系統(tǒng)也部署在這個“操作系統(tǒng)”中。這樣當持續(xù)集成平臺構建完這個鏡像后,就可以直接運行它,無需任何依賴的安裝,從而極大簡化了構建的復雜度。但是,鏡像往往比較龐大,構建鏡像的過程也較長,從而當我們將生成的鏡像從持續(xù)集成服務器發(fā)布到部署服務器的時間將會很長,這無疑降低了部署的效率。不過好在Docker的出現(xiàn)解決了這一問題。持續(xù)集成平臺在構建過程中并不需要生成一個鏡像,而只需生成一個鏡像的Dockerfile文件即可。Dockerfile文件用命令定義了鏡像所包含的內(nèi)容,以及鏡像創(chuàng)建的過程。從而持續(xù)集成服務器只需將這個體積較小的鏡像文件發(fā)布到部署服務器上即可。然后部署服務器會通過docker build命令基于這個Dockerfile文件創(chuàng)建鏡像,并創(chuàng)建該鏡像的容器,從而完成服務的部署。
相對于平臺構建物和操作系統(tǒng)構建物而言,鏡像構建物在部署時不需要安裝額外的環(huán)境依賴,它把環(huán)境依賴的配置都在持續(xù)集成平臺構建Dockerfile文件時完成,從而簡化了部署的過程。
轉(zhuǎn)載于:https://www.cnblogs.com/weilingfeng/p/11165620.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結
- 上一篇: docker修改容器名字
- 下一篇: linus下centos7防火墙设置