|
原文鏈接:https://blog.csdn.net/qq_36236890/article/details/82355522
一、分布式鎖 分布式鎖是在分布式場景下一種常見技術(shù),通常通過基于redis和zookeeper來實現(xiàn),本文主要介紹redis分布式鎖和zookeeper分布式鎖的實現(xiàn)方案和對比: ?。?)基于redis的普通實現(xiàn) 這個方案的加鎖主要實現(xiàn)是基于redis的”SET key 隨機值 NX PX 過期時間(毫秒)”指令,NX代表只有key不存在時才設(shè)置成功,PX代表在過期時間后會自動釋放。 這個方案的釋放鎖是通過lua腳本刪除key的方式,判斷value一樣則刪除key。 使用隨機值的原因是如果某個獲取到鎖的客戶端阻塞了很長時間,導致了它獲取到的鎖已經(jīng)自動釋放,此時可能有其他客戶端已經(jīng)獲取到了鎖,如果直接刪除是有問題的,所以要通過隨機值加上lua腳本去判斷如果value相等時再刪除。 這個方案存在一個問題就是,如果采用redis單實例可能會存在單點故障問題,但如果采用普通主從方式,如果主節(jié)點掛了key還沒來得及同步到從節(jié)點,此時從節(jié)點被切換到了主節(jié)點,由于沒有同步到數(shù)據(jù)別人就會拿到鎖。 (2)redis的RedLock算法 這個方案是redis官方推薦的分布式鎖的解決方案,假設(shè)有5個redis master實例,然后執(zhí)行如下步驟去獲取一把鎖: 1)獲取當前時間戳,單位是毫秒 2)跟上面類似,輪流嘗試在每個master節(jié)點上創(chuàng)建鎖,過期時間較短,一般就幾十毫秒 3)嘗試在大多數(shù)節(jié)點上建立一個鎖,比如5個節(jié)點就要求是3個節(jié)點(n / 2 1) 4)客戶端計算建立好鎖的時間,如果建立鎖的時間小于超時時間,就算建立成功了 5)要是鎖建立失敗了,那么就依次刪除這個鎖 6)只要別人建立了一把分布式鎖,你就得不斷輪詢?nèi)L試獲取鎖 這個方案實現(xiàn)起來較為麻煩,不過貌似有很多開源的插件封裝了這個算法,但是這個算法好像在國外是有爭議的。 ?。?) zookeeper的臨時節(jié)點 這個方案的實現(xiàn)是通過在獲取鎖時在zookeeper上創(chuàng)建臨時節(jié)點(常用為臨時順序節(jié)點),如果創(chuàng)建成功(如果是臨時順序節(jié)點則為當前最?。?,就代表獲取了到了這個鎖;如果創(chuàng)建創(chuàng)建失?。ㄈ绻桥R時順序節(jié)點,當前自己節(jié)點不是最?。瑒t注冊監(jiān)聽器監(jiān)聽這個鎖。釋放鎖時會刪除這個臨時節(jié)點,測試由于注冊了監(jiān)聽器,其他等待鎖的線程則會收到釋放鎖的消息,然后再去嘗試獲取鎖。 (4)redis分布式鎖和zookeeper分布式鎖的對比 通過對比redis分布式鎖和zk分布式鎖可以發(fā)現(xiàn),redis分布式鎖類似于自旋鎖的方式需要自己不斷嘗試去獲取鎖,這個是比較耗性能的。zk獲取不到鎖的話則可以注冊監(jiān)聽器,不需要不斷嘗試,這樣的活性能開銷較??;其次,redis鎖有一個問題就是,如果獲取到鎖的客戶端崩潰了或者沒有正常釋放鎖則會導致只能等到過期時間完了才能獲取到鎖,而zk建立的由于是臨時節(jié)點,客戶端崩潰了或者掛了,臨時節(jié)點會自動刪除,此時會自動釋放鎖;最后,這個redis的實現(xiàn)方式如果采用RedLock算法的話較為復雜并且還存在爭議,普通的算法存在單點故障和主從同步的問題,所以一般來說,個人認為zk分布式鎖要比redis分布式鎖更可靠并且易用。 二、分布式session 在傳統(tǒng)的單體應(yīng)用下,我們可以通過session去存儲一些數(shù)據(jù),但是在分布式和為微服務(wù)結(jié)構(gòu)下,如果需要使用session就需要采取一些手段去維護,以下提供2個方案去實現(xiàn)分布式session: (1)tomcat redis 由于我們傳統(tǒng)的應(yīng)用基本都是通過tomcat去部署的,我們可以利用tomcat的RedisSessionManager來將session的數(shù)據(jù)都存在redis中。 但這種方案現(xiàn)在不怎么使用了,因為移植性很差,如果要換web容器就尷尬了。 (2)spring session redis 給sping session配置基于redis來存儲session數(shù)據(jù),然后配置一個spring session的過濾器,這樣的話,session相關(guān)操作都會交給spring session來管了。接著在代碼中,就用原生的session操作,就是直接基于spring sesion從redis中獲取數(shù)據(jù)了。 這個方案現(xiàn)在還是比較主流的,因為現(xiàn)在主流的開發(fā)基本都是基于spring開源的一些框架,所以說如果換技術(shù)棧的影響不會很大。 三、分布式事務(wù) 現(xiàn)在分布式系統(tǒng)成為了主流,但使用分布式也隨之帶來了一些問題和痛點,分布式事務(wù)就是最常見的一個問題,本文主要介紹分布式事務(wù)的一些常見解決方案。 ?。?)兩階段提交方案/XA方案? 這種分布式事務(wù)方案,比較適合單塊應(yīng)用里,跨多個庫的分布式事務(wù),而且因為嚴重依賴于數(shù)據(jù)庫層面來搞定復雜的事務(wù),效率很低,絕對不適合高并發(fā)的場景。如果要玩兒,那么基于spring JTA就可以搞定。 這個方案很少用,一般來說某個系統(tǒng)內(nèi)部如果出現(xiàn)跨多個庫的這么一個操作,是不合規(guī)的。我可以給大家介紹一下, 現(xiàn)在微服務(wù),一個大的系統(tǒng)分成幾百個服務(wù),幾十個服務(wù)。一般來說,我們的規(guī)定和規(guī)范,是要求說每個服務(wù)只能操作自己對應(yīng)的一個數(shù)據(jù)庫。如果你要操作別的服務(wù)對應(yīng)的庫,不允許直連別的服務(wù)的庫,違反微服務(wù)架構(gòu)的規(guī)范,如果你要操作別人的服務(wù)的庫,你必須是通過調(diào)用別的服務(wù)的接口來實現(xiàn),絕對不允許你交叉訪問別人的數(shù)據(jù)庫! ?。?)TCC方案 TCC的全程是:Try、Confirm、Cancel。 這個其實是用到了補償?shù)母拍睿譃榱巳齻€階段: 1)Try階段:這個階段說的是對各個服務(wù)的資源做檢測以及對資源進行鎖定或者預留 2)Confirm階段:這個階段說的是在各個服務(wù)中執(zhí)行實際的操作 3)Cancel階段:如果任何一個服務(wù)的業(yè)務(wù)方法執(zhí)行出錯,那么這里就需要進行補償,就是執(zhí)行已經(jīng)執(zhí)行成功的業(yè)務(wù)邏輯的回滾操作。 這種方案說實話幾乎很少用人使用,因為這個事務(wù)回滾實際上是嚴重依賴于你自己寫代碼來回滾和補償了,會造成補償代碼巨大,非常之惡心。 比較適合的場景:這個就是除非你是真的一致性要求太高,是你系統(tǒng)中核心之核心的場景,比如常見的就是資金類的場景,那你可以用TCC方案了,自己編寫大量的業(yè)務(wù)邏輯,自己判斷一個事務(wù)中的各個環(huán)節(jié)是否ok,不ok就執(zhí)行補償/回滾代碼。 ?。?)本地消息表 本地消息表示國外的ebay搞出來的這么一套思想 本地消息表來實現(xiàn)分布式事務(wù)的思路大致如下: 1)A系統(tǒng)在自己本地一個事務(wù)里操作同時,插入一條數(shù)據(jù)到消息表 2)接著A系統(tǒng)將這個消息發(fā)送到MQ中去 3)B系統(tǒng)接收到消息之后,在一個事務(wù)里,往自己本地消息表里插入一條數(shù)據(jù),同時執(zhí)行其他的業(yè)務(wù)操作,如果這個消息已經(jīng)被處理過了,那么此時這個事務(wù)會回滾,這樣保證不會重復處理消息 4)B系統(tǒng)執(zhí)行成功之后,就會更新自己本地消息表的狀態(tài)以及A系統(tǒng)消息表的狀態(tài) 5)如果B系統(tǒng)處理失敗了,那么就不會更新消息表狀態(tài),那么此時A系統(tǒng)會定時掃描自己的消息表,如果有沒處理的消息,會再次發(fā)送到MQ中去,讓B再次處理 6)這個方案保證了最終一致性,哪怕B事務(wù)失敗了,但是A會不斷重發(fā)消息,直到B那邊成功為止。 這個方案最大的弊端在于依賴于數(shù)據(jù)庫消息表來保證事務(wù),但是在高并發(fā)場景下,數(shù)據(jù)庫就成了瓶頸。 ?。?)可靠消息最終一致性方案 這個方案的大致思路為: 1)A系統(tǒng)先發(fā)送一個prepared消息到mq,如果這個prepared消息發(fā)送失敗那么就直接取消操作別執(zhí)行了 2)如果這個消息發(fā)送成功過了,那么接著執(zhí)行本地事務(wù),如果成功就告訴mq發(fā)送確認消息,如果失敗就告訴mq回滾消息 3)如果發(fā)送了確認消息,那么此時B系統(tǒng)會接收到確認消息,然后執(zhí)行本地的事務(wù) 4)mq會自動定時輪詢所有prepared消息回調(diào)你的接口,問你,這個消息是不是本地事務(wù)處理失敗了,所有沒發(fā)送確認消息?那是繼續(xù)重試還是回滾?一般來說這里你就可以查下數(shù)據(jù)庫看之前本地事務(wù)是否執(zhí)行,如果回滾了,那么這里也回滾吧。這個就是避免可能本地事務(wù)執(zhí)行成功了,別確認消息發(fā)送失敗了。 5)這個方案里,要是系統(tǒng)B的事務(wù)失敗了咋辦?重試咯,自動不斷重試直到成功,如果實在是不行,要么就是針對重要的資金類業(yè)務(wù)進行回滾,比如B系統(tǒng)本地回滾后,想辦法通知系統(tǒng)A也回滾;或者是發(fā)送報警由人工來手工回滾和補償。 這個方案是目前國內(nèi)公司采用較多的一種方案。 ?。?)最大努力通知方案 思路: 1)系統(tǒng)A本地事務(wù)執(zhí)行完之后,發(fā)送個消息到MQ 2)這里會有個專門消費MQ的最大努力通知服務(wù),這個服務(wù)會消費MQ然后寫入數(shù)據(jù)庫中記錄下來,或者是放入個內(nèi)存隊列也可以,接著調(diào)用系統(tǒng)B的接口 3)要是系統(tǒng)B執(zhí)行成功就ok了;要是系統(tǒng)B執(zhí)行失敗了,那么最大努力通知服務(wù)就定時嘗試重新調(diào)用系統(tǒng)B,反復N次,最后還是不行就放棄。 ?。?)究竟如何來使用分布式事務(wù)? 咨詢過一些互聯(lián)網(wǎng)公司的大佬,在他們的業(yè)務(wù)場景下起碼幾百個服務(wù),復雜的分布式大型系統(tǒng),里面其實也沒幾個分布式事務(wù)。 其實用任何一個分布式事務(wù)的這么一個方案,都會導致你那塊兒代碼會復雜10倍。很多情況下,系統(tǒng)A調(diào)用系統(tǒng)B、系統(tǒng)C、系統(tǒng)D,我們可能根本就不做分布式事務(wù)。如果調(diào)用報錯會打印異常日志或者通過返回值判斷是否需要回滾。每個月也就那么幾個bug,很多bug是功能性的,體驗性的,真的是涉及到數(shù)據(jù)層面的一些bug,一個月就幾個,兩三個?如果你為了確保系統(tǒng)自動保證數(shù)據(jù)100%不能錯,上了幾十個分布式事務(wù),代碼太復雜;性能太差,系統(tǒng)吞吐量、性能大幅度下跌。 99%的分布式接口調(diào)用,一些大佬給的建議不要做分布式事務(wù),直接就是監(jiān)控(發(fā)郵件、發(fā)短信)、記錄日志(一旦出錯,完整的日志)、事后快速的定位、排查和出解決方案、修復數(shù)據(jù)。這樣比你做50個分布式事務(wù),成本要來的低上百倍,低幾十倍。 所以在要用分布式事務(wù)的時候要權(quán)衡,分布式事務(wù)一定是有成本,代碼會很復雜,開發(fā)很長時間,性能和吞吐量下跌,系統(tǒng)更加復雜更加脆弱反而更加容易出bug;但是好處就是如果做好了,TCC、可靠消息最終一致性方案,一定可以100%保證你那快數(shù)據(jù)不會出錯。 |
|
|