1. 程式人生 > >分散式場景常見問題及解決方案

分散式場景常見問題及解決方案

一、分散式鎖

  分散式鎖是在分散式場景下一種常見技術,通常通過基於redis和zookeeper來實現,本文主要介紹redis分散式鎖和zookeeper分散式鎖的實現方案和對比:

  (1)基於redis的普通實現

  這個方案的加鎖主要實現是基於redis的”SET key 隨機值 NX PX 過期時間(毫秒)”指令,NX代表只有key不存在時才設定成功,PX代表在過期時間後會自動釋放。

  這個方案的釋放鎖是通過lua指令碼刪除key的方式,判斷value一樣則刪除key。

  使用隨機值的原因是如果某個獲取到鎖的客戶端阻塞了很長時間,導致了它獲取到的鎖已經自動釋放,此時可能有其他客戶端已經獲取到了鎖,如果直接刪除是有問題的,所以要通過隨機值加上lua指令碼去判斷如果value相等時再刪除。

  這個方案存在一個問題就是,如果採用redis單例項可能會存在單點故障問題,但如果採用普通主從方式,如果主節點掛了key還沒來得及同步到從節點,此時從節點被切換到了主節點,由於沒有同步到資料別人就會拿到鎖。

  (2)redis的RedLock演算法

  這個方案是redis官方推薦的分散式鎖的解決方案,假設有5個redis master例項,然後執行如下步驟去獲取一把鎖:

  1)獲取當前時間戳,單位是毫秒

  2)跟上面類似,輪流嘗試在每個master節點上建立鎖,過期時間較短,一般就幾十毫秒

  3)嘗試在大多數節點上建立一個鎖,比如5個節點就要求是3個節點(n / 2 +1)

  4)客戶端計算建立好鎖的時間,如果建立鎖的時間小於超時時間,就算建立成功了

  5)要是鎖建立失敗了,那麼就依次刪除這個鎖

  6)只要別人建立了一把分散式鎖,你就得不斷輪詢去嘗試獲取鎖

  這個方案實現起來較為麻煩,不過貌似有很多開源的外掛封裝了這個演算法,但是這個演算法好像在國外是有爭議的。

  (3) zookeeper的臨時節點

  這個方案的實現是通過在獲取鎖時在zookeeper上建立臨時節點(常用為臨時順序節點),如果建立成功(如果是臨時順序節點則為當前最小),就代表獲取了到了這個鎖;如果建立建立失敗(如果是臨時順序節點,當前自己節點不是最小),則註冊監聽器監聽這個鎖。釋放鎖時會刪除這個臨時節點,測試由於註冊了監聽器,其他等待鎖的執行緒則會收到釋放鎖的訊息,然後再去嘗試獲取鎖。

  (4)redis分散式鎖和zookeeper分散式鎖的對比

  通過對比redis分散式鎖和zk分散式鎖可以發現,redis分散式鎖類似於自旋鎖的方式需要自己不斷嘗試去獲取鎖,這個是比較耗效能的。zk獲取不到鎖的話則可以註冊監聽器,不需要不斷嘗試,這樣的活效能開銷較小;其次,redis鎖有一個問題就是,如果獲取到鎖的客戶端崩潰了或者沒有正常釋放鎖則會導致只能等到過期時間完了才能獲取到鎖,而zk建立的由於是臨時節點,客戶端崩潰了或者掛了,臨時節點會自動刪除,此時會自動釋放鎖;最後,這個redis的實現方式如果採用RedLock演算法的話較為複雜並且還存在爭議,普通的演算法存在單點故障和主從同步的問題,所以一般來說,個人認為zk分散式鎖要比redis分散式鎖更可靠並且易用。

二、分散式session

  在傳統的單體應用下,我們可以通過session去儲存一些資料,但是在分散式和為微服務結構下,如果需要使用session就需要採取一些手段去維護,以下提供2個方案去實現分散式session:

  (1)tomcat+redis

  由於我們傳統的應用基本都是通過tomcat去部署的,我們可以利用tomcat的RedisSessionManager來將session的資料都存在redis中。

  但這種方案現在不怎麼使用了,因為移植性很差,如果要換web容器就尷尬了。

  (2)spring session + redis

  給sping session配置基於redis來儲存session資料,然後配置一個spring session的過濾器,這樣的話,session相關操作都會交給spring session來管了。接著在程式碼中,就用原生的session操作,就是直接基於spring sesion從redis中獲取資料了。

  這個方案現在還是比較主流的,因為現在主流的開發基本都是基於spring開源的一些框架,所以說如果換技術棧的影響不會很大。

三、分散式事務

  現在分散式系統成為了主流,但使用分散式也隨之帶來了一些問題和痛點,分散式事務就是最常見的一個問題,本文主要介紹分散式事務的一些常見解決方案。

  (1)兩階段提交方案/XA方案
  
  兩階段提交,有一個事務管理器的概念,負責協調多個數據庫(資源管理器)的事務,事務管理器先問問各個資料庫你準備好了嗎?如果每個資料庫都回復ok,那麼就正式提交事務,在各個資料庫上執行操作;如果任何一個數據庫回答不ok,那麼就回滾事務。

  這種分散式事務方案,比較適合單塊應用裡,跨多個庫的分散式事務,而且因為嚴重依賴於資料庫層面來搞定複雜的事務,效率很低,絕對不適合高併發的場景。如果要玩兒,那麼基於spring + JTA就可以搞定。

  這個方案很少用,一般來說某個系統內部如果出現跨多個庫的這麼一個操作,是不合規的。我可以給大家介紹一下, 現在微服務,一個大的系統分成幾百個服務,幾十個服務。一般來說,我們的規定和規範,是要求說每個服務只能操作自己對應的一個數據庫。如果你要操作別的服務對應的庫,不允許直連別的服務的庫,違反微服務架構的規範,如果你要操作別人的服務的庫,你必須是通過呼叫別的服務的介面來實現,絕對不允許你交叉訪問別人的資料庫!

  (2)TCC方案

  TCC的全程是:Try、Confirm、Cancel。

  這個其實是用到了補償的概念,分為了三個階段:

  1)Try階段:這個階段說的是對各個服務的資源做檢測以及對資源進行鎖定或者預留

  2)Confirm階段:這個階段說的是在各個服務中執行實際的操作

  3)Cancel階段:如果任何一個服務的業務方法執行出錯,那麼這裡就需要進行補償,就是執行已經執行成功的業務邏輯的回滾操作。

  這種方案說實話幾乎很少用人使用,因為這個事務回滾實際上是嚴重依賴於你自己寫程式碼來回滾和補償了,會造成補償程式碼巨大,非常之噁心。

  比較適合的場景:這個就是除非你是真的一致性要求太高,是你係統中核心之核心的場景,比如常見的就是資金類的場景,那你可以用TCC方案了,自己編寫大量的業務邏輯,自己判斷一個事務中的各個環節是否ok,不ok就執行補償/回滾程式碼。

  (3)本地訊息表

  本地訊息表示國外的ebay搞出來的這麼一套思想

  本地訊息表來實現分散式事務的思路大致如下:

  1)A系統在自己本地一個事務裡操作同時,插入一條資料到訊息表

  2)接著A系統將這個訊息傳送到MQ中去

  3)B系統接收到訊息之後,在一個事務裡,往自己本地訊息表裡插入一條資料,同時執行其他的業務操作,如果這個訊息已經被處理過了,那麼此時這個事務會回滾,這樣保證不會重複處理訊息

  4)B系統執行成功之後,就會更新自己本地訊息表的狀態以及A系統訊息表的狀態

  5)如果B系統處理失敗了,那麼就不會更新訊息表狀態,那麼此時A系統會定時掃描自己的訊息表,如果有沒處理的訊息,會再次傳送到MQ中去,讓B再次處理

  6)這個方案保證了最終一致性,哪怕B事務失敗了,但是A會不斷重發訊息,直到B那邊成功為止。

  這個方案最大的弊端在於依賴於資料庫訊息表來保證事務,但是在高併發場景下,資料庫就成了瓶頸。

  (4)可靠訊息最終一致性方案

  這個方案的大致思路為:

  1)A系統先發送一個prepared訊息到mq,如果這個prepared訊息傳送失敗那麼就直接取消操作別執行了

  2)如果這個訊息傳送成功過了,那麼接著執行本地事務,如果成功就告訴mq傳送確認訊息,如果失敗就告訴mq回滾訊息

  3)如果傳送了確認訊息,那麼此時B系統會接收到確認訊息,然後執行本地的事務

  4)mq會自動定時輪詢所有prepared訊息回撥你的介面,問你,這個訊息是不是本地事務處理失敗了,所有沒傳送確認訊息?那是繼續重試還是回滾?一般來說這裡你就可以查下資料庫看之前本地事務是否執行,如果回滾了,那麼這裡也回滾吧。這個就是避免可能本地事務執行成功了,別確認訊息傳送失敗了。

  5)這個方案裡,要是系統B的事務失敗了咋辦?重試咯,自動不斷重試直到成功,如果實在是不行,要麼就是針對重要的資金類業務進行回滾,比如B系統本地回滾後,想辦法通知系統A也回滾;或者是傳送報警由人工來手工回滾和補償。

  這個方案是目前國內公司採用較多的一種方案。

  (5)最大努力通知方案

  思路:

  1)系統A本地事務執行完之後,傳送個訊息到MQ

  2)這裡會有個專門消費MQ的最大努力通知服務,這個服務會消費MQ然後寫入資料庫中記錄下來,或者是放入個記憶體佇列也可以,接著呼叫系統B的介面

  3)要是系統B執行成功就ok了;要是系統B執行失敗了,那麼最大努力通知服務就定時嘗試重新呼叫系統B,反覆N次,最後還是不行就放棄。

  (6)究竟如何來使用分散式事務?

  諮詢過一些網際網路公司的大佬,在他們的業務場景下起碼幾百個服務,複雜的分散式大型系統,裡面其實也沒幾個分散式事務。

  其實用任何一個分散式事務的這麼一個方案,都會導致你那塊兒程式碼會複雜10倍。很多情況下,系統A呼叫系統B、系統C、系統D,我們可能根本就不做分散式事務。如果呼叫報錯會列印異常日誌或者通過返回值判斷是否需要回滾。每個月也就那麼幾個bug,很多bug是功能性的,體驗性的,真的是涉及到資料層面的一些bug,一個月就幾個,兩三個?如果你為了確保系統自動保證資料100%不能錯,上了幾十個分散式事務,程式碼太複雜;效能太差,系統吞吐量、效能大幅度下跌。

  99%的分散式介面呼叫,一些大佬給的建議不要做分散式事務,直接就是監控(發郵件、發簡訊)、記錄日誌(一旦出錯,完整的日誌)、事後快速的定位、排查和出解決方案、修復資料。這樣比你做50個分散式事務,成本要來的低上百倍,低幾十倍。

  所以在要用分散式事務的時候要權衡,分散式事務一定是有成本,程式碼會很複雜,開發很長時間,效能和吞吐量下跌,系統更加複雜更加脆弱反而更加容易出bug;但是好處就是如果做好了,TCC、可靠訊息最終一致性方案,一定可以100%保證你那快資料不會出錯。

微信訂閱號:
微信訂閱號