分布式系統(tǒng)遇到的十個(gè)問題
前言
我們都在討論分布式,特別是面試的時(shí)候,不管是招初級(jí)軟件工程師還是高級(jí),都會(huì)要求懂分布式,甚至要求用過。傳得沸沸揚(yáng)揚(yáng)的分布式到底是什么東東,有什么優(yōu)勢(shì)?
借用火影忍術(shù)
風(fēng)遁·螺旋手里劍
看過火影的同學(xué)肯定知道漩渦鳴人的招牌忍術(shù):多重影分身之術(shù)。
- 這個(gè)術(shù)有一個(gè)特別厲害的地方,過程和心得:多個(gè)分身的感受和經(jīng)歷都是相通的。比如 A 分身去找卡卡西(鳴人的老師)請(qǐng)教問題,那么其他分身也會(huì)知道 A 分身問的什么問題。
- 漩渦鳴人?有另外一個(gè)超級(jí)厲害的忍術(shù),需要由幾個(gè)影分身完成:風(fēng)遁·螺旋手里劍。這個(gè)忍術(shù)是靠三個(gè)鳴人一起協(xié)作完成的。
這兩個(gè)忍術(shù)和分布式有什么關(guān)系?
- 分布在不同地方的系統(tǒng)或服務(wù),是彼此相互關(guān)聯(lián)的。
- 分布式系統(tǒng)是分工合作的。
案例:
- 比如 Redis 的哨兵機(jī)制?,可以知道集群環(huán)境下哪臺(tái)Redis 節(jié)點(diǎn)掛了。
- Kafka的Leader 選舉機(jī)制?,如果某個(gè)節(jié)點(diǎn)掛了,會(huì)從follower 中重新選舉一個(gè) leader 出來。(leader 作為寫數(shù)據(jù)的入口,follower 作為讀的入口)
那多重影分身之術(shù)有什么缺點(diǎn)?
- 會(huì)消耗大量的查克拉。分布式系統(tǒng)同樣具有這個(gè)問題,需要幾倍的資源來支持。
對(duì)分布式的通俗理解
- 是一種工作方式
- 若干獨(dú)立計(jì)算機(jī)的集合,這些計(jì)算機(jī)對(duì)于用戶來說就像單個(gè)相關(guān)系統(tǒng)
- 將不同的業(yè)務(wù)分布在不同的地方
優(yōu)勢(shì)可以從兩方面考慮:一個(gè)是宏觀,一個(gè)是微觀。
- 宏觀層面:多個(gè)功能模塊糅合在一起的系統(tǒng)進(jìn)行服務(wù)拆分,來解耦服務(wù)間的調(diào)用。
- 微觀層面:將模塊提供的服務(wù)分布到不同的機(jī)器或容器里,來擴(kuò)大服務(wù)力度。
任何事物有陰必有陽,那分布式又會(huì)帶來哪些問題呢?
- 需要更多優(yōu)質(zhì)人才懂分布式,人力成本增加
- 架構(gòu)設(shè)計(jì)變得異常復(fù)雜,學(xué)習(xí)成本高
- 運(yùn)維部署和維護(hù)成本顯著增加
- 多服務(wù)間鏈路變長(zhǎng),開發(fā)排查問題難度加大
- 環(huán)境高可靠性問題
- 數(shù)據(jù)冪等性問題
- 數(shù)據(jù)的順序問題
- 等等
講到分布式不得不知道 CAP 定理和 Base 理論,這里給不知道的同學(xué)做一個(gè)掃盲。
CAP 定理
在理論計(jì)算機(jī)科學(xué)中,CAP 定理指出對(duì)于一個(gè)分布式計(jì)算系統(tǒng)來說,不可能通是滿足以下三點(diǎn):
- 一致性(Consistency)
- 所有節(jié)點(diǎn)訪問同一份最新的數(shù)據(jù)副本。
- 可用性(Availability)
- 每次請(qǐng)求都能獲取到非錯(cuò)的響應(yīng),但不保證獲取的數(shù)據(jù)為最新數(shù)據(jù)
- 分區(qū)容錯(cuò)性(Partition tolerance)
- 不能在時(shí)限內(nèi)達(dá)成數(shù)據(jù)一致性,就意味著發(fā)生了分區(qū)的情況,必須就當(dāng)前操作在 C 和 A 之間做出選擇)
BASE 理論
BASE 是 Basically Available(基本可用)、Soft state(軟狀態(tài))和 Eventually consistent(最終一致性)三個(gè)短語的縮寫。BASE 理論是對(duì) CAP 中 AP 的一個(gè)擴(kuò)展,通過犧牲強(qiáng)一致性來獲得可用性,當(dāng)出現(xiàn)故障允許部分不可用但要保證核心功能可用,允許數(shù)據(jù)在一段時(shí)間內(nèi)是不一致的,但最終達(dá)到一致狀態(tài)。滿足 BASE 理論的事務(wù),我們稱之為柔性事務(wù)。
- 基本可用 :分布式系統(tǒng)在出現(xiàn)故障時(shí),允許損失部分可用功能,保證核心功能可用。如電商網(wǎng)址交易付款出現(xiàn)問題來,商品依然可以正常瀏覽。
- 軟狀態(tài):由于不要求強(qiáng)一致性,所以BASE允許系統(tǒng)中存在中間狀態(tài)(也叫軟狀態(tài)),這個(gè)狀態(tài)不影響系統(tǒng)可用性,如訂單中的“支付中”、“數(shù)據(jù)同步中”等狀態(tài),待數(shù)據(jù)最終一致后狀態(tài)改為“成功”狀態(tài)。
- 最終一致性:最終一致是指的經(jīng)過一段時(shí)間后,所有節(jié)點(diǎn)數(shù)據(jù)都將會(huì)達(dá)到一致。如訂單的“支付中”狀態(tài),最終會(huì)變?yōu)椤爸Ц冻晒Α被蛘摺爸Ц妒 ?,使訂單狀態(tài)與實(shí)際交易結(jié)果達(dá)成一致,但需要一定時(shí)間的延遲、等待。
一、分布式消息隊(duì)列的坑
消息隊(duì)列如何做分布式?
將消息隊(duì)列里面的消息分?jǐn)偟蕉鄠€(gè)節(jié)點(diǎn)(指某臺(tái)機(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ù)的前后一致性也能接受。
場(chǎng)景分析
RabbitMQ、RocketMQ、Kafka 消息隊(duì)列中間件都有可能出現(xiàn)消息重復(fù)消費(fèi)問題。這種問題并不是 MQ 自己保證的,而是需要開發(fā)人員來保證。
這幾款消息隊(duì)列中間都是是全球最牛的分布式消息隊(duì)列,那肯定考慮到了消息的冪等性。我們以 Kafka 為例,看看 Kafka 是怎么保證消息隊(duì)列的冪等性。
Kafka 有一個(gè) 偏移量 的概念,代表著消息的序號(hào),每條消息寫到消息隊(duì)列都會(huì)有一個(gè)偏移量,消費(fèi)者消費(fèi)了數(shù)據(jù)之后,每過一段固定的時(shí)間,就會(huì)把消費(fèi)過的消息的偏移量提交一下,表示已經(jīng)消費(fèi)過了,下次消費(fèi)就從偏移量后面開始消費(fèi)。
坑:當(dāng)消費(fèi)完消息后,還沒來得及提交偏移量,系統(tǒng)就被關(guān)機(jī)了,那么未提交偏移量的消息則會(huì)再次被消費(fèi)。
如下圖所示,隊(duì)列中的數(shù)據(jù) A、B、C,對(duì)應(yīng)的偏移量分別為 100、101、102,都被消費(fèi)者消費(fèi)了,但是只有數(shù)據(jù) A 的偏移量 100 提交成功,另外 2 個(gè)偏移量因系統(tǒng)重啟而導(dǎo)致未及時(shí)提交。
系統(tǒng)重啟,偏移量未提交
重啟后,消費(fèi)者又是拿偏移量 100 以后的數(shù)據(jù),從偏移量 101 開始拿消息。所以數(shù)據(jù) B 和數(shù)據(jù) C 被重復(fù)消息。
如下圖所示:
重啟后,重復(fù)消費(fèi)消息
避坑指南
- 微信支付結(jié)果通知場(chǎng)景
微信官方文檔上提到微信支付通知結(jié)果可能會(huì)推送多次,需要開發(fā)者自行保證冪等性。第一次我們可以直接修改訂單狀態(tài)(如支付中 -> 支付成功),第二次就根據(jù)訂單狀態(tài)來判斷,如果不是支付中,則不進(jìn)行訂單處理邏輯。
- 插入數(shù)據(jù)庫(kù)場(chǎng)景
- 每次插入數(shù)據(jù)時(shí),先檢查下數(shù)據(jù)庫(kù)中是否有這條數(shù)據(jù)的主鍵 id,如果有,則進(jìn)行更新操作。
- 寫 Redis 場(chǎng)景
- Redis 的 Set 操作天然冪等性,所以不用考慮 Redis 寫數(shù)據(jù)的問題。
- 其他場(chǎng)景方案
- 生產(chǎn)者發(fā)送每條數(shù)據(jù)時(shí),增加一個(gè)全局唯一 id,類似訂單 id。每次消費(fèi)時(shí),先去 Redis 查下是否有這個(gè) id,如果沒有,則進(jìn)行正常處理消息,且將 id 存到 Redis。如果查到有這個(gè) id,說明之前消費(fèi)過,則不要進(jìn)行重復(fù)處理這條消息。
- 不同業(yè)務(wù)場(chǎng)景,可能會(huì)有不同的冪等性方案,大家選擇合適的即可,上面的幾種方案只是提供常見的解決思路。
2. 消息隊(duì)列的坑之消息丟失
坑:消息丟失會(huì)帶來什么問題?如果是訂單下單、支付結(jié)果通知、扣費(fèi)相關(guān)的消息丟失,則可能造成財(cái)務(wù)損失,如果量很大,就會(huì)給甲方帶來巨大損失。
那消息隊(duì)列是否能保證消息不丟失呢?答案:否。主要有三種場(chǎng)景會(huì)導(dǎo)致消息丟失。
消息隊(duì)列之消息丟失
(1)生產(chǎn)者存放消息的過程中丟失消息
生產(chǎn)者丟失消息
解決方案
- 事務(wù)機(jī)制(不推薦,異步方式)
對(duì)于 RabbitMQ 來說,生產(chǎn)者發(fā)送數(shù)據(jù)之前開啟 RabbitMQ 的事務(wù)機(jī)制channel.txselect ,如果消息沒有進(jìn)隊(duì)列,則生產(chǎn)者受到異常報(bào)錯(cuò),并進(jìn)行回滾 channel.txRollback,然后重試發(fā)送消息;如果收到了消息,則可以提交事務(wù) channel.txCommit。但這是一個(gè)同步的操作,會(huì)影響性能。
- confirm 機(jī)制(推薦,異步方式)
我們可以采用另外一種模式:confirm 模式來解決同步機(jī)制的性能問題。每次生產(chǎn)者發(fā)送的消息都會(huì)分配一個(gè)唯一的 id,如果寫入到了 RabbitMQ 隊(duì)列中,則 RabbitMQ 會(huì)回傳一個(gè) ack 消息,說明這個(gè)消息接收成功。如果 RabbitMQ 沒能處理這個(gè)消息,則回調(diào) nack 接口。說明需要重試發(fā)送消息。
也可以自定義超時(shí)時(shí)間 + 消息 id 來實(shí)現(xiàn)超時(shí)等待后重試機(jī)制。但可能出現(xiàn)的問題是調(diào)用 ack 接口時(shí)失敗了,所以會(huì)出現(xiàn)消息被發(fā)送兩次的問題,這個(gè)時(shí)候就需要保證消費(fèi)者消費(fèi)消息的冪等性。
事務(wù)模式 和 confirm 模式的區(qū)別:
- 事務(wù)機(jī)制是同步的,提交事務(wù)后悔被阻塞直到提交事務(wù)完成后。
- confirm 模式異步接收通知,但可能接收不到通知。需要考慮接收不到通知的場(chǎng)景。
(2)消息隊(duì)列丟失消息
消息隊(duì)列丟失消息
消息隊(duì)列的消息可以放到內(nèi)存中,或?qū)?nèi)存中的消息轉(zhuǎn)到硬盤(比如數(shù)據(jù)庫(kù))中,一般都是內(nèi)存和硬盤中都存有消息。如果只是放在內(nèi)存中,那么當(dāng)機(jī)器重啟了,消息就全部丟失了。如果是硬盤中,則可能存在一種極端情況,就是將內(nèi)存中的數(shù)據(jù)轉(zhuǎn)換到硬盤的期間中,消息隊(duì)列出問題了,未能將消息持久化到硬盤。
解決方案
- 創(chuàng)建Queue 的時(shí)候?qū)⑵湓O(shè)置為持久化。
- 發(fā)送消息的時(shí)候?qū)⑾⒌膁eliveryMode 設(shè)置為 2 。
- 開啟生產(chǎn)者confirm 模式,可以重試發(fā)送消息。
(3)消費(fèi)者丟失消息
消費(fèi)者丟失消息
消費(fèi)者剛拿到數(shù)據(jù),還沒開始處理消息,結(jié)果進(jìn)程因?yàn)楫惓M顺隽?,消費(fèi)者沒有機(jī)會(huì)再次拿到消息。
解決方案
- 關(guān)閉 RabbitMQ 的自動(dòng)ack?,每次生產(chǎn)者將消息寫入消息隊(duì)列后,就自動(dòng)回傳一個(gè)ack 給生產(chǎn)者。
- 消費(fèi)者處理完消息再主動(dòng)ack,告訴消息隊(duì)列我處理完了。
問題: 那這種主動(dòng) ack 有什么漏洞了?如果 主動(dòng) ack 的時(shí)候掛了,怎么辦?
則可能會(huì)被再次消費(fèi),這個(gè)時(shí)候就需要冪等處理了。
問題: 如果這條消息一直被重復(fù)消費(fèi)怎么辦?
則需要有加上重試次數(shù)的監(jiān)測(cè),如果超過一定次數(shù)則將消息丟失,記錄到異常表或發(fā)送異常通知給值班人員。
(4)RabbitMQ 消息丟失總結(jié)
RabbitMQ 丟失消息的處理方案
(5)Kafka 消息丟失
場(chǎng)景:Kafka 的某個(gè) broker(節(jié)點(diǎn))宕機(jī)了,重新選舉 leader (寫入的節(jié)點(diǎn))。如果 leader 掛了,follower 還有些數(shù)據(jù)未同步完,則 follower 成為 leader 后,消息隊(duì)列會(huì)丟失一部分?jǐn)?shù)據(jù)。
解決方案
- 給 topic 設(shè)置replication.factor 參數(shù),值必須大于 1,要求每個(gè) partition 必須有至少 2 個(gè)副本。
- 給 kafka 服務(wù)端設(shè)置min.insyc.replicas 必須大于 1,表示一個(gè) leader 至少一個(gè) follower 還跟自己保持聯(lián)系。
3. 消息隊(duì)列的坑之消息亂序
坑: 用戶先下單成功,然后取消訂單,如果順序顛倒,則最后數(shù)據(jù)庫(kù)里面會(huì)有一條下單成功的訂單。
RabbitMQ 場(chǎng)景:
- 生產(chǎn)者向消息隊(duì)列按照順序發(fā)送了 2 條消息,消息1:增加數(shù)據(jù) A,消息2:刪除數(shù)據(jù) A。
- 期望結(jié)果:數(shù)據(jù) A 被刪除。
- 但是如果有兩個(gè)消費(fèi)者,消費(fèi)順序是:消息2、消息 1。則最后結(jié)果是增加了數(shù)據(jù) A。
RabbitMQ消息亂序場(chǎng)景
RabbitMQ 消息亂序場(chǎng)景
RabbitMQ 解決方案:
- 將 Queue 進(jìn)行拆分,創(chuàng)建多個(gè)內(nèi)存 Queue,消息 1 和 消息 2 進(jìn)入同一個(gè) Queue。
- 創(chuàng)建多個(gè)消費(fèi)者,每一個(gè)消費(fèi)者對(duì)應(yīng)一個(gè) Queue。
RabbitMQ 解決方案
Kafka 場(chǎng)景:
- 創(chuàng)建了 topic,有 3 個(gè) partition。
- 創(chuàng)建一條訂單記錄,訂單 id 作為 key,訂單相關(guān)的消息都丟到同一個(gè) partition 中,同一個(gè)生產(chǎn)者創(chuàng)建的消息,順序是正確的。
- 為了快速消費(fèi)消息,會(huì)創(chuàng)建多個(gè)消費(fèi)者去處理消息,而為了提高效率,每個(gè)消費(fèi)者可能會(huì)創(chuàng)建多個(gè)線程來并行的去拿消息及處理消息,處理消息的順序可能就亂序了。
Kafka 消息丟失場(chǎng)景
Kafka 解決方案:
- 解決方案和 RabbitMQ 類似,利用多個(gè) 內(nèi)存 Queue,每個(gè)線程消費(fèi) 1個(gè) Queue。
- 具有相同 key 的消息 進(jìn)同一個(gè) Queue。
Kafka 消息亂序解決方案
4. 消息隊(duì)列的坑之消息積壓
消息積壓:消息隊(duì)列里面有很多消息來不及消費(fèi)。
場(chǎng)景 1: 消費(fèi)端出了問題,比如消費(fèi)者都掛了,沒有消費(fèi)者來消費(fèi)了,導(dǎo)致消息在隊(duì)列里面不斷積壓。
場(chǎng)景 2: 消費(fèi)端出了問題,比如消費(fèi)者消費(fèi)的速度太慢了,導(dǎo)致消息不斷積壓。
坑:比如線上正在做訂單活動(dòng),下單全部走消息隊(duì)列,如果消息不斷積壓,訂單都沒有下單成功,那么將會(huì)損失很多交易。
消息隊(duì)列之消息積壓
解決方案:解鈴還須系鈴人
- 修復(fù)代碼層面消費(fèi)者的問題,確保后續(xù)消費(fèi)速度恢復(fù)或盡可能加快消費(fèi)的速度。
- 停掉現(xiàn)有的消費(fèi)者。
- 臨時(shí)建立好原先 5 倍的 Queue 數(shù)量。
- 臨時(shí)建立好原先 5 倍數(shù)量的 消費(fèi)者。
- 將堆積的消息全部轉(zhuǎn)入臨時(shí)的 Queue,消費(fèi)者來消費(fèi)這些 Queue。
消息積壓解決方案
5. 消息隊(duì)列的坑之消息過期失效
坑:RabbitMQ 可以設(shè)置過期時(shí)間,如果消息超過一定的時(shí)間還沒有被消費(fèi),則會(huì)被 RabbitMQ 給清理掉。消息就丟失了。
消息過期失效
解決方案:
- 準(zhǔn)備好批量重導(dǎo)的程序
- 手動(dòng)將消息閑時(shí)批量重導(dǎo)
消息過期失效解決方案
6. 消息隊(duì)列的坑之隊(duì)列寫滿
坑:當(dāng)消息隊(duì)列因消息積壓導(dǎo)致的隊(duì)列快寫滿,所以不能接收更多的消息了。生產(chǎn)者生產(chǎn)的消息將會(huì)被丟棄。
解決方案:
- 判斷哪些是無用的消息,RabbitMQ 可以進(jìn)行Purge Message 操作。
- 如果是有用的消息,則需要將消息快速消費(fèi),將消息里面的內(nèi)容轉(zhuǎn)存到數(shù)據(jù)庫(kù)。
- 準(zhǔn)備好程序?qū)⑥D(zhuǎn)存在數(shù)據(jù)庫(kù)中的消息再次重導(dǎo)到消息隊(duì)列。
閑時(shí)重導(dǎo)消息到消息隊(duì)列。
二、分布式緩存的坑
在高頻訪問數(shù)據(jù)庫(kù)的場(chǎng)景中,我們會(huì)在業(yè)務(wù)層和數(shù)據(jù)層之間加入一套緩存機(jī)制,來分擔(dān)數(shù)據(jù)庫(kù)的訪問壓力,畢竟訪問磁盤 I/O 的速度是很慢的。比如利用緩存來查數(shù)據(jù),可能5ms就能搞定,而去查數(shù)據(jù)庫(kù)可能需要 50 ms,差了一個(gè)數(shù)量級(jí)。而在高并發(fā)的情況下,數(shù)據(jù)庫(kù)還有可能對(duì)數(shù)據(jù)進(jìn)行加鎖,導(dǎo)致訪問數(shù)據(jù)庫(kù)的速度更慢。
分布式緩存我們用的最多的就是 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é)將故障信息報(bào)警給運(yùn)維人員。
- 故障轉(zhuǎn)移:負(fù)責(zé)將主節(jié)點(diǎn)轉(zhuǎn)移到備用節(jié)點(diǎn)上。
- 配置中心:通知客戶端更新主節(jié)點(diǎn)地址。
- 分布式:有多個(gè)哨兵分布在每個(gè)主備節(jié)點(diǎn)上,互相協(xié)同工作。
- 分布式選舉:需要大部分哨兵都同意,才能進(jìn)行主備切換。
- 高可用:即使部分哨兵節(jié)點(diǎn)宕機(jī)了,哨兵集群還是能正常工作。
坑: 當(dāng)主節(jié)點(diǎn)發(fā)生故障時(shí),需要進(jìn)行主備切換,可能會(huì)導(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)。而這個(gè)從節(jié)點(diǎn)又被選舉為主節(jié)點(diǎn),這個(gè)時(shí)候就有部分?jǐn)?shù)據(jù)丟失了。
腦裂導(dǎo)致的數(shù)據(jù)丟失
主節(jié)點(diǎn)所在機(jī)器脫離了集群網(wǎng)絡(luò),實(shí)際上自身還是運(yùn)行著的。但哨兵選舉出了備用節(jié)點(diǎn)作為主節(jié)點(diǎn),這個(gè)時(shí)候就有兩個(gè)主節(jié)點(diǎn)都在運(yùn)行,相當(dāng)于兩個(gè)大腦在指揮這個(gè)集群干活,但到底聽誰的呢?這個(gè)就是腦裂。
那怎么腦裂怎么會(huì)導(dǎo)致數(shù)據(jù)丟失呢?如果發(fā)生腦裂后,客戶端還沒來得及切換到新的主節(jié)點(diǎn),連的還是第一個(gè)主節(jié)點(diǎn),那么有些數(shù)據(jù)還是寫入到了第一個(gè)主節(jié)點(diǎn)里面,新的主節(jié)點(diǎn)沒有這些數(shù)據(jù)。那等到第一個(gè)主節(jié)點(diǎn)恢復(fù)后,會(huì)被作為備用節(jié)點(diǎn)連到集群環(huán)境,而且自身數(shù)據(jù)會(huì)被清空,重新從新的主節(jié)點(diǎn)復(fù)制數(shù)據(jù)。而新的主節(jié)點(diǎn)因沒有客戶端之前寫入的數(shù)據(jù),所以導(dǎo)致數(shù)據(jù)丟失了一部分。
避坑指南
- 配置 min-slaves-to-write 1,表示至少有一個(gè)備用節(jié)點(diǎn)。
- 配置 min-slaves-max-lag 10,表示數(shù)據(jù)復(fù)制和同步的延遲不能超過 10 秒。最多丟失 10 秒的數(shù)據(jù)
注意:緩存雪崩、緩存穿透、緩存擊穿并不是分布式所獨(dú)有的,單機(jī)的時(shí)候也會(huì)出現(xiàn)。所以不在分布式的坑之列。
三、分庫(kù)分表的坑
1.分庫(kù)分表的坑之?dāng)U容
分庫(kù)、分表、垂直拆分和水平拆分
- 分庫(kù):因一個(gè)數(shù)據(jù)庫(kù)支持的最高并發(fā)訪問數(shù)是有限的,可以將一個(gè)數(shù)據(jù)庫(kù)的數(shù)據(jù)拆分到多個(gè)庫(kù)中,來增加最高并發(fā)訪問數(shù)。
- 分表:因一張表的數(shù)據(jù)量太大,用索引來查詢數(shù)據(jù)都搞不定了,所以可以將一張表的數(shù)據(jù)拆分到多張表,查詢時(shí),只用查拆分后的某一張表,SQL 語句的查詢性能得到提升。
- 分庫(kù)分表優(yōu)勢(shì):分庫(kù)分表后,承受的并發(fā)增加了多倍;磁盤使用率大大降低;單表數(shù)據(jù)量減少,SQL 執(zhí)行效率明顯提升。
- 水平拆分:把一個(gè)表的數(shù)據(jù)拆分到多個(gè)數(shù)據(jù)庫(kù),每個(gè)數(shù)據(jù)庫(kù)中的表結(jié)構(gòu)不變。用多個(gè)庫(kù)抗更高的并發(fā)。比如訂單表每個(gè)月有500萬條數(shù)據(jù)累計(jì),每個(gè)月都可以進(jìn)行水平拆分,將上個(gè)月的數(shù)據(jù)放到另外一個(gè)數(shù)據(jù)庫(kù)。
- 垂直拆分:把一個(gè)有很多字段的表,拆分成多張表到同一個(gè)庫(kù)或多個(gè)庫(kù)上面。高頻訪問字段放到一張表,低頻訪問的字段放到另外一張表。利用數(shù)據(jù)庫(kù)緩存來緩存高頻訪問的行數(shù)據(jù)。比如將一張很多字段的訂單表拆分成幾張表分別存不同的字段(可以有冗余字段)。
- 分庫(kù)、分表的方式:
根據(jù)租戶來分庫(kù)、分表。
利用時(shí)間范圍來分庫(kù)、分表。
利用 ID 取模來分庫(kù)、分表。
坑:分庫(kù)分表是一個(gè)運(yùn)維層面需要做的事情,有時(shí)會(huì)采取凌晨宕機(jī)開始升級(jí)??赡馨疽沟教炝?,結(jié)果升級(jí)失敗,則需要回滾,其實(shí)對(duì)技術(shù)團(tuán)隊(duì)都是一種煎熬。
怎么做成自動(dòng)的來節(jié)省分庫(kù)分表的時(shí)間?
- 雙寫遷移方案:遷移時(shí),新數(shù)據(jù)的增刪改操作在新庫(kù)和老庫(kù)都做一遍。
- 使用分庫(kù)分表工具 Sharding-jdbc 來完成分庫(kù)分表的累活。
- 使用程序來對(duì)比兩個(gè)庫(kù)的數(shù)據(jù)是否一致,直到數(shù)據(jù)一致。
坑: 分庫(kù)分表看似光鮮亮麗,但分庫(kù)分表會(huì)引入什么新的問題呢?
垂直拆分帶來的問題
- 依然存在單表數(shù)據(jù)量過大的問題。
- 部分表無法關(guān)聯(lián)查詢,只能通過接口聚合方式解決,提升了開發(fā)的復(fù)雜度。
- 分布式事處理復(fù)雜。
水平拆分帶來的問題
- 跨庫(kù)的關(guān)聯(lián)查詢性能差。
- 數(shù)據(jù)多次擴(kuò)容和維護(hù)量大。
- 跨分片的事務(wù)一致性難以保證。
2.分庫(kù)分表的坑之唯一 ID
為什么分庫(kù)分表需要唯一 ID
- 如果要做分庫(kù)分表,則必須得考慮表主鍵 ID 是全局唯一的,比如有一張訂單表,被分到 A 庫(kù)和 B 庫(kù)。如果 兩張訂單表都是從 1 開始遞增,那查詢訂單數(shù)據(jù)時(shí)就錯(cuò)亂了,很多訂單 ID 都是重復(fù)的,而這些訂單其實(shí)不是同一個(gè)訂單。
- 分庫(kù)的一個(gè)期望結(jié)果就是將訪問數(shù)據(jù)的次數(shù)分?jǐn)偟狡渌麕?kù),有些場(chǎng)景是需要均勻分?jǐn)偟模敲磾?shù)據(jù)插入到多個(gè)數(shù)據(jù)庫(kù)的時(shí)候就需要交替生成唯一的 ID 來保證請(qǐng)求均勻分?jǐn)偟剿袛?shù)據(jù)庫(kù)。
坑: 唯一 ID 的生成方式有 n 種,各有各的用途,別用錯(cuò)了。
生成唯一 ID 的原則
- 全局唯一性
- 趨勢(shì)遞增
- 單調(diào)遞增
- 信息安全
生成唯一 ID 的幾種方式
- 數(shù)據(jù)庫(kù)自增 ID。每個(gè)數(shù)據(jù)庫(kù)每增加一條記錄,自己的 ID 自增 1。
多個(gè)庫(kù)的 ID 可能重復(fù),這個(gè)方案可以直接否掉了,不適合分庫(kù)分表后的 ID 生成。
信息不安全
缺點(diǎn)
- 適用 UUID 唯一 ID。
UUID 太長(zhǎng)、占用空間大。
不具有有序性,作為主鍵時(shí),在寫入數(shù)據(jù)時(shí),不能產(chǎn)生有順序的 append 操作,只能進(jìn)行 insert 操作,導(dǎo)致讀取整個(gè) B+ 樹節(jié)點(diǎn)到內(nèi)存,插入記錄后將整個(gè)節(jié)點(diǎn)寫回磁盤,當(dāng)記錄占用空間很大的時(shí)候,性能很差。
缺點(diǎn)
- 獲取系統(tǒng)當(dāng)前時(shí)間作為唯一 ID。
高并發(fā)時(shí),1 ms內(nèi)可能有多個(gè)相同的 ID。
信息不安全
缺點(diǎn)
- Twitter 的 snowflake(雪花算法):Twitter 開源的分布式 id 生成算法,64 位的 long 型的 id,分為 4 部分
snowflake 算法
基本原理和優(yōu)缺點(diǎn):
1 bit:不用,統(tǒng)一為 0
41 bits:毫秒時(shí)間戳,可以表示 69 年的時(shí)間。
10 bits:5 bits 代表機(jī)房 id,5 個(gè) bits 代表機(jī)器 id。最多代表 32 個(gè)機(jī)房,每個(gè)機(jī)房最多代表 32 臺(tái)機(jī)器。
12 bits:同一毫秒內(nèi)的 id,最多 4096 個(gè)不同 id,自增模式。
優(yōu)點(diǎn):
毫秒數(shù)在高位,自增序列在低位,整個(gè)ID都是趨勢(shì)遞增的。
? 不依賴數(shù)據(jù)庫(kù)等第三方系統(tǒng),以服務(wù)的方式部署,穩(wěn)定性更高,生成ID的性能也是非常高的。
? 可以根據(jù)自身業(yè)務(wù)特性分配bit位,非常靈活。
缺點(diǎn):
? 強(qiáng)依賴機(jī)器時(shí)鐘,如果機(jī)器上時(shí)鐘回?fù)埽梢运阉?2017 年閏秒 7:59:60),會(huì)導(dǎo)致發(fā)號(hào)重復(fù)或者服務(wù)會(huì)處于不可用狀態(tài)。
百度的 UIDGenerator 算法。
- 基于 Snowflake 的優(yōu)化算法。
- 借用未來時(shí)間和雙 Buffer 來解決時(shí)間回?fù)芘c生成性能等問題,同時(shí)結(jié)合 MySQL 進(jìn)行 ID 分配。
- 優(yōu)點(diǎn):解決了時(shí)間回?fù)芎蜕尚阅軉栴}。
- 缺點(diǎn):依賴 MySQL 數(shù)據(jù)庫(kù)。
美團(tuán)的 Leaf-Snowflake 算法。
- 雙緩沖:當(dāng)前一批的 id 使用 10%時(shí),再訪問數(shù)據(jù)庫(kù)獲取新的一批 id 緩存起來,等上批的 id 用完后直接用。
- 優(yōu)點(diǎn):
Leaf服務(wù)可以很方便的線性擴(kuò)展,性能完全能夠支撐大多數(shù)業(yè)務(wù)場(chǎng)景。
ID號(hào)碼是趨勢(shì)遞增的8byte的64位數(shù)字,滿足上述數(shù)據(jù)庫(kù)存儲(chǔ)的主鍵要求。
容災(zāi)性高:Leaf服務(wù)內(nèi)部有號(hào)段緩存,即使DB宕機(jī),短時(shí)間內(nèi)Leaf仍能正常對(duì)外提供服務(wù)。
可以自定義max_id的大小,非常方便業(yè)務(wù)從原有的ID方式上遷移過來。
即使DB宕機(jī),Leaf仍能持續(xù)發(fā)號(hào)一段時(shí)間。
偶爾的網(wǎng)絡(luò)抖動(dòng)不會(huì)影響下個(gè)號(hào)段的更新。
- 缺點(diǎn):
ID號(hào)碼不夠隨機(jī),能夠泄露發(fā)號(hào)數(shù)量的信息,不太安全。
怎么選擇:一般自己的內(nèi)部系統(tǒng),雪花算法足夠,如果還要更加安全可靠,可以選擇百度或美團(tuán)的生成唯一 ID 的方案。
四、分布式事務(wù)的坑
怎么理解事務(wù)?
- 事務(wù)可以簡(jiǎn)單理解為要么這件事情全部做完,要么這件事情一點(diǎn)都沒做,跟沒發(fā)生一樣。
- 在分布式的世界中,存在著各個(gè)服務(wù)之間相互調(diào)用,鏈路可能很長(zhǎng),如果有任何一方執(zhí)行出錯(cuò),則需要回滾涉及到的其他服務(wù)的相關(guān)操作。比如訂單服務(wù)下單成功,然后調(diào)用營(yíng)銷中心發(fā)券接口發(fā)了一張代金券,但是微信支付扣款失敗,則需要退回發(fā)的那張券,且需要將訂單狀態(tài)改為異常訂單。
坑:如何保證分布式中的事務(wù)正確執(zhí)行,是個(gè)大難題。
分布式事務(wù)的幾種主要方式
- XA 方案(兩階段提交方案)
- TCC 方案(try、confirm、cancel)
- SAGA 方案
- 可靠消息最終一致性方案
- 最大努力通知方案
XA 方案原理
XA 方案
- 事務(wù)管理器負(fù)責(zé)協(xié)調(diào)多個(gè)數(shù)據(jù)庫(kù)的事務(wù),先問問各個(gè)數(shù)據(jù)庫(kù)準(zhǔn)備好了嗎?如果準(zhǔn)備好了,則在數(shù)據(jù)庫(kù)執(zhí)行操作,如果任一數(shù)據(jù)庫(kù)沒有準(zhǔn)備,則回滾事務(wù)。
- 適合單體應(yīng)用,不適合微服務(wù)架構(gòu)。因?yàn)槊總€(gè)服務(wù)只能訪問自己的數(shù)據(jù)庫(kù),不允許交叉訪問其他微服務(wù)的數(shù)據(jù)庫(kù)。
TCC 方案
- Try 階段:對(duì)各個(gè)服務(wù)的資源做檢測(cè)以及對(duì)資源進(jìn)行鎖定或者預(yù)留。
- Confirm 階段:各個(gè)服務(wù)中執(zhí)行實(shí)際的操作。
- Cancel 階段:如果任何一個(gè)服務(wù)的業(yè)務(wù)方法執(zhí)行出錯(cuò),需要將之前操作成功的步驟進(jìn)行回滾。
應(yīng)用場(chǎng)景:
- 跟支付、交易打交道,必須保證資金正確的場(chǎng)景。
- 對(duì)于一致性要求高。
缺點(diǎn):
- 但因?yàn)橐獙懞芏嘌a(bǔ)償邏輯的代碼,且不易維護(hù),所以其他場(chǎng)景建議不要這么做。
Sega 方案
基本原理:
- 業(yè)務(wù)流程中的每個(gè)步驟若有一個(gè)失敗了,則補(bǔ)償前面操作成功的步驟。
適用場(chǎng)景:
- 業(yè)務(wù)流程長(zhǎng)、業(yè)務(wù)流程多。
- 參與者包含其他公司或遺留系統(tǒng)服務(wù)。
優(yōu)勢(shì):
- 第一個(gè)階段提交本地事務(wù)、無鎖、高性能。
- 參與者可異步執(zhí)行、高吞吐。
- 補(bǔ)償服務(wù)易于實(shí)現(xiàn)。
缺點(diǎn):
- 不保證事務(wù)的隔離性。
可靠消息一致性方案
可靠消息一致性方案
基本原理:
- 利用消息中間件RocketMQ 來實(shí)現(xiàn)消息事務(wù)。
- 第一步:A 系統(tǒng)發(fā)送一個(gè)消息到 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 也會(huì)定時(shí)輪詢所有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? 信號(hào),丟棄消息。若執(zhí)行本地事務(wù)成功,則 MQ 收到?
?Commit?
? 信號(hào)。 - B 系統(tǒng)收到消息后,開始執(zhí)行本地事務(wù),如果執(zhí)行失敗,則自動(dòng)不斷重試直到成功?;?B 系統(tǒng)采取回滾的方式,同時(shí)要通過其他方式通知 A 系統(tǒng)也進(jìn)行回滾。
- B 系統(tǒng)需要保證冪等性。
最大努力通知方案
基本原理:
- 系統(tǒng) A 本地事務(wù)執(zhí)行完之后,發(fā)送消息到 MQ。
- MQ 將消息持久化。
- 系統(tǒng) B 如果執(zhí)行本地事務(wù)失敗,則最大努力服務(wù)會(huì)定時(shí)嘗試重新調(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ā)排查,先重試幾次看能不能成功。
寫在最后
分布式還有很多坑,這篇只是一個(gè)小小的總結(jié),從這些坑中,我們也知道分布式有它的優(yōu)勢(shì)也有它的劣勢(shì),那到底該不該用分布式,完全取決于業(yè)務(wù)、時(shí)間、成本以及開發(fā)團(tuán)隊(duì)的綜合實(shí)力。后續(xù)我會(huì)繼續(xù)分享分布式中的一些底層原理,當(dāng)然也少不了分享一些避坑指南。