Code Review 方法論與實(shí)踐總結(jié)梳理
引言
作者:方基成(潤(rùn)甫)
作為卓越工程文化的一部分,Code Review 其實(shí)一直在進(jìn)行中,只是各團(tuán)隊(duì)根據(jù)自身情況張馳有度,松緊可能也不一,這里簡(jiǎn)單梳理一下 CR 的方法和團(tuán)隊(duì)實(shí)踐。
為什么要CR
- 提前發(fā)現(xiàn)缺陷在 CodeReview 階段發(fā)現(xiàn)的邏輯錯(cuò)誤、業(yè)務(wù)理解偏差、性能隱患等時(shí)有發(fā)生, CR 可以提前發(fā)現(xiàn)問(wèn)題。
- 提高代碼質(zhì)量主要體現(xiàn)在代碼健壯性、設(shè)計(jì)合理性、代碼優(yōu)雅性等方面,持續(xù) CodeReview 可以提升團(tuán)隊(duì)整體代碼質(zhì)量。
- 統(tǒng)一規(guī)范和風(fēng)格集團(tuán)編碼規(guī)范自不必說(shuō),對(duì)于代碼風(fēng)格要不要統(tǒng)一,可能會(huì)有不同的看法,個(gè)人觀點(diǎn)對(duì)于風(fēng)格也不強(qiáng)求。但代碼其實(shí)不是寫(xiě)給自己看的,是寫(xiě)給下一任看的,就像經(jīng)常被調(diào)侃的“程序員不喜歡寫(xiě)注釋,更不喜歡別人不寫(xiě)注釋”,代碼風(fēng)格的統(tǒng)一更有助于代碼的可讀性及繼任者的快速上手。
- 防止架構(gòu)腐爛架構(gòu)的維護(hù)者是誰(shuí)??jī)H靠架構(gòu)師或應(yīng)用 Owner 是遠(yuǎn)遠(yuǎn)不夠的,需要所有成員的努力,所謂人人都是架構(gòu)師。架構(gòu)防腐最好前置在設(shè)計(jì)階段,但 CodeReview 作為對(duì)最終產(chǎn)出代碼的檢查,也算是最后一道關(guān)鍵工序。
- 知識(shí)分享每一次 CodeReview,都是一次知識(shí)的分享,磨合一定時(shí)間后,團(tuán)隊(duì)成員間會(huì)你中有我、我中有你,集百家之所長(zhǎng),融百家之所思。同時(shí),業(yè)務(wù)邏輯都在代碼中,團(tuán)隊(duì) CodeReview 也是一種新人業(yè)務(wù)細(xì)節(jié)學(xué)習(xí)的途徑。
- 團(tuán)隊(duì)共識(shí)通過(guò)多次討論與交流,逐步達(dá)成團(tuán)隊(duì)共識(shí),特別是對(duì)架構(gòu)理解和設(shè)計(jì)原則的認(rèn)知,在共識(shí)的基礎(chǔ)上團(tuán)隊(duì)也會(huì)更有凝聚力,特別是在較多新人加入時(shí)尤為重要。
他山之石
2.1 某大廠A
非常重視 Code Review,基本上代碼需要至少有兩位以上 Reviewer 審核通過(guò)后,才會(huì)讓你 Check In。
2.1.1 代碼評(píng)審準(zhǔn)則
- 如果變更達(dá)到可以提升系統(tǒng)整體代碼質(zhì)量的程度,就可以讓它們通過(guò),即使它們可能還不完美。這是所有代碼評(píng)審準(zhǔn)則的最高原則。
- 世界上沒(méi)有“完美”的代碼,只有更好的代碼。評(píng)審者不應(yīng)該要求代碼提交者在每個(gè)細(xì)節(jié)都寫(xiě)得很完美。評(píng)審者應(yīng)該做好修改時(shí)間與修改重要性之間的權(quán)衡。
2.1.2 代碼評(píng)審原則
- 以客觀的技術(shù)因素與數(shù)據(jù)為準(zhǔn),而非個(gè)人偏好。
- 在代碼樣式上,遵從代碼樣式指南,所有代碼都應(yīng)與其保持一致,任何與代碼樣式指南不一致的觀點(diǎn)都是個(gè)人偏好。但如果某項(xiàng)代碼樣式在指南中未提及,那就接受作者的樣式。
- 任務(wù)涉及軟件設(shè)計(jì)的問(wèn)題,都應(yīng)取決于基本設(shè)計(jì)原則,而不應(yīng)由個(gè)人喜好來(lái)決定。當(dāng)同時(shí)有多種可行方案時(shí),如果作者能證明(以數(shù)據(jù)或公認(rèn)的軟件工程原理為依據(jù))這些方案基本差不多,那就接受作者的選項(xiàng);否則,應(yīng)由標(biāo)準(zhǔn)的軟件設(shè)計(jì)原則為準(zhǔn)。
- 如果沒(méi)有可用的規(guī)則,那么審核者應(yīng)該讓作者與當(dāng)前代碼庫(kù)保持一致,至少不會(huì)惡化代碼系統(tǒng)的質(zhì)量。(一旦惡化代碼質(zhì)量,就會(huì)帶來(lái)破窗效應(yīng),導(dǎo)致系統(tǒng)的代碼質(zhì)量逐漸下降)
2.1.3 代碼審核者應(yīng)該看什么
設(shè)計(jì):代碼是否設(shè)計(jì)良好?這種設(shè)計(jì)是否適合當(dāng)前系統(tǒng)?
功能:代碼實(shí)現(xiàn)的行為與作者的期望是否相符?代碼實(shí)現(xiàn)的交互界面是否對(duì)用戶友好?
復(fù)雜性:代碼可以更簡(jiǎn)單嗎?如果將來(lái)有其他開(kāi)發(fā)者使用這段代碼,他能很快理解嗎?
測(cè)試:這段代碼是否有正確的、設(shè)計(jì)良好的自動(dòng)化測(cè)試?
命名:在為變量、類名、方法等命名時(shí),開(kāi)發(fā)者使用的名稱是否清晰易懂?
注釋:所有的注釋是否都一目了然?
代碼樣式:所有的代碼是否都遵循代碼樣式?
文檔:開(kāi)發(fā)者是否同時(shí)更新了相關(guān)文檔?
2.2 某大廠B
在開(kāi)發(fā)流程上專門(mén)有這個(gè)環(huán)節(jié),排期會(huì)明確排進(jìn)日程,比如5天開(kāi)發(fā)會(huì)排2天來(lái)做代碼審核,分為代碼自審、交叉審核、集中審核。
有明確的量化指標(biāo),如8人時(shí)審核/每千行代碼,8個(gè)以上非提示性有效問(wèn)題/每千行代碼。
2.3 某大廠C
推行 Code Owner 機(jī)制,每個(gè)代碼變更必須有 Code Owner 審核通過(guò)才可以提交。
所有的一線工程師,無(wú)論職級(jí)高低,最重要的工程輸出原則是“show me the code”,而 Code Review 是最能夠反應(yīng)這個(gè)客觀輸出的。
盡量讓每個(gè)人的 Code Review 參與狀況都公開(kāi)透明,每個(gè)變更發(fā)送給項(xiàng)目合作者,及轉(zhuǎn)發(fā)到小組內(nèi)成員,小組內(nèi)任何人都可以去 Review 其他人的代碼。
明確每個(gè)人的考評(píng)和 Code Review 表現(xiàn)相關(guān),包括 Code Review 輸出狀況及提交代碼的質(zhì)量等。
我們?cè)趺醋?CR
3.1 作為代碼提交者
- 發(fā)起時(shí)機(jī):發(fā)起 Code Review 盡量提前,開(kāi)發(fā)過(guò)程小步快跑
代碼行數(shù):提交 Code Review 的代碼行數(shù)最好在400行以下。根據(jù)數(shù)據(jù)分析發(fā)現(xiàn),從代碼行數(shù)來(lái)看,超過(guò)400行的 CR,缺陷發(fā)現(xiàn)率會(huì)急劇下降;從 CR 速度來(lái)看,超過(guò)500行/小時(shí)后,Review 質(zhì)量也會(huì)大大降低,一個(gè)高質(zhì)量的 CR 最好控制在一個(gè)小時(shí)以內(nèi)。
明確意圖:編寫(xiě)語(yǔ)義明確的標(biāo)題(必填)和描述(選填,可以包括背景、思路、改造點(diǎn)和影響面、風(fēng)險(xiǎn)等)
善用工具:IDEA 打開(kāi)編碼規(guī)約實(shí)時(shí)檢測(cè),減少代碼樣式、編碼規(guī)約等基礎(chǔ)性問(wèn)題
阿里編碼規(guī)約插件: github.com/alibaba/p3c…
3.2 作為代碼評(píng)審者
3.2.1 評(píng)審范圍
主要從兩方面來(lái)評(píng)審:
代碼邏輯
- 功能完整:代碼實(shí)現(xiàn)是否滿足功能需求,實(shí)現(xiàn)上有沒(méi)有需求的理解偏差,對(duì)用戶是否友好;
- 邏輯設(shè)計(jì):是否考慮了全局設(shè)計(jì)和兼容現(xiàn)有業(yè)務(wù)細(xì)節(jié),是否考慮邊界條件和并發(fā)控制;
- 安全隱患:是否存在數(shù)據(jù)安全隱患及敏感信息泄漏,如越權(quán)、SQL注入、CSRF、敏感信息未脫敏等;
- 性能隱患:是否存在損害性能的隱患,如死鎖、死循環(huán)、FullGC、慢SQL、緩存數(shù)據(jù)熱點(diǎn)等;
- 測(cè)試用例:?jiǎn)卧獪y(cè)試用例的驗(yàn)證邏輯是否有效,測(cè)試用例的代碼行覆蓋率和分支覆蓋率;
代碼質(zhì)量
- 編碼規(guī)范:命名、注釋、領(lǐng)域術(shù)語(yǔ)、架構(gòu)分層、日志打印、代碼樣式等是否符合規(guī)范
- 可讀性:是否邏輯清晰、易理解,避免使用奇巧技,避免過(guò)度拆分
- 簡(jiǎn)潔性:是否有重復(fù)可簡(jiǎn)化的復(fù)雜邏輯,代碼復(fù)雜度是否過(guò)高,符合KISS和DRY原則
- 可維護(hù)性:在可讀性和簡(jiǎn)潔性基礎(chǔ)上,是否分層清晰、模塊化合理、高內(nèi)聚低耦合、遵從基本設(shè)計(jì)原則
- 可擴(kuò)展性:是否僅僅是滿足一次性需求的代碼,是否有必要的前瞻性擴(kuò)展設(shè)計(jì)
- 可測(cè)試性:代碼是否方便寫(xiě)單元測(cè)試及分支覆蓋,是否便于自動(dòng)化測(cè)試
3.2.2 評(píng)審注意事項(xiàng)
- 盡快完成評(píng)審
- 避免過(guò)度追求完美
- 明確評(píng)論是否要解決
- 避免使用反問(wèn)句來(lái)評(píng)價(jià)
我們主要是通過(guò)交叉 CR、集中 CR 相結(jié)合的方式,由應(yīng)用 Owner+SM+架構(gòu)師+TL完成。
CR 怎么避免流于形式
CR 流于形式的因素很多,大概如下:
不認(rèn)同 CodeReview
- 評(píng)審者的姿態(tài)?有沒(méi)有帶來(lái)好處?有沒(méi)有從中收獲?這些都會(huì)直觀影響團(tuán)隊(duì)成員的認(rèn)可度
- 每個(gè) Review 建議的提出都是一次思想交流,評(píng)論要友好、中肯、具體,避免教條式及負(fù)面詞匯,在遵守評(píng)審原則下,同時(shí)尊重個(gè)性展現(xiàn)
- 團(tuán)隊(duì)集中 CodeReview 盡量不要太正式和嚴(yán)肅,輕松的氣氛下更有助于互相理解,來(lái)點(diǎn)水果,聊聊業(yè)務(wù)聊聊代碼
- 在 Review 過(guò)程有時(shí)候會(huì)陷入誰(shuí)對(duì)誰(shuí)錯(cuò)的爭(zhēng)論,只要是為了尋求真理辯證的去看問(wèn)題,哪怕是討論再激烈也是有收獲的,注意只對(duì)事不對(duì)人。
CodeReview 后改動(dòng)太大
發(fā)布前發(fā)現(xiàn)問(wèn)題多,改動(dòng)太大,影響項(xiàng)目計(jì)劃
大項(xiàng)目要求編碼前設(shè)計(jì)評(píng)審,小需求可以事先Review設(shè)計(jì)思路,避免最后的驚喜
每次 Review 的代碼行數(shù)最好控制在數(shù)百行以內(nèi)
評(píng)審者沒(méi)有足夠時(shí)間
評(píng)審者在任務(wù)安排上盡量預(yù)留好時(shí)間
盡快評(píng)審,代碼在百行以內(nèi)及時(shí)響應(yīng),在千行以內(nèi)當(dāng)日完結(jié)
評(píng)審者不了解業(yè)務(wù)和代碼
代碼提交人編寫(xiě)清晰的標(biāo)題和描述
有必要的情況下評(píng)審者需要了解PRD
評(píng)審者需要提前了解系統(tǒng)和代碼
Review 建議未修改
這一點(diǎn)極為重要,需要對(duì)修改后的代碼再次 Review,確保理解一致,以及預(yù)防帶問(wèn)題上線
應(yīng)用可以設(shè)置 Review 建議需全部解決的卡點(diǎn),同時(shí)對(duì)于非必需修改的建議可以進(jìn)行打標(biāo)或說(shuō)明
CR 實(shí)踐中發(fā)現(xiàn)的幾個(gè)常見(jiàn)代碼問(wèn)題
筆者對(duì)個(gè)人 CR 評(píng)論問(wèn)題做了個(gè)大概統(tǒng)計(jì),Bug 發(fā)現(xiàn)數(shù)占比約4%(直接或潛在Bug),重復(fù)代碼數(shù)占比約5%,其他還有規(guī)范、安全、性能、設(shè)計(jì)等問(wèn)題。在CR代碼質(zhì)量時(shí),可以參考《重構(gòu):改善既有代碼的設(shè)計(jì)》,書(shū)中所列的22種壞味道在CR中基本都會(huì)遇到。而此處我們主要聚焦以下幾個(gè)常見(jiàn)問(wèn)題:
5.1 DRY
DRY 是 Don't Repeat Yourself 的縮寫(xiě),DRY 是 Andy Hunt 和 Dave Thomas's 在《 The Pragmatic Programmer 》一書(shū)中提出的核心原則。DRY 原則描述的重復(fù)是知識(shí)和意圖的重復(fù),包含代碼重復(fù)、文檔重復(fù)、數(shù)據(jù)重復(fù)、表征重復(fù),我們這里重點(diǎn)講講代碼重復(fù)。
5.1.1 代碼重復(fù)
《重構(gòu)》中對(duì)“Duplicated Code(重復(fù)代碼)”的描述:壞味道行列中首當(dāng)其沖的就是Duplicated Code。如果你在一個(gè)以上的地點(diǎn)看到相同的程序結(jié)構(gòu),那么可以肯定:設(shè)法將它們合而為一,程序會(huì)變得更好。
最單純的Duplicated Code就是“同一個(gè)類的兩個(gè)函數(shù)含有相同的表達(dá)式”。這時(shí)候你需要做的就是采用Extract Method (110)提煉出重復(fù)的代碼,然后讓這兩個(gè)地點(diǎn)都調(diào)用被提煉出來(lái)的那一段代碼。
另一種常見(jiàn)情況就是“兩個(gè)互為兄弟的子類內(nèi)含相同表達(dá)式”。要避免這種情況,只需對(duì)兩個(gè)類都使用Extract Method (110),然后再對(duì)被提煉出來(lái)的代碼使用Pull Up Method (332),將它推入超類內(nèi)。如果代碼之間只是類似,并非完全相同,那么就得運(yùn)用Extract Method (110)將相似部分和差異部分割開(kāi),構(gòu)成單獨(dú)一個(gè)函數(shù)。然后你可能發(fā)現(xiàn)可以運(yùn)用Form Template Method (345)獲得一個(gè)Template Method設(shè)計(jì)模式。如果有些函數(shù)以不同的算法做相同的事,你可以選擇其中較清晰的一個(gè),并使用Substitute Algorithm (139)將其他函數(shù)的算法替換掉。
如果兩個(gè)毫不相關(guān)的類出現(xiàn)Duplicated Code,你應(yīng)該考慮對(duì)其中一個(gè)使用Extract Class (149),將重復(fù)代碼提煉到一個(gè)獨(dú)立類中,然后在另一個(gè)類內(nèi)使用這個(gè)新類。但是,重復(fù)代碼所在的函數(shù)也可能的確只應(yīng)該屬于某個(gè)類,另一個(gè)類只能調(diào)用它,抑或這個(gè)函數(shù)可能屬于第三個(gè)類,而另兩個(gè)類應(yīng)該引用這第三個(gè)類。你必須決定這個(gè)函數(shù)放在哪兒最合適,并確保它被安置后就不會(huì)再在其他任何地方出現(xiàn)。
代碼重復(fù)的幾種場(chǎng)景:
- 一個(gè)類中重復(fù)代碼抽象為一個(gè)方法
- 兩個(gè)子類間重復(fù)代碼抽象到父類
- 兩個(gè)不相關(guān)類間重復(fù)代碼抽象到第三個(gè)類
CASE:
反例
private BillVO convertBillDTO2BillVO(BillDTO billDTO) { if (billDTO == null) { return null; } BillVO billVO = new BillVO(); Money cost = billDTO.getCost(); if (cost != null && cost.getAmount() != null) { billVO.setCostDisplayText(String.format("%s %s", cost.getCurrency(), cost.getAmount())); } Money sale = billDTO.getSale(); if (sale != null && sale.getAmount() != null) { billVO.setSaleDisplayText(String.format("%s %s", sale.getCurrency(), sale.getAmount())); } Money grossProfit = billDTO.getGrossProfit(); if (grossProfit != null && grossProfit.getAmount() != null) { billVO.setGrossProfitDisplayText(String.format("%s %s", grossProfit.getCurrency(), grossProfit.getAmount())); } return billVO; }
正例
private static final String MONEY_DISPLAY_TEXT_PATTERN = "%s %s"; private BillVO convertBillDTO2BillVO(BillDTO billDTO) { if (billDTO == null) { return null; } BillVO billVO = new BillVO(); billVO.setCostDisplayText(buildMoneyDisplayText(billDTO.getCost())); billVO.setSaleDisplayText(buildMoneyDisplayText(billDTO.getSale())); billVO.setGrossProfitDisplayText(buildMoneyDisplayText(billDTO.getGrossProfit())); return billVO; } private String buildMoneyDisplayText(Money money) { if (money == null || money.getAmount() == null) { return StringUtils.EMPTY; } return String.format(MONEY_DISPLAY_TEXT_PATTERN, money.getCurrency(), money.getAmount().toPlainString()); }
5.1.2 DYR 實(shí)踐忠告:
- 不要借用 DRY 之名,過(guò)度提前抽象,請(qǐng)遵循 Rule of three 原則。
- 不要過(guò)度追求 DRY,破壞了內(nèi)聚性,實(shí)踐中需要平衡復(fù)用與內(nèi)聚。
5.2 Primitive Obsession
《重構(gòu)》中對(duì)“Primitive Obsession(基本類型偏執(zhí))”的描述:大多數(shù)編程環(huán)境都有兩種數(shù)據(jù):結(jié)構(gòu)類型允許你將數(shù)據(jù)組織成有意義的形式;基本類型則是構(gòu)成結(jié)構(gòu)類型的積木塊。結(jié)構(gòu)總是會(huì)帶來(lái)一定的額外開(kāi)銷(xiāo)。它們可能代表著數(shù)據(jù)庫(kù)中的表,如果只為做一兩件事而創(chuàng)建結(jié)構(gòu)類型也可能顯得太麻煩。
對(duì)象的一個(gè)極大的價(jià)值在于:它們模糊(甚至打破)了橫亙于基本數(shù)據(jù)和體積較大的類之間的界限。你可以輕松編寫(xiě)出一些與語(yǔ)言內(nèi)置(基本)類型無(wú)異的小型類。例如,Java 就以基本類型表示數(shù)值,而以類表示字符串和日期——這兩個(gè)類型在其他許多編程環(huán)境中都以基本類型表現(xiàn)。
對(duì)象技術(shù)的新手通常不愿意在小任務(wù)上運(yùn)用小對(duì)象——像是結(jié)合數(shù)值和幣種的 money 類、由一個(gè)起始值和一個(gè)結(jié)束值組成的 range 類、電話號(hào)碼或郵政編碼(ZIP)等的特殊字符串。你可以運(yùn)用 Replace Data Valuewith Object (175)將原本單獨(dú)存在的數(shù)據(jù)值替換為對(duì)象,從而走出傳統(tǒng)的洞窟,進(jìn)入炙手可熱的對(duì)象世界。如果想要替換的數(shù)據(jù)值是類型碼,而它并不影響行為,則可以運(yùn)用 Replace Type Code with Class (218)將它換掉。如果你有與類型碼相關(guān)的條件表達(dá)式,可運(yùn)用Replace Type Codewith Subclass (213)或Replace Type Code with State/Strategy (227)加以處理。
如果你有一組應(yīng)該總是被放在一起的字段,可運(yùn)用 Extract Class(149)。如果你在參數(shù)列中看到基本型數(shù)據(jù),不妨試試 IntroduceParameter Object (295)。如果你發(fā)現(xiàn)自己正從數(shù)組中挑選數(shù)據(jù),可運(yùn)用 Replace Array with Object (186)。
給我們的啟示主要有兩點(diǎn):
- 大部分業(yè)務(wù)場(chǎng)景和語(yǔ)言環(huán)境下,結(jié)構(gòu)化類型導(dǎo)致的開(kāi)銷(xiāo)基本可以忽略
- 結(jié)構(gòu)化類型帶來(lái)更清晰的語(yǔ)義和復(fù)用
CASE:
反例
@Data public class XxxConfigDTO implements Serializable { private static final long serialVersionUID = 8018480763009740953L; /** * 租戶ID */ private Long tenantId; /** * 工商稅務(wù)企業(yè)類型 */ private String companyType; /** * 企業(yè)名稱 */ private String companyName; /** * 企業(yè)納稅人識(shí)別號(hào) */ private String companyTaxNo; /** * 審單員工工號(hào) */ private String auditEmpNo; /** * 審單員工姓名 */ private String auditEmpName; /** * 跟單員工工號(hào) */ private String trackEmpNo; /** * 跟單員工姓名 */ private String trackEmpName; }
正例
@Data public class XxxConfigDTO2 implements Serializable { private static final long serialVersionUID = 8018480763009740953L; /** * 租戶ID */ private Long tenantId; /** * 企業(yè)信息 */ private Company company; /** * 審單員工信息 */ private Employee auditEmployee; /** * 跟單員工信息 */ private Employee trackEmployee; } @Data public class Company { /** * 工商稅務(wù)企業(yè)類型 */ private String companyType; /** * 企業(yè)名稱 */ private String companyName; /** * 企業(yè)納稅人識(shí)別號(hào) */ private String companyTaxNo; } @Data public class Employee { /** * 員工工號(hào) */ private String empNo; /** * 員工姓名 */ private String empName; }
其實(shí)就是怎么去抽象,對(duì)于特定領(lǐng)域的對(duì)象可以參考 DDD 里面的 Domain Primitive(DP)。
5.3 分布式鎖
5.3.1 未處理鎖失敗
private void process(String orderId) { // do validate try { boolean lockSuccess = lockService.tryLock(LockBizType.ORDER, orderId); if (!lockSuccess) { // TODO 此處需要處理鎖失敗,重試或拋出異常 return; } // do something } finally { lockService.unlock(LockBizType.ORDER, orderId); } }
分布式鎖的目的是為了防止并發(fā)沖突和保證數(shù)據(jù)一致性,鎖失敗時(shí)未處理直接返回,會(huì)帶來(lái)非預(yù)期結(jié)果的影響,除非明確失敗可放棄。
5.3.2 手寫(xiě)解鎖容易遺漏
上面的加鎖和解鎖都是手動(dòng)編寫(xiě),而這兩個(gè)動(dòng)作一般是成對(duì)出現(xiàn)的,在手動(dòng)編寫(xiě)時(shí)容易發(fā)生遺漏解鎖而導(dǎo)致線上問(wèn)題,推薦封裝一個(gè)加解鎖的方法來(lái)實(shí)現(xiàn),會(huì)更加安全和便利。
private void procoess(String orderId) { // do validate Boolean processSuccess = lockService.executeWithLock(LockBizType.ORDER, orderId, () -> doProcess(orderId)); // do something } private Boolean doProcess(String orderId) { // do something return Boolean.TRUE; } // LockService public <T> T executeWithLock(LockBizType bizType, String bizId, Supplier<T> supplier) { return executeWithLock(bizType, bizId, 60, 3, supplier); } public <T> T execteWithLock(LockBizType bizType, String bizId, int expireSeconds, int retryTimes, Supplier<T> supplier) { // 嘗試加鎖 int lockTimes = 1; boolean lock = tryLock(bizType, bizId, expireSeconds); while(lockTimes < retryTimes && !lock) { try { Thread.sleep(10); } catch (Exception e) { // do something } lock = tryLock(bizType, bizId, expireSeconds); lockTimes++; } // 鎖失敗拋異常 if (!lock) { throw new LockException("try lock fail"); } // 解鎖 try { return supplier.get(); } finally { unlock(bizType, bizId); } }
5.3.3 加鎖 KEY 無(wú)效
private void process(String orderId) { // do validate try { // 此處加鎖類型與加鎖KEY不匹配 boolean lockSuccess = lockService.tryLock(LockBizType.PRODUCT, orderId); if (!lockSuccess) { // TODO 重試或拋出異常 return; } // do something } finally { lockService.unlock(LockBizType.PRODUCT, orderId); } }
注意加鎖類型與加鎖 KEY 在同一個(gè)維度,否則加鎖會(huì)失效。
5.4 分頁(yè)查詢
5.4.1 完全沒(méi)有分頁(yè)
反例
private List<OrderDTO> queryOrderList(Long customerId) { if (customerId == null) { return Lists.newArrayList(); } List<OrderDO> orderDOList = orderMapper.list(customerId); return orderConverter.doList2dtoList(orderDOList); }
正例
private Page<OrderDTO> queryOrderList(OrderPageQuery query) { Preconditions.checkNotNull(query, "查詢條件不能為空"); Preconditions.checkArgument(query.getPageSize() <= MAX_PAGE_SIZE, "分頁(yè)size不能大于" + MAX_PAGE_SIZE); // 分頁(yè)size一般由前端傳入 // query.setPageSize(20); long cnt = orderMapper.count(query); if (cnt == 0) { return PageQueryUtil.buildPageData(query, null, cnt); } List<OrderDO> orderDOList = orderMapper.list(query); List<OrderDTO> orderDTOList = orderConverter.doList2dtoList(orderDOList); return PageQueryUtil.buildPageData(query, orderDTOList, cnt); }
沒(méi)有分頁(yè)的列表查詢對(duì) DB 性能影響非常大,特別是在項(xiàng)目初期,因?yàn)閿?shù)據(jù)量非常小問(wèn)題不明顯,而導(dǎo)致沒(méi)有及時(shí)發(fā)現(xiàn),會(huì)給未來(lái)留坑。
5.4.2 分頁(yè) size 太大
反例
private Page<OrderDTO> queryOrderList2(OrderPageQuery query) { Preconditions.checkNotNull(query, "查詢條件不能為空"); query.setPageSize(10000); long cnt = orderMapper.count(query); if (cnt == 0) { return PageQueryUtil.buildPageData(query, null, cnt); } List<OrderDO> orderDOList = orderMapper.list(query); List<OrderDTO> orderDTOList = orderConverter.doList2dtoList(orderDOList); return PageQueryUtil.buildPageData(query, orderDTOList, cnt); }
分頁(yè) size 的大小并沒(méi)有一個(gè)固定的標(biāo)準(zhǔn),取決于業(yè)務(wù)需求、數(shù)據(jù)量及數(shù)據(jù)庫(kù)等,但動(dòng)輒幾千上萬(wàn)的分頁(yè) size,會(huì)帶來(lái)性能瓶頸,而大量的慢 SQL 不但影響客戶體驗(yàn),對(duì)系統(tǒng)穩(wěn)定性也是極大的隱患。
5.4.3 超多分頁(yè)慢 SQL
反例
<!-- 分頁(yè)查詢訂單列表 --> <select id="list" parameterType="com.xxx.OrderPageQuery" resultType="com.xxx.OrderDO"> SELECT <include refid="all_columns"/> FROM t_order <include refid="listConditions"/> ORDER BY id DESC LIMIT #{offset},#{pageSize} </select>
正例
<!-- 分頁(yè)查詢訂單列表 --> <select id="list" parameterType="com.xxx.OrderPageQuery" resultType="com.xxx.OrderDO"> SELECT <include refid="all_columns"/> FROM t_order a INNER JOIN ( SELECT id AS bid FROM t_order <include refid="listConditions"/> ORDER BY id DESC LIMIT #{offset},#{pageSize} ) b ON a.id = b.bid </select>
以上 bad case 的 SQL 在超多頁(yè)分頁(yè)查詢時(shí)性能極其低下,存在多次回表甚至 Using Filesort 的問(wèn)題,在阿里巴巴編碼規(guī)范中也有明確的規(guī)避方案,此處不展開(kāi)。
最后,我們工程師的智慧結(jié)晶都盡在代碼之中,而 Code Review 可以促進(jìn)結(jié)晶更加清瑩通透、純潔無(wú)瑕、精致完美,值得大家一起持續(xù)精進(jìn)!
以上就是Code Review 方法論與實(shí)踐總結(jié)梳理的詳細(xì)內(nèi)容,更多關(guān)于Code Review 方法論與實(shí)踐總結(jié)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript中內(nèi)存泄漏的介紹與教程(推薦)
內(nèi)存泄露是指一塊被分配的內(nèi)存既不能使用,又不能回收,直到瀏覽器進(jìn)程結(jié)束。下面這篇文章主要給的大家介紹了關(guān)于JavaScript中內(nèi)存泄漏的相關(guān)資料,文中介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-06-06JavaScript實(shí)現(xiàn)數(shù)組在指定位置插入若干元素的方法
這篇文章主要介紹了JavaScript實(shí)現(xiàn)數(shù)組在指定位置插入若干元素的方法,涉及javascript中splice方法的使用技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04微信小程序?qū)崙?zhàn)教程之WXS語(yǔ)法詳解
WXS是小程序的一套腳本語(yǔ)言,結(jié)合 WXML,wxs類似js和js又不一樣,好多js語(yǔ)法能在js使用不能在wxs使用可以構(gòu)建出頁(yè)面的結(jié)構(gòu),這篇文章主要給大家介紹了關(guān)于微信小程序?qū)崙?zhàn)教程之WXS語(yǔ)法詳解的相關(guān)資料,需要的朋友可以參考下2024-09-09JS實(shí)現(xiàn)簡(jiǎn)單打字測(cè)試
這篇文章主要為大家詳細(xì)介紹了JS實(shí)現(xiàn)簡(jiǎn)單打字測(cè)試,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06npm install報(bào)錯(cuò)無(wú)法創(chuàng)建packge.json文件的解決辦法
當(dāng)你在運(yùn)行 npm install 時(shí)遇到錯(cuò)誤,提示無(wú)法找到 package.json 文件,也沒(méi)有創(chuàng)建一個(gè) package.json 文件,只創(chuàng)建了一個(gè)package-lock.json文件,本文給大家介紹詳細(xì)的解決辦法,需要的朋友可以參考下2024-02-02webpack多頁(yè)面開(kāi)發(fā)實(shí)踐
這篇文章主要介紹了webpack多頁(yè)面開(kāi)發(fā)實(shí)踐,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12javascript代碼簡(jiǎn)寫(xiě)的幾種常用方式匯總
任何一種編程語(yǔ)言的簡(jiǎn)寫(xiě)小技巧都是為了幫助你寫(xiě)出更簡(jiǎn)潔、更完善的代碼,讓你用更少的編碼實(shí)現(xiàn)你的需求,這篇文章主要給大家介紹了關(guān)于javascript代碼簡(jiǎn)寫(xiě)的幾種常用方式,需要的朋友可以參考下2021-08-08