ThreadLocal引发的Bug

前言

最近同事在开发的时候,在调用RPC获取用户的积分的时候,发生了返回的用户以及积分信息不是我们请求的uid对应的用户信息的情况,uid变成了另一个人的uid,积分信息也变成了另一个人的,是不是很神奇。同事叫我帮忙查找原因,RPC和调用方的代码都是该同事写的,我在本地用他给的请求参数去请求那个RPC,发现确实会出现那么神奇的情况,返回的信息不是请求参数中的用户的信息。反复请求几次,依旧是这样。

问题确实很奇怪,但是可以肯定是被调用方代码有问题,然后拉下RPC提供方代码。

分析Bug

  • 查看源码

获取用户信息部分代码

  public UserBizInfo detail(UserBizInfo param) {
        // 校验
        userBizInfoValidation.detail(param);
        String uid = AppUserUtil.getUid();
        if (StringUtils.isNotBlank(uid)) {
            param.setUid(uid);
        } else {
            uid = param.getUid();
        }
   //...   
  }

public class AppUserUtil {

    private static ThreadLocal<String> local = new ThreadLocal<String>();

    public static void setUid(String uid) {
        local.set(uid);
    }

    public static String getUid() {
        return local.get();
    }
}

我们看到他这里的逻辑是先从当前线程的ThreadLocalMap中去获取key为local实例的Entry的value值。如果获取到了uid,就将param参数的uid设置为ThreadLocal中的vlaue。分析到这里其实就已经定位到问题了,就是这个ThreadLocal搞的鬼。我们知道我们的Web服务器在处理请求的时候肯定是要使用到线程池的。

当一个请求进入时,他向当前线程的ThreadLocalMap中添加了key为local这个ThreadLocal实例的Entry,value为一个用户的uid,然后当前请求结束,结束之前并没有remove掉这个Entry。

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // TODO Auto-generated method stub
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Object bean = handlerMethod.getBean();
        Method method = handlerMethod.getMethod();
        AppSecurityAuth bean_auth = bean.getClass().getAnnotation(AppSecurityAuth.class);
        AppSecurityAuth method_auth = method.getAnnotation(AppSecurityAuth.class);
        if ((bean_auth != null && !bean_auth.login_auth()) || (method_auth != null && !method_auth.login_auth())) {
            return true;
        }
        String uid = apiAuth.getUserId();
        if (StringUtils.isEmpty(uid)) {
            // 未获取uid,校验是否可直接访问
            throw ExceptionUtil.getAuthException("身份校验失败");
        }
        AppUserUtil.setUid(uid);
        return true;
    }

项目中是使用拦截器来设置Entry的,前台的接口都需要经过这个拦截器来获取用户的uid。

接着我们的RPC请求来了,注意,我们的RPC请求是不需要经过这个拦截器的,而恰好这个时候Web服务器的线程池中的线程已经满了,RPC请求复用了处理之前的请求的线程,RPC请求进入到detail方法中,String uid = AppUserUtil.getUid();获取到了之前请求设置的uid,导致了bug的发生。

  • 解决Bug
  public UserBizInfo detail(UserBizInfo param) {
        // 校验
        userBizInfoValidation.detail(param);
        String uid =param.getUid();
        if (StringUtils.isBlank(uid)) {
         uid=AppUserUtil.getUid();
        } 
    
   //...   
  }

Bug的解决很简单,先判断是否请求参数中带有uid,如果带有的话,证明是RPC请求,如果没有的话,证明是前台请求,从ThreadLocal中拿数据。如果都还没有拿到的话,就需要抛出异常了。

  • 为什么会发生这个Bug?

为什么会出现这个Bug呢,从这个Bug的表现来说,还挺唬人的,返回的用户信息居然变成了另一个人的用户信息。出现这个Bug的原因,主要还是对线程池的理解不够,或者说根本就没有考虑到请求线程复用的问题。

detail方法最开始只会被前台请求调用,由于每一个前台请求都会经过拦截器,在到达detail方法之前都会进行AppUserUtil.setUid(uid);,其实这个操作,就会把之前请求设置的uid给清掉,然后重新设置一个新的,这样才保证了detail方法拿到的uid都是当前请求设置的uid,如果不是在拦截器中每次都会重新设置uid,那么detail方法一开始就会有问题。而后来,另一个项目也需要获取用户积分了,同事打算提供一个RPC供新项目调用获取用户积分,这个时候他在兼容两种调用的时候,写的代码出了问题,先使用AppUserUtil.getUid();来获取uid,导致如果是RPC请求的话,在复用线程的时候,就会拿到之前的前台请求设置的uid,导致Bug。

  • 反思

这个Bug的出现是由于在兼容代码的时候,忽略了ThreadLocal在线程复用时候出现的问题,在有线程池的情况下,使用ThreadLocal是一定要小心的,在每次任务(线程池中的Task,在web服务器中就是一个请求)设置值之前都清掉之前任务设置的值,或者在任务结束前,清掉该任务设置的值。否则就会出现问题。

但是就我而言,我并不觉得这样兼容代码是一个很好的选择。我会新增一个方法专门给RPC调用使用,而不是继续使用detail方法,在这个方法上做兼容。

  public UserBizInfo rpcDetail(UserBizInfo param) {
        // 校验
        userBizInfoValidation.detail(param);
        String uid =param.getUid();
        if (StringUtils.isBlank(uid)) {
         	ExceptionUtil.getParamsException("uid不能为空");
        } 
    //获取用户信息逻辑,抽出一个方法,detail方法和rpcDetail方法公用
      getUserInfoDetail(param);
   //...   
  }
  public UserBizInfo detail(UserBizInfo param) {
        // 校验
        userBizInfoValidation.detail(param);
        param.setUid(AppUserUtil.getUid());
    //获取用户信息逻辑,抽出一个方法,detail方法和rpcDetail方法公用
          getUserInfoDetail(param);

   //...   
  }
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值