高頻分佈式面試題解析

一:爲啥進行系統拆分?如何進行系統拆分?爲啥要使用dubbo?

1.爲啥進行系統拆分

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

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

幾十個人負責維護一個幾十萬行代碼的單塊應用,每次上線,準備幾個禮拜,上線 -> 部署 -> 檢查自己負責的功能。拆分了以後,整個世界清爽了,幾十萬行代碼的系統,拆分成 20 個服務,平均每個服務就 1~2 萬行代碼,每個服務部署到單獨的機器上。20 個工程,20 個 git 代碼倉庫裏,20 個碼農,每個人維護自己的那個服務就可以了,是自己獨立的代碼,跟別人沒關係。再也沒有代碼衝突了,爽。每次就測試我自己的代碼就可以了,爽。每次就發佈我自己的一個小服務就可以了,爽。技術上想怎麼升級就怎麼升級,保持接口不變就可以了,爽。

所以簡單來說,一句話總結,如果是那種代碼量多達幾十萬行的中大型項目,團隊裏有幾十個人,那麼如果不拆分系統,開發效率極其低下,問題很多。但是拆分系統之後,每個人就負責自己的一小部分就好了,可以隨便玩兒隨便弄。分佈式系統拆分之後,可以大幅度提升複雜系統大型團隊的開發效率。但是同時,也要提醒的一點是,系統拆分成分佈式系統之後,大量的分佈式系統面臨的問題也是接踵而來,所以後面的問題都是在圍繞分佈式系統帶來的複雜技術挑戰在說。

2.如何進行系統拆分

系統拆分爲分佈式系統,拆成多個服務,拆成微服務的架構,是需要拆很多輪的。並不是說上來一個架構師一次就給拆好了,而以後都不用拆。

第一輪;團隊繼續擴大,拆好的某個服務,剛開始是 1 個人維護 1 萬行代碼,後來業務系統越來越複雜,這個服務是 10 萬行代碼,5 個人;第二輪,1個服務 -> 5個服務,每個服務 2 萬行代碼,每人負責一個服務。

如果是多人維護一個服務,最理想的情況下,幾十個人,1 個人負責 1 個或 2~3 個服務;某個服務工作量變大了,代碼量越來越多,某個同學,負責一個服務,代碼量變成了 10 萬行了,他自己不堪重負,他現在一個人拆開,5 個服務,1 個人頂着,負責 5 個人,接着招人,2 個人,給那個同學帶着,3 個人負責 5 個服務,其中 2 個人每個人負責 2 個服務,1 個人負責 1 個服務。

個人建議,一個服務的代碼不要太多,1萬行左右,兩三萬撐死了吧。大部分的系統,是要進行多輪拆分的,第一次拆分,可能就是將以前的多個模塊該拆分開來了,比如說將電商系統拆分成訂單系統、商品系統、採購系統、倉儲系統、用戶系統,等等吧。但是後面可能每個系統又變得越來越複雜了,比如說採購系統裏面又分成了供應商管理系統、採購單管理系統,訂單系統又拆分成了購物車系統、價格系統、訂單管理系統。扯深了實在很深,所以這裏先給大家舉個例子,你自己感受一下,核心意思就是根據情況,先拆分一輪,後面如果系統更復雜了,可以繼續分拆。你根據自己負責系統的例子,來考慮一下就好了。

3.爲啥使用dubbo,不使用dubbo可以嗎?

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

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

二:dubbo的工作原理是啥?註冊中心掛掉後可以繼續工作嗎?

1.dubbo的工作原理

dubbo分層:

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

dubbo工作流程:

  • provider 向註冊中心去註冊
  • consumer 從註冊中心訂閱服務,註冊中心會通知 consumer 註冊好的服務
  • consumer 調用 provider
  • consumer 和 provider 都異步通知監控中心

2.註冊中心掛掉後可以繼續工作嗎

可以,因爲剛開始初始化的時候,消費者會將提供者的地址等信息拉取到本地緩存,所以註冊中心掛了可以繼續通信。

三:dubbo支持哪些通信協議及序列化協議?

1.dubbo支持哪些通信協議

dubbo 協議(默認):

單一長連接,進行的是 NIO 異步通信,基於 hessian 作爲序列化協議。使用的場景是:傳輸數據量小(每次請求在 100kb 以內),但是併發量很高。爲了要支持高併發場景,一般是服務提供者就幾臺機器,但是服務消費者有上百臺,可能每天調用量達到上億次!此時用長連接是最合適的,就是跟每個服務消費者維持一個長連接就可以,可能總共就 100 個連接。然後後面直接基於長連接 NIO 異步通信,可以支撐高併發請求。長連接,通俗點說,就是建立連接過後可以持續發送請求,無須再建立連接。

長連接:建立好後,連接長期保存,後面基於這個連接發送請求

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

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

http 協議:json序列化

webservice:SOAP 文本序列化。

2.dubbo支持的序列化協議

dubbo 支持 hession、Java 二進制序列化、json、SOAP 文本序列化多種序列化協議。但是 hessian 是其默認的序列化協議。

3.序列化協議對比:https://blog.csdn.net/sanyaoxu_2/article/details/79722431

四:dubbo支持哪些種負載均衡,高可用(集羣容錯)及動態代理策略?

1.dubbo的負載均衡策略

  • random loadbalance:默認情況下,dubbo 是 random load balance 隨機調用實現負載均衡,可以對 provider 不同實例設置不同的權重,會按照權重來負載均衡,權重(權重可以根據機器的性能進行設置)越大分配流量越高,一般就用這個默認的就可以了。
  • roundrobin loadbalance:這個的話默認就是均勻地將流量打到各個機器上去,但是如果各個機器的性能不一樣,容易導致性能差的機器負載過高。所以此時需要調整權重,讓性能差的機器承載權重小一些,流量少一些。
  • leastactive loadbalance:這個就是自動感知一下,如果某個機器性能越差,那麼接收的請求越少,越不活躍,此時就會給不活躍的性能差的機器更少的請求。
  • consistanthash loadbalance:一致性 Hash 算法,相同參數的請求一定分發到一個 provider 上去,provider 掛掉的時候,會基於虛擬節點均勻分配剩餘的流量,抖動不會太大。如果你需要的不是隨機負載均衡,是要一類請求都到一個節點,那就走這個一致性 hash 策略。

2.dubbo的集羣容錯策略

  • failover cluster 模式失敗自動切換,自動重試其他機器,默認就是這個,常見於讀操作。(失敗重試其它機器)
  • failfast cluster模式快速失敗,一次調用失敗就立即失敗,常見於寫操作。(調用失敗就立即失敗)
  • failsafe cluster 模式失敗安全​​​​​​​,出現異常時忽略掉,常用於不重要的接口調用,比如記錄日誌。
  • failback cluster 模式失敗自動恢復​​​​​​​,失敗了後臺自動記錄請求,然後定時重發,比較適合於寫消息隊列這種。
  • forking cluster 模式並行調用多個 provider,只要一個成功就立即返回
  • broadcacst cluster模式廣播調用,逐個調用所有的 provider,任意一個報錯就報錯

3.dubbo動態代理策略

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

五:SPI是啥?dubbo的SPI咋玩?

1.SPI是啥:SPI,簡單來說,就是 service provider interface,說白了是什麼意思呢,比如你有個接口,現在這個接口有 3 個實現類,那麼在系統運行的時候對這個接口到底選擇哪個實現類呢?這就需要 spi 了,需要根據指定的配置或者是默認的配置,去找到對應的實現類加載進來,然後用這個實現類的實例對象。你有一個接口A。A1/A2/A3 分別是接口A的不同實現。你通過配置 接口A=實現A2,那麼在系統實際運行的時候,會加載你的配置,用實現A2實例化一個對象來提供服務。SPI機制一般用在哪兒?插件擴展的場景,比如說你開發了一個給別人使用的開源框架,如果你想讓別人自己寫個插件,插到你的開源框架裏面,從而擴展某個功能,這個時候 spi 思想就用上了。java的JDBC驅動就是SPI思想的一種體現。

2.dubbo中SPI的使用(以Protocol接口爲例)

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。如果想要動態替換掉默認的實現類,需要使用 @Adaptive 接口,Protocol 接口中,有兩個方法加了 @Adaptive 註解,就是說那倆接口會被代理實現。

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

3.在dubbo中擴展自己的組件

自己寫個maven工程,打成 jar 包,裏面的 src/main/resources 目錄下,搞一個 META-INF/services,裏面放個文件叫:com.alibaba.dubbo.rpc.Protocol,文件裏搞一個my=com.peng.MyProtocol。然後自己搞一個 dubbo provider 工程,在這個工程裏面依賴你自己搞的那個 jar,然後在 spring 配置文件裏給個配置:

<dubbo:protocol name=”my” />

provider 啓動的時候,就會加載到我們 jar 包裏的my=com.peng.MyProtocol 這行配置裏,接着會根據你的配置使用你定義好的 MyProtocol 了,這個就是簡單說明一下,你通過上述方式,可以替換掉大量的 dubbo 內部的組件,就是扔個你自己的 jar 包,然後配置一下即可。

dubbo 裏面提供了大量的類似上面的擴展點,就是說,你如果要擴展一個東西,只要自己寫個 jar,讓你的 consumer 或者是 provider 工程,依賴你的那個 jar,在你的 jar 裏指定目錄下配置好接口名稱對應的文件,裏面通過 key=實現類

然後對對應的組件,用類似 <dubbo:protocol> 用你的那個 key 對應的實現類來實現某個接口,你可以自己去擴展 dubbo 的各種功能,提供你自己的實現,更多組件擴展:http://dubbo.apache.org/zh-cn/docs/dev/impls/protocol.html

六:dubbo如何做服務治理,服務降級,服務重試,超時重試?

1.服務治理(公司沒有這個能力去研發,大概思路如下)

調用鏈路自動生成:

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

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

需要自動統計各個接口和服務之間的調用次數以及訪問延時,而且要分成兩個級別。

  • 一個級別是接口粒度,就是每個服務的每個接口每天被調用多少次,TP50/TP90/TP99,三個檔次的請求延時分別是多少;
  • 第二個級別是從源頭入口開始,一個完整的請求鏈路經過幾十個服務之後,完成一次請求,每天全鏈路走多少次,全鏈路請求延時的 TP50/TP90/TP99,分別是多少。

這些東西都搞定了之後,後面纔可以來看當前系統的壓力主要在哪裏,如何來擴容和優化啊。

其他:

  • 服務分層(避免循環依賴)
  • 調用鏈路失敗監控和報警
  • 服務鑑權
  • 每個服務的可用性的監控(接口調用成功率?幾個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.服務重試及超時重試:具體參考:https://blog.csdn.net/qq_36625757/article/details/90144580

可以結合你們公司具體的場景來說說你是怎麼設置這些參數的:

  • timeout:一般設置爲 200ms,我們認爲不能超過 200ms還沒返回。
  • retries:設置 retries,一般是在讀請求的時候,比如你要查詢個數據,你可以設置個 retries,如果第一次沒讀到,報錯,重試指定的次數,嘗試再次讀取。

七:分佈式服務接口的冪等性如何設計(比如不能重複扣款)?

所謂冪等性,就是說一個接口,多次發起同一個請求,你這個接口得保證結果是準確的,比如不能多扣款、不能多插入一條數據、不能將統計值多加了 1。這就是冪等性。其實保證冪等性主要是三點:

  • 對於每個請求必須有一個唯一的標識,舉個栗子:訂單支付請求,肯定得包含訂單 id,一個訂單 id 最多支付一次,對吧。
  • 每次處理完請求之後,必須有一個記錄標識這個請求處理過了。常見的方案是在 mysql 中記錄個狀態啥的,比如支付之前記錄一條這個訂單的支付流水,支付流水中id=訂單id。
  • 每次接收請求需要進行判斷,判斷之前是否處理過。比如說,如果有一個訂單已經支付了,就已經有了一條支付流水,那麼如果重複發送這個請求,則此時先插入支付流水,orderId 已經存在了,唯一鍵約束生效,報錯插入不進去的。然後你就不用再扣款了。

實際運作過程中,你要結合自己的業務來,比如說利用 redis,用 orderId 作爲唯一鍵。只有成功插入這個支付流水,纔可以執行實際的支付扣款。要求是支付一個訂單,必須插入一條支付流水,order_id 建一個唯一鍵 unique key。你在支付一個訂單之前,先插入一條支付流水,order_id 就已經進去了。你就可以寫一個標識到 redis 裏面去,set order_id payed,下一次重複請求過來了,先查 redis 的 order_id 對應的 value,如果是 payed 就說明已經支付過了,你就別重複支付了。

八:分佈式服務接口請求的順序性如何保證?

首先你得用 dubbo 的一致性 hash 負載均衡策略,將比如某一個訂單 id 對應的請求都給分發到某個機器上去,接着就是在那個機器上因爲可能還是多線程併發執行的,你可能得立即將某個訂單 id 對應的請求扔一個內存隊列裏去,強制排隊,這樣來確保他們的順序性。但是這樣引發的後續問題就很多,比如說要是某個訂單對應的請求特別多,造成某臺機器成熱點怎麼辦?解決這些問題又要開啓後續一連串的複雜技術方案......曾經這類問題弄的我們頭疼不已,所以,還是建議什麼呢?最好是比如說剛纔那種,一個訂單的插入和刪除操作,能不能合併成一個操作,就是一個刪除,或者是什麼,避免這種問題的產生。

九:如何自己設計一個類似dubbo的rpc框架?

  • 上來你的服務就得去註冊中心註冊吧,你是不是得有個註冊中心,保留各個服務的信心,可以用 zookeeper 來做,對吧。
  • 然後你的消費者需要去註冊中心拿對應的服務信息吧,對吧,而且每個服務可能會存在於多臺機器上。
  • 接着你就該發起一次請求了,咋發起?當然是基於動態代理了,你面向接口獲取到一個動態代理,這個動態代理就是接口在本地的一個代理,然後這個代理會找到服務對應的機器地址。
  • 然後找哪個機器發送請求?那肯定得有個負載均衡算法了,比如最簡單的可以隨機輪詢是不是。
  • 接着找到一臺機器,就可以跟它發送請求了,第一個問題咋發送?你可以說用 netty 了,nio 方式;第二個問題發送啥格式數據?你可以說用 hessian 序列化協議了,或者是別的,對吧。然後請求過去了。
  • 服務器那邊一樣的,需要針對你自己的服務生成一個動態代理,監聽某個網絡端口了,然後代理你本地的服務代碼。接收到請求的時候,就調用對應的服務代碼,對吧。

十: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 感知到切換到備用進程。

十一:分佈式鎖是啥?對比一下redis及zookeeper兩種分佈式鎖的優劣勢?

1.redis實現分佈式鎖

官方叫做RedLock算法,是redis官方支持的分佈式鎖算法。這個分佈式鎖有3個重要的考量點,互斥(只能有一個客戶端獲取鎖),不能死鎖,容錯(大部分redis節點或者這個鎖就可以加可以釋放)。

第一個最普通的實現方式:

就是在redis裏創建一個key就算加鎖。SET my:lock 隨機值 NX PX 30000,這個命令就ok,這個的NX的意思就是隻有key不存在的時候纔會設置成功,存在則設置失敗,PX 30000的意思是30秒key失效,鎖自動釋放。別人創建的時候如果發現已經有了就不能加鎖了。釋放鎖就是刪除key,但是一般可以用lua腳本刪除,判斷value一樣才刪除:

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實例。然後執行如下步驟獲取一把鎖:

  • 獲取當前時間戳,單位是毫秒;
  • 跟上面類似,輪流嘗試在每個master節點上創建鎖,過期時間較短,一般就幾十毫秒;
  • 嘗試在大多數節點上建立一個鎖,比如5個節點就要求是3個節點(n / 2 +1);
  • 客戶端計算建立好鎖的時間,如果建立鎖的時間小於超時時間,就算建立成功了;
  • 要是鎖建立失敗了,那麼就依次刪除這個鎖;
  • 只要別人建立了一把分佈式鎖,你就得不斷輪詢去嘗試獲取鎖。

2.zookeeper實現分佈式鎖

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

zk分佈式鎖的簡單實現(基於臨時節點):

/**
 * ZooKeeperSession
 */
public class ZooKeeperSession {  
	private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
	private ZooKeeper zookeeper;
	private CountDownLatch latch;
	public ZooKeeperSession() {
		try {
			//創建會話
			this.zookeeper = new ZooKeeper("192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 50000,new ZooKeeperWatcher());            
			try {
				connectedSemaphore.await();
			} catch(InterruptedException e) {
				e.printStackTrace();
			}

			System.out.println("ZooKeeper session established......");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	* 獲取單例
	*/
	public static ZooKeeperSession getInstance() {
		return Singleton.getInstance();
	}

	/**
	* 初始化單例的便捷方法
	*/
	public static void init() {
		getInstance();
	}
    
	
	/**
	 * 獲取分佈式鎖
	 * @param productId
	 */
	public Boolean acquireDistributedLock(Long productId) {
		//節點名稱
		String path = "/product-lock-" + productId; 
		try {
			//常見臨時節點,成功則返回true,失敗則會拋出異常
			zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
			return true;
		} catch (Exception e) {
			while(true) {//死循環
				try {
					Stat stat = zk.exists(path, true); //相當於是給node註冊一個監聽器,去看看這個監聽器是否存在
					if(stat != null) {//臨時節點不存在時stat=null
						this.latch = new CountDownLatch(1);
						//阻塞,等待zk臨時節點被刪除
						this.latch.await(waitTime, TimeUnit.MILLISECONDS);
						this.latch = null;
					}
					//創建臨時節點
					zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
					return true;
				} catch(Exception e) {//有創建失敗
					continue;//跳出這次循環,繼續下次循環
				}
			}
		}
		return true;
	}


	/**
	* 釋放掉一個分佈式鎖
	* @param productId
	*/
	public void releaseDistributedLock(Long productId) {
		String path = "/product-lock-" + productId;
		try {
			//刪除節點
			zookeeper.delete(path, -1); 
			System.out.println("release the lock for product[id=" + productId + "]......");  
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	
}

/**
* 建立zk session的watcher監聽器
*/
private class ZooKeeperWatcher implements Watcher {
	//當節點發生變化時,zk會回調這個方法
	public void process(WatchedEvent event) {
		System.out.println("Receive watched event: " + event.getState());
		if(KeeperState.SyncConnected == event.getState()) {
			//臨時節點被刪除,CountDownLatch-1,阻塞線程恢復運行
			connectedSemaphore.countDown();
		} 
		if(this.latch != null) {  
			this.latch.countDown();  
		}
	}
	
}


/**
 * 封裝單例的靜態內部類
 */
private static class Singleton {
	private static ZooKeeperSession instance;
	static {
		instance = new ZooKeeperSession();
	}
	public static ZooKeeperSession getInstance() {
		return instance;
	}
}

3.redis與zookeeper分佈式鎖對比

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

4.基於zk臨時順序節點實現分佈式鎖:

public class ZooKeeperDistributedLock implements Watcher{
    
    private ZooKeeper zk;
    private String locksRoot= "/locks";
    private String productId;
    private String waitNode;
    private String lockNode;
    private CountDownLatch latch;
    private CountDownLatch connectedLatch = new CountDownLatch(1);
	private int sessionTimeout = 30000; 
 
    public ZooKeeperDistributedLock(String productId){
        this.productId = productId;
         try {
			String address = "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181";
            zk = new ZooKeeper(address, sessionTimeout, this);
            connectedLatch.await();
        } catch (IOException e) {
            throw new LockException(e);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
    }
 
	//zk回調方法
    public void process(WatchedEvent event) {
        if(event.getState()==KeeperState.SyncConnected){
            connectedLatch.countDown();
            return;
        }
        if(this.latch != null) {  
            this.latch.countDown(); 
        }
    }
 
	//獲取鎖
    public void acquireDistributedLock() {   
        try {
            if(this.tryLock()){
                return;
            }
            else{
                waitForLock(waitNode, sessionTimeout);
            }
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        } 
	}
 
	//嘗試獲取鎖
    public boolean tryLock() {
        try {
			// 傳入進去的locksRoot + “/” + productId
			// 假設productId代表了一個商品id,比如說1
			// locksRoot = locks
			// /locks/10000000000,/locks/10000000001,/locks/10000000002,創建臨時順序節點,
			//多個客戶端創建節點不會出錯,而是編號0000000000,0000000001.....
            lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
   
            // 看看剛創建的節點是不是最小的節點
			// locks:10000000000,10000000001,10000000002
            List<String> locks = zk.getChildren(locksRoot, false);
            Collections.sort(locks);
    
			//如果是最小的節點,則表示取得鎖
            if(lockNode.equals(locksRoot+"/"+ locks.get(0))){
                return true;
            }
    
            //如果不是最小的節點,找到比自己小1的節點
			int previousLockIndex = -1;
            for(int i = 0; i < locks.size(); i++) {
				if(lockNode.equals(locksRoot + "/" + locks.get(i))) {
					previousLockIndex = i - 1;
					break;
				}
			}
       
			this.waitNode = locks.get(previousLockIndex);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
        return false;
    }
     
	//等待獲取鎖
    private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {
		//exists會對上一個節點創建一個監聽器
        Stat stat = zk.exists(locksRoot + "/" + waitNode, true);
        if(stat != null){//上一個節點!=null,表示鎖還沒有釋放
            this.latch = new CountDownLatch(1);
			//阻塞
            this.latch.await(waitTime, TimeUnit.MILLISECONDS);
			this.latch = null;
        }
        return true;
	}
 
	//釋放鎖
    public void unlock() {
        try {
			// 刪除/locks/10000000000節點
			// 刪除/locks/10000000001節點
            System.out.println("unlock " + lockNode);
            zk.delete(lockNode,-1);
            lockNode = null;
            zk.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
	}
 
    public class LockException extends RuntimeException {
        private static final long serialVersionUID = 1L;
        public LockException(String e){
            super(e);
        }
        public LockException(Exception e){
            super(e);
        }
	}
 
}  

如果有一把鎖,被多個人給競爭,此時多個人會排隊,第一個拿到鎖的人會執行,然後釋放鎖,後面的每個人都會去監聽排在自己前面的那個人創建的node上,一旦某個人釋放了鎖,排在自己後面的人就會被zookeeper給通知,一旦被通知了之後,就ok了,自己就獲取到了鎖,就可以執行代碼了

十二:分佈式session如何實現?

1.tomcat+redis實現

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

在 tomcat 的配置文件中配置(單機redis):

<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"/>

redis主從複製版:還可以用上面這種方式基於 redis 哨兵支持的 redis 高可用集羣來保存 session 數據。

<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"/>

這種方式會與 tomcat 容器重耦合,如果我要將 web 容器遷移成 jetty,難道還要重新把 jetty 都配置一遍?

2.spring session + redis

因爲上面那種 tomcat + redis 的方式好用,但是會嚴重依賴於web容器,不好將代碼移植到其他 web 容器上去,尤其是你要是換了技術棧咋整?比如換成了 spring cloud 或者是 spring boot 之類的呢?所以現在比較好的還是基於 Java 一站式解決方案,也就是 spring。人家 spring 基本上包掉了大部分我們需要使用的框架,spirng cloud 做微服務,spring boot 做腳手架,所以用 sping session 是一個很好的選擇。

加依賴:

<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
  <version>1.2.1.RELEASE</version>
</dependency>
<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>2.8.1</version>
</dependency>

spring配置文件:

<bean id="redisHttpSessionConfiguration"
     class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
    <property name="maxInactiveIntervalInSeconds" value="600"/>
</bean>

<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxTotal" value="100" />
    <property name="maxIdle" value="10" />
</bean>

<bean id="jedisConnectionFactory"
      class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
    <property name="hostName" value="${redis_hostname}"/>
    <property name="port" value="${redis_port}"/>
    <property name="password" value="${redis_pwd}" />
    <property name="timeout" value="3000"/>
    <property name="usePool" value="true"/>
    <property name="poolConfig" ref="jedisPoolConfig"/>
</bean>

web.xml文件配置:

<filter>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

事例代碼:

@Controller
@RequestMapping("/test")
public class TestController {

    @RequestMapping("/putIntoSession")
    @ResponseBody
    public String putIntoSession(HttpServletRequest request, String username) {
        request.getSession().setAttribute("name",  “leo”);

        return "ok";
    }

    @RequestMapping("/getFromSession")
    @ResponseBody
    public String getFromSession(HttpServletRequest request, Model model){
        String name = request.getSession().getAttribute("name");
        return name;
    }
}

上面的代碼就是 ok 的,給 sping session 配置基於 redis 來存儲 session 數據,然後配置了一個 spring session 的過濾器,這樣的話,session 相關操作都會交給 spring session 來管了。接着在代碼中,就用原生的 session 操作,就是直接基於 spring sesion 從 redis 中獲取數據了。實現分佈式的會話,有很多種很多種方式,我說的只不過比較常見的兩種方式,tomcat + redis 早期比較常用,但是會重耦合到 tomcat 中;近些年,通過 spring session 來實現。

十三:瞭解分佈式事務嗎?咋實現的?有啥坑?

在服務的調用鏈上,一個服務寫數據庫失敗,之前其他服務對數據庫的操作要全部失效

1.兩階段提交方案/XA方案

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

這種分佈式事務方案,比較適合單塊應用裏,跨多個庫的分佈式事務,而且因爲嚴重依賴於數據庫層面來搞定複雜的事務,效率很低,絕對不適合高併發的場景。如果要玩兒,那麼基於 spring + JTA 就可以搞定,自己隨便搜個 demo 看看就知道了。

這個方案,我們很少用,一般來說某個系統內部如果出現跨多個庫的這麼一個操作,是不合規的。我可以給大家介紹一下, 現在微服務,一個大的系統分成幾百個服務,幾十個服務。一般來說,我們的規定和規範,是要求每個服務只能操作自己對應的一個數據庫。如果你要操作別的服務對應的庫,不允許直連別的服務的庫,違反微服務架構的規範,你隨便交叉胡亂訪問,幾百個服務的話,全體亂套,這樣的一套服務是沒法管理的,沒法治理的,可能會出現數據被別人改錯,自己的庫被別人寫掛等情況。

如果你要操作別人的服務的庫,你必須是通過調用別的服務的接口來實現,絕對不允許交叉訪問別人的數據庫。

2.TCC 方案

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

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

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

比如說我們,一般來說跟錢相關的,跟錢打交道的,支付、交易相關的場景,我們會用 TCC,嚴格保證分佈式事務要麼全部成功,要麼全部自動回滾,嚴格保證資金的正確性,保證在資金上不會出現問題。而且最好是你的各個業務執行的時間都比較短。但是說實話,一般儘量別這麼搞,自己手寫回滾邏輯,或者是補償邏輯,實在太噁心了,那個業務代碼很難維護。

3.本地消息表

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

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

4.可靠消息最終一致性方案:這個的意思,就是乾脆不要用本地的消息表了,直接基於 MQ 來實現事務。比如阿里的 RocketMQ 就支持消息事務。

  • A 系統先發送一個 prepared 消息到 mq,如果這個 prepared 消息發送失敗那麼就直接取消操作別執行了;
  • 如果這個消息發送成功過了,那麼接着執行本地事務,如果成功就告訴 mq 發送確認消息,如果失敗就告訴 mq 回滾消息;
  • 如果發送了確認消息,那麼此時 B 系統會接收到確認消息,然後執行本地的事務;
  • 如果過A系統發送確認消息失敗,mq 會自動定時輪詢所有 prepared 消息回調你的接口,問你,這個消息是不是本地事務處理失敗了,所有沒發送確認的消息,是繼續重試還是回滾?一般來說這裏你就可以查下數據庫看之前本地事務是否執行,如果回滾了,那麼這裏也回滾吧。這個就是避免可能本地事務執行成功了,而確認消息卻發送失敗了。
  • 要是系統 B 的事務失敗了就讓MQ將消息在重發一次,B系統重新消費,自動不斷重試直到成功,要不就是在A系統提交確認消息成功後在ZK中創建一個節點,AB系統同時監聽這個節點,當B系統事務失敗後,修改ZK節點,A監聽到後再重新發送消息,這裏要注意B系統要保證系統的冪等性。
  • 這個還是比較合適的,目前國內互聯網公司大都是這麼玩兒的,要不你基於 RocketMQ 支持的,要不你就自己基於類似 ActiveMQ?RabbitMQ?自己封裝一套類似的邏輯出來,總之思路就是這樣子的。

5.最大努力通知方案

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

6.你們公司如何實現分佈式事務

如果你真的被問到,可以這麼說,我們某某特別嚴格的場景,用的是 TCC 來保證強一致性;然後其他的一些場景基於阿里的 RocketMQ 來實現了分佈式事務。你找一個嚴格資金要求絕對不能錯的場景,你可以說你是用的 TCC 方案;如果是一般的分佈式事務場景,訂單插入之後要調用庫存服務更新庫存,庫存數據沒有資金那麼的敏感,可以用可靠消息最終一致性方案。友情提示一下,RocketMQ 3.2.6 之前的版本,是可以按照上面的思路來的,但是之後接口做了一些改變。當然如果你願意,你可以參考可靠消息最終一致性方案來自己實現一套分佈式事務,比如基於 RocketMQ 來玩兒。如果系統非常龐大,服務特別多,也不適宜用特別多的分佈式事務,因爲分佈式事務實現的代碼特別複雜,難以維護,分佈式事務實現後,整個系統的吞吐量就會下降,分佈式事務的實現就能保證沒有bug嗎?往往就是服務之間直接調用,如果某個系統數據出錯,將錯誤信息返回給調用者,通過郵件的形式做監控,並記錄日誌,事後快速定位出解決方案,人工修復數據,99%用這種方案,效果比多分佈式事務要好。

十四:如何簡單設計一個高併發系統架構?

  • 系統拆分,將一個系統拆分爲多個子系統,用dubbo來搞。然後每個系統連一個數據庫,這樣本來就一個庫,現在多個數據庫,不也可以抗高併發麼。
  • 緩存,必須得用緩存。大部分的高併發場景,都是讀多寫少,那你完全可以在數據庫和緩存裏都寫一份,然後讀的時候大量走緩存不就得了。畢竟人家redis輕輕鬆鬆單機幾萬的併發啊。沒問題的。所以你可以考慮考慮你的項目裏,那些承載主要請求的讀場景,怎麼用緩存來抗高併發。
  • MQ,必須得用MQ。可能你還是會出現高併發寫的場景,比如說一個業務操作裏要頻繁搞數據庫幾十次,增刪改增刪改,瘋了。那高併發絕對搞掛你的系統,你要是用redis來承載寫那肯定不行,人家是緩存,數據隨時就被LRU了,數據格式還無比簡單,沒有事務支持。所以該用mysql還得用mysql啊。那你咋辦?用MQ吧,大量的寫請求灌入MQ裏,排隊慢慢玩兒,後邊系統消費後慢慢寫,控制在mysql承載範圍之內。所以你得考慮考慮你的項目裏,那些承載複雜寫業務邏輯的場景裏,如何用MQ來異步寫,提升併發性。MQ單機抗幾萬併發也是ok的,這個之前還特意說過。
  • 分庫分表,可能到了最後數據庫層面還是免不了抗高併發的要求,好吧,那麼就將一個數據庫拆分爲多個庫,多個庫來抗更高的併發;然後將一個表拆分爲多個表,每個表的數據量保持少一點,提高sql跑的性能。
  • 讀寫分離,這個就是說大部分時候數據庫可能也是讀多寫少,沒必要所有請求都集中在一個庫上吧,可以搞個主從架構,主庫寫入,從庫讀取,搞一個讀寫分離。讀流量太多的時候,還可以加更多的從庫。
  • Elasticsearch,可以考慮用es。es是分佈式的,可以隨便擴容,分佈式天然就可以支撐高併發,因爲動不動就可以擴容加機器來抗更高的併發。那麼一些比較簡單的查詢、統計類的操作,可以考慮用es來承載,還有一些全文搜索類的操作,也可以考慮用es來承載。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章