互聯網應用發展到今天,從單體應用架構到SOA以及今天的微服務,隨着微服務化的不斷升級進化,服務和服務之間的穩定性變得愈來愈重要,分佈式系統之因此複雜,主要緣由是分佈式系統須要考慮到網絡的延時和不可靠,微服務很重要的一個特質就是須要保證服務冪等,保證冪等性很重要的前提須要分佈式鎖控制併發,同時緩存、降級和限流是保護微服務系統運行穩定性的三大利器。前端
隨着業務不斷的發展,按業務域的劃分子系統愈來愈多,每一個業務系統都須要緩存、限流、分佈式鎖、冪等工具組件,distributed-tools組件(暫未開源)正式包含了上述分佈式系統所須要的基礎功能組件。redis
distributed-tools組件基於tair、redis分別提供了2個springboot starter,使用起來很是簡單。
以使用緩存使用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
接下來的篇幅,重點會介紹一下緩存、限流、分佈式鎖、冪等的使用方式。spring
緩存的使用能夠說無處不在,從應用請求的訪問路徑來看,用戶user -> 瀏覽器緩存 -> 反向代理緩存-> WEB服務器緩存 -> 應用程序緩存 -> 數據庫緩存等,幾乎每條鏈路都充斥着緩存的使用,緩存最直白的解釋就是「用空間換時間」的算法。緩存就是把一些數據暫時存放於某些地方,多是內存,也有可能硬盤。總之,目的就是爲了不某些耗時的操做。咱們常見的耗時的操做,好比數據庫的查詢、一些數據的計算結果,或者是爲了減輕服務器的壓力。其實減輕壓力也是因查詢或計算,雖然短耗時,但操做很頻繁,累加起來也很長,形成嚴重排隊等狀況,服務器抗不住。數據庫
distributed-tools組件提供了一個CacheEngine接口,基於Tair、Redis分別有不一樣的實現,具體CacheEngine定義以下:api
public String get(String key); /** * 獲取指定的key對應的對象,異常也會返回null * * @param key * @param clazz * @return */ public <T> T get(String key, Class<T> clz); /** * 存儲緩存數據,忽略過時時間 * * @param key * @param value * @return */ public <T extends Serializable> boolean put(String key, T value); /** * 存儲緩存數據 * * @param key * @param value * @param expiredTime * @param unit * @return */ public <T extends Serializable> boolean put(String key, T value, int expiredTime, TimeUnit unit); /** * 基於key刪除緩存數據 * * @param key * @return */ public boolean invalid(String key);
get方法針對key進行查詢,put存儲緩存數據,invalid刪除緩存數據。瀏覽器
在分佈式系統中,尤爲面對一些秒殺、瞬時高併發場景,都須要進行一些限流措施,保證系統的高可用。一般來講限流的目的是經過對併發訪問/請求進行限速,或者一個時間窗口內的的請求進行限速來保護系統,一旦達到限制速率則能夠 拒絕服務(定向到錯誤頁或告知資源沒有了)、排隊 或 等待(好比秒殺、評論、下單)、降級(返回託底數據或默認數據,如商品詳情頁庫存默認有貨)。緩存
常見的一些限流算法包括固定窗口、滑動窗口、漏桶、令牌桶,distributed-tools組件目前基於計數器只實現了固定窗口算法,具體使用方式以下:springboot
/** * 指定過時時間自增計數器,默認每次+1,非滑動窗口 * * @param key 計數器自增key * @param expireTime 過時時間 * @param unit 時間單位 * @return */ public long incrCount(String key, int expireTime, TimeUnit unit); /** * 指定過時時間自增計數器,單位時間內超過最大值rateThreshold返回true,不然返回false * * @param key 限流key * @param rateThreshold 限流閾值 * @param expireTime 固定窗口時間 * @param unit 時間單位 * @return */ public boolean rateLimit(final String key, final int rateThreshold, int expireTime, TimeUnit unit);
基於CacheEngine的rateLimit方法能夠實現限流,expireTime只能設定固定窗口時間,非滑動窗口時間。
另外distributed-tools組件提供了模板RateLimitTemplate能夠簡化限流的易用性,能夠直接調用RateLimitTemplate的execute方法處理限流問題。服務器
/** * @param limitKey 限流KEY * @param resultSupplier 回調方法 * @param rateThreshold 限流閾值 * @param limitTime 限制時間段 * @param blockDuration 阻塞時間段 * @param unit 時間單位 * @param errCodeEnum 指定限流錯誤碼 * @return */ public <T> T execute(String limitKey, Supplier<T> resultSupplier, long rateThreshold, long limitTime, long blockDuration, TimeUnit unit, ErrCodeEnum errCodeEnum) { boolean blocked = tryAcquire(limitKey, rateThreshold, limitTime, blockDuration, unit); if (errCodeEnum != null) { AssertUtils.assertTrue(blocked, errCodeEnum); } else { AssertUtils.assertTrue(blocked, ExceptionEnumType.ACQUIRE_LOCK_FAIL); } return resultSupplier.get(); }
另外distributed-tools組件還提供了註解@RateLimit的使用方式,具體註解RateLimit定義以下:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface RateLimit { /** * 限流KEY */ String limitKey(); /** * 容許訪問的次數,默認值MAX_VALUE */ long limitCount() default Long.MAX_VALUE; /** * 時間段 */ long timeRange(); /** * 阻塞時間段 */ long blockDuration(); /** * 時間單位,默認爲秒 */ 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指定攔截範圍內),如上代碼表示以參數key做爲限流key,每2分鐘請求次數不超過5次,超過限制後阻塞3分鐘。
在Java單一進程中經過synchronized關鍵字和ReentrantLock可重入鎖能夠實如今多線程環境中控制對資源的併發訪問,一般本地的加鎖每每不能知足咱們的須要,咱們更多的面對場景是分佈式系統跨進程的鎖,簡稱爲分佈式鎖。分佈式鎖實現手段一般是將鎖標記存在內存中,只是該內存不是某個進程分配的內存而是公共內存如Redis、Tair,至於利用數據庫、文件等作鎖與單機的實現是同樣的,只要保證標記能互斥就行。分佈式鎖相對單機進程的鎖之因此複雜,主要緣由是分佈式系統須要考慮到網絡的延時和不可靠。
distributed-tools組件提供的分佈式鎖要具有以下特性:
互斥性:同本地鎖同樣具備互斥性,可是分佈式鎖須要保證在不一樣節點進程的不一樣線程的互斥。
可重入性:同一個節點上的同一個線程若是獲取了鎖以後那麼也能夠再次獲取這個鎖。
鎖超時:和本地鎖同樣支持鎖超時,防止死鎖,經過異步心跳demon線程刷新過時時間,防止特殊場景(如FGC死鎖超時)下死鎖。
高性能、高可用:加鎖和解鎖須要高性能,同時也須要保證高可用防止分佈式鎖失效,能夠增長降級。
支持阻塞和非阻塞:同ReentrantLock同樣支持lock和trylock以及tryLock(long timeOut)。
公平鎖和非公平鎖(不支持):公平鎖是按照請求加鎖的順序得到鎖,非公平鎖就相反是無序的,目前distributed-tools組件提供的分佈式鎖不支持該特性。
distributed-tools組件提供的分佈式鎖,使用起來很是簡單,提供了一個分佈式鎖模板:DistributedLockTemplate,能夠直接調用模板提供的靜態方法(以下):
/** * 分佈式鎖處理模板執行器 * * @param lockKey 分佈式鎖key * @param resultSupplier 分佈式鎖處理回調 * @param waitTime 鎖等待時間 * @param unit 時間單位 * @param errCodeEnum 指定特殊錯誤碼返回 * @return */ public static <T> T execute(String lockKey, Supplier<T> resultSupplier, long waitTime, TimeUnit unit, ErrCodeEnum errCodeEnum) { AssertUtils.assertTrue(StringUtils.isNotBlank(lockKey), ExceptionEnumType.PARAMETER_ILLEGALL); boolean locked = false; Lock lock = DistributedReentrantLock.newLock(lockKey); try { locked = waitTime > 0 ? lock.tryLock(waitTime, unit) : lock.tryLock(); } catch (InterruptedException e) { throw new RuntimeException(String.format("lock error,lockResource:%s", lockKey), e); } if (errCodeEnum != null) { AssertUtils.assertTrue(locked, errCodeEnum); } else { AssertUtils.assertTrue(locked, ExceptionEnumType.ACQUIRE_LOCK_FAIL); } try { return resultSupplier.get(); } finally { lock.unlock(); } }
在分佈式系統設計中冪等性設計中十分重要的,尤爲在複雜的微服務中一套系統中包含了多個子系統服務,而一個子系統服務每每會去調用另外一個服務,而服務調用服務無非就是使用RPC通訊或者restful,分佈式系統中的網絡延時或中斷是避免不了的,一般會致使服務的調用層觸發重試。具備這一性質的接口在設計時老是秉持這樣的一種理念:調用接口發生異常而且重複嘗試時,老是會形成系統所沒法承受的損失,因此必須阻止這種現象的發生。
冪等一般會有兩個維度:
1. 空間維度上的冪等,即冪等對象的範圍,是我的仍是機構,是某一次交易仍是某種類型的交易。
2. 時間維度上的冪等,即冪等的保證時間,是幾個小時、幾天仍是永久性的。
在實際系統中有不少操做,無論操做多少次,都應該產生同樣的效果或返回相同的結果。如下這些應用場景也是一般比較常見的應用場景:
1. 前端重複提交請求,且請求數據相同時,後臺須要返回對應這個請求的相同結果。
2. 發起一次支付請求,支付中心應該只扣用戶帳戶一次錢,當遇到網絡中斷或系統異常時,也應該只扣一次錢。
3. 發送消息,一樣內容的短信發給用戶只發一次。
4. 建立業務訂單,一次業務請求只能建立一個,重試請求建立多個就會出大問題。
5. 基於msgId的消息冪等處理
在正式使用distributed-tools組件提供的冪等以前,咱們先看下distributed-tools冪等組件的設計。
冪等key的提取支持2中註解:IdempotentTxId、IdempotentTxIdGetter,任意方法添加以上2註解,便可提取到相關冪等key,前提條件是須要將Idempotent註解添加相關須要冪等的方法上。
若是單純使用冪等模板進行業務處理,須要本身設置相關冪等key,且要保證其惟一性。
distributed-tools冪等組件須要使用自身提供的分佈式鎖功能,保證其併發惟一性,distributed-tools提供的分佈式鎖可以提供其可靠、穩定的加鎖、解鎖能力。
distributed-tools冪等組件提供了基於tair、redis的存儲實現,同時支持自定義一級、二級存儲經過spring依賴注入到IdempotentService,建議distributed-tools冪等存儲結果一級存儲tair mdb,二級存儲ldb或者tablestore,一級存儲保證其高性能,二級存儲保證其可靠性。
二級存儲並行查詢會返回查詢最快的冪等結果。
二級存儲並行異步寫入,進一步提升性能。
distributed-tools冪等組件支持二級存儲,爲了保證其高可用,畢竟二級存儲出現故障的機率過低,不會致使業務上不可用,若是二級存儲同時出現故障,業務上作了必定的容錯,針對不肯定性的異常採起重試策略,會執行具體冪等方法。
一級存儲與二級存儲的寫入與查詢處理進行隔離,任何一級存儲的異常不會影響總體業務執行。
在瞭解了distributed-tools組件冪等以後,接下來咱們來看下如何去使用冪等組件,首先了解下common-api提供的冪等註解,具體冪等註解使用方式以下:
冪等攔截器獲取冪等ID的優先級:
代碼使用示例:
@Idempotent(spelKey = "#request.requestId", firstLevelExpireDate = 7,secondLevelExpireDate = 30) public void execute(BizFlowRequest request) { .................. }
如上述代碼表示從request獲取requestId做爲冪等key,一級存儲有效期7天,二級存儲有效期30天。
distributed-tools除了可使用冪等註解外,冪等組件還提供了一個通用冪等模板IdempotentTemplate,使用冪等模板的前提必須設置tair.idempotent.enabled=true或者redis.idempotent.enabled=true,默認爲false,同時須要指定冪等結果一級存儲,冪等結果存儲爲可選項配置。
具體使用冪等模板IdempotentTemplate的方法以下:
/** * 冪等模板處理器 * * @param request 冪等Request信息 * @param executeSupplier 冪等處理回調function * @param resultPreprocessConsumer 冪等結果回調function 能夠對結果作些預處理 * @param ifResultNeedIdempotence 除了根據異常還須要根據結果斷定是否須要冪等性的場景能夠提供此參數 * @return */ public R execute(IdempotentRequest<P> request, Supplier<R> executeSupplier, Consumer<IdempotentResult<P, R>> resultPreprocessConsumer, Predicate<R> ifResultNeedIdempotence) { ........ }
request:
冪等參數IdempotentRequest組裝,能夠設置冪等參數和冪等惟一ID
executeSupplier:
具體冪等的方法邏輯,好比針對支付、下單接口,能夠經過JDK8函數式接口Supplier Callback進行處理。
resultBiConsumer:
冪等返回結果的處理,該參數能夠爲空,若是爲空採起默認的處理,根據冪等結果,若是成功、不可重試的異常錯誤碼,直接返回結果,若是失敗可重試異常錯誤碼,會進行重試處理。
若是該參數值不爲空,能夠針對返回冪等結果進行特殊邏輯處理設置ResultStatus(ResultStatus包含三種狀態包括成功、失敗可重試、失敗不可重試)。
本文做者:中間件小哥
本文爲雲棲社區原創內容,未經容許不得轉載。