Shiro & Java8 parallelStream & Dubbo Filter 引发线上问题

背景描述

需求对Dubbo Provider的返回的数据做一层数据过滤
即Dubbo Provider 接口返回的数据必须和Web层用户登录的信息相同
实现方案比较简单 使用Dubbo官方提供的Filter 机制即可

实现方案

使用Dubbo官方提供的Filter 机制即可
在Web层自定义Dubbo Filter 过滤Dubbo Provider返回的结果信息
然后获取用户上下文信息 比对返回结果的数据是否和用户上下文一致

问题描述

当使用Java8 parallerStream调用Dubbo Provider时会发生用户下文信息错乱的问题
当Controller中使用parallerStream并行调用Dubbo Provider接口时
Dubbo Filter过滤Provider返回的结果时 发现和当前用户上下文不一致

问题排查&定位

  • 首先排查Dubbo Filter中获取用户上下文的方式
    • 项目使用的是Shiro框架 其获取用户上下文代码如下
      • org.apache.shiro.SecurityUtils#getSubject
         public static Subject getSubject() {
                Subject subject = ThreadContext.getSubject();
                if (subject == null) {
                    subject = (new Subject.Builder()).buildSubject();
                    ThreadContext.bind(subject);
                }
                return subject;
            }
        
    • 看到这里可知Shiro将用户上下文存放在线程上下文ThreadLocal中

      ? 其实当时看到这段代码我是很疑惑的 因为parallerStream是基于ForkJoinPooll来实现的
      如果使用了parallerStream 那么在Dubbo Filter我们获取到的用户上下文应该是空的
      因为Shiro框架是运行在Tomcat 线程中的 ForkJoinPoll的线程池肯定和Tomcat的线程池不是同一个

    • 带上疑惑继续跟一下Shiro的源码
      • org.apache.shiro.util.ThreadContext#getSubject
      • org.apache.shiro.util.ThreadContext#get
      • org.apache.shiro.util.ThreadContext#getValue
         private static Object getValue(Object key) {
               //  存储用户上下文的容器找到了 resource
                Map<Object, Object> perThreadResources = resources.get();
                return perThreadResources != null ? perThreadResources.get(key) : null;
            }
        
      • org.apache.shiro.util.ThreadContext#resources
            private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();
        
      • 进入resources 看看它的实现
        private static final class InheritableThreadLocalMap<T extends Map<Object, Object>> extends InheritableThreadLocal<Map<Object, Object>> {
                 ...
            }
        
    • Shiro使用ThreadLocal子类InheritableThreadLocal来存放用户上下文

      ? 为什么InheritableThreadLocal会导致并行流中ForkJoinPoll池中的线程能获取到用户上下文呢

    • 带着疑问查看InheritableThreadLocalMap的源码
      public class InheritableThreadLocal<T> extends ThreadLocal<T> {
          public InheritableThreadLocal() {
          }
      
          protected T childValue(T parentValue) {
              return parentValue;
          }
      
          ThreadLocalMap getMap(Thread t) {
              return t.inheritableThreadLocals;
          }
      
          void createMap(Thread t, T firstValue) {
              t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
          }
      }
      

      只是简单重写了下几个方法但是这个java.lang.Thread#inheritableThreadLocals引起了注意
      java.lang.Thread#inheritableThreadLocals 是Thread中的变量
      右键使用Find Usages查看那些地方使用了该变量Shiro & Java8 parallelStream & Dubbo Filter 引发线上问题_第1张图片
      主要关注Value write 那些地方对变量进行了赋值
      同样的方法init方法是创建线程时的初始化方法

  • 分析java.lang.InheritableThreadLocal在线程创建时作用是什么
    • init方法418-420行

      java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)

      	// 如果 parent.inheritableThreadLocals 不为空 则将父线程的inheritableThreadLocals赋值给当前线程的inheritableThreadLocals
      			if (inheritThreadLocals && parent.inheritableThreadLocals != null)
      			            this.inheritableThreadLocals =
      			                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
      
    • init方法374行

              //父线程就是调用new Thread(...)构造方法的线程
              Thread parent = currentThread();
      
    • 说明Java8 parallerStream使用的ForkJoinPoll池中的线程创建时继承Tomcat线程池中的InheritableThreadLocal即用户上下文信息

问题原因

  • 至此疑惑解开了

    为什么InheritableThreadLocal会导致并行流中ForkJoinPoll池中的线程能获取到用户上下文?
    当使用Java8 parallerStream调用Dubbo Provider时
    此时Dubbo Filter运行在ForkJoinPoll中 当前线程就是ForkJoinPoll中线程
    当取获取用户上下文时 就可能获取到错误的用户上下文信息

  • 这里还有一个细节即 Java8 parallerStream使用的ForkJoinPoll是在什么时候创建的 ?

    如果直接使用集合.parallerStream…
    第一次使用就会创建ForkJoinPoll 并且在同一个JVM进程中共享同一个

                  // java.util.concurrent.ForkJoinPool#common
    		      /**
    			     * Common (static) pool. Non-null for public use unless a static
    			     * construction exception, but internal usages null-check on use
    			     * to paranoically avoid potential initialization circularities
    			     * as well as to simplify generated code.
    			     */
     			   static final ForkJoinPool common;
    
  • Java8 parallelStream和ForkJoinPoll的原理可点击参考这篇作者写的文章

问题解决

  • 方案1 禁用Java8 parallelStream 调用Dubbo Provider

    是强制不让使用倒是满足业务需求 但是ForkJoinPoll线程池
    它的工作窃取算法 和 每个Workder 一个 Queue 在并行网络请求上表现不错

  • 方案2 在Dubbo Filter中 添加如下代码
             //并行流不进行用户上下文过滤逻辑
            if(Thread.currentThread() instanceof ForkJoinWorkerThread){
                  return invoker.invoke(invocation);
            }
    

    只是做了兼容方法 并不能过滤并行流调用Dubbo Provider 业务上是所有Dubbo Provider接口的返回结果都要过滤

  • 方案3
    • 在Dubbo Filter中不直接使用Shiro获取用户上下文
    • 将UserContext作为一个对象封装到所有Dubbo Provider接口参数中
      可封装一个BaseDubboRequestParam
    • 然后在Dubbo Filter中从接口参数中中获取UserContext

    最合理方案 但是涉及各个项目组都要改动 影响大 难以推动

你可能感兴趣的:(Shiro,Java8,parallelStream,Dubbo,Filter,Dubbo)