吐血總結(jié)了分布式各種問題
本文公眾號來源:悟空聊架構(gòu) 作者:悟空聊架構(gòu) 本文已收錄至我的GitHub
本篇主要內(nèi)容如下:

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

解決方案:
準(zhǔn)備好批量重導(dǎo)的程序 手動將消息閑時批量重導(dǎo)

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

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

基本原理:
利用消息中間件 RocketMQ來實(shí)現(xiàn)消息事務(wù)。第一步:A 系統(tǒng)發(fā)送一個消息到 MQ,MQ將消息狀態(tài)標(biāo)記為 prepared(預(yù)備狀態(tài),半消息),該消息無法被訂閱。第二步:MQ 響應(yīng) A 系統(tǒng),告訴 A 系統(tǒng)已經(jīng)接收到消息了。 第三步:A 系統(tǒng)執(zhí)行本地事務(wù)。 第四步:若 A 系統(tǒng)執(zhí)行本地事務(wù)成功,將 prepared消息改為commit(提交事務(wù)消息),B 系統(tǒng)就可以訂閱到消息了。第五步:MQ 也會定時輪詢所有 prepared的消息,回調(diào) A 系統(tǒng),讓 A 系統(tǒng)告訴 MQ 本地事務(wù)處理得怎么樣了,是繼續(xù)等待還是回滾。第六步:A 系統(tǒng)檢查本地事務(wù)的執(zhí)行結(jié)果。 第七步:若 A 系統(tǒng)執(zhí)行本地事務(wù)失敗,則 MQ 收到 Rollback信號,丟棄消息。若執(zhí)行本地事務(wù)成功,則 MQ 收到Commit信號。B 系統(tǒng)收到消息后,開始執(zhí)行本地事務(wù),如果執(zhí)行失敗,則自動不斷重試直到成功?;?B 系統(tǒng)采取回滾的方式,同時要通過其他方式通知 A 系統(tǒng)也進(jìn)行回滾。 B 系統(tǒng)需要保證冪等性。
最大努力通知方案
基本原理:
系統(tǒng) A 本地事務(wù)執(zhí)行完之后,發(fā)送消息到 MQ。 MQ 將消息持久化。 系統(tǒng) B 如果執(zhí)行本地事務(wù)失敗,則 最大努力服務(wù)會定時嘗試重新調(diào)用系統(tǒng) B,盡自己最大的努力讓系統(tǒng) B 重試,重試多次后,還是不行就只能放棄了。轉(zhuǎn)到開發(fā)人員去排查以及后續(xù)人工補(bǔ)償。
幾種方案如何選擇
跟支付、交易打交道,優(yōu)先 TCC。 大型系統(tǒng),但要求不那么嚴(yán)格,考慮 消息事務(wù)或 SAGA 方案。 單體應(yīng)用,建議 XA 兩階段提交就可以了。 最大努力通知方案建議都加上,畢竟不可能一出問題就交給開發(fā)排查,先重試幾次看能不能成功。
寫在最后
分布式還有很多坑,這篇只是一個小小的總結(jié),從這些坑中,我們也知道分布式有它的優(yōu)勢也有它的劣勢,那到底該不該用分布式,完全取決于業(yè)務(wù)、時間、成本以及開發(fā)團(tuán)隊(duì)的綜合實(shí)力。
原創(chuàng)電子書原創(chuàng)思維導(dǎo)圖
掃碼或微信搜 Java3y?回復(fù)「888」領(lǐng)取1000+頁原創(chuàng)電子書和思維導(dǎo)圖。
![]() |
|





