记一次异步代码ThreadLocal导致的问题

背景

项目中有很多微服务,当一个 http 请求A服务并且带了一些header信息,而A服务需要调用B、C服务,B、C服务也需要获取这些header信息,这种场景在项目中非常常见,为了再调用其它服务时让这些header自动传递到下一个服务,我们写了一个全局的feign RequestInterceptor,在拦截器中通过RequestContextHolder去获取当前请求的header信息,并将这些header信息设置到调用下游服务的接口的header头中。

@Bean
RequestInterceptor globalRequestInterceptor() {
    return (template) -> {
        WHITE_LIST_HEADER_FIELDS.forEach((headerField) -> {
            String headerValue = this.getHeader(headerField);
            if (!template.headers().containsKey(headerField) && Objects.nonNull(headerValue)) {
              template.header(headerField, new String[]{headerValue});
            }
        });
    };
}
      
private String getHeader(String header) {
    HttpServletRequest request = this.getHttpServletRequest();
    return request == null ? null : request.getHeader(header);
}

private HttpServletRequest getHttpServletRequest() {
    ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
    return attributes == null ? null : attributes.getRequest();
}
复制代码

问题

一个 http 请求过来A服务的a1接口,a1接口有一部分代码逻辑a2是异步的,在异步的a2方法中需要调用B服务。当通过feign调用B服务,走到RequestInterceptor之时,线上环境却偶现获取不到header了。

@Service
public class Service1 {

  @Autowired
  private Service2 service2;

  public void a1() {
    // some sync code
    service2.a2();
    //some sync code
  }
}
复制代码
@Service
public class Service2 {

  @Autowired
  private BClient bClient;

  @Async
  public void a2(){
    // do some thing
    bClient.callB1();
  }
}
复制代码
public interface BClient {

  @PostMapping("/b1")
  void callB1();
}
复制代码

分析

首先看到获取RequestAttributes的代码RequestContextHolder.getRequestAttributes(),通过debug发现是在这里获取到的RequestAttributes是null。

进到RequestContextHolder源码可以看到实际上是把requestAttributesHolder或者inheritableRequestAttributesHolder的value返回了,而这两个其实是threadlocal对象,源码的注释也告诉了我们这个方法其实会放回绑定了当前线程的requestAttributes,这就好理解了,由于是threadlocal对象线程之间隔离了,那在子线程中自然就获取不到requestAttribute了。

WeChat827aff220bc509eafee0249c81518745.png

既然这样最直接的办法在主线程代码获取到header,再通过传参的方式一层一层传到feign client就可以了。但这样做的话每个异步方法都要写类似的代码感觉有点太过麻烦。 再想了想我们让每个子线程执行前去获取主线程的RequestAtribute然后设置到子线程不就好了,直接使用ThreadPoolTaskExecutorsetTaskDecorator的setTaskDecorator来进行装饰就可以了。

    threadPoolTaskExecutor.setTaskDecorator((runnable)->{
      final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
      return () -> {
        try {
          RequestContextHolder.setRequestAttributes(requestAttributes);
          runnable.run();
        } finally {
          RequestContextHolder.resetRequestAttributes();
        }
      };
    });
复制代码

似乎大功告成了,commit代码本地测试没问题,然而神奇的事情发生了,bug从稳定出现变成了偶现。

看来事情没有这么简单,继续跟踪源码发现实际上spring在RequestContextFilter类的doFilterInternal方法中进行了RequestAttributes的初始化以及销毁。

    @Override
	protected void doFilterInternal(
			HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
		//初始化ServletRequestAttributes到threadlocal
		initContextHolders(request, attributes);

		try {
			filterChain.doFilter(request, response);
		}
		finally {
		    //remove掉ThreadLocal<RequestAttributes>
			resetContextHolders();
			if (logger.isTraceEnabled()) {
				logger.trace("Cleared thread-bound request context: " + request);
			}
			attributes.requestCompleted();
		}
	}
复制代码

看到这里就明白了,主线程执行完后对ThreadLocal执行了remove操作,当子线程获取RequestAttribute的时候主线程已经执行完了,自然就获取不到了RequestAttribute了。

那在setTaskDecorator的时,将主线程获取到的RequestAttribute拷贝一份再设置到子线程不就好了,这下总没有什么问题了吧,再次自信commit。

很幸运bug再次偶现,但这次不是RequestAttribute获取不到了,而是header变成了null,经过一顿操作,终于发现了其中蹊跷。

tomcat的Http11InputBuffer有个recycle方法,当http请求连接关闭的时候就会去调用,recycle方法里面又会去调用Request的recycle方法,在这个方法里面就会去清除掉contentLength、headers 之类的信息了

    /**
     * Recycle the input buffer. This should be called when closing the
     * connection.
     */
    void recycle() {
        wrapper = null;
        request.recycle();

        for (int i = 0; i <= lastActiveFilter; i++) {
            activeFilters[i].recycle();
        }

        byteBuffer.limit(0).position(0);
        lastActiveFilter = -1;
        parsingHeader = true;
        swallowInput = true;

        headerParsePos = HeaderParsePosition.HEADER_START;
        parsingRequestLine = true;
        parsingRequestLinePhase = 0;
        parsingRequestLineEol = false;
        parsingRequestLineStart = 0;
        parsingRequestLineQPos = -1;
        headerData.recycle();
    }

复制代码
    public void recycle() {
        bytesRead=0;

        contentLength = -1;
        contentTypeMB = null;
        charset = null;
        characterEncoding = null;
        expectation = false;
        headers.recycle();
        trailerFields.clear();
        serverNameMB.recycle();
        serverPort=-1;
        localAddrMB.recycle();
        localNameMB.recycle();
        localPort = -1;
        remoteAddrMB.recycle();
        remoteHostMB.recycle();
        remotePort = -1;
        available = 0;
        sendfile = true;

        serverCookies.recycle();
        parameters.recycle();
        pathParameters.clear();

        uriMB.recycle();
        decodedUriMB.recycle();
        queryMB.recycle();
        methodMB.recycle();
        protoMB.recycle();

        schemeMB.recycle();

        remoteUser.recycle();
        remoteUserNeedsAuthorization = false;
        authType.recycle();
        attributes.clear();

        listener = null;
        allDataReadEventSent.set(false);

        startTime = -1;
    }
复制代码

request-recycle.jpg

找到原因了,在这里的话有两个解决思路,一个就是将RequestAttribute对象进行深拷贝再存到子线程的。 另一个思路既然我们现在只需要header的信息,直接在主线程将header信息取出来,自定义一个HeaderContextHolder,再设置到子线程的threadlocal就可以了,我们在拦截器中获取header的时候,先去HeaderContextHolder去取header信息(能取到说明当前线程为主线程),如果HeaderContextHolder没取到再去spring的RequestContextHolder去获取heder。这里我们用第二种思路。

@Slf4j
public class ContextTaskDecorator implements TaskDecorator {

  /**
   * Bind the given RequestAttributes to the current thread
   */
  @Override
  public Runnable decorate(Runnable runnable) {
    //组装 header map
    Map<String, String> map = wrapHeaderMap();

    return () -> {
      try {
        HeaderContextHolder.setContext(map);
        runnable.run();
      } finally {
        HeaderContextHolder.removeContext();
      }
    };
  }
}
复制代码
public class HeaderContextHolder {

  private HeaderContextHolder() {
  }

  private static final ThreadLocal<Map<String, String>> CTX = new ThreadLocal<>();

  public static void setContext(Map<String, String> map) {
    CTX.set(map);
  }

  public static void removeContext() {
    CTX.remove();
  }

  public static String getHeader(String key) {
    if (isEmpty()) {
      return null;
    }
    return CTX.get().get(key);
  }

  public static boolean isEmpty() {
    return MapUtils.isEmpty(CTX.get());
  }


}
复制代码

到此总算解决问题了。

猜你喜欢

转载自juejin.im/post/5ecb9b5ee51d4578853d1f92