1. 程式人生 > >記一場由於錯誤使用final宣告方法和CGLib引發的問題

記一場由於錯誤使用final宣告方法和CGLib引發的問題

前言:

近期發現某系統在業務高峰時,訪問量回突然下降,開發測試並抓包後發現是呼叫獲取城市列表功能時發生異常所致。下面是發現和解決問題的大概步驟:

1:分析程式碼,發現出問題的介面查詢的所有資料均已快取到redis中,然後查系統日誌,發現在這個時間點有大量的redis訪問超時。

2:問題比較清晰,大概率是redis調用出現問題,初期懷疑可能為redis連線數不足導致。

3:觀測redis連線數的確偏小,改大後測試,問題沒有解決。

4:繼續分析線上各類指標,發現在此時生產cpu等指標會上升。

5:在排除掉其他環境配置可能引起的問題後。最終判斷,程式碼層面可能有問題,仔細調查後發現某同事將呼叫redis的封裝介面getAll()錯誤的使用了final宣告,而我們系統的代理方式為CGLib。(下面會分析具體原因)

6:將final去掉:,發版測試後問題解決。

正文:

一:出問題部分的程式碼如下,該介面封裝的比較深,平時很難發現,這也是我們在工作用會遇到的一個問題,介面或者基礎包封裝的比較多的話開發業務邏輯會比較方便,但是一旦封裝的介面不夠好或者架構有問題就會產生一些隱藏的bug

//以下程式碼為虛擬碼:為防侵權,刪除了部分實現,僅留有相關邏輯部分
public final List<T> getAll() { // 取得全表快取
		List<T> allPoList = null;
		try {
			allPoList = "查詢redis“//此處注意
		} catch (Exception e) {
		//此處格外注意,此處allCacheKey賦值處在建構函式中,這裡的賦值失效,allCacheKey為空
			allPoList = Optional.ofNullable(jedisUtil.smembers(jedisPool, allCacheKey));
		}
		if (CollectionUtils.isEmpty(allPoList)) {
			return refreshAll();//此處格外注意
		} else {
			return allPoList;
		}
	}
	
protected List<T> refreshAll() {
	delCacheByKey(allCacheKey);//此處刪除所有key對應的快取
	selectDB();//查詢資料庫所有該快取表的資料
	jedisUtil.set();//將所有資料快取到快取中
	return allPoList;
	});
    }

值得注意的是,該方法用final宣告,由於CGLib由於是採用動態建立子類的方法,對於final方法,無法進行代理由於使用final方法不能直接呼叫代理類,所以呼叫到了被代理類,而allCacheKey賦值給了代理類,所以每次呼叫getAll()時都會走如下流程:查詢redis->資料為空->刪除redis快取->查庫並返回查詢資料結果給呼叫方->把資料更新到redis,由於資料庫和redis配置都比較高,在平時常態訪問量下一般不會暴露出問題,但是當流量比較大的情況下頻繁重新整理redis,自然扛不住。

二:對於在建構函式中賦值,代理類中為空的情況我也做了分析,分析過程為手動模擬CGLib實現方式

public static void main(String[] args) {
        DBQueryProxy dbQueryProxy = new DBQueryProxy();
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\");
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(DBQuery.class);
        enhancer.setCallback(dbQueryProxy);
        DBQuery dbQuery = (DBQuery) enhancer.create();
        System.out.println(dbQuery.getElement("Hello"));
        System.out.println();
        System.out.println(dbQuery.getAllElements());
        System.out.println(dbQuery.sayHello());
        System.out.println(dbQuery.a);
    }

生成的代理類中建構函式如下:可以看到賦值時被代理類中的值為空

public DBQuery$$EnhancerByCGLIB$$6dcb339a() {
        CGLIB$BIND_CALLBACKS(this);
    }
  private static final void CGLIB$BIND_CALLBACKS(Object var0) {
        DBQuery$$EnhancerByCGLIB$$6dcb339a var1 = (DBQuery$$EnhancerByCGLIB$$6dcb339a)var0;
        if (!var1.CGLIB$BOUND) {
            var1.CGLIB$BOUND = true;
            Object var10000 = CGLIB$THREAD_CALLBACKS.get();
            if (var10000 == null) {
                var10000 = CGLIB$STATIC_CALLBACKS;
                if (CGLIB$STATIC_CALLBACKS == null) {
                    return;
                }
            }

            var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0];
        }

    }

另外:關於代理方式上,一般有兩種jdk和cglib,在jdk1.6以前,由於cglib是基於fastclass機制,可以直接到用到方法無需像jdk的反射式呼叫。要快一些,但是jdk1.7和1.8時一直在對動態代理進行優化,所以效能和速度已經不是選擇動態代理方式的標準了,僅以需要為主。