微服務就是一些可獨立運行、可協同工做的小的服務。程序員
從概念中咱們能夠提取三個關鍵詞:可獨立運行、可協同工做、小。這三個詞高度歸納了微服務的核心特性。下面咱們就對這三個詞做詳細解釋。算法
當你再也不年輕,你還能作什麼? sql
隨着大數據時代的到來,業務系統的數據量日益增大,數據存儲能力逐漸成爲影響系統性能的瓶頸。目前主流的關係型數據庫單表存儲上限爲1000萬條記錄,而這一存儲能力顯然已經沒法知足大數據背景下的業務系統存儲要求了。隨着微服務架構、分佈式存儲等概念的出現,數據存儲問題也漸漸迎來了起色。而數據分片是目前解決海量數據持久化存儲與高效查詢的一種重要手段。數據分庫分表的過程在系統設計階段完成,要求系統設計人員根據系統預期的業務量,將將來可能出現瓶頸的數據庫、數據表按照必定規則拆分紅多個庫、多張表。這些數據庫和數據表須要部署在不一樣的服務器上,從而將數據讀寫壓力分攤至集羣中的各個節點,提高數據庫總體處理能力,避免出現讀寫瓶頸的現象。docker
目前數據分片的方式一共有兩種:離散分片和連續分片。數據庫
數據庫擴展一共有四種分配方式,分別是:垂直分庫、垂直分表、水平分表、水平數據分片。每一種策略都有各自的適用場景。編程
垂直分庫 垂直分庫便是將一個完整的數據庫根據業務功能拆分紅多個獨立的數據庫,這些數據庫能夠運行在不一樣的服務器上,從而提高數據庫總體的數據讀寫性能。這種方式在微服務架構中很是經常使用。微服務架構的核心思想是將一個完整的應用按照業務功能拆分紅多個可獨立運行的子系統,這些子系統稱爲「微服務」,各個服務之間經過RPC接口通訊,這樣的結構使得系統耦合度更低、更易於擴展。垂直分庫的理念與微服務的理念不謀而合,能夠將本來完整的數據按照微服務拆分系統的方式,拆分紅多個獨立的數據庫,使得每一個微服務系統都有各自獨立的數據庫,從而能夠避免單個數據庫節點壓力過大,影響系統的總體性能,以下圖所示。 緩存
垂直分表 垂直分表若是一張表的字段很是多,那麼頗有可能會引發數據的跨頁存儲,這會形成數據庫額外的性能開銷,而垂直分表能夠解決這個問題。垂直分表就是將一張表中不經常使用的字段拆分到另外一張表中,從而保證第一章表中的字段較少,避免出現數據庫跨頁存儲的問題,從而提高查詢效率。而另外一張表中的數據經過外鍵與第一張表進行關聯,以下圖所示。 安全
水平分表 若是一張表中的記錄數過多(超過1000萬條記錄),那麼會對數據庫的讀寫性能產生較大的影響,雖然此時仍然可以正確地讀寫,但讀寫的速度已經到了業務沒法忍受的地步,此時就須要使用水平分表來解決這個問題。水平分表是將一張含有不少記錄數的表水平切分,拆分紅幾張結構相同的表。舉個例子,假設一張訂單表目前存儲了2000萬條訂單的數據,致使數據讀寫效率極低。此時能夠採用水平分表的方式,將訂單表拆分紅100張結構相同的訂單表,分別叫作order_一、order_2……、order_100。而後能夠根據訂單所屬用戶的id進行哈希取模後均勻地存儲在這100張表中,從而每張表中只存儲了20萬條訂單記錄,極大提高了訂單的讀寫效率,以下圖所示。固然,若是拆分出來的表都存儲在同一個數據庫節點上,那麼當請求量過大的時候,畢竟單臺服務器的處理能力是有限的,數據庫仍然會成爲系統的瓶頸,因此爲了解決這個問題,就出現了水平數據分片的解決方案。 bash
水平分庫分表 水平數據分片與數據分片區別在於:水平數據分片首先將數據表進行水平拆分,而後按照某一分片規則存儲在多臺數據庫服務器上。從而將單庫的壓力分攤到了多庫上,從而避免由於數據庫硬件資源有限致使的數據庫性能瓶頸,以下圖所示。 服務器
目前經常使用的數據分片策略有兩種,分別是連續分片和離散分片。
衆所周知,數據庫能實現本地事務,也就是在同一個數據庫中,你能夠容許一組操做要麼全都正確執行,要麼全都不執行。這裏特別強調了本地事務,也就是目前的數據庫只能支持同一個數據庫中的事務。但如今的系統每每採用微服務架構,業務系統擁有獨立的數據庫,所以就出現了跨多個數據庫的事務需求,這種事務即爲「分佈式事務」。那麼在目前數據庫不支持跨庫事務的狀況下,咱們應該如何實現分佈式事務呢?本文首先會爲你們梳理分佈式事務的基本概念和理論基礎,而後介紹幾種目前經常使用的分佈式事務解決方案。
事務由一組操做構成,咱們但願這組操做可以所有正確執行,若是這一組操做中的任意一個步驟發生錯誤,那麼就須要回滾以前已經完成的操做。也就是同一個事務中的全部操做,要麼全都正確執行,要麼全都不要執行。
說到事務,就不得不提一下事務著名的四大特性。
注意:事務只能保證數據庫的高可靠性,即數據庫自己發生問題後,事務提交後的數據仍然能恢復;而若是不是數據庫自己的故障,如硬盤損壞了,那麼事務提交的數據可能就丟失了。這屬於『高可用性』的範疇。所以,事務只能保證數據庫的『高可靠性』,而『高可用性』須要整個系統共同配合實現。
在事務的四大特性ACID中,要求的隔離性是一種嚴格意義上的隔離,也就是多個事務是串行執行的,彼此之間不會受到任何干擾。這確實可以徹底保證數據的安全性,但在實際業務系統中,這種方式性能不高。所以,數據庫定義了四種隔離級別,隔離級別和數據庫的性能是呈反比的,隔離級別越低,數據庫性能越高,而隔離級別越高,數據庫性能越差。
3.1 事務併發執行會出現的問題
咱們先來看一下在不一樣的隔離級別下,數據庫可能會出現的問題:
當有兩個併發執行的事務,更新同一行數據,那麼有可能一個事務會把另外一個事務的更新覆蓋掉。當數據庫沒有加任何鎖操做的狀況下會發生。
一個事務讀到另外一個還沒有提交的事務中的數據。該數據可能會被回滾從而失效。 若是第一個事務拿着失效的數據去處理那就發生錯誤了。
不可重複度的含義:一個事務對同一行數據讀了兩次,卻獲得了不一樣的結果。它具體分爲以下兩種狀況:
虛讀:在事務1兩次讀取同一記錄的過程當中,事務2對該記錄進行了修改,從而事務1第二次讀到了不同的記錄。 幻讀:事務1在兩次查詢的過程當中,事務2對該表進行了插入、刪除操做,從而事務1第二次查詢的結果發生了變化。
不可重複讀 與 髒讀 的區別? 髒讀讀到的是還沒有提交的數據,而不可重複讀讀到的是已經提交的數據,只不過在兩次讀的過程當中數據被另外一個事務改過了。
3.2 數據庫的四種隔離級別
數據庫一共有以下四種隔離級別:
1.Read uncommitted 讀未提交 在該級別下,一個事務對一行數據修改的過程當中,不容許另外一個事務對該行數據進行修改,但容許另外一個事務對該行數據讀。 所以本級別下,不會出現更新丟失,但會出現髒讀、不可重複讀。
2.Read committed 讀提交 在該級別下,未提交的寫事務不容許其餘事務訪問該行,所以不會出現髒讀;可是讀取數據的事務容許其餘事務的訪問該行數據,所以會出現不可重複讀的狀況。
3.Repeatable read 重複讀 在該級別下,讀事務禁止寫事務,但容許讀事務,所以不會出現同一事務兩次讀到不一樣的數據的狀況(不可重複讀),且寫事務禁止其餘一切事務。
4.Serializable 序列化 該級別要求全部事務都必須串行執行,所以能避免一切因併發引發的問題,但效率很低。
隔離級別越高,越能保證數據的完整性和一致性,可是對併發性能的影響也越大。對於多數應用程序,能夠優先考慮把數據庫系統的隔離級別設爲Read Committed。它可以避免髒讀取,並且具備較好的併發性能。儘管它會致使不可重複讀、幻讀和第二類丟失更新這些併發問題,在可能出現這類問題的個別場合,能夠由應用程序採用悲觀鎖或樂觀鎖來控制。
到此爲止,所介紹的事務都是基於單數據庫的本地事務,目前的數據庫僅支持單庫事務,並不支持跨庫事務。而隨着微服務架構的普及,一個大型業務系統每每由若干個子系統構成,這些子系統又擁有各自獨立的數據庫。每每一個業務流程須要由多個子系統共同完成,並且這些操做可能須要在一個事務中完成。在微服務系統中,這些業務場景是廣泛存在的。此時,咱們就須要在數據庫之上經過某種手段,實現支持跨數據庫的事務支持,這也就是你們常說的「分佈式事務」。 這裏舉一個分佈式事務的典型例子——用戶下單過程。 當咱們的系統採用了微服務架構後,一個電商系統每每被拆分紅以下幾個子系統:商品系統、訂單系統、支付系統、積分系統等。整個下單的過程以下:
上述步驟二、三、4須要在一個事務中完成。對於傳統單體應用而言,實現事務很是簡單,只需將這三個步驟放在一個方法A中,再用Spring的@Transactional註解標識該方法便可。Spring經過數據庫的事務支持,保證這些步驟要麼全都執行完成,要麼全都不執行。但在這個微服務架構中,這三個步驟涉及三個系統,涉及三個數據庫,此時咱們必須在數據庫和應用系統之間,經過某項黑科技,實現分佈式事務的支持。
CAP理論說的是:在一個分佈式系統中,最多隻能知足C、A、P中的兩個需求。 CAP的含義:
CAP理論告訴咱們,在分佈式系統中,C、A、P三個條件中咱們最多隻能選擇兩個。那麼問題來了,究竟選擇哪兩個條件較爲合適呢? 對於一個業務系統來講,可用性和分區容錯性是必需要知足的兩個條件,而且這二者是相輔相成的。業務系統之因此使用分佈式系統,主要緣由有兩個:
提高總體性能 當業務量猛增,單個服務器已經沒法知足咱們的業務需求的時候,就須要使用分佈式系統,使用多個節點提供相同的功能,從而總體上提高系統的性能,這就是使用分佈式系統的第一個緣由。
實現分區容錯性 單一節點 或 多個節點處於相同的網絡環境下,那麼會存在必定的風險,萬一該機房斷電、該地區發生天然災害,那麼業務系統就全面癱瘓了。爲了防止這一問題,採用分佈式系統,將多個子系統分佈在不一樣的地域、不一樣的機房中,從而保證系統高可用性。
這說明分區容錯性是分佈式系統的根本,若是分區容錯性不能知足,那使用分佈式系統將失去意義。 此外,可用性對業務系統也尤其重要。在大談用戶體驗的今天,若是業務系統時常出現「系統異常」、響應時間過長等狀況,這使得用戶對系統的好感度大打折扣,在互聯網行業競爭激烈的今天,相同領域的競爭者不甚枚舉,系統的間歇性不可用會立馬致使用戶流向競爭對手。所以,咱們只能經過犧牲一致性來換取系統的可用性和分區容錯性。這也就是下面要介紹的BASE理論。
CAP理論告訴咱們一個悲慘但不得不接受的事實——咱們只能在C、A、P中選擇兩個條件。而對於業務系統而言,咱們每每選擇犧牲一致性來換取系統的可用性和分區容錯性。不過這裏要指出的是,所謂的「犧牲一致性」並非徹底放棄數據一致性,而是犧牲強一致性換取弱一致性。下面來介紹下BASE理論。
BA:Basic Available 基本可用 整個系統在某些不可抗力的狀況下,仍然可以保證「可用性」,即必定時間內仍然可以返回一個明確的結果。只不過「基本可用」和「高可用」的區別是:
S:Soft State:柔性狀態 同一數據的不一樣副本的狀態,能夠不須要實時一致。 E:Eventual Consisstency:最終一致性 同一數據的不一樣副本的狀態,能夠不須要實時一致,但必定要保證通過必定時間後仍然是一致的。
ACID可以保證事務的強一致性,即數據是實時一致的。這在本地事務中是沒有問題的,在分佈式事務中,強一致性會極大影響分佈式系統的性能,所以分佈式系統中遵循BASE理論便可。但分佈式系統的不一樣業務場景對一致性的要求也不一樣。如交易場景下,就要求強一致性,此時就須要遵循ACID理論,而在註冊成功後發送短信驗證碼等場景下,並不須要實時一致,所以遵循BASE理論便可。所以要根據具體業務場景,在ACID和BASE之間尋求平衡。
下面介紹幾種實現分佈式事務的協議。
8.1 兩階段提交協議 2PC 分佈式系統的一個難點是如何保證架構下多個節點在進行事務性操做的時候保持一致性。爲實現這個目的,二階段提交算法的成立基於如下假設:
一、第一階段(投票階段):
二、第二階段(提交執行階段):
當協調者節點從全部參與者節點得到的相應消息都爲"贊成"時:
若是任一參與者節點在第一階段返回的響應消息爲"停止",或者 協調者節點在第一階段的詢問超時以前沒法獲取全部參與者節點的響應消息時:
無論最後結果如何,第二階段都會結束當前事務。
二階段提交看起來確實可以提供原子性的操做,可是不幸的事,二階段提交仍是有幾個缺點的:
爲此,Dale Skeen和Michael Stonebraker在「A Formal Model of Crash Recovery in a Distributed System」中提出了三階段提交協議(3PC)。
8.2 三階段提交協議 3PC 與兩階段提交不一樣的是,三階段提交有兩個改動點。
也就是說,除了引入超時機制以外,3PC把2PC的準備階段再次一分爲二,這樣三階段提交就有CanCommit、PreCommit、DoCommit三個階段。
CanCommit階段 3PC的CanCommit階段其實和2PC的準備階段很像。協調者向參與者發送commit請求,參與者若是能夠提交就返回Yes響應,不然返回No響應。
PreCommit階段 協調者根據參與者的反應狀況來決定是否能夠記性事務的PreCommit操做。根據響應狀況,有如下兩種可能。 假如協調者從全部的參與者得到的反饋都是Yes響應,那麼就會執行事務的預執行。
假若有任何一個參與者向協調者發送了No響應,或者等待超時以後,協調者都沒有接到參與者的響應,那麼就執行事務的中斷。
doCommit階段 該階段進行真正的事務提交,也能夠分爲如下兩種狀況。 一、執行提交
二、中斷事務 協調者沒有接收到參與者發送的ACK響應(多是接受者發送的不是ACK響應,也可能響應超時),那麼就會執行中斷事務。
分佈式事務的解決方案有以下幾種:
9.1 方案1:全局事務(DTP模型)
全局事務基於DTP模型實現。DTP是由X/Open組織提出的一種分佈式事務模型——X/Open Distributed Transaction Processing Reference Model。它規定了要實現分佈式事務,須要三種角色:
它就是咱們開發的業務系統,在咱們開發的過程當中,能夠使用資源管理器提供的事務接口來實現分佈式事務。
- 分佈式事務的實現由事務管理器來完成,它會提供分佈式事務的操做接口供咱們的業務系統調用。這些接口稱爲TX接口。
- 事務管理器還管理着全部的資源管理器,經過它們提供的XA接口來同一調度這些資源管理器,以實現分佈式事務。
- DTP只是一套實現分佈式事務的規範,並無定義具體如何實現分佈式事務,TM能夠採用2PC、3PC、Paxos等協議實現分佈式事務。
複製代碼
9.2 方案2:基於可靠消息服務的分佈式事務
這種實現分佈式事務的方式須要經過消息中間件來實現。假設有A和B兩個系統,分別能夠處理任務A和任務B。此時系統A中存在一個業務流程,須要將任務A和任務B在同一個事務中處理。下面來介紹基於消息中間件來實現這種分佈式事務。
上述過程能夠得出以下幾個結論:
上述過程當中,若是任務A處理失敗,那麼須要進入回滾流程,以下圖所示:
此時系統又處於一致性狀態,由於任務A和任務B都沒有執行。
上面所介紹的Commit和Rollback都屬於理想狀況,但在實際系統中,Commit和Rollback指令都有可能在傳輸途中丟失。那麼當出現這種狀況的時候,消息中間件是如何保證數據一致性呢?——答案就是超時詢問機制。
系統A除了實現正常的業務流程外,還需提供一個事務詢問的接口,供消息中間件調用。當消息中間件收到一條事務型消息後便開始計時,若是到了超時時間也沒收到系統A發來的Commit或Rollback指令的話,就會主動調用系統A提供的事務詢問接口詢問該系統目前的狀態。該接口會返回三種結果:
若得到的狀態是「提交」,則將該消息投遞給系統B。
若得到的狀態是「回滾」,則直接將條消息丟棄。
若得到的狀態是「處理中」,則繼續等待。
消息中間件的超時詢問機制可以防止上游系統因在傳輸過程當中丟失Commit/Rollback指令而致使的系統不一致狀況,並且能下降上游系統的阻塞時間,上游系統只要發出Commit/Rollback指令後即可以處理其餘任務,無需等待確認應答。而Commit/Rollback指令丟失的狀況經過超時詢問機制來彌補,這樣大大下降上游系統的阻塞時間,提高系統的併發度。
下面來講一說消息投遞過程的可靠性保證。 當上遊系統執行完任務並向消息中間件提交了Commit指令後,即可以處理其餘任務了,此時它能夠認爲事務已經完成,接下來消息中間件必定會保證消息被下游系統成功消費掉!那麼這是怎麼作到的呢?這由消息中間件的投遞流程來保證。
消息中間件向下遊系統投遞完消息後便進入阻塞等待狀態,下游系統便當即進行任務的處理,任務處理完成後便向消息中間件返回應答。消息中間件收到確認應答後便認爲該事務處理完畢!
若是消息在投遞過程當中丟失,或消息的確認應答在返回途中丟失,那麼消息中間件在等待確認應答超時以後就會從新投遞,直到下游消費者返回消費成功響應爲止。固然,通常消息中間件能夠設置消息重試的次數和時間間隔,好比:當第一次投遞失敗後,每隔五分鐘重試一次,一共重試3次。若是重試3次以後仍然投遞失敗,那麼這條消息就須要人工干預。
有的同窗可能要問:消息投遞失敗後爲何不回滾消息,而是不斷嘗試從新投遞?
這就涉及到整套分佈式事務系統的實現成本問題。咱們知道,當系統A將向消息中間件發送Commit指令後,它便去作別的事情了。若是此時消息投遞失敗,須要回滾的話,就須要讓系統A事先提供回滾接口,這無疑增長了額外的開發成本,業務系統的複雜度也將提升。對於一個業務系統的設計目標是,在保證性能的前提下,最大限度地下降系統複雜度,從而可以下降系統的運維成本。
不知你們是否發現,上游系統A向消息中間件提交Commit/Rollback消息採用的是異步方式,也就是當上遊系統提交完消息後即可以去作別的事情,接下來提交、回滾就徹底交給消息中間件來完成,而且徹底信任消息中間件,認爲它必定能正確地完成事務的提交或回滾。然而,消息中間件向下遊系統投遞消息的過程是同步的。也就是消息中間件將消息投遞給下游系統後,它會阻塞等待,等下游系統成功處理完任務返回確認應答後才取消阻塞等待。爲何這二者在設計上是不一致的呢?
首先,上游系統和消息中間件之間採用異步通訊是爲了提升系統併發度。業務系統直接和用戶打交道,用戶體驗尤其重要,所以這種異步通訊方式可以極大程度地下降用戶等待時間。此外,異步通訊相對於同步通訊而言,沒有了長時間的阻塞等待,所以系統的併發性也大大增長。但異步通訊可能會引發Commit/Rollback指令丟失的問題,這就由消息中間件的超時詢問機制來彌補。
那麼,消息中間件和下游系統之間爲何要採用同步通訊呢?
異步能提高系統性能,但隨之會增長系統複雜度;而同步雖然下降系統併發度,但實現成本較低。所以,在對併發度要求不是很高的狀況下,或者服務器資源較爲充裕的狀況下,咱們能夠選擇同步來下降系統的複雜度。 咱們知道,消息中間件是一個獨立於業務系統的第三方中間件,它不和任何業務系統產生直接的耦合,它也不和用戶產生直接的關聯,它通常部署在獨立的服務器集羣上,具備良好的可擴展性,因此沒必要太過於擔憂它的性能,若是處理速度沒法知足咱們的要求,能夠增長機器來解決。並且,即便消息中間件處理速度有必定的延遲那也是能夠接受的,由於前面所介紹的BASE理論就告訴咱們了,咱們追求的是最終一致性,而非實時一致性,所以消息中間件產生的時延致使事務短暫的不一致是能夠接受的。
9.3 方案3:最大努力通知(按期校對)
最大努力通知也被稱爲按期校對,其實在方案二中已經包含,這裏再單獨介紹,主要是爲了知識體系的完整性。這種方案也須要消息中間件的參與,其過程以下:
上面是一個理想化的過程,但在實際場景中,每每會出現以下幾種意外狀況:
對於第一種狀況,消息中間件具備重試機制,咱們能夠在消息中間件中設置消息的重試次數和重試時間間隔,對於網絡不穩定致使的消息投遞失敗的狀況,每每重試幾回後消息即可以成功投遞,若是超過了重試的上限仍然投遞失敗,那麼消息中間件再也不投遞該消息,而是記錄在失敗消息表中,消息中間件須要提供失敗消息的查詢接口,下游系統會按期查詢失敗消息,並將其消費,這就是所謂的「按期校對」。
若是重複投遞和按期校對都不能解決問題,每每是由於下游系統出現了嚴重的錯誤,此時就須要人工干預。
對於第二種狀況,須要在上游系統中創建消息重發機制。能夠在上游系統創建一張本地消息表,並將 任務處理過程 和 向本地消息表中插入消息 這兩個步驟放在一個本地事務中完成。若是向本地消息表插入消息失敗,那麼就會觸發回滾,以前的任務處理結果就會被取消。若是這量步都執行成功,那麼該本地事務就完成了。接下來會有一個專門的消息發送者不斷地發送本地消息表中的消息,若是發送失敗它會返回重試。固然,也要給消息發送者設置重試的上限,通常而言,達到重試上限仍然發送失敗,那就意味着消息中間件出現嚴重的問題,此時也只有人工干預才能解決問題。
對於不支持事務型消息的消息中間件,若是要實現分佈式事務的話,就能夠採用這種方式。它可以經過重試機制+按期校對實現分佈式事務,但相比於第二種方案,它達到數據一致性的週期較長,並且還須要在上游系統中實現消息重試發佈機制,以確保消息成功發佈給消息中間件,這無疑增長了業務系統的開發成本,使得業務系統不夠純粹,而且這些額外的業務邏輯無疑會佔用業務系統的硬件資源,從而影響性能。
所以,儘可能選擇支持事務型消息的消息中間件來實現分佈式事務,如RocketMQ。
9.4 方案4:TCC(兩階段型、補償型)
TCC即爲Try Confirm Cancel,它屬於補償型分佈式事務。顧名思義,TCC實現分佈式事務一共有三個步驟:
這個過程並未執行業務,只是完成全部業務的一致性檢查,並預留好執行所需的所有資源
這個過程真正開始執行業務,因爲Try階段已經完成了一致性檢查,所以本過程直接執行,而不作任何檢查。而且在執行的過程當中,會使用到Try階段預留的業務資源。
若業務執行失敗,則進入Cancel階段,它會釋放全部佔用的業務資源,並回滾Confirm階段執行的操做。
下面以一個轉帳的例子來解釋下TCC實現分佈式事務的過程。
假設用戶A用他的帳戶餘額給用戶B發一個100元的紅包,而且餘額系統和紅包系統是兩個獨立的系統。
在傳統事務機制中,業務邏輯的執行和事務的處理,是在不一樣的階段由不一樣的部件來完成的:業務邏輯部分訪問資源實現數據存儲,其處理是由業務系統負責;事務處理部分經過協調資源管理器以實現事務管理,其處理由事務管理器來負責。兩者沒有太多交互的地方,因此,傳統事務管理器的事務處理邏輯,僅須要着眼於事務完成(commit/rollback)階段,而沒必要關注業務執行階段。
TCC全局事務必須基於RM本地事務來實現全局事務
TCC服務是由Try/Confirm/Cancel業務構成的,其Try/Confirm/Cancel業務在執行時,會訪問資源管理器(Resource Manager,下文簡稱RM)來存取數據。這些存取操做,必需要參與RM本地事務,以使其更改的數據要麼都commit,要麼都rollback。 這一點不難理解,考慮一下以下場景:
假設圖中的服務B沒有基於RM本地事務(以RDBS爲例,可經過設置auto-commit爲true來模擬),那麼一旦[B:Try]操做中途執行失敗,TCC事務框架後續決定回滾全局事務時,該[B:Cancel]則須要判斷[B:Try]中哪些操做已經寫到DB、哪些操做尚未寫到DB:假設[B:Try]業務有5個寫庫操做,[B:Cancel]業務則須要逐個判斷這5個操做是否生效,並將生效的操做執行反向操做。
不幸的是,因爲[B:Cancel]業務也有n(0<=n<=5)個反向的寫庫操做,此時一旦[B:Cancel]也中途出錯,則後續的[B:Cancel]執行任務更加繁重。由於,相比第一次[B:Cancel]操做,後續的[B:Cancel]操做還須要判斷先前的[B:Cancel]操做的n(0<=n<=5)個寫庫中哪幾個已經執行、哪幾個尚未執行,這就涉及到了冪等性問題。而對冪等性的保障,又極可能還須要涉及額外的寫庫操做,該寫庫操做又會由於沒有RM本地事務的支持而存在相似問題。。。可想而知,若是不基於RM本地事務,TCC事務框架是沒法有效的管理TCC全局事務的。
反之,基於RM本地事務的TCC事務,這種狀況則會很容易處理:[B:Try]操做中途執行失敗,TCC事務框架將其參與RM本地事務直接rollback便可。後續TCC事務框架決定回滾全局事務時,在知道「[B:Try]操做涉及的RM本地事務已經rollback」的狀況下,根本無需執行[B:Cancel]操做。
換句話說,基於RM本地事務實現TCC事務框架時,一個TCC型服務的cancel業務要麼執行,要麼不執行,不須要考慮部分執行的狀況。
TCC事務框架應該提供Confirm/Cancel服務的冪等性保障
通常認爲,服務的冪等性,是指針對同一個服務的屢次(n>1)請求和對它的單次(n=1)請求,兩者具備相同的反作用。
在TCC事務模型中,Confirm/Cancel業務可能會被重複調用,其緣由不少。好比,全局事務在提交/回滾時會調用各TCC服務的Confirm/Cancel業務邏輯。執行這些Confirm/Cancel業務時,可能會出現如網絡中斷的故障而使得全局事務不能完成。所以,故障恢復機制後續仍然會從新提交/回滾這些未完成的全局事務,這樣就會再次調用參與該全局事務的各TCC服務的Confirm/Cancel業務邏輯。
既然Confirm/Cancel業務可能會被屢次調用,就須要保障其冪等性。那麼,應該由TCC事務框架來提供冪等性保障?仍是應該由業務系統自行來保障冪等性呢?我的認爲,應該是由TCC事務框架來提供冪等性保障。若是僅僅只是極個別服務存在這個問題的話,那麼由業務系統來負責也是能夠的;然而,這是一類公共問題,毫無疑問,全部TCC服務的Confirm/Cancel業務存在冪等性問題。TCC服務的公共問題應該由TCC事務框架來解決;並且,考慮一下由業務系統來負責冪等性須要考慮的問題,就會發現,這無疑增大了業務系統的複雜度。
當咱們完成業務代碼的開發後,就須要進入部署階段。在部署過程當中,咱們將會引入持續集成、持續交付、持續部署,而且闡述如何在微服務中使用他們。
在介紹這三個概念以前,咱們首先來了解下使用了這三個概念以後的軟件開發流程,以下圖所示:
首先是代碼的開發階段,當代碼完成開發後須要提交至代碼倉庫,此時須要對代碼進行編譯、打包,打包後的產物被稱爲「構建物」,如:對Web項目打包以後生成的war包、jar包就是一種構建物。此時的構建物雖然沒有語法錯誤,但其質量是沒法保證的,必須通過一系列嚴格的測試以後才能具備部署到生產環境的資格。咱們通常會給系統分配多套環境,如開發環境、測試環境、預發環境、生產環境。每套環境都有它測試標準,當構建物完成了一套環境的測試,並達到交付標準時,就會自動進入下一個環境。構建物依次會通過這四套環境,構建物每完成一套環境的驗證,就具有交付給下一套環境的資格。當完成預發環境的驗證後,就具有的上線的資格。
測試和交付過程是相互伴隨的,每一套環境都有各自的測試標準。如在開發環境中,當代碼提交後須要經過編譯、打包生成構建物,在編譯的過程當中會對代碼進行單元測試,若是有任何測試用例沒經過,整個構建流程就會被停止。此時開發人員須要當即修復問題,並從新提交代碼、從新編譯打包。
當單元測試經過以後,構建物就具有了進入測試環境的資格,此時它會被自動部署到測試環境,進行新一輪的測試。在測試環境中,通常須要完成接口測試和人工測試。接口測試由自動化腳本完成,這個過程完成後還須要人工進行功能性測試。人工測試完成後,須要手動觸發進入下一個階段。
此時構建物將會被部署到預發環境。預發環境是一種「類生產環境」,它和生產環境的服務器配置須要保持高度一致。在預發環境中,通常須要對構建物進行性能測試,瞭解其性能指標是否能知足上線的要求。當經過預發驗證後,構建物已經具有了上線的資格,此時它能夠隨時上線。
上述過程涵蓋了持續集成、持續交付、持續部署,那麼下面咱們就從理論角度來介紹這三個概念。
1.1 持續集成 「集成」指的是修改後/新增的代碼向代碼倉庫合併的過程,而「持續集成」指的是代碼高頻率合併。這樣有什麼好處呢?你們不妨想想,若是咱們集成代碼的頻率變高了,那麼每次集成的代碼量就會變少,因爲每次集成的時候都會進行單元測試,從而當出現問題的時候問題出現的範圍就被縮小的,這樣就能快速定位到出錯的地方,尋找問題就更容易了。此外,頻繁集成可以使問題儘早地暴露,這樣解決問題的成本也就越低。由於在軟件測試中有這樣一條定律,時間和bug修復的成本成正比,也就是時間越長,bug修復的成本也就越大。因此持續集成可以儘早發現問題,並可以及時修復問題,這對於軟件的質量是很是重要的。
1.2 持續部署 「持續部署」指的是當存在多套環境時,當構建物成完上一套環境的測試後,自動部署到下一套環境並進行一系列的測試,直到構建物知足上線的要求爲止。
1.3 持續交付 當系統經過了全部的測試以後,就具有了部署到生產環境的資格,這個過程也就被稱爲「交付」。「持續交付」指的是每一個版本的構建物都具備上線的資格,這就要求每當代碼庫中有新的版本後,都須要自動觸發構建、測試、部署、交付等一系列流程,當構建物在某個階段的測試未經過時,就須要開發人員當即解決這個問題,並從新構建,從而保證每一個版本的構建物都具有上線的資格,能夠隨時部署到生產環境中。
當咱們瞭解了持續集成後,下面來介紹微服務如何與持續集成相整合。當咱們對系統進行了微服務化後,本來單一的系統被拆分紅多個課獨立運行的微服務。單服務系統的持續集成較爲簡單,代碼庫、構建和構建物之間都是一對一的關係。然而,當咱們將系統微服務化後,持續集成就變得複雜了。下面介紹兩種在微服務中使用持續集成的方法,分別是單庫多構建和多庫多構建,並依次介紹這兩種方式的優缺點及使用場景。
2.1 單庫多構建 「單庫」指的是單個代碼倉庫,即整個系統的多個模塊的代碼均由一個代碼倉庫維護。「多構建」指的是持續集成平臺中的構建項目會有多個,每一個構建都會生成一個構建物,以下如所示:
在這種持續集成的模式中,整個項目的全部代碼均在同一個代碼倉庫中維護。但在持續集成平臺中,每一項服務都有各自獨立的構建,從而持續集成平臺可以爲每一項服務產出各自的構建物。
這種持續集成的模式在微服務架構中顯然是不合理的。首先,一個系統的可能會有不少服務構成,若是將這些服務的代碼均在同一個代碼倉庫中維護,那麼一個程序員在開發服務A代碼的時候頗有可能會由於疏忽,修改了服務B的代碼,此時服務B構建以後就會存在安全隱患,若是這個問題在服務B上線前被發現,那麼還好,但無疑增長了額外的工做量;但若是這個問題及其隱諱,致使以前的測試用例沒有覆蓋到,從而服務B會帶着這個問題進入生產環境,這可能會給企業帶來巨大的損失。因此,在微服務架構中,儘可能選擇多庫多構建模式來實現持續集成,它將帶來更大的安全性。
雖然這種模式不合理,但它也有存在的必要性,當咱們在項目建設初期的時候,這種模式會給咱們帶來更多的便利性。由於項目在建設初期,服務之間的邊界每每是比較模糊的,並且須要通過一段時間的演化纔可以構建出穩定的邊界。因此若是在項目建設初期直接使用微服務架構,那麼服務邊界頻繁地調整會極大增長系統開發的複雜度,你要知道,在多個系統之間調整邊界比在單個系統的多個模塊之間調整邊界的成本要高不少。因此在項目建設初期,咱們能夠使用單服務結構,服務內部採用模塊做爲將來各個微服務的邊界,當系統演化出較爲清晰、穩定的邊界後再將系統拆分紅多個微服務。此時代碼在同一個代碼倉庫中維護是合理的,這也符合敏捷開發中快速迭代的理念。
2.2 多庫多構建
當系咱們的系統擁有了穩定、清晰的邊界後,就能夠將系統向微服務架構演進。與此同時,持續集成模式也能夠從單庫多構建向多庫多構建演進。
在多庫多構建模式中,每項服務都有各自獨立的代碼倉庫,代碼倉庫之間互不干擾。開發團隊只需關注屬於本身的某幾項服務的代碼倉庫便可。每一項服務都有各自獨立的構建。這種方式邏輯清晰,維護成本較低,並且能避免單庫多構建模式中出現的影響其餘服務的問題。
持續集成平臺對源碼編譯、大包後生成的產物稱爲「構建物」。根據打包的粒度不一樣,能夠將構建物分爲以下三種:平臺構建物、操做系統構建物和鏡像構建物。
3.1 平臺構建物 平臺構建物指的是由某一特定平臺生成的構建物,好比JVM平臺生成的Jar包、War包,Python生成的egg等都屬於平臺構建物。但平臺構建物運行須要部署在特定的容器中,如war須要運行在Servlet容器中,而Servlet容器又依賴的JVM環境。因此若要部署平臺構建物,則須要先給它們提供好運行所需的環境。
3.2 操做系統構建物 操做系統構建物是將系統打包成一個操做系統可執行程序,,如CentOS的RPM包、Windows的MSI包等。這些安裝包能夠在操做系統上直接安裝運行。但和平臺構建物相同的是,操做系統構建物每每也須要依賴於其餘環境,因此也須要在部署以前搭建好安裝包所需的依賴。此外,配置操做系統構建物的複雜度較大,構建的成本較高,因此通常不使用這種方式,這裏僅做介紹。
3.3 鏡像構建物 平臺構建物和操做系統構建物都有一個共同的缺點就是須要安裝構建物運行的額外依賴,增長部署複雜度,而鏡像構建物能很好地解決這個問題。
咱們能夠把鏡像理解成一個小型操做系統,這個操做系統中包含了系統運行所需的全部依賴,並將系統也部署在這個「操做系統」中。這樣當持續集成平臺構建完這個鏡像後,就能夠直接運行它,無需任何依賴的安裝,從而極大簡化了構建的複雜度。可是,鏡像每每比較龐大,構建鏡像的過程也較長,從而當咱們將生成的鏡像從持續集成服務器發佈到部署服務器的時間將會很長,這無疑下降了部署的效率。不過好在Docker的出現解決了這一問題。持續集成平臺在構建過程當中並不須要生成一個鏡像,而只需生成一個鏡像的Dockerfile文件便可。Dockerfile文件用命令定義了鏡像所包含的內容,以及鏡像建立的過程。從而持續集成服務器只需將這個體積較小的鏡像文件發佈到部署服務器上便可。而後部署服務器會經過docker build命令基於這個Dockerfile文件建立鏡像,並建立該鏡像的容器,從而完成服務的部署。
相對於平臺構建物和操做系統構建物而言,鏡像構建物在部署時不須要安裝額外的環境依賴,它把環境依賴的配置都在持續集成平臺構建Dockerfile文件時完成,從而簡化了部署的過程。