1. 程式人生 > >這三年被分散式坑慘了,曝光十大坑

這三年被分散式坑慘了,曝光十大坑

本篇主要內容如下: ![主要內容](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/01ed0519820547948ebc156509922ca6~tplv-k3u1fbpfcp-zoom-1.image) # 前言 我們都在討論分散式,特別是面試的時候,不管是招初級軟體工程師還是高階,都會要求懂分散式,甚至要求用過。傳得沸沸揚揚的分散式到底是什麼東東,有什麼優勢? ### 借用火影忍術 ![風遁 螺旋手裡劍](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1d167cd4524c4af09343dc8069ee5ce1~tplv-k3u1fbpfcp-zoom-1.image) 看過`火影`的同學肯定知道`漩渦鳴人`的招牌忍術:`多重影分身之術`。 - 這個術有一個特別厲害的地方,`過程和心得`:多個分身的感受和經歷都是相通的。比如 A 分身去找卡卡西(鳴人的老師)請教問題,那麼其他分身也會知道 A 分身問的什麼問題。 - `漩渦鳴人`有另外一個超級厲害的忍術,需要由幾個影分身完成:`風遁·螺旋手裡劍`。這個忍術是靠三個鳴人一起協作完成的。 這兩個忍術和分散式有什麼關係? - 分佈在不同地方的系統或服務,是彼此相互關聯的。 - 分散式系統是分工合作的。 案例: - 比如 Redis 的`哨兵機制`,可以知道叢集環境下哪臺 `Redis` 節點掛了。 - Kafka的 `Leader 選舉機制`,如果某個節點掛了,會從 `follower` 中重新選舉一個 leader 出來。(leader 作為寫資料的入口,follower 作為讀的入口) 那`多重影分身之術`有什麼缺點? - 會消耗大量的查克拉。分散式系統同樣具有這個問題,需要幾倍的資源來支援。 ### 對分散式的通俗理解 - 是一種工作方式 - 若干獨立計算機的集合,這些計算機對於使用者來說就像單個相關係統 - 將不同的業務分佈在不同的地方 ### 優勢可以從兩方面考慮:一個是巨集觀,一個是微觀。 - 巨集觀層面:多個功能模組糅合在一起的系統進行服務拆分,來解耦服務間的呼叫。 - 微觀層面:將模組提供的服務分佈到不同的機器或容器裡,來擴大服務力度。 ### 任何事物有陰必有陽,那分散式又會帶來哪些問題呢? - 需要更多優質人才懂分散式,人力成本增加 - 架構設計變得異常複雜,學習成本高 - 運維部署和維護成本顯著增加 - 多服務間鏈路變長,開發排查問題難度加大 - 環境高可靠性問題 - 資料冪等性問題 - 資料的順序問題 - 等等 講到`分散式`不得不知道 `CAP` 定理和 `Base` 理論,這裡給不知道的同學做一個掃盲。 ### CAP 定理 在理論電腦科學中,CAP 定理指出對於一個分散式計算系統來說,不可能通是滿足以下三點: - **一致性(Consistency)** - 所有節點訪問同一份最新的資料副本。 - **可用性(Availability)** - 每次請求都能獲取到非錯的響應,但不保證獲取的資料為最新資料 - **分割槽容錯性(Partition tolerance)** - 不能在時限內達成資料一致性,就意味著發生了分割槽的情況,必須就當前操作在 C 和 A 之間做出選擇) ### BASE 理論 `BASE` 是 `Basically Available`(基本可用)、`Soft state`(軟狀態)和 `Eventually consistent`(最終一致性)三個短語的縮寫。`BASE` 理論是對 `CAP ` 中 `AP` 的一個擴充套件,通過犧牲強一致性來獲得可用性,當出現故障允許部分不可用但要保證核心功能可用,允許資料在一段時間內是不一致的,但最終達到一致狀態。滿足 `BASE` 理論的事務,我們稱之為`柔性事務`。 - **基本可用 :** 分散式系統在出現故障時,允許損失部分可用功能,保證核心功能可用。如電商網址交易付款出現問題來,商品依然可以正常瀏覽。 - **軟狀態:** 由於不要求強一致性,所以BASE允許系統中存在中間狀態(也叫軟狀態),這個狀態不影響系統可用性,如訂單中的“支付中”、“資料同步中”等狀態,待資料最終一致後狀態改為“成功”狀態。 - **最終一致性:** 最終一致是指的經過一段時間後,所有節點資料都將會達到一致。如訂單的“支付中”狀態,最終會變為“支付成功”或者“支付失敗”,使訂單狀態與實際交易結果達成一致,但需要一定時間的延遲、等待。 # 一、分散式訊息佇列的坑 ### 訊息佇列如何做分散式? 將訊息佇列裡面的訊息分攤到多個節點(指某臺機器或容器)上,所有節點的訊息佇列之和就包含了所有訊息。 ## 1. 訊息佇列的坑之非冪等 ### (1)冪等性概念 所謂冪等性就是無論多少次操作和第一次的操作結果一樣。如果訊息被多次消費,很有可能造成資料的不一致。而如果訊息不可避免地被消費多次,如果我們開發人員能通過技術手段保證資料的前後一致性,那也是可以接受的,這讓我想起了 Java 併發程式設計中的 ABA 問題,如果出現了 [ABA 問題]([用積木講解 ABA 原理 | 老婆居然又聽懂了!](https://juejin.im/post/6864945088721027079)),若能保證所有資料的前後一致性也能接受。 ### (2)場景分析 `RabbitMQ`、`RocketMQ`、`Kafka` 訊息佇列中介軟體都有可能出現訊息重複消費問題。這種問題並不是 MQ 自己保證的,而是需要開發人員來保證。 這幾款訊息佇列中間都是是全球最牛的分散式訊息佇列,那肯定考慮到了訊息的冪等性。我們以 Kafka 為例,看看 Kafka 是怎麼保證訊息佇列的冪等性。 Kafka 有一個 ` 偏移量 ` 的概念,代表著訊息的序號,每條訊息寫到訊息佇列都會有一個偏移量,消費者消費了資料之後,每過一段固定的時間,就會把消費過的訊息的偏移量提交一下,表示已經消費過了,下次消費就從偏移量後面開始消費。 > `坑:`當消費完訊息後,還沒來得及提交偏移量,系統就被關機了,那麼未提交偏移量的訊息則會再次被消費。 如下圖所示,佇列中的資料 A、B、C,對應的偏移量分別為 100、101、102,都被消費者消費了,但是隻有資料 A 的偏移量 100 提交成功,另外 2 個偏移量因系統重啟而導致未及時提交。 ![系統重啟,偏移量未提交](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/678db990060545389720a20b6de58a78~tplv-k3u1fbpfcp-zoom-1.image) 重啟後,消費者又是拿偏移量 100 以後的資料,從偏移量 101 開始拿訊息。所以資料 B 和資料 C 被重複訊息。 如下圖所示: ![重啟後,重複消費訊息](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/efb9c03422bd446aafe3d15a301058b2~tplv-k3u1fbpfcp-zoom-1.image) ### (3)避坑指南 - 微信支付結果通知場景 - 微信官方文件上提到微信支付通知結果可能會推送多次,需要開發者自行保證冪等性。第一次我們可以直接修改訂單狀態(如支付中 -> 支付成功),第二次就根據訂單狀態來判斷,如果不是支付中,則不進行訂單處理邏輯。 - 插入資料庫場景 - 每次插入資料時,先檢查下資料庫中是否有這條資料的主鍵 id,如果有,則進行更新操作。 - 寫 Redis 場景 - Redis 的 `Set` 操作天然冪等性,所以不用考慮 Redis 寫資料的問題。 - 其他場景方案 - 生產者傳送每條資料時,增加一個全域性唯一 id,類似訂單 id。每次消費時,先去 Redis 查下是否有這個 id,如果沒有,則進行正常處理訊息,且將 id 存到 Redis。如果查到有這個 id,說明之前消費過,則不要進行重複處理這條訊息。 - 不同業務場景,可能會有不同的冪等性方案,大家選擇合適的即可,上面的幾種方案只是提供常見的解決思路。 ## 2. 訊息佇列的坑之訊息丟失 > `坑:`訊息丟失會帶來什麼問題?如果是訂單下單、支付結果通知、扣費相關的訊息丟失,則可能造成財務損失,如果量很大,就會給甲方帶來巨大損失。 那訊息佇列是否能保證訊息不丟失呢?答案:否。主要有三種場景會導致訊息丟失。 ![訊息佇列之訊息丟失](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c444b2838f8d4d92ad3c56ecb46ed675~tplv-k3u1fbpfcp-zoom-1.image) ### (1)生產者存放訊息的過程中丟失訊息 ![生產者丟失訊息](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/80bf3723352843cbbc82133d8ff0e130~tplv-k3u1fbpfcp-zoom-1.image) #### 解決方案 - 事務機制(不推薦,非同步方式) 對於 RabbitMQ 來說,生產者傳送資料之前開啟 RabbitMQ 的**事務機制**`channel.txselect` ,如果訊息沒有進佇列,則生產者受到異常報錯,並進行回滾 `channel.txRollback`,然後重試傳送訊息;如果收到了訊息,則可以提交事務 `channel.txCommit`。但這是一個同步的操作,會影響效能。 - confirm 機制(推薦,非同步方式) 我們可以採用另外一種模式: `confirm` 模式來解決同步機制的效能問題。每次生產者傳送的訊息都會分配一個唯一的 id,如果寫入到了 RabbitMQ 佇列中,則 RabbitMQ 會回傳一個 `ack` 訊息,說明這個訊息接收成功。如果 RabbitMQ 沒能處理這個訊息,則回撥 `nack` 介面。說明需要重試傳送訊息。 也可以自定義超時時間 + 訊息 id 來實現超時等待後重試機制。但可能出現的問題是呼叫 ack 介面時失敗了,所以會出現訊息被髮送兩次的問題,這個時候就需要保證消費者消費訊息的冪等性。 #### ` 事務模式 ` 和 `confirm` 模式的區別: - 事務機制是同步的,提交事務後悔被**阻塞**直到提交事務完成後。 - confirm 模式非同步接收通知,但可能**接收不到通知**。需要考慮接收不到通知的場景。 ### (2)訊息佇列丟失訊息 ![訊息佇列丟失訊息](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/496ffed09a34431991bab1de7054d8a9~tplv-k3u1fbpfcp-zoom-1.image) 訊息佇列的訊息可以放到記憶體中,或將記憶體中的訊息轉到硬碟(比如資料庫)中,一般都是記憶體和硬碟中都存有訊息。如果只是放在記憶體中,那麼當機器重啟了,訊息就全部丟失了。如果是硬碟中,則可能存在一種極端情況,就是將記憶體中的資料轉換到硬碟的期間中,訊息隊列出問題了,未能將訊息持久化到硬碟。 **解決方案** - 建立 `Queue` 的時候將其設定為持久化。這個地方沒搞懂,歡迎探討解答。 - 傳送訊息的時候將訊息的 `deliveryMode` 設定為 2 。 - 開啟生產者 `confirm` 模式,可以重試傳送訊息。 ### (3)消費者丟失訊息 ![消費者丟失訊息](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2ef429275fe84923974035a5f88dbc7a~tplv-k3u1fbpfcp-zoom-1.image) 消費者剛拿到資料,還沒開始處理訊息,結果程序因為異常退出了,消費者沒有機會再次拿到訊息。 **解決方案** - 關閉 RabbitMQ 的自動 `ack`,每次生產者將訊息寫入訊息佇列後,就自動回傳一個 `ack` 給生產者。 - 消費者處理完訊息再主動 `ack`,告訴訊息佇列我處理完了。 **問題:** 那這種主動 `ack` 有什麼漏洞了?如果 主動 `ack` 的時候掛了,怎麼辦? 則可能會被再次消費,這個時候就需要冪等處理了。 **問題:** 如果這條訊息一直被重複消費怎麼辦? 則需要有加上重試次數的監測,如果超過一定次數則將訊息丟失,記錄到異常表或傳送異常通知給值班人員。 ### (4)RabbitMQ 訊息丟失總結 ![RabbitMQ 丟失訊息的處理方案](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/756811711f934472a9edbc48eb603707~tplv-k3u1fbpfcp-zoom-1.image) ### (5)Kafka 訊息丟失 **場景:**`Kafka` 的某個 broker(節點)宕機了,重新選舉 leader (寫入的節點)。如果 leader 掛了,follower 還有些資料未同步完,則 follower 成為 leader 後,訊息佇列會丟失一部分資料。 **解決方案** - 給 topic 設定 `replication.factor` 引數,值必須大於 1,要求每個 partition 必須有至少 2 個副本。 - 給 kafka 服務端設定 `min.insyc.replicas` 必須大於 1,表示一個 leader 至少一個 follower 還跟自己保持聯絡。 ## 3. 訊息佇列的坑之訊息亂序 > `坑:` 使用者先下單成功,然後取消訂單,如果順序顛倒,則最後資料庫裡面會有一條下單成功的訂單。 **RabbitMQ 場景:** - 生產者向訊息佇列按照順序傳送了 2 條訊息,訊息1:增加資料 A,訊息2:刪除資料 A。 - 期望結果:資料 A 被刪除。 - 但是如果有兩個消費者,消費順序是:訊息2、訊息 1。則最後結果是增加了資料 A。 ![RabbitMQ訊息亂序場景](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e4a83f8dfeb542dfa47295d121648604~tplv-k3u1fbpfcp-zoom-1.image) ![RabbitMQ 訊息亂序場景](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c3b00611a8e74f1d9696996c53ffd2cb~tplv-k3u1fbpfcp-zoom-1.image) **RabbitMQ 解決方案:** - 將 Queue 進行拆分,建立多個記憶體 Queue,訊息 1 和 訊息 2 進入同一個 Queue。 - 建立多個消費者,每一個消費者對應一個 Queue。 ![RabbitMQ 解決方案](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54c37e64198f4cb98777b155955d3377~tplv-k3u1fbpfcp-zoom-1.image) **Kafka 場景:** - 建立了 topic,有 3 個 partition。 - 建立一條訂單記錄,訂單 id 作為 key,訂單相關的訊息都丟到同一個 partition 中,同一個生產者建立的訊息,順序是正確的。 - 為了快速消費訊息,會建立多個消費者去處理訊息,而為了提高效率,每個消費者可能會建立多個執行緒來並行的去拿訊息及處理訊息,處理訊息的順序可能就亂序了。 ![Kafka 訊息丟失場景](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b56e9cbaad484eadb20f1e288f1bb535~tplv-k3u1fbpfcp-zoom-1.image) **Kafka 解決方案:** - 解決方案和 RabbitMQ 類似,利用多個 記憶體 Queue,每個執行緒消費 1個 Queue。 - 具有相同 key 的訊息 進同一個 Queue。 ![Kafka 訊息亂序解決方案](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf58802508854d56829a7ecdcb158073~tplv-k3u1fbpfcp-zoom-1.image) ## 4. 訊息佇列的坑之訊息積壓 訊息積壓:訊息佇列裡面有很多訊息來不及消費。 **場景 1:** 消費端出了問題,比如消費者都掛了,沒有消費者來消費了,導致訊息在佇列裡面不斷積壓。 **場景 2:** 消費端出了問題,比如消費者消費的速度太慢了,導致訊息不斷積壓。 > 坑:比如線上正在做訂單活動,下單全部走訊息佇列,如果訊息不斷積壓,訂單都沒有下單成功,那麼將會損失很多交易。 ![訊息佇列之訊息積壓](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9c4cf34f137843e1a5533a4395e7b9f1~tplv-k3u1fbpfcp-zoom-1.image) 解決方案:**解鈴還須繫鈴人** - 修復程式碼層面消費者的問題,確保後續消費速度恢復或儘可能加快消費的速度。 - 停掉現有的消費者。 - 臨時建立好原先 5 倍的 Queue 數量。 - 臨時建立好原先 5 倍數量的 消費者。 - 將堆積的訊息全部轉入臨時的 Queue,消費者來消費這些 Queue。 ![訊息積壓解決方案](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a3596bdfff3f4a1697330e49bb7e0fa4~tplv-k3u1fbpfcp-zoom-1.image) ## 5. 訊息佇列的坑之訊息過期失效 > `坑:`RabbitMQ 可以設定過期時間,如果訊息超過一定的時間還沒有被消費,則會被 RabbitMQ 給清理掉。訊息就丟失了。 ![訊息過期失效](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d455c4fbf2c84cf681659dfc02a40e05~tplv-k3u1fbpfcp-zoom-1.image) 解決方案: - 準備好批量重導的程式 - 手動將訊息閒時批量重導 ![訊息過期失效解決方案](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/96765f014404428c8965a70e8b8fd99a~tplv-k3u1fbpfcp-zoom-1.image) ## 6. 訊息佇列的坑之佇列寫滿 > `坑:`當訊息佇列因訊息積壓導致的佇列快寫滿,所以不能接收更多的訊息了。生產者生產的訊息將會被丟棄。 解決方案: - 判斷哪些是無用的訊息,RabbitMQ 可以進行 `Purge Message` 操作。 - 如果是有用的訊息,則需要將訊息快速消費,將訊息裡面的內容轉存到資料庫。 - 準備好程式將轉存在資料庫中的訊息再次重導到訊息佇列。 - 閒時重導訊息到訊息佇列。 # 二、分散式快取的坑 在高頻訪問資料庫的場景中,我們會在業務層和資料層之間加入一套快取機制,來分擔資料庫的訪問壓力,畢竟訪問磁碟 I/O 的速度是很慢的。比如利用快取來查資料,可能5ms就能搞定,而去查資料庫可能需要 50 ms,差了一個數量級。而在高併發的情況下,資料庫還有可能對資料進行加鎖,導致訪問資料庫的速度更慢。 分散式快取我們用的最多的就是 Redis了,它可以提供分散式快取服務。 ## 1. Redis 資料丟失的坑 ### 哨兵機制 Redis 可以實現利用`哨兵機制`實現叢集的高可用。那什麼十哨兵機制呢? - 英文名:`sentinel`,中文名:`哨兵`。 - 叢集監控:負責主副程序的正常工作。 - 訊息通知:負責將故障資訊報警給運維人員。 - 故障轉移:負責將主節點轉移到備用節點上。 - 配置中心:通知客戶端更新主節點地址。 - 分散式:有多個哨兵分佈在每個主備節點上,互相協同工作。 - 分散式選舉:需要大部分哨兵都同意,才能進行主備切換。 - 高可用:即使部分哨兵節點宕機了,哨兵叢集還是能正常工作。 > `坑:` 當主節點發生故障時,需要進行主備切換,可能會導致資料丟失。 ### 非同步複製資料導致的資料丟失 主節點非同步同步資料給備用節點的過程中,主節點宕機了,導致有部分資料未同步到備用節點。而這個從節點又被選舉為主節點,這個時候就有部分資料丟失了。 ### 腦裂導致的資料丟失 主節點所在機器脫離了叢集網路,實際上自身還是執行著的。但哨兵選舉出了備用節點作為主節點,這個時候就有兩個主節點都在執行,相當於兩個大腦在指揮這個叢集幹活,但到底聽誰的呢?這個就是腦裂。 那怎麼腦裂怎麼會導致資料丟失呢?如果發生腦裂後,客戶端還沒來得及切換到新的主節點,連的還是第一個主節點,那麼有些資料還是寫入到了第一個主節點裡面,新的主節點沒有這些資料。那等到第一個主節點恢復後,會被作為備用節點連到叢集環境,而且自身資料會被清空,重新從新的主節點複製資料。而新的主節點因沒有客戶端之前寫入的資料,所以導致資料丟失了一部分。 ### 避坑指南 - 配置 min-slaves-to-write 1,表示至少有一個備用節點。 - 配置 min-slaves-max-lag 10,表示資料複製和同步的延遲不能超過 10 秒。最多丟失 10 秒的資料 注意:`快取雪崩`、`快取穿透`、`快取擊穿`並不是分散式所獨有的,單機的時候也會出現。所以不在分散式的坑之列。 # 三、分庫分表的坑 ## 1.分庫分表的坑之擴容 #### 分庫、分表、垂直拆分和水平拆分 - **分庫:** 因一個數據庫支援的最高併發訪問數是有限的,可以將一個數據庫的資料拆分到多個庫中,來增加最高併發訪問數。 - **分表:** 因一張表的資料量太大,用索引來查詢資料都搞不定了,所以可以將一張表的資料拆分到多張表,查詢時,只用查拆分後的某一張表,SQL 語句的查詢效能得到提升。 - 分庫分表優勢:分庫分表後,承受的併發增加了多倍;磁碟使用率大大降低;單表資料量減少,SQL 執行效率明顯提升。 - **水平拆分:** 把一個表的資料拆分到多個數據庫,每個資料庫中的表結構不變。用多個庫抗更高的併發。比如訂單表每個月有500萬條資料累計,每個月都可以進行水平拆分,將上個月的資料放到另外一個數據庫。 - **垂直拆分:** 把一個有很多欄位的表,拆分成多張表到同一個庫或多個庫上面。高頻訪問欄位放到一張表,低頻訪問的欄位放到另外一張表。利用資料庫快取來快取高頻訪問的行資料。比如將一張很多欄位的訂單表拆分成幾張表分別存不同的欄位(可以有冗餘欄位)。 - **分庫、分表的方式:** - 根據租戶來分庫、分表。 - 利用時間範圍來分庫、分表。 - 利用 ID 取模來分庫、分表。 > `坑:`分庫分表是一個運維層面需要做的事情,有時會採取凌晨宕機開始升級。可能熬夜到天亮,結果升級失敗,則需要回滾,其實對技術團隊都是一種煎熬。 #### 怎麼做成自動的來節省分庫分表的時間? - 雙寫遷移方案:遷移時,新資料的增刪改操作在新庫和老庫都做一遍。 - 使用分庫分表工具 Sharding-jdbc 來完成分庫分表的累活。 - 使用程式來對比兩個庫的資料是否一致,直到資料一致。 > `坑:` 分庫分表看似光鮮亮麗,但分庫分表會引入什麼新的問題呢? #### 垂直拆分帶來的問題 - 依然存在單表資料量過大的問題。 - 部分表無法關聯查詢,只能通過介面聚合方式解決,提升了開發的複雜度。 - 分散式事處理複雜。 #### 水平拆分帶來的問題 - 跨庫的關聯查詢效能差。 - 資料多次擴容和維護量大。 - 跨分片的事務一致性難以保證。 ## 2.分庫分表的坑之唯一 ID ### 為什麼分庫分表需要唯一 ID - 如果要做分庫分表,則必須得考慮表主鍵 ID 是全域性唯一的,比如有一張訂單表,被分到 A 庫和 B 庫。如果 兩張訂單表都是從 1 開始遞增,那查詢訂單資料時就錯亂了,很多訂單 ID 都是重複的,而這些訂單其實不是同一個訂單。 - 分庫的一個期望結果就是將訪問資料的次數分攤到其他庫,有些場景是需要均勻分攤的,那麼資料插入到多個數據庫的時候就需要交替生成唯一的 ID 來保證請求均勻分攤到所有資料庫。 > `坑:` 唯一 ID 的生成方式有 n 種,各有各的用途,別用錯了。 ### 生成唯一 ID 的原則 - 全域性唯一性 - 趨勢遞增 - 單調遞增 - 資訊保安 ### 生成唯一 ID 的幾種方式 - 資料庫自增 ID。每個資料庫每增加一條記錄,自己的 ID 自增 1。 - 缺點 - 多個庫的 ID 可能重複,這個方案可以直接否掉了,不適合分庫分表後的 ID 生成。 - 資訊不安全 - 適用 `UUID` 唯一 ID。 - 缺點 - UUID 太長、佔用空間大。 - 不具有有序性,作為主鍵時,在寫入資料時,不能產生有順序的 append 操作,只能進行 insert 操作,導致讀取整個 `B+` 樹節點到記憶體,插入記錄後將整個節點寫回磁碟,當記錄佔用空間很大的時候,效能很差。 - 獲取系統當前時間作為唯一 ID。 - 缺點 - 高併發時,1 ms內可能有多個相同的 ID。 - 資訊不安全 - Twitter 的 `snowflake`(雪花演算法):Twitter 開源的分散式 id 生成演算法,64 位的 long 型的 id,分為 4 部分 ![snowflake 演算法](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a62edd50cc104558917c27de40e0151d~tplv-k3u1fbpfcp-zoom-1.image) - 1 bit:不用,統一為 0 - 41 bits:毫秒時間戳,可以表示 69 年的時間。 - 10 bits:5 bits 代表機房 id,5 個 bits 代表機器 id。最多代表 32 個機房,每個機房最多代表 32 臺機器。 - 12 bits:同一毫秒內的 id,最多 4096 個不同 id,自增模式 - 優點: - 毫秒數在高位,自增序列在低位,整個ID都是趨勢遞增的。 - 不依賴資料庫等第三方系統,以服務的方式部署,穩定性更高,生成ID的效能也是非常高的。 - 可以根據自身業務特性分配bit位,非常靈活。 - 缺點: - 強依賴機器時鐘,如果機器上時鐘回撥(可以搜尋 **2017 年閏秒 7:59:60**),會導致發號重複或者服務會處於不可用狀態。 - 百度的 `UIDGenerator` 演算法。 - 基於 Snowflake 的優化演算法。 - 借用未來時間和雙 Buffer 來解決時間回撥與生成效能等問題,同時結合 MySQL 進行 ID 分配。 ![UIDGenerator 演算法](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf4b28af7d3a4646b88fa6ffd2bcf951~tplv-k3u1fbpfcp-zoom-1.image) - 美團的 `Leaf-Snowflake` 演算法。 - 為什麼叫 Leaf(葉子):來自數學家萊布尼茨的一句話:“世界上沒有兩片相同的樹葉”,也就是說這個演算法生成的 ID 是唯一的。 ![圖片來源於美團](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e3803f0b3c8b4370954eee8bb96e1f85~tplv-k3u1fbpfcp-zoom-1.image) - 獲取 id 是通過代理服務訪問資料庫獲取一批 id(號段)。 - 雙緩衝:當前一批的 id 使用 10%時,再訪問資料庫獲取新的一批 id 快取起來,等上批的 id 用完後直接用。 - 優點: - Leaf服務可以很方便的線性擴充套件,效能完全能夠支撐大多數業務場景。 - ID號碼是趨勢遞增的8byte的64位數字,滿足上述資料庫儲存的主鍵要求。 - 容災性高:Leaf服務內部有號段快取,即使DB宕機,短時間內Leaf仍能正常對外提供服務。 - 可以自定義max_id的大小,非常方便業務從原有的ID方式上遷移過來。 - 即使DB宕機,Leaf仍能持續發號一段時間。 - 偶爾的網路抖動不會影響下個號段的更新。 - 缺點: - ID號碼不夠隨機,能夠洩露發號數量的資訊,不太安全。 # 四、分散式事務的坑 ### 怎麼理解事務? - 事務可以簡單理解為要麼這件事情全部做完,要麼這件事情一點都沒做,跟沒發生一樣。 - 在分散式的世界中,存在著各個服務之間相互呼叫,鏈路可能很長,如果有任何一方執行出錯,則需要回滾涉及到的其他服務的相關操作。比如訂單服務下單成功,然後呼叫營銷中心發券介面發了一張代金券,但是微信支付扣款失敗,則需要退回發的那張券,且需要將訂單狀態改為異常訂單。 > `坑`:如何保證分散式中的事務正確執行,是個大難題。 ### 分散式事務的幾種主要方式 - XA 方案(兩階段提交方案) - TCC 方案(try、confirm、cancel) - SAGA 方案 - 可靠訊息最終一致性方案 - 最大努力通知方案 ### XA 方案原理 ![XA 方案](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f7af1e5e568d442aa299b1c231d2405c~tplv-k3u1fbpfcp-zoom-1.image) - 事務管理器負責協調多個數據庫的事務,先問問各個資料庫準備好了嗎?如果準備好了,則在資料庫執行操作,如果任一資料庫沒有準備,則回滾事務。 - 適合單體應用,不適合微服務架構。因為每個服務只能訪問自己的資料庫,不允許交叉訪問其他微服務的資料庫。 ### TCC 方案 - Try 階段:對各個服務的資源做檢測以及對資源進行鎖定或者預留。 - Confirm 階段:各個服務中執行實際的操作。 - Cancel 階段:如果任何一個服務的業務方法執行出錯,需要將之前操作成功的步驟進行回滾。 應用場景: - 跟支付、交易打交道,必須保證資金正確的場景。 - 對於一致性要求高。 缺點: - 但因為要寫很多補償邏輯的程式碼,且不易維護,所以其他場景建議不要這麼做。 ### Sega 方案 基本原理: - 業務流程中的每個步驟若有一個失敗了,則補償前面操作成功的步驟。 適用場景: - 業務流程長、業務流程多。 - 參與者包含其他公司或遺留系統服務。 優勢: - 第一個階段提交本地事務、無鎖、高效能。 - 參與者可非同步執行、高吞吐。 - 補償服務易於實現。 缺點: - 不保證事務的隔離性。 ### 可靠訊息一致性方案 ![可靠訊息一致性方案](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/947ba79f14f34f09927df303ce93ffa5~tplv-k3u1fbpfcp-zoom-1.image) 基本原理: - 利用訊息中介軟體 `RocketMQ` 來實現訊息事務。 - 第一步:A 系統傳送一個訊息到 MQ,MQ將訊息狀態標記為 `prepared`(預備狀態,半訊息),該訊息無法被訂閱。 - 第二步:MQ 響應 A 系統,告訴 A 系統已經接收到訊息了。 - 第三步:A 系統執行本地事務。 - 第四步:若 A 系統執行本地事務成功,將 `prepared` 訊息改為 `commit`(提交事務訊息),B 系統就可以訂閱到訊息了。 - 第五步:MQ 也會定時輪詢所有 `prepared`的訊息,回撥 A 系統,讓 A 系統告訴 MQ 本地事務處理得怎麼樣了,是繼續等待還是回滾。 - 第六步:A 系統檢查本地事務的執行結果。 - 第七步:若 A 系統執行本地事務失敗,則 MQ 收到 `Rollback` 訊號,丟棄訊息。若執行本地事務成功,則 MQ 收到 `Commit` 訊號。 - B 系統收到訊息後,開始執行本地事務,如果執行失敗,則自動不斷重試直到成功。或 B 系統採取回滾的方式,同時要通過其他方式通知 A 系統也進行回滾。 - B 系統需要保證冪等性。 ### 最大努力通知方案 基本原理: - 系統 A 本地事務執行完之後,傳送訊息到 MQ。 - MQ 將訊息持久化。 - 系統 B 如果執行本地事務失敗,則`最大努力服務`會定時嘗試重新呼叫系統 B,儘自己最大的努力讓系統 B 重試,重試多次後,還是不行就只能放棄了。轉到開發人員去排查以及後續人工補償。 ### 幾種方案如何選擇 - 跟支付、交易打交道,優先 TCC。 - 大型系統,但要求不那麼嚴格,考慮 訊息事務或 SAGA 方案。 - 單體應用,建議 XA 兩階段提交就可以了。 - 最大努力通知方案建議都加上,畢竟不可能一出問題就交給開發排查,先重試幾次看能不能成功。 ## 寫在最後 分散式還有很多坑,這篇只是一個小小的總結,從這些坑中,我們也知道分散式有它的優勢也有它的劣勢,那到底該不該用分散式,完全取決於業務、時間、成本以及開發團隊的綜合實力。後續我會繼續分享分散式中的一些底層原理,當然也少不了分享一些避坑指南。 參考資料: 美團的 Leaf-Snowflake 演算法。 百度的 UIDGenerator 演算法。 Advanced-java > 你好,我是`悟空哥`,**「7年專案開發經驗,全棧工程師,開發組長,超喜歡圖解程式設計底層原理」**。 我還`手寫了 2 個小程式`,`Java 刷題小程式`,`PMP 刷題小程式`,點選我的公眾號選單開啟! 另外有 111 本架構師資料以及 1000 道 Java 面試題,都整理成了PDF。 可以關注公眾號 **「悟空聊架構」** 回覆 `悟空` 領取優質資料。 **「轉發->在看->點贊->收藏->評論!!!」** 是對我最大的支援! **《Java併發必知必會》系列:** [1.反制面試官 | 14張原理圖 | 再也不怕被問 volatile!](https://juejin.im/post/6861885337568804871) [2.程式設計師深夜慘遭老婆鄙視,原因竟是CAS原理太簡單?](https://juejin.im/post/6863799243182702599) [3.用積木講解ABA原理 | 老婆居然又聽懂了!](https://juejin.im/post/6864945088721027079) [4.全網最細 | 21張圖帶你領略集合的執行緒不安全](https://juejin.im/post/6866444584688451591) [5.5000字 | 24張圖帶你徹底理解Java中的21種鎖](https://juejin.im/post/6867922895536914446) [6.乾貨 | 一口氣說出18種佇列(Queue),面試穩了](https://juejin.im/post/6870298844425371655) [