分佈式系統學習筆記

分佈式系統

一、什麼是分佈式系統?

在這裏插入圖片描述

二、爲什麼要進行分佈式系統的拆分?

1)要是不拆分,一個大系統幾十萬行代碼,20個人維護一份代碼,簡直是悲劇啊。代碼經常改着改着就衝突了,各種代碼衝突和合並要處理,非常耗費時間;經常我改動了我的代碼,你調用了我,導致你的代碼也得重新測試,麻煩的要死;然後每次發佈都是幾十萬行代碼的系統一起發佈,大家得一起提心吊膽準備上線,幾十萬行代碼的上線,可能每次上線都要做很多的檢查,很多異常問題的處理,簡直是又麻煩又痛苦;而且如果我現在打算把技術升級到最新的spring版本,還不行,因爲這可能導致你的代碼報錯,我不敢隨意亂改技術。

假設一個系統是20萬行代碼,其中小A在裏面改了1000行代碼,但是此時發佈的時候是這個20萬行代碼的大系統一塊兒發佈。就意味着20萬上代碼在線上就可能出現各種變化,20個人,每個人都要緊張地等在電腦面前,上線之後,檢查日誌,看自己負責的那一塊兒有沒有什麼問題。
小A就檢查了自己負責的1萬行代碼對應的功能,確保ok就閃人了;結果不巧的是,小A上線的時候不小心修改了線上機器的某個配置,導致另外小B和小C負責的2萬行代碼對應的一些功能,出錯了
幾十個人負責維護一個幾十萬行代碼的單塊應用,每次上線,準備幾個禮拜,上線 -> 部署 -> 檢查自己負責的功能

2)拆分了以後,整個世界清爽了,幾十萬行代碼的系統,拆分成20個服務,平均每個服務就1~2萬行代碼,每個服務部署到單獨的機器上。20個工程,20個git代碼倉庫裏,20個碼農,每個人維護自己的那個服務就可以了,是自己獨立的代碼,跟別人沒關係。再也沒有代碼衝突了,爽。每次就測試我自己的代碼就可以了,爽。每次就發佈我自己的一個小服務就可以了,爽。技術上想怎麼升級就怎麼升級,保持接口不變就可以了,爽。
所以簡單來說,一句話總結,如果是那種代碼量多達幾十萬行的中大型項目,團隊裏有幾十個人,那麼如果不拆分系統,開發效率極其低下,問題很多。但是拆分系統之後,每個人就負責自己的一小部分就好了,可以隨便玩兒隨便弄。分佈式系統拆分之後,可以大幅度提升複雜系統大型團隊的開發效率。

三、dubbo

對dubbo熟悉不熟悉:
(1)dubbo工作原理:服務註冊,註冊中心,消費者,代理通信,負載均衡
(2)網絡通信、序列化:dubbo協議,長連接,NIO,hessian序列化協議
(3)負載均衡策略,集羣容錯策略,動態代理策略:dubbo跑起來的時候一些功能是如何運轉的,怎麼做負載均衡?怎麼做集羣容錯?怎麼生成動態代理?
(4)dubbo SPI機制:你瞭解不瞭解dubbo的SPI機制?如何基於SPI機制對dubbo進行擴展?
(5)dubbo的服務治理、降級、重試

1. 爲什麼要用dubbo?不用dubbo可以嗎?

當然可以了,大不了最次,就是各個系統之間,直接基於spring mvc,就純http接口互相通信唄,還能咋樣。但是這個肯定是有問題的,因爲http接口通信維護起來成本很高,你要考慮超時重試、負載均衡等等各種亂七八糟的問題,比如說你的訂單系統調用商品系統,商品系統部署了5臺機器,你怎麼把請求均勻地甩給那5臺機器?這不就是負載均衡?你要是都自己搞那是可以的,但是確實很痛苦。

所以dubbo說白了,是一種rpc框架,就是本地就是進行接口調用,但是dubbo會代理這個調用請求,跟遠程機器網絡通信,給你處理掉負載均衡了、服務實例上下線自動感知了、超時重試了,等等亂七八糟的問題。那你就不用自己做了,用dubbo就可以了。

2. dubbo的工作原理

第一層:service層,接口層,給服務提供者和消費者來實現的
第二層:config層,配置層,主要是對dubbo進行各種配置的
第三層:proxy層,服務代理層,透明生成客戶端的stub和服務單的skeleton
第四層:registry層,服務註冊層,負責服務的註冊與發現
第五層:cluster層,集羣層,封裝多個服務提供者的路由以及負載均衡,將多個實例組合成一個服務
第六層:monitor層,監控層,對rpc接口的調用次數和調用時間進行監控
第七層:protocol層,遠程調用層,封裝rpc調用
第八層:exchange層,信息交換層,封裝請求響應模式,同步轉異步
第九層:transport層,網絡傳輸層,抽象mina和netty爲統一接口
第十層:serialize層,數據序列化層

工作流程:
1)第一步,provider向註冊中心去註冊
2)第二步,consumer從註冊中心訂閱服務,註冊中心會通知consumer註冊好的服務
3)第三步,consumer調用provider
4)第四步,consumer和provider都異步的通知監控中心
在這裏插入圖片描述

3. dubbo支持的通訊協議以及序列化

(1)dubbo支持不同的通信協議

1)dubbo協議

默認就是走dubbo協議的,單一長連接,NIO異步通信,基於hessian作爲序列化協議
適用的場景就是:傳輸數據量很小(每次請求在100kb以內),但是併發量很高
爲了要支持高併發場景,一般是服務提供者就幾臺機器,但是服務消費者有上百臺,可能每天調用量達到上億次!此時用長連接是最合適的,就是跟每個服務消費者維持一個長連接就可以,可能總共就100個連接。然後後面直接基於長連接NIO異步通信,可以支撐高併發請求。
否則如果上億次請求每次都是短連接的話,服務提供者會扛不住。
而且因爲走的是單一長連接,所以傳輸數據量太大的話,會導致併發能力降低。所以一般建議是傳輸數據量很小,支撐高併發訪問。

2)rmi協議

走java二進制序列化,多個短連接,適合消費者和提供者數量差不多,適用於文件的傳輸,一般較少用

3)hessian協議

走hessian序列化協議,多個短連接,適用於提供者數量比消費者數量還多,適用於文件的傳輸,一般較少用

4)http協議

走json序列化

5)webservice

走SOAP文本序列化

(2)dubbo支持的序列化協議

所以dubbo實際基於不同的通信協議,支持hessian、java二進制序列化、json、SOAP文本序列化多種序列化協議。但是hessian是其默認的序列化協議。

4. dubbo負載均衡以及集羣容錯策略、動態代理

(1)dubbo負載均衡策略

1)random loadbalance

默認情況下,dubbo是random load balance隨機調用實現負載均衡,可以對provider不同實例設置不同的權重,會按照權重來負載均衡,權重越大分配流量越高,一般就用這個默認的就可以了。

2)roundrobin loadbalance

這個的話默認就是均勻地將流量打到各個機器上去,但是如果各個機器的性能不一樣,容易導致性能差的機器負載過高。所以此時需要調整權重,讓性能差的機器承載權重小一些,流量少一些。

3)leastactive loadbalance

這個就是自動感知一下,如果某個機器性能越差,那麼接收的請求越少,越不活躍,此時就會給不活躍的性能差的機器更少的請求

4)consistanthash loadbalance

一致性Hash算法,相同參數的請求一定分發到一個provider上去,provider掛掉的時候,會基於虛擬節點均勻分配剩餘的流量,抖動不會太大。如果你需要的不是隨機負載均衡,是要一類請求都到一個節點,那就走這個一致性hash策略。
在這裏插入圖片描述

(2)dubbo集羣容錯策略

1)failover cluster模式
失敗自動切換,自動重試其他機器,默認就是這個,常見於讀操作
2)failfast cluster模式
一次調用失敗就立即失敗,常見於寫操作
3)failsafe cluster模式
出現異常時忽略掉,常用於不重要的接口調用,比如記錄日誌
4)failbackc cluster模式
失敗了後臺自動記錄請求,然後定時重發,比較適合於寫消息隊列這種
5)forking cluster
並行調用多個provider,只要一個成功就立即返回
6)broadcacst cluster
逐個調用所有的provider

(3)dubbo動態代理策略

默認使用javassist動態字節碼生成,創建代理類
但是可以通過spi擴展機制配置自己的動態代理策略

5. dubbo的SPI原理

spi,簡單來說,就是service provider interface,說白了是什麼意思呢,比如你有個接口,現在這個接口有3個實現類,那麼在系統運行的時候對這個接口到底選擇哪個實現類呢?這就需要spi了,需要根據指定的配置或者是默認的配置,去找到對應的實現類加載進來,然後用這個實現類的實例對象。
比如說你要通過jar包的方式給某個接口提供實現,然後你就在自己jar包的META-INF/services/目錄下放一個跟接口同名的文件,裏面指定接口的實現裏是自己這個jar包裏的某個類。ok了,別人用了一個接口,然後用了你的jar包,就會在運行的時候通過你的jar包的那個文件找到這個接口該用哪個實現類。
這是jdk提供的一個功能。

SPI機制,一般來說用在哪兒?插件擴展的場景,比如說你開發的是一個給別人使用的開源框架,如果你想讓別人自己寫個插件,插到你的開源框架裏面來,擴展某個功能。
經典的思想體現,大家平時都在用,比如說jdbc,java定義了一套jdbc的接口,但是java是沒有提供jdbc的實現類。但是實際上項目跑的時候,要使用jdbc接口的哪些實現類呢?一般來說,我們要根據自己使用的數據庫,比如msyql,你就將mysql-jdbc-connector.jar,引入進來;oracle,你就將oracle-jdbc-connector.jar,引入進來。在系統跑的時候,碰到你使用jdbc的接口,他會在底層使用你引入的那個jar中提供的實現類

dubbo也用了spi思想,不過沒有用jdk的spi機制,是自己實現的一套spi機制。
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
Protocol接口,dubbo要判斷一下,在系統運行的時候,應該選用這個Protocol接口的哪個實現類來實例化對象來使用呢?
他會去找一個你配置的Protocol,他就會將你配置的Protocol實現類,加載到jvm中來,然後實例化對象,就用你的那個Protocol實現類就可以了
這行代碼就是dubbo裏大量使用的,就是對很多組件,都是保留一個接口和多個實現,然後在系統運行的時候動態根據配置去找到對應的實現類。如果你沒配置,那就走默認的實現好了,沒問題。

@SPI("dubbo")  
public interface Protocol {  
    int getDefaultPort();  

    @Adaptive  
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;  

    @Adaptive  
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;  

    void destroy();  
}  

在dubbo自己的jar裏,在/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol文件中:

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol

所以說,這就看到了dubbo的spi機制默認是怎麼玩兒的了,其實就是Protocol接口,@SPI(“dubbo”)說的是,通過SPI機制來提供實現類,實現類是通過dubbo作爲默認key去配置文件裏找到的,配置文件名稱與接口全限定名一樣的,通過dubbo作爲key可以找到默認的實現了就是com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol。
dubbo的默認網絡通信協議,就是dubbo協議,用的DubboProtocol
如果想要動態替換掉默認的實現類,需要使用@Adaptive接口,Protocol接口中,有兩個方法加了@Adaptive註解,就是說那倆接口會被代理實現。
啥意思呢?

比如這個Protocol接口搞了倆@Adaptive註解標註了方法,在運行的時候會針對Protocol生成代理類,這個代理類的那倆方法裏面會有代理代碼,代理代碼會在運行的時候動態根據url中的protocol來獲取那個key,默認是dubbo,你也可以自己指定,你如果指定了別的key,那麼就會獲取別的實現類的實例了。
通過這個url中的參數不通,就可以控制動態使用不同的組件實現類

在這裏插入圖片描述

6. dubbo如何做服務治理、服務降級以及重試

(1)服務治理

1)調用鏈路自動生成

一個大型的分佈式系統,或者說是用現在流行的微服務架構來說吧,分佈式系統由大量的服務組成。那麼這些服務之間互相是如何調用的?調用鏈路是啥?說實話,幾乎到後面沒人搞的清楚了,因爲服務實在太多了,可能幾百個甚至幾千個服務。
那就需要基於dubbo做的分佈式系統中,對各個服務之間的調用自動記錄下來,然後自動將各個服務之間的依賴關係和調用鏈路生成出來,做成一張圖,顯示出來,大家纔可以看到對吧。

服務A -> 服務B -> 服務C
​               -> 服務E
​      -> 服務D
​               -> 服務F
​     -> 服務W

2)服務訪問壓力以及時長統計

需要自動統計各個接口和服務之間的調用次數以及訪問延時,而且要分成兩個級別。一個級別是接口粒度,就是每個服務的每個接口每天被調用多少次,TP50,TP90,TP99,三個檔次的請求延時分別是多少;第二個級別是從源頭入口開始,一個完整的請求鏈路經過幾十個服務之後,完成一次請求,每天全鏈路走多少次,全鏈路請求延時的TP50,TP90,TP99,分別是多少。
這些東西都搞定了之後,後面纔可以來看當前系統的壓力主要在哪裏,如何來擴容和優化啊

3)其他的

服務分層(避免循環依賴),調用鏈路失敗監控和報警,服務鑑權,每個服務的可用性的監控(接口調用成功率?幾個9?)99.99%,99.9%,99%

(2)服務降級

比如說服務A調用服務B,結果服務B掛掉了,服務A重試幾次調用服務B,還是不行,直接降級,走一個備用的邏輯,給用戶返回響應

public interface HelloService {
   void sayHello();
}

public class HelloServiceImpl implements HelloService {
    public void sayHello() {
        System.out.println("hello world......");
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://code.alibabatech.com/schema/dubbo        http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <dubbo:application name="dubbo-provider" />
    <dubbo:registry address="zookeeper://127.0.0.1:2181" />
    <dubbo:protocol name="dubbo" port="20880" />
    <dubbo:service interface="com.zhss.service.HelloService" ref="helloServiceImpl" timeout="10000" />
    <bean id="helloServiceImpl" class="com.zhss.service.HelloServiceImpl" />
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://code.alibabatech.com/schema/dubbo        http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
    <dubbo:application name="dubbo-consumer"  />
    <dubbo:registry address="zookeeper://127.0.0.1:2181" />
    <dubbo:reference id="fooService" interface="com.test.service.FooService"  timeout="10000" check="false" mock="return null">
    </dubbo:reference>
</beans>

現在就是mock,如果調用失敗統一返回null
但是可以將mock修改爲true,然後在跟接口同一個路徑下實現一個Mock類,命名規則是接口名稱加Mock後綴。然後在Mock類裏實現自己的降級邏輯。

public class HelloServiceMock implements HelloService {
    public void sayHello() {
    // 降級邏輯
    }
}

(3)失敗重試和超時重試

所謂失敗重試,就是consumer調用provider要是失敗了,比如拋異常了,此時應該是可以重試的,或者調用超時了也可以重試。

<dubbo:reference id="xxxx" interface="xx" check="true" async="false" retries="3" timeout="2000"/>

某個服務的接口,要耗費5s,你這邊不能幹等着,你這邊配置了timeout之後,我等待2s,還沒返回,我直接就撤了,不能幹等你
如果是超時了,timeout就會設置超時時間;如果是調用失敗了自動就會重試指定的次數
你就結合你們公司的具體的場景來說說你是怎麼設置這些參數的,timeout,一般設置爲200ms,我們認爲不能超過200ms還沒返回
retries,3次,設置retries,還一般是在讀請求的時候,比如你要查詢個數據,你可以設置個retries,如果第一次沒讀到,報錯,重試指定的次數,嘗試再次讀取2次

7. 分佈式服務接口的冪等性如何設計

其實保證冪等性主要是三點:

(1)對於每個請求必須有一個唯一的標識,舉個例子:訂單支付請求,肯定得包含訂單id,一個訂單id最多支付一次,對吧

(2)每次處理完請求之後,必須有一個記錄標識這個請求處理過了,比如說常見的方案是在mysql中記錄個狀態啥的,比如支付之前記錄一條這個訂單的支付流水,而且支付流水採

(3)每次接收請求需要進行判斷之前是否處理過的邏輯處理,比如說,如果有一個訂單已經支付了,就已經有了一條支付流水,那麼如果重複發送這個請求,則此時先插入支付流水,orderId已經存在了,唯一鍵約束生效,報錯插入不進去的。然後你就不用再扣款了。

上面只是給大家舉個例子,實際運作過程中,你要結合自己的業務來,比如說用redis用orderId作爲唯一鍵。只有成功插入這個支付流水,纔可以執行實際的支付扣款。

在這裏插入圖片描述

8. 分佈式服務接口請求的順序性如何保證

首先,一般來說,我個人給你的建議是,你們從業務邏輯上最好設計的這個系統不需要這種順序性的保證,因爲一旦引入順序性保障,會導致系統複雜度上升,而且會帶來效率低下,熱點數據壓力過大,等問題。

下面我給個我們用過的方案吧,簡單來說,首先你得用dubbo的一致性hash負載均衡策略,將比如某一個訂單id對應的請求都給分發到某個機器上去,接着就是在那個機器上因爲可能還是多線程併發執行的,你可能得立即將某個訂單id對應的請求扔一個內存隊列裏去,強制排隊,這樣來確保他們的順序性。

在這裏插入圖片描述

四、zookepper

1. zookeeper一般都有哪些使用場景

(1)分佈式協調

這個其實是zk很經典的一個用法,簡單來說,就好比,你A系統發送個請求到mq,然後B消息消費之後處理了。那A系統如何知道B系統的處理結果?用zk就可以實現分佈式系統之間的協調工作。A系統發送請求之後可以在zk上對某個節點的值註冊個監聽器,一旦B系統處理完了就修改zk那個節點的值,A立馬就可以收到通知,完美解決。

在這裏插入圖片描述

(2)分佈式鎖

對某一個數據連續發出兩個修改操作,兩臺機器同時收到了請求,但是隻能一臺機器先執行另外一個機器再執行。那麼此時就可以使用zk分佈式鎖,一個機器接收到了請求之後先獲取zk上的一把分佈式鎖,就是可以去創建一個znode,接着執行操作;然後另外一個機器也嘗試去創建那個znode,結果發現自己創建不了,因爲被別人創建了。。。。那隻能等着,等第一個機器執行完了自己再執行。

在這裏插入圖片描述

(3)元數據/配置信息管理

zk可以用作很多系統的配置信息的管理,比如kafka、storm等等很多分佈式系統都會選用zk來做一些元數據、配置信息的管理,包括dubbo註冊中心不也支持zk麼

在這裏插入圖片描述

(4)HA高可用性

這個應該是很常見的,比如hadoop、hdfs、yarn等很多大數據系統,都選擇基於zk來開發HA高可用機制,就是一個重要進程一般會做主備兩個,主進程掛了立馬通過zk感知到切換到備用進程

在這裏插入圖片描述

2. 分佈式鎖是啥?對比下redis和zk兩種分佈式鎖的優劣?

(1)redis分佈式鎖

官方叫做RedLock算法,是redis官方支持的分佈式鎖算法。
這個分佈式鎖有3個重要的考量點,互斥(只能有一個客戶端獲取鎖),不能死鎖,容錯(大部分redis節點或者這個鎖就可以加可以釋放)
第一個最普通的實現方式,如果就是在redis裏創建一個key算加鎖,SET my:lock 隨機值 NX PX 30000,這個命令就ok,這個的NX的意思就是隻有key不存在的時候纔會設置成功,PX 30000的意思是30秒後鎖自動釋放。別人創建的時候如果發現已經有了就不能加鎖了。
釋放鎖就是刪除key,但是一般可以用lua腳本刪除,判斷value一樣才刪除:
關於redis如何執行lua腳本,自行百度

if redis.call(“get”,KEYS[1]) == ARGV[1] then
return redis.call(“del”,KEYS[1])
else
​ return 0
end

爲啥要用隨機值呢?因爲如果某個客戶端獲取到了鎖,但是阻塞了很長時間才執行完,此時可能已經自動釋放鎖了,此時可能別的客戶端已經獲取到了這個鎖,要是你這個時候直接刪除key的話會有問題,所以得用隨機值加上面的lua腳本來釋放鎖。
但是這樣是肯定不行的。因爲如果是普通的redis單實例,那就是單點故障。或者是redis普通主從,那redis主從異步複製,如果主節點掛了,key還沒同步到從節點,此時從節點切換爲主節點,別人就會拿到鎖。

在這裏插入圖片描述

第二個問題,RedLock算法

這個場景是假設有一個redis cluster,有5個redis master實例。然後執行如下步驟獲取一把鎖:

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

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

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

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

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

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

    在這裏插入圖片描述

(2)zk分佈式鎖

zk分佈式鎖,其實可以做的比較簡單,就是某個節點嘗試創建臨時znode,此時創建成功了就獲取了這個鎖;這個時候別的客戶端來創建鎖會失敗,只能註冊個監聽器監聽這個鎖。釋放鎖就是刪除這個znode,一旦釋放掉就會通知客戶端,然後有一個等待着的客戶端就可以再次重新枷鎖。

在這裏插入圖片描述

(3)redis分佈式鎖和zk分佈式鎖的對比

redis分佈式鎖,其實需要自己不斷去嘗試獲取鎖,比較消耗性能
zk分佈式鎖,獲取不到鎖,註冊個監聽器即可,不需要不斷主動嘗試獲取鎖,性能開銷較小
另外一點就是,如果是redis獲取鎖的那個客戶端bug了或者掛了,那麼只能等待超時時間之後才能釋放鎖;而zk的話,因爲創建的是臨時znode,只要客戶端掛了,znode就沒了,此時就自動釋放鎖
redis分佈式鎖大家每發現好麻煩嗎?遍歷上鎖,計算時間等等。。。zk的分佈式鎖語義清晰實現簡單
所以先不分析太多的東西,就說這兩點,我個人實踐認爲zk的分佈式鎖比redis的分佈式鎖牢靠、而且模型簡單易用

五、session

1. 分佈式session方案是啥?

常見常用的是兩種:

(1)tomcat + redis

這個其實還挺方便的,就是使用session的代碼跟以前一樣,還是基於tomcat原生的session支持即可,然後就是用一個叫做Tomcat RedisSessionManager的東西,讓所有我們部署的tomcat都將session數據存儲到redis即可。
在tomcat的配置文件中,配置一下

<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />

<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
         host="{redis.host}"
         port="{redis.port}"
         database="{redis.dbnum}"
         maxInactiveInterval="60"/>

<!--搞一個類似上面的配置即可,你看是不是就是用了RedisSessionManager,然後指定了redis的host和 port就ok了。-->

<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
	 sentinelMaster="mymaster"
	 sentinels="<sentinel1-ip>:26379,<sentinel2-ip>:26379,<sentinel3-ip>:26379"
	 maxInactiveInterval="60"/>

還可以用上面這種方式基於redis哨兵支持的redis高可用集羣來保存session數據,都是ok的

(2)spring session + redis

分佈式會話的這個東西重耦合在tomcat中,如果我要將web容器遷移成jetty,難道你重新把jetty都配置一遍嗎?
因爲上面那種tomcat + redis的方式好用,但是會嚴重依賴於web容器,不好將代碼移植到其他web容器上去,尤其是你要是換了技術棧咋整?比如換成了spring cloud或者是spring boot之類的。還得好好思忖思忖。
所以現在比較好的還是基於java一站式解決方案,spring了。人家spring基本上包掉了大部分的我們需要使用的框架了,spirng cloud做微服務了,spring boot做腳手架了,所以用sping session是一個很好的選擇。

在這裏插入圖片描述

六、分佈式事務

1. 單系統與分佈式

1.1 但系統中的事務

在這裏插入圖片描述

1.2 分佈式系統中的事務

在這裏插入圖片描述

2. 常見的分佈式事務解決方案

(1)兩階段提交方案/XA方案

也叫做兩階段提交事務方案,這個舉個例子,比如說咱們公司裏經常tb是吧(就是團建),然後一般會有個tb主席(就是負責組織團建的那個人)。
第一個階段,一般tb主席會提前一週問一下團隊裏的每個人,說,大傢伙,下週六我們去滑雪+燒烤,去嗎?這個時候tb主席開始等待每個人的回答,如果所有人都說ok,那麼就可以決定一起去這次tb。如果這個階段裏,任何一個人回答說,我有事不去了,那麼tb主席就會取消這次活動。
第二個階段,那下週六大家就一起去滑雪+燒烤了
所以這個就是所謂的XA事務,兩階段提交,有一個事務管理器的概念,負責協調多個數據庫(資源管理器)的事務,事務管理器先問問各個數據庫你準備好了嗎?如果每個數據庫都回復ok,那麼就正式提交事務,在各個數據庫上執行操作;如果任何一個數據庫回答不ok,那麼就回滾事務。
這種分佈式事務方案,比較適合單塊應用裏,跨多個庫的分佈式事務,而且因爲嚴重依賴於數據庫層面來搞定複雜的事務,效率很低,絕對不適合高併發的場景.

這個方案,我們很少用,一般來說某個系統內部如果出現跨多個庫的這麼一個操作,是不合規的。

在這裏插入圖片描述

(2)TCC方案

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

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

1)Try階段:這個階段說的是對各個服務的資源做檢測以及對資源進行鎖定或者預留
2)Confirm階段:這個階段說的是在各個服務中執行實際的操作
3)Cancel階段:如果任何一個服務的業務方法執行出錯,那麼這裏就需要進行補償,就是執行已經執行成功的業務邏輯的回滾操作

給大家舉個例子吧,比如說跨銀行轉賬的時候,要涉及到兩個銀行的分佈式事務,如果用TCC方案來實現,思路是這樣的:

1)Try階段:先把兩個銀行賬戶中的資金給它凍結住就不讓操作了
2)Confirm階段:執行實際的轉賬操作,A銀行賬戶的資金扣減,B銀行賬戶的資金增加
3)Cancel階段:如果任何一個銀行的操作執行失敗,那麼就需要回滾進行補償,就是比如A銀行賬戶如果已經扣減了,但是B銀行賬戶資金增加失敗了,那麼就得把A銀行賬戶資金給加回去

這種方案說實話幾乎很少用人使用,我們用的也比較少,但是也有使用的場景。因爲這個事務回滾實際上是嚴重依賴於你自己寫代碼來回滾和補償了,會造成補償代碼巨大,非常之噁心。

在這裏插入圖片描述

(3)本地消息表

國外的ebay搞出來的這麼一套思想

這個大概意思是這樣的

1)A系統在自己本地一個事務裏操作同時,插入一條數據到消息表
2)接着A系統將這個消息發送到MQ中去
3)B系統接收到消息之後,在一個事務裏,往自己本地消息表裏插入一條數據,同時執行其他的業務操作,如果這個消息已經被處理過了,那麼此時這個事務會回滾,這樣保證不會重複處理消息
4)B系統執行成功之後,就會更新自己本地消息表的狀態以及A系統消息表的狀態
5)如果B系統處理失敗了,那麼就不會更新消息表狀態,那麼此時A系統會定時掃描自己的消息表,如果有沒處理的消息,會再次發送到MQ中去,讓B再次處理
6)這個方案保證了最終一致性,哪怕B事務失敗了,但是A會不斷重發消息,直到B那邊成功爲止

這個方案說實話最大的問題就在於嚴重依賴於數據庫的消息表來管理事務啥的???這個會導致如果是高併發場景咋辦呢?咋擴展呢?所以一般確實很少用

在這裏插入圖片描述

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

這個的意思,就是乾脆不要用本地的消息表了,直接基於MQ來實現事務。比如阿里的RocketMQ就支持消息事務。

大概的意思就是:

1)A系統先發送一個prepared消息到mq,如果這個prepared消息發送失敗那麼就直接取消操作別執行了
2)如果這個消息發送成功過了,那麼接着執行本地事務,如果成功就告訴mq發送確認消息,如果失敗就告訴mq回滾消息
3)如果發送了確認消息,那麼此時B系統會接收到確認消息,然後執行本地的事務
4)mq會自動定時輪詢所有prepared消息回調你的接口,問你,這個消息是不是本地事務處理失敗了,所有沒發送確認消息?那是繼續重試還是回滾?一般來說這裏你就可以查下數據庫看之前本地事務是否執行,如果回滾了,那麼這裏也回滾吧。這個就是避免可能本地事務執行成功了,別確認消息發送失敗了。
5)這個方案裏,要是系統B的事務失敗了咋辦?重試咯,自動不斷重試直到成功,如果實在是不行,要麼就是針對重要的資金類業務進行回滾,比如B系統本地回滾後,想辦法通知系統A也回滾;或者是發送報警由人工來手工回滾和補償
這個還是比較合適的,目前國內互聯網公司大都是這麼玩兒的,要不你舉用RocketMQ支持的,要不你就自己基於類似ActiveMQ?RabbitMQ?自己封裝一套類似的邏輯出來,總之思路就是這樣子的

在這裏插入圖片描述

(5)最大努力通知方案

這個方案的大致意思就是:

1)系統A本地事務執行完之後,發送個消息到MQ
2)這裏會有個專門消費MQ的最大努力通知服務,這個服務會消費MQ然後寫入數據庫中記錄下來,或者是放入個內存隊列也可以,接着調用系統B的接口
3)要是系統B執行成功就ok了;要是系統B執行失敗了,那麼最大努力通知服務就定時嘗試重新調用系統B,反覆N次,最後還是不行就放棄

在這裏插入圖片描述

六、分庫分表

在這裏插入圖片描述

1. 用過哪些分庫分表中間件?不同的分庫分表中間件都有什麼優點和缺點?

比較常見的包括:cobar、TDDL、atlas、sharding-jdbc、mycat

  • cobar:阿里b2b團隊開發和開源的,屬於proxy層方案。早些年還可以用,但是最近幾年都沒更新了,基本沒啥人用,差不多算是被拋棄的狀態吧。而且不支持讀寫分離、存儲過程、跨庫join和分頁等操作。
  • TDDL:淘寶團隊開發的,屬於client層方案。不支持join、多表查詢等語法,就是基本的crud語法是ok,但是支持讀寫分離。目前使用的也不多,因爲還依賴淘寶的diamond配置管理系統
  • atlas:360開源的,屬於proxy層方案,以前是有一些公司在用的,但是確實有一個很大的問題就是社區最新的維護都在5年前了。所以,現在用的公司基本也很少了。
  • sharding-jdbc:噹噹開源的,屬於client層方案。確實之前用的還比較多一些,因爲SQL語法支持也比較多,沒有太多限制,而且目前推出到了2.0版本,支持分庫分表、讀寫分離、分佈式id生成、柔性事務(最大努力送達型事務、TCC事務)。而且確實之前使用的公司會比較多一些(這個在官網有登記使用的公司,可以看到從2017年一直到現在,是不少公司在用的),目前社區也還一直在開發和維護,還算是比較活躍,個人認爲算是一個現在也可以選擇的方案。
  • mycat:基於cobar改造的,屬於proxy層方案,支持的功能非常完善,而且目前應該是非常火的而且不斷流行的數據庫中間件,社區很活躍,也有一些公司開始在用了。但是確實相比於sharding jdbc來說,年輕一些,經歷的錘鍊少一些。

所以綜上所述,現在其實建議考量的,就是sharding-jdbc和mycat,這兩個都可以去考慮使用。

2. 你們具體是如何對數據庫如何進行垂直拆分或水平拆分的?

水平拆分的意思,就是把一個表的數據給弄到多個庫的多個表裏去,但是每個庫的表結構都一樣,只不過每個庫表放的數據是不同的,所有庫表的數據加起來就是全部數據。水平拆分的意義,就是將數據均勻放更多的庫裏,然後用多個庫來抗更高的併發,還有就是用多個庫的存儲容量來進行擴容。
垂直拆分的意思,就是把一個有很多字段的表給拆分成多個表,或者是多個庫上去。每個庫表的結構都不一樣,每個庫表都包含部分字段。一般來說,會將較少的訪問頻率很高的字段放到一個表裏去,然後將較多的訪問頻率很低的字段放到另外一個表裏去。因爲數據庫是有緩存的,你訪問頻率高的行字段越少,就可以在緩存裏緩存更多的行,性能就越好。這個一般在表層面做的較多一些。
這個其實挺常見的,不一定我說,大家很多同學可能自己都做過,把一個大表拆開,訂單表、訂單支付表、訂單商品表。
還有表層面的拆分,就是分表,將一個表變成N個表,就是讓每個表的數據量控制在一定範圍內,保證SQL的性能。否則單表數據量越大,SQL性能就越差。一般是200萬行左右,不要太多,但是也得看具體你怎麼操作,也可能是500萬,或者是100萬。你的SQL越複雜,就最好讓單錶行數越少。
好了,無論是分庫了還是分表了,上面說的那些數據庫中間件都是可以支持的。就是基本上那些中間件可以做到你分庫分表之後,中間件可以根據你指定的某個字段值,比如說userid,自動路由到對應的庫上去,然後再自動路由到對應的表裏去。
你就得考慮一下,你的項目裏該如何分庫分表?一般來說,垂直拆分,你可以在表層面來做,對一些字段特別多的表做一下拆分;水平拆分,你可以說是併發承載不了,或者是數據量太大,容量承載不了,你給拆了,按什麼字段來拆,你自己想好;分表,你考慮一下,你如果哪怕是拆到每個庫裏去,併發和容量都ok了,但是每個庫的表還是太大了,那麼你就分表,將這個表分開,保證每個表的數據量並不是很大。
而且這兒還有兩種分庫分表的方式,一種是按照range來分,就是每個庫一段連續的數據,這個一般是按比如時間範圍來的,但是這種一般較少用,因爲很容易產生熱點問題,大量的流量都打在最新的數據上了;或者是按照某個字段hash一下均勻分散,這個較爲常用。
range來分,好處在於說,後面擴容的時候,就很容易,因爲你只要預備好,給每個月都準備一個庫就可以了,到了一個新的月份的時候,自然而然,就會寫新的庫了;缺點,但是大部分的請求,都是訪問最新的數據。實際生產用range,要看場景,你的用戶不是僅僅訪問最新的數據,而是均勻的訪問現在的數據以及歷史的數據
hash分法,好處在於說,可以平均分配沒給庫的數據量和請求壓力;壞處在於說擴容起來比較麻煩,會有一個數據遷移的這麼一個過程

在這裏插入圖片描述

3. 如何把系統遷移到分庫分表

(1)停機遷移方案

我先給你說一個最low的方案,就是很簡單,大家夥兒凌晨12點開始運維,網站或者app掛個公告,說0點到早上6點進行運維,無法訪問。。。。。。
接着到0點,停機,系統挺掉,沒有流量寫入了,此時老的單庫單表數據庫靜止了。然後你之前得寫好一個導數的一次性工具,此時直接跑起來,然後將單庫單表的數據嘩嘩譁讀出來,寫到分庫分表裏面去。
導數完了之後,就ok了,修改系統的數據庫連接配置啥的,包括可能代碼和SQL也許有修改,那你就用最新的代碼,然後直接啓動連到新的分庫分表上去。
驗證一下,ok了,完美,大家伸個懶腰,看看看凌晨4點鐘的北京夜景,打個滴滴回家吧
但是這個方案比較low,誰都能幹,我們來看看高大上一點的方案

在這裏插入圖片描述

(2)雙寫遷移方案

這個是我們常用的一種遷移方案,比較靠譜一些,不用停機,不用看北京凌晨4點的風景
簡單來說,就是在線上系統裏面,之前所有寫庫的地方,增刪改操作,都除了對老庫增刪改,都加上對新庫的增刪改,這就是所謂雙寫,同時寫倆庫,老庫和新庫。
然後系統部署之後,新庫數據差太遠,用之前說的導數工具,跑起來讀老庫數據寫新庫,寫的時候要根據gmt_modified這類字段判斷這條數據最後修改的時間,除非是讀出來的數據在新庫裏沒有,或者是比新庫的數據新纔會寫。
接着導萬一輪之後,有可能數據還是存在不一致,那麼就程序自動做一輪校驗,比對新老庫每個表的每條數據,接着如果有不一樣的,就針對那些不一樣的,從老庫讀數據再次寫。反覆循環,直到兩個庫每個表的數據都完全一致爲止。
接着當數據完全一致了,就ok了,基於僅僅使用分庫分表的最新代碼,重新部署一次,不就僅僅基於分庫分表在操作了麼,還沒有幾個小時的停機時間,很穩。所以現在基本玩兒數據遷移之類的,都是這麼幹了。

在這裏插入圖片描述

4. 如何設計可以動態擴容縮容的分庫分表方案

一開始上來就是32個庫,每個庫32個表,1024張表

我可以告訴各位同學說,這個分法,第一,基本上國內的互聯網肯定都是夠用了,第二,無論是併發支撐還是數據量支撐都沒問題
每個庫正常承載的寫入併發量是1000,那麼32個庫就可以承載32 * 1000 = 32000的寫併發,如果每個庫承載1500的寫併發,32 * 1500 = 48000的寫併發,接近5萬/s的寫入併發,前面再加一個MQ,削峯,每秒寫入MQ 8萬條數據,每秒消費5萬條數據。
有些除非是國內排名非常靠前的這些公司,他們的最核心的系統的數據庫,可能會出現幾百臺數據庫的這麼一個規模,128個庫,256個庫,512個庫
1024張表,假設每個表放500萬數據,在MySQL裏可以放50億條數據
每秒的5萬寫併發,總共50億條數據,對於國內大部分的互聯網公司來說,其實一般來說都夠了剛開始的時候,這個庫可能就是邏輯庫,建在一個數據庫上的,就是一個mysql服務器可能建了n個庫,比如16個庫。後面如果要拆分,就是不斷在庫和mysql服務器之間做遷移就可以了。然後系統配合改一下配置即可。
哪怕是要減少庫的數量,也很簡單,其實說白了就是按倍數縮容就可以了,然後修改一下路由規則。
在這裏插入圖片描述

5. 分庫分表之後全局id咋生成

(1)數據庫自增id

這個就是說你的系統裏每次得到一個id,都是往一個庫的一個表裏插入一條沒什麼業務含義的數據,然後獲取一個數據庫自增的一個id。拿到這個id之後再往對應的分庫分表裏去寫入。這個方案的好處就是方便簡單,誰都會用;缺點就是單庫生成自增id,要是高併發的話,就會有瓶頸的;如果你硬是要改進一下,那麼就專門開一個服務出來,這個服務每次就拿到當前id最大值,然後自己遞增幾個id,一次性返回一批id,然後再把當前最大id值修改成遞增幾個id之後的一個值;但是無論怎麼說都是基於單個數據庫。適合的場景:你分庫分表就倆原因,要不就是單庫併發太高,要不就是單庫數據量太大;除非是你併發不高,但是數據量太大導致的分庫分表擴容,你可以用這個方案,因爲可能每秒最高併發最多就幾百,那麼就走單獨的一個庫和表生成自增主鍵即可。併發很低,幾百/s,但是數據量大,幾十億的數據,所以需要靠分庫分表來存放海量的數據

(2)uuid

好處就是本地生成,不要基於數據庫來了;不好之處就是,uuid太長了,作爲主鍵性能太差了,不適合用於主鍵。適合的場景:如果你是要隨機生成個什麼文件名了,編號之類的,你可以用uuid,但是作爲主鍵是不能用uuid的。

UUID.randomUUID().toString().replace(“-”, “”) -> sfsdf23423rr234sfdaf

(3)獲取系統當前時間

這個就是獲取當前時間即可,但是問題是,併發很高的時候,比如一秒併發幾千,會有重複的情況,這個是肯定不合適的。基本就不用考慮了。適合的場景:一般如果用這個方案,是將當前時間跟很多其他的業務字段拼接起來,作爲一個id,如果業務上你覺得可以接受,那麼也是可以的。你可以將別的業務字段值跟當前時間拼接起來,組成一個全局唯一的編號,訂單編號,時間戳 + 用戶id + 業務含義編碼

(4)snowflake算法

twitter開源的分佈式id生成算法,就是把一個64位的long型的id,1個bit是不用的,用其中的41 bit作爲毫秒數,用10 bit作爲工作機器id,12 bit作爲序列號
1 bit:不用,爲啥呢?因爲二進制裏第一個bit爲如果是1,那麼都是負數,但是我們生成的id都是正數,所以第一個bit統一都是0
41 bit:表示的是時間戳,單位是毫秒。41 bit可以表示的數字多達2^41 - 1,也就是可以標識2 ^ 41 - 1個毫秒值,換算成年就是表示69年的時間。
10 bit:記錄工作機器id,代表的是這個服務最多可以部署在2^10臺機器上哪,也就是1024臺機器。但是10 bit裏5個bit代表機房id,5個bit代表機器id。意思就是最多代表2 ^ 5個機房(32個機房),每個機房裏可以代表2 ^ 5個機器(32臺機器)。
12 bit:這個是用來記錄同一個毫秒內產生的不同id,12 bit可以代表的最大正整數是2 ^ 12 - 1 = 4096,也就是說可以用這個12bit代表的數字來區分同一個毫秒內的4096個不同的id

64位的long型的id,64位的long -> 二進制

0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000

2018-01-01 10:00:00 -> 做了一些計算,再換算成一個二進制,41bit來放 -> 0001100 10100010 10111110 10001001 01011100 00

機房id,17 -> 換算成一個二進制 -> 10001

機器id,25 -> 換算成一個二進制 -> 11001

snowflake算法服務,會判斷一下,當前這個請求是否是,機房17的機器25,在2175/11/7 12:12:14時間點發送過來的第一個請求,如果是第一個請求

假設,在2175/11/7 12:12:14時間裏,機房17的機器25,發送了第二條消息,snowflake算法服務,會發現說機房17的機器25,在2175/11/7 12:12:14時間裏,在這一毫秒,之前已經生成過一個id了,此時如果你同一個機房,同一個機器,在同一個毫秒內,再次要求生成一個id,此時我只能把加1

0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000001

比如我們來觀察上面的那個,就是一個典型的二進制的64位的id,換算成10進制就是910499571847892992。

在這裏插入圖片描述

七、Mysql主從複製及讀寫分離

(1)如何實現mysql的讀寫分離?

其實很簡單,就是基於主從複製架構,簡單來說,就搞一個主庫,掛多個從庫,然後我們就單單只是寫主庫,然後主庫會自動把數據給同步到從庫上去。

(2)MySQL主從複製原理的是啥?

主庫將變更寫binlog日誌,然後從庫連接到主庫之後,從庫有一個IO線程,將主庫的binlog日誌拷貝到自己本地,寫入一箇中繼日誌中。接着從庫中有一個SQL線程會從中繼日誌讀取binlog,然後執行binlog日誌中的內容,也就是在自己本地再次執行一遍SQL,這樣就可以保證自己跟主庫的數據是一樣的。
這裏有一個非常重要的一點,就是從庫同步主庫數據的過程是串行化的,也就是說主庫上並行的操作,在從庫上會串行執行。所以這就是一個非常重要的點了,由於從庫從主庫拷貝日誌以及串行執行SQL的特點,在高併發場景下,從庫的數據一定會比主庫慢一些,是有延時的。所以經常出現,剛寫入主庫的數據可能是讀不到的,要過幾十毫秒,甚至幾百毫秒才能讀取到。
而且這裏還有另外一個問題,就是如果主庫突然宕機,然後恰好數據還沒同步到從庫,那麼有些數據可能在從庫上是沒有的,有些數據可能就丟失了。所以mysql實際上在這一塊有兩個機制,一個是半同步複製,用來解決主庫數據丟失問題;一個是並行複製,用來解決主從同步延時問題。

這個所謂半同步複製,semi-sync複製,指的就是主庫寫入binlog日誌之後,就會將強制此時立即將數據同步到從庫,從庫將日誌寫入自己本地的relay log之後,接着會返回一個ack給主庫,主庫接收到至少一個從庫的ack之後纔會認爲寫操作完成了。

所謂並行複製,指的是從庫開啓多個線程,並行讀取relay log中不同庫的日誌,然後並行重放不同庫的日誌,這是庫級別的並行。
1)主從複製的原理
2)主從延遲問題產生的原因
3)主從複製的數據丟失問題,以及半同步複製的原理
4)並行複製的原理,多庫併發重放relay日誌,緩解主從延遲問題

在這裏插入圖片描述

(3)mysql主從同步延時問題(精華)

線上確實處理過因爲主從同步延時問題,導致的線上的bug,小型的生產事故
show status,Seconds_Behind_Master,你可以看到從庫複製主庫的數據落後了幾ms
其實這塊東西我們經常會碰到,就比如說用了mysql主從架構之後,可能會發現,剛寫入庫的數據結果沒查到,結果就完蛋了。。。。
所以實際上你要考慮好應該在什麼場景下來用這個mysql主從同步,建議是一般在讀遠遠多於寫,而且讀的時候一般對數據時效性要求沒那麼高的時候,用mysql主從同步
所以這個時候,我們可以考慮的一個事情就是,你可以用mysql的並行複製,但是問題是那是庫級別的並行,所以有時候作用不是很大
所以這個時候。。通常來說,我們會對於那種寫了之後立馬就要保證可以查到的場景,採用強制讀主庫的方式,這樣就可以保證你肯定的可以讀到數據了吧。其實用一些數據庫中間件是沒問題的。
一般來說,如果主從延遲較爲嚴重

  • 1、分庫,將一個主庫拆分爲4個主庫,每個主庫的寫併發就500/s,此時主從延遲可以忽略不計
  • 2、打開mysql支持的並行複製,多個庫並行複製,如果說某個庫的寫入併發就是特別高,單庫寫並發達到了2000/s,並行複製還是沒意義。28法則,很多時候比如說,就是少數的幾個訂單表,寫入了2000/s,其他幾十個表10/s。
  • 3、重寫代碼,寫代碼的同學,要慎重,當時我們其實短期是讓那個同學重寫了一下代碼,插入數據之後,直接就更新,不要查詢
  • 4、如果確實是存在必須先插入,立馬要求就查詢到,然後立馬就要反過來執行一些操作,對這個查詢設置直連主庫。不推薦這種方法,你這麼搞導致讀寫分離的意義就喪失了

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章