分布式高可用架構(gòu)總結(jié)(二)

目錄
1 消息隊列
2 緩存
3 分布式系統(tǒng)
4 Dubbo
5 數(shù)據(jù)庫

參考:
· 中華石杉視頻

3 分布式系統(tǒng)

3.1 分布式系統(tǒng)是什么?

image

3.2 分布式服務(wù)接口的冪等性如何設(shè)計(比如不能重復(fù)扣款)?

image

所謂冪等性,就是說一個接口,多次發(fā)起同一個請求,你這個接口得保證結(jié)果是準(zhǔn)確的,比如不能多扣款,不能多插入一條數(shù)據(jù),不能將統(tǒng)計值多加了1。這就是冪等性,不給大家來學(xué)術(shù)性詞語了。

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

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

(2)每次處理完請求之后,必須有一個記錄標(biāo)識這個請求處理過了,比如說常見的方案是在mysql中記錄個狀態(tài)啥的

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

上面只是給大家舉個例子,實際運作過程中,你要結(jié)合自己的業(yè)務(wù)來,比如說用redis用orderId作為唯一鍵。只有成功插入這個支付流水,才可以執(zhí)行實際的支付扣款。要求是支付一個訂單,必須插入一條支付流水,order_id建一個唯一鍵,unique key。所以你在支付一個訂單之前,先插入一條支付流水,order_id就已經(jīng)進(jìn)去了。你就可以寫一個標(biāo)識到redis里面去,set order_id payed,下一次重復(fù)請求過來了,先查redis的order_id對應(yīng)的value,如果是payed就說明已經(jīng)支付過了,你就別重復(fù)支付了。然后呢,你再重復(fù)支付這個訂單的時候,你也嘗試插入一條支付流水,數(shù)據(jù)庫給你報錯了,說unique key沖突了,整個事務(wù)回滾就可以了。
保存一個是否處理過的標(biāo)識也可以,服務(wù)的不同實例可以一起操作redis。

3.3 分布式服務(wù)接口請求的順序性如何保證?

image

(1)特定表示通過一致性哈希到同一臺機(jī)器 + 內(nèi)存隊列保存順序 + 同一線程讀取

(2)MQ保證順序,做成異步

(3)Zookeeper分布式鎖(開銷大)或順序節(jié)點

3.4 集群部署時的分布式session如何實現(xiàn)?

image

其實方法很多,但是常見常用的是兩種:

(1)tomcat + redis

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

在tomcat的配置文件中,配置一下:

        host="{redis.host}"
        port="{redis.port}"
        database="{redis.dbnum}"
        maxInactiveInterval="60"/>

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

          sentinelMaster="mymaster"
          sentinels=":26379,:26379,:26379"
          maxInactiveInterval="60"/>

還可以用上面這種方式基于redis哨兵支持的redis高可用集群來保存session數(shù)據(jù),都是ok的

(2)spring session + redis
tomcat配置分布式session的缺點:分布式會話的這個東西重耦合在tomcat中,如果我要將web容器遷移成jetty,難道你重新把jetty都配置一遍嗎?因為上面那種tomcat + redis的方式好用,但是會嚴(yán)重依賴于web容器,不好將代碼移植到其他web容器上去,尤其是你要是換了技術(shù)棧咋整?比如換成了spring cloud或者是spring boot之類的。還得好好思忖思忖。

所以現(xiàn)在比較好的還是基于java一站式解決方案,spring了。人家spring基本上包掉了大部分的我們需要使用的框架了,spirng cloud做微服務(wù)了,spring boot做腳手架了,所以用sping session是一個很好的選擇。

pom.xml

<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, Stringusername){

       request.getSession().setAttribute("name",  “l(fā)eo”);

       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數(shù)據(jù),然后配置了一個spring session的過濾器,這樣的話,session相關(guān)操作都會交給spring session來管了。接著在代碼中,就用原生的session操作,就是直接基于spring sesion從redis中獲取數(shù)據(jù)了。

實現(xiàn)分布式的會話,有很多種很多種方式,我說的只不過比較常見的兩種方式,tomcat + redis早期比較常用;近些年,重耦合到tomcat中去,通過spring session來實現(xiàn)。

3.5 分布式事務(wù)了解嗎?你們?nèi)绾谓鉀Q分布式事務(wù)問題的?

image.png

image.png

在實際場景中,只有1%,0.1%,0.01%的業(yè)務(wù),如資金、交易、訂單,我們會用分布式事務(wù)方案來保證,會員積分、優(yōu)惠券、商品信息,其實不要這么搞了,不然是的系統(tǒng)設(shè)計過于復(fù)雜而不可靠。

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

image.png

也叫做兩階段提交事務(wù)方案,這個舉個例子,比如說咱們公司里經(jīng)常tb是吧(就是團(tuán)建,team building),然后一般會有個tb主席(就是負(fù)責(zé)組織團(tuán)建的那個人)。

第一個階段,一般tb主席會提前一周問一下團(tuán)隊里的每個人,說,大家伙,下周六我們?nèi)セ?燒烤,去嗎?這個時候tb主席開始等待每個人的回答,如果所有人都說ok,那么就可以決定一起去這次tb。如果這個階段里,任何一個人回答說,我有事不去了,那么tb主席就會取消這次活動。

第二個階段,那下周六大家就一起去滑雪+燒烤了

所以這個就是所謂的XA事務(wù),兩階段提交,有一個事務(wù)管理器的概念,負(fù)責(zé)協(xié)調(diào)多個數(shù)據(jù)庫(資源管理器)的事務(wù),事務(wù)管理器先問問各個數(shù)據(jù)庫你準(zhǔn)備好了嗎?如果每個數(shù)據(jù)庫都回復(fù)ok,那么就正式提交事務(wù),在各個數(shù)據(jù)庫上執(zhí)行操作;如果任何一個數(shù)據(jù)庫回答不ok,那么就回滾事務(wù)。

這種分布式事務(wù)方案,比較適合單塊應(yīng)用里,跨多個庫的分布式事務(wù),而且因為嚴(yán)重依賴于數(shù)據(jù)庫層面來搞定復(fù)雜的事務(wù),效率很低,絕對不適合高并發(fā)的場景。如果要玩兒,那么基于spring + JTA就可以搞定,自己隨便搜個demo看看就知道了。

這個方案,我們很少用,一般來說某個系統(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ù)的話,全體亂套,這樣的一套服務(wù)是沒法管理的,沒法治理的,經(jīng)常數(shù)據(jù)被別人改錯,自己的庫被別人寫掛。

如果你要操作別人的服務(wù)的庫,你必須是通過調(diào)用別的服務(wù)的接口來實現(xiàn),絕對不允許你交叉訪問別人的數(shù)據(jù)庫!

(2)TCC方案

image.png

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

這個其實是用到了補償?shù)母拍睿譃榱巳齻€階段:

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

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

3)Cancel階段:如果任何一個服務(wù)的業(yè)務(wù)方法執(zhí)行出錯,那么這里就需要進(jìn)行補償,就是執(zhí)行已經(jīng)執(zhí)行成功的業(yè)務(wù)邏輯的回滾操作

給大家舉個例子吧,比如說跨銀行轉(zhuǎn)賬的時候,要涉及到兩個銀行的分布式事務(wù),如果用TCC方案來實現(xiàn),思路是這樣的:

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

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

比如說我們,一般來說跟錢相關(guān)的,跟錢打交道的,支付、交易相關(guān)的場景,我們會用TCC,嚴(yán)格嚴(yán)格保證分布式事務(wù)要么全部成功,要么全部自動回滾,嚴(yán)格保證資金的正確性,在資金上出現(xiàn)問題

比較適合的場景:這個就是除非你是真的一致性要求太高,是你系統(tǒng)中核心之核心的場景,比如常見的就是資金類的場景,那你可以用TCC方案了,自己編寫大量的業(yè)務(wù)邏輯,自己判斷一個事務(wù)中的各個環(huán)節(jié)是否ok,不ok就執(zhí)行補償/回滾代碼

而且最好是你的各個業(yè)務(wù)執(zhí)行的時間都比較短。

但是說實話,一般盡量別這么搞,自己手寫回滾邏輯,或者是補償邏輯,實在太惡心了,那個業(yè)務(wù)代碼很難維護(hù)

(3)本地消息表

image.png

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

這個大概意思是這樣的

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ù)會回滾,這樣保證不會重復(fù)處理消息

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那邊成功為止

這個方案說實話最大的問題就在于嚴(yán)重依賴于數(shù)據(jù)庫的消息表來管理事務(wù)啥的???這個會導(dǎo)致如果是高并發(fā)場景咋辦呢?咋擴(kuò)展呢?所以一般確實很少用

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

image.png

這個的意思,就是干脆不要用本地的消息表了,直接基于MQ來實現(xiàn)事務(wù)。比如阿里的RocketMQ就支持消息事務(wù)。

大概的意思就是:

1)A系統(tǒng)先發(fā)送一個prepared消息到mq,如果這個prepared消息發(fā)送失敗那么就直接取消操作別執(zhí)行了
2)如果這個消息發(fā)送成功過了,那么接著執(zhí)行本地事務(wù),如果成功就告訴mq發(fā)送確認(rèn)消息,如果失敗就告訴mq回滾消息
3)如果發(fā)送了確認(rèn)消息,那么此時B系統(tǒng)會接收到確認(rèn)消息,然后執(zhí)行本地的事務(wù)
4)mq會自動定時輪詢所有prepared消息回調(diào)你的接口,問你,這個消息是不是本地事務(wù)處理失敗了,沒發(fā)送確認(rèn)消息?那是繼續(xù)重試還是回滾?一般來說這里你就可以查下數(shù)據(jù)庫看之前本地事務(wù)是否執(zhí)行,如果回滾了,那么這里也回滾吧。這個就是避免可能本地事務(wù)執(zhí)行成功了,別確認(rèn)消息發(fā)送失敗了。
5)這個方案里,要是系統(tǒng)B的事務(wù)失敗了咋辦?重試咯,自動不斷重試直到成功,如果實在是不行,要么就是針對重要的資金類業(yè)務(wù)進(jìn)行回滾,比如B系統(tǒng)本地回滾后,想辦法通知系統(tǒng)A也回滾;或者是發(fā)送報警由人工來手工回滾和補償

這個還是比較合適的,目前國內(nèi)互聯(lián)網(wǎng)公司大都是這么玩兒的,要不你舉用RocketMQ支持的,要不你就自己基于類似ActiveMQ?RabbitMQ?自己封裝一套類似的邏輯出來,總之思路就是這樣子的

(5)最大努力通知方案

image.png

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

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,反復(fù)N次,最后還是不行就放棄

3.6 如何設(shè)計一個高并發(fā)系統(tǒng)?

image.png

(1)系統(tǒng)拆分,將一個系統(tǒng)拆分為多個子系統(tǒng),用dubbo來搞。然后每個系統(tǒng)連一個數(shù)據(jù)庫,這樣本來就一個庫,現(xiàn)在多個數(shù)據(jù)庫,不也可以抗高并發(fā)么。

(2)緩存,必須得用緩存。大部分的高并發(fā)場景,都是讀多寫少,那你完全可以在數(shù)據(jù)庫和緩存里都寫一份,然后讀的時候大量走緩存不就得了。畢竟人家redis輕輕松松單機(jī)幾萬的并發(fā)啊。沒問題的。所以你可以考慮考慮你的項目里,那些承載主要請求的讀場景,怎么用緩存來抗高并發(fā)。

(3)MQ,必須得用MQ??赡苣氵€是會出現(xiàn)高并發(fā)寫的場景,比如說一個業(yè)務(wù)操作里要頻繁搞數(shù)據(jù)庫幾十次,增刪改增刪改,瘋了。那高并發(fā)絕對搞掛你的系統(tǒng),你要是用redis來承載寫那肯定不行,人家是緩存,數(shù)據(jù)隨時就被LRU了,數(shù)據(jù)格式還無比簡單,沒有事務(wù)支持。所以該用mysql還得用mysql啊。那你咋辦?用MQ吧,大量的寫請求灌入MQ里,排隊慢慢玩兒,后邊系統(tǒng)消費后慢慢寫,控制在mysql承載范圍之內(nèi)。所以你得考慮考慮你的項目里,那些承載復(fù)雜寫業(yè)務(wù)邏輯的場景里,如何用MQ來異步寫,提升并發(fā)性。MQ單機(jī)抗幾萬并發(fā)也是ok的,這個之前還特意說過。

(4)分庫分表,可能到了最后數(shù)據(jù)庫層面還是免不了抗高并發(fā)的要求,好吧,那么就將一個數(shù)據(jù)庫拆分為多個庫,多個庫來抗更高的并發(fā);然后將一個表拆分為多個表,每個表的數(shù)據(jù)量保持少一點,提高sql跑的性能。

(5)讀寫分離,這個就是說大部分時候數(shù)據(jù)庫可能也是讀多寫少,沒必要所有請求都集中在一個庫上吧,可以搞個主從架構(gòu),主庫寫入,從庫讀取,搞一個讀寫分離。讀流量太多的時候,還可以加更多的從庫。

(6)Elasticsearch,可以考慮用es。es是分布式的,可以隨便擴(kuò)容,分布式天然就可以支撐高并發(fā),因為動不動就可以擴(kuò)容加機(jī)器來抗更高的并發(fā)。那么一些比較簡單的查詢、統(tǒng)計類的操作,可以考慮用es來承載,還有一些全文搜索類的操作,也可以考慮用es來承載。

上面的6點,基本就是高并發(fā)系統(tǒng)肯定要干的一些事兒,大家可以仔細(xì)結(jié)合之前講過的知識考慮一下,到時候你可以系統(tǒng)的把這塊闡述一下,然后每個部分要注意哪些問題,之前都講過了,你都可以闡述闡述,表明你對這塊是有點積累的。

3.7 如何進(jìn)行系統(tǒng)拆分?

系統(tǒng)拆分分布式系統(tǒng),拆成多個服務(wù),拆成微服務(wù)的架構(gòu),拆很多輪的。上來一個架構(gòu)師第一輪就給拆好了,第一輪;團(tuán)隊繼續(xù)擴(kuò)大,拆好的某個服務(wù),剛開始是1個人維護(hù)1萬行代碼,后來業(yè)務(wù)系統(tǒng)越來越復(fù)雜,這個服務(wù)是10萬行代碼,5個人;第二輪,1個服務(wù) -> 5個服務(wù),每個服務(wù)2萬行代碼,每人負(fù)責(zé)一個服務(wù)

如果是多人維護(hù)一個服務(wù),<=3個人維護(hù)這個服務(wù);最理想的情況下,幾十個人,1個人負(fù)責(zé)1個或2~3個服務(wù);某個服務(wù)工作量變大了,代碼量越來越多,某個同學(xué),負(fù)責(zé)一個服務(wù),代碼量變成了10萬行了,他自己不堪重負(fù),他現(xiàn)在一個人拆開,5個服務(wù),1個人頂著,負(fù)責(zé)5個人,接著招人,2個人,給那個同學(xué)帶著,3個人負(fù)責(zé)5個服務(wù),其中2個人每個人負(fù)責(zé)2個服務(wù),1個人負(fù)責(zé)1個服務(wù)

我個人建議,一個服務(wù)的代碼不要太多,1萬行左右,兩三萬撐死了吧

大部分的系統(tǒng),是要進(jìn)行多輪拆分的,第一次拆分,可能就是將以前的多個模塊該拆分開來了,比如說將電商系統(tǒng)拆分成訂單系統(tǒng)、商品系統(tǒng)、采購系統(tǒng)、倉儲系統(tǒng)、用戶系統(tǒng),等等吧。

但是后面可能每個系統(tǒng)又變得越來越復(fù)雜了,比如說采購系統(tǒng)里面又分成了供應(yīng)商管理系統(tǒng)、采購單管理系統(tǒng),訂單系統(tǒng)又拆分成了購物車系統(tǒng)、價格系統(tǒng)、訂單管理系統(tǒng)

3.8 zk都有哪些使用場景?

(1)分布式協(xié)調(diào):

這個其實是zk很經(jīng)典的一個用法,簡單來說,就好比,你A系統(tǒng)發(fā)送個請求到mq,然后B消息消費之后處理了。那A系統(tǒng)如何知道B系統(tǒng)的處理結(jié)果?用zk就可以實現(xiàn)分布式系統(tǒng)之間的協(xié)調(diào)工作。A系統(tǒng)發(fā)送請求之后可以在zk上對某個節(jié)點的值注冊個監(jiān)聽器,一旦B系統(tǒng)處理完了就修改zk那個節(jié)點的值,A立馬就可以收到通知,完美解決。


image.png

(2)分布式鎖:

對某一個數(shù)據(jù)連續(xù)發(fā)出兩個修改操作,兩臺機(jī)器同時收到了請求,但是只能一臺機(jī)器先執(zhí)行另外一個機(jī)器再執(zhí)行。那么此時就可以使用zk分布式鎖,一個機(jī)器接收到了請求之后先獲取zk上的一把分布式鎖,就是可以去創(chuàng)建一個znode,接著執(zhí)行操作;然后另外一個機(jī)器也嘗試去創(chuàng)建那個znode,結(jié)果發(fā)現(xiàn)自己創(chuàng)建不了,因為被別人創(chuàng)建了。。。。那只能等著,等第一個機(jī)器執(zhí)行完了自己再執(zhí)行。


image.png

(3)元數(shù)據(jù)/配置信息管理:

zk可以用作很多系統(tǒng)的配置信息的管理,比如kafka、storm等等很多分布式系統(tǒng)都會選用zk來做一些元數(shù)據(jù)、配置信息的管理,包括dubbo注冊中心不也支持zk么


image.png

(4)HA高可用性:

這個應(yīng)該是很常見的,比如hadoop、hdfs、yarn等很多大數(shù)據(jù)系統(tǒng),都選擇基于zk來開發(fā)HA高可用機(jī)制,就是一個重要進(jìn)程一般會做主備兩個,主進(jìn)程掛了立馬通過zk感知到切換到備用進(jìn)程


image.png

3.9 使用redis如何設(shè)計分布式鎖?使用zk來設(shè)計分布式鎖可以嗎?這兩種分布式鎖的實現(xiàn)方式哪種效率比較高?

(1)redis分布式鎖

官方叫做RedLock算法,是redis官方支持的分布式鎖算法。

這個分布式鎖有3個重要的考量點,互斥(只能有一個客戶端獲取鎖),不能死鎖,容錯(大部分redis節(jié)點或者這個鎖就可以加可以釋放)

第一個最普通的實現(xiàn)方式,如果就是在redis里創(chuàng)建一個key算加鎖

image.png

SET my:lock 隨機(jī)值 NX PX 30000
這個命令就ok,這個的NX的意思就是只有key不存在的時候才會設(shè)置成功,PX 30000的意思是30秒后鎖自動釋放。別人創(chuàng)建的時候如果發(fā)現(xiàn)已經(jīng)有了就不能加鎖了。

釋放鎖就是刪除key,但是一般可以用lua腳本刪除,判斷value一樣才刪除:

關(guān)于redis如何執(zhí)行l(wèi)ua腳本,自行百度

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

為啥要用隨機(jī)值呢?因為如果某個客戶端獲取到了鎖,但是阻塞了很長時間才執(zhí)行完,此時可能已經(jīng)自動釋放鎖了,此時可能別的客戶端已經(jīng)獲取到了這個鎖,要是你這個時候直接刪除key的話會有問題,所以得用隨機(jī)值加上面的lua腳本來釋放鎖。

但是這樣是肯定不行的。因為如果是普通的redis單實例,那就是單點故障?;蛘呤莚edis普通主從,那redis主從異步復(fù)制,如果主節(jié)點掛了,key還沒同步到從節(jié)點,此時從節(jié)點切換為主節(jié)點,別人就會拿到鎖。

第二個問題,RedLock算法

image.png

這個場景是假設(shè)有一個redis cluster,有5個redis master實例。然后執(zhí)行如下步驟獲取一把鎖:

1)獲取當(dāng)前時間戳,單位是毫秒
2)跟上面類似,輪流嘗試在每個master節(jié)點上創(chuàng)建鎖,過期時間較短,一般就幾十毫秒
3)嘗試在大多數(shù)節(jié)點上建立一個鎖,比如5個節(jié)點就要求是3個節(jié)點(n / 2 +1)
4)客戶端計算建立好鎖的時間,如果建立鎖的時間小于超時時間,就算建立成功了
5)要是鎖建立失敗了,那么就依次刪除這個鎖
6)只要別人建立了一把分布式鎖,你就得不斷輪詢?nèi)L試獲取鎖

(2)zk分布式鎖

image.png

zk分布式鎖,其實可以做的比較簡單,就是某個節(jié)點嘗試創(chuàng)建臨時znode,此時創(chuàng)建成功了就獲取了這個鎖;這個時候別的客戶端來創(chuàng)建鎖會失敗,只要注冊個監(jiān)聽器監(jiān)聽這個鎖。釋放鎖就是刪除這個znode,一旦釋放掉就會通知客戶端,然后有一個等待著的客戶端就可以再次重新加鎖。

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

redis分布式鎖,其實需要自己不斷去嘗試獲取鎖,比較消耗性能

zk分布式鎖,獲取不到鎖,注冊個監(jiān)聽器即可,不需要不斷主動嘗試獲取鎖,性能開銷較小

另外一點就是,如果是redis獲取鎖的那個客戶端bug了或者掛了,那么只能等待超時時間之后才能釋放鎖;而zk的話,因為創(chuàng)建的是臨時znode,只要客戶端掛了,znode就沒了,此時就自動釋放鎖

redis分布式鎖大家每發(fā)現(xiàn)好麻煩嗎?遍歷上鎖,計算時間等等。。。zk的分布式鎖語義清晰實現(xiàn)簡單

所以先不分析太多的東西,就說這兩點,我個人實踐認(rèn)為zk的分布式鎖比redis的分布式鎖牢靠、而且模型簡單易用

3.10 hystrix

3.10.1 簡介

1. hystrix是什么?

netflix(國外最大的類似于,愛奇藝,優(yōu)酷)視頻網(wǎng)站,五六年前,也是,感覺自己的系統(tǒng),整個網(wǎng)站,經(jīng)常出故障,可用性不太高.

hystrix,框架,提供了高可用相關(guān)的各種各樣的功能,然后確保說在hystrix的保護(hù)下,整個系統(tǒng)可以長期處于高可用的狀態(tài),100%,99.99999%

最理想的狀況下,軟件的故障,就不應(yīng)該說導(dǎo)致整個系統(tǒng)的崩潰,服務(wù)器硬件的一些故障,服務(wù)的冗余


image.png

唯一有可能導(dǎo)致系統(tǒng)徹底崩潰,就是類似于之前,支付寶的那個事故,工人施工,挖斷了電纜,導(dǎo)致幾個機(jī)房都停電

2. 高可用系統(tǒng)架構(gòu)

資源隔離、限流、熔斷、降級、運維監(jiān)控

  • 資源隔離:讓你的系統(tǒng)里,某一塊東西,在故障的情況下,不會耗盡系統(tǒng)所有的資源,比如線程資源

    我實際的項目中的一個case,有一塊東西,是要用多線程做一些事情,小伙伴做項目的時候,沒有太留神,資源隔離,那塊代碼,在遇到一些故障的情況下,每個線程在跑的時候,因為那個bug,直接就死循環(huán)了,導(dǎo)致那塊東西啟動了大量的線程,每個線程都死循環(huán).最終導(dǎo)致我的系統(tǒng)資源耗盡,崩潰,不工作,不可用,廢掉了

資源隔離,那一塊代碼,最多最多就是用掉10個線程,不能再多了,就廢掉了,限定好的一些資源

  • 限流:高并發(fā)的流量涌入進(jìn)來,比如說突然間一秒鐘100萬QPS,廢掉了,10萬QPS進(jìn)入系統(tǒng),其他90萬QPS被拒絕了

  • 熔斷:系統(tǒng)后端的一些依賴,出了一些故障,比如說mysql掛掉了,每次請求都是報錯的,熔斷了,后續(xù)的請求過來直接不接收了,拒絕訪問,10分鐘之后再嘗試去看看mysql恢復(fù)沒有

  • 降級:mysql掛了,系統(tǒng)發(fā)現(xiàn)了,自動降級,從內(nèi)存里存的少量數(shù)據(jù)中,去提取一些數(shù)據(jù)出來

  • 運維監(jiān)控:監(jiān)控+報警+優(yōu)化,各種異常的情況,有問題就及時報警,優(yōu)化一些系統(tǒng)的配置和參數(shù),或者代碼

3. Hystrix要解決的問題是什么

在復(fù)雜的分布式系統(tǒng)架構(gòu)中,每個服務(wù)都有很多的依賴服務(wù),而每個依賴服務(wù)都可能會故障。如果服務(wù)沒有和自己的依賴服務(wù)進(jìn)行隔離,那么可能某一個依賴服務(wù)的故障就會拖垮當(dāng)前這個服務(wù)

舉例來說,某個服務(wù)有30個依賴服務(wù),每個依賴服務(wù)的可用性非常高,已經(jīng)達(dá)到了99.99%的高可用性。那么該服務(wù)的可用性就是99.99%的30次方,也就是99.7%的可用性

99.7%的可用性就意味著3%的請求可能會失敗,因為3%的時間內(nèi)系統(tǒng)可能出現(xiàn)了故障不可用了。對于1億次訪問來說,3%的請求失敗,也就意味著300萬次請求會失敗,也意味著每個月有2個小時的時間系統(tǒng)是不可用的。在真實生產(chǎn)環(huán)境中,可能更加糟糕

畫圖分析說,當(dāng)某一個依賴服務(wù)出現(xiàn)了調(diào)用延遲或者調(diào)用失敗時,為什么會拖垮當(dāng)前這個服務(wù)?以及在分布式系統(tǒng)中,故障是如何快速蔓延的?


image.png

4. Hystrix是如何實現(xiàn)它的目標(biāo)的?

(1)通過HystrixCommand或者HystrixObservableCommand來封裝對外部依賴的訪問請求,這個訪問請求一般會運行在獨立的線程中,資源隔離
(2)對于超出我們設(shè)定閾值的服務(wù)調(diào)用,直接進(jìn)行超時,不允許其耗費過長時間阻塞住。這個超時時間默認(rèn)是99.5%的訪問時間,但是一般我們可以自己設(shè)置一下
(3)為每一個依賴服務(wù)維護(hù)一個獨立的線程池,或者是semaphore,當(dāng)線程池已滿時,直接拒絕對這個服務(wù)的調(diào)用
(4)對依賴服務(wù)的調(diào)用的成功次數(shù),失敗次數(shù),拒絕次數(shù),超時次數(shù),進(jìn)行統(tǒng)計
(5)如果對一個依賴服務(wù)的調(diào)用失敗次數(shù)超過了一定的閾值,自動進(jìn)行熔斷,在一定時間內(nèi)對該服務(wù)的調(diào)用直接降級,一段時間后再自動嘗試恢復(fù)
(6)當(dāng)一個服務(wù)調(diào)用出現(xiàn)失敗,被拒絕,超時,短路等異常情況時,自動調(diào)用fallback降級機(jī)制
(7)對屬性和配置的修改提供近實時的支持

畫圖分析,對依賴進(jìn)行資源隔離后,如何避免依賴服務(wù)調(diào)用延遲或失敗導(dǎo)致當(dāng)前服務(wù)的故障


image.png

3.10.2 一個Demo

1. pom.xml

<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-core</artifactId>
    <version>1.5.12</version>
</dependency>

2. 將商品服務(wù)接口調(diào)用的邏輯進(jìn)行封裝

hystrix進(jìn)行資源隔離,其實是提供了一個抽象,叫做command,就是說,你如果要把對某一個依賴服務(wù)的所有調(diào)用請求,全部隔離在同一份資源池內(nèi)。對這個依賴服務(wù)的所有調(diào)用請求,全部走這個資源池內(nèi)的資源,不會去用其他的資源了,這個就叫做資源隔離

hystrix最最基本的資源隔離的技術(shù),線程池隔離技術(shù)

所以哪怕是對這個依賴服務(wù),商品服務(wù),現(xiàn)在同時發(fā)起的調(diào)用量已經(jīng)到了1000了,但是線程池內(nèi)就10個線程,最多就只會用這10個線程去執(zhí)行。不會說,對商品服務(wù)的請求,因為接口調(diào)用延遲,將tomcat內(nèi)部所有的線程資源全部耗盡,不會出現(xiàn)了

  public class CommandHelloWorld extends HystrixCommand<String> {
    private final String name;

    public CommandHelloWorld(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.name = name;
    }

    @Override
    protected String run() {
        return "Hello " + name + "!";
    }
}

不讓超出這個量的請求去執(zhí)行了,保護(hù)說,不要因為某一個依賴服務(wù)的故障,導(dǎo)致耗盡了緩存服務(wù)中的所有的線程資源去執(zhí)行

3. 開發(fā)一個支持批量商品變更的接口

HystrixCommand:是用來獲取一條數(shù)據(jù)的
HystrixObservableCommand:是設(shè)計用來獲取多條數(shù)據(jù)的

public class ObservableCommandHelloWorld extends HystrixObservableCommand<String> {

    private final String name;

    public ObservableCommandHelloWorld(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.name = name;
    }

    @Override
    protected Observable<String> construct() {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> observer) {
                try {
                    if (!observer.isUnsubscribed()) {
                        observer.onNext("Hello " + name + "!");
                        observer.onNext("Hi " + name + "!");
                        observer.onCompleted();
                    }
                } catch (Exception e) {
                    observer.onError(e);
                }
            }
         } ).subscribeOn(Schedulers.io());
    }
}

4. command的四種調(diào)用方式

  • 同步:
    new CommandHelloWorld("World").execute()
    new ObservableCommandHelloWorld("World").toBlocking().toFuture().get()

如果你認(rèn)為observable command只會返回一條數(shù)據(jù),那么可以調(diào)用上面的模式,去同步執(zhí)行,返回一條數(shù)據(jù)

  • 異步:
    new CommandHelloWorld("World").queue()
    new ObservableCommandHelloWorld("World").toBlocking().toFuture()

對command調(diào)用queue(),僅僅將command放入線程池的一個等待隊列,就立即返回,拿到一個Future對象,后面可以做一些其他的事情,然后過一段時間對future調(diào)用get()方法獲取數(shù)據(jù)

// observe():hot,已經(jīng)執(zhí)行過了
// toObservable(): cold,還沒執(zhí)行過

Observable<String> fWorld = new CommandHelloWorld("World").observe();

assertEquals("Hello World!", fWorld.toBlocking().single());

fWorld.subscribe(new Observer<String>() {

    @Override
    public void onCompleted() {

    }

    @Override
    public void onError(Throwable e) {
        e.printStackTrace();
    }

    @Override
    public void onNext(String v) {
        System.out.println("onNext: " + v);
    }

});

Observable<String> fWorld = new ObservableCommandHelloWorld("World").toObservable();

assertEquals("Hello World!", fWorld.toBlocking().single());

fWorld.subscribe(new Observer<String>() {

    @Override
    public void onCompleted() {

    }

    @Override
    public void onError(Throwable e) {
        e.printStackTrace();
    }

    @Override
    public void onNext(String v) {
        System.out.println("onNext: " + v);
    }

});
image.png

3.10.3 Demo中利用hystrix第二種隔離技術(shù)semaphore

1. 線程池隔離技術(shù)與信號量隔離技術(shù)的區(qū)別

hystrix的資源隔離,兩種技術(shù):

  • 線程池的資源隔離
  • 信號量的資源隔離,信號量,semaphore

信號量跟線程池,兩種資源隔離的技術(shù),區(qū)別到底在哪兒呢?


2. 線程池隔離技術(shù)和信號量隔離技術(shù),分別在什么樣的場景下去使用呢??

線程池:適合絕大多數(shù)的場景,99%的,線程池,對依賴服務(wù)的網(wǎng)絡(luò)請求的調(diào)用和訪問,timeout這種問題

信號量:適合,你的訪問不是對外部依賴的訪問,而是對內(nèi)部的一些比較復(fù)雜的業(yè)務(wù)邏輯的訪問,但是像這種訪問,系統(tǒng)內(nèi)部的代碼,其實不涉及任何的網(wǎng)絡(luò)請求,那么只要做信號量的普通限流就可以了,因為不需要去捕獲timeout類似的問題,算法+數(shù)據(jù)結(jié)構(gòu)的效率不是太高,并發(fā)量突然太高,因為這里稍微耗時一些,導(dǎo)致很多線程卡在這里的話,不太好,所以進(jìn)行一個基本的資源隔離和訪問,避免內(nèi)部復(fù)雜的低效率的代碼,導(dǎo)致大量的線程被hang住

3. 采用信號量技術(shù)對地理位置獲取邏輯進(jìn)行資源隔離與限流

super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
        .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
               .withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)));

3.10.4 hystrix兩種隔離技術(shù)細(xì)粒度控制的參數(shù)設(shè)置

1. execution.isolation.strategy

指定了HystrixCommand.run()的資源隔離策略,THREAD或者SEMAPHORE,一種是基于線程池,一種是信號量

  • 線程池機(jī)制,每個command運行在一個線程中,限流是通過線程池的大小來控制的

  • 信號量機(jī)制,command是運行在調(diào)用線程中,但是通過信號量的容量來進(jìn)行限流

如何在線程池和信號量之間做選擇?默認(rèn)的策略就是線程池

線程池其實最大的好處就是對于網(wǎng)絡(luò)訪問請求,如果有超時的話,可以避免調(diào)用線程阻塞住

而使用信號量的場景,通常是針對超大并發(fā)量的場景下,每個服務(wù)實例每秒都幾百的QPS,那么此時你用線程池的話,線程一般不會太多,可能撐不住那么高的并發(fā),如果要撐住,可能要耗費大量的線程資源,那么就是用信號量,來進(jìn)行限流保護(hù)
一般用信號量常見于那種基于純內(nèi)存的一些業(yè)務(wù)邏輯服務(wù),而不涉及到任何網(wǎng)絡(luò)訪問請求

netflix有100+的command運行在40+的線程池中,只有少數(shù)command是不運行在線程池中的,就是從純內(nèi)存中獲取一些元數(shù)據(jù),或者是對多個command包裝起來的facacde command,是用信號量限流的

// to use thread isolation
HystrixCommandProperties.Setter()
   .withExecutionIsolationStrategy(ExecutionIsolationStrategy.THREAD)
// to use semaphore isolation
HystrixCommandProperties.Setter()
   .withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)

2. command名稱和command組

線程池隔離,依賴服務(wù)->接口->線程池,如何來劃分,你的每個command,都可以設(shè)置一個自己的名稱,同時可以設(shè)置一個自己的組。

private static final Setter cachedSetter = 
    Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
        .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"));    

public CommandHelloWorld(String name) {
    super(cachedSetter);
    this.name = name;
}

command group,是一個非常重要的概念,默認(rèn)情況下,因為就是通過command group來定義一個線程池的,而且還會通過command group來聚合一些監(jiān)控和報警信息

同一個command group中的請求,都會進(jìn)入同一個線程池中

3. command線程池

threadpool key代表了一個HystrixThreadPool,用來進(jìn)行統(tǒng)一監(jiān)控,統(tǒng)計,緩存

默認(rèn)的threadpool key就是command group名稱

每個command都會跟它的threadpool key對應(yīng)的thread pool綁定在一起

如果不想直接用command group,也可以手動設(shè)置thread pool name

public CommandHelloWorld(String name) {
    super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
            .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
            .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")));
    this.name = name;
}

command threadpool -> command group -> command key

command key,代表了一類command,一般來說,代表了底層的依賴服務(wù)的一個接口

command group,代表了某一個底層的依賴服務(wù),合理,一個依賴服務(wù)可能會暴露出來多個接口,每個接口就是一個command key

command group,在邏輯上去組織起來一堆command key的調(diào)用,統(tǒng)計信息,成功次數(shù),timeout超時次數(shù),失敗次數(shù),可以看到某一個服務(wù)整體的一些訪問情況

command group,一般來說,推薦是根據(jù)一個服務(wù)去劃分出一個線程池,command key默認(rèn)都是屬于同一個線程池的

比如說你以一個服務(wù)為粒度,估算出來這個服務(wù)每秒的所有接口加起來的整體QPS在100左右

你調(diào)用那個服務(wù)的當(dāng)前服務(wù),部署了10個服務(wù)實例,每個服務(wù)實例上,其實用這個command group對應(yīng)這個服務(wù),給一個線程池,量大概在10個左右,就可以了,你對整個服務(wù)的整體的訪問QPS大概在每秒100左右

一般來說,command group是用來在邏輯上組合一堆command的

舉個例子,對于一個服務(wù)中的某個功能模塊來說,希望將這個功能模塊內(nèi)的所有command放在一個group中,那么在監(jiān)控和報警的時候可以放一起看

command group,對應(yīng)了一個服務(wù),但是這個服務(wù)暴露出來的幾個接口,訪問量很不一樣,差異非常之大

你可能就希望在這個服務(wù)command group內(nèi)部,包含的對應(yīng)多個接口的command key,做一些細(xì)粒度的資源隔離

對同一個服務(wù)的不同接口,都使用不同的線程池

command key -> command group

command key -> 自己的threadpool key

邏輯上來說,多個command key屬于一個command group,在做統(tǒng)計的時候,會放在一起統(tǒng)計

每個command key有自己的線程池,每個接口有自己的線程池,去做資源隔離和限流

但是對于thread pool資源隔離來說,可能是希望能夠拆分的更加一致一些,比如在一個功能模塊內(nèi),對不同的請求可以使用不同的thread pool

command group一般來說,可以是對應(yīng)一個服務(wù),多個command key對應(yīng)這個服務(wù)的多個接口,多個接口的調(diào)用共享同一個線程池

如果說你的command key,要用自己的線程池,可以定義自己的threadpool key,就ok了

4. coreSize

設(shè)置線程池的大小,默認(rèn)是10

HystrixThreadPoolProperties.Setter()
   .withCoreSize(int value)

一般來說,用這個默認(rèn)的10個線程大小就夠了

5. queueSizeRejectionThreshold

image.png

控制queue滿后reject的threshold,因為maxQueueSize不允許熱修改,因此提供這個參數(shù)可以熱修改,控制隊列的最大大小

HystrixCommand在提交到線程池之前,其實會先進(jìn)入一個隊列中,這個隊列滿了之后,才會reject

默認(rèn)值是5

HystrixThreadPoolProperties.Setter()
   .withQueueSizeRejectionThreshold(int value)

6. execution.isolation.semaphore.maxConcurrentRequests

設(shè)置使用SEMAPHORE隔離策略的時候,允許訪問的最大并發(fā)量,超過這個最大并發(fā)量,請求直接被reject

這個并發(fā)量的設(shè)置,跟線程池大小的設(shè)置,應(yīng)該是類似的,但是基于信號量的話,性能會好很多,而且hystrix框架本身的開銷會小很多

默認(rèn)值是10,設(shè)置的小一些,否則因為信號量是基于調(diào)用線程去執(zhí)行command的,而且不能從timeout中抽離,因此一旦設(shè)置的太大,而且有延時發(fā)生,可能瞬間導(dǎo)致tomcat本身的線程資源本占滿

HystrixCommandProperties.Setter()
   .withExecutionIsolationSemaphoreMaxConcurrentRequests(int value)

3.10.5 hystrix執(zhí)行時的8大流程以及內(nèi)部原理

image.png

1. 構(gòu)建一個HystrixCommand或者HystrixObservableCommand

一個HystrixCommand或一個HystrixObservableCommand對象,代表了對某個依賴服務(wù)發(fā)起的一次請求或者調(diào)用

構(gòu)造的時候,可以在構(gòu)造函數(shù)中傳入任何需要的參數(shù)

HystrixCommand主要用于僅僅會返回一個結(jié)果的調(diào)用
HystrixObservableCommand主要用于可能會返回多條結(jié)果的調(diào)用

HystrixCommand command = new HystrixCommand(arg1, arg2);
HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);

2. 調(diào)用command的執(zhí)行方法

執(zhí)行Command就可以發(fā)起一次對依賴服務(wù)的調(diào)用

要執(zhí)行Command,需要在4個方法中選擇其中的一個:execute(),queue(),observe(),toObservable()

其中execute()和queue()僅僅對HystrixCommand適用

execute():調(diào)用后直接block住,屬于同步調(diào)用,直到依賴服務(wù)返回單條結(jié)果,或者拋出異常
queue():返回一個Future,屬于異步調(diào)用,后面可以通過Future獲取單條結(jié)果
observe():訂閱一個Observable對象,Observable代表的是依賴服務(wù)返回的結(jié)果,獲取到一個那個代表結(jié)果的Observable對象的拷貝對象
toObservable():返回一個Observable對象,如果我們訂閱這個對象,就會執(zhí)行command并且獲取返回結(jié)果

K             value   = command.execute();
Future<K>     fValue  = command.queue();
Observable<K> ohValue = command.observe();         
Observable<K> ocValue = command.toObservable();    

execute()實際上會調(diào)用queue().get().queue(),接著會調(diào)用toObservable().toBlocking().toFuture()

也就是說,無論是哪種執(zhí)行command的方式,最終都是依賴toObservable()去執(zhí)行的

3. 檢查是否開啟緩存

從這一步開始,進(jìn)入我們的底層的運行原理啦,了解hysrix的一些更加高級的功能和特性

如果這個command開啟了請求緩存,request cache,而且這個調(diào)用的結(jié)果在緩存中存在,那么直接從緩存中返回結(jié)果

4. 檢查是否開啟了短路器

檢查這個command對應(yīng)的依賴服務(wù)是否開啟了短路器

如果斷路器被打開了,那么hystrix就不會執(zhí)行這個command,而是直接去執(zhí)行fallback降級機(jī)制

5. 檢查線程池/隊列/semaphore是否已經(jīng)滿了

如果command對應(yīng)的線程池/隊列/semaphore已經(jīng)滿了,那么也不會執(zhí)行command,而是直接去調(diào)用fallback降級機(jī)制

6. 執(zhí)行command

調(diào)用HystrixObservableCommand.construct()或HystrixCommand.run()來實際執(zhí)行這個command

HystrixCommand.run()是返回一個單條結(jié)果,或者拋出一個異常
HystrixObservableCommand.construct()是返回一個Observable對象,可以獲取多條結(jié)果

如果HystrixCommand.run()或HystrixObservableCommand.construct()的執(zhí)行,超過了timeout時長的話,那么command所在的線程就會拋出一個TimeoutException

如果timeout了,也會去執(zhí)行fallback降級機(jī)制,而且就不會管run()或construct()返回的值了

這里要注意的一點是,我們是不可能終止掉一個調(diào)用嚴(yán)重延遲的依賴服務(wù)的線程的,只能說給你拋出來一個TimeoutException,但是還是可能會因為嚴(yán)重延遲的調(diào)用線程占滿整個線程池的

即使這個時候新來的流量都被限流了。。。

如果沒有timeout的話,那么就會拿到一些調(diào)用依賴服務(wù)獲取到的結(jié)果,然后hystrix會做一些logging記錄和metric統(tǒng)計

7. 短路健康檢查

Hystrix會將每一個依賴服務(wù)的調(diào)用成功,失敗,拒絕,超時,等事件,都會發(fā)送給circuit breaker斷路器

短路器就會對調(diào)用成功/失敗/拒絕/超時等事件的次數(shù)進(jìn)行統(tǒng)計

短路器會根據(jù)這些統(tǒng)計次數(shù)來決定,是否要進(jìn)行短路,如果打開了短路器,那么在一段時間內(nèi)就會直接短路,然后如果在之后第一次檢查發(fā)現(xiàn)調(diào)用成功了,就關(guān)閉斷路器

8. 調(diào)用fallback降級機(jī)制

在以下幾種情況中,hystrix會調(diào)用fallback降級機(jī)制:run()或construct()拋出一個異常,短路器打開,線程池/隊列/semaphore滿了,command執(zhí)行超時了

一般在降級機(jī)制中,都建議給出一些默認(rèn)的返回值,比如靜態(tài)的一些代碼邏輯,或者從內(nèi)存中的緩存中提取一些數(shù)據(jù),盡量在這里不要再進(jìn)行網(wǎng)絡(luò)請求了

即使在降級中,一定要進(jìn)行網(wǎng)絡(luò)調(diào)用,也應(yīng)該將那個調(diào)用放在一個HystrixCommand中,進(jìn)行隔離

在HystrixCommand中,上線getFallback()方法,可以提供降級機(jī)制

在HystirxObservableCommand中,實現(xiàn)一個resumeWithFallback()方法,返回一個Observable對象,可以提供降級結(jié)果

如果fallback返回了結(jié)果,那么hystrix就會返回這個結(jié)果

對于HystrixCommand,會返回一個Observable對象,其中會發(fā)返回對應(yīng)的結(jié)果
對于HystrixObservableCommand,會返回一個原始的Observable對象

如果沒有實現(xiàn)fallback,或者是fallback拋出了異常,Hystrix會返回一個Observable,但是不會返回任何數(shù)據(jù)

不同的command執(zhí)行方式,其fallback為空或者異常時的返回結(jié)果不同

對于execute(),直接拋出異常
對于queue(),返回一個Future,調(diào)用get()時拋出異常
對于observe(),返回一個Observable對象,但是調(diào)用subscribe()方法訂閱它時,理解拋出調(diào)用者的onError方法
對于toObservable(),返回一個Observable對象,但是調(diào)用subscribe()方法訂閱它時,理解拋出調(diào)用者的onError方法

3.10.6 請求緩存

  • 創(chuàng)建command,2種command類型
  • 執(zhí)行command,4種執(zhí)行方式
  • 查找是否開啟了request cache,是否有請求緩存,如果有緩存,直接取用緩存,返回結(jié)果

首先,有一個概念,叫做reqeust context,請求上下文,一般來說,在一個web應(yīng)用中,hystrix

我們會在一個filter里面,對每一個請求都施加一個請求上下文,就是說,tomcat容器內(nèi),每一次請求,就是一次請求上下文

然后在這次請求上下文中,我們會去執(zhí)行N多代碼,調(diào)用N多依賴服務(wù),有的依賴服務(wù)可能還會調(diào)用好幾次

在一次請求上下文中,如果有多個command,參數(shù)都是一樣的,調(diào)用的接口也是一樣的,其實結(jié)果可以認(rèn)為也是一樣的

那么這個時候,我們就可以讓第一次command執(zhí)行,返回的結(jié)果,被緩存在內(nèi)存中,然后這個請求上下文中,后續(xù)的其他對這個依賴的調(diào)用全部從內(nèi)存中取用緩存結(jié)果就可以了

不用在一次請求上下文中反復(fù)多次的執(zhí)行一樣的command,提升整個請求的性能

HystrixCommand和HystrixObservableCommand都可以指定一個緩存key,然后hystrix會自動進(jìn)行緩存,接著在同一個request context內(nèi),再次訪問的時候,就會直接取用緩存

用請求緩存,可以避免重復(fù)執(zhí)行網(wǎng)絡(luò)請求

多次調(diào)用一個command,那么只會執(zhí)行一次,后面都是直接取緩存

對于請求緩存(request caching),請求合并(request collapsing),請求日志(request log),等等技術(shù),都必須自己管理HystrixReuqestContext的聲明周期

在一個請求執(zhí)行之前,都必須先初始化一個request context

HystrixRequestContext context = HystrixRequestContext.initializeContext();

然后在請求結(jié)束之后,需要關(guān)閉request context

context.shutdown();

一般來說,在java web來的應(yīng)用中,都是通過filter過濾器來實現(xiàn)的


image.png

3.10.7 fallback降級

  • 創(chuàng)建command
  • 執(zhí)行command
  • request cache
  • 短路器,如果打開了,fallback降級機(jī)制

1. fallback降級機(jī)制

hystrix調(diào)用各種接口,或者訪問外部依賴,mysql,redis,zookeeper,kafka,等等,如果出現(xiàn)了任何異常的情況

比如說報錯了,訪問mysql報錯,redis報錯,zookeeper報錯,kafka報錯,error

對每個外部依賴,無論是服務(wù)接口,中間件,資源隔離,對外部依賴只能用一定量的資源去訪問,線程池/信號量,如果資源池已滿,reject

訪問外部依賴的時候,訪問時間過長,可能就會導(dǎo)致超時,報一個TimeoutException異常,timeout

上述三種情況,都是我們說的異常情況,對外部依賴的東西訪問的時候出現(xiàn)了異常,發(fā)送異常事件到短路器中去進(jìn)行統(tǒng)計

如果短路器發(fā)現(xiàn)異常事件的占比達(dá)到了一定的比例,直接開啟短路,circuit breaker

上述四種情況,都會去調(diào)用fallback降級機(jī)制

fallback,降級機(jī)制,你之前都是必須去調(diào)用外部的依賴接口,或者從mysql中去查詢數(shù)據(jù)的,但是為了避免說可能外部依賴會有故障

比如,你可以再內(nèi)存中維護(hù)一個ehcache,作為一個純內(nèi)存的基于LRU自動清理的緩存,數(shù)據(jù)也可以放入緩存內(nèi)

如果說外部依賴有異常,fallback這里,直接嘗試從ehcache中獲取數(shù)據(jù)

比如說,本來你是從mysql,redis,或者其他任何地方去獲取數(shù)據(jù)的,獲取調(diào)用其他服務(wù)的接口的,結(jié)果人家故障了,人家掛了,fallback,可以返回一個默認(rèn)值

兩種最經(jīng)典的降級機(jī)制:純內(nèi)存數(shù)據(jù),默認(rèn)值

run()拋出異常,超時,線程池或信號量滿了,或短路了,都會調(diào)用fallback機(jī)制

給大家舉個例子,比如說我們現(xiàn)在有個商品數(shù)據(jù),brandId,品牌,一般來說,假設(shè),正常的邏輯,拿到了一個商品數(shù)據(jù)以后,用brandId再調(diào)用一次請求,到其他的服務(wù)去獲取品牌的最新名稱

假如說,那個品牌服務(wù)掛掉了,那么我們可以嘗試本地內(nèi)存中,會保留一份時間比較過期的一份品牌數(shù)據(jù),有些品牌沒有,有些品牌的名稱過期了,Nike++,Nike

調(diào)用品牌服務(wù)失敗了,fallback降級就從本地內(nèi)存中獲取一份過期的數(shù)據(jù),先湊合著用著

public class CommandHelloFailure extends HystrixCommand<String> {

    private final String name;

    public CommandHelloFailure(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.name = name;
    }

    @Override
    protected String run() {
        throw new RuntimeException("this command always fails");
    }

    @Override
    protected String getFallback() {
        return "Hello Failure " + name + "!";
    }

}

@Test
public void testSynchronous() {
    assertEquals("Hello Failure World!", new CommandHelloFailure("World").execute());
}

HystrixObservableCommand,是實現(xiàn)resumeWithFallback方法

2. fallback.isolation.semaphore.maxConcurrentRequests

這個參數(shù)設(shè)置了HystrixCommand.getFallback()最大允許的并發(fā)請求數(shù)量,默認(rèn)值是10,也是通過semaphore信號量的機(jī)制去限流

如果超出了這個最大值,那么直接被reject

HystrixCommandProperties.Setter()
   .withFallbackIsolationSemaphoreMaxConcurrentRequests(int value)

3.10.8 短路器深入的工作原理

1. 如果經(jīng)過短路器的流量超過了一定的閾值,

HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()

舉個例子,可能看起來是這樣子的,要求在10s內(nèi),經(jīng)過短路器的流量必須達(dá)到20個;在10s內(nèi),經(jīng)過短路器的流量才10個,那么根本不會去判斷要不要短路

2. 如果斷路器統(tǒng)計到的異常調(diào)用的占比超過了一定的閾值,

HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()

如果達(dá)到了上面的要求,比如說在10s內(nèi),經(jīng)過短路器的流量(你,只要執(zhí)行一個command,這個請求就一定會經(jīng)過短路器),達(dá)到了30個;同時其中異常的訪問數(shù)量,占到了一定的比例,比如說60%的請求都是異常(報錯,timeout,reject),會開啟短路

3. 然后斷路器從close狀態(tài)轉(zhuǎn)換到open狀態(tài)

4. 斷路器打開的時候,所有經(jīng)過該斷路器的請求全部被短路,不調(diào)用后端服務(wù),直接走fallback降級

5. 經(jīng)過了一段時間之后,HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds(),會half-open,讓一條請求經(jīng)過短路器,看能不能正常調(diào)用。如果調(diào)用成功了,那么就自動恢復(fù),轉(zhuǎn)到close狀態(tài)

短路器,會自動恢復(fù)的,half-open,半開狀態(tài)

6. circuit breaker短路器的配置

(1)circuitBreaker.enabled

控制短路器是否允許工作,包括跟蹤依賴服務(wù)調(diào)用的健康狀況,以及對異常情況過多時是否允許觸發(fā)短路,默認(rèn)是true

HystrixCommandProperties.Setter()
.withCircuitBreakerEnabled(boolean value)

(2)circuitBreaker.requestVolumeThreshold

設(shè)置一個rolling window,滑動窗口中,最少要有多少個請求時,才觸發(fā)開啟短路

舉例來說,如果設(shè)置為20(默認(rèn)值),那么在一個10秒的滑動窗口內(nèi),如果只有19個請求,即使這19個請求都是異常的,也是不會觸發(fā)開啟短路器的

HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(int value)

(3)circuitBreaker.sleepWindowInMilliseconds

設(shè)置在短路之后,需要在多長時間內(nèi)直接reject請求,然后在這段時間之后,再重新導(dǎo)holf-open狀態(tài),嘗試允許請求通過以及自動恢復(fù),默認(rèn)值是5000毫秒

HystrixCommandProperties.Setter()
.withCircuitBreakerSleepWindowInMilliseconds(int value)

(4)circuitBreaker.errorThresholdPercentage

設(shè)置異常請求量的百分比,當(dāng)異常請求達(dá)到這個百分比時,就觸發(fā)打開短路器,默認(rèn)是50,也就是50%

HystrixCommandProperties.Setter()
.withCircuitBreakerErrorThresholdPercentage(int value)

(5)circuitBreaker.forceOpen

如果設(shè)置為true的話,直接強(qiáng)迫打開短路器,相當(dāng)于是手動短路了,手動降級,默認(rèn)false

HystrixCommandProperties.Setter()
.withCircuitBreakerForceOpen(boolean value)

(6)circuitBreaker.forceClosed

如果設(shè)置為ture的話,直接強(qiáng)迫關(guān)閉短路器,相當(dāng)于是手動停止短路了,手動升級,默認(rèn)false

HystrixCommandProperties.Setter()
.withCircuitBreakerForceClosed(boolean value

3.10.9 線程池或者信號量的容量是否已滿,reject,限流

  • command的創(chuàng)建和執(zhí)行:資源隔離
  • request cache:請求緩存
  • allback:優(yōu)雅降級
  • circuit breaker:短路器,快速熔斷(一旦后端服務(wù)故障,立刻熔斷,阻止對其的訪問)

把一個分布式系統(tǒng)中的某一個服務(wù),打造成一個高可用的服務(wù),需要資源隔離,優(yōu)雅降級,熔斷

  • 判斷,線程池或者信號量的容量是否已滿,reject,限流

限流,限制對后端的服務(wù)的訪問量,比如說你對mysql,redis,zookeeper,各種后端的中間件的資源,訪問,其實為了避免過大的流浪打死后端的服務(wù),線程池或者信號量,限流,來限制服務(wù)對后端的資源的訪問。

線程池隔離,學(xué)術(shù)名稱:bulkhead,艙壁隔離

接口限流實驗

假設(shè),一個線程池,大小是15個,隊列大小是10個,timeout時長設(shè)置的長一些,5s

模擬發(fā)送請求,然后寫死代碼,在command內(nèi)部做一個sleep,比如每次sleep 1s,10個請求發(fā)送過去以后,直接被hang死,線程池占滿

再發(fā)送請求,就會堵塞在緩沖隊列,queue,10個,20個,10個,后10個應(yīng)該就直接reject,fallback邏輯

15 + 10 = 25個請求,15在執(zhí)行,10個緩沖在隊列里了,剩下的流量全部被reject,限流,降級

withCoreSize:設(shè)置你的線程池的大小
withMaxQueueSize:設(shè)置的是你的等待隊列,緩沖隊列的大小
withQueueSizeRejectionThreshold:如果withMaxQueueSize<withQueueSizeRejectionThreshold,那么取的是withMaxQueueSize,反之,取得是withQueueSizeRejectionThreshold

線程池本身的大小,如果你不設(shè)置另外兩個queue相關(guān)的參數(shù),等待隊列是關(guān)閉的

queue大小,等待隊列的大小,timeout時長

先進(jìn)去線程池的是10個請求,然后有8個請求進(jìn)入等待隊列,線程池里有空閑,等待隊列中的請求如果還沒有timeout,那么就進(jìn)去線程池去執(zhí)行

10 + 8 = 18個請求之外,7個請求,直接會被reject掉,限流,fallback

withExecutionTimeoutInMilliseconds(20000):timeout也設(shè)置大一些,否則如果請求放等待隊列中時間太長了,直接就會timeout,等不到去線程池里執(zhí)行了
withFallbackIsolationSemaphoreMaxConcurrentRequests(30):fallback,sempahore限流,30個,避免太多的請求同時調(diào)用fallback被拒絕訪問.

3.10.10 超時timeout控制

如果你不對各種依賴接口的調(diào)用,做超時的控制,來給你的服務(wù)提供安全保護(hù)措施,那么很可能你的服務(wù)就被各種垃圾的依賴服務(wù)的性能給拖死了

大量的接口調(diào)用很慢,大量線程就卡死了,資源隔離,線程池的線程卡死了,超時的控制

(1)execution.isolation.thread.timeoutInMilliseconds

手動設(shè)置timeout時長,一個command運行超出這個時間,就被認(rèn)為是timeout,然后將hystrix command標(biāo)識為timeout,同時執(zhí)行fallback降級邏輯

默認(rèn)是1000,也就是1000毫秒

HystrixCommandProperties.Setter()
   .withExecutionTimeoutInMilliseconds(int value)

(2)execution.timeout.enabled

控制是否要打開timeout機(jī)制,默認(rèn)是true

HystrixCommandProperties.Setter()
   .withExecutionTimeoutEnabled(boolean value)

讓一個command執(zhí)行timeout,然后看是否會調(diào)用fallback降級

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容