项目总结|Feign远程调用和异步调用丢失请求头问题

前言

最近一直在梳理之前做过的项目,想到之前遇到过的一个问题,场景是这样的,在我提交订单时,需要查询用户的地址信息和购物车被勾选的购物项,这样的话,我需要调用两个服务,一个是会员服务,一个是购物车服务。由于用户登陆信息是在整个系统共享的(这里采用分布式session解决),所以我在提交订单的时,调用购物车服务的时候,购物车服务的拦截器会拦截请求,判断用户是否登录。这时候请求头丢失,导致购物车服务拦截器返回用户未登录,但实际上是已经登录过的。还有一个问题是异步调用的时候,老请求线程不共享的问题,导致我业务中获取不到老请求报空指针异常的问题

远程调用出现的问题及解决方案

在分布式项目中,发送请求大致就两种,一种是浏览器访问,第二种是服务与服务之间通过OpenFeign远程调用。浏览器发送请求时,它会带上请求头的信息的,所以不会导致cookie丢失,这样用户真实登录的情况下不会判断未登录的异常情况。深入源码发现,Feign会重新创建一个request,这个请求是没有任何请求头的,这个请求模板会遍历请求拦截器的apply方法来丰富这个请求模板
在这里插入图片描述
看到这个地方就有办法解决了,解决方案就是,我写了一个feign拦截器,这里面注入了一个RequestInterceptor的对象,它是一个接口,我重写了它的apply方法,在里面拿到老请求中的请求头信息,放到这个新的请求模板里,我这里更新的是cookie。来看下代码:

@Configuration
public class GuliFeignConfig {

    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {

        RequestInterceptor requestInterceptor = new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //1、使用RequestContextHolder拿到刚进来的请求数据
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

                if (requestAttributes != null) {
                    //老请求
                    HttpServletRequest request = requestAttributes.getRequest();

                    if (request != null) {
                        //2、同步请求头的数据(主要是cookie)
                        //把老请求的cookie值放到新请求上来,进行一个同步
                        String cookie = request.getHeader("Cookie");
                        template.header("Cookie", cookie);
                    }
                }
            }
        };
        return requestInterceptor;
    }
}

这样每次进行feign远程调用的时候都需要走到这里丰富一下请求模板,带上cookie,这样用户登录信息就能正常的判断已登录问题。
这个问题就得到解决了。我画了个整体的流程图:
在这里插入图片描述

异步调用出现的问题及解决方案

前面已经说了场景了,我这个service里有两个查询任务,一个是去会员服务中查询会员的详细地址信息,一个是购物车服务中查询购物车勾选的购物项信息,这里我做了步优化,将这两个任务丢到我在这个模块下创建的线程池里,让其异步处理,这里用到CompletableFuture异步编排的功能,这样吞吐量就会有所提升。但是问题来了,在我获取老请求的时候,出现空指针的问题,我们来看看获取老请求是怎么获取的。
在这里插入图片描述
再深入源码看看,这个RequestContextHolder是怎么存储的请求数据

private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
@Nullable
public static RequestAttributes getRequestAttributes() {
    RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
    if (attributes == null) {
        attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
    }

    return attributes;
}

这个requestAttributesHolder其实就是个ThreadLocal,ThreadLocal是线程内部共享的,但是跨线程就是完全不同的数据了,我这里就是,当我将两个任务丢到线程池中创建其他的线程执行的话,它是拿不到我主线程的请求信息的,所以就导致了获取值为空的现象。
我是这么来解决的,来看下我的代码

 /**
     * 订单确认页返回需要用的数据
     * @return
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {

        //构建OrderConfirmVo
        OrderConfirmVo confirmVo = new OrderConfirmVo();

        //获取当前用户登录的信息
        MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();

        //TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        //开启第一个异步任务
        CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //1、远程查询所有的收获地址列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
            confirmVo.setMemberAddressVos(address);
        }, threadPoolExecutor);

        //开启第二个异步任务
        CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //2、远程查询购物车所有选中的购物项
            List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
            confirmVo.setItems(currentCartItems);
            //feign在远程调用之前要构造请求,调用很多的拦截器
        }, threadPoolExecutor).thenRunAsync(() -> {
            List<OrderItemVo> items = confirmVo.getItems();
            //获取全部商品的id
            List<Long> skuIds = items.stream()
                    .map((itemVo -> itemVo.getSkuId()))
                    .collect(Collectors.toList());

            //远程查询商品库存信息
            R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
            List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});

            if (skuStockVos != null && skuStockVos.size() > 0) {
                //将skuStockVos集合转换为map
                Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(skuHasStockMap);
            }
        },threadPoolExecutor);

        //3、查询用户积分
        Integer integration = memberResponseVo.getIntegration();
        confirmVo.setIntegration(integration);

        //4、价格数据自动计算

        //TODO 5、防重令牌(防止表单重复提交)
        //为用户设置一个token,三十分钟过期时间(存在redis)
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(token);


        CompletableFuture.allOf(addressFuture,cartInfoFuture).get();

        return confirmVo;
    }

在这里插入图片描述
每一个线程都来共享之前的这个请求数据。问题得到解决。来画张图来看一下这个整体的问题
在这里插入图片描述

  • 7
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值