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