[享學Netflix] 三十五、Hystrix執行過程集大成者:AbstractCommand詳解

最快的腳步不是跨越,而是繼續;最慢的步伐不是緩慢,而是徘徊。

–> 返回專欄總目錄 <–
代碼下載地址:https://github.com/f641385712/netflix-learning

前言

Hystrix的源碼因爲是基於RxJava來書寫的,一方面是很多小夥伴對RxJava並不熟悉,另一方面是基於觀察者模式實現的代碼繞來繞去就是不好理解,所以總的來說Hystrix的源碼是比較難啃的。

前面我們已經把Hystrix的正常執行 + 異常fallback執行都“逐個擊破”了,有了良好的知識鋪墊,本文主要僅需做歸併即可捋出其執行原理。另外,雖然我們最常使用的是HystrixCommand,而真正的執行邏輯99%都是在AbstractCommand裏,它纔是集大成者。

正常執行部分請參考這裏:https://fangshixiang.blog.csdn.net/article/details/104556721
異常執行部分請參考這裏:https://fangshixiang.blog.csdn.net/article/details/104718511


正文

如圖,這是Hystrix的執行過程示意圖:

在這裏插入圖片描述

AbstractCommand源碼解析

它是HystrixCommandHystrixObservableCommand的抽象父類,實現了絕大部分的執行邏輯以及熔斷器控制、事件發送等…

說明:xxxCollapser系列如:HystrixCollapserHystrixObservableCollapser它們是沒有提取Abstract抽象實現的,而是直接實現的接口。

每個請求都會生成一個command實例,而每個command實例都對應着一個HystrixCommandKey、HystrixCircuitBreaker、HystrixThreadPool、HystrixConcurrencyStrategy、HystrixRequestCache...等等組件來實現各式各樣的功能。

說明:每次請求command是新生成的一個實例,但是對應的那些組件們可不是新的實例哦,因爲都是同一個HystrixCommandKey的請求共用一個組件實例的~

AbstractCommand是一個“很大的”類,此類源代碼行數2000+,所以拆分成如下幾個部分講解。


成員屬性

AbstractCommand類擁有近30個成員屬性,但還好有了前面N篇文章的鋪陳,大部分的API、組件功能都已瞭然於胸了,所以閱讀起來還是很流暢的。

abstract class AbstractCommand<R> implements HystrixInvokableInfo<R>, HystrixObservable<R> {
	
	// command的id
	protected final HystrixCommandKey commandKey; 
	// 線程池分組名(理論上不同的Command可以共用一個線程池,節約資源嘛)
    protected final HystrixThreadPoolKey threadPoolKey; 
    // 邏輯分組。用於統計
    protected final HystrixCommandGroupKey commandGroup;

	// 各種properties配置  均可以通過SPI方式提供
	protected final HystrixCommandProperties properties;	
	
	// SPI接口(這幾個接口有詳細介紹,不陌生)
    protected final HystrixEventNotifier eventNotifier;
    protected final HystrixConcurrencyStrategy concurrencyStrategy;
    protected final HystrixCommandExecutionHook executionHook;

	// 熔斷器。若你使用配置顯示enable=false了
	// 其實現就是NoOpCircuitBreaker,否則就是默認實現
    protected final HystrixCircuitBreaker circuitBreaker;
    
    // 線程池。默認實現是HystrixThreadPoolDefault
    // 線程池參數使用HystrixThreadPoolProperties配置
    // 通過HystrixConcurrencyStrategy#getThreadPool()得到ThreadPoolExecutor執行器
    // 說明:每次getThreadPool()一下都會用最新的配置配置執行器。所以可以達到動態化的目的
    // 比如說CorePoolSize、MaximumPoolSize等等都可以在properties裏動態改變
    protected final HystrixThreadPool threadPool;
	// Command指標收集器
	protected final HystrixCommandMetrics metrics;

	// 執行時候時使用的信號量(每個key一個信號量控制哦)
	// 說明:它並沒有使用JDK的java.util.concurrent.Semaphore,而是自己的實現
	// 信號量的實現非常簡單,所以就略嘍。
	// 當沒開啓信號量隔離的時候,該實現類使用的是TryableSemaphoreNoOp
	protected final TryableSemaphore executionSemaphoreOverride;
	// 發生fallabck時的信號量,也是每個key一個。至於fallback都需要信號量隔離,前面有詳細說明
	// 它哥倆均只有在隔離策略是SEMAPHORE纔有效。它哥倆信號量默認值都是10
	protected final TryableSemaphore executionSemaphoreOverride;
 
 	// 各種狀態值(這裏也說明command是有狀態的:一個實例只能執行一次)
    protected AtomicReference<CommandState> commandState = new AtomicReference<>(CommandState.NOT_STARTED);
    protected AtomicReference<ThreadState> threadState = new AtomicReference<>(ThreadState.NOT_USING_THREAD);
    protected final AtomicReference<TimedOutStatus> isCommandTimedOut = new AtomicReference<>(TimedOutStatus.NOT_EXECUTED);

	// 執行結果
	protected volatile ExecutionResult executionResult = ExecutionResult.EMPTY;
	// 取消時的執行結果
	protected volatile ExecutionResult executionResultAtTimeOfCancellation;
	
	// 表示爲:響應是否來自於緩存
	// 若緩存開啓了,並且請求時緩存命中了,那此值就會被置爲true
	protected volatile boolean isResponseFromCache = false;
	// command開始執行的時刻(並不代表一定會執行到目標方法哦)
	protected volatile long commandStartTimestamp = -1L;

	// 緩存、日誌相關
	protected final HystrixRequestCache requestCache;
	protected final HystrixRequestLog currentRequestLog;


	// 緩存默認名稱(官方數據,加了這個緩存後效率提升了1-2微秒)
	// 默認的key名使用getSimpleName(),簡單類名
	// 但若你沒有簡單類名(比如內部類),那就使用全類名getName()
	private static ConcurrentHashMap<Class<?>, String> defaultNameCache = new ConcurrentHashMap<>();
	// 緩存該key是否有fallback方法,這樣如果木有就避免每次都反射去找了
	protected static ConcurrentHashMap<HystrixCommandKey, Boolean> commandContainsFallback = new ConcurrentHashMap<>();

	// 唯一構造器,完成了所有屬性的初始化
	protected AbstractCommand(...){
		// group不能爲null,但是key可以爲null -> 自動用簡單類名
        this.commandGroup = initGroupKey(group);
        this.commandKey = initCommandKey(key, getClass());
        ...
        // 自己沒配置,那就使用傳入的值。若傳入爲null,就使用groupKey
        // 大多數情況下讓其保持和groupKey一樣即可
		this.threadPoolKey = initThreadPoolKey(threadPoolKey, this.commandGroup, this.properties.executionIsolationThreadPoolKeyOverride().get());
		...
		this.threadPool = initThreadPool(threadPool, this.threadPoolKey, threadPoolPropertiesDefaults);
		...
	}
	... // 省略所有的init方法


	// 允許摺疊器將此命令實例標記爲用於摺疊請求以及摺疊多少請求
    void markAsCollapsedCommand(HystrixCollapserKey collapserKey, int sizeOfBatch) {
        eventNotifier.markEvent(HystrixEventType.COLLAPSED, this.commandKey);
        executionResult = executionResult.markCollapsed(collapserKey, sizeOfBatch);
    }


	// ========執行方法============
	public Observable<R> observe() { ... }
	public Observable<R> toObservable() { ... }

	// 抽象方法:目標方法以及針對的fallback方法
    protected abstract Observable<R> getExecutionObservable();
    protected abstract Observable<R> getFallbackObservable();
}

光看AbstractCommand擁有的成員屬性,就知道它有多複雜了。需要注意的是:它的屬性的訪問權限大都是protected的,所以子類均可直接訪問。它的這些屬性的初始化均在唯一的構造器裏完成,每個屬性的初始化邏輯大體相似:緩存 -> SPI -> 初始化 -> 默認值,因爲比較簡單,本文略。

完成成員屬性的準備工作後,下面就開始它的執行過程部分了。


toObservable() 所有執行方式的入口

我們知道HystrixCommand的執行方法有多種,但其實不管哪種執行方法,最終都依賴於toObservable()這個方法,toObservable()它是執行原理的集大成者,所有執行方式的入口。

execute() 依賴於 queue() 依賴於 toObservable()
observe() 依賴於 toObservable()

該方法作用:用於訂閱Observable的回調命令的異步執行,也就是說自己返回一個可被訂閱的對象:數據發射器。一旦有訂閱者就會延遲的發送數據/命令,新訂閱者是不會監聽到歷史數據的

AbstractCommand:

	public Observable<R> toObservable() {
		... // 暫時省略非常多的Action和Func們
		// 需要注意的是:這裏使用的是defer(),所以裏面內容是不會立馬執行的,知道有訂閱者訂閱了就執行
		// 也就是說observable = command.toObservable()是不會執行
		// observable.subscribe(xxx)訂閱後,纔會開始執行
		// 下面使用defer的效果都一樣~~~~~~~~~
		return Observable.defer(() -> {
			
			... // 檢驗線程狀態CommandState,每個command只能被執行一次,否則額拋出HystrixRuntimeException異常
			commandStartTimestamp = System.currentTimeMillis(); // 命令開始執行
			... // 記錄日誌:不管發生了什麼,都要記錄這個命令的執行
			// 是否允許請求緩存:properties.requestCacheEnabled().get() && getCacheKey() != null;
			// 雖然properties裏默認是true開啓的,但是需要你重寫getCacheKey()緩存纔會生效的呀
			boolean requestCacheEnabled = isRequestCachingEnabled();
			String cacheKey = getCacheKey(); // 默認是null。重寫它緩存纔會生效
			// 如果開啓了緩存,就先從緩存裏找結果
			if (requestCacheEnabled) {
				... // 一旦命中緩存,就處理好數據後return(關於請求緩存會後面再聊)
				// 若命中了緩存,設置isResponseFromCache = true;
			}
			
			// =========如果沒有獲取到緩存,則需要執行命令獲得結果。========
			// applyHystrixSemantics函數:執行熔斷器 + 目標方法等核心邏輯(最爲複雜和關鍵的一個函數,下有詳解)
			// 所以這裏返回的hystrixObservable已經是目標命令結果了
			// wrapWithAllOnNextHooks:觸發HystrixCommandExecutionHook相關回調
			// 另外,此處也是defer實現哦,所以目標方法並不會立馬執行哦~~~(執行時機交給調用者纔對嘛)
			Observable<R> hystrixObservable = Observable.defer(applyHystrixSemantics)
														.map(wrapWithAllOnNextHooks);
			// 它是最終的return
			Observable<R> afterCache;
			// 若開啓了緩存
			if(requestCacheEnabled){
				... // 把結果hystrixObservable緩存起來並處理後返回
			} else {
				afterCache = hystrixObservable;
			}
			
			// terminateCommandCleanup:執行清理(分爲目標方法執行了or沒執行)
			// unsubscribeCommandCleanup:取消訂閱
			// fireOnCompletedHook:觸發executionHook.onSuccess(_cmd)該方法
			return afterCache.doOnTerminate(terminateCommandCleanup)
							.doOnUnsubscribe(unsubscribeCommandCleanup)
							.doOnCompleted(fireOnCompletedHook);
		});
	}

執行步驟文字描述
  1. 判斷線程狀態是否是NOT_STARTED,否則拋出HystrixRuntimeException異常:一個命令只能執行一次
  2. 命令開始,使用HystrixRequestLog記錄該命令的執行(顯示配置requestLogEnabled = false可關閉日誌的記錄)
  3. 若開啓了請求緩存,那就先從緩存裏找結果(不會執行目標方法)
    1. 緩存開啓的條件是:requestCacheEnabled = true且且且getCacheKey() != null。所以你若想要請求緩存有效,請重寫此方法並不要返回null
  4. 沒開啓緩存(緩存沒命中),則需要執行目標命令獲得結果
    1. Observable.defer()保證了目標方法此時並不會被執行,而是訂閱時才異步執行(交給調用者決定嘛)
    2. applyHystrixSemantics()方法爲執行目標方法最最最核心邏輯,後有詳解
  5. 若開啓了緩存,把結果放進緩存裏
  6. 返回結果。並且註冊上相關清理動作:
    1. terminateCommandCleanup:把線程狀態標記爲TERMINAL。分爲兩種情況:
      1. 目標代碼沒有被執行(比如從緩存裏拿的結果):清空定時監聽、記錄執行耗時、HystrixCommandMetrics#markCommandDone(),觸發執行完成後的函數回調(若endCurrentThreadExecutingCommand不爲null的話)
      2. 目標代碼執行了。邏輯完全同上,只是markCommandDone(true)此處傳true而已
    2. unsubscribeCommandCleanup:把線程狀態標記爲UNSUBSCRIBED。觸發executionHook.onUnsubscribe等動作,並且,並且重複和上步驟一模一樣的動作
    3. fireOnCompletedHook: 僅觸發動作executionHook.onSuccess(_cmd)

這裏主要是套了一層緩存,以及清理相關動作。但其實最爲核心的還是在applyHystrixSemantics()這個函數裏,它纔是真正的關鍵。


applyHystrixSemantics()

AbstractCommand:

	private Observable<R> applyHystrixSemantics(final AbstractCommand<R> _cmd) {
		executionHook.onStart(_cmd); // 開始執行

		// 若斷路器放行(斷路器閉合狀態,當然嘍也可能是半開狀態)
		if (circuitBreaker.allowRequest()) {
			// 若你實現的是線程池隔離,那麼此處實現就是TryableSemaphoreNoOp
			// 若你使用信號量隔離,它就會生效啦
			TryableSemaphore executionSemaphore = getExecutionSemaphore();
			...
			// 嘗試申請信號資源
			if (executionSemaphore.tryAcquire()) {
				executionResult = executionResult.setInvocationStartTime(System.currentTimeMillis());

                return executeCommandAndObserve(_cmd) // =====執行目標方法=====
                        .doOnError(markExceptionThrown) // eventNotifier.markEvent() ... 
                        .doOnTerminate(singleSemaphoreRelease) // 確保釋放信號量
                        .doOnUnsubscribe(singleSemaphoreRelease); // 確保釋放信號量
			} else { // 若木有信號資源了,進入到信號量的fallabck
				return handleSemaphoreRejectionViaFallback();
			}
		} else { // 斷路器打開了禁止你訪問,那就直接執行fallabck邏輯
			return handleShortCircuitViaFallback();
		}

	}

這裏有一個小技巧:TryableSemaphore信號量看起來是不管咋樣都存在的,但是若你是線程池隔離的話,它的實現是NoOp空的,完美的嵌入到了正常流程裏。

執行步驟:

  1. 詢問斷路器是否允許請求:circuitBreaker.allowRequest(),若不允許執行直接執行ShortCircuit短路fallabck邏輯,否則繼續
  2. 嘗試緩存信號量資源(若是線程池隔離,此處永遠爲true),若沒有信號量資源了,觸發信號量拒絕的fallabck邏輯,否則繼續
  3. 執行目標方法邏輯executeCommandAndObserve(_cmd)

到這裏,執行流程分爲兩大分支:正常執行和異常執行,剛好和前面內容完成接軌


總結

關於Hystrix的執行原理,AbstractCommand詳解就介紹到這了,到此關於AbstractCommand的整個內容算是全部講述完成,所以你對Hystrix的原理應該基本掌握。Hystrix在設計上還是蠻值得借鑑的,面向使用者的API可以提供多個,但是最終都歸一到一處,達到更高的內聚效果,維護起來也更加的方便。
分隔線

聲明

原創不易,碼字不易,多謝你的點贊、收藏、關注。把本文分享到你的朋友圈是被允許的,但拒絕抄襲。你也可【左邊掃碼/或加wx:fsx641385712】邀請你加入我的 Java高工、架構師 系列羣大家庭學習和交流。
往期精選

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章