可擴(kuò)展性是衡量架構(gòu)設(shè)計(jì)的一個(gè)因素,也經(jīng)常被開(kāi)發(fā)者提到。但是,一個(gè)系統(tǒng)要設(shè)計(jì)出比較好的可擴(kuò)展性是有一定難度的,而且可擴(kuò)展性體現(xiàn)在不同層次上,有大的可擴(kuò)展性,也有小的可擴(kuò)展性,本文從可擴(kuò)展的本質(zhì)出發(fā),通過(guò)平時(shí)常用的框架來(lái)印證,最后通過(guò)實(shí)際案例說(shuō)明如何設(shè)計(jì)高可擴(kuò)展性系統(tǒng)。
一、可擴(kuò)展的本質(zhì)是什么?
可擴(kuò)展的意思是在面對(duì)變化時(shí),用最少的代價(jià)去實(shí)現(xiàn),平時(shí)我們聽(tīng)得最多的是面向抽象 (接口) 編程,如果只是把這里的抽象理解成接口,那么就有些狹隘了,抽象是通式通法,而接口只是其中一個(gè),所以在談可擴(kuò)展實(shí)現(xiàn)之前一定要講清楚可擴(kuò)展的本質(zhì)是什么,連本質(zhì)都不知道,怎么提出系統(tǒng)性解決方案。
1.1 擴(kuò)展的本質(zhì)
擴(kuò)展的本質(zhì)就是占位符,明確告訴你這里被占了,具體誰(shuí)占了不清楚。那么問(wèn)題來(lái)了:占位符到底是什么?它是怎么表達(dá)的?又要如何實(shí)現(xiàn)的?如果可以把這三個(gè)問(wèn)題理清楚,就可以想到很多可擴(kuò)展性方案,而不再是單一的面向接口編程。
占位符到底是什么:占位符僅僅是一個(gè)標(biāo)識(shí),標(biāo)志這里會(huì)有變化,一句話可以概括:凡是可以表達(dá)變化的就是占位符,然而具體的變化實(shí)現(xiàn)又沒(méi)有給出,真正體現(xiàn)了做什么和怎么做的分離。
占位符怎么表達(dá):要回答這個(gè)標(biāo)識(shí)是用什么來(lái)表達(dá),變量、接口、配置項(xiàng)…這些都可以表達(dá)占位符,變量能被賦值同一類(lèi)型的數(shù)據(jù);接口可以有不同的實(shí)現(xiàn);配置項(xiàng)也可以被賦予不同的值…所以,實(shí)現(xiàn)可擴(kuò)展的思路一下就打開(kāi)了。
如何實(shí)現(xiàn):再往深層次思考,實(shí)現(xiàn)一個(gè)接口,如何在執(zhí)行時(shí)動(dòng)態(tài)找到實(shí)現(xiàn)類(lèi)?如果把這個(gè)問(wèn)題想清楚,在實(shí)際中實(shí)現(xiàn)可擴(kuò)展又會(huì)有一套系統(tǒng)性解決方案。整個(gè)過(guò)程就兩點(diǎn):識(shí)別和執(zhí)行,識(shí)別的意思就是要找到對(duì)應(yīng)目標(biāo),接下來(lái)就是執(zhí)行。
綜上,到這里可能已經(jīng)有自己應(yīng)對(duì)可擴(kuò)展的方法,上面已經(jīng)給了從不同角度看可擴(kuò)展性的示例,接下來(lái)就是系統(tǒng)化提出應(yīng)對(duì)可擴(kuò)展的方法。
結(jié)論一:擴(kuò)展的本質(zhì)就是占位符,凡是可以表達(dá)變化的就是占位符。
1.2 應(yīng)對(duì)可擴(kuò)展的方法
先給出應(yīng)對(duì)可擴(kuò)展的方法:規(guī)范、識(shí)別、注冊(cè)、使用,這 4 點(diǎn)都是從上面可推導(dǎo)出來(lái)的,下面一一進(jìn)行詳細(xì)說(shuō)明。
規(guī)范:規(guī)范是從占位符推導(dǎo)出來(lái)的,既然是標(biāo)志有變化,一定要遵循一定的規(guī)范表達(dá),否則別人是不知道的,如接口,就是很直接地表達(dá)這里是有變化的,具體的實(shí)現(xiàn)還不知道;變量天然地表達(dá)這里是變化的數(shù)據(jù)。
識(shí)別:有了規(guī)范定義之后,接下來(lái)就是識(shí)別,之前為什么可擴(kuò)展一直對(duì)我們來(lái)講很虛,那是因?yàn)橐?guī)范和識(shí)別都是系統(tǒng)幫我們做的,我們只是知道而沒(méi)有真正實(shí)踐。規(guī)范是定義,識(shí)別是找出有哪些實(shí)現(xiàn)了規(guī)范。
注冊(cè):識(shí)別出來(lái)之后,就要把信息存儲(chǔ)起來(lái),可以存儲(chǔ)在本地,也可以存儲(chǔ)在遠(yuǎn)程,如果存儲(chǔ)在遠(yuǎn)程就是一個(gè)注冊(cè)的過(guò)程,這里的注冊(cè)就是存儲(chǔ)的意思。簡(jiǎn)單理解就是識(shí)別出來(lái)之后要集中管理。
使用:使用就很簡(jiǎn)單,找到具體實(shí)現(xiàn)并執(zhí)行邏輯處理。
上面四個(gè)單詞看起來(lái)簡(jiǎn)單,除了使用是終極目標(biāo)外,其它三個(gè)都是抽象的表達(dá),比如規(guī)范如何定義、怎么識(shí)別、如何注冊(cè)?通過(guò)上面的表述可以看到具體要怎么實(shí)踐,這里再總結(jié)下:
規(guī)范如何去定義:凡是可以表達(dá)變化的就能用它來(lái)定義,常見(jiàn)的有配置項(xiàng)、變量、接口、注解等;
怎么去識(shí)別:這個(gè)要具體去看如何定義規(guī)范,如配置項(xiàng)的變化有一個(gè)監(jiān)聽(tīng)變化;注解是要掃描類(lèi)來(lái)識(shí)別 annotation;
如何去注冊(cè):如果系統(tǒng)的交互只是一個(gè),那么存儲(chǔ)在本地就行,如果系統(tǒng)的交互是多個(gè),那么要注冊(cè)到一個(gè)注冊(cè)中心上去。
結(jié)論二:應(yīng)對(duì)可擴(kuò)展的方法:規(guī)范、識(shí)別、注冊(cè)、使用。
1.3 擴(kuò)展的經(jīng)典案例
此處使用一個(gè)經(jīng)典案例來(lái)說(shuō)明可擴(kuò)展性,并從其原理上印證上述方法。
在 Java 中,SPI 對(duì)于大部分人來(lái)講并不陌生,最典型的加載數(shù)據(jù)庫(kù)驅(qū)動(dòng)就是通過(guò) SPI 來(lái)實(shí)現(xiàn)的。如果你看了 SPI 的原理,再去看上面寫(xiě)的,會(huì)感覺(jué)兩個(gè)思路很相似。
SPI 有它的規(guī)范,要到指定目錄下加載對(duì)應(yīng)文件;找到文件后進(jìn)行解析、識(shí)別并加載;最后就是使用。整個(gè)流程能印證上面所提到的:規(guī)范、識(shí)別、注冊(cè)、使用。所以,方法的提出有一個(gè)點(diǎn)就是從具體案例中進(jìn)行抽象,提煉共性的東西,再去推演其它案例看能不能也滿足。
二、可擴(kuò)展性系統(tǒng)實(shí)踐之路
此處以?xún)?yōu)惠券業(yè)務(wù)平臺(tái)為例講解可擴(kuò)展性系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn),在上一篇文章中已經(jīng)講了優(yōu)惠券系統(tǒng)是一個(gè)平臺(tái)型的業(yè)務(wù)系統(tǒng),要做到業(yè)務(wù)與業(yè)務(wù)的隔離、業(yè)務(wù)與平臺(tái)的隔離。
2.1 識(shí)別變化
經(jīng)過(guò)整體分析之后,已經(jīng)確定大業(yè)務(wù)流程:建券、發(fā)券、用券、退券,以及對(duì)應(yīng)的子流程,接下來(lái)就是要分析出哪些內(nèi)容會(huì)變化。
比較明顯的變化就是領(lǐng)券、用券門(mén)檻的變化,因?yàn)椴煌瑯I(yè)務(wù)線有不同的限制條件,有的要限制不同人群,有的要限制領(lǐng)取次數(shù)…已經(jīng)認(rèn)別了變化接下來(lái)就是要處理這些變化。
結(jié)論三:找擴(kuò)展點(diǎn)就是找系統(tǒng)經(jīng)常變化的地方。
2.2 處理變化的常見(jiàn)手段
2.2.1 野蠻處理
一個(gè)最簡(jiǎn)單的處理,就是在代碼中寫(xiě) if else,它的特點(diǎn)是簡(jiǎn)單直接上線,不足的點(diǎn)長(zhǎng)期下去,系統(tǒng)會(huì)變得很難維護(hù)、可擴(kuò)展性較差。
if(productId = ProductEnum.A){ // 具體的處理 }else if(productId = ProductEnum.B){ // 具體的處理 }else if(productId = ProductEnum.C){ // 具體的處理 }else{ ...... }
這種代碼放到現(xiàn)在,很多系統(tǒng)還是這么做的,而且是在業(yè)務(wù)發(fā)展初期最喜歡用這種野蠻處理方式,搞上去就能直接上線,快速支持業(yè)務(wù)。
2.2.2 面向接口設(shè)計(jì)
對(duì)上面野蠻方式的一個(gè)常見(jiàn)處理就是面向接口設(shè)計(jì),抽象出一個(gè)限制條件檢查的接口,不同的業(yè)務(wù)線有對(duì)應(yīng)的實(shí)現(xiàn),通過(guò)配置指定業(yè)務(wù)線下所有的實(shí)現(xiàn),將這些實(shí)現(xiàn)放到一個(gè)映射中,在程序執(zhí)行過(guò)程中,通過(guò)業(yè)務(wù)線就可以執(zhí)行所有的接口實(shí)現(xiàn)類(lèi)并依次執(zhí)行。
List<Rule> ruleList = RuleFacotry.getByProductId(prodcutId);
這種方法比第一種明顯要好,體現(xiàn)了一定的可擴(kuò)展性,新加一個(gè)規(guī)則限制,重新實(shí)現(xiàn)接口就行,然后在配置項(xiàng)中加上這個(gè)新的實(shí)現(xiàn),代碼的改動(dòng)量也還好。它有一個(gè)明顯的問(wèn)題就是每次新加一個(gè)實(shí)現(xiàn)就要發(fā)布上線,有沒(méi)有辦法不發(fā)布上線就能滿足目的呢?有,就是下面提到的一類(lèi)可擴(kuò)展性設(shè)計(jì)的方法。
2.3 一類(lèi)可擴(kuò)展性設(shè)計(jì)的方法
再來(lái)明確一下目標(biāo):系統(tǒng)具備可擴(kuò)展性和不發(fā)布系統(tǒng)就能實(shí)現(xiàn)新增功能。
還是使用上面說(shuō)的方法:規(guī)范、認(rèn)別、注冊(cè)、使用,下面結(jié)合這個(gè)具體的案例來(lái)說(shuō)明。
規(guī)范:這里是用接口來(lái)作為規(guī)范描述限制條件,包含入?yún)⒑统鰠ⅲ@里有一個(gè)開(kāi)放平臺(tái),實(shí)現(xiàn)了一個(gè)接口后就可以提交代碼。
識(shí)別:在建優(yōu)惠券時(shí),會(huì)加載業(yè)務(wù)線有哪些業(yè)務(wù)規(guī)則實(shí)現(xiàn),在領(lǐng)取、使用時(shí)可以進(jìn)行配置選擇,此時(shí)只是插入一個(gè)變量標(biāo)識(shí)使用某個(gè)限制條件 (如限人群,這個(gè)實(shí)現(xiàn)的邏輯可能會(huì)變化,通過(guò)變量名來(lái)標(biāo)識(shí)變化)。
注冊(cè):系統(tǒng)在執(zhí)行的過(guò)程中,發(fā)現(xiàn)有限制條件的變量名,拿這個(gè)變量名從開(kāi)放平臺(tái)中拉取具體的實(shí)現(xiàn)存儲(chǔ)在本地 (有一個(gè)緩存時(shí)間,具體的過(guò)期時(shí)間依業(yè)務(wù)考慮,我們?nèi)〉氖?30 分鐘)。
執(zhí)行:拿到具體的實(shí)現(xiàn)后,依次執(zhí)行。
再整理下流程步驟,讓大家更進(jìn)一步掌握該設(shè)計(jì)方法:
在開(kāi)放平臺(tái)提交限制條件接口的實(shí)現(xiàn)代碼,有限制人群的實(shí)現(xiàn)、限制領(lǐng)取券次數(shù)…
在開(kāi)放平臺(tái)提交之后,會(huì)入庫(kù)存儲(chǔ),數(shù)據(jù)庫(kù)里會(huì)存儲(chǔ)一個(gè)業(yè)務(wù)線對(duì)應(yīng)的多個(gè)限制實(shí)現(xiàn)。
創(chuàng)建優(yōu)惠券時(shí),會(huì)加載業(yè)務(wù)下的限制規(guī)則,通過(guò)配置選擇具體要使用到的限制規(guī)則 (相同業(yè)務(wù)線下的不同優(yōu)惠券可以有不同的規(guī)則限制),配置選擇后,會(huì)在規(guī)范字段中存儲(chǔ)規(guī)則實(shí)現(xiàn)的 id(規(guī)則實(shí)現(xiàn)可能會(huì)變化,會(huì)有多次提交),所以這里存儲(chǔ)的是 id,在執(zhí)行的時(shí)候可以拿到這個(gè) id。
在領(lǐng)券、用券時(shí),會(huì)檢查規(guī)則限制有哪些,通過(guò) id 列表從遠(yuǎn)程開(kāi)放平臺(tái)拉取具體實(shí)現(xiàn),把 java 代碼拉下來(lái)之后就可以編譯,并存儲(chǔ)到本地或者集群緩存中。
最后就是執(zhí)行具體的實(shí)現(xiàn)邏輯。
結(jié)合這張圖看就會(huì)清晰很多,整體的業(yè)務(wù)平臺(tái)架構(gòu)比較清晰,分為開(kāi)放平臺(tái)、配置平臺(tái)、業(yè)務(wù)平臺(tái)和數(shù)據(jù)平臺(tái),一個(gè)新業(yè)務(wù)方接進(jìn)來(lái)很簡(jiǎn)單,簡(jiǎn)單配置下就可以使用。
三、小結(jié)
本篇文章主要講可擴(kuò)展性系統(tǒng)的設(shè)計(jì)與實(shí)現(xiàn),從可擴(kuò)展的本質(zhì)講起,可擴(kuò)展的本質(zhì)就是占位符,凡是可表達(dá)變化的都可以稱(chēng)之為占位符,常見(jiàn)的有變量、接口、配置項(xiàng)、注解等,然后提出應(yīng)對(duì)可擴(kuò)展性的方法:規(guī)范、識(shí)別、注冊(cè)、使用四個(gè)步驟,雖然只有 8 個(gè)字,但它包含了一套系統(tǒng)的處理方案,不再是單一的面向接口編程,最后結(jié)合具體的案例進(jìn)行說(shuō)明如何設(shè)計(jì)可擴(kuò)展性系統(tǒng)。
作者介紹:
高福來(lái),先后在 Oracle、阿里工作,目前在滴滴小桔車(chē)服加油團(tuán)隊(duì)負(fù)責(zé)營(yíng)銷(xiāo)基礎(chǔ) (優(yōu)惠券、獎(jiǎng)勵(lì)金),在分布式中間件和系統(tǒng)架構(gòu)方面積累了一定的經(jīng)驗(yàn),擅長(zhǎng)用通俗易懂的語(yǔ)言描述復(fù)雜問(wèn)題。
大家可以加我的程序員交流群:833145934,群內(nèi)有阿里技術(shù)大牛講解的最新Java架構(gòu)技術(shù)。作為給廣大朋友的加群福利——分布式(Dubbo、Redis、RabbitMQ、Netty、RPC、Zookeeper、高并發(fā)、高可用架構(gòu))/微服務(wù)(Spring Boot、Spring Cloud)/源碼(Spring、Mybatis)/性能優(yōu)化(JVM、TomCat、MySQL)【加群備注好消息領(lǐng)取最新面試資料】