概述
互聯(lián)網(wǎng)應(yīng)用發(fā)展到今天,從單體應(yīng)用架構(gòu)到SOA以及今天的微服務(wù),隨著微服務(wù)化的不斷升級(jí)進(jìn)化,服務(wù)和服務(wù)之間的穩(wěn)定性變得越來(lái)越重要,分布式系統(tǒng)之所以復(fù)雜,主要原因是分布式系統(tǒng)需要考慮到網(wǎng)絡(luò)的延時(shí)和不可靠,微服務(wù)很重要的一個(gè)特質(zhì)就是需要保證服務(wù)冪等,保證冪等性很重要的前提需要分布式鎖控制并發(fā),同時(shí)緩存、降級(jí)和限流是保護(hù)微服務(wù)系統(tǒng)運(yùn)行穩(wěn)定性的三大利器。
隨著業(yè)務(wù)不斷的發(fā)展,按業(yè)務(wù)域的劃分子系統(tǒng)越來(lái)越多,每個(gè)業(yè)務(wù)系統(tǒng)都需要緩存、限流、分布式鎖、冪等工具組件,distributed-tools組件(暫未開(kāi)源)正式包含了上述分布式系統(tǒng)所需要的基礎(chǔ)功能組件。
distributed-tools組件基于tair、redis分別提供了2個(gè)springboot starter,使用起來(lái)非常簡(jiǎn)單。 以使用緩存使用redis為例,application.properties添加如下配置
redis.extend.hostName=127.0.0.1 redis.extend.port=6379 redis.extend.password=pwdcode redis.extend.timeout=10000 redis.idempotent.enabled=true
接下來(lái)的篇幅,重點(diǎn)會(huì)介紹一下緩存、限流、分布式鎖、冪等的使用方式。
緩存
緩存的使用可以說(shuō)無(wú)處不在,從應(yīng)用請(qǐng)求的訪問(wèn)路徑來(lái)看,用戶user -> 瀏覽器緩存 -> 反向代理緩存-> WEB服務(wù)器緩存 -> 應(yīng)用程序緩存 -> 數(shù)據(jù)庫(kù)緩存等,幾乎每條鏈路都充斥著緩存的使用,緩存最直白的解釋就是“用空間換時(shí)間”的算法。緩存就是把一些數(shù)據(jù)暫時(shí)存放于某些地方,可能是內(nèi)存,也有可能硬盤(pán)??傊?,目的就是為了避免某些耗時(shí)的操作。我們常見(jiàn)的耗時(shí)的操作,比如數(shù)據(jù)庫(kù)的查詢(xún)、一些數(shù)據(jù)的計(jì)算結(jié)果,或者是為了減輕服務(wù)器的壓力。其實(shí)減輕壓力也是因查詢(xún)或計(jì)算,雖然短耗時(shí),但操作很頻繁,累加起來(lái)也很長(zhǎng),造成嚴(yán)重排隊(duì)等情況,服務(wù)器抗不住。
distributed-tools組件提供了一個(gè)CacheEngine接口,基于Tair、Redis分別有不同的實(shí)現(xiàn),具體CacheEngine定義如下:
public?String?get(String?key); ????/** ?????*?獲取指定的key對(duì)應(yīng)的對(duì)象,異常也會(huì)返回null ?????*? ?????*?@param?key ?????*?@param?clazz ?????*?@return ?????*/ ????public?
get方法針對(duì)key進(jìn)行查詢(xún),put存儲(chǔ)緩存數(shù)據(jù),invalid刪除緩存數(shù)據(jù)。
限流
在分布式系統(tǒng)中,尤其面對(duì)一些秒殺、瞬時(shí)高并發(fā)場(chǎng)景,都需要進(jìn)行一些限流措施,保證系統(tǒng)的高可用。通常來(lái)說(shuō)限流的目的是通過(guò)對(duì)并發(fā)訪問(wèn)/請(qǐng)求進(jìn)行限速,或者一個(gè)時(shí)間窗口內(nèi)的的請(qǐng)求進(jìn)行限速來(lái)保護(hù)系統(tǒng),一旦達(dá)到限制速率則可以 拒絕服務(wù)(定向到錯(cuò)誤頁(yè)或告知資源沒(méi)有了)、排隊(duì) 或 等待(比如秒殺、評(píng)論、下單)、降級(jí)(返回托底數(shù)據(jù)或默認(rèn)數(shù)據(jù),如商品詳情頁(yè)庫(kù)存默認(rèn)有貨)。
常見(jiàn)的一些限流算法包括固定窗口、滑動(dòng)窗口、漏桶、令牌桶,distributed-tools組件目前基于計(jì)數(shù)器只實(shí)現(xiàn)了固定窗口算法,具體使用方式如下:
/** ?????*?指定過(guò)期時(shí)間自增計(jì)數(shù)器,默認(rèn)每次+1,非滑動(dòng)窗口 ?????*? ?????*?@param?key?計(jì)數(shù)器自增key ?????*?@param?expireTime?過(guò)期時(shí)間 ?????*?@param?unit??時(shí)間單位 ?????*?@return ?????*/ ????public?long?incrCount(String?key,?int?expireTime,?TimeUnit?unit); ????/** ?????*?指定過(guò)期時(shí)間自增計(jì)數(shù)器,單位時(shí)間內(nèi)超過(guò)最大值rateThreshold返回true,否則返回false ?????*? ?????*?@param?key?限流key ?????*?@param?rateThreshold?限流閾值 ?????*?@param?expireTime?固定窗口時(shí)間 ?????*?@param?unit?時(shí)間單位 ?????*?@return ?????*/ ????public?boolean?rateLimit(final?String?key,?final?int?rateThreshold,?int?expireTime,?TimeUnit?unit);
基于CacheEngine的rateLimit方法可以實(shí)現(xiàn)限流,expireTime只能設(shè)定固定窗口時(shí)間,非滑動(dòng)窗口時(shí)間。 另外distributed-tools組件提供了模板RateLimitTemplate可以簡(jiǎn)化限流的易用性,可以直接調(diào)用RateLimitTemplate的execute方法處理限流問(wèn)題。
/** ?????*?@param?limitKey?限流KEY ?????*?@param?resultSupplier?回調(diào)方法 ?????*?@param?rateThreshold?限流閾值 ?????*?@param?limitTime?限制時(shí)間段 ?????*?@param?blockDuration?阻塞時(shí)間段 ?????*?@param?unit?時(shí)間單位 ?????*?@param?errCodeEnum?指定限流錯(cuò)誤碼 ?????*?@return ?????*/ ????public?
另外distributed-tools組件還提供了注解@RateLimit的使用方式,具體注解RateLimit定義如下:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public?@interface?RateLimit?{ ????/** ?????*?限流KEY ?????*/ ????String?limitKey(); ????/** ?????*?允許訪問(wèn)的次數(shù),默認(rèn)值MAX_VALUE ?????*/ ????long?limitCount()?default?Long.MAX_VALUE; ????/** ?????*?時(shí)間段 ?????*/ ????long?timeRange(); ????/** ?????*?阻塞時(shí)間段 ?????*/ ????long?blockDuration(); ????/** ?????*?時(shí)間單位,默認(rèn)為秒 ?????*/ ????TimeUnit?timeUnit()?default?TimeUnit.SECONDS; }
基于注解的方式限流使用代碼如下:
@RateLimit(limitKey?=?"#key",?limitCount?=?5,?timeRange?=?2,?blockDuration?=?3,?timeUnit?=?TimeUnit.MINUTES) public?String?testLimit2(String?key)?{ ????.......... ????return?key; }
任何方法添加上述注解具備了一定的限流能力(具體方法需要在spring aop指定攔截范圍內(nèi)),如上代碼表示以參數(shù)key作為限流key,每2分鐘請(qǐng)求次數(shù)不超過(guò)5次,超過(guò)限制后阻塞3分鐘。
分布式鎖
在Java單一進(jìn)程中通過(guò)synchronized關(guān)鍵字和ReentrantLock可重入鎖可以實(shí)現(xiàn)在多線程環(huán)境中控制對(duì)資源的并發(fā)訪問(wèn),通常本地的加鎖往往不能滿足我們的需要,我們更多的面對(duì)場(chǎng)景是分布式系統(tǒng)跨進(jìn)程的鎖,簡(jiǎn)稱(chēng)為分布式鎖。分布式鎖實(shí)現(xiàn)手段通常是將鎖標(biāo)記存在內(nèi)存中,只是該內(nèi)存不是某個(gè)進(jìn)程分配的內(nèi)存而是公共內(nèi)存如Redis、Tair,至于利用數(shù)據(jù)庫(kù)、文件等做鎖與單機(jī)的實(shí)現(xiàn)是一樣的,只要保證標(biāo)記能互斥就行。分布式鎖相對(duì)單機(jī)進(jìn)程的鎖之所以復(fù)雜,主要原因是分布式系統(tǒng)需要考慮到網(wǎng)絡(luò)的延時(shí)和不可靠。
distributed-tools組件提供的分布式鎖要具備如下特性:互斥性:同本地鎖一樣具有互斥性,但是分布式鎖需要保證在不同節(jié)點(diǎn)進(jìn)程的不同線程的互斥??芍厝胄?同一個(gè)節(jié)點(diǎn)上的同一個(gè)線程如果獲取了鎖之后那么也可以再次獲取這個(gè)鎖。鎖超時(shí):和本地鎖一樣支持鎖超時(shí),防止死鎖,通過(guò)異步心跳demon線程刷新過(guò)期時(shí)間,防止特殊場(chǎng)景(如FGC死鎖超時(shí))下死鎖。高性能、高可用:加鎖和解鎖需要高性能,同時(shí)也需要保證高可用防止分布式鎖失效,可以增加降級(jí)。支持阻塞和非阻塞:同ReentrantLock一樣支持lock和trylock以及tryLock(long timeOut)。公平鎖和非公平鎖(不支持):公平鎖是按照請(qǐng)求加鎖的順序獲得鎖,非公平鎖就相反是無(wú)序的,目前distributed-tools組件提供的分布式鎖不支持該特性。
distributed-tools組件提供的分布式鎖,使用起來(lái)非常簡(jiǎn)單,提供了一個(gè)分布式鎖模板:DistributedLockTemplate,可以直接調(diào)用模板提供的靜態(tài)方法(如下):
/** ?????*?分布式鎖處理模板執(zhí)行器 ?????*? ?????*?@param?lockKey?分布式鎖key ?????*?@param?resultSupplier?分布式鎖處理回調(diào) ?????*?@param?waitTime?鎖等待時(shí)間 ?????*?@param?unit?時(shí)間單位 ?????*?@param?errCodeEnum?指定特殊錯(cuò)誤碼返回 ?????*?@return ?????*/ ????public?static?
冪等
在分布式系統(tǒng)設(shè)計(jì)中冪等性設(shè)計(jì)中十分重要的,尤其在復(fù)雜的微服務(wù)中一套系統(tǒng)中包含了多個(gè)子系統(tǒng)服務(wù),而一個(gè)子系統(tǒng)服務(wù)往往會(huì)去調(diào)用另一個(gè)服務(wù),而服務(wù)調(diào)用服務(wù)無(wú)非就是使用RPC通信或者restful,分布式系統(tǒng)中的網(wǎng)絡(luò)延時(shí)或中斷是避免不了的,通常會(huì)導(dǎo)致服務(wù)的調(diào)用層觸發(fā)重試。具有這一性質(zhì)的接口在設(shè)計(jì)時(shí)總是秉持這樣的一種理念:調(diào)用接口發(fā)生異常并且重復(fù)嘗試時(shí),總是會(huì)造成系統(tǒng)所無(wú)法承受的損失,所以必須阻止這種現(xiàn)象的發(fā)生。
冪等通常會(huì)有兩個(gè)維度: 1. 空間維度上的冪等,即冪等對(duì)象的范圍,是個(gè)人還是機(jī)構(gòu),是某一次交易還是某種類(lèi)型的交易。 2. 時(shí)間維度上的冪等,即冪等的保證時(shí)間,是幾個(gè)小時(shí)、幾天還是永久性的。
在實(shí)際系統(tǒng)中有很多操作,不管操作多少次,都應(yīng)該產(chǎn)生一樣的效果或返回相同的結(jié)果。以下這些應(yīng)用場(chǎng)景也是通常比較常見(jiàn)的應(yīng)用場(chǎng)景: 1. 前端重復(fù)提交請(qǐng)求,且請(qǐng)求數(shù)據(jù)相同時(shí),后臺(tái)需要返回對(duì)應(yīng)這個(gè)請(qǐng)求的相同結(jié)果。 2. 發(fā)起一次支付請(qǐng)求,支付中心應(yīng)該只扣用戶賬戶一次錢(qián),當(dāng)遇到網(wǎng)絡(luò)中斷或系統(tǒng)異常時(shí),也應(yīng)該只扣一次錢(qián)。 3. 發(fā)送消息,同樣內(nèi)容的短信發(fā)給用戶只發(fā)一次。 4. 創(chuàng)建業(yè)務(wù)訂單,一次業(yè)務(wù)請(qǐng)求只能創(chuàng)建一個(gè),重試請(qǐng)求創(chuàng)建多個(gè)就會(huì)出大問(wèn)題。 5. 基于msgId的消息冪等處理
在正式使用distributed-tools組件提供的冪等之前,我們先看下distributed-tools冪等組件的設(shè)計(jì)。
冪等key提取能力:獲取唯一冪等key冪等key的提取支持2中注解:IdempotentTxId、IdempotentTxIdGetter,任意方法添加以上2注解,即可提取到相關(guān)冪等key,前提條件是需要將Idempotent注解添加相關(guān)需要冪等的方法上。
如果單純使用冪等模板進(jìn)行業(yè)務(wù)處理,需要自己設(shè)置相關(guān)冪等key,且要保證其唯一性。
分布式鎖服務(wù)能力:提供全局加鎖、解鎖的能力distributed-tools冪等組件需要使用自身提供的分布式鎖功能,保證其并發(fā)唯一性,distributed-tools提供的分布式鎖能夠提供其可靠、穩(wěn)定的加鎖、解鎖能力。
高性能的寫(xiě)入、查詢(xún)能力:針對(duì)冪等結(jié)果查詢(xún)與存儲(chǔ)distributed-tools冪等組件提供了基于tair、redis的存儲(chǔ)實(shí)現(xiàn),同時(shí)支持自定義一級(jí)、二級(jí)存儲(chǔ)通過(guò)spring依賴(lài)注入到IdempotentService,建議distributed-tools冪等存儲(chǔ)結(jié)果一級(jí)存儲(chǔ)tair mdb,二級(jí)存儲(chǔ)ldb或者tablestore,一級(jí)存儲(chǔ)保證其高性能,二級(jí)存儲(chǔ)保證其可靠性。
二級(jí)存儲(chǔ)并行查詢(xún)會(huì)返回查詢(xún)最快的冪等結(jié)果。 二級(jí)存儲(chǔ)并行異步寫(xiě)入,進(jìn)一步提高性能。
高可用的冪等寫(xiě)入、查詢(xún)能力:冪等存儲(chǔ)出現(xiàn)異常,不影響業(yè)務(wù)正常流程,增加容錯(cuò)distributed-tools冪等組件支持二級(jí)存儲(chǔ),為了保證其高可用,畢竟二級(jí)存儲(chǔ)出現(xiàn)故障的概率太低,不會(huì)導(dǎo)致業(yè)務(wù)上不可用,如果二級(jí)存儲(chǔ)同時(shí)出現(xiàn)故障,業(yè)務(wù)上做了一定的容錯(cuò),針對(duì)不確定性的異常采取重試策略,會(huì)執(zhí)行具體冪等方法。
一級(jí)存儲(chǔ)與二級(jí)存儲(chǔ)的寫(xiě)入與查詢(xún)處理進(jìn)行隔離,任何一級(jí)存儲(chǔ)的異常不會(huì)影響整體業(yè)務(wù)執(zhí)行。
在了解了distributed-tools組件冪等之后,接下來(lái)我們來(lái)看下如何去使用冪等組件,首先了解下common-api提供的冪等注解,具體冪等注解使用方式如下:
注解定義使用范圍使用描述Idempotent方法Idempotent需要定義到具體Method上。Idempotent有個(gè)屬性定義:
expireDate表示冪等有效期,默認(rèn)30天。
spelKey表示可以使用spring表達(dá)式生成冪等唯一ID,比如直接獲取到對(duì)象屬性或者方法或者其他表達(dá)式。IdempotentTxId參數(shù)、對(duì)象屬性IdempotentTxId可以直接定義到方法參數(shù)或者參數(shù)對(duì)象屬性上,直接獲取冪等IDIdempotentTxIdGetter方法IdempotentTxIdGetter可以直接定義參數(shù)對(duì)象的方法上,調(diào)用該方法獲取冪等ID
冪等攔截器獲取冪等ID的優(yōu)先級(jí):
首先判斷Idempotent的spelKey的屬性是否為空,如果不為空會(huì)根據(jù)spelKey定義的spring表達(dá)式生成冪等ID。
其次判斷參數(shù)是否包含IdempotentTxId注解,如果有IdempotentTxId,會(huì)直接獲取參數(shù)值生成冪等ID。
再次通過(guò)反射獲取參數(shù)對(duì)象屬性是否包含IdempotentTxId注解,如果對(duì)象屬性包含IdempotentTxId注解會(huì)獲取該參數(shù)對(duì)象屬性生成冪等ID。
最后以上三種情況仍未獲取到冪等ID,會(huì)進(jìn)一步通過(guò)反射獲取參數(shù)對(duì)象的Method是否定義IdempotentTxIdGetter注解,如果包含該注解則通過(guò)反射生成冪等ID。
代碼使用示例:
@Idempotent(spelKey?=?"#request.requestId",?firstLevelExpireDate?=?7,secondLevelExpireDate?=?30) ????public?void?execute(BizFlowRequest?request)?{ ???????.................. ????}
如上述代碼表示從request獲取requestId作為冪等key,一級(jí)存儲(chǔ)有效期7天,二級(jí)存儲(chǔ)有效期30天。
distributed-tools除了可以使用冪等注解外,冪等組件還提供了一個(gè)通用冪等模板IdempotentTemplate,使用冪等模板的前提必須設(shè)置tair.idempotent.enabled=true或者redis.idempotent.enabled=true,默認(rèn)為false,同時(shí)需要指定冪等結(jié)果一級(jí)存儲(chǔ),冪等結(jié)果存儲(chǔ)為可選項(xiàng)配置。 具體使用冪等模板IdempotentTemplate的方法如下:
/** ?????*?冪等模板處理器 ?????* ?????*?@param?request?冪等Request信息 ?????*?@param?executeSupplier?冪等處理回調(diào)function ?????*?@param?resultPreprocessConsumer?冪等結(jié)果回調(diào)function?可以對(duì)結(jié)果做些預(yù)處理 ?????*?@param?ifResultNeedIdempotence?除了根據(jù)異常還需要根據(jù)結(jié)果判定是否需要冪等性的場(chǎng)景可以提供此參數(shù) ?????*?@return ?????*/ ????public?R?execute(IdempotentRequest
?request,?Supplier
request:冪等參數(shù)IdempotentRequest組裝,可以設(shè)置冪等參數(shù)和冪等唯一ID
executeSupplier:具體冪等的方法邏輯,比如針對(duì)支付、下單接口,可以通過(guò)JDK8函數(shù)式接口Supplier Callback進(jìn)行處理。
resultBiConsumer:冪等返回結(jié)果的處理,該參數(shù)可以為空,如果為空采取默認(rèn)的處理,根據(jù)冪等結(jié)果,如果成功、不可重試的異常錯(cuò)誤碼,直接返回結(jié)果,如果失敗可重試異常錯(cuò)誤碼,會(huì)進(jìn)行重試處理。 如果該參數(shù)值不為空,可以針對(duì)返回冪等結(jié)果進(jìn)行特殊邏輯處理設(shè)置ResultStatus(ResultStatus包含三種狀態(tài)包括成功、失敗可重試、失敗不可重試)。
評(píng)論
查看更多