异步调用引发的RequestAttributes不一致问题
在实际工作过程中,我们经常会遇到某些功能或者需求,他的实时性要求不高,从而选择通过另起一个线程的方式去实现,以防止主流程的阻塞,带给用户更好的体验。
但是在实际使用的过程中,常常会遇到各种各样的问题,例如线程安全,变量丢失等;
这里我描述下我在实际开发中遇到的问题:
需求描述:
实现提醒用户有新的优惠券可领的功能
实现方案:
方案1:
使用数据库实现,创建一张表用来记录每一张优惠券对应每一个用户的状态,当用户查看以后修改记录状态;
优势:层次很细,细到用户对每一张优惠券都有一个记录,能完全实现需求,且能持久化;
劣势:需要使用到数据库,创建一张新的表,开发量大,后期表数据量比较大,数据记录基本是优惠券数量*用户数量;
方案2:
利用redis的hashMap数据结构实现,当有新的优惠券产生时,设置用户对应的优惠券状态为true,点击查看以后修改为false;
优势:操作速度快,开发效率快,基本能实现需求;
劣势:无法做到像上面一样针对每一张优惠券,如果需要控制优惠券过期时,取消原有的提醒就无法做到
这里我选择方案是第二种,因为业务本身,并不需要那么细,只要起到提醒的作用就好,容错率高,即使提醒不对,点进去也没关系;
实现过程中遇到的问题
在实现过程中我都选择了异步去处理这些逻辑;
创建时,异步的对用户添加状态
CompletableFuture.runAsync(() -> {
Result<List<Long>> result = userFeignService.findAllUserId();
Map<Long,Integer> couponFlagMap= Maps.newHashMap();
Optional.ofNullable(result.getData()).orElse(new ArrayList<>()).forEach(userId->couponFlagMap.put(userId,1));
RedisTemplateUtil.hPutAll(RedisKeyConstantUser.USER_COUPON_MESSAGE_KEY,couponFlagMap);
}, commonExecutorService);
这里采用异步的方式主要是因为查询用户的操作可能存在耗时,为防止影响主业务,所以采用这种方式;
在实际调试过程中,发现当我异步调用的时候,发现我日志中设置的trceId没有带过去,导致查看日志的时候无法将这部分的请求日志跟主线程中的对应起来,这是为什么呢?
下面是我的feign拦截器代码
@Component
@Slf4j
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (null != requestAttributes) {
//追加mdc
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
String requestNo = MDC.get("requestNO");
if (StringUtils.isNotBlank(requestNo)) {
requestTemplate.header("request-no", requestNo);
}
}
}
}
调试的时候发现这里拿到的requestAttributes一直是空,进入getRequestAttributes()方法查看
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");
@Nullable
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
if (attributes == null) {
attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
}
return attributes;
}
发现RequestAttributes是从requestAttributesHolder或者inheritableRequestAttributesHolder中获取到的,然而这两变量的类型都是ThreadLocal,这个类我们很熟悉啊,是针对当前线程的局部变量,那么原因就找到了,是由于线程引发的线程变量不一致的问题;因为我们外部的主线程和里面的线程不是同一个,所以导致这里获取不到RequestAttributes;
所以当我们在另一个服务里面通过RequestAttributes去获取请求中携带的信息时获取不到任何内容,因为这个RequestAttributes是一个新的,并没有把原来的例如token,requestNo等数据携带过来,导致后续的一些业务处理产生不可预知的问题;
解决方案:
自己手动设置,将主线程的数据先拿出来,然后在请求之前手动设置进去;例如
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
String requestNo = MDC.get("requestNO");
CompletableFuture.runAsync(() -> {
MDC.put("requestNO", requestNo);
RequestContextHolder.setRequestAttributes(requestAttributes);
}, commonExecutorService);
当我们在使用多线程的方式进行接口调用的时候,这个尤其需要注意。