1. 程式人生 > 其它 >SpringCloud-Hystrix傳播ThreadLocal物件

SpringCloud-Hystrix傳播ThreadLocal物件

問題

在開發時遇到

當Feign開啟Hystrix支援時,

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

是 null 。

使用Hystrix時,如何傳播ThreadLocal物件?

我們知道,Hystrix有隔離策略:THREAD以及SEMAPHORE。

如果你不知道Hystrix的隔離策略,可以閱讀書籍《Spring Cloud與Docker微服務架構實戰》,或者參考文件:https://github.com/Netflix/Hystrix/wiki/Configuration#executionisolationstrategy

引子

當隔離策略為 THREAD 時,是沒辦法拿到 ThreadLocal 中的值的。

舉個例子,使用Feign呼叫某個遠端API,這個遠端API需要傳遞一個Header,這個Header是動態的,跟你的HttpRequest相關,我們選擇編寫一個攔截器來實現Header的傳遞(當然也可以在Feign Client介面的方法上加RequestHeader )。

示例程式碼:

public class KeycloakRequestInterceptor implements RequestInterceptor {

    private static final String AUTHORIZATION_HEADER = "Authorization";

    @Override
    public void apply(RequestTemplate template) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        Principal principal = attributes.getRequest().getUserPrincipal();

        if (principal != null && principal instanceof KeycloakPrincipal) {
            KeycloakSecurityContext keycloakSecurityContext = ((KeycloakPrincipal) principal)
                    .getKeycloakSecurityContext();

            if (keycloakSecurityContext instanceof RefreshableKeycloakSecurityContext) {
                RefreshableKeycloakSecurityContext.class.cast(keycloakSecurityContext)
                        .refreshExpiredToken(true);
                template.header(AUTHORIZATION_HEADER, "Bearer " + keycloakSecurityContext.getTokenString());
            }

        }
        // 否則啥都不幹
    }
}

你可能不知道Keycloak是什麼,不過沒有關係,相信這段程式碼並不難閱讀,該攔截器做了幾件事:

  • 使用 RequestContextHolder.getRequestAttributes() 靜態方法獲得Request。
  • 從Request獲得當前使用者的身份,然後使用Keycloak的API拿到Token,並扔到Header裡。
  • 這樣,Feign使用這個攔截器時,就會用你這個Header去請求了。

現實很骨感

以上程式碼可完美執行——但僅限於Feign不開啟Hystrix支援時。

注:Spring Cloud Dalston以及更高版可使用 feign.hystrix.enabled=true

為Feign開啟Hystrix支援。

當Feign開啟Hystrix支援時,

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

null

原因在於,Hystrix的預設隔離策略是THREAD 。而 RequestContextHolder 原始碼中,使用了兩個血淋淋的ThreadLocal

解決方案一:調整隔離策略

將隔離策略設為SEMAPHORE即可:

hystrix.command.default.execution.isolation.strategy: SEMAPHORE

這樣配置後,Feign可以正常工作。

但該方案不是特別好。原因是Hystrix官方強烈建議使用THREAD作為隔離策略! 參考文件:

Thread or Semaphore

The default, and the recommended setting, is to run HystrixCommands using thread isolation (THREAD) and HystrixObservableCommands using semaphore isolation (SEMAPHORE).

Commands executed in threads have an extra layer of protection against latencies beyond what network timeouts can offer.

Generally the only time you should use semaphore isolation for HystrixCommands is when the call is so high volume (hundreds per second, per instance) that the overhead of separate threads is too high; this typically only applies to non-network calls.

於是,那麼有沒有更好的方案呢?

解決方案二:自定義併發策略

既然Hystrix不太建議使用SEMAPHORE作為隔離策略,那麼是否有其他方案呢?答案是自定義併發策略,目前,Spring Cloud Sleuth以及Spring Security都通過該方式傳遞 ThreadLocal 物件。

下面我們來編寫自定義的併發策略。

編寫自定義併發策略

編寫自定義併發策略比較簡單,只需編寫一個類,讓其繼承HystrixConcurrencyStrategy ,並重寫wrapCallable 方法即可。

程式碼示例:

@Component
public class RequestAttributeHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
    private static final Log log = LogFactory.getLog(RequestHystrixConcurrencyStrategy.class);

    public RequestHystrixConcurrencyStrategy() {
        HystrixPlugins.reset();
        HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
    }

    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        return new WrappedCallable<>(callable, requestAttributes);
    }

    static class WrappedCallable<T> implements Callable<T> {

        private final Callable<T> target;
        private final RequestAttributes requestAttributes;

        public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) {
            this.target = target;
            this.requestAttributes = requestAttributes;
        }

        @Override
        public T call() throws Exception {
            try {
                RequestContextHolder.setRequestAttributes(requestAttributes);
                return target.call();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        }
    }
}

如程式碼所示,我們編寫了一個RequestHystrixConcurrencyStrategy ,在其中:

  • wrapCallable 方法拿到 RequestContextHolder.getRequestAttributes() ,也就是我們想傳播的物件;
  • WrappedCallable 類中,我們將要傳播的物件作為成員變數,並在其中的call方法中,為靜態方法設值。
  • 這樣,在Hystrix包裹的方法中,就可以使用RequestContextHolder.getRequestAttributes() 獲取到相關屬性——也就是說,可以拿到RequestContextHolder 中的ThreadLocal 屬性。

經過測試,程式碼能正常工作。

新的問題

至此,我們已經實現了ThreadLocal 屬性的傳遞,然而Hystrix只允許有一個併發策略!這意味著——如果不做任何處理,Sleuth、Spring Security將無法正常拿到上下文!(上文說過,目前Sleuth、Spring Security都是通過自定義併發策略的方式來傳遞ThreadLocal物件的。)

如何解決這個問題呢?

我們知道,Spring Cloud中,Spring Cloud Security與Spring Cloud Sleuth是可以共存的!我們不妨參考下Sleuth以及Spring Security的實現:

  • Sleuth:org.springframework.cloud.sleuth.instrument.hystrix.SleuthHystrixConcurrencyStrategy
  • Spring Security:org.springframework.cloud.netflix.hystrix.security.SecurityContextConcurrencyStrategy

閱讀完後,你將恍然大悟——於是,我們可以模仿它們的寫法,改寫上文編寫的併發策略:

public class RequestAttributeHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
    private static final Log log = LogFactory.getLog(RequestAttributeHystrixConcurrencyStrategy.class);

    private HystrixConcurrencyStrategy delegate;

    public RequestAttributeHystrixConcurrencyStrategy() {
        try {
            this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
            if (this.delegate instanceof RequestAttributeHystrixConcurrencyStrategy) {
                // Welcome to singleton hell...
                return;
            }
            HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins
                    .getInstance().getCommandExecutionHook();
            HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance()
                    .getEventNotifier();
            HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance()
                    .getMetricsPublisher();
            HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance()
                    .getPropertiesStrategy();
            this.logCurrentStateOfHystrixPlugins(eventNotifier, metricsPublisher,
                    propertiesStrategy);
            HystrixPlugins.reset();
            HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
            HystrixPlugins.getInstance()
                    .registerCommandExecutionHook(commandExecutionHook);
            HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
            HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
            HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
        }
        catch (Exception e) {
            log.error("Failed to register Sleuth Hystrix Concurrency Strategy", e);
        }
    }

    private void logCurrentStateOfHystrixPlugins(HystrixEventNotifier eventNotifier,
            HystrixMetricsPublisher metricsPublisher,
            HystrixPropertiesStrategy propertiesStrategy) {
        if (log.isDebugEnabled()) {
            log.debug("Current Hystrix plugins configuration is ["
                    + "concurrencyStrategy [" + this.delegate + "]," + "eventNotifier ["
                    + eventNotifier + "]," + "metricPublisher [" + metricsPublisher + "],"
                    + "propertiesStrategy [" + propertiesStrategy + "]," + "]");
            log.debug("Registering Sleuth Hystrix Concurrency Strategy.");
        }
    }

    @Override
    public