目錄
1 消息隊列
2 緩存
3 分布式系統(tǒng)
4 Dubbo
5 數(shù)據(jù)庫參考:
· 中華石杉視頻
3 分布式系統(tǒng)
3.1 分布式系統(tǒng)是什么?
3.2 分布式服務(wù)接口的冪等性如何設(shè)計(比如不能重復(fù)扣款)?
所謂冪等性,就是說一個接口类浪,多次發(fā)起同一個請求,你這個接口得保證結(jié)果是準(zhǔn)確的鳍烁,比如不能多扣款载绿,不能多插入一條數(shù)據(jù),不能將統(tǒng)計值多加了1辙培。這就是冪等性蔑水,不給大家來學(xué)術(shù)性詞語了。
其實保證冪等性主要是三點:
(1)對于每個請求必須有一個唯一的標(biāo)識扬蕊,舉個例子:訂單支付請求搀别,肯定得包含訂單id,一個訂單id最多支付一次尾抑,對吧
(2)每次處理完請求之后歇父,必須有一個記錄標(biāo)識這個請求處理過了,比如說常見的方案是在mysql中記錄個狀態(tài)啥的
(3)每次接收請求需要進行判斷之前是否處理過的邏輯處理再愈,比如說榜苫,如果有一個訂單已經(jīng)支付了,就已經(jīng)有了一條支付流水翎冲,那么如果重復(fù)發(fā)送這個請求垂睬,則此時先插入支付流水,orderId已經(jīng)存在了抗悍,唯一鍵約束生效羔飞,報錯插入不進去的。然后你就不用再扣款了檐春。
上面只是給大家舉個例子,實際運作過程中么伯,你要結(jié)合自己的業(yè)務(wù)來疟暖,比如說用redis用orderId作為唯一鍵。只有成功插入這個支付流水田柔,才可以執(zhí)行實際的支付扣款俐巴。要求是支付一個訂單,必須插入一條支付流水硬爆,order_id建一個唯一鍵欣舵,unique key。所以你在支付一個訂單之前缀磕,先插入一條支付流水缘圈,order_id就已經(jīng)進去了劣光。你就可以寫一個標(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ù)接口請求的順序性如何保證戏仓?
(1)特定表示通過一致性哈希到同一臺機器 + 內(nèi)存隊列保存順序 + 同一線程讀取
(2)MQ保證順序,做成異步
(3)Zookeeper分布式鎖(開銷大)或順序節(jié)點
3.4 集群部署時的分布式session如何實現(xiàn)亡鼠?
其實方法很多赏殃,但是常見常用的是兩種:
(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ù)問題的闲孤?
在實際場景中谆级,只有1%,0.1%讼积,0.01%的業(yè)務(wù)肥照,如資金、交易勤众、訂單舆绎,我們會用分布式事務(wù)方案來保證,會員積分们颜、優(yōu)惠券吕朵、商品信息,其實不要這么搞了掌桩,不然是的系統(tǒng)設(shè)計過于復(fù)雜而不可靠。
(1)兩階段提交方案/XA方案
也叫做兩階段提交事務(wù)方案姑食,這個舉個例子波岛,比如說咱們公司里經(jīng)常tb是吧(就是團建,team building),然后一般會有個tb主席(就是負(fù)責(zé)組織團建的那個人)音半。
第一個階段则拷,一般tb主席會提前一周問一下團隊里的每個人贡蓖,說,大家伙煌茬,下周六我們?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方案
TCC的全稱是:Try、Confirm底循、Cancel巢株。
這個其實是用到了補償?shù)母拍睿譃榱巳齻€階段:
1)Try階段:這個階段說的是對各個服務(wù)的資源做檢測以及對資源進行鎖定或者預(yù)留
2)Confirm階段:這個階段說的是在各個服務(wù)中執(zhí)行實際的操作
3)Cancel階段:如果任何一個服務(wù)的業(yè)務(wù)方法執(zhí)行出錯熙涤,那么這里就需要進行補償阁苞,就是執(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í)行失敗,那么就需要回滾進行補償慌植,就是比如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ù)代碼很難維護。
(3)本地消息表
國外的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ā)場景咋辦呢姑隅?咋擴展呢?所以一般確實很少用
(4)可靠消息最終一致性方案
這個的意思倔撞,就是干脆不要用本地的消息表了讲仰,直接基于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ù)進行回滾惑艇,比如B系統(tǒng)本地回滾后,想辦法通知系統(tǒng)A也回滾拇泛;或者是發(fā)送報警由人工來手工回滾和補償
這個還是比較合適的滨巴,目前國內(nèi)互聯(lián)網(wǎng)公司大都是這么玩兒的,要不你舉用RocketMQ支持的俺叭,要不你就自己基于類似ActiveMQ恭取?RabbitMQ?自己封裝一套類似的邏輯出來熄守,總之思路就是這樣子的
(5)最大努力通知方案
這個方案的大致意思就是:
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)?
(1)系統(tǒng)拆分紊扬,將一個系統(tǒng)拆分為多個子系統(tǒng),用dubbo來搞唉擂。然后每個系統(tǒng)連一個數(shù)據(jù)庫餐屎,這樣本來就一個庫,現(xiàn)在多個數(shù)據(jù)庫玩祟,不也可以抗高并發(fā)么腹缩。
(2)緩存,必須得用緩存空扎。大部分的高并發(fā)場景藏鹊,都是讀多寫少,那你完全可以在數(shù)據(jù)庫和緩存里都寫一份转锈,然后讀的時候大量走緩存不就得了盘寡。畢竟人家redis輕輕松松單機幾萬的并發(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單機抗幾萬并發(fā)也是ok的笨农,這個之前還特意說過就缆。
(4)分庫分表,可能到了最后數(shù)據(jù)庫層面還是免不了抗高并發(fā)的要求谒亦,好吧竭宰,那么就將一個數(shù)據(jù)庫拆分為多個庫,多個庫來抗更高的并發(fā)份招;然后將一個表拆分為多個表切揭,每個表的數(shù)據(jù)量保持少一點,提高sql跑的性能锁摔。
(5)讀寫分離廓旬,這個就是說大部分時候數(shù)據(jù)庫可能也是讀多寫少,沒必要所有請求都集中在一個庫上吧谐腰,可以搞個主從架構(gòu)嗤谚,主庫寫入,從庫讀取怔蚌,搞一個讀寫分離巩步。讀流量太多的時候,還可以加更多的從庫桦踊。
(6)Elasticsearch椅野,可以考慮用es。es是分布式的籍胯,可以隨便擴容竟闪,分布式天然就可以支撐高并發(fā),因為動不動就可以擴容加機器來抗更高的并發(fā)杖狼。那么一些比較簡單的查詢炼蛤、統(tǒng)計類的操作,可以考慮用es來承載蝶涩,還有一些全文搜索類的操作理朋,也可以考慮用es來承載絮识。
上面的6點,基本就是高并發(fā)系統(tǒng)肯定要干的一些事兒嗽上,大家可以仔細(xì)結(jié)合之前講過的知識考慮一下次舌,到時候你可以系統(tǒng)的把這塊闡述一下,然后每個部分要注意哪些問題兽愤,之前都講過了彼念,你都可以闡述闡述,表明你對這塊是有點積累的浅萧。
3.7 如何進行系統(tǒng)拆分逐沙?
系統(tǒng)拆分分布式系統(tǒng),拆成多個服務(wù)洼畅,拆成微服務(wù)的架構(gòu)吩案,拆很多輪的。上來一個架構(gòu)師第一輪就給拆好了土思,第一輪;團隊繼續(xù)擴大忆嗜,拆好的某個服務(wù)己儒,剛開始是1個人維護1萬行代碼,后來業(yè)務(wù)系統(tǒng)越來越復(fù)雜捆毫,這個服務(wù)是10萬行代碼闪湾,5個人;第二輪绩卤,1個服務(wù) -> 5個服務(wù)途样,每個服務(wù)2萬行代碼,每人負(fù)責(zé)一個服務(wù)
如果是多人維護一個服務(wù)濒憋,<=3個人維護這個服務(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)浇衬,是要進行多輪拆分的,第一次拆分餐济,可能就是將以前的多個模塊該拆分開來了耘擂,比如說將電商系統(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立馬就可以收到通知,完美解決贤壁。
(2)分布式鎖:
對某一個數(shù)據(jù)連續(xù)發(fā)出兩個修改操作悼枢,兩臺機器同時收到了請求,但是只能一臺機器先執(zhí)行另外一個機器再執(zhí)行脾拆。那么此時就可以使用zk分布式鎖馒索,一個機器接收到了請求之后先獲取zk上的一把分布式鎖莹妒,就是可以去創(chuàng)建一個znode,接著執(zhí)行操作绰上;然后另外一個機器也嘗試去創(chuàng)建那個znode旨怠,結(jié)果發(fā)現(xiàn)自己創(chuàng)建不了,因為被別人創(chuàng)建了蜈块。鉴腻。。百揭。那只能等著爽哎,等第一個機器執(zhí)行完了自己再執(zhí)行。
(3)元數(shù)據(jù)/配置信息管理:
zk可以用作很多系統(tǒng)的配置信息的管理器一,比如kafka课锌、storm等等很多分布式系統(tǒng)都會選用zk來做一些元數(shù)據(jù)、配置信息的管理祈秕,包括dubbo注冊中心不也支持zk么
(4)HA高可用性:
這個應(yīng)該是很常見的渺贤,比如hadoop、hdfs请毛、yarn等很多大數(shù)據(jù)系統(tǒng)志鞍,都選擇基于zk來開發(fā)HA高可用機制,就是一個重要進程一般會做主備兩個获印,主進程掛了立馬通過zk感知到切換到備用進程
3.9 使用redis如何設(shè)計分布式鎖述雾?使用zk來設(shè)計分布式鎖可以嗎街州?這兩種分布式鎖的實現(xiàn)方式哪種效率比較高兼丰?
(1)redis分布式鎖
官方叫做RedLock算法,是redis官方支持的分布式鎖算法唆缴。
這個分布式鎖有3個重要的考量點鳍征,互斥(只能有一個客戶端獲取鎖),不能死鎖面徽,容錯(大部分redis節(jié)點或者這個鎖就可以加可以釋放)
第一個最普通的實現(xiàn)方式艳丛,如果就是在redis里創(chuàng)建一個key算加鎖
SET my:lock 隨機值 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
為啥要用隨機值呢铛嘱?因為如果某個客戶端獲取到了鎖暖释,但是阻塞了很長時間才執(zhí)行完袭厂,此時可能已經(jīng)自動釋放鎖了,此時可能別的客戶端已經(jīng)獲取到了這個鎖球匕,要是你這個時候直接刪除key的話會有問題纹磺,所以得用隨機值加上面的lua腳本來釋放鎖。
但是這樣是肯定不行的亮曹。因為如果是普通的redis單實例橄杨,那就是單點故障∏溃或者是redis普通主從讥珍,那redis主從異步復(fù)制,如果主節(jié)點掛了窄瘟,key還沒同步到從節(jié)點衷佃,此時從節(jié)點切換為主節(jié)點,別人就會拿到鎖蹄葱。
第二個問題氏义,RedLock算法
這個場景是假設(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分布式鎖
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的保護下憔晒,整個系統(tǒng)可以長期處于高可用的狀態(tài),100%蔑舞,99.99999%
最理想的狀況下拒担,軟件的故障,就不應(yīng)該說導(dǎo)致整個系統(tǒng)的崩潰攻询,服務(wù)器硬件的一些故障从撼,服務(wù)的冗余
唯一有可能導(dǎo)致系統(tǒng)徹底崩潰,就是類似于之前,支付寶的那個事故低零,工人施工婆翔,挖斷了電纜,導(dǎo)致幾個機房都停電
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ā)的流量涌入進來,比如說突然間一秒鐘100萬QPS觉壶,廢掉了脑题,10萬QPS進入系統(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ù)進行隔離,那么可能某一個依賴服務(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)中岩瘦,故障是如何快速蔓延的未巫?
4. Hystrix是如何實現(xiàn)它的目標(biāo)的?
(1)通過HystrixCommand
或者HystrixObservableCommand
來封裝對外部依賴的訪問請求担钮,這個訪問請求一般會運行在獨立的線程中橱赠,資源隔離
(2)對于超出我們設(shè)定閾值的服務(wù)調(diào)用尤仍,直接進行超時箫津,不允許其耗費過長時間阻塞住。這個超時時間默認(rèn)是99.5%的訪問時間,但是一般我們可以自己設(shè)置一下
(3)為每一個依賴服務(wù)維護一個獨立的線程池
苏遥,或者是semaphore
饼拍,當(dāng)線程池已滿時,直接拒絕對這個服務(wù)的調(diào)用
(4)對依賴服務(wù)的調(diào)用的成功次數(shù)田炭,失敗次數(shù)师抄,拒絕次數(shù),超時次數(shù)教硫,進行統(tǒng)計
(5)如果對一個依賴服務(wù)的調(diào)用失敗次數(shù)超過了一定的閾值叨吮,自動進行熔斷,在一定時間內(nèi)對該服務(wù)的調(diào)用直接降級瞬矩,一段時間后再自動嘗試恢復(fù)
(6)當(dāng)一個服務(wù)調(diào)用出現(xiàn)失敗茶鉴,被拒絕,超時景用,短路等異常情況時涵叮,自動調(diào)用fallback降級機制
(7)對屬性和配置的修改提供近實時的支持
畫圖分析,對依賴進行資源隔離后伞插,如何避免依賴服務(wù)調(diào)用延遲或失敗導(dǎo)致當(dāng)前服務(wù)的故障
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)用的邏輯進行封裝
hystrix進行資源隔離割粮,其實是提供了一個抽象,叫做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í)行了,保護說唬格,不要因為某一個依賴服務(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);
}
});
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)致很多線程卡在這里的話,不太好遣鼓,所以進行一個基本的資源隔離和訪問啸盏,避免內(nèi)部復(fù)雜的低效率的代碼,導(dǎo)致大量的線程被hang住
3. 采用信號量技術(shù)對地理位置獲取邏輯進行資源隔離與限流
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回懦,一種是基于線程池气笙,一種是信號量
線程池機制,每個command運行在一個線程中粉怕,限流是通過線程池的大小來控制的
信號量機制健民,command是運行在調(diào)用線程中抒巢,但是通過信號量的容量來進行限流
如何在線程池和信號量之間做選擇贫贝?默認(rèn)的策略就是線程池
線程池其實最大的好處就是對于網(wǎng)絡(luò)訪問請求,如果有超時的話蛉谜,可以避免調(diào)用線程阻塞住
而使用信號量的場景稚晚,通常是針對超大并發(fā)量的場景下,每個服務(wù)實例每秒都幾百的QPS型诚,那么此時你用線程池的話客燕,線程一般不會太多,可能撐不住那么高的并發(fā)狰贯,如果要撐住也搓,可能要耗費大量的線程資源,那么就是用信號量涵紊,來進行限流保護
一般用信號量常見于那種基于純內(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中的請求娱局,都會進入同一個線程池中
3. command線程池
threadpool key代表了一個HystrixThreadPool,用來進行統(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
控制queue滿后reject的threshold辜荠,因為maxQueueSize不允許熱修改汽抚,因此提供這個參數(shù)可以熱修改抓狭,控制隊列的最大大小
HystrixCommand在提交到線程池之前,其實會先進入一個隊列中造烁,這個隊列滿了之后否过,才會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)部原理
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. 檢查是否開啟緩存
從這一步開始萎坷,進入我們的底層的運行原理啦凹联,了解hysrix的一些更加高級的功能和特性
如果這個command開啟了請求緩存,request cache哆档,而且這個調(diào)用的結(jié)果在緩存中存在蔽挠,那么直接從緩存中返回結(jié)果
4. 檢查是否開啟了短路器
檢查這個command對應(yīng)的依賴服務(wù)是否開啟了短路器
如果斷路器被打開了,那么hystrix就不會執(zhí)行這個command瓜浸,而是直接去執(zhí)行fallback降級機制
5. 檢查線程池/隊列/semaphore是否已經(jīng)滿了
如果command對應(yīng)的線程池/隊列/semaphore已經(jīng)滿了澳淑,那么也不會執(zhí)行command,而是直接去調(diào)用fallback降級機制
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降級機制,而且就不會管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ù)進行統(tǒng)計
短路器會根據(jù)這些統(tǒng)計次數(shù)來決定混驰,是否要進行短路,如果打開了短路器,那么在一段時間內(nèi)就會直接短路栖榨,然后如果在之后第一次檢查發(fā)現(xiàn)調(diào)用成功了昆汹,就關(guān)閉斷路器
8. 調(diào)用fallback降級機制
在以下幾種情況中,hystrix會調(diào)用fallback降級機制:run()或construct()拋出一個異常婴栽,短路器打開满粗,線程池/隊列/semaphore滿了,command執(zhí)行超時了
一般在降級機制中愚争,都建議給出一些默認(rèn)的返回值映皆,比如靜態(tài)的一些代碼邏輯,或者從內(nèi)存中的緩存中提取一些數(shù)據(jù)轰枝,盡量在這里不要再進行網(wǎng)絡(luò)請求了
即使在降級中捅彻,一定要進行網(wǎng)絡(luò)調(diào)用,也應(yīng)該將那個調(diào)用放在一個HystrixCommand中鞍陨,進行隔離
在HystrixCommand中步淹,上線getFallback()方法,可以提供降級機制
在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會自動進行緩存介牙,接著在同一個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)的
3.10.7 fallback降級
- 創(chuàng)建command
- 執(zhí)行command
- request cache
- 短路器景埃,如果打開了,fallback降級機制
1. fallback降級機制
hystrix調(diào)用各種接口粱哼,或者訪問外部依賴季二,mysql,redis揭措,zookeeper胯舷,kafka,等等绊含,如果出現(xiàn)了任何異常的情況
比如說報錯了桑嘶,訪問mysql報錯,redis報錯躬充,zookeeper報錯逃顶,kafka報錯,error
對每個外部依賴充甚,無論是服務(wù)接口以政,中間件,資源隔離伴找,對外部依賴只能用一定量的資源去訪問盈蛮,線程池/信號量,如果資源池已滿疆瑰,reject
訪問外部依賴的時候眉反,訪問時間過長,可能就會導(dǎo)致超時穆役,報一個TimeoutException異常,timeout
上述三種情況梳凛,都是我們說的異常情況耿币,對外部依賴的東西訪問的時候出現(xiàn)了異常,發(fā)送異常事件到短路器中去進行統(tǒng)計
如果短路器發(fā)現(xiàn)異常事件的占比達(dá)到了一定的比例韧拒,直接開啟短路淹接,circuit breaker
上述四種情況,都會去調(diào)用fallback降級機制
fallback叛溢,降級機制塑悼,你之前都是必須去調(diào)用外部的依賴接口,或者從mysql中去查詢數(shù)據(jù)的楷掉,但是為了避免說可能外部依賴會有故障
比如厢蒜,你可以再內(nèi)存中維護一個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)典的降級機制:純內(nèi)存數(shù)據(jù)栽渴,默認(rèn)值
run()拋出異常,超時稳懒,線程池或信號量滿了闲擦,或短路了,都會調(diào)用fallback機制
給大家舉個例子场梆,比如說我們現(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信號量的機制去限流
如果超出了這個最大值寸爆,那么直接被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的話嗅定,直接強迫打開短路器,相當(dāng)于是手動短路了用踩,手動降級渠退,默認(rèn)false
HystrixCommandProperties.Setter()
.withCircuitBreakerForceOpen(boolean value)
(6)circuitBreaker.forceClosed
如果設(shè)置為ture的話,直接強迫關(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時長
先進去線程池的是10個請求走哺,然后有8個請求進入等待隊列,線程池里有空閑哲虾,等待隊列中的請求如果還沒有timeout丙躏,那么就進去線程池去執(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ù)提供安全保護措施,那么很可能你的服務(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機制路捧,默認(rèn)是true
HystrixCommandProperties.Setter()
.withExecutionTimeoutEnabled(boolean value)
讓一個command執(zhí)行timeout关霸,然后看是否會調(diào)用fallback降級