目錄
分布式系統介紹
一、定義與概念
二、分布式系統的特點
三、分布式系統面臨的挑戰
四、分布式系統的常見應用場景
CAP 定理
BASE 理論
BASE理論是如何保證最終一致性的
分布式鎖的常見使用場景有哪些?
1. 防止多節點重復操作
2. 資源互斥訪問
3. 分布式事務
4. 全局 ID 生成器
5. 分布式限流
6. 任務調度與協調
7. 防止緩存雪崩
8. 分布式系統初始化
常見實現方式
避免分布式鎖帶來的死鎖問題?
1. 設置鎖超時時間
2. 按順序獲取鎖
3. 使用可重入鎖
4. 實現鎖的公平性
5. 檢測與恢復機制
6. 原子化操作
7. 鎖粒度控制
8. 心跳機制與健康檢查
9. 事務與補償機制
10. 降級與熔斷策略
分布式環境下高可用解決方案
分布式事務
一、分布式事務的挑戰
二、常見解決方案
1. 兩階段提交(2PC)
2. 三階段提交(3PC)
3. TCC(Try-Confirm-Cancel)
4. 消息隊列(最終一致性)
5. Saga 模式
6. 最大努力通知
三、分布式事務工具
四、最佳實踐
五、對比總結
接口的冪等性/解決重復消費
接口冪等性
解決重復消費
分布式唯一ID?
概述
常見實現方案
1. UUID (Universally Unique Identifier)
2. 數據庫自增 ID
3. Snowflake 算法(雪花算法)
4. Redis 原子操作
5. 數據庫號段模式
6. ULID (Universally Unique Lexicographically Sortable Identifier)
選擇建議
RPC
一、核心概念
二、常見 RPC 框架
三、工作流程
四、核心技術
五、優缺點
六、Java 實現示例(gRPC)
七、適用場景
八、對比 REST API
Protocol Buffers vs Thrift
一、核心特性對比
二、序列化性能
三、IDL(接口定義語言)
Protobuf 示例
Thrift 示例
四、RPC 集成
五、適用場景
六、優缺點總結
Protocol Buffers
Thrift
七、選擇建議
八、性能對比測試
九、總結
分布式系統介紹
一、定義與概念
分布式系統是由多個通過網絡連接的獨立計算機節點組成的系統,這些節點相互協作,共同完成一個或多個任務,對外表現得像一個單一的系統。每個節點都有自己的處理器、內存和存儲,它們通過消息傳遞進行通信和協調。例如,大型電商平臺如淘寶、京東,背后支撐其運行的就是復雜的分布式系統,眾多服務器節點分別負責用戶管理、商品展示、訂單處理等不同功能,協同為用戶提供服務。
二、分布式系統的特點
- 分布性:組件分布在不同的物理或虛擬節點上,這些節點可能位于不同地理位置的數據中心。例如,一家跨國公司的業務系統,其用戶數據可能存儲在位于不同國家的數據中心節點上。
- 并發性:多個節點可能同時處理不同的任務,節點之間需要協調和同步操作,以避免數據沖突等問題。比如在分布式數據庫中,多個節點可能同時對相同的數據進行讀寫操作。
- 故障獨立性:某個節點出現故障不應導致整個系統崩潰,其他節點應能繼續提供部分或全部功能。像云存儲系統,即使個別存儲節點發生故障,用戶仍然可以訪問和存儲數據。
- 透明性:對于用戶和應用程序而言,分布式系統的復雜性被隱藏,它們看到的就像一個單一的系統。例如,用戶在使用在線支付功能時,無需關心背后是由多少個分布式節點協同完成支付處理的。
三、分布式系統面臨的挑戰
- 網絡問題
- 網絡延遲:不同節點間的數據傳輸需要時間,可能導致操作響應緩慢。比如,從歐洲的數據中心向亞洲的數據中心請求數據,由于物理距離遠,網絡延遲較高。
- 網絡分區:網絡故障可能導致部分節點間通信中斷,形成網絡分區,使系統出現數據不一致等問題。
- 數據一致性
在分布式環境下,確保多個節點上的數據一致性是一大挑戰。例如,在分布式數據庫的讀寫操作中,不同節點可能由于網絡延遲等原因,在同一時刻的數據狀態不一致。 - 故障處理
由于節點眾多,某個節點發生故障的可能性增加。需要設計有效的故障檢測、恢復和容錯機制,以保證系統的可用性。例如,服務器硬件故障、軟件崩潰等情況都需要系統能夠快速應對。
四、分布式系統的常見應用場景
- 大型網站和互聯網應用:如社交媒體平臺、在線游戲平臺等,需要處理海量用戶的并發請求,通過分布式系統可以實現水平擴展,提高系統的處理能力和可用性。
- 大數據處理:分布式計算框架如 Hadoop、Spark 用于處理大規模數據集,將數據和計算任務分布到多個節點上并行處理,加快數據處理速度。
- 云計算平臺:提供彈性計算、存儲和網絡等服務,背后依靠分布式系統實現資源的動態分配和管理,滿足不同用戶的需求。
分布式系統一定是由多個節點組成的系統。 其中節點指的是計算機服務器,而且這些節點一般不是孤立的,而是互通的。
微服務架構是一種架構風格,它提倡將單一應用程序劃分成一組小的服務,每個服務運行在自己進程中,服務間通信機制采用輕量級通訊機制(通常是基于 HTTP 的 RESTful API ) 。
CAP 定理
- 定義:CAP 定理指出,在一個分布式系統中,一致性(Consistency)、可用性(Availability)和分區容錯性(Partition Tolerance)這三個特性不能同時滿足,最多只能同時滿足其中兩個。
- 一致性:所有節點在同一時間具有相同的數據,即更新操作成功并返回客戶端后,所有節點在同一時間的數據完全一致。
- 可用性:系統在任何時候都能響應客戶端的請求,即每個請求都能在有限時間內得到響應,不會出現無限期的等待。
- 分區容錯性:系統能夠在網絡分區的情況下繼續正常運行,即當網絡出現分區(部分節點之間無法通信)時,系統仍然能夠提供服務。
BASE 理論
- 定義:BASE 理論是對 CAP 定理中一致性和可用性權衡的結果,它的核心思想是通過犧牲強一致性來獲得高可用性。BASE 是基本可用(Basically Available)、軟狀態(Soft State)和最終一致性(Eventually Consistent)三個短語的縮寫。
- 基本可用:系統在出現故障時,允許損失部分可用性,但仍然能夠提供基本的服務。例如,在某些情況下,系統可能會降低響應速度或返回部分數據,但不會完全不可用。
- 軟狀態:系統中的數據可以存在中間狀態,并且允許這種中間狀態在一段時間內存在。也就是說,數據不需要在所有節點上立即保持一致,而是可以在一段時間內逐漸達到一致。
- 最終一致性:盡管系統中的數據在一段時間內可能不一致,但最終會達到一致狀態。在最終一致性的系統中,只要客戶端在一段時間后再次請求數據,就能夠得到最新的、一致的數據。
CAP 定理強調了分布式系統中三個關鍵特性之間的權衡關系,而 BASE 理論則提供了一種在實際應用中實現分布式系統的思路,即在滿足分區容錯性的前提下,通過犧牲強一致性來換取高可用性和可擴展性。
BASE理論是如何保證最終一致性的
BASE 理論通過以下幾種方式來保證最終一致性:
- 異步處理與補償機制
- 操作異步執行:在分布式系統中,當一個寫操作發生時,系統不會立即要求所有相關節點都完成數據更新,而是先將操作記錄下來,并異步地將更新請求發送到其他節點。這樣可以避免因等待所有節點同步完成而導致的性能下降和可用性降低。
- 補償操作:如果在異步更新過程中出現錯誤或失敗,系統會采用補償機制來確保數據最終達到一致。例如,通過重試機制重新發送更新請求,或者執行一些補償性的操作來修正數據。
- 數據復制與傳播
- 多副本數據存儲:在分布式系統中,數據通常會在多個節點上進行復制,以提高系統的可用性和容錯能力。當一個節點的數據發生更新時,更新會逐漸傳播到其他副本節點。
- 傳播策略:通過采用合適的傳播策略,如基于時間戳或版本號的更新傳播方式,確保數據在不同節點之間能夠正確地同步。例如,當一個節點接收到一個更新請求時,它會比較請求中的數據版本號與本地數據的版本號,如果請求的版本號更高,則更新本地數據。
- 一致性檢查與修復
- 定期檢查:系統會定期對數據進行一致性檢查,通過比較不同節點上的數據版本、校驗和或其他一致性指標,來發現數據不一致的情況。
- 主動修復:一旦發現數據不一致,系統會采取主動修復措施,例如從其他正確的節點獲取最新的數據來更新不一致的節點,或者通過一些特定的算法來計算出正確的數據狀態并進行修復。
- 事務處理與協調
- 柔性事務:在 BASE 理論中,通常采用柔性事務來處理分布式數據的更新。柔性事務允許在一定程度上放松對事務的嚴格一致性要求,通過使用一些補償性的操作來保證最終一致性。例如,使用 TCC(Try - Confirm - Cancel)模式,先嘗試執行操作,然后根據情況確認或取消操作,如果出現問題則通過補償操作來恢復數據的一致性。
- 分布式事務協調:通過分布式事務協調器來管理和協調不同節點之間的事務處理。協調器會跟蹤事務的執行狀態,并在必要時采取措施來保證事務的最終一致性,如在部分節點出現故障時,協調其他節點進行相應的處理,以確保整個事務能夠正確完成。
分布式鎖的常見使用場景有哪些?
分布式鎖在分布式系統中用于控制多個節點對共享資源的并發訪問,常見使用場景包括:
1. 防止多節點重復操作
- 定時任務冪等性:在分布式系統中,多個節點可能同時觸發同一個定時任務(如數據同步、報表生成),使用分布式鎖可以確保只有一個節點執行任務,避免重復計算或數據沖突。
- 防止重復提交:在高并發場景下,用戶可能多次提交同一請求(如支付、訂單創建),通過分布式鎖可以確保同一操作只被執行一次。
2. 資源互斥訪問
- 分布式緩存更新:當多個節點同時更新同一個緩存項時,使用分布式鎖可以保證只有一個節點進行更新操作,避免緩存擊穿或數據不一致。
- 數據庫寫沖突:多個節點同時修改同一數據庫記錄時,通過分布式鎖可以防止臟寫或數據沖突。例如,庫存扣減、賬戶余額更新等操作。
3. 分布式事務
- 跨服務資源鎖定:在分布式事務中,涉及多個服務對不同資源的操作,使用分布式鎖可以確保這些操作的原子性。例如,在訂單支付過程中,鎖定庫存和賬戶余額,防止超賣或余額不足。
4. 全局 ID 生成器
- 唯一 ID 生成:多個節點需要生成全局唯一 ID 時,通過分布式鎖可以保證生成的 ID 不重復。例如,使用數據庫自增 ID 或 UUID 時,可能存在并發沖突,分布式鎖可以確保 ID 生成的唯一性。
5. 分布式限流
- 全局流量控制:在分布式系統中,為防止某個服務被過多請求壓垮,可以使用分布式鎖實現全局限流。例如,限制同一時間內最多有 N 個請求訪問某個資源。
6. 任務調度與協調
- Leader 選舉:在分布式集群中,多個節點需要選舉一個 Leader 來協調任務分配,使用分布式鎖可以確保只有一個節點被選為 Leader。
- 資源搶占:多個節點競爭同一資源(如分布式文件系統中的文件鎖、數據庫連接池中的連接)時,分布式鎖可以實現公平或非公平的資源分配。
7. 防止緩存雪崩
- 熱點緩存重建:當緩存失效時,多個節點可能同時嘗試重建緩存,導致數據庫壓力驟增。使用分布式鎖可以確保只有一個節點負責重建緩存,其他節點等待緩存更新完成后直接使用新緩存。
8. 分布式系統初始化
- 系統啟動互斥:多個節點同時啟動時,可能需要對共享資源進行初始化(如配置加載、數據預熱),使用分布式鎖可以確保初始化操作只被執行一次。
常見實現方式
- 基于數據庫:通過數據庫的唯一索引或行鎖實現。
- 基于 Redis:使用
SETNX
命令或 RedLock 算法。 - 基于 ZooKeeper:通過創建臨時有序節點實現公平鎖。
- 基于 Etcd:利用其 CAS(Compare - And - Swap)機制實現分布式鎖。
選擇合適的分布式鎖實現需要根據具體場景權衡性能、可靠性和可用性。
避免分布式鎖帶來的死鎖問題?
在分布式系統中,死鎖是使用分布式鎖時需要特別注意的問題,以下是避免死鎖的幾種常見方法:
1. 設置鎖超時時間
- 自動釋放機制:為每個鎖設置合理的超時時間(TTL),即使持有鎖的節點崩潰或處理超時,鎖也會在超時后自動釋放,避免其他節點無限等待。
- 示例:在 Redis 中使用
SET key value NX PX timeout
命令,或在 ZooKeeper 中創建臨時節點。
2. 按順序獲取鎖
- 固定加鎖順序:當需要獲取多個鎖時,所有節點必須按照相同的順序獲取鎖,避免循環等待。例如,若業務需要同時鎖定資源 A 和 B,所有節點必須先鎖 A 再鎖 B。
3. 使用可重入鎖
- 避免自我死鎖:允許同一節點在持有鎖的情況下再次獲取鎖(需記錄重入次數),防止自身因重復請求鎖而導致死鎖。
4. 實現鎖的公平性
- 公平鎖機制:使用公平鎖(如 ZooKeeper 的臨時有序節點)確保鎖的獲取按照請求順序進行,避免饑餓和死鎖。
5. 檢測與恢復機制
- 死鎖檢測算法:實現分布式死鎖檢測,定期檢查是否存在循環等待的鎖依賴關系,一旦發現則強制釋放某些鎖。
- 超時重試策略:當獲取鎖失敗時,設置合理的重試次數和退避策略(如指數退避),避免頻繁重試加劇死鎖風險。
6. 原子化操作
- 使用原子指令:利用 Redis 的
SETNX
、RedLock
算法或 ZooKeeper 的 CAS(Compare - And - Swap)操作確保鎖的獲取和釋放是原子性的,減少中間狀態導致的死鎖。
7. 鎖粒度控制
- 細化鎖粒度:盡量縮小鎖的范圍,只鎖定關鍵資源,減少鎖的持有時間,從而降低死鎖概率。例如,使用分段鎖替代全局鎖。
8. 心跳機制與健康檢查
- 節點狀態監控:通過心跳機制檢測持有鎖的節點是否存活,若節點失效則自動釋放鎖。例如,在 Redis 中使用 Lua 腳本原子化檢查鎖和節點狀態。
9. 事務與補償機制
- 柔性事務:對于復雜操作,使用 TCC(Try - Confirm - Cancel)或 Saga 模式替代傳統的剛性事務,通過補償操作釋放已獲取的鎖。
10. 降級與熔斷策略
- 服務保護:當系統負載過高或鎖競爭過于激烈時,自動降級某些非關鍵業務,避免因鎖等待導致的級聯故障。
分布式環境下高可用解決方案
在分布式環境下,實現高可用的解決方案通常涉及到多個方面,包括冗余設計、故障檢測與恢復、負載均衡、數據一致性等。以下是一些常見的分布式環境下高可用解決方案:
- 冗余與集群技術
- 服務器冗余:部署多個服務器節點,通過集群技術將它們組成一個邏輯整體。當某個節點出現故障時,其他節點可以接管其工作,實現自動故障轉移,如通過 Keepalived 實現服務器的主備切換。
- 數據冗余:采用數據復制技術,將數據同步到多個節點或不同的數據中心,確保數據的安全性和可訪問性。如 MySQL 的主從復制,以及分布式文件系統 Ceph 通過多副本機制實現數據冗余。
- 故障檢測與恢復
- 健康檢查機制:通過心跳檢測、監控系統等手段,定期檢查節點和服務的狀態。如 ZooKeeper 通過心跳機制檢測節點的存活狀態,發現故障節點后通知其他節點進行相應處理。
- 自動恢復策略:一旦檢測到故障,系統自動觸發恢復流程,如自動重啟故障服務、切換到備用節點或進行數據恢復操作。像 Kubernetes 可以自動重啟故障的容器,并根據預設的策略重新調度到其他可用節點上。
- 負載均衡
- 請求分發:使用負載均衡器將客戶端請求均勻分配到多個服務器節點上,避免單點負載過高。常見的負載均衡器有 Nginx、F5 等,可以根據不同的算法(如輪詢、加權輪詢、最少連接數等)進行請求分發。
- 動態負載調整:根據服務器的負載情況實時調整負載均衡策略,將請求分配到負載較輕的節點上。一些云平臺提供的負載均衡服務可以自動根據服務器的 CPU、內存等資源使用情況進行動態調整。
- 數據一致性保證
- 分布式一致性算法:采用如 Paxos、Raft 等一致性算法,確保在分布式環境中數據的一致性。這些算法通過選舉領導者、日志復制等方式,保證數據在多個節點之間的一致狀態。
- 數據同步機制:對于分布式數據庫或存儲系統,需要建立可靠的數據同步機制,確保數據在不同節點之間的及時更新和一致。如 Cassandra 等分布式數據庫通過 gossip 協議進行數據同步和狀態傳播。
- 分布式配置管理
- 集中式配置管理:使用集中式的配置管理工具(如 Apollo、Nacos),將系統的配置信息統一管理和分發。這樣可以方便在分布式環境中對各個節點的配置進行修改和更新,確保配置的一致性和準確性。
- 配置版本控制:對配置信息進行版本管理,記錄配置的變更歷史,方便回滾和跟蹤。同時,支持配置的動態更新,使系統能夠在不重啟的情況下應用新的配置。
- 監控與告警系統
- 全面的監控體系:建立覆蓋整個分布式系統的監控系統,包括服務器性能、網絡狀況、應用程序指標等方面的監控。通過收集和分析這些監控數據,及時發現潛在的問題和異常。如 Prometheus 可以收集各種指標數據,并通過 Grafana 進行可視化展示。
- 實時告警機制:當監控到異常情況時,能夠及時發送告警信息給相關人員。告警方式可以包括郵件、短信、即時通訊等多種方式,以便快速響應和處理問題。
- 服務治理
- 服務注冊與發現:使用服務注冊中心(如 Eureka、Consul),讓服務提供者將自己的服務信息注冊到中心,服務消費者可以從中心獲取服務的地址和相關信息,實現服務的動態發現和調用。
- 熔斷與降級:當某個服務出現故障或負載過高時,通過熔斷機制快速切斷對該服務的調用,避免故障擴散。同時,根據業務情況進行服務降級,如返回緩存數據或簡化的響應結果,保證系統的核心功能仍然可用。
這些解決方案通常需要結合具體的業務場景和技術架構進行綜合應用,以構建一個高可用的分布式系統。
分布式事務
分布式事務是指操作跨越多個資源(如數據庫、服務)的事務,需要保證這些操作的原子性、一致性、隔離性和持久性(ACID)。在分布式系統中,由于網絡延遲、節點故障等因素,實現分布式事務具有挑戰性。以下是常見的解決方案和關鍵概念:
一、分布式事務的挑戰
- CAP 定理:在分布式系統中,一致性(Consistency)、可用性(Availability)和分區容錯性(Partition Tolerance)只能同時滿足兩個。
- BASE 理論:通過犧牲強一致性換取高可用性,采用最終一致性模型。
- 網絡問題:跨節點通信可能失敗或延遲,導致數據不一致。
二、常見解決方案
1. 兩階段提交(2PC)
- 流程:
- 準備階段:協調者向所有參與者發送事務請求,參與者執行操作并反饋結果。
- 提交階段:若所有參與者成功,則協調者發送提交指令;否則回滾。
- 優點:實現強一致性。
- 缺點:同步阻塞,單點故障(協調者),性能差。
- 適用場景:對一致性要求極高且節點數少的場景。
2. 三階段提交(3PC)
- 改進:在 2PC 基礎上增加CanCommit階段,減少參與者阻塞時間,并引入超時機制。
- 流程:CanCommit → PreCommit → DoCommit。
- 優點:減少阻塞,提高可用性。
- 缺點:仍存在數據不一致風險(如網絡分區)。
3. TCC(Try-Confirm-Cancel)
- 柔性事務:將事務分為三個階段:
- Try:預留資源(如凍結賬戶余額)。
- Confirm:確認執行,提交資源。
- Cancel:回滾,釋放預留資源。
- 優點:無長時間鎖,性能高。
- 缺點:開發成本高,需要編寫補償邏輯。
- 示例:支付系統中,先凍結用戶余額(Try),訂單確認后扣款(Confirm),失敗則解凍(Cancel)。
4. 消息隊列(最終一致性)
- 流程:
- 業務操作完成后發送消息到隊列。
- 消費者異步處理消息,更新相關資源。
- 通過重試或補償機制確保最終一致性。
- 優點:高吞吐量,解耦服務。
- 缺點:不保證實時一致性,需冪等設計。
- 工具:RocketMQ、Kafka 等支持事務消息的隊列。
5. Saga 模式
- 長事務分解:將大事務拆分為多個本地事務,每個子事務有對應的補償操作。
- 兩種模式:
- 向前恢復:失敗時重試子事務。
- 向后恢復:失敗時執行補償操作回滾。
- 優點:無單點問題,適合長流程事務。
- 缺點:補償邏輯復雜,不保證強一致性。
6. 最大努力通知
- 流程:
- 業務操作完成后發送通知(如短信、郵件)。
- 接收方定期查詢狀態,確認最終結果。
- 適用場景:對實時性要求不高的場景(如支付結果通知)。
三、分布式事務工具
- Seata:阿里巴巴開源框架,支持 AT(自動補償)、TCC、Saga 等模式。
- ByteTCC:支持 TCC 和消息事務。
- OpenTransaction:輕量級分布式事務框架。
- 數據庫原生支持:如 MySQL XA 協議、PostgreSQL 的 dblink。
四、最佳實踐
- 優先避免分布式事務:通過業務設計減少跨服務事務(如緩存、最終一致性)。
- 選擇合適的一致性級別:根據業務場景權衡強一致性和可用性(如支付用 2PC,訂單用最終一致性)。
- 冪等設計:所有事務操作必須支持重試(如唯一 ID 防重復提交)。
- 監控與補償:建立事務監控系統,對失敗事務自動重試或人工干預。
- 熔斷與降級:對關鍵服務設置熔斷機制,防止級聯故障。
五、對比總結
方案 | 一致性級別 | 性能 | 實現復雜度 | 適用場景 |
---|---|---|---|---|
2PC | 強一致 | 低 | 低 | 金融轉賬等嚴格場景 |
TCC | 最終一致 | 中 | 高 | 業務流程長且需回滾 |
消息隊列 | 最終一致 | 高 | 中 | 異步通知、高并發場景 |
Saga | 最終一致 | 高 | 高 | 微服務長事務 |
最大努力通知 | 最終一致 | 極高 | 低 | 實時性要求不高的通知 |
根據業務需求選擇合適的方案,通常結合多種模式以達到最佳效果。
接口的冪等性/解決重復消費
在分布式系統中,接口的冪等性和解決重復消費是非常重要的概念,它們有助于確保系統的穩定性和數據的一致性。以下是關于它們的詳細介紹:
接口冪等性
- 定義:冪等性是指一個接口在多次調用時,產生的結果與一次調用相同,不會對系統造成額外的副作用。例如,對于查詢接口,無論調用多少次,只要查詢條件不變,返回的結果應該是一致的;對于刪除接口,多次刪除同一個資源,結果與一次刪除相同,即資源被刪除,不會出現多次刪除導致錯誤的情況。
- 實現方式
- 使用唯一 ID:在請求中攜帶一個唯一的標識,如訂單號、交易 ID 等。服務端在處理請求時,根據這個唯一 ID 判斷請求是否已經被處理過。如果已經處理過,則直接返回之前的結果,不再執行實際的業務邏輯。
- 數據庫約束:利用數據庫的唯一約束來保證冪等性。例如,在插入數據時,通過設置唯一索引,如果重復插入相同的數據,數據庫會拋出異常,從而避免數據重復插入。
- 狀態機控制:對于一些有明確狀態流轉的業務,如訂單的創建、支付、發貨等,可以通過狀態機來控制接口的冪等性。只有當訂單處于特定狀態時,才能執行相應的操作,避免重復操作導致狀態混亂。
解決重復消費
- 問題場景:在消息隊列等異步處理場景中,可能會出現消息被重復消費的情況。例如,消費者在處理消息時,由于網絡故障、進程崩潰等原因,導致消息處理未完成,但消息已經被標記為已消費,當消費者重新啟動后,可能會再次消費該消息,從而造成數據不一致或業務邏輯錯誤。
- 解決方法
- 消息冪等性處理:與接口冪等性類似,可以在消息處理邏輯中加入冪等性判斷。例如,給每條消息分配一個唯一的 ID,消費者在處理消息時,先根據消息 ID 檢查是否已經處理過該消息,如果是,則直接跳過處理。
- 使用消息隊列的特性:一些消息隊列提供了消息去重的功能,如 RocketMQ 的事務消息和 Kafka 的冪等生產者。可以利用這些特性來確保消息在生產和消費過程中的唯一性。
- 消費狀態記錄:在數據庫或緩存中記錄消息的消費狀態,消費者在處理消息前先查詢狀態,判斷是否已經消費過。如果已經消費過,則不再處理。同時,要確保消費狀態的記錄和消息處理的操作在同一個事務中,以保證數據的一致性。
- 分布式鎖:在處理消息時,先獲取分布式鎖,只有獲取到鎖的消費者才能處理消息。這樣可以避免多個消費者同時處理同一條消息,從而防止重復消費。但使用分布式鎖要注意避免死鎖問題。
無論是實現接口冪等性還是解決重復消費問題,都需要根據具體的業務場景和技術架構選擇合適的方法,以確保系統的穩定性和數據的準確性。
新增、更新會出現。
場景:1、用戶重復點擊多次 2、接口超時/重試?3、消息重復消費
解決方案:
- 數據庫唯一索引?-- 防止新增臟數據
- Token+Redis機制防重?-- 防止頁面重復提交(?第一次請求生成token,第二次發起請求后攜帶token,執行完成后刪除。)
- 樂觀鎖 -- 基于版本號version實現, 在更新數據那一刻校驗數據
- 分布式鎖 -- redis(redisson、zookeeper) 性能低。
- 狀態機 -- 狀態變更, 更新數據時判斷狀態
- 異步請求攜帶唯一標識
面試:如何保證接口的冪等性?常見的實現方案有哪些?-騰訊云開發者社區-騰訊云
https://zhuanlan.zhihu.com/p/345428483
分布式唯一ID?
概述
在分布式系統中,唯一 ID 是用于標識不同節點上的資源、事務或消息的標識符。設計分布式唯一 ID 需要考慮以下核心要素:
- 全局唯一性:確保所有節點生成的 ID 不重復
- 趨勢遞增 / 有序性:部分場景需要 ID 按時間有序生成
- 高性能:生成過程高效,避免成為系統瓶頸
- 高可用:確保分布式環境下持續生成 ID
- 安全性:ID 不可預測,避免被猜測
常見實現方案
1. UUID (Universally Unique Identifier)
原理:基于時間戳、MAC 地址或隨機數生成 128 位標識符
優點:本地生成,無需依賴外部服務,性能極高
缺點:無序字符串,索引效率低;占用空間大 (16 字節)
Java 實現:
import java.util.UUID;public class UUIDGenerator {public static String generate() {return UUID.randomUUID().toString().replace("-", "");}
}
2. 數據庫自增 ID
原理:利用數據庫的 AUTO_INCREMENT 特性生成唯一 ID
優點:實現簡單,有序性好
缺點:單點故障風險;性能瓶頸 (依賴數據庫事務)
優化方案:
- 號段模式:數據庫預分配多個 ID 段,應用本地緩存使用
- 雙 Buffer 優化:當前號段使用到一定比例時,異步預取下一號段
3. Snowflake 算法(雪花算法)
原理:Twitter 開源的 64 位長整型 ID 生成算法,結構如下:
- 1 位符號位(始終為 0)
- 41 位時間戳(毫秒級,支持約 69 年)
- 5 位數據中心 ID(最多 32 個)
- 5 位機器 ID(每個數據中心最多 32 臺機器)
- 12 位序列號(同一毫秒內生成的 ID,最多 4096 個)
雪花算法是 64 位?的二進制,一共包含了四部分:
- 1位是符號位,也就是最高位,始終是0,沒有任何意義,因為要是唯一計算機二進制補碼中就是負數,0才是正數。
- 41位是時間戳,具體到毫秒,41位的二進制可以使用69年,因為時間理論上永恒遞增,所以根據這個排序是可以的。
- 10位是機器標識,可以全部用作機器ID,也可以用來標識機房ID + 機器ID,10位最多可以表示1024臺機器。
- 12位是計數序列號,也就是同一臺機器上同一時間,理論上還可以同時生成不同的ID,12位的序列號能夠區分出4096個ID。
優點:高性能、趨勢遞增、可定制化
缺點:依賴系統時鐘(需處理時鐘回撥問題)
Java 實現:
public class SnowflakeIdGenerator {private final long startTimeStamp = 1609459200000L; // 2021-01-01 00:00:00 UTCprivate final long dataCenterIdBits = 5L;private final long machineIdBits = 5L;private final long sequenceBits = 12L;private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);private final long maxMachineId = -1L ^ (-1L << machineIdBits);private final long machineIdShift = sequenceBits;private final long dataCenterIdShift = sequenceBits + machineIdBits;private final long timestampLeftShift = sequenceBits + machineIdBits + dataCenterIdBits;private final long sequenceMask = -1L ^ (-1L << sequenceBits);private final long dataCenterId;private final long machineId;private long sequence = 0L;private long lastTimestamp = -1L;public SnowflakeIdGenerator(long dataCenterId, long machineId) {if (dataCenterId > maxDataCenterId || dataCenterId < 0) {throw new IllegalArgumentException("DataCenter ID must be between 0 and " + maxDataCenterId);}if (machineId > maxMachineId || machineId < 0) {throw new IllegalArgumentException("Machine ID must be between 0 and " + maxMachineId);}this.dataCenterId = dataCenterId;this.machineId = machineId;}public synchronized long nextId() {long currentTimestamp = System.currentTimeMillis();// 處理時鐘回撥if (currentTimestamp < lastTimestamp) {throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - currentTimestamp) + " milliseconds");}if (currentTimestamp == lastTimestamp) {sequence = (sequence + 1) & sequenceMask;if (sequence == 0) {// 當前毫秒內序列號用完,等待下一毫秒currentTimestamp = waitNextMillis(lastTimestamp);}} else {sequence = 0L;}lastTimestamp = currentTimestamp;return ((currentTimestamp - startTimeStamp) << timestampLeftShift) |(dataCenterId << dataCenterIdShift) |(machineId << machineIdShift) |sequence;}private long waitNextMillis(long lastTimestamp) {long timestamp = System.currentTimeMillis();while (timestamp <= lastTimestamp) {timestamp = System.currentTimeMillis();}return timestamp;}
}
4. Redis 原子操作
原理:利用 Redis 的原子自增命令 (INCR) 生成唯一 ID
優點:高性能、支持分布式
缺點:依賴 Redis 可用性;需處理持久化問題
Java 實現:
import redis.clients.jedis.Jedis;public class RedisIdGenerator {private final Jedis jedis;private final String key;public RedisIdGenerator(String host, int port, String key) {this.jedis = new Jedis(host, port);this.key = key;}public long nextId() {return jedis.incr(key);}public void close() {if (jedis != null) {jedis.close();}}
}
Incr 命令將 key 中儲存的數字值增一?, Increment 英 /???kr?m?nt/
decr key???遞減
5. 數據庫號段模式
原理:數據庫存儲當前號段的最大值和步長,應用批量獲取號段本地使用
優點:高性能、減少數據庫訪問
缺點:號段預分配可能導致 ID 浪費
Java 實現:
import java.sql.*;
import java.util.concurrent.atomic.AtomicLong;public class SegmentIdGenerator {private final String jdbcUrl;private final String username;private final String password;private final String bizType;private final int step;private AtomicLong currentId;private AtomicLong maxId;public SegmentIdGenerator(String jdbcUrl, String username, String password, String bizType, int step) {this.jdbcUrl = jdbcUrl;this.username = username;this.password = password;this.bizType = bizType;this.step = step;this.currentId = new AtomicLong(0);this.maxId = new AtomicLong(0);}public synchronized long nextId() {if (currentId.get() >= maxId.get()) {// 號段用完,獲取下一個號段refreshSegment();}return currentId.incrementAndGet();}private void refreshSegment() {try (Connection conn = DriverManager.getConnection(jdbcUrl, username, password)) {// 開啟事務conn.setAutoCommit(false);// 查詢當前號段String selectSql = "SELECT max_id FROM id_generator WHERE biz_type = ? FOR UPDATE";try (PreparedStatement pstmt = conn.prepareStatement(selectSql)) {pstmt.setString(1, bizType);try (ResultSet rs = pstmt.executeQuery()) {if (rs.next()) {long oldMaxId = rs.getLong("max_id");long newMaxId = oldMaxId + step;// 更新號段String updateSql = "UPDATE id_generator SET max_id = ? WHERE biz_type = ? AND max_id = ?";try (PreparedStatement updateStmt = conn.prepareStatement(updateSql)) {updateStmt.setLong(1, newMaxId);updateStmt.setString(2, bizType);updateStmt.setLong(3, oldMaxId);int rows = updateStmt.executeUpdate();if (rows == 0) {throw new RuntimeException("Failed to update segment for bizType: " + bizType);}// 更新本地號段currentId.set(oldMaxId);maxId.set(newMaxId);}} else {// 初始化號段String insertSql = "INSERT INTO id_generator (biz_type, max_id, step) VALUES (?, ?, ?)";try (PreparedStatement insertStmt = conn.prepareStatement(insertSql)) {insertStmt.setString(1, bizType);insertStmt.setLong(2, step);insertStmt.setInt(3, step);insertStmt.executeUpdate();// 更新本地號段currentId.set(0);maxId.set(step);}}}}conn.commit();} catch (SQLException e) {throw new RuntimeException("Failed to get next segment", e);}}
}
6. ULID (Universally Unique Lexicographically Sortable Identifier)
原理:結合 UUID 和 Snowflake 優點,生成 128 位 ID:
- 48 位時間戳(毫秒級,有序)
- 80 位隨機數(保證唯一性)
優點:字符串形式、有序性、高性能
Java 實現(需引入依賴):
import com.github.f4b6a3.ulid.UlidCreator;public class ULIDGenerator {public static String generate() {return UlidCreator.getUlid().toString();}
}
Maven 依賴:
<dependency><groupId>com.github.f4b6a3</groupId><artifactId>ulid-creator</artifactId><version>5.0.0</version>
</dependency>
選擇建議
方案 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
UUID | 本地生成、簡單 | 無序、占用空間大 | 緩存鍵、日志追蹤 |
數據庫自增 ID | 有序、簡單 | 單點故障、性能低 | 小規模系統、數據庫主鍵 |
Snowflake | 高性能、有序 | 時鐘回撥問題 | 高并發場景(如訂單 ID) |
Redis INCR | 高性能、分布式 | 依賴 Redis | 分布式計數器、全局 ID |
號段模式 | 高性能、數據庫壓力小 | 號段浪費 | 中大規模系統 |
ULID | 有序字符串、高性能 | 需引入依賴 | 分布式系統唯一標識 |
根據業務需求選擇合適的方案,通常需要權衡性能、有序性、唯一性和系統復雜度。?
RPC
RPC(Remote Procedure Call,遠程過程調用)是一種允許程序調用遠程計算機上的服務或方法的技術,使得遠程調用看起來就像本地調用一樣簡單。以下是關于 RPC 的詳細介紹:
一、核心概念
-
基本原理:
- 本地調用:程序直接調用本地函數 / 方法。
- 遠程調用:通過網絡將請求發送到遠程服務器,服務器執行相應方法后返回結果。
-
核心組件:
- 客戶端(Client):發起調用的程序。
- 服務端(Server):提供服務的程序。
- 客戶端存根(Client Stub):負責將調用參數打包(序列化)并發送請求。
- 服務端存根(Server Stub):接收請求、解包參數并調用實際服務。
- 網絡傳輸層:負責數據傳輸(如 TCP/IP、HTTP)。
-
關鍵特性:
- 透明性:調用遠程方法與本地方法語法一致。
- 可靠性:處理網絡異常、超時等問題。
- 高性能:減少網絡開銷,優化序列化 / 反序列化。
二、常見 RPC 框架
-
gRPC:
- 語言:支持多語言(Java、Python、Go 等)。
- 協議:基于 HTTP/2,使用 Protocol Buffers 序列化。
- 特點:高性能、強類型、支持流式通信。
-
Apache Dubbo:
- 語言:主要支持 Java。
- 協議:支持多種協議(Dubbo、REST、gRPC 等)。
- 特點:服務治理能力強(負載均衡、服務發現、熔斷等)。
-
Thrift:
- 語言:多語言支持。
- 協議:二進制協議,支持多種傳輸層。
- 特點:Facebook 開源,適合跨語言場景。
-
JSON-RPC/XML-RPC:
- 協議:基于 JSON/XML 的簡單 RPC 協議。
- 特點:輕量、易實現,適合簡單場景。
-
Spring Cloud:
- 語言:主要支持 Java。
- 協議:基于 HTTP(RESTful)。
- 特點:微服務生態完善(服務發現、配置中心等)。
三、工作流程
-
客戶端:
- 調用本地代理方法(Client Stub)。
- 參數序列化(如轉為 JSON、Protobuf)。
- 通過網絡發送請求到服務端。
-
服務端:
- 接收請求并反序列化參數。
- 調用實際服務方法。
- 將結果序列化并返回給客戶端。
四、核心技術
-
序列化與反序列化:
- 將對象轉換為字節流以便傳輸,常見方式:
- 文本格式:JSON、XML。
- 二進制格式:Protocol Buffers、Thrift、Kryo。
- 語言特定:Java 序列化、Python Pickle。
- 將對象轉換為字節流以便傳輸,常見方式:
-
服務發現:
- 客戶端如何找到服務端?
- 靜態配置:硬編碼服務地址。
- 注冊中心:服務啟動時注冊到中心(如 ZooKeeper、Nacos、Consul)。
- 客戶端如何找到服務端?
-
負載均衡:
- 多個服務實例時,如何選擇?
- 輪詢:按順序依次選擇。
- 隨機:隨機選擇一個實例。
- 加權:根據實例性能分配權重。
- 多個服務實例時,如何選擇?
-
熔斷與限流:
- 熔斷:當服務不可用時,快速失敗避免級聯故障。
- 限流:限制請求速率,保護服務不被壓垮。
五、優缺點
- 優點:
- 簡化分布式系統開發,降低復雜度。
- 提高開發效率,屏蔽網絡細節。
- 支持跨語言、跨平臺調用。
- 缺點:
- 增加系統復雜度(網絡延遲、故障處理)。
- 調試和排查問題難度大。
- 過度依賴 RPC 可能導致服務間耦合度高。
六、Java 實現示例(gRPC)
- 定義服務接口(.proto 文件):
syntax = "proto3";package example;// 定義請求和響應消息
message HelloRequest {string name = 1;
}message HelloResponse {string message = 1;
}// 定義服務
service Greeter {rpc SayHello (HelloRequest) returns (HelloResponse);
}
-
生成代碼:
- 使用
protoc
編譯.proto 文件,生成客戶端和服務端代碼。
- 使用
-
服務端實現:
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;public class HelloServer {private Server server;private void start() throws Exception {server = ServerBuilder.forPort(50051).addService(new GreeterImpl()).build().start();System.out.println("Server started, listening on " + 50051);}private static class GreeterImpl extends GreeterGrpc.GreeterImplBase {@Overridepublic void sayHello(HelloRequest req, StreamObserver<HelloResponse> responseObserver) {HelloResponse reply = HelloResponse.newBuilder().setMessage("Hello " + req.getName()).build();responseObserver.onNext(reply);responseObserver.onCompleted();}}public static void main(String[] args) throws Exception {final HelloServer server = new HelloServer();server.start();server.blockUntilShutdown();}private void blockUntilShutdown() throws InterruptedException {if (server != null) {server.awaitTermination();}}
}
- 客戶端實現:
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;public class HelloClient {private final ManagedChannel channel;private final GreeterGrpc.GreeterBlockingStub blockingStub;public HelloClient(String host, int port) {channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();blockingStub = GreeterGrpc.newBlockingStub(channel);}public void shutdown() throws InterruptedException {channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);}public void greet(String name) {HelloRequest request = HelloRequest.newBuilder().setName(name).build();HelloResponse response;try {response = blockingStub.sayHello(request);} catch (StatusRuntimeException e) {System.err.println("RPC failed: " + e.getStatus());return;}System.out.println("Greeting: " + response.getMessage());}public static void main(String[] args) throws Exception {HelloClient client = new HelloClient("localhost", 50051);try {String user = "World";if (args.length > 0) {user = args[0];}client.greet(user);} finally {client.shutdown();}}
}
七、適用場景
- 微服務架構中不同服務間的通信。
- 分布式系統中跨節點的方法調用。
- 異構系統間的集成(如 Java 服務調用 Python 服務)。
八、對比 REST API
特性 | RPC | REST API |
---|---|---|
調用方式 | 像本地方法一樣調用 | 通過 HTTP 請求調用 |
協議 | 自定義協議(如 HTTP/2) | 基于 HTTP 協議 |
數據格式 | 二進制(如 Protobuf) | 文本(如 JSON/XML) |
性能 | 通常更高(二進制) | 較低(文本格式) |
靈活性 | 較低(強類型) | 較高(URL 資源導向) |
跨語言 | 需生成代碼 | 天然支持 |
適用場景 | 內部服務調用 | 對外 API 暴露 |
根據場景選擇合適的通信方式,兩者并非互斥,可結合使用。
Protocol Buffers vs Thrift
Protocol Buffers(簡稱 Protobuf)和 Apache Thrift 都是用于序列化結構化數據的高性能框架,廣泛應用于分布式系統和 RPC 通信中。以下是它們的詳細對比:
一、核心特性對比
特性 | Protocol Buffers | Thrift |
---|---|---|
開發公司 | Google(2001 年) | Facebook(2007 年)→ Apache |
語言支持 | 多語言(Java、Python、Go、C++ 等) | 多語言(Java、Python、Go、C++、PHP 等) |
協議類型 | 僅序列化協議 | 序列化協議 + RPC 框架 |
IDL 語法 | .proto 文件,簡潔 | .thrift 文件,更復雜(支持服務定義) |
序列化速度 | 極快(二進制編碼優化) | 快(略慢于 Protobuf) |
壓縮率 | 高(二進制格式緊湊) | 高(二進制格式) |
版本兼容性 | 良好(支持字段增刪改,需遵循規則) | 良好(支持字段增刪改,需遵循規則) |
元數據 | 需生成代碼(.proto ?→ 語言特定類) | 需生成代碼(.thrift ?→ 語言特定類) |
擴展能力 | 通過oneof 、map 等靈活擴展 | 通過struct 、union 、service 擴展 |
二、序列化性能
- Protobuf:
- 二進制格式更緊湊,序列化速度更快。
- 生成的代碼更優化,內存占用更小。
- Thrift:
- 性能略低于 Protobuf,但支持多種傳輸協議(如 Binary、Compact、JSON)。
三、IDL(接口定義語言)
Protobuf 示例
syntax = "proto3";message Person {string name = 1;int32 id = 2;string email = 3;
}
Thrift 示例
struct Person {1: required string name,2: required i32 id,3: optional string email
}service PersonService {Person getPerson(1: i32 id),void savePerson(1: Person person)
}
四、RPC 集成
- Protobuf:
- 需結合 gRPC 等 RPC 框架使用。
- gRPC 基于 HTTP/2,提供高性能、流式通信能力。
- Thrift:
- 內置 RPC 支持,提供多種傳輸協議和服務模型。
- 支持同步、異步、單線程、多線程服務模型。
五、適用場景
場景 | Protobuf + gRPC | Thrift |
---|---|---|
內部微服務通信 | 推薦(高性能、強類型) | 推薦(內置 RPC,多語言支持) |
異構系統集成 | 推薦(跨語言支持) | 推薦(跨語言支持更豐富) |
需要多種協議 | 需額外開發(僅支持 HTTP/2) | 內置多種協議(Binary、Compact、JSON) |
快速開發 | 需分別實現序列化和 RPC | 一站式解決方案(IDL 定義服務) |
大數據量傳輸 | 推薦(壓縮率高、性能優) | 推薦(性能接近) |
六、優缺點總結
Protocol Buffers
- 優點:
- 性能卓越,序列化速度快。
- 社區活躍,支持語言廣泛。
- 與 gRPC 深度集成,生態完善。
- 缺點:
- 僅專注于序列化,需額外集成 RPC 框架。
- 二進制格式可讀性差,調試復雜。
Thrift
- 優點:
- 一站式解決方案(序列化 + RPC)。
- 支持更多傳輸協議和服務模型。
- 內置服務發現和負載均衡能力。
- 缺點:
- 性能略低于 Protobuf。
- 社區活躍度低于 Protobuf。
七、選擇建議
-
優先選擇 Protobuf + gRPC:
- 若項目已采用 gRPC 或需要極致性能。
- 僅需序列化功能,計劃自行集成 RPC 框架。
- 主要使用 Java、Go、Python 等主流語言。
-
優先選擇 Thrift:
- 需要一站式 RPC 解決方案。
- 需支持 PHP、Ruby 等小眾語言。
- 需要靈活選擇傳輸協議(如 JSON、Binary)。
-
混合使用:
- 不同服務間根據需求選擇(如內部服務用 Protobuf,對外服務用 Thrift)。
八、性能對比測試
測試場景 | Protobuf (ms) | Thrift (ms) | JSON (ms) |
---|---|---|---|
序列化 1000 對象 | 12 | 15 | 35 |
反序列化 1000 對象 | 8 | 10 | 40 |
數據大小(KB) | 28 | 32 | 45 |
(數據來源:第三方性能測試,僅供參考)
九、總結
兩者都是優秀的序列化框架,選擇取決于具體需求:
- Protobuf:性能之王,適合追求極致性能的場景,需與 RPC 框架結合。
- Thrift:功能全面,適合需要一站式解決方案的場景,支持更多協議和語言。
客戶端在不知道調用細節的情況下,調用存在于遠程計算機上的某個對象,就像調用本地應用程序中的對象一樣。
比較正式的描述是:一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議。
RPC: (Remote Procedure Call) 采用客戶端/服務器方式(請求/響應),發送請求到服務器端,
服務端執行方法后返回結果。優點是跨語言跨平臺,缺點是編譯期無法排錯,只能在運行時檢查。
常見的 RPC 框架:阿里的 Dubbo,Google 開源的 gRpc,百度開源的 brpc,Dubbo等。
RPC 組成架構
一個完整的 RPC 架構里包含了四個核心的組件,分別是 Client、Client Stub、Server 以及 Server Stub,
這個 Stub 可以理解為存根。
- Client(客戶端):服務的調用者
- Client Stub(客戶端存根):存放服務端的地址消息,
再將客戶端的請求參數打包成網絡消息,然后通過網絡遠程發送給服務方
- Server(服務端):服務的提供者
- Server Stub(服務端存根):接收客戶端發送過來的消息,將消息解包,并調用本地的方法
鏈接:https://juejin.cn/post/7029481269415116813
?總結:
分布式系統是由多個通過網絡連接的獨立計算機節點組成的系統,這些節點相互協作,共同完成任務,對外表現為一個單一系統。分布式系統的特點包括分布性、并發性、故障獨立性和透明性。然而,分布式系統也面臨網絡延遲、數據一致性、故障處理等挑戰。常見的應用場景包括大型網站、大數據處理和云計算平臺。分布式事務的實現方案包括兩階段提交(2PC)、三階段提交(3PC)、TCC、消息隊列和Saga模式等。接口的冪等性和解決重復消費問題在分布式系統中尤為重要,常見的解決方案包括數據庫唯一索引、Token+Redis機制、樂觀鎖、分布式鎖和狀態機等。分布式唯一ID的生成方案有UUID、數據庫自增ID、Snowflake算法、Redis原子操作、數據庫號段模式和ULID等。RPC(遠程過程調用)技術允許程序調用遠程計算機上的服務,常見的RPC框架包括gRPC、Apache Dubbo、Thrift和Spring Cloud等。RPC的核心組件包括客戶端、服務端、客戶端存根和服務端存根,其工作流程涉及參數序列化、網絡傳輸和結果反序列化。RPC的優缺點包括簡化分布式系統開發、提高開發效率,但也增加了系統復雜度和調試難度。