分布式常見的十大坑,你了解幾個?
本篇主要內(nèi)容如下:

前言
我們都在討論分布式,特別是面試的時候,不管是招初級軟件工程師還是高級,都會要求懂分布式,甚至要求用過。傳得沸沸揚揚的分布式到底是什么東東,有什么優(yōu)勢?
借用火影忍術(shù)

看過火影的同學肯定知道漩渦鳴人的招牌忍術(shù):多重影分身之術(shù)。
這個術(shù)有一個特別厲害的地方, 過程和心得:多個分身的感受和經(jīng)歷都是相通的。比如 A 分身去找卡卡西(鳴人的老師)請教問題,那么其他分身也會知道 A 分身問的什么問題。漩渦鳴人有另外一個超級厲害的忍術(shù),需要由幾個影分身完成:風遁·螺旋手里劍。這個忍術(shù)是靠三個鳴人一起協(xié)作完成的。
這兩個忍術(shù)和分布式有什么關(guān)系?
分布在不同地方的系統(tǒng)或服務,是彼此相互關(guān)聯(lián)的。
分布式系統(tǒng)是分工合作的。
案例:
比如 Redis 的 哨兵機制,可以知道集群環(huán)境下哪臺Redis節(jié)點掛了。Kafka的 Leader 選舉機制,如果某個節(jié)點掛了,會從follower中重新選舉一個 leader 出來。(leader 作為寫數(shù)據(jù)的入口,follower 作為讀的入口)
那多重影分身之術(shù)有什么缺點?
會消耗大量的查克拉。分布式系統(tǒng)同樣具有這個問題,需要幾倍的資源來支持。
對分布式的通俗理解
是一種工作方式 若干獨立計算機的集合,這些計算機對于用戶來說就像單個相關(guān)系統(tǒng) 將不同的業(yè)務分布在不同的地方
優(yōu)勢可以從兩方面考慮:一個是宏觀,一個是微觀。
宏觀層面:多個功能模塊糅合在一起的系統(tǒng)進行服務拆分,來解耦服務間的調(diào)用。 微觀層面:將模塊提供的服務分布到不同的機器或容器里,來擴大服務力度。
任何事物有陰必有陽,那分布式又會帶來哪些問題呢?
需要更多優(yōu)質(zhì)人才懂分布式,人力成本增加 架構(gòu)設計變得異常復雜,學習成本高 運維部署和維護成本顯著增加 多服務間鏈路變長,開發(fā)排查問題難度加大 環(huán)境高可靠性問題 數(shù)據(jù)冪等性問題 數(shù)據(jù)的順序問題 等等
講到分布式不得不知道 CAP 定理和 Base 理論,這里給不知道的同學做一個掃盲。
CAP 定理
在理論計算機科學中,CAP 定理指出對于一個分布式計算系統(tǒng)來說,不可能通是滿足以下三點:
一致性(Consistency) 所有節(jié)點訪問同一份最新的數(shù)據(jù)副本。 可用性(Availability) 每次請求都能獲取到非錯的響應,但不保證獲取的數(shù)據(jù)為最新數(shù)據(jù) 分區(qū)容錯性(Partition tolerance) 不能在時限內(nèi)達成數(shù)據(jù)一致性,就意味著發(fā)生了分區(qū)的情況,必須就當前操作在 C 和 A 之間做出選擇)
BASE 理論
BASE 是 Basically Available(基本可用)、Soft state(軟狀態(tài))和 Eventually consistent(最終一致性)三個短語的縮寫。BASE 理論是對 CAP 中 AP 的一個擴展,通過犧牲強一致性來獲得可用性,當出現(xiàn)故障允許部分不可用但要保證核心功能可用,允許數(shù)據(jù)在一段時間內(nèi)是不一致的,但最終達到一致狀態(tài)。滿足 BASE 理論的事務,我們稱之為柔性事務。
基本可用 : 分布式系統(tǒng)在出現(xiàn)故障時,允許損失部分可用功能,保證核心功能可用。如電商網(wǎng)址交易付款出現(xiàn)問題來,商品依然可以正常瀏覽。 軟狀態(tài): 由于不要求強一致性,所以BASE允許系統(tǒng)中存在中間狀態(tài)(也叫軟狀態(tài)),這個狀態(tài)不影響系統(tǒng)可用性,如訂單中的“支付中”、“數(shù)據(jù)同步中”等狀態(tài),待數(shù)據(jù)最終一致后狀態(tài)改為“成功”狀態(tài)。 最終一致性: 最終一致是指的經(jīng)過一段時間后,所有節(jié)點數(shù)據(jù)都將會達到一致。如訂單的“支付中”狀態(tài),最終會變?yōu)椤爸Ц冻晒Α被蛘摺爸Ц妒 ?,使訂單狀態(tài)與實際交易結(jié)果達成一致,但需要一定時間的延遲、等待。
一、分布式消息隊列的坑
消息隊列如何做分布式?
將消息隊列里面的消息分攤到多個節(jié)點(指某臺機器或容器)上,所有節(jié)點的消息隊列之和就包含了所有消息。
1. 消息隊列的坑之非冪等
冪等性概念
所謂冪等性就是無論多少次操作和第一次的操作結(jié)果一樣。如果消息被多次消費,很有可能造成數(shù)據(jù)的不一致。而如果消息不可避免地被消費多次,如果我們開發(fā)人員能通過技術(shù)手段保證數(shù)據(jù)的前后一致性,那也是可以接受的,這讓我想起了 Java 并發(fā)編程中的 ABA 問題,如果出現(xiàn)了 [ABA問題),若能保證所有數(shù)據(jù)的前后一致性也能接受。
場景分析
RabbitMQ、RocketMQ、Kafka 消息隊列中間件都有可能出現(xiàn)消息重復消費問題。這種問題并不是 MQ 自己保證的,而是需要開發(fā)人員來保證。
這幾款消息隊列中間都是是全球最牛的分布式消息隊列,那肯定考慮到了消息的冪等性。我們以 Kafka 為例,看看 Kafka 是怎么保證消息隊列的冪等性。
Kafka 有一個 偏移量 的概念,代表著消息的序號,每條消息寫到消息隊列都會有一個偏移量,消費者消費了數(shù)據(jù)之后,每過一段固定的時間,就會把消費過的消息的偏移量提交一下,表示已經(jīng)消費過了,下次消費就從偏移量后面開始消費。
坑:當消費完消息后,還沒來得及提交偏移量,系統(tǒng)就被關(guān)機了,那么未提交偏移量的消息則會再次被消費。
如下圖所示,隊列中的數(shù)據(jù) A、B、C,對應的偏移量分別為 100、101、102,都被消費者消費了,但是只有數(shù)據(jù) A 的偏移量 100 提交成功,另外 2 個偏移量因系統(tǒng)重啟而導致未及時提交。

重啟后,消費者又是拿偏移量 100 以后的數(shù)據(jù),從偏移量 101 開始拿消息。所以數(shù)據(jù) B 和數(shù)據(jù) C 被重復消息。
如下圖所示:

避坑指南
微信支付結(jié)果通知場景 微信官方文檔上提到微信支付通知結(jié)果可能會推送多次,需要開發(fā)者自行保證冪等性。第一次我們可以直接修改訂單狀態(tài)(如支付中 -> 支付成功),第二次就根據(jù)訂單狀態(tài)來判斷,如果不是支付中,則不進行訂單處理邏輯。 插入數(shù)據(jù)庫場景 每次插入數(shù)據(jù)時,先檢查下數(shù)據(jù)庫中是否有這條數(shù)據(jù)的主鍵 id,如果有,則進行更新操作。 寫 Redis 場景 Redis 的 Set操作天然冪等性,所以不用考慮 Redis 寫數(shù)據(jù)的問題。其他場景方案 生產(chǎn)者發(fā)送每條數(shù)據(jù)時,增加一個全局唯一 id,類似訂單 id。每次消費時,先去 Redis 查下是否有這個 id,如果沒有,則進行正常處理消息,且將 id 存到 Redis。如果查到有這個 id,說明之前消費過,則不要進行重復處理這條消息。 不同業(yè)務場景,可能會有不同的冪等性方案,大家選擇合適的即可,上面的幾種方案只是提供常見的解決思路。
2. 消息隊列的坑之消息丟失
坑:消息丟失會帶來什么問題?如果是訂單下單、支付結(jié)果通知、扣費相關(guān)的消息丟失,則可能造成財務損失,如果量很大,就會給甲方帶來巨大損失。
那消息隊列是否能保證消息不丟失呢?答案:否。主要有三種場景會導致消息丟失。

(1)生產(chǎn)者存放消息的過程中丟失消息

解決方案
事務機制(不推薦,異步方式)
對于 RabbitMQ 來說,生產(chǎn)者發(fā)送數(shù)據(jù)之前開啟 RabbitMQ 的事務機制channel.txselect ,如果消息沒有進隊列,則生產(chǎn)者受到異常報錯,并進行回滾 channel.txRollback,然后重試發(fā)送消息;如果收到了消息,則可以提交事務 channel.txCommit。但這是一個同步的操作,會影響性能。
confirm 機制(推薦,異步方式)
我們可以采用另外一種模式:confirm 模式來解決同步機制的性能問題。每次生產(chǎn)者發(fā)送的消息都會分配一個唯一的 id,如果寫入到了 RabbitMQ 隊列中,則 RabbitMQ 會回傳一個 ack 消息,說明這個消息接收成功。如果 RabbitMQ 沒能處理這個消息,則回調(diào) nack 接口。說明需要重試發(fā)送消息。
也可以自定義超時時間 + 消息 id 來實現(xiàn)超時等待后重試機制。但可能出現(xiàn)的問題是調(diào)用 ack 接口時失敗了,所以會出現(xiàn)消息被發(fā)送兩次的問題,這個時候就需要保證消費者消費消息的冪等性。
事務模式 和 confirm 模式的區(qū)別:
事務機制是同步的,提交事務后悔被阻塞直到提交事務完成后。 confirm 模式異步接收通知,但可能接收不到通知。需要考慮接收不到通知的場景。
(2)消息隊列丟失消息

消息隊列的消息可以放到內(nèi)存中,或?qū)?nèi)存中的消息轉(zhuǎn)到硬盤(比如數(shù)據(jù)庫)中,一般都是內(nèi)存和硬盤中都存有消息。如果只是放在內(nèi)存中,那么當機器重啟了,消息就全部丟失了。如果是硬盤中,則可能存在一種極端情況,就是將內(nèi)存中的數(shù)據(jù)轉(zhuǎn)換到硬盤的期間中,消息隊列出問題了,未能將消息持久化到硬盤。
解決方案
創(chuàng)建 Queue的時候?qū)⑵湓O置為持久化。發(fā)送消息的時候?qū)⑾⒌? deliveryMode設置為 2 。開啟生產(chǎn)者 confirm模式,可以重試發(fā)送消息。
(3)消費者丟失消息

消費者剛拿到數(shù)據(jù),還沒開始處理消息,結(jié)果進程因為異常退出了,消費者沒有機會再次拿到消息。
解決方案
關(guān)閉 RabbitMQ 的自動 ack,每次生產(chǎn)者將消息寫入消息隊列后,就自動回傳一個ack給生產(chǎn)者。消費者處理完消息再主動 ack,告訴消息隊列我處理完了。
問題: 那這種主動 ack 有什么漏洞了?如果 主動 ack 的時候掛了,怎么辦?
則可能會被再次消費,這個時候就需要冪等處理了。
問題: 如果這條消息一直被重復消費怎么辦?
則需要有加上重試次數(shù)的監(jiān)測,如果超過一定次數(shù)則將消息丟失,記錄到異常表或發(fā)送異常通知給值班人員。
(4)RabbitMQ 消息丟失總結(jié)

(5)Kafka 消息丟失
場景:Kafka 的某個 broker(節(jié)點)宕機了,重新選舉 leader (寫入的節(jié)點)。如果 leader 掛了,follower 還有些數(shù)據(jù)未同步完,則 follower 成為 leader 后,消息隊列會丟失一部分數(shù)據(jù)。
解決方案
給 topic 設置 replication.factor參數(shù),值必須大于 1,要求每個 partition 必須有至少 2 個副本。給 kafka 服務端設置 min.insyc.replicas必須大于 1,表示一個 leader 至少一個 follower 還跟自己保持聯(lián)系。
3. 消息隊列的坑之消息亂序
坑:用戶先下單成功,然后取消訂單,如果順序顛倒,則最后數(shù)據(jù)庫里面會有一條下單成功的訂單。
RabbitMQ 場景:
生產(chǎn)者向消息隊列按照順序發(fā)送了 2 條消息,消息1:增加數(shù)據(jù) A,消息2:刪除數(shù)據(jù) A。 期望結(jié)果:數(shù)據(jù) A 被刪除。 但是如果有兩個消費者,消費順序是:消息2、消息 1。則最后結(jié)果是增加了數(shù)據(jù) A。


RabbitMQ 解決方案:
將 Queue 進行拆分,創(chuàng)建多個內(nèi)存 Queue,消息 1 和 消息 2 進入同一個 Queue。 創(chuàng)建多個消費者,每一個消費者對應一個 Queue。

Kafka 場景:
創(chuàng)建了 topic,有 3 個 partition。 創(chuàng)建一條訂單記錄,訂單 id 作為 key,訂單相關(guān)的消息都丟到同一個 partition 中,同一個生產(chǎn)者創(chuàng)建的消息,順序是正確的。 為了快速消費消息,會創(chuàng)建多個消費者去處理消息,而為了提高效率,每個消費者可能會創(chuàng)建多個線程來并行的去拿消息及處理消息,處理消息的順序可能就亂序了。

Kafka 解決方案:
解決方案和 RabbitMQ 類似,利用多個 內(nèi)存 Queue,每個線程消費 1個 Queue。 具有相同 key 的消息 進同一個 Queue。

4. 消息隊列的坑之消息積壓
消息積壓:消息隊列里面有很多消息來不及消費。
場景 1: 消費端出了問題,比如消費者都掛了,沒有消費者來消費了,導致消息在隊列里面不斷積壓。
場景 2: 消費端出了問題,比如消費者消費的速度太慢了,導致消息不斷積壓。
坑:比如線上正在做訂單活動,下單全部走消息隊列,如果消息不斷積壓,訂單都沒有下單成功,那么將會損失很多交易。

解決方案:解鈴還須系鈴人
修復代碼層面消費者的問題,確保后續(xù)消費速度恢復或盡可能加快消費的速度。 停掉現(xiàn)有的消費者。 臨時建立好原先 5 倍的 Queue 數(shù)量。 臨時建立好原先 5 倍數(shù)量的 消費者。 將堆積的消息全部轉(zhuǎn)入臨時的 Queue,消費者來消費這些 Queue。

5. 消息隊列的坑之消息過期失效
坑:RabbitMQ 可以設置過期時間,如果消息超過一定的時間還沒有被消費,則會被 RabbitMQ 給清理掉。消息就丟失了。

解決方案:
準備好批量重導的程序 手動將消息閑時批量重導

6. 消息隊列的坑之隊列寫滿
坑:當消息隊列因消息積壓導致的隊列快寫滿,所以不能接收更多的消息了。生產(chǎn)者生產(chǎn)的消息將會被丟棄。
解決方案:
判斷哪些是無用的消息,RabbitMQ 可以進行 Purge Message操作。如果是有用的消息,則需要將消息快速消費,將消息里面的內(nèi)容轉(zhuǎn)存到數(shù)據(jù)庫。 準備好程序?qū)⑥D(zhuǎn)存在數(shù)據(jù)庫中的消息再次重導到消息隊列。 閑時重導消息到消息隊列。
二、分布式緩存的坑
在高頻訪問數(shù)據(jù)庫的場景中,我們會在業(yè)務層和數(shù)據(jù)層之間加入一套緩存機制,來分擔數(shù)據(jù)庫的訪問壓力,畢竟訪問磁盤 I/O 的速度是很慢的。比如利用緩存來查數(shù)據(jù),可能5ms就能搞定,而去查數(shù)據(jù)庫可能需要 50 ms,差了一個數(shù)量級。而在高并發(fā)的情況下,數(shù)據(jù)庫還有可能對數(shù)據(jù)進行加鎖,導致訪問數(shù)據(jù)庫的速度更慢。
分布式緩存我們用的最多的就是 Redis了,它可以提供分布式緩存服務。
1. Redis 數(shù)據(jù)丟失的坑
哨兵機制
Redis 可以實現(xiàn)利用哨兵機制實現(xiàn)集群的高可用。那什么十哨兵機制呢?
英文名: sentinel,中文名:哨兵。集群監(jiān)控:負責主副進程的正常工作。 消息通知:負責將故障信息報警給運維人員。 故障轉(zhuǎn)移:負責將主節(jié)點轉(zhuǎn)移到備用節(jié)點上。 配置中心:通知客戶端更新主節(jié)點地址。 分布式:有多個哨兵分布在每個主備節(jié)點上,互相協(xié)同工作。 分布式選舉:需要大部分哨兵都同意,才能進行主備切換。 高可用:即使部分哨兵節(jié)點宕機了,哨兵集群還是能正常工作。
坑:當主節(jié)點發(fā)生故障時,需要進行主備切換,可能會導致數(shù)據(jù)丟失。
異步復制數(shù)據(jù)導致的數(shù)據(jù)丟失
主節(jié)點異步同步數(shù)據(jù)給備用節(jié)點的過程中,主節(jié)點宕機了,導致有部分數(shù)據(jù)未同步到備用節(jié)點。而這個從節(jié)點又被選舉為主節(jié)點,這個時候就有部分數(shù)據(jù)丟失了。
腦裂導致的數(shù)據(jù)丟失
主節(jié)點所在機器脫離了集群網(wǎng)絡,實際上自身還是運行著的。但哨兵選舉出了備用節(jié)點作為主節(jié)點,這個時候就有兩個主節(jié)點都在運行,相當于兩個大腦在指揮這個集群干活,但到底聽誰的呢?這個就是腦裂。
那怎么腦裂怎么會導致數(shù)據(jù)丟失呢?如果發(fā)生腦裂后,客戶端還沒來得及切換到新的主節(jié)點,連的還是第一個主節(jié)點,那么有些數(shù)據(jù)還是寫入到了第一個主節(jié)點里面,新的主節(jié)點沒有這些數(shù)據(jù)。那等到第一個主節(jié)點恢復后,會被作為備用節(jié)點連到集群環(huán)境,而且自身數(shù)據(jù)會被清空,重新從新的主節(jié)點復制數(shù)據(jù)。而新的主節(jié)點因沒有客戶端之前寫入的數(shù)據(jù),所以導致數(shù)據(jù)丟失了一部分。
避坑指南
配置 min-slaves-to-write 1,表示至少有一個備用節(jié)點。 配置 min-slaves-max-lag 10,表示數(shù)據(jù)復制和同步的延遲不能超過 10 秒。最多丟失 10 秒的數(shù)據(jù)
注意:緩存雪崩、緩存穿透、緩存擊穿并不是分布式所獨有的,單機的時候也會出現(xiàn)。所以不在分布式的坑之列。
三、分庫分表的坑
1.分庫分表的坑之擴容
分庫、分表、垂直拆分和水平拆分
分庫: 因一個數(shù)據(jù)庫支持的最高并發(fā)訪問數(shù)是有限的,可以將一個數(shù)據(jù)庫的數(shù)據(jù)拆分到多個庫中,來增加最高并發(fā)訪問數(shù)。
分表: 因一張表的數(shù)據(jù)量太大,用索引來查詢數(shù)據(jù)都搞不定了,所以可以將一張表的數(shù)據(jù)拆分到多張表,查詢時,只用查拆分后的某一張表,SQL 語句的查詢性能得到提升。
分庫分表優(yōu)勢:分庫分表后,承受的并發(fā)增加了多倍;磁盤使用率大大降低;單表數(shù)據(jù)量減少,SQL 執(zhí)行效率明顯提升。
水平拆分: 把一個表的數(shù)據(jù)拆分到多個數(shù)據(jù)庫,每個數(shù)據(jù)庫中的表結(jié)構(gòu)不變。用多個庫抗更高的并發(fā)。比如訂單表每個月有500萬條數(shù)據(jù)累計,每個月都可以進行水平拆分,將上個月的數(shù)據(jù)放到另外一個數(shù)據(jù)庫。
垂直拆分: 把一個有很多字段的表,拆分成多張表到同一個庫或多個庫上面。高頻訪問字段放到一張表,低頻訪問的字段放到另外一張表。利用數(shù)據(jù)庫緩存來緩存高頻訪問的行數(shù)據(jù)。比如將一張很多字段的訂單表拆分成幾張表分別存不同的字段(可以有冗余字段)。
分庫、分表的方式:
根據(jù)租戶來分庫、分表。 利用時間范圍來分庫、分表。 利用 ID 取模來分庫、分表。
坑:分庫分表是一個運維層面需要做的事情,有時會采取凌晨宕機開始升級??赡馨疽沟教炝粒Y(jié)果升級失敗,則需要回滾,其實對技術(shù)團隊都是一種煎熬。
怎么做成自動的來節(jié)省分庫分表的時間?
雙寫遷移方案:遷移時,新數(shù)據(jù)的增刪改操作在新庫和老庫都做一遍。 使用分庫分表工具 Sharding-jdbc ?來完成分庫分表的累活。 使用程序來對比兩個庫的數(shù)據(jù)是否一致,直到數(shù)據(jù)一致。
坑:分庫分表看似光鮮亮麗,但分庫分表會引入什么新的問題呢?
垂直拆分帶來的問題
依然存在單表數(shù)據(jù)量過大的問題。 部分表無法關(guān)聯(lián)查詢,只能通過接口聚合方式解決,提升了開發(fā)的復雜度。 分布式事處理復雜。
水平拆分帶來的問題
跨庫的關(guān)聯(lián)查詢性能差。 數(shù)據(jù)多次擴容和維護量大。 跨分片的事務一致性難以保證。
2.分庫分表的坑之唯一 ID
為什么分庫分表需要唯一 ID
如果要做分庫分表,則必須得考慮表主鍵 ID 是全局唯一的,比如有一張訂單表,被分到 A 庫和 B 庫。如果 兩張訂單表都是從 1 開始遞增,那查詢訂單數(shù)據(jù)時就錯亂了,很多訂單 ID 都是重復的,而這些訂單其實不是同一個訂單。 分庫的一個期望結(jié)果就是將訪問數(shù)據(jù)的次數(shù)分攤到其他庫,有些場景是需要均勻分攤的,那么數(shù)據(jù)插入到多個數(shù)據(jù)庫的時候就需要交替生成唯一的 ID 來保證請求均勻分攤到所有數(shù)據(jù)庫。
坑:唯一 ID 的生成方式有 n 種,各有各的用途,別用錯了。
生成唯一 ID 的原則
全局唯一性 趨勢遞增 單調(diào)遞增 信息安全
生成唯一 ID 的幾種方式
數(shù)據(jù)庫自增 ID。每個數(shù)據(jù)庫每增加一條記錄,自己的 ID 自增 1。
多個庫的 ID 可能重復,這個方案可以直接否掉了,不適合分庫分表后的 ID 生成。 信息不安全 缺點 適用
UUID唯一 ID。UUID 太長、占用空間大。 不具有有序性,作為主鍵時,在寫入數(shù)據(jù)時,不能產(chǎn)生有順序的 append 操作,只能進行 insert 操作,導致讀取整個 B+樹節(jié)點到內(nèi)存,插入記錄后將整個節(jié)點寫回磁盤,當記錄占用空間很大的時候,性能很差。缺點 獲取系統(tǒng)當前時間作為唯一 ID。
高并發(fā)時,1 ms內(nèi)可能有多個相同的 ID。 信息不安全 缺點 Twitter 的
snowflake(雪花算法):Twitter 開源的分布式 id 生成算法,64 位的 long 型的 id,分為 4 部分
snowflake 算法 1 bit:不用,統(tǒng)一為 0
41 bits:毫秒時間戳,可以表示 69 年的時間。
10 bits:5 bits 代表機房 id,5 個 bits 代表機器 id。最多代表 32 個機房,每個機房最多代表 32 臺機器。
12 bits:同一毫秒內(nèi)的 id,最多 4096 個不同 id,自增模式。
優(yōu)點:
毫秒數(shù)在高位,自增序列在低位,整個ID都是趨勢遞增的。
不依賴數(shù)據(jù)庫等第三方系統(tǒng),以服務的方式部署,穩(wěn)定性更高,生成ID的性能也是非常高的。
可以根據(jù)自身業(yè)務特性分配bit位,非常靈活。
缺點:
強依賴機器時鐘,如果機器上時鐘回撥(可以搜索?2017 年閏秒 7:59:60),會導致發(fā)號重復或者服務會處于不可用狀態(tài)。
百度的
UIDGenerator算法。
UIDGenerator 算法 基于 Snowflake 的優(yōu)化算法。 借用未來時間和雙 Buffer 來解決時間回撥與生成性能等問題,同時結(jié)合 MySQL 進行 ID 分配。 優(yōu)點:解決了時間回撥和生成性能問題。 缺點:依賴?MySQL?數(shù)據(jù)庫。 美團的
Leaf-Snowflake算法。獲取 id 是通過代理服務訪問數(shù)據(jù)庫獲取一批 id(號段)。
雙緩沖:當前一批的 id 使用 10%時,再訪問數(shù)據(jù)庫獲取新的一批 id 緩存起來,等上批的 id 用完后直接用。
優(yōu)點:
Leaf服務可以很方便的線性擴展,性能完全能夠支撐大多數(shù)業(yè)務場景。
ID號碼是趨勢遞增的8byte的64位數(shù)字,滿足上述數(shù)據(jù)庫存儲的主鍵要求。
容災性高:Leaf服務內(nèi)部有號段緩存,即使DB宕機,短時間內(nèi)Leaf仍能正常對外提供服務。
可以自定義max_id的大小,非常方便業(yè)務從原有的ID方式上遷移過來。
即使DB宕機,Leaf仍能持續(xù)發(fā)號一段時間。
偶爾的網(wǎng)絡抖動不會影響下個號段的更新。
缺點:
ID號碼不夠隨機,能夠泄露發(fā)號數(shù)量的信息,不太安全。
基本原理和優(yōu)缺點:
怎么選擇:一般自己的內(nèi)部系統(tǒng),雪花算法足夠,如果還要更加安全可靠,可以選擇百度或美團的生成唯一 ID 的方案。
四、分布式事務的坑
怎么理解事務?
事務可以簡單理解為要么這件事情全部做完,要么這件事情一點都沒做,跟沒發(fā)生一樣。
在分布式的世界中,存在著各個服務之間相互調(diào)用,鏈路可能很長,如果有任何一方執(zhí)行出錯,則需要回滾涉及到的其他服務的相關(guān)操作。比如訂單服務下單成功,然后調(diào)用營銷中心發(fā)券接口發(fā)了一張代金券,但是微信支付扣款失敗,則需要退回發(fā)的那張券,且需要將訂單狀態(tài)改為異常訂單。
坑:如何保證分布式中的事務正確執(zhí)行,是個大難題。
分布式事務的幾種主要方式
XA 方案(兩階段提交方案) TCC 方案(try、confirm、cancel) SAGA 方案 可靠消息最終一致性方案 最大努力通知方案
XA 方案原理

事務管理器負責協(xié)調(diào)多個數(shù)據(jù)庫的事務,先問問各個數(shù)據(jù)庫準備好了嗎?如果準備好了,則在數(shù)據(jù)庫執(zhí)行操作,如果任一數(shù)據(jù)庫沒有準備,則回滾事務。 適合單體應用,不適合微服務架構(gòu)。因為每個服務只能訪問自己的數(shù)據(jù)庫,不允許交叉訪問其他微服務的數(shù)據(jù)庫。
TCC 方案
Try 階段:對各個服務的資源做檢測以及對資源進行鎖定或者預留。 Confirm 階段:各個服務中執(zhí)行實際的操作。 Cancel 階段:如果任何一個服務的業(yè)務方法執(zhí)行出錯,需要將之前操作成功的步驟進行回滾。
應用場景:
跟支付、交易打交道,必須保證資金正確的場景。 對于一致性要求高。
缺點:
但因為要寫很多補償邏輯的代碼,且不易維護,所以其他場景建議不要這么做。
Sega 方案
基本原理:
業(yè)務流程中的每個步驟若有一個失敗了,則補償前面操作成功的步驟。
適用場景:
業(yè)務流程長、業(yè)務流程多。 參與者包含其他公司或遺留系統(tǒng)服務。
優(yōu)勢:
第一個階段提交本地事務、無鎖、高性能。 參與者可異步執(zhí)行、高吞吐。 補償服務易于實現(xiàn)。
缺點:
不保證事務的隔離性。
可靠消息一致性方案

基本原理:
利用消息中間件 RocketMQ來實現(xiàn)消息事務。第一步:A 系統(tǒng)發(fā)送一個消息到 MQ,MQ將消息狀態(tài)標記為 prepared(預備狀態(tài),半消息),該消息無法被訂閱。第二步:MQ 響應 A 系統(tǒng),告訴 A 系統(tǒng)已經(jīng)接收到消息了。 第三步:A 系統(tǒng)執(zhí)行本地事務。 第四步:若 A 系統(tǒng)執(zhí)行本地事務成功,將 prepared消息改為commit(提交事務消息),B 系統(tǒng)就可以訂閱到消息了。第五步:MQ 也會定時輪詢所有 prepared的消息,回調(diào) A 系統(tǒng),讓 A 系統(tǒng)告訴 MQ 本地事務處理得怎么樣了,是繼續(xù)等待還是回滾。第六步:A 系統(tǒng)檢查本地事務的執(zhí)行結(jié)果。 第七步:若 A 系統(tǒng)執(zhí)行本地事務失敗,則 MQ 收到 Rollback信號,丟棄消息。若執(zhí)行本地事務成功,則 MQ 收到Commit信號。B 系統(tǒng)收到消息后,開始執(zhí)行本地事務,如果執(zhí)行失敗,則自動不斷重試直到成功。或 B 系統(tǒng)采取回滾的方式,同時要通過其他方式通知 A 系統(tǒng)也進行回滾。 B 系統(tǒng)需要保證冪等性。
最大努力通知方案
基本原理:
系統(tǒng) A 本地事務執(zhí)行完之后,發(fā)送消息到 MQ。 MQ 將消息持久化。 系統(tǒng) B 如果執(zhí)行本地事務失敗,則 最大努力服務會定時嘗試重新調(diào)用系統(tǒng) B,盡自己最大的努力讓系統(tǒng) B 重試,重試多次后,還是不行就只能放棄了。轉(zhuǎn)到開發(fā)人員去排查以及后續(xù)人工補償。
幾種方案如何選擇
跟支付、交易打交道,優(yōu)先 TCC。 大型系統(tǒng),但要求不那么嚴格,考慮 消息事務或 SAGA 方案。 單體應用,建議 XA 兩階段提交就可以了。 最大努力通知方案建議都加上,畢竟不可能一出問題就交給開發(fā)排查,先重試幾次看能不能成功。
寫在最后
分布式還有很多坑,這篇只是一個小小的總結(jié),從這些坑中,我們也知道分布式有它的優(yōu)勢也有它的劣勢,那到底該不該用分布式,完全取決于業(yè)務、時間、成本以及開發(fā)團隊的綜合實力。后續(xù)我會繼續(xù)分享分布式中的一些底層原理,當然也少不了分享一些避坑指南。
參考資料:
美團的 Leaf-Snowflake 算法。
百度的 UIDGenerator 算法。
Advanced-Java
?閑聊
昨天和大學童鞋一起出玩,碰巧有一個武漢某211自動化專業(yè)的一位研究生隨同。自動化專業(yè)的這位老哥說:”我們實驗室里的基本所有人都轉(zhuǎn) CS,畢竟這玩意工資確實比較高。996什么的, 我們根本不在乎,只要錢多就夠了”。
我當時上大學的時候,也覺得錢夠了 996 沒啥,真正到了這個時候,還是有點難以接受的。哈哈哈!
雖然,計算機行業(yè)的人越來越多,但我個人覺得是沒辦法避免的。未來我相信計算機教育會越來越被普及,我也非??春蒙賰壕幊探逃W屛覀兪媚恳源?!
我是Guide哥,Java后端開發(fā),會一點前端知識,喜歡烹飪,自由的少年。一個三觀比主角還正的技術(shù)人。我們下期再見!
