🌏 環境:
🌳 JDK11
🌱 IDEA 2019.03
🌾 Resilience4j 0.13.2
🍃 知識依賴:juc,位圖
一、什么是熔斷
在分布式系統中,各服務間的相互調用更加頻繁,上下游調用中充滿了可能性,一個服務可能會被很多其他服務依賴並調用,在這個過程中如果某個服務由於某種原因出錯(業務出錯、負載過高),可能會導致整個分布式調用鏈路失敗:
圖1
上面這個過程最終可能會導致全鏈路癱瘓(服務雪崩),此時需要一種可以解決上述問題的策略,此策略設計目標為:
- 在發現有服務調用失敗后,及時計算失敗率
- 失敗率達到某種閾值時,切斷與該服務的所有交互,服務走切斷后的自定義邏輯
- 切斷並且不再調用該服務后主動監聽被切斷的服務是否已經恢復了處理能力,若恢復,則繼續讓其提供服務
這個策略被放進圖1中,就變成了下面這樣:
圖2
這個過程中,C服務在自己出問題的情況下,並不會像圖1里那樣仍然有大量流量打進來,也不會影響到上游服務,這個結果讓調用鏈看起來比圖1更加的穩定,這個過程就叫熔斷。
針對這個過程,可以看到在C不可用時,B走了熔斷后的降級邏輯,這個邏輯可以自定義,如果C在整個調用鏈里屬於那種必須要成功的服務,那么這里的邏輯就可以是直接拋錯,如果C屬於那種失敗了也無所謂,不影響整個業務處理,那么降級邏輯里就可以不做處理,例如下面的場景:
圖3
類似這種接口,降級策略很適合不做處理,返回空信息即可,這樣最壞的情況就是頁面少了某個板塊的信息,可能會對用戶造成不太好的體驗,但是不影響其對外服務,被熔斷的服務恢復后頁面也會重新回歸正常。熔斷后的降級處理方式是件值得思考的事情,熔斷和降級是相互獨立的概念,熔斷后必然會有降級操作(哪怕直接拋異常也是一種降級策略),這個降級操作是熔斷這個動作導致的,所以很多時候會把熔斷和降級放在一起說,其實降級還可以由其他動作觸發,比如限流后拋出“系統繁忙”,這也是一種降級策略,只不過它是由限流觸發的,再比如通過開關埋點在系統負載過高時主動關停一些次要服務來提升核心功能的響應速度,這也是一種降級策略,降級是最終產物,而產生它的方式有很多種。
二、Resilience4j中的熔斷器
2.1:Resilience4j是什么?
它是一個輕量、易用、可組裝的高可用框架,支持熔斷、高頻控制、隔離、限流、限時、重試等多種高可用機制。本篇文章只關注其熔斷部分。
2.2:如何使用?
通過第一部分的介紹,可以認為一個熔斷器必須要具備統計單位請求內的錯誤率、全熔斷、半熔斷放量、恢復這幾個流程,帶着這個流程,下面來介紹下Resilience4j里熔斷器的用法。
通過圖2里服務B調用服務C的例子,現在利用java類來進行簡單模擬下這個流程。
首先定義ServerC類,用於模擬服務C:
public class ServerC {
//使用該方法模擬服務C獲取C信息的方法,假設現在服務C的getCInfo方法里有個bug,當輸入的id為0時報錯,其他情況正常
public String getCInfo(int id) {
if (id == 0) {
throw new RuntimeException("輸入0異常");
}
return "id=" + id + "的C信息";
}
}
代碼塊1
再定義ServerB類,用於模擬服務B,這里給服務B調用服務C方法那里加上熔斷器處理,注意這個類里的注釋,會詳細說明熔斷器的主要配置項以及其使用方法:
public class ServerB {
private CircuitBreakerRegistry breakerRegistry;
private ServerC serverC = new ServerC(); //讓服務B持有一個服務C的引用,用來表示正常服務間調用里的一個連接引用
ServerB() {
//初始化breaker注冊器,可以利用該對象生產各種breaker對象(注:凡是用同一個注冊器生產出來的breaker,都會繼承注冊器的配置屬性)
breakerRegistry = CircuitBreakerRegistry.of(CircuitBreakerConfig.custom() //of方法里面放的就是breaker的配置屬性對象
.enableAutomaticTransitionFromOpenToHalfOpen() //開啟從全開狀態經過下面的waitDurationInOpenState時間后自動切換到半開狀態
.failureRateThreshold(50) //熔斷器閉合狀態下的錯誤率閾值,50表示50%,如果錯誤率達到這個閾值,那么熔斷器將進入全熔斷狀態
.ringBufferSizeInClosedState(100) //熔斷器閉合狀態下,以該值為單位請求數,計算錯誤率,跟上面錯誤率閾值綜合理解,這個值表示至少有100個請求,且錯誤50個以上才會觸發全熔斷
.ringBufferSizeInHalfOpenState(10) //熔斷器半熔斷狀態下,以該值為單位請求數,計算錯誤率,跟上面錯誤率閾值綜合理解,這個值表示至少有10個請求,且錯誤5個以上會再次觸發全熔斷,相比閉合狀態,半熔斷狀態下更容易再次進入全熔斷狀態
.waitDurationInOpenState(Duration.ofMillis(1000L)) //熔斷器全熔斷狀態持續的時間,全熔斷后經過該時間后進入半熔斷狀態
.build());
}
//服務B通過服務C來獲取到C的info信息,該方法就是用來干這個的,它會發起對服務C的調用
public String getCInfo(int id) {
//breaker對象是按照name划分全局單例的
CircuitBreaker breaker = breakerRegistry.circuitBreaker("getCInfo"); //這里給熔斷器取個名,一般情況就是一個服務的path或方法名
try {
return breaker.executeCallable(() -> serverC.getCInfo(id));
} catch (CircuitBreakerOpenException e) { //一旦拋出該異常說明已經進入全熔斷狀態
//被熔斷后的降級邏輯
return "服務C出錯,觸發服務B的降級邏輯";
} catch (Exception e) {
//熔斷關閉或者半熔斷狀態下,C拋出的錯誤會被catch到這里
return "調用服務C出錯";
}
}
public CircuitBreaker getBreaker() {
return breakerRegistry.circuitBreaker("getCInfo"); //為了方便做測試,這里返回對應的breaker對象
}
}
代碼塊2
上述配置的熔斷器解釋如下:
在熔斷器閉合的情況下(也即是正常情況下),以100個請求為單位窗口計算錯誤率,一旦錯誤率達到50%,立刻進入全熔斷狀態,該狀態下服務B不會再發生對服務C的調用,直接走自己的降級邏輯,經過1000ms后恢復為半熔斷狀態,此時流量開始打進服務C,此時仍然會計算錯誤率,只是半熔斷狀態下,是以10個請求為單位窗口計算的錯誤率,這個可以保證在服務C沒有恢復正常的情況下可以更快速的進入全熔斷狀態。
2.3:測試-熔斷器狀態切換
然后開始編寫測試方法,下面會通過測試方法來詳細解析該熔斷器的狀態變遷:
public void testBreak() throws Exception {
//按照B服務里熔斷器的配置,如果進行100次請求,有50次失敗了,則對ServerC的調用進入全熔斷狀態
//1000ms后恢復為半熔斷狀態,半熔斷狀態下進行10次請求,如果有5次依然失敗,則再次進入全熔斷狀態
for (int i = 0; i < 100; i++) {
if (i < 50) {
serverB.getCInfo(0); //前50次全部報錯
} else {
serverB.getCInfo(1); //后50次全部成功
}
}
//斷言:此時熔斷器為全熔斷狀態
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.OPEN));
//全熔斷狀態下並不會實際調用C,而是會走服務B的降級邏輯,即便我們輸入的參數是對的,也一樣會被降級
System.out.println(serverB.getCInfo(1));
Thread.sleep(500L);
//斷言:由於全熔斷狀態配置的持續時間時1000ms,所以500ms過去后,仍然是全熔斷狀態
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.OPEN));
Thread.sleep(500L);
//斷言:1000ms過后,熔斷器處於半熔斷狀態
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.HALF_OPEN));
//半熔斷狀態下會嘗試恢復,所以會實際調用C,分別輸入正確和錯誤的參數進行測試
System.out.println(serverB.getCInfo(1));
System.out.println(serverB.getCInfo(0));
//半熔斷狀態下,只需要請求10次,有5次出錯即可再次進入全熔斷狀態
for (int i = 0; i < 10; i++) {
if (i < 4) { //因為上面傳過一次0了,所以這里只需要4次便可以重新回到全開狀態
serverB.getCInfo(0); //前5次全部報錯
} else {
serverB.getCInfo(1); //后5次全部成功
}
}
//斷言:此時熔斷器為全熔斷狀態
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.OPEN));
//同樣的,全熔斷狀態下並不會實際調用C,而是會走服務B的降級邏輯
System.out.println(serverB.getCInfo(1));
//這時靜待1000ms,再次進入半熔斷狀態,我們嘗試恢復服務C的調用
Thread.sleep(1000L);
//這時我們讓其10次請求里有6次成功
for (int i = 0; i < 10; i++) {
if (i < 6) { //前6次成功
serverB.getCInfo(1);
} else { //后4次失敗
serverB.getCInfo(0);
}
}
//由於10次請求里只失敗了4次,達不到50%的全開閾值,所以此時會恢復
//斷言:此時熔斷器為閉合狀態
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.CLOSED));
System.out.println(serverB.getCInfo(1)); //正常輸出
System.out.println(serverB.getCInfo(0)); //走普通異常邏輯
}
代碼塊3
最終輸出如下:
true
服務C出錯,觸發服務B的降級邏輯
true
true
id=1的C信息
調用服務C出錯
true
服務C出錯,觸發服務B的降級邏輯
true
id=1的C信息
調用服務C出錯
可以看到,單位請求內達到錯誤率閾值后熔斷器會進入全開狀態(全熔斷),全開狀態下走降級邏輯,此時不再會實際請求服務C,一段時間后(全開持續時間),進入半開狀態(半熔斷),半開時仍然正常打入服務C,只是由於單位請求量相比閉合時更小,若服務還沒恢復,計算錯誤率會更快達到錯誤率閾值而迅速進入全開狀態,以此類推。如果服務已經恢復,那么將會從半開狀態進入閉合狀態。
2.4:測試-錯誤率統計方式
通過上面的測試用例可以知道觸發熔斷器狀態切換的時機,而且閉合狀態下和半熔斷狀態下統計錯誤率的單位請求數不相同,那么這個請求數量又是怎么統計的呢?如果一個請求先錯誤了49次,然后在第101次請求的時候再錯誤1次是否可以成功觸發熔斷器全開?如果把這49次失敗往后挪一位呢?現在再來按照設想測試下其錯誤率的統計方式:
public void testRate() {
//首先閉合狀態下單位請求仍然是100,現在讓前49次全部失敗
for (int i = 0; i < 100; i++) {
if (i < 49) {
serverB.getCInfo(0);
} else {
serverB.getCInfo(1);
}
}
//斷言:雖然請求了100次,但是錯誤率並沒有達到閾值(50%),所以這里仍然是閉合狀態的
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.CLOSED));
//這里再讓其失敗一次
serverB.getCInfo(0);
//斷言:這里應該還是閉合狀態的,按照100次單位請求來看,第一次失敗的那個請求會被這次失敗這個請求頂替掉(這里不理解沒關系,下面有圖)
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.CLOSED));
}
代碼塊4
輸出結果為:
true
true
然后我們讓第一次失敗的那次請求和其后面出錯的請求后移一位:
public void testRate() {
//首先閉合狀態下單位請求仍然是100,仍然讓其錯誤49次,但現在讓第2~50次失敗
for (int i = 0; i < 100; i++) {
if (i != 0 && i < 50) { //第2~50次請求失敗,總計失敗49次
serverB.getCInfo(0);
} else {
serverB.getCInfo(1);
}
}
//斷言:跟上面例子一樣,錯誤率並沒有達到閾值,仍然是閉合狀態
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.CLOSED));
//這里再讓其失敗一次
serverB.getCInfo(0);
//斷言:這里應該是全開狀態,按照100次單位請求來看,第一次成功的那個請求會被這次失敗這個請求頂替掉,然后湊夠50次失敗請求(參考圖4)
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.OPEN));
}
代碼塊5
輸出結果為:
true
true
用圖來描述下導致這兩種情況發生的流程:
圖4
所以Resilience4j在計算失敗率的時候,是會發生滑動的,錯誤率是根據當前滑動窗口內的請求進行計算得出的,每次請求都會導致窗口移動,都會重新計算當前失敗率,這個在源碼解析里會說明這是怎樣的一種結構,這里簡單了解即可。
三、源碼解析
3.1:注冊器入口
通過上面ServerB類里的使用,首先會通過CircuitBreakerRegistry.of生成一個注冊器對象,然后利用注冊器對象的circuitBreaker方法來生成一個實際的breaker對象,代碼如下:
public interface CircuitBreakerRegistry {
//靜態方法返回了InMemoryCircuitBreakerRegistry的實例
static CircuitBreakerRegistry of(CircuitBreakerConfig circuitBreakerConfig) {
return new InMemoryCircuitBreakerRegistry(circuitBreakerConfig);
}
}
代碼塊6
InMemoryCircuitBreakerRegistry類代碼如下(已簡化處理,只展示流程相關代碼):
public final class InMemoryCircuitBreakerRegistry implements CircuitBreakerRegistry {
//所有的breaker被存方在這個map里,breaker按照name不同而不同,每個breaker里都有自己的一份錯誤率統計數據
private final ConcurrentMap<String, CircuitBreaker> circuitBreakers;
private final CircuitBreakerConfig defaultCircuitBreakerConfig; //開始的配置對象,閉合狀態單位請求量、半開狀態單位請求量、錯誤率閾值等都會放在這里面
public InMemoryCircuitBreakerRegistry(CircuitBreakerConfig defaultCircuitBreakerConfig) {
this.defaultCircuitBreakerConfig = Objects.requireNonNull(defaultCircuitBreakerConfig, "CircuitBreakerConfig must not be null");
this.circuitBreakers = new ConcurrentHashMap<>();
}
@Override
public CircuitBreaker circuitBreaker(String name) { //添加一個breaker,若存在,直接返回
return circuitBreakers.computeIfAbsent(Objects.requireNonNull(name, "Name must not be null"),
(k) -> CircuitBreaker.of(name, defaultCircuitBreakerConfig));
}
}
代碼塊7
這個流程很簡單,就是用一個map來維護所有breaker的,所以需要注意的是,命名breaker的時候,不要攜帶一些id之類的字段,很容易把map撐爆。
3.2:Breaker實體-CircuitBreaker
拿到breaker實體后首先會通過其executeCallable方法執行需要被熔斷的邏輯塊,之前提到的所有的錯誤率統計、狀態切換都發生在這個實體內。
public interface CircuitBreaker {
default T executeCallable(Callable callable) throws Exception{
return decorateCallable(this, callable).call(); //包裝原始的callable
}
//方法包裝,返回一個Callable對象,真正的業務邏輯callable在這里被執行
static Callable decorateCallable(CircuitBreaker circuitBreaker, Callable callable){
return () -> {
//全熔斷狀態下,這里返回false,會拋出CircuitBreakerOpenException類型的異常,ServerB里判定是否走降級邏輯就是通過catch該異常來決定的
if(!circuitBreaker.isCallPermitted()) {
throw new CircuitBreakerOpenException(String.format("CircuitBreaker '%s' is open", circuitBreaker.getName()));
}
//非全熔斷狀態觸發下面的邏輯
long start = System.nanoTime();
try {
T returnValue = callable.call(); //執行實際的業務邏輯
long durationInNanos = System.nanoTime() - start;
circuitBreaker.onSuccess(durationInNanos); //非常關鍵的方法,用來累計執行成功的數量,計算錯誤率
return returnValue;
} catch (Throwable throwable) { //執行異常,調用onError累計出錯數
long durationInNanos = System.nanoTime() - start;
circuitBreaker.onError(durationInNanos, throwable); //非常關鍵的方法,用來累計執行失敗的數量,計算錯誤率
throw throwable;
}
};
}
}
代碼塊8
CircuitBreaker是一個接口,CircuitBreakerStateMachine是它的實現類,上述代碼里比較關鍵的isCallPermitted、onSuccess、onError都是在這個CircuitBreakerStateMachine類里實現的。 CircuitBreakerStateMachine類比較復雜,牽扯到整個熔斷器的狀態切換、錯誤統計觸發等,精簡一下該類,只關注核心部分:
public final class CircuitBreakerStateMachine implements CircuitBreaker {
//熔斷器的名稱
private final String name;
/**
* 非常非常關鍵的一個屬性,它是一個引用對象,CircuitBreakerState一共有以下子類:ClosedState、HalfOpenState、OpenState、DisabledState、ForcedOpenState
* 熔斷器每次發生狀態切換,都會new出一個新的XXState對象,讓下面的引用指向新的狀態對象
*/
private final AtomicReference stateReference;
//開始設置的熔斷器配置,通過該對象可以拿到錯誤率閾值、全熔斷持續狀態等信息
private final CircuitBreakerConfig circuitBreakerConfig;
//&&& 事件處理器,這里不是重點,放到第四部分說,可以先忽略
private final CircuitBreakerEventProcessor eventProcessor;
//構造器
public CircuitBreakerStateMachine(String name, CircuitBreakerConfig circuitBreakerConfig) {
this.name = name;
this.circuitBreakerConfig = circuitBreakerConfig;
this.stateReference = new AtomicReference<>(new ClosedState(this)); //初始化的時候,熔斷器狀態都是閉合狀態,所以首先new一個ClosedState並讓stateReference指向它
this.eventProcessor = new CircuitBreakerEventProcessor();
}
//切換到閉合狀態,new ClosedState,可以看到每個XXState對象都持有當前CircuitBreakerStateMachine對象
@Override
public void transitionToClosedState() {
stateTransition(CLOSED, currentState -> new ClosedState(this, currentState.getMetrics()));
}
//切換到全熔斷狀態,new OpenState,
@Override
public void transitionToOpenState() {
stateTransition(OPEN, currentState -> new OpenState(this, currentState.getMetrics()));
}
//切換到半熔斷狀態,new HalfOpenState,
@Override
public void transitionToHalfOpenState() {
stateTransition(HALF_OPEN, currentState -> new HalfOpenState(this));
}
//狀態切換方法(也即是XXState對象切換的地方)
private void stateTransition(State newState, Function<CircuitBreakerState, CircuitBreakerState> newStateGenerator) {
//引用指向新的XXState對象
CircuitBreakerState previousState = stateReference.getAndUpdate(currentState -> {
if (currentState.getState() == newState) {
return currentState;
}
return newStateGenerator.apply(currentState);
});
if (previousState.getState() != newState) {
//&&& 狀態切換事件發布,本部分忽略,參考第四部分
publishStateTransitionEvent(StateTransition.transitionBetween(previousState.getState(), newState));
}
}
//代碼塊8里的isCallPermitted方法,這個方法決定了是否拋出"已熔斷"異常
@Override
public boolean isCallPermitted() {
//可以看到,這個解決取決於對應XXState里isCallPermitted方法的返回結果
boolean callPermitted = stateReference.get().isCallPermitted();
if (!callPermitted) {
//&&& 已熔斷異常事件發布,本部分忽略,參考第四部分
publishCallNotPermittedEvent();
}
return callPermitted;
}
//代碼塊8里的onError方法,業務處理錯誤后會觸發這個方法的調用
@Override
public void onError(long durationInNanos, Throwable throwable) {
//這個判斷是過濾需要忽略的異常處理,一般情況下沒配置的話所有異常都會走下面實際的onError邏輯
if (circuitBreakerConfig.getRecordFailurePredicate().test(throwable)) {
//&&& 處理錯誤事件發布,參考第四部分
publishCircuitErrorEvent(name, durationInNanos, throwable);
//可以看到,實際上onError也是調用的XXState里的onError方法
stateReference.get().onError(throwable);
} else {
//&&& 命中了可忽略的異常,忽略錯誤事件發布,本部分忽略,參考第四部分
publishCircuitIgnoredErrorEvent(name, durationInNanos, throwable);
}
}
//代碼塊8里的onSuccess方法,業務處理正常會觸發這個方法的調用
@Override
public void onSuccess(long durationInNanos) {
//&&& 處理正常事件發布,本部分忽略,參考第四部分
publishSuccessEvent(durationInNanos);
//同樣的,onSuccess也是調用的XXState里的onError方法
stateReference.get().onSuccess();
}
}
代碼塊9
3.3:狀態類
通過上面的代碼可以知道isCallPermitted、onSuccess、onError這三個方法實際上都是調用對應XXState對象里的方法,下面來看下ClosedState、OpenState、HalfOpenState這三個狀態對象里有關這三個方法的實現(因為上面的測試用例只涉及這三種狀態的互轉,實際上這三種狀態也是最常用的,所以為了避免混亂,只展示這三種,所有狀態類均繼承自CircuitBreakerState抽象類)
3.3.1:ClosedState
閉合狀態時初始狀態,中途只會由半熔斷狀態切換而來,正常情況下都是閉合狀態,代碼如下:
final class ClosedState extends CircuitBreakerState {
//用來度量錯誤率的對象
private final CircuitBreakerMetrics circuitBreakerMetrics;
//就是配置里的failureRateThreshold屬性,閉合狀態時的錯誤率閾值(第二部分的測試用例中是50)
private final float failureRateThreshold;
//參考代碼塊9的CircuitBreakerStateMachine構造器中初始化stateReference時,初始態都是閉合狀態,最初都是通過該方法完成初始化的
ClosedState(CircuitBreakerStateMachine stateMachine) {
this(stateMachine, null);
}
//這個構造器是狀態轉換時觸發的,參考代碼塊9里的transitionToClosedState方法
ClosedState(CircuitBreakerStateMachine stateMachine, CircuitBreakerMetrics circuitBreakerMetrics) {
super(stateMachine);
//拿到熔斷器的配置
CircuitBreakerConfig circuitBreakerConfig = stateMachine.getCircuitBreakerConfig();
if(circuitBreakerMetrics == null){
//初始化metrics對象,傳進去的是閉合狀態時計算錯誤率的單位請求數(第二部分的測試用例中是100)
this.circuitBreakerMetrics = new CircuitBreakerMetrics(
circuitBreakerConfig.getRingBufferSizeInClosedState());
}else{
//中途進行狀態轉換,調用的都是這里的邏輯,利用circuitBreakerMetrics的copy方法,重新賦值給circuitBreakerMetrics屬性,暫時忽略,參考第3.4部分
this.circuitBreakerMetrics = circuitBreakerMetrics.copy(circuitBreakerConfig.getRingBufferSizeInClosedState());
}
//賦值錯誤率閾值
this.failureRateThreshold = stateMachine.getCircuitBreakerConfig().getFailureRateThreshold();
}
@Override
boolean isCallPermitted() {
//閉合狀態下返回true,不會觸發降級邏輯(ps:只有在全熔斷狀態下才會返回true)
return true;
}
@Override
void onError(Throwable throwable) {
// 閉合狀態下,onerror需要記錄錯誤率,注:circuitBreakerMetrics的onError方法會記錄一筆錯誤的記錄,並把當前的錯誤率返回
checkFailureRate(circuitBreakerMetrics.onError());
}
@Override
void onSuccess() {
// 閉合狀態下,onerror需要記錄成功數,注:circuitBreakerMetrics的onSuccess方法會記錄一筆正確的記錄,並把當前的錯誤率返回
checkFailureRate(circuitBreakerMetrics.onSuccess());
}
//根據當前的錯誤率,決定是否切到半熔斷狀態
private void checkFailureRate(float currentFailureRate) {
if (currentFailureRate >= failureRateThreshold) { //這里判斷當前錯誤率是否超過閾值
// 利用CircuitBreakerStateMachine的transitionToOpenState方法,將狀態對象轉換成OpenState
stateMachine.transitionToOpenState();
}
}
}
代碼塊10
3.3.2:OpenState
一般全熔斷狀態會從閉合或者半熔斷狀態里切換而來,它的代碼如下:
final class OpenState extends CircuitBreakerState {
//根據全熔斷持續時間推出的進入半熔斷狀態的時間
private final Instant retryAfterWaitDuration;
//同樣是用來度量錯誤率的對象,該對象就是上一個State對象里的Metrics對象
private final CircuitBreakerMetrics circuitBreakerMetrics;
OpenState(CircuitBreakerStateMachine stateMachine, CircuitBreakerMetrics circuitBreakerMetrics) {
super(stateMachine);
//就是配置里的waitDurationInOpenState屬性,全熔斷持續時間(第二部分的測試用例中是100ms)
final Duration waitDurationInOpenState = stateMachine.getCircuitBreakerConfig().getWaitDurationInOpenState();
//當前時間加上持續時間,就是切換至半熔斷狀態的時機
this.retryAfterWaitDuration = Instant.now().plus(waitDurationInOpenState);
//直接用之前的circuitBreakerMetrics對象
this.circuitBreakerMetrics = circuitBreakerMetrics;
//如果配置了自動切換半熔斷狀態的開關為true,則會發起一個延時任務,用來主動切換狀態
if (stateMachine.getCircuitBreakerConfig().isAutomaticTransitionFromOpenToHalfOpenEnabled()) {
AutoTransitioner.scheduleAutoTransition(stateMachine::transitionToHalfOpenState, waitDurationInOpenState);
}
}
@Override
boolean isCallPermitted() {
// 如果全熔斷狀態持續時間超出目標范圍,則認為現在可以切換為半熔斷狀態,然后返回true
if (Instant.now().isAfter(retryAfterWaitDuration)) {
stateMachine.transitionToHalfOpenState();
return true;
}
circuitBreakerMetrics.onCallNotPermitted(); //記錄一次NotPermitted(簡單的累加)
return false; //全熔斷狀態,直接返回false,表示已被熔斷,讓調用方拋出CircuitBreakerOpenException異常
}
@Override
void onError(Throwable throwable) {
//理論上處於全熔斷狀態,isCallPermitted返回false,onError不會被觸發(參考代碼塊8里的decorateCallable方法)
//但是存在一種特殊的情況,假設有倆線程,線程1執行的時候還是閉合狀態,isCallPermitted返回true,這時線程2里觸發了熔斷閾值
//線程2把stateReference的指向置為OpenState,這時線程1繼續往下執行,觸發的onError其實是OpenState里的onError(也即是本例中的這個方法)
//全熔斷狀態下即便是上面這種臨界情況發生,這次失敗也會被統計上去
circuitBreakerMetrics.onError();
}
/**
* Should never be called when isCallPermitted returns false.
*/
@Override
void onSuccess() {
//跟onError一樣,有概率會訪問到
circuitBreakerMetrics.onSuccess();
}
}
代碼塊11
3.3.3:HalfOpenState
半熔斷狀態一定是由全熔斷切換出來的,來看下它的代碼:
final class HalfOpenState extends CircuitBreakerState {
//同樣是用來度量錯誤率的對象
private CircuitBreakerMetrics circuitBreakerMetrics;
//同樣是配置里的failureRateThreshold屬性
private final float failureRateThreshold;
HalfOpenState(CircuitBreakerStateMachine stateMachine) {
super(stateMachine);
CircuitBreakerConfig circuitBreakerConfig = stateMachine.getCircuitBreakerConfig();
//初始化度量對象,相比閉合狀態,這里傳入的是ringBufferSizeInHalfOpenState(第二部分的測試用例中是10)
this.circuitBreakerMetrics = new CircuitBreakerMetrics(
circuitBreakerConfig.getRingBufferSizeInHalfOpenState());
//閉合狀態和半開狀態共用同一個錯誤率閾值(第二部分的測試用例中是50)
this.failureRateThreshold = stateMachine.getCircuitBreakerConfig().getFailureRateThreshold();
}
@Override
boolean isCallPermitted() {
//跟閉合狀態一樣,返回true
return true;
}
@Override
void onError(Throwable throwable) {
// 跟閉合狀態一樣,要記錄和判斷當前的錯誤率(來決定是恢復閉合狀態還是進入全熔斷狀態)
checkFailureRate(circuitBreakerMetrics.onError());
}
@Override
void onSuccess() {
// 同上
checkFailureRate(circuitBreakerMetrics.onSuccess());
}
//通過該方法,判斷錯誤率,決定是否恢復為閉合狀態或者再次進入全熔斷狀態
private void checkFailureRate(float currentFailureRate) {
//Metrics返回-1表示請求量表示還沒有達到單位請求量(ringBufferSizeInHalfOpenState)
//下面的邏輯可以看出,在半熔斷狀態下,經過ringBufferSizeInHalfOpenState次請求后根據錯誤率判斷,就可以決定出下一步切換到哪個狀態了
if (currentFailureRate != -1) {
//當前錯誤率如果再次超出閾值,則再次進入全熔斷狀態
if (currentFailureRate >= failureRateThreshold) {
stateMachine.transitionToOpenState();
} else { //否則恢復為閉合狀態
stateMachine.transitionToClosedState();
}
}
}
}
代碼塊12
3.3.4:狀態間的切換關系
上面三種狀態的切換關系如下:
圖5
在這些狀態中,最初為熔斷閉合狀態,ServerB的所有請求正常訪問ServerC,ServerC報錯,錯誤率累計達到50%后觸發熔斷全開狀態,此時Server對ServerC發出的請求將走ServerB的降級邏輯,不再實際訪問ServerC的方法,這個狀態會持續waitDurationInOpenState這么久(測試用例中是1000ms),然后進入熔斷半開狀態,此時跟閉合狀態一樣,ServerB的所有請求仍會正常訪問ServerC,不同的是半開狀態下只需要滿足ringBufferSizeInHalfOpenState次調用(測試用例中是10次),就可以直接判斷錯誤率是否達到閾值,這點可以在代碼塊12里的checkFailureRate方法體現,圖5中可以看到,如果未達到錯誤閾值表示ServerC已恢復,則可以關閉熔斷,否則再次進入全熔斷狀態。
3.3.5:度量對象(CircuitBreakerMetrics)的傳遞
這個對象在3.4中會詳細說明,目前只需要知道該類用於做錯誤統計用,錯誤率計算的核心,核心方法為onError和onSuccess,這倆方法用於錯誤/正確請求的觸發點,用於觸發CircuitBreakerMetrics對象對錯誤率的統計。
通過代碼塊10、11、12可以看到CircuitBreakerMetrics對象的流向,首先初始化的時候是調用ClosedState第一個構造器觸發第二個構造器,第二個構造器會new一個CircuitBreakerMetrics,傳過去的size為ringBufferSizeInClosedState,然后由ClosedState切換至OpenState狀態時,其CircuitBreakerMetrics會被傳遞給OpenState對象,根據代碼塊11可以知道,OpenState利用該對象統計熔斷期間被熔斷的次數,然后OpenState切換至HalfOpenState時,HalfOpenState沒有接受CircuitBreakerMetrics對象的構造器,不管由誰切換到半開狀態,CircuitBreakerMetrics對象都是全新的,由代碼塊12可知,初始化CircuitBreakerMetrics對象時傳過去的size就是ringBufferSizeInHalfOpenState。
CircuitBreakerMetrics對象的傳遞以及傳遞后在State對象里所做的操作:
圖6
圖6根據代碼塊10、11、12畫出,簡單體現了Metrics對象的生成以及流向,以及這個對象在各State對象里所做的主要操作。通過圖6可以看出實際產生新的Metrics對象的地方為閉合態和半開態,因為這倆地方是需要做錯誤統計的,需要全新的Metric對象,全開態下僅接收前一狀態的Metrics對象,在命中熔斷后對其內部numberOfNotPermittedCalls(不是很懂這個屬性,簡單的累加,連用到的地方都沒,可能僅僅是做個熔斷數統計讓業務方獲取的吧,做監控可以用),在半開態再次進入閉合態時,其Metrics仍然被傳遞給了閉合態,由代碼塊10可知,如果傳了Metrics對象,閉合態在產生新的Metrics對象時,會通過copy方法來產生,這個方法在3.4會詳細說明,簡單來說就是把前一個狀態(只可能是半開態)的Metrics里的請求計數同步到它自己的Metrics里,這樣做有一個好處,就是新的閉合態不用重新累計錯誤率了,以單元測試所配的參數試想一下,如果在半開態下,進行了10次請求,發生了4次錯誤,此時會切回閉合態,閉合態copy了這10次請求的數據,那么只需要再經過90次請求和46次錯誤便可以再次進入全熔斷狀態(其實就是保證了狀態的平滑切換,不丟失之前已經統計了的數據)。
3.4:錯誤統計
3.4.1:CircuitBreakerMetrics
通過3.3的了解,閉合和半開時的請求狀態計數都是通過CircuitBreakerMetrics對象來完成的,現在來看下這個類里都干了些什么:
class CircuitBreakerMetrics implements CircuitBreaker.Metrics {
//通過3.3的代碼塊可知,該值就是閉合或者半開狀態下設置的ringBufferSizeInClosedState和ringBufferSizeInHalfOpenState
//表示一次請求窗口的大小,測試用例中就是閉合時的100以及半開時的10,通過圖4和下方的getFailureRate方法可以知道,
//至少要累計完成一個請求窗口的請求量后才會實際計算錯誤率
private final int ringBufferSize;
//實際用來記錄一個請求窗口的請求統計數據的結構,本節不深究,詳細參考3.4.2
private final RingBitSet ringBitSet;
//全開狀態下累計被熔斷的請求個數,觸發點參考圖6以及代碼塊11
private final LongAdder numberOfNotPermittedCalls;
//構造器1,參考圖6,在最開始的閉合狀態以及后續的半開狀態下初始化Metrics對象用的就是該構造器
CircuitBreakerMetrics(int ringBufferSize) {
this(ringBufferSize, null);
}
//參考圖6,由半開轉到閉合態的時候,是通過該方法進行初始化的
public CircuitBreakerMetrics copy(int targetRingBufferSize) {
return new CircuitBreakerMetrics(targetRingBufferSize, this.ringBitSet); //這里會把當前Metrics對象里的ringBitSet傳遞下去
}
//構造器2
CircuitBreakerMetrics(int ringBufferSize, RingBitSet sourceSet) {
this.ringBufferSize = ringBufferSize;
if(sourceSet != null) {
//通過copy初始化會走這里(每次的半開態轉閉合態),將原來Metrics對象里的ringBitSet傳遞下去(用來初始化新的請求窗口)
this.ringBitSet = new RingBitSet(this.ringBufferSize, sourceSet);
}else{
//非copy新建Metrics對象(每次的半開態和最初的閉合態)
this.ringBitSet = new RingBitSet(this.ringBufferSize);
}
this.numberOfNotPermittedCalls = new LongAdder();
}
//onError和onSuccess的觸發點參考3.3里的State類
//當請求發生錯誤時觸發該方法,該方法用於記一次失敗,然后把當前錯誤率返回
float onError() {
int currentNumberOfFailedCalls = ringBitSet.setNextBit(true); //通過ringBitSet的setNextBit置為true,算作一筆失敗的記錄
return getFailureRate(currentNumberOfFailedCalls);
}
//當請求正常時觸發該方法,該方法用於記一次成功,然后把當前錯誤率返回
float onSuccess() {
int currentNumberOfFailedCalls = ringBitSet.setNextBit(false); //通過ringBitSet的setNextBit置為false,算作一筆成功的記錄
return getFailureRate(currentNumberOfFailedCalls);
}
//全開狀態下累計被熔斷的請求個數
void onCallNotPermitted() {
numberOfNotPermittedCalls.increment();
}
//通過getFailureRate計算錯誤率並返回
@Override
public float getFailureRate() {
return getFailureRate(getNumberOfFailedCalls());
}
//下方注釋中的窗口大小就是ringBufferSize屬性
//該方法通過ringBitSet對象返回當前請求窗口內發生請求的總次數,如果達到了ringBufferSize次,則這個值就恆等於ringBufferSize
@Override
public int getNumberOfBufferedCalls() {
return this.ringBitSet.length();
}
//該方法通過ringBitSet對象返回當前請求窗口內發生錯誤的次數
@Override
public int getNumberOfFailedCalls() {
return this.ringBitSet.cardinality();
}
//錯誤率計算方法
private float getFailureRate(int numberOfFailedCalls) {
//若請求還沒有完成一個請求窗口,則返回-1
if (getNumberOfBufferedCalls() < ringBufferSize) {
return -1.0f;
}
//完成了一次請求窗口,才會真正計算錯誤率
return numberOfFailedCalls * 100.0f / ringBufferSize;
}
}
代碼塊13
通過上面的代碼可以知道最終統計錯誤數的是在RingBitSet結構中,下面來仔細了解下這個類~
3.4.2:位圖&BitSetMod
了解RingBitSet之前,先來了解一種數據結構-位圖,如果已經了解過位圖,那么可以直接去看RingBitSet。
RingBitSet持有一個BitSetMod對象,BitSetMod基於位圖實現,位圖是怎樣的一種結構呢?先看下圖7,然后再去解析它的源碼實現。
圖7
通過上圖可知,位圖就是利用數組內每個元素的bit位存入一個標記,標記只有存在或者不存在(對應二進制的0和1),這樣就可以做到用一個long型的數字就可以產生出64個標記信息,非常適合數據量龐大而判斷狀態少的應用場景,比如判斷一個詞語是否是屏蔽詞,首先屏蔽詞狀態只有兩種:命中or不命中,但是屏蔽詞可能是個非常龐大的集合,如果一個個拿來比較,效率完全保證不了,那么就可以利用這個數據結構來解決這類問題,可以首先把所有的屏蔽詞放到一個位圖結構里,如果有相同的詞語,只需要簡單的兩部運算就可以拿到是否命中結果,構建這個位圖結構的過程如下:
圖8
通過上圖,屏蔽詞位圖結構就構建好了,如果有個詞語需要判定是否命中屏蔽詞,只需要讓這個詞語通過上面的哈希算法計算出哈希值,然后找到對應的數組下標,通過位運算算出其所在位置,將該位置的值取出,如果是0,則認為沒有命中,1則認為命中。
以上就是位圖結構,通過上面的例子,可以認為同一個值一定命中位圖里的同一個位置,那么抽象成熔斷器的錯誤率,錯誤狀態只有0和1,1表示錯誤,0表示正確,給每次請求編號,當成是圖8中的哈希值,相同編號的請求一定會落到同一個位置,現在不理解沒關系,這個要結合RingBitSet一起看,目前只需要理解位圖特性即可。
Resilience4j里通過BitSetMod簡單實現了一個位圖結構,來看下代碼(注:代碼里有大量位運算,過程說明都寫在了注釋里):
/**
* 下方為源碼注釋↓↓
* {@link io.github.resilience4j.circuitbreaker.internal.BitSetMod} is simplified version of {@link java.util.BitSet}.
* It has no dynamic allocation, expanding logic, boundary checks
* and it's set method returns previous bit state.
*/
public class BitSetMod {
/**
* 1.此類是一種怎樣的數據結構?
* 根據原有注釋,可知這是一個簡易版的BitSet,即位圖結構,可以通過圖7更為直觀的了解下該結構
* 由圖7可知,位圖分為x,y軸,y軸就是本類的long型的數組(words),其中內部每一個元素都包括64個bit,因此bit位橫向擴展就是x軸(x軸大小恆等於64)
* 如果要標記一個數字是否存在於圖中,只需要先找到所屬的y軸位置(即對應的words下標),然后再計算出它應該出現的x軸long型數字中哪個bit位,
* 然后判斷該bit位是否已被標記為true,若是,則返回已存在,否則返回不存在。
* <p>
* <p>
* 2.位運算
* 簡單了解下本類中出現的位運算,任意兩個數的乘法或除法都可以用<<(左移)或>>(右移)來表示
* 例:
* a * b == a << log2(b)
* a / b == a >> log2(b)
* 本例中的ADDRESS_BITS_PER_WORD屬性,其實就是long型位數以2為底的對數,即log2(64) = 6
* 那么接下來代碼中針對ADDRESS_BITS_PER_WORD的位運算就可以簡單理解為乘以/除以64了
*/
//long類型bit位的對數,即log2(64)=6,利用該值可以進行簡單的乘除法的位運算
private final static int ADDRESS_BITS_PER_WORD = 6;
//最終可以存放的總位數,計算方式:words.length * 64(每個long型有64位,利用數組長度乘以位數,就計算出了位圖的總位數)
//用位運算表示為:words.length << 6(右移表示乘法,右移6位表示乘以2^6,即words.length * 64)
private final int size;
//位圖數組,long型,64個bit位
private final long[] words;
//構造器,傳入位圖的容量大小
public BitSetMod(final int capacity) {
//計算數組大小(即y軸大小),根據上面對位圖的基本解釋,可以知道,y軸是一個long型數組,
//而每次一個數字進來,會首先找到y軸所屬的位置,那么這個數組得多大才合適呢?我們知道x軸固定為64個,
//也就是說正常情況下,任意數字進來后都會被分到某個y軸對應的long型數字里的某一位,那么y軸大小就很好推算了,
//利用給出的容量大小(這個表示任意數最大時為多大),除以64進行平均分組,這樣不管傳的任意數為多大,始終都可以找到對應的[x,y],且不會越界
int countOfWordsRequired = wordIndex(capacity - 1) + 1;
//上面說過,size就是位圖里所有位數,即x * y,也就是words.length * 64,用位運算表示為:words.length << 6
size = countOfWordsRequired << ADDRESS_BITS_PER_WORD;
//最終((capacity - 1)/64)+1就是y軸數組大小,初始化數組即可
words = new long[countOfWordsRequired];
//到這里,一個位圖對象就被我們創建好了,數組(y軸)是它實際的實體,x軸是數組里long型數字的二進制位(64)
}
private static int wordIndex(int bitIndex) {
//下面這個位運算等同於:bitIndex/64
return bitIndex >> ADDRESS_BITS_PER_WORD;
}
//開始設置數字信息,bitIndex為目標放置位置,value為值(0或1)
public int set(int bitIndex, boolean value) {
// 利用位置數字除以64,推算出它對應的y軸下標
int wordIndex = wordIndex(bitIndex);
// 注:下面的代碼都是位運算,開始前先來了解一下如何定位某個數字的二進制第n位上的數字是0還是1
// 將1右移bitIndex位,可以得到一個類似1000000的二進制數字,利用這個數字跟原來的數字本身做位與運算,可以推算出原數第bitIndex位上的數字是1還是0
// 舉個例子,我想知道下面這個二進制數字中第5位的數字是0還是1(跟十進制一樣,位數是從右往左數,位數最高的在最左邊,下標從0開始算起)
// 假設該二進制數為λ,設:λ=101010101
// 現在將1右移5位得到bitMask,它用二進制表示為:100000,1的位置正好位於第5位(從右往左,下標從0算起)
// 利用λ跟bitMask進行位與運算:
// 101010101(λ)
// &
// 000100000(bitMask)
// ------------------
// 000000000(位與結果)
// 由這個過程可以發現,λ的第5位如果是0,位與后的結果也是0,如果是1,那么位與運算后的結果肯定是不等於0的,通過這種方式,我們就可以利用1右移的方式,
// 知道λ的第n位是0還是1
//
// 通過上面的例子,可以知道,任意數與1右移后的數字(bitMask)進行位與運算的結果要么不等於0,要么等於0,因為1右移n位后生成的二進制數在其n位上一定為1,
// 其余位置一定為0,0&0、0&1均為0,所以最后的結果要么是000000000,要么還等於1右移后的那個數:000010000,這取決於原始數字里第n位上是否是1,
// 如果是1,則相與后的結果值一定不等於0,反之則等於0
// 結合上面所有的描述,這里可以再思考一個問題,為什么位不會相互覆蓋?比如我傳了一個bitIndex為100,long型1<<100等價於1<<36(以64為模輪回),那么當我傳100的時候豈不是會覆蓋掉傳36時那次做標記?
// 這個問題答案是否定的,因為在最初的時候就已經把bitIndex按照64為單位進行相除計算出下標了,也就是說bitIndex等於100那次,跟bitIndex等於36那次,不在一個下標里(不在一個次元)
// 根據這些規則,下面的代碼就好理解了。
long bitMask = 1L << bitIndex;
int previous = (words[wordIndex] & bitMask) != 0 ? 1 : 0; //把該位置上當前的值(0或1)賦值給previous(也就是最后返回出去的結果)
if (value) {
// 重新賦值,注意,這里是原值跟bitMask進行或運算,意味着目標位的值會直接變成1,其余位置的值均不變
words[wordIndex] = words[wordIndex] | bitMask;
// 結合例子,參考下面這個過程更容易理解
// 101010101
// |
// 000100000
// ---------
// 101110101
} else {
// value等於false的時候,bitMask取反后跟原值進行與運算,跟上面相反,這是把目標位變成0
words[wordIndex] = words[wordIndex] & ~bitMask;
// 結合例子,參考下面這個過程更容易理解
// 101010101
// &
// 111011111(bitMask的反碼)
// ---------
// 101010101
}
return previous;
}
int size() {
//返回位圖里的總位數
return size;
}
boolean get(int bitIndex) {
// 注:如果對下方的右移等操作還不是很了解,請先看set方法里的注釋
// 首先還是利用位置數字除以64,推算出它對應的y軸下標
int wordIndex = wordIndex(bitIndex);
//如果set里的位運算理解了,下面這個很容易理解,這個流程跟set方法里獲取previous一樣
long bitMask = 1L << bitIndex;
return (words[wordIndex] & bitMask) != 0; //大於0時返回true,表示目標位是1,否則返回false,目標位是0
}
}
代碼塊14
上面是Resilience4j針對位圖的簡單實現,它負責存儲單位請求內的錯誤/成功標志。
3.4.3:RingBitSet
之前說過,最終請求被放到了一個環形結構里才對,沿着環執行一周就是一次單位請求,回看下圖4,其實第101次請求就是頂替掉第一次請求的結果罷了,現在把圖4中以100為請求窗口彎曲成一個環,假如第一次請求是失敗的,第101次請求是成功的(綠色背景表示成功的請求,紅色背景表示失敗的請求):
圖9
如何利用位圖結構記錄每次請求的錯誤/成功標記然后再實現圖9里的環形結構呢?Resilience4j通過RingBitSet來實現,來看下它的代碼:
public class RingBitSet {
//單位請求數,根據State類初始化RingSet時給的size值,可以確定該值就是各種ringBufferSize
private final int size;
//真正存放錯誤率的位圖結構
private final BitSetMod bitSet;
//在完成一個請求窗口后,該值為false,表示請求已滿一次請求窗口
private boolean notFull;
//給請求編號,方便位圖計算位置
private int index = -1;
//請求數量,最終等於size
private volatile int length;
//當前請求窗口內的錯誤數,就是利用這個數實時計算錯誤率的(參考CircuitBreakerMetrics.getNumberOfFailedCalls)
private volatile int cardinality = 0;
RingBitSet(int bitSetSize) {
notFull = true;
size = bitSetSize;
bitSet = new BitSetMod(bitSetSize);
}
//攜帶RingBitSet參數的構造器會把sourceSet里的統計數據賦值給新的RingBitSet(繼承其請求數、錯誤率等)
//調用該構造器的觸發點在CircuitBreakerMetrics.copy中觸發,通過圖6可知,每次由半開狀態轉到閉合狀態時,都會調用copy方法,
//讓新的閉合態繼承上次半開態的請求量和錯誤率,這是合理的,比較平滑無損的過度到閉合態。
RingBitSet(int bitSetSize, RingBitSet sourceSet) {
this(bitSetSize);
int targetLength = Integer.min(bitSetSize, sourceSet.length);
int sourceIndex = sourceSet.index;
int forwardIndex = sourceSet.size - sourceIndex;
for (int i = 0; i < targetLength; i++) {
this.setNextBit(sourceSet.bitSet.get(sourceIndex));
// looping sourceIndex backwards without conditional statements
forwardIndex = (forwardIndex + 1) % sourceSet.size;
sourceIndex = (sourceSet.size - forwardIndex) % sourceSet.size;
}
}
//非常非常重要的方法,它的觸發點在CircuitBreakerMetrics的onError和onSuccess,主要用於記錄錯誤率
public synchronized int setNextBit(boolean value) {
increaseLength();
//環形結構依靠這里來實現,index永遠在0~size間循環累加,類似:[0,1,2,3...99,0,1,2,3...99]
index = (index + 1) % size;
//利用位圖,將本次的錯誤/成功標記設置到對應index的位置上,
//並且拿到當前index對應上次請求窗口中同樣為index位置的請求結果previous,至於為啥要拿到這個值,參考下方的邏輯
int previous = bitSet.set(index, value);
//本次請求結果
int current = value ? 1 : 0;
//下面這一步就是刷新錯誤數的,計算方式為:減去同位置上個請求窗口的請求結果,然后加上這次的請求結果
//舉個例子,假設單位請求窗口是100,第一個請求窗口的第一次請求錯誤,index=0的位置被標為1,第101次請求,也就是第二個請求窗口的第一次請求,
//意味着index仍然為0,那么第101次請求的結果就會覆蓋掉第1次請求的那個結果,以此來完成窗口滾動(參考圖9)
cardinality = cardinality - previous + current;
return cardinality;
}
//返回當前請求窗口內的錯誤總量
public int cardinality() {
return cardinality;
}
public int size() {
return bitSet.size();
}
public int length() {
return length;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
for (int i = 0; i < size; i++) {
result.append(bitSet.get(i) ? '1' : '0');
}
return result.toString();
}
synchronized int getIndex() {
return index;
}
//累加當前請求窗口內的請求量,當完成一次單位請求窗口時,length恆等於單位請求窗口大小(size)
private void increaseLength() {
if (notFull) {
int nextLength = length + 1;
if (nextLength < size) {
length = nextLength;
} else {
length = size;
notFull = false;
}
}
}
}
代碼塊15
四、總結
Resilience4j通過CircuitBreakerStateMachine來獨立出一個熔斷器,其內部持有一個CircuitBreakerState對象的引用,在錯誤率達到某個閾值時,會發生狀態切換,CircuitBreakerState的引用會指向新的狀態對象。每個狀態對象持有一個CircuitBreakerMetrics對象,用於做實時統計和錯誤率監聽使用,CircuitBreakerMetrics對象通過RingBitSet來完成單位請求窗口的錯誤率統計,這個統計是實時的,每次請求都會觸發一次錯誤率的判斷。RingBitSet通過Resilience4j自己實現的一個輕量級的位圖結構BitSetMod來標記請求錯誤/成功,順便說下,這里通過RingBitSet來保證環形結構,而位圖只負責存儲請求結果,那么既然這樣,我用普通的數組或者其他的可以通過下標獲取數值的集合結構也可以實現啊,為什么一定要用位圖呢?猜測是位圖既可以保證跟數組一樣高效,都是O(1)的復雜度,又可以節省存儲空間,比如我的單位請求是1w次,如果是數組結構,雖然效率跟位圖一樣高,但是數組卻需要存1w個0或1這樣的數組,即便用byte類型的數組,每個數組元素都浪費了7個bit位。其他集合就更不用說了,效率無法保證,其次他們浪費的內存比單純數組要高,所以,類似這種只有true或false的數據的存儲,位圖再適合不過了。
感覺有些地方說的不太清晰,待后續改進描述方式。