這是一個真實案例,本周在工作中發現的,案例情況比較極端,因此顯得很滑稽很搞笑。但是深入一下,還是有些東西值得思考。
先來看這個案例,在性能優化的過程中,通過thread dump發現有非常多的線程都在執行同一個數據庫訪問。而按照分析,在cache開啟的情況下應該只訪問一次才是,后面的數據庫訪問都是不應該的。
隨即跟蹤到問題代碼:
//1. get pk as method parameter
public TrafficProfile createTrafficProfile(
long serviceCapabilityPrimaryKey, String serviceProviderId,
String applicationId) throws NotFoundException {
// 2. do database query to get serviceCapabilityProfile by pk
ServiceCapabilityProfile serviceCapabilityProfile = new ServiceCapabilityProfilePreLoadFullSerializableImpl(getContext(),
serviceCapabilityPrimaryKey);
// 3. generate key using obj serviceCapabilityProfile
String key = buildTrafficProfileCacheKey(serviceProviderId, applicationId, serviceCapabilityProfile);
TrafficProfile trafficProfile = (TrafficProfile) trafficProfileCache.get(key);
//5. found in cache and return
if ((trafficProfile != null)) {
return trafficProfile;
}
trafficProfile = new TrafficProfilePreLoadFullSerializableImpl(getContext(), serviceCapabilityProfile,
serviceProviderId, applicationId);
trafficProfileCache.put(key, trafficProfile);
return trafficProfile;
}
//4. notice: in fact only pk is used
private String buildTrafficProfileCacheKey(String serviceProviderId, String applicationId,
ServiceCapabilityProfile serviceCapabilityProfile) {
return serviceCapabilityProfile.getServiceCapabilityPrimaryKey() + "," + serviceProviderId + ","
+ applicationId;
}
因此可以看到,如果cache有效,我們其實只需要一個pk就可以組合出key從而從cache中得到保存的
trafficProfile對象。但是現在在我們的代碼中,為了得到key,我們進行了一個從pk -> serviceCapabilityProfile 對象的數據庫查詢,而在使用這個serviceCapabilityProfile 對象的函數中,很驚訝的發現,其實這里真正用到的不過是一個pk而且,而這個pk我們本來就持有,何須去數據庫里跑一回?
pk ----> get serviceCapabilityProfile from database by pk ---> get pk by serviceCapabilityProfile.getServiceCapabilityPrimaryKey();
讓我們來看看為什么會犯下如此可笑的錯誤,隨即在這個類中我們找到了另外一個createTrafficProfile():
// parameter is serviceCapabilityProfile obj
public TrafficProfile createTrafficProfile(
ServiceCapabilityProfile serviceCapabilityProfile,
String serviceProviderId, String applicationId)
throws NotFoundException {
// pass to buildTrafficProfileCacheKey() is obj, not pk
String key = buildTrafficProfileCacheKey(serviceProviderId, applicationId, serviceCapabilityProfile);
現在原因就很清楚了:在方法buildTrafficProfileCacheKey()中,實際只需要一個long類型的pk值,但是在它的方法參數定義中,它卻要求傳入一個serviceCapabilityProfile 的對象。
可以想象一下這個代碼開發的過程:
1. 第一個人先增加了以serviceCapabilityProfile對象為參數的createTrafficProfile()方法
2. 他創建了buildTrafficProfileCacheKey()方法,因為手頭就有serviceCapabilityProfile對象,因此他選擇了將整個對象傳入
3. 這兩個函數工作正常,雖然這個參數傳遞的有點感覺不大好,但至少沒有造成問題
4. 后來,另外一個人來修改這個代碼,他添加了使用long serviceCapabilityPrimaryKey的createTrafficProfile()方法
5. 他試圖調用buildTrafficProfileCacheKey()方法,然后發現這個方法需要一個serviceCapabilityProfile 對象
6. 他不得不進行一次數據庫訪問來獲取整個對象數據......
從這個案例中,我們可以看到,一個含糊的參數是如何導致我們最終犯錯的 ^0^
這個錯誤的修改當然非常簡單,將buildTrafficProfileCacheKey()方法的參數調整為傳入long類型的pk就解決了問題。
在日常代碼中,我們有非常多的大對象諸如“****DTO/context/profile”,而它們經常被作為參數在代碼之間傳遞。因此需要小心:
1. 當定義一個類似buildTrafficProfileCacheKey()的方法時
盡量將接口的參數簡單化,如果我們確認只是需要使用到某個大對象的一兩個簡單屬性,請將方法定義為簡單類型,不需要傳入整個對象。
或者在方法上通過javadoc說明我們只需要這個對象的某個或某幾個屬性。
2. 當調用類似buildTrafficProfileCacheKey()的方法時
需要稍微謹慎一些,進去目標方法,看看代碼實現,到底是需要什么數據,是否真的需要整個對象從而導致我們需要進行數據庫查詢這種的重量級操作。
例如上面的例子,如果原有buildTrafficProfileCacheKey()的方法不容許修改,那么我們大可以new 一個serviceCapabilityProfile 對象,然后setPK()來解決,比訪問數據庫快捷多了。
前面提到說這個案例有點"極端",這里的極端指的是buildTrafficProfileCacheKey()方法本身就在這個類之中,代碼量也非常少,意圖非常明確,本來應該很容易被發現的。因此犯錯的情況顯得比較可笑,但是我們推開來想一想,問題似乎沒有這么簡單了:如果buildTrafficProfileCacheKey()中的代碼比較復雜,可能還通過調用其他的類從而將對serviceCapabilityProfile對象的時候的代碼邏輯轉移,惡劣的情況下可能還有多層調用,甚至出現接口抽象實際代碼運行時注入等復雜場景,再假設我們沒有辦法直接看到最終的使用代碼,我們無法知道原來底層只是需要一個pk而已!那么這個問題就一點都不可笑,上面這個白白訪問一次數據庫的錯誤一定會再次發生,因為上層調用者不知道到底需要什么數據,只好整個對象全給!何況通常上層都有良好的代碼封裝,通過一個pk獲取一個對象這種事情,可能只需要一兩行代碼調用就搞定,于是我們很可能輕松自如的,一腳踩進坑里!
所以說想復雜點問題就變得嚴峻起來:底層代碼的實現者,需要如何設計接口參數,才能準確的告知上層調用者,到底哪些數據是真實需要的?上面的案例中將參數簡單的簡化為只傳入一個pk值就明確的達到了目標,對調用者來說足夠清晰明確。但是我們考慮一下復雜場景:如果底層的實現邏輯沒有這么簡單明確,底層代碼的實現者可能擔心未來的實現邏輯會發生更改,比如需要serviceCapabilityProfile的其他數據,因此為了保持接口穩定,底層代碼的實現者一定會傾向于使用serviceCapabilityProfile對象作為參數從而保留未來不需要修改接口/函數定義就可以擴展的自由。不經意間,挖了一個坑...
我們似乎又回到了原來犯錯的軌道中,那個看似搞笑的錯誤似乎又在對我們揮手微笑......
只是現在,我頗有點笑不起來了:下一次,如果我面對一個函數/接口,要求傳入一個大對象,我手頭只有一個pk,還有一個現成的函數可以一行代碼就搞定查詢,我要如何才能擋住誘惑?