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查看那些地方使用了該變量在這裏插入圖片描述
      主要關注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

    最合理方案 但是涉及各個項目組都要改動 影響大 難以推動

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章