HystrixRequestContext實現Request級別的上下文


一、簡介

  在微服務架構中,我們會有這樣的需求,A服務調用B服務,B服務調用C服務,ABC服務都需要用到當前用戶上下文信息(userId、orgId等),那么如何實現呢?
方案一: 攔截器加上ThreadLocal實現,但是如果在這次請求中創建了一個新的線程就拿不到了,也就是無法跨線程傳遞數據。
方案二: 使用攔截器加上 HystrixRequestContext 這個 request level 的 context實現,即保存到HystrixRequestContext中的數據在整個請求中都能訪問。

二、使用

2.1代碼示例

首先需要在pom文件引入依賴hystrix

<dependency>
      <groupId>com.netflix.hystrix</groupId>
      <artifactId>hystrix-core</artifactId>
      <version>1.5.12</version>
    </dependency>

保存上下文信息的對象ServiceContextHolder

package cn.sp.context;

import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault;

/** * Created by 2YSP on 2019/7/28. */
public class ServiceContextHolder {

  private static final HystrixRequestVariableDefault<ServiceContext> context = new HystrixRequestVariableDefault<>();


  public static ServiceContext getServiceContext() {
    initServiceContext();
    return context.get();
  }

  public static void setServiceContext(ServiceContext serviceContext) {
    initServiceContext();
    context.set(serviceContext);
  }

  private static void initServiceContext() {
    if (!HystrixRequestContext.isCurrentThreadInitialized()) {
      HystrixRequestContext.initializeContext();
    }
  }

  public static void destroy() {
    if (HystrixRequestContext.isCurrentThreadInitialized()) {
      HystrixRequestContext.getContextForCurrentThread().shutdown();
    }
  }
}

ServiceContextInterceptor的作用是將請求頭中的userId保存到上下文對象中。

@Slf4j
public class ServiceContextInterceptor extends HandlerInterceptorAdapter {


  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    initServiceContext(request, request.getRequestURL().toString());
    return true;
  }

  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    ServiceContextHolder.destroy();
  }

  private void initServiceContext(HttpServletRequest request, String url) {
    ServiceContext serviceContext = new ServiceContext();
    String userId = request.getHeader("userId");
    serviceContext.setUserId(Long.valueOf(userId));
    ServiceContextHolder.setServiceContext(serviceContext);
  }
}

添加攔截器配置

@Configuration
@EnableWebMvc
@Import(value = {RestResponseBodyAdvice.class})
public class MvcConfig implements WebMvcConfigurer {

  @Bean
  public ServiceContextInterceptor getServiceContextInterceptor() {
    return new ServiceContextInterceptor();
  }

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(getServiceContextInterceptor()).addPathPatterns("/request-context/**");
  }
 
}

用於測試的RequestContextTestController

@RestController
@RequestMapping("request-context")
@Slf4j
public class RequestContextTestController {

  @RequestMapping(value = "test", method = RequestMethod.GET)
  public String test() {
    System.out.println("請求的用戶id:" + ServiceContextHolder.getServiceContext().getUserId() + "");

    HystrixContextRunnable runnable =
        new HystrixContextRunnable(() -> {
          //從新的線程中獲取當前用戶id
          ServiceContext context = ServiceContextHolder.getServiceContext();
          System.out.println("新線程的用戶id:" + context.getUserId());
          context.setUserId(110L);
        });

    new Thread(runnable).start();

    try {
      Thread.sleep(100);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return ServiceContextHolder.getServiceContext().getUserId() + "";
  }
}

注意: 只有使用HystrixContextRunnableHystrixContextCallable創建線程才能在線程間傳遞數據,JDK自帶的是無效的。

2.2測試

使用postman發送請求

 

請求示例
請求示例

請求頭中的userId是22,返回結果卻變成110,說明在新線程中改變了ServiceContextHolder中保存的userId。

 

控制台日志如下:

請求的用戶id:22
2019-08-31 14:25:29.787 [http-nio-80-exec-1] WARN  c.n.c.sources.URLConfigurationSource - No URLs will be polled as dynamic configuration sources.
2019-08-31 14:25:29.787 [http-nio-80-exec-1] INFO  c.n.c.sources.URLConfigurationSource - To enable URLs as dynamic configuration sources, define System property archaius.configurationSource.additionalUrls or make config.properties available on classpath.
2019-08-31 14:25:29.798 [http-nio-80-exec-1] INFO  c.n.config.DynamicPropertyFactory - DynamicPropertyFactory is initialized with configuration sources: com.netflix.config.ConcurrentCompositeConfiguration@a6f6807
新線程的用戶id:22

說明新線程也能獲取到ServiceContextHolder中的數據,這種又是怎么實現的呢?下面介紹原理。

三、原理

上下文信息其實是保存在HystrixRequestVariableDefault類型的變量中,所以先看看這個類的源碼。
HystrixRequestVariableDefault是HystrixRequestVariable接口的實現類,HystrixRequestVariable接口表示request level的屬性,僅提供了get()來獲取屬性。

public interface HystrixRequestVariable<T> extends HystrixRequestVariableLifecycle<T> {

    public T get();

}

HystrixRequestVariableDefault和ThreadLocal一樣,提供了 T get() 和 set(T value) 兩個工具方法。

public class HystrixRequestVariableDefault<T> implements HystrixRequestVariable<T> {
    static final Logger logger = LoggerFactory.getLogger(HystrixRequestVariableDefault.class);

    @SuppressWarnings("unchecked")
    public T get() {
        if (HystrixRequestContext.getContextForCurrentThread() == null) {
            throw new IllegalStateException(HystrixRequestContext.class.getSimpleName() + ".initializeContext() must be called at the beginning of each request before RequestVariable functionality can be used.");
        }
        // 拿到當前線程的存儲結構,以自己為key索引數據
        ConcurrentHashMap<HystrixRequestVariableDefault<?>, LazyInitializer<?>> variableMap = HystrixRequestContext.getContextForCurrentThread().state;

        // short-circuit the synchronized path below if we already have the value in the ConcurrentHashMap
        LazyInitializer<?> v = variableMap.get(this);
       ...
    }

   
   
    public void set(T value) {
    // 拿到當前線程的存儲結構,以自己為key來存儲實際的數據。
        HystrixRequestContext.getContextForCurrentThread().state.put(this, new LazyInitializer<T>(this, value));
    }
    
}

set/get方法都調用了HystrixRequestContext的方法完成的,HystrixRequestContext的部分源碼如下:

public class HystrixRequestContext implements Closeable {


   //每個線程的ThreadLocal將保存HystrixRequestVariableState
    private static ThreadLocal<HystrixRequestContext> requestVariables = new ThreadLocal<HystrixRequestContext>();

    // 當前線程是否初始化了HystrixRequestContext
    public static boolean isCurrentThreadInitialized() {
        HystrixRequestContext context = requestVariables.get();
        return context != null && context.state != null;
    }

    // 從當前線程獲取HystrixRequestContext
    public static HystrixRequestContext getContextForCurrentThread() {
        HystrixRequestContext context = requestVariables.get();
        if (context != null && context.state != null) {
         
            return context;
        } else {
            return null;
        }
    }

    public static void setContextOnCurrentThread(HystrixRequestContext state) {
        requestVariables.set(state);
    }

     // 在每個請求開始的時候調用此方法,創建一個HystrixRequestContext,並與當前線程關聯
    public static HystrixRequestContext initializeContext() {
        HystrixRequestContext state = new HystrixRequestContext();
        requestVariables.set(state);
        return state;
    }


 ConcurrentHashMap<HystrixRequestVariableDefault<?>, HystrixRequestVariableDefault.LazyInitializer<?>> state = new ConcurrentHashMap<HystrixRequestVariableDefault<?>, HystrixRequestVariableDefault.LazyInitializer<?>>();

   

}

可以看出實際數據是存儲在state這個ConcurrentHashMap中的,每個線程關聯一個HystrixRequestContext,每個HystrixRequestContext有個Map結構存儲數據,key就是HystrixRequestVariableDefault。

如何實現request level context?

HystrixContextRunnable源碼如下:

// HystrixContextRunnable是個Runnable,一個可用於執行的任務
public class HystrixContextRunnable implements Runnable {

    private final Callable<Void> actual;
    private final HystrixRequestContext parentThreadState;

    public HystrixContextRunnable(Runnable actual) {
        this(HystrixPlugins.getInstance().getConcurrencyStrategy(), actual);
    }
    
    public HystrixContextRunnable(HystrixConcurrencyStrategy concurrencyStrategy, final Runnable actual) {
        // 獲取當前線程的HystrixRequestContext
        this(concurrencyStrategy, HystrixRequestContext.getContextForCurrentThread(), actual);
    }

    // 關鍵的構造器
    public HystrixContextRunnable(final HystrixConcurrencyStrategy concurrencyStrategy, final HystrixRequestContext hystrixRequestContext, final Runnable actual) {
        
        // 將原始任務Runnable包裝成Callable, 創建了一個新的callable
        this.actual = concurrencyStrategy.wrapCallable(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                actual.run();
                return null;
            }
        });
        // 存儲當前線程的hystrixRequestContext
        this.parentThreadState = hystrixRequestContext;
    }

    @Override
    public void run() {
        // 運行實際的Runnable之前先保存當前線程已有的HystrixRequestContext
        HystrixRequestContext existingState = HystrixRequestContext.getContextForCurrentThread();
        try {
            // 設置當前線程的HystrixRequestContext,來自上一級線程,因此兩個線程是同一個HystrixRequestContext
            HystrixRequestContext.setContextOnCurrentThread(parentThreadState);
            try {
                actual.call();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } finally {
            // 還原當前線程的HystrixRequestContext
            HystrixRequestContext.setContextOnCurrentThread(existingState);
        }
    }
}


代碼地址


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM