電影售票系統(tǒng)開發(fā)流程及其bug修復(fù)日志--業(yè)務(wù)開發(fā)(1)

傳統(tǒng)業(yè)務(wù)

傳統(tǒng)業(yè)務(wù)應(yīng)用基本把所有的東西都集合在一起,傳統(tǒng)應(yīng)用帶來(lái)的問題挤牛,單一業(yè)務(wù)的開發(fā)和迭代困難忽匈,這個(gè)時(shí)候牽扯到兩個(gè)部分,第一是有可能只是針對(duì)用戶模塊增加了許多需求方淤,其他模塊沒有變更钉赁,這種情況下,第一不談開發(fā)難度携茂,我們把所以的用戶模塊的內(nèi)容都開發(fā)完了你踩,在測(cè)試的時(shí)候有兩種測(cè)試,一種是冒煙測(cè)試讳苦,一種是回歸測(cè)試带膜,除了要測(cè)試用戶還要測(cè)試其他的。這樣問他來(lái)了医吊,如果用戶有一點(diǎn)要修改钱慢,那么其他的測(cè)試也工作量也很大。而且用戶模塊修改也有可能修改公共類了卿堂。第三當(dāng)我們有一種新的技術(shù)準(zhǔn)備應(yīng)用的時(shí)候,比如發(fā)現(xiàn)數(shù)據(jù)庫(kù)是瓶頸了,想要提升系統(tǒng)草描,這個(gè)時(shí)候就可能要用到緩存了览绿,這個(gè)時(shí)候就要用原有代碼進(jìn)行全量修改了,在調(diào)整一個(gè)模塊的時(shí)候可能會(huì)與其他的包進(jìn)行沖突穗慕。其次饿敲,擴(kuò)容困難,現(xiàn)在有一個(gè)業(yè)務(wù)系統(tǒng)逛绵,這個(gè)業(yè)務(wù)系統(tǒng)包含了我們的其他模塊怀各,比如影院模塊,訂單模塊等等术浪,這種情況下瓢对,用戶模塊的并發(fā)量不大,影院的也不大胰苏,但是訂單模塊就不一定了硕蛹,比如說(shuō)內(nèi)容可能是256G,訂單模塊不夠了硕并,要512G法焰,但是要知道,傳統(tǒng)業(yè)務(wù)他的內(nèi)存是統(tǒng)一分配的倔毙,在部署一臺(tái)256G內(nèi)存機(jī)器埃仪,那么訂單可能就只有一點(diǎn)點(diǎn)內(nèi)存,所以這個(gè)時(shí)候擴(kuò)容可能要1T才有可能達(dá)到要求陕赃。 部署和回滾卵蛉,傳統(tǒng)在部署是,比如用戶模塊要ES凯正,影院模塊要redis毙玻,那么部署的時(shí)候就要全部都部署上才能跑的起來(lái),回滾就很簡(jiǎn)單了廊散,比如現(xiàn)在針對(duì)訂單模塊的一修改桑滩,這種情況下訂單模塊出問題,其他模塊沒有問題允睹,這個(gè)時(shí)候全部做回滾运准。

微服務(wù)發(fā)展歷程

很久以前就有提出過面向服務(wù)開發(fā)——SOA,在EJB的時(shí)代就提出了缭受,然后到微服務(wù)開發(fā)胁澳。SOA,原先可能有一個(gè)大的系統(tǒng)米者,現(xiàn)在拆了他韭畸,拆成權(quán)限系統(tǒng)和用戶系統(tǒng)宇智,現(xiàn)在他們需要通信,可以用webservice胰丁,當(dāng)然這個(gè)技術(shù)是很老的技術(shù)了随橘。微服務(wù)和soa最主要就差在微字,首先锦庸,微服務(wù)是一種將業(yè)務(wù)系統(tǒng)進(jìn)一步拆分的架構(gòu)風(fēng)格忿磅,將業(yè)務(wù)系統(tǒng)進(jìn)一步拆分的架構(gòu)風(fēng)格绳慎,微服務(wù)強(qiáng)調(diào)每一個(gè)單一業(yè)務(wù)都獨(dú)立運(yùn)行谷婆,比如原來(lái)有一個(gè)系統(tǒng)财松,有登錄,退出等等一系列業(yè)務(wù)扬卷,這個(gè)用戶底下有很多個(gè)業(yè)務(wù)模塊牙言,每一個(gè)業(yè)務(wù)模塊占用一個(gè)進(jìn)程,或者說(shuō)一個(gè)JVM邀泉,這樣就做了一個(gè)資源拆分嬉挡,這個(gè)業(yè)務(wù)就是一個(gè)應(yīng)用,這就是微服務(wù)強(qiáng)調(diào)的汇恤,資源獨(dú)立庞钢,業(yè)務(wù)獨(dú)立,這就有點(diǎn)像進(jìn)程進(jìn)化到線程一樣因谎,共享相同資源基括,但是又有自己獨(dú)立的棧地址;同時(shí)每一個(gè)單一服務(wù)都應(yīng)該使用更輕量級(jí)的機(jī)制保持通信财岔。在微服務(wù)里面一般會(huì)使用更輕量級(jí)的協(xié)議而不是像webservice這樣這么沉重的风皿。而且每一個(gè)服務(wù)不強(qiáng)調(diào)環(huán)境,可以用不同語(yǔ)言或數(shù)據(jù)源匠璧,只是要求提供好的服務(wù)即可桐款。

微服務(wù)核心概念

Provider:服務(wù)提供者,提供服務(wù)實(shí)現(xiàn)夷恍。Consumer:服務(wù)調(diào)用者魔眨,調(diào)用Provider提供服務(wù)的人。 同一個(gè)服務(wù)可以既是Provider也可以是Consumer酿雪。

環(huán)境搭建

Spring + dubbo

在idea選擇quickstart遏暴,只是服務(wù)之間的調(diào)用,完全可以滿足了指黎。



接下來(lái)還要兩個(gè)子工程朋凉,一個(gè)Provider,一個(gè)Consumer醋安, 至少一個(gè)吧杂彭。按照相同的方法建立兩個(gè)子工程墓毒。在src下面建立resource文件夾,設(shè)置成resource root文件盖灸。然后把依賴引進(jìn)了蚁鳖,依賴這兩個(gè)Provider和Consumer都需要用磺芭,所以直接引近父工程的即可赁炎,由于是spring,不是springboot钾腺,還要application.xml配置文件徙垫,所以引入application-hello.xml:



注意第七行和第十三行就是引入了dubbo的命名空間。這個(gè)時(shí)候環(huán)境基本完成放棒,現(xiàn)在就是要引入dubbo集成了姻报。簡(jiǎn)單寫下測(cè)試,在Provider里面新建立一個(gè)服務(wù)接口以及服務(wù)實(shí)現(xiàn):
public class QuickStartServiceImpl implements ServiceAPI {
    @Override
    public String sendMessage(String message) {
        return "quickstart-provider-message=" + message;
    }
}

實(shí)現(xiàn)的接口间螟。這就是Provider要提供的一個(gè)接口吴旋,這個(gè)接口是要Consumer來(lái)調(diào)用的,然后就要在application-hello-Provider.xml里面進(jìn)行配置:

    <dubbo:application name="hello-world-app"/>


    <!-- use dubbo protocol to export service on port 20880 -->
    <dubbo:protocol name="dubbo" port="20880"/>
    <!-- service implementation, as same as regular local bean -->
    <bean id="providerService" class="org.greenarrow.quickstart.QuickStartServiceImpl"/>
    <!-- declare the service interface to be exported -->
    <dubbo:service
            registry="N/A"
            interface="org.greenarrow.ServiceAPI"
            ref="providerService"/>

name就是服務(wù)的名稱厢破,也是唯一標(biāo)識(shí)荣瑟,protocol就是服務(wù)的地址,bean其實(shí)就是接口的實(shí)現(xiàn)類摩泪,service就是接口本身笆焰,通過這個(gè)接口本身去調(diào)用服務(wù)。
接著就是Consumer的配置:


    <dubbo:application name="demo-consumer"/>
    <!-- generate proxy for the remote service, then demoService can be used in the same way as the
    local regular interface -->
    <dubbo:reference
            id="consumerService"
            interface="org.greenarrow.ServiceAPI"
            url="dubbo://localhost:20880"
    />

這里意思是裝配上Provider的服務(wù)见坑,URL為dubbo://localhost:20880嚷掠,和

<dubbo:protocol name="dubbo" port="20880"/>

相對(duì)應(yīng),可能serviceAPI會(huì)報(bào)錯(cuò)荞驴,但是創(chuàng)建一個(gè)就好了不皆。

public class ConsumerClient {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext-hello-consumer.xml");
        context.start();
        while (true){
            Scanner scanner = new Scanner(System.in);
            String message = scanner.next();
            //獲取接口
            ServiceAPI serviceAPI = (ServiceAPI)context.getBean("consumerService");
            System.out.println(serviceAPI.sendMessage(message));
        }
    }
}

啟動(dòng)的時(shí)候兩個(gè)都要啟動(dòng)。運(yùn)行的時(shí)候遇到一個(gè)問題:會(huì)發(fā)現(xiàn)dubbo://localhost:20880找不到熊楼,顯示這個(gè)服務(wù)已經(jīng)關(guān)閉霹娄,調(diào)了好久,然后重啟一下idea孙蒙,完了好了项棠,啥事都沒有。

為什么會(huì)出現(xiàn)這個(gè)問題挎峦,看到網(wǎng)上大多數(shù)問題都是ZX沒有注冊(cè)或者是內(nèi)外網(wǎng)的問題香追,所以不了了之了。其實(shí)整個(gè)流程是這樣:Consumer先在xml里面配置得到了Provider的bean坦胶,quickstart透典,然后用Provider的這個(gè)服務(wù)給自己返回了一個(gè)消息晴楔,再輸出。

SpringBoot + dubbo

這個(gè)就比較簡(jiǎn)單了峭咒,idea就自帶了spring initial税弃,直接建立即可西壮,不需要任何依賴。

這就是基本目錄签杈。和spring是一樣的鹦付,只不過把大部分配置變成了注解祈噪。接口按照spring的一樣宁赤,但是實(shí)現(xiàn)類需要把配置文件的信息變成注解:

注意Service不要倒錯(cuò)包哆窿。然后在Provider啟動(dòng)類加上另外一個(gè)注解:

這就是Provider的編碼,Consumer就簡(jiǎn)單許多了:

注意ServiceAPI的路徑在Provider和Consumer的路徑要一樣,剛剛就犯了這個(gè)錯(cuò)誤码荔。
Provider

Consumer

路徑要一樣。
結(jié)果:

還是出現(xiàn)是配置spring+dubbo的問題堂鲤,超時(shí)問題,這個(gè)問題是不定時(shí)出現(xiàn)的谅阿,上一次的配置是重啟了idea就好了半哟,這次是重啟電腦好的。懷疑的電腦配置問題導(dǎo)致的超時(shí)問題签餐,因?yàn)閐ubbo默認(rèn)是1000ms就會(huì)報(bào)超時(shí)寓涨,于是把他調(diào)到了50000ms,可能與內(nèi)存電腦配置有關(guān)氯檐,具體原因尚未知戒良。

Zookeeper

上面這種就是直連提供者了,要求Consumer知道Provider的服務(wù)地址男摧,直接找到服務(wù)蔬墩,這種方式太固定了译打,不利于擴(kuò)展。所以延伸出了用一個(gè)注冊(cè)中心來(lái)注冊(cè)所有的服務(wù)拇颅,zookeeper就是這樣一個(gè)注冊(cè)中心奏司。


invoke就所用的直連提供者。安裝zookeeper還是很簡(jiǎn)單的樟插,下載下來(lái)解壓即可韵洋,然后進(jìn)入bin,./zkServer.sh start運(yùn)行即可黄锤。

在spring+dubbo中的配置比較簡(jiǎn)單搪缨,加上幾個(gè)依賴即可,把registry=N/A改成zookeeper://localhost:2181即可鸵熟,N/A就是什么都不用副编。
springboot+dubbo配置需要在父工程加上依賴:

注意子工程的parent要改成父工程,否則依賴是無(wú)法引入的流强。
父工程加上:
表明這兩個(gè)是parent的兒子痹届。

子工程加上,表面父母坐標(biāo)打月,這樣才能繼承依賴队腐。
springboot的依賴最好不要加在子工程上党窜,會(huì)出現(xiàn)日志沖突忠售。接著和原來(lái)一樣:

spring.application.name=dubbo-spring-boot-starter
spring.dubbo.server=true
spring.dubbo.registry=zookeeper://localhost:2181

N/A去掉雀监,接上服務(wù)器的位置甫匹。Provider就改完了断部。Consumer也是改一樣的地方也物,把注解改了:

@Component
public class QuickstartConsumer  {
    @Reference(interfaceClass = ServiceAPI.class)
    private ServiceAPI serviceAPI;

    public void sendMessage(String message){
        System.out.println(serviceAPI.sendMessage(message));
    }
}

還有一個(gè)需要注意的問題逛漫,springboot的application只會(huì)默認(rèn)掃描同路徑下或者是子路徑的包:


application在Provider下面例诀,那么它只會(huì)掃描和她同級(jí)的包和Provider的子路徑充易,所以impl放在quickstart放在了Provider下面是可以掃描到的梗脾,如果把implement放在dubbo就掃碼不到了,這個(gè)時(shí)候就要加注解:

@SpringBootApplication(scanBasePackages = "com.greenarrow.springboot.dubbo")

就會(huì)從dubbo這個(gè)包下面開始掃描盹靴。

構(gòu)建業(yè)務(wù)環(huán)境

API網(wǎng)關(guān)

首先介紹一下API網(wǎng)關(guān)炸茧,API網(wǎng)關(guān)有點(diǎn)像設(shè)計(jì)模式中的Facade模式,比如現(xiàn)在有很多個(gè)服務(wù)稿静,用戶服務(wù)梭冠,產(chǎn)品服務(wù),訂單服務(wù)改备,如果一個(gè)網(wǎng)站控漠,想要訪問產(chǎn)品服務(wù),這個(gè)時(shí)候可能就要檢查是不是登錄了,或者權(quán)限等等盐捷,那么就要先訪問用戶服務(wù)看看你登錄了沒有偶翅,登錄了,才訪問產(chǎn)品碉渡,但是這樣會(huì)帶來(lái)問題聚谁,因?yàn)榭蛻舳吮旧聿话踩晕⒎?wù)這塊對(duì)外相當(dāng)于是暴露了滞诺,都知道是什么參數(shù)了形导,對(duì)外來(lái)說(shuō)相當(dāng)于是透明,所以安全比較難做习霹;其次朵耕,在提交訂單的時(shí)候要做三步操作,檢查登錄淋叶,產(chǎn)品是否足夠阎曹,下訂單,其實(shí)是在點(diǎn)擊下訂單的時(shí)候爸吮,這三個(gè)步驟要一起完成芬膝,而這三個(gè)操作是三個(gè)不同的微服務(wù),有順序性的了形娇,所以時(shí)間長(zhǎng)。打個(gè)比方筹误,現(xiàn)在想看新聞桐早,一會(huì)兒想看搜狐新聞,一會(huì)兒想看頭條新聞厨剪,每次都要輸入就很麻煩哄酝,那么這個(gè)時(shí)候就會(huì)出現(xiàn)一個(gè)公共網(wǎng)站,比如hao123祷膳,這些網(wǎng)站陶衅,既可以看到搜狐,也可以看到頭條新聞直晨,其他的新聞網(wǎng)站對(duì)于我們老師都是透明的搀军,甚至有時(shí)候新聞來(lái)源都是不知道的。介于這種情況勇皇,就會(huì)出現(xiàn)一個(gè)網(wǎng)關(guān)的東西罩句,gateway,這東西就相當(dāng)于后臺(tái)服務(wù)與前端客戶端的一個(gè)接口敛摘,那么至于怎么訪問门烂,異步同步等等前端都不需要管。都只需要面向一個(gè)接口,其他都只是后端的屯远,所以API網(wǎng)關(guān)就相當(dāng)于是微服務(wù)中的一個(gè)門面蔓姚。


API網(wǎng)關(guān)作用:既然已經(jīng)充當(dāng)門面了,首要就是需要驗(yàn)證你身份合不合格了慨丐,就相當(dāng)于一個(gè)防火墻坡脐;審查與監(jiān)察,網(wǎng)關(guān)有一個(gè)比較特殊的作用咖气,類似于之前的攔截器挨措,審查和分發(fā),回來(lái)還要經(jīng)過網(wǎng)關(guān)崩溪,再返回浅役。這種情況下,就可以把邊緣信息統(tǒng)計(jì)一下伶唯,比如執(zhí)行時(shí)間觉既,調(diào)用了啥服務(wù),響應(yīng)時(shí)間等等乳幸。其次還可以做動(dòng)態(tài)路由瞪讼,dubbo做好了,但是springcloud沒有粹断,springcloud需要處理符欠。壓力測(cè)試也有可能,一般是接替測(cè)試瓶埋,負(fù)載均衡希柿,靜態(tài)響應(yīng)分離。整個(gè)業(yè)務(wù)結(jié)構(gòu)大概就是客戶端訪問API網(wǎng)關(guān)(服務(wù)聚合养筒,熔斷降級(jí)曾撤,身份安全),然后網(wǎng)關(guān)再把請(qǐng)求分發(fā)下去晕粪。

guns環(huán)境搭建

guns這里使用還是v3.1挤悉,現(xiàn)在已經(jīng)到了v6了,很早之前就下載過3.1版本巫湘,懶得換了装悲。直接啟動(dòng)會(huì)有一些錯(cuò)誤:



首先是log4j問題,下一個(gè)包就好了剩膘。



這個(gè)是時(shí)區(qū)的問題衅斩,另外他也沒有識(shí)別出關(guān)鍵字zeroDateTimeBehavior,但是查了一下這個(gè)確實(shí)是MySQL的一個(gè)參數(shù)怠褐,也不知道為什么畏梆,修改一下連接路徑:
      url: jdbc:mysql://127.0.0.1:3306/guns_rest?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8

去掉即可。


admin是主要操作,也是主要業(yè)務(wù)奠涌,core是核心實(shí)現(xiàn)宪巨,generator是代碼生成,rest是連接數(shù)據(jù)庫(kù)的溜畅。原來(lái)還有一個(gè)parent捏卓,我把他搞在外面了。啟動(dòng)guns-rest慈格,在application.yml里面有一個(gè)auth-path怠晴,就是路徑,http://localhost/auth?userName=admin&password=admin訪問浴捆,默認(rèn)賬號(hào)密碼admin(這個(gè)要看官網(wǎng)蒜田,我自己改的),出現(xiàn):

就表示成功了选泻。接下來(lái)就是配置dubbo環(huán)境了冲粤,和springboot一樣,加依賴即可页眯,使用guns-rest作為后端模板往常網(wǎng)關(guān)梯捕,所以直接復(fù)制一個(gè)就好了,對(duì)于zookeeper這些和原來(lái)的一樣窝撵,啟動(dòng)之后./zkServer.sh start傀顾,./zkCli.sh打開客戶端查看zookeeper注冊(cè)符號(hào):

可以看到當(dāng)前服務(wù)已經(jīng)注冊(cè)進(jìn)去了。那么環(huán)境基本就是這樣了碌奉,剩下就是業(yè)務(wù)開發(fā)了锣笨。

抽離業(yè)務(wù)接口

就我們現(xiàn)在的工程,每一個(gè)模塊一個(gè)實(shí)現(xiàn)類就一個(gè)接口道批,在微服務(wù)中這些接口各個(gè)工程都要有,就如前面實(shí)現(xiàn)的spring/springboot+dubbo直連一樣入撒,serviceAPI各個(gè)工程都要有隆豹,provide要有,Consumer也要有晒喷,很麻煩宿接。如果是這樣的話导而,為什么不可以把這些接口全部獨(dú)立出來(lái),做成一個(gè)工程呢碉考?然后把實(shí)現(xiàn)類也扔到一個(gè)子工程里面,然后就當(dāng)成是一個(gè)依賴一樣引入pom.xml文件中即可挺身。所以復(fù)制一份guns-core改名侯谁,在project structure改名,可能會(huì)出現(xiàn)can not contain source root這些錯(cuò)誤,這個(gè)時(shí)候要把原來(lái)的module的重復(fù)source root刪除:



復(fù)制完成后右邊那一列(Add Content Root)會(huì)出現(xiàn)兩個(gè)source root 刪除一個(gè)即可change name了墙贱。之前測(cè)試使用的UserAPI移到這里热芹。



打包這個(gè)模塊,maven里面lifecycle install惨撇,打包完成之后引入到guns-gateway做測(cè)試:
@Component
@Service(interfaceClass = UserAPI.class)
public class UserImpl implements UserAPI {
    @Override
    public boolean login(String userName, String password) {
        return true;
    }
}

這里的UserAPI導(dǎo)入guns-api里面即可伊脓。



可以看到,這個(gè)時(shí)候注冊(cè)的又是guns.api.user.UserAPI的接口了魁衙。

Dubbo調(diào)用流程

dubbo有兩種調(diào)用方式报腔,直連提供者和注冊(cè)中心,兩種都在剛剛的環(huán)境搭建中就簡(jiǎn)單測(cè)試過了剖淀。首先是直連提供者:在開發(fā)和測(cè)試環(huán)境纯蛾,常常是需要繞過注冊(cè)中心,直接指定提供者是誰(shuí)祷蝌,其實(shí)就是點(diǎn)對(duì)點(diǎn)直連茅撞,類似數(shù)據(jù)鏈路層吧,如果需要?jiǎng)討B(tài)擴(kuò)容巨朦,每一個(gè)地址都要讓Consumer知道米丘,缺點(diǎn)就是全寫在代碼里面了,寫的好點(diǎn)的可以扔在配置文件糊啡,但是都要重新啟動(dòng)拄查,所以這種方式僅僅用于開發(fā)和測(cè)試,如果使用注冊(cè)中心棚蓄,就簡(jiǎn)單許多了堕扶∷笠溃基于注冊(cè)中心:首先先要有Provider科平,dubbo提供了一個(gè)容器,用這個(gè)容器來(lái)裝載Provider多柑,當(dāng)啟動(dòng)系統(tǒng)時(shí)初嘹,這個(gè)Provider就會(huì)在注冊(cè)中心留下地址温眉,其實(shí)就是發(fā)現(xiàn)服務(wù)的過程懈词,也叫register蒂窒;而Consumer也會(huì)subscribe一下注冊(cè)中心,把服務(wù)地址下載下來(lái)秧秉,同時(shí)注冊(cè)中心一有變化就notify Consumer象迎,Consumer又更新一次赃春,dubbo本身就有moniter锥涕,用于監(jiān)控檢測(cè)等等

dubbo多協(xié)議

dubbo默認(rèn)支持阿里開源的dubbo協(xié)議,同時(shí)也支持rmi狭吼,hessian等等協(xié)議层坠。Dubbo協(xié)議特點(diǎn): 傳入傳出參數(shù)數(shù)據(jù)包較小(建議小于100K)刁笙,消費(fèi)者比提供者個(gè)數(shù)多破花,單一消費(fèi)者無(wú)法壓滿提供者,盡量不要用dubbo協(xié)議傳輸大文件或超大字符串疲吸,基于以上描述座每,我們一般建議Dubbo用于小數(shù)據(jù)量大并發(fā)的服務(wù)調(diào)用,以及服務(wù)消費(fèi)者機(jī)器數(shù)遠(yuǎn)大于服務(wù)提供者機(jī)器數(shù)的情況磅氨。
RMI協(xié)議特點(diǎn): 傳入傳出參數(shù)數(shù)據(jù)包大小混合尺栖,消費(fèi)者與提供者個(gè)數(shù)差不多,可傳文件烦租⊙佣模基于以上描述,我們一般對(duì)傳輸管道和效率沒有那么高的要求叉橱,同時(shí)又有傳輸文件這一類的要求時(shí)挫以,可以嘗試采用RMI協(xié)議。
Hessian協(xié)議特點(diǎn): 傳入傳出參數(shù)數(shù)據(jù)包大小混合窃祝,提供者比消費(fèi)者個(gè)數(shù)多掐松,可用瀏覽器查看,可用表單或URL傳入?yún)?shù)粪小,暫不支持傳文件大磺。比較適用于需同時(shí)給應(yīng)用程序和瀏覽器JS使用的服務(wù),Hessian協(xié)議的相關(guān)內(nèi)容與HTTP基本差不多探膊,這里就不再贅述了杠愧。
WebService協(xié)議特點(diǎn): 基于CXF的frontend-simple和transports-http實(shí)現(xiàn),適用于系統(tǒng)集成逞壁,跨語(yǔ)言調(diào)用流济。 不過如非必要锐锣,強(qiáng)烈不推薦使用這個(gè)方式,WebService是一個(gè)相對(duì)比較重的協(xié)議傳輸類型绳瘟,無(wú)論從性能雕憔、效率和安全性上都不太能滿足微服務(wù)的需要,如果確實(shí)存在異構(gòu)系統(tǒng)的調(diào)用糖声,建議可以采用其他的形式斤彼。http協(xié)議也支持。

用戶模塊開發(fā)

JWT驗(yàn)證

遠(yuǎn)離Java太久了姨丈,之前使用的方式都是使用cookie+session畅卓,或者用上sso單點(diǎn)登錄吧,jwt有點(diǎn)不一樣蟋恬,不需要存儲(chǔ)session翁潘,用戶從客戶端輸入賬號(hào)密碼訪問服務(wù)器,服務(wù)器給他個(gè)token歼争,結(jié)構(gòu)如下圖所示:



header是加密方式拜马,就是用什么方式加密把,payload就是承載信息了沐绒,用戶還是管理員等等俩莽,簽名就是利用前面兩個(gè)信息進(jìn)行兩次哈希加密得到的。然后每一次客戶端就帶著這個(gè)token乔遮,服務(wù)器只需要驗(yàn)證這個(gè)token是不是正確的就好了扮超。
還是先要建表:


user_t的字段屬性。guns_gateway基本已經(jīng)完成了蹋肮,只需要相互調(diào)通即可出刷。重新復(fù)制一個(gè)guns-gateway修改名字為guns-user,注意是gateway這個(gè)門戶去調(diào)用服務(wù)坯辩,所以這兩個(gè)東西都要啟動(dòng)起來(lái)馁龟,所以端口肯定要不一樣,所以dubbo端口要變一下漆魔,而且權(quán)限驗(yàn)證jwt這是在gateway門戶做的坷檩,通過之后再跑到gate-user調(diào)用服務(wù)。所以端口和jwt要設(shè)置一下:


啟動(dòng)測(cè)試一下改抡。
現(xiàn)在的流程是矢炼,客戶端調(diào)用服務(wù),是直接調(diào)用gateway門戶里面的api阿纤,gateway再根據(jù)各種需求調(diào)用后面各種模塊的服務(wù)裸删,測(cè)試一把。
登錄進(jìn)去一定會(huì)調(diào)用jwt做驗(yàn)證阵赠,就直接在jwt上面做測(cè)試吧涯塔。

@Component
@Service(interfaceClass = UserAPI.class)
public class UserImpl implements UserAPI {
    @Override
    public boolean login(String userName, String password) {
        System.out.println("this is user service!" + userName + " " + password);
        return false;
    }
}

這是guns-users模塊的服務(wù),也就是gateway要調(diào)用的服務(wù)清蚀,如果能打印出來(lái)語(yǔ)句就OK了匕荸。



在gateway的auth的controller加上測(cè)試。



注意枷邪,打印是打印在UserApplication里面榛搔,因?yàn)檫@個(gè)服務(wù)是在user模塊完成的。出現(xiàn)打印那么說(shuō)明互相調(diào)用就OK了东揣。

配置可以忽略的URL

有些URL是可以忽略的践惑,比如注冊(cè),/user/register嘶卧,登錄尔觉,/user/login,這些很明顯是不需要的芥吟,所以還是需要配置一下侦铜。首先理解一下guns的jwt:


gateway里面的配置文件有一項(xiàng)就是jwt,在springboot中有一項(xiàng)就是要把其內(nèi)容全部讀進(jìn)去嬉荆。

這是屬于springboot的注解配置驯遇,configuration就是讀取在yml配置文件所有前綴是JWT_PREFIX屠尊,下面配置了JWT_PREFIX = "jwt",就是讀取yml中jwt:中的內(nèi)容贡未。所以在yml配置文件jwt中我配置了ignore-url,作為忽略的URL蒙袍,那么在JwrProperties就要讀進(jìn)來(lái)了俊卤。

注意在springboot中,讀取配置這里默認(rèn)會(huì)把-u變成大寫U左敌,ignore-url就變成ignoreUrl瘾蛋,讀取進(jìn)來(lái)記得加上getset方法。既然添加完路徑了矫限,首要就是做處理了哺哼,處理這種東西肯定是在讀取進(jìn)來(lái)的權(quán)限做限制,那么在module auth filter里面修改即可叼风,有一個(gè)AuthFilter:

這里的chain.doFilter是按照鏈?zhǔn)竭^濾的意思取董,如果多個(gè)filter,那么按照f(shuō)ilter1->filter2->filter3......以此類推无宿,但是下面沒有其他的filter了茵汰,所以直接返回頁(yè)面,所以這里也代表著直接通過的意思孽鸡。那么仿照它把ignore-url路徑加上:

這樣就配置好蹂午。

用戶模塊API以及相應(yīng)的類

用戶模塊api肯定是添加在guns-api這塊栏豺,在添加兩個(gè)類,一個(gè)類是UserModel豆胸,這是用戶注冊(cè)的類奥洼,這個(gè)類是不能被修改的,僅僅作為注冊(cè)使用晚胡,因?yàn)樽?cè)的內(nèi)容有一些敏感內(nèi)容灵奖,所以需要一個(gè)新類,也就是UserInfoModel作為真正的可以被修改的類:

public class UserInfoModel {
    private String username;
    private String nickname;
    private String email;
    private String phone;
    private int sex;
    private String birthday;
    private String liftState;
    private String biography;
    private String address;
    private String headAddress;
    private Long beginTime;
    private Long updateTime;


    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public int getSex() {
        return sex;
    }

    public void setSex(int sex) {
        this.sex = sex;
    }

    public String getBirthday() {
        return birthday;
    }

    public void setBirthday(String birthday) {
        this.birthday = birthday;
    }

    public String getLiftState() {
        return liftState;
    }

    public void setLiftState(String liftState) {
        this.liftState = liftState;
    }

    public String getBiography() {
        return biography;
    }

    public void setBiography(String biography) {
        this.biography = biography;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getHeadAddress() {
        return headAddress;
    }

    public void setHeadAddress(String headAddress) {
        this.headAddress = headAddress;
    }

    public Long getBeginTime() {
        return beginTime;
    }

    public void setBeginTime(Long beginTime) {
        this.beginTime = beginTime;
    }

    public Long getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(Long updateTime) {
        this.updateTime = updateTime;
    }
}

按照返回格式來(lái)修改登錄之后要返回的數(shù)據(jù):



修改api中的返回?cái)?shù)據(jù)估盘,然后讓其返回用戶的uid瓷患,因?yàn)榉祷氐膖oken使用的就是用戶的UID來(lái)構(gòu)建,構(gòu)建一個(gè)返回model遣妥,泛型使用M擅编,因?yàn)檫@里的data是其他類型:



然后在AuthController修改一下返回類型和內(nèi)容即可。

用戶信息保存

ThredLocal用戶信息保存的方法代替把信息保存到session中燥透,threadlocal每一個(gè)線程分開使用沙咏,不同線程的threadlocal不一樣,好比線程之間的棧地址不可共享班套,同一個(gè)進(jìn)程的線程資源與空間可以共享肢藐。可以使用threadlocal直接保存用戶信息吱韭,也可以只保存UID或者某些關(guān)鍵信息吆豹,這里使用保存UID的方法:


有了方法還得維護(hù)他,每一次登陸或者是注冊(cè)完之后理盆,都會(huì)發(fā)放一個(gè)jwt痘煤,每一次客戶端進(jìn)入頁(yè)面帶來(lái)點(diǎn)jwt中也有uid,也要把UID拿出來(lái)放在threadlocal中猿规,也就是再filter那里衷快,在登陸進(jìn)來(lái),訪問URL進(jìn)來(lái)的時(shí)候都需要進(jìn)過的一個(gè)過濾器姨俩,在AuthFilter里面修改:

這里使用getUsernameFromToken是因?yàn)樵贏uthController中我們保存的就是uid蘸拔,只不過uid替代了username這個(gè)位置。那么用戶的jwt就修改完成了环葵。既然都改完了调窍,做一個(gè)測(cè)試吧!
首先準(zhǔn)備一個(gè)控制器:

如果能通過這個(gè)控制器驗(yàn)證成功就可以打印出uid张遭,返回請(qǐng)求成功邓萨。
回看一下請(qǐng)求流程,首先登陸,登陸成功就會(huì)返回一個(gè)uid缔恳,每一次客戶端就會(huì)拿著這個(gè)Uid組成的token登陸宝剖,查看一下filter的AuthController:

顯示把前面7個(gè)固定字符取出來(lái),然后再驗(yàn)證后面的token歉甚,使用postman測(cè)試:

先登陸得到token诈闺,然后用token登陸,注意token在header里面:

測(cè)試的時(shí)候還是遇到了一些問題铃芦,一開始啟動(dòng)不了gateway,后來(lái)嘗試了很多遍襟雷,把gateway的target刪掉就可以運(yùn)行了刃滓。既然都已經(jīng)調(diào)通了,那么開始把數(shù)據(jù)層對(duì)接上吧耸弄,啟用代碼生成工具直接生成就好了咧虎。
額,直接生成就好了计呈,guns這個(gè)框架很快的砰诵。

注意區(qū)別一下目前存在的三個(gè)模型:

UserModel和UserInfoModel都是模型之間交互的,但是區(qū)別就在于UserModel只是用于注冊(cè)捌显,而UserInfoModel是用于各模塊之間的交互以及修改茁彭,UserT就是guns-user自用的Dao層而已,不會(huì)跨越模塊扶歪。然后就實(shí)現(xiàn)各種服務(wù)了理肺,這個(gè)很簡(jiǎn)單,沒得說(shuō)善镰,注意密碼不能明文存儲(chǔ)即可妹萨。需要注意的主要就是用戶退出這個(gè)功能,一般會(huì)把用戶的信息存兩份炫欺,前端先存jwt乎完,一般存7天,在這種情況下就會(huì)存在一個(gè)問題品洛,jwt的刷新树姨;那么這個(gè)時(shí)候后端就起著刷新作用,服務(wù)端就存儲(chǔ)活動(dòng)用戶信息毫别,一般30分鐘娃弓,如果在30分鐘之內(nèi)能查到用戶,那么就認(rèn)為是活躍用戶岛宦,如果沒有,哪怕你有jwt也認(rèn)為你需要重新登錄,所以logout要做兩件事挽霉,首先刪除前端jwt防嗡,然后刪除后端活動(dòng)緩存即可。
接著就是測(cè)試了侠坎,遇到一個(gè)很牛逼的bug:

這個(gè)問題吧蚁趁,就是前面提到好幾次的問題,我當(dāng)時(shí)解決對(duì)了一半实胸,確實(shí)是機(jī)器問題,但是不是性能超時(shí)時(shí)延的問題淆党,而是WiFi問題,說(shuō)我信息發(fā)不出去盐须,通道關(guān)閉了匆骗,那就是鏈路問題,但是我ping了一下127.0.0.1,可以通,那么tcp/ip就沒有問題了扣猫,ping了一下另外一臺(tái)電腦磁奖,可以通南誊,那么網(wǎng)卡或者說(shuō)網(wǎng)關(guān)就沒有問題了,然后看到網(wǎng)上很多人說(shuō)WiFi問題史煎,然后我把WiFi關(guān)了氢橙,然后就可以通了。不過打開WiFi在測(cè)試的過程中還是有某幾次是可以連上的恬偷,但關(guān)閉WiFi就一定可以連上充蓝。關(guān)鍵是他這個(gè)錯(cuò)誤,也就是cause:message can not be send,channal is closed.這個(gè)錯(cuò)誤不是一下就提出來(lái)了喉磁,還是我把check=false設(shè)置了之后才出現(xiàn)谓苟。接著就是測(cè)試接口了,注意一些model里面的值最好使用對(duì)象协怒,比如使用Integer或者String對(duì)象涝焙,不要使用int這樣的,因?yàn)橛锌赡軙?huì)出現(xiàn)null值孕暇。
測(cè)試完成后仑撞,基本上用戶模塊差不多了赤兴,但是現(xiàn)在還有一個(gè)小問題,就是啟動(dòng)的時(shí)候必須要有順序隧哮,要不然gateway會(huì)找不到服務(wù)桶良,其次還有負(fù)載均衡的問題。啟動(dòng)順序那個(gè)就是check=false的問題沮翔,負(fù)載均衡策略dubbo有四種陨帆,Random,按權(quán)重設(shè)置隨機(jī)概率采蚀,RoundRobin昔脯,按公約后的權(quán)重設(shè)置輪循比率治宣,LeastActive,最少活躍調(diào)度數(shù)嗜桌,如果活躍數(shù)相同拐格,那么隨機(jī)鸥诽,不同就按照排序炭臭,ConsistentHash迎膜,相同參數(shù)就到同一個(gè)提供者,不同參數(shù)到另外一個(gè)神妹。一般多用輪循袁滥,但是有可能受到機(jī)器影響,如果三臺(tái)機(jī)器的效率并不相同灾螃,如果第三個(gè)請(qǐng)求到了第三臺(tái)機(jī)器,但是第三臺(tái)機(jī)器炸毛了揩徊,沒有返回腰鬼,那么就一直卡在這,因?yàn)檩喲谌齻€(gè)request一定是到第三個(gè)塑荒,但是第三個(gè)一直不能返回熄赡,就造成了dubbo的雪崩。負(fù)載均衡這里有兩種配置方式:

客戶端端配置意思是訪問服務(wù)提供者是一種什么形式齿税,而服務(wù)端服務(wù)是所有的客戶端訪問這個(gè)服務(wù)的訪問形式是什么樣的彼硫,簡(jiǎn)單點(diǎn)說(shuō),就是客戶端級(jí)別就是當(dāng)前這個(gè)客戶端訪問是怎么來(lái)的凌箕,其他客戶端沒有影響拧篮,而服務(wù)端是影響到了所有客戶端,所以在Impl配上roundrobin就好了牵舱。
dubbo的多協(xié)議之前提到過串绩,簡(jiǎn)單再提一下,dubbo支持多協(xié)議中芜壁,最主要的區(qū)別就是鏈接方式礁凡,dubbo協(xié)議建立的是長(zhǎng)鏈接高氮,一旦建立就會(huì)建立一個(gè)管道,不需要每一次都要進(jìn)行建立顷牌,類似HTTP的長(zhǎng)短鏈接剪芍,但是dubbo本身不是一種協(xié)議,只是封裝了TCP窟蓝,然后在TCP的基礎(chǔ)上變成了dubbo這個(gè)協(xié)議罪裹,那么dubbo的傳輸協(xié)議就是TCP了,另外疗锐,dubbo用到是NIO的異步傳輸坊谁。

影片模塊

影片模塊有點(diǎn)復(fù)雜,數(shù)據(jù)庫(kù)設(shè)計(jì)也有點(diǎn)多滑臊,首頁(yè)內(nèi)容比較多口芍,首頁(yè)每一個(gè)人都是一樣的,所以直接搞了一個(gè)數(shù)據(jù)庫(kù)表給他雇卷,這樣直接取出來(lái)就好了鬓椭,首頁(yè)需要實(shí)現(xiàn):


這么多功能需要用一個(gè)功能完成,這就是網(wǎng)關(guān)的功能聚合关划,前端只調(diào)用一次接口小染,全部加載出來(lái),不需要這么多次HTTP請(qǐng)求贮折。

actor_t是演員表裤翩,banner是首頁(yè)圖片的表,其實(shí)就是滑動(dòng)窗口的圖片调榄,cat_dict_t就是字典了踊赠,分類字典,比如懸疑每庆,犯罪筐带,動(dòng)作,愛情等等缤灵,film_info電影信息表伦籍,這個(gè)表存儲(chǔ)電影的少量核心信息,這類信息是經(jīng)常要使用的腮出,信息量不大帖鸦,可以加快查詢速度;film_t就是電影信息全的表了胚嘲,所有信息都在里面了富蓄,接下來(lái)就來(lái)源表,年份表慢逾。這些表都沒有進(jìn)行外鍵關(guān)聯(lián)立倍。

@Data
public class FilmindexVO {
    private List<BannerVO> banners;
    private FilmVO hotFilms;
    private FilmVO soonFilms;
    private List<FilmInfo> boxRanking;
    private List<FilmInfo> expectRanking;
    private List<FilmInfo> top100;
}

前端準(zhǔn)備要返回的模型灭红,通過一個(gè)接口全部裝進(jìn)去。

public interface FilmServiceAPI {
    //get banners info
    List<BannerVO> getBanners();
    //get hot films
    FilmVO getHotFilms(boolean isLimit, Integer nums);
    //get films displayed soon
    FilmVO getSoonFilms(boolean isLimit, Integer nums);
    //get boxRanking
    List<FilmInfo> getBoxRanking();
    //population Ranking
    List<FilmInfo> getExpectRanking();
    //get top
    List<FilmInfo> getTop();

}

要實(shí)現(xiàn)的接口口注。里面很多交互的模型都有共同點(diǎn)变擒,直接使用同一個(gè)模型即可。實(shí)現(xiàn)網(wǎng)關(guān)里面的首頁(yè)控制器寝志,別忘了把前端地址加到ignore_url上面:

    @RequestMapping(value = "getIndex", method = RequestMethod.GET)
    public ResponseVO getIndex() {

        /**
         * banner信息
         * 正在熱映影片
         * 即將上映
         * 票房排行
         * 人氣榜單
         * 前100
         */

        FilmindexVO filmindexVO = new FilmindexVO();
        filmindexVO.setBanners(filmServiceAPI.getBanners());
        filmindexVO.setHotFilms(filmServiceAPI.getHotFilms(true, 8));
        filmindexVO.setSoonFilms(filmServiceAPI.getSoonFilms(true, 8));
        filmindexVO.setBoxRanking(filmServiceAPI.getBoxRanking());
        filmindexVO.setExpectRanking(filmServiceAPI.getExpectRanking());
        filmindexVO.setTop100(filmServiceAPI.getTop());
        return ResponseVO.success(IMG_PRE, filmindexVO);
    }

很簡(jiǎn)單娇斑,就是裝進(jìn)去直接返回即可。測(cè)試一下:



寫到這里基本調(diào)通了材部。接下來(lái)就是快速的業(yè)務(wù)開發(fā)毫缆。業(yè)務(wù)開發(fā)這塊復(fù)雜點(diǎn)的其實(shí)也就按條件查詢這里有點(diǎn)復(fù)雜,其他還算OK乐导。類似于電影天堂里面那些按照國(guó)籍查詢苦丁,按照演員查詢等等:


前端接口如此,isActive就是是否是選中的物臂,如果是選中就是true旺拉,其他的就是false了,前端會(huì)返回選中的分類棵磷,接著后端會(huì)對(duì)比選中的分類蛾狗,選中的返回true,其他返回false仪媒,如果是'全選'沉桌,標(biāo)識(shí)是99:
    @RequestMapping(value = "getConditionList", method = RequestMethod.GET)
    public ResponseVO getConditionList(@RequestParam(name = "catId", required = false, defaultValue = "99") String catId,
                                       @RequestParam(name = "sourceId", required = false, defaultValue = "99") String sourceId,
                                       @RequestParam(name = "yearId", required = false, defaultValue = "99") String yearId) {

        boolean flag = false;
        FilmConditionVO filmConditionVO = new FilmConditionVO();
        List<CatVO> cats = filmServiceAPI.getCats();
        List<CatVO> catResult = new ArrayList<>();
        CatVO cat = new CatVO();
        for (CatVO catVO : cats) {
            if (catVO.getCatId().equals("99")) {
                cat = catVO;
                continue;
            }
            if (catVO.getCatId().equals(catId)) {
                flag = true;
                catVO.setActive(true);
            } else {
                catVO.setActive(false);
            }
            catResult.add(catVO);
        }
        if (!flag) {
            cat.setActive(true);
            catResult.add(cat);
        } else {
            cat.setActive(false);
            catResult.add(cat);

        }

        flag = false;
        List<SourceVO> sources = filmServiceAPI.getSources();
        List<SourceVO> sourceResult = new ArrayList<>();
        SourceVO source = new SourceVO();
        for (SourceVO sourceVO : sources) {
            if (sourceVO.getSourceId().equals("99")) {
                source = sourceVO;
                continue;
            }
            if (sourceVO.getSourceId().equals(sourceId)) {
                flag = true;
                sourceVO.setActive(true);
            } else {
                sourceVO.setActive(false);
            }
            sourceResult.add(sourceVO);

        }
        if (!flag) {
            source.setActive(true);
            sourceResult.add(source);
        } else {
            source.setActive(false);
            sourceResult.add(source);

        }

        flag = false;
        List<YearVO> years = filmServiceAPI.getYears();
        List<YearVO> yearResult = new ArrayList<>();
        YearVO year = new YearVO();
        for (YearVO yearVO : years) {
            if (yearVO.getYearId().equals("99")) {
                year = yearVO;
                continue;
            }
            if (yearVO.getYearId().equals(yearId)) {
                flag = true;
                yearVO.setActive(true);
            } else {
                yearVO.setActive(false);
            }
            yearResult.add(yearVO);

        }
        if (!flag) {
            year.setActive(true);
            yearResult.add(year);
        } else {
            year.setActive(false);
            yearResult.add(year);

        }

        filmConditionVO.setCatInfo(catResult);
        filmConditionVO.setSourceInfo(sourceResult);
        filmConditionVO.setYearInfo(yearResult);

        return ResponseVO.success(filmConditionVO);
    }


有點(diǎn)復(fù)雜,先遍歷一次算吩,遇到99了先存下來(lái)留凭,如果沒有匹配到的,說(shuō)明傳回來(lái)的就是99赌莺,那么把99全選傳回去就好了。沒有用到什么特別的算法松嘶,如果想快點(diǎn)用上二分可能好點(diǎn)艘狭。測(cè)試結(jié)果:


這樣就測(cè)試成功了。(WiFi斷掉才能練上這個(gè)問題還是存在翠订,WiFi連著巢音,可能可以找到服務(wù),WiFi斷開是一定可以找到服務(wù)尽超,問題還是message can not send官撼,channel is closed.)

影片查詢接口

影片查詢接口順便實(shí)現(xiàn)一次重構(gòu), 比如說(shuō)在首頁(yè)的時(shí)候:



islimit = true似谁,即為首頁(yè)傲绣,islimit為false則為列表掠哥,但是這樣是完全不能滿足功能需求的。影片查詢需要7個(gè)參數(shù)秃诵,影片類型续搀,排序方式,來(lái)源菠净,分類禁舷,年份,當(dāng)前第幾頁(yè)毅往,總頁(yè)數(shù)牵咙,如果使用參數(shù)直接散開傳到控制器是可以的,但是很麻煩攀唯,所以使用一個(gè)模型來(lái)接收洁桌。那么控制器主要做幾個(gè)事情:首先是根據(jù)showType判斷類型,接著根據(jù)sortID排序革答,然后添加各種查詢條件战坤,判斷當(dāng)前是第幾頁(yè)。之前有實(shí)現(xiàn)過geHotFilms等等類似功能的函數(shù)残拐,重構(gòu)這些函數(shù)途茫,改成可用的,先修改API:

    FilmVO getHotFilms(boolean isLimit, Integer nums, Integer nowPage, Integer sortId, Integer sourceId,Integer yearId, Integer catId);
    //get films displayed soon
    FilmVO getSoonFilms(boolean isLimit, Integer nums, Integer nowPage, Integer sortId, Integer sourceId,Integer yearId, Integer catId);
    FilmVO getClassicFilms(Integer nums, Integer nowPage, Integer sortId, Integer sourceId,Integer yearId, Integer catId);

修改查詢接口的時(shí)候sourceId和yearId是一樣的溪食,catId可能有點(diǎn)麻煩囊卜,一個(gè)電影可能有多個(gè)標(biāo)簽,可能既是動(dòng)作错沃,又是愛情栅组,所以數(shù)據(jù)庫(kù)里面是2#4#5#7這樣存放。如果查詢3枢析,那么可以查詢#3#玉掸,以此類推:

            Page<FilmT> page = new Page<>(nowPage, nums);
            if (sourceId != 99) {
                entityWrapper.eq("film_source", sourceId);
            }
            if (yearId != 99) {
                entityWrapper.eq("film_date", yearId);
            }
            if (catId != 99) {
                String catStr = "%#" + catId + "#%";
                entityWrapper.like("film_cats", catStr);
            }
            List<FilmT> films = filmTMapper.selectPage(page, entityWrapper);
            filmInfos = getFilmInfo(films);
            filmVO.setFilmNum(films.size());

            int totalCounts = filmTMapper.selectCount(entityWrapper);
            int totalPages = (totalCounts / nums) + 1;


            filmVO.setFilmInfo(filmInfos);
            filmVO.setTotalPage(totalPages);
            filmVO.setNowPage(nowPage);

別忘了還有排序,排序需要按照不同的需要對(duì)影片排序:

            switch (sortId){
                case 1:
                    page = new Page<>(nowPage, nums, "film_preSaleNum");
                    break;
                case 2:
                    page = new Page<>(nowPage, nums, "film_preSaleNum");
                    break;
                case 3:
                    page = new Page<>(nowPage, nums, "film_score");
                    break;
                default:
                    page = new Page<>(nowPage, nums, "film_preSaleNum");
                    break;
            }

把/film/getFilms加入忽略列表里面醒叁,首頁(yè)也是可以查詢的司浪,并且不需要登錄處理。如果出現(xiàn)一下:



那么這就調(diào)通了把沼。

影片詳情接口

套路都很簡(jiǎn)單啊易,首先在控制器添加一個(gè)方法,查詢?cè)敿?xì)信息本身是很簡(jiǎn)單的饮睬,只是簡(jiǎn)單的調(diào)用接口就好了沃缘,但是這里會(huì)使用到dubbo的一個(gè)特性——異步特性量九,那么這個(gè)時(shí)候就需要重新定義API了兔综。影片這里的詳細(xì)信息查詢關(guān)系到關(guān)聯(lián)查詢,在Mappering里面添加API:

public interface FilmTMapper extends BaseMapper<FilmT> {
    FilmDetailVO getFilmDetailByName(@Param("filmName") String filmName);
    FilmDetailVO getFilmDetailById(@Param("uuid") String uuid);


}

然后在Mapper添加基本語(yǔ)句窟却,然后重寫SQL語(yǔ)句了。主要是在film_t和film_info_t這兩張表進(jìn)行查詢劫拗,查詢的結(jié)果后還需要進(jìn)行拼接间校,其實(shí)還是挺簡(jiǎn)單的。



version1版本页慷,簡(jiǎn)單實(shí)現(xiàn)一下憔足。



拼接拼的差不多了,info1那里的轉(zhuǎn)換還有點(diǎn)問題酒繁,還有上映時(shí)間日期也沒有弄好:

組織好后面的上映日期∽艺茫現(xiàn)在就是要處理#1#2#4#5#這類字符串了,先去掉收尾的#號(hào)再把中間的#變成州袒,號(hào):

select trim(both '#' from film_cats) from film_t;

both就是收尾都要去掉揭绑。接著就是替換:

select replace(trim(both '#' from film_cats), '#', ',') from film_t;

這樣就替換成功了。那么只需要看看哪一個(gè)是IN (選擇語(yǔ)句)就好了郎哭,按道理:

select * from cat_dict_t where UUID in (select replace(trim(both '#' from film_cats), '#', ',') from film_t);

這樣就好了他匪,但是結(jié)果只是出現(xiàn)一個(gè):



所以這種做法是不可以的,F(xiàn)IND_IN_SET函數(shù)可以做到夸研,可以用find_in_set函數(shù)把他們套起來(lái)邦蜜,find_in_set(字段,子集)==》

select * from cat_dict_t t where  FIND_IN_SET(t.uuid, (select replace(trim(both '#' from film_cats), '#', ',') from film_t));


差不多了亥至,但是還有一個(gè)問題悼沈,就是如何拼接的問題,自己需要在中間加上group_concat(字段 separator ',')即可:

select group_concat(show_name separator ',') from cat_dict_t t where  FIND_IN_SET(t.uuid, (select replace(trim(both '#' from film_cats), '#', ',') from film_t));


那么接下來(lái)組織一下就好了姐扮,先簡(jiǎn)單的讀出數(shù)據(jù):

select film.film_name                             as filmName,
       info.film_en_name                          as filmEnName,
       film.img_address                           as imgAddress,
       info.film_score                            as score,
       info.film_score_num                        as scoreNum,
       film.film_box_office                       as totalBox,
       film_cats                                  as info1,
       concat(film.film_source, info.film_length) as info02,
       concat(film.film_time, (select show_name from source_dict_t where film_source = source_dict_t.UUID),
              '上映')                               as info03
from film_t film,
     film_info_t info
where film.UUID = info.film_id
  and film.UUID = 2;

接下來(lái)就是按照上面的做法先解析分類:

select film.film_name                                       as filmName,
       info.film_en_name                                    as filmEnName,
       film.img_address                                     as imgAddress,
       info.film_score                                      as score,
       info.film_score_num                                  as scoreNum,
       film.film_box_office                                 as totalBox,
       (select group_concat(show_name separator ',')
        from cat_dict_t t
        where FIND_IN_SET(t.UUID,
                          (select replace(trim(both '#' from film_cats), '#', ',')
                           from film_t
                           where film_t.UUID = film.UUID))) as info1,
       concat(film.film_source, info.film_length)           as info02,
       concat(film.film_time, (select show_name from source_dict_t where film_source = source_dict_t.UUID),
              '上映')                                         as info03
from film_t film,
     film_info_t info
where film.UUID = info.film_id
  and film.UUID = 2;

接下來(lái)就是后面的拼接:



這玩意有點(diǎn)嚇人絮供,講道理惊搏,還沒寫過這么長(zhǎng)的,如果把那些分類全部放程序里面的話有不好改,不靈活疑务。接下來(lái)就是補(bǔ)充基本的控制器即可,但是這上面完成的只是電影基本信息吱雏,電影的介紹,演員表,截圖等等還沒有完成活箕。接下來(lái)就是電影的演員蛮粮,電影與演員的對(duì)應(yīng)關(guān)系是一對(duì)多的關(guān)系,如果直接寫不太好些,所以用一個(gè)演員映射表來(lái)實(shí)現(xiàn):

CREATE TABLE mooc_film_actor_t(
  UUID INT PRIMARY KEY AUTO_INCREMENT COMMENT '主鍵編號(hào)',
  film_id INT COMMENT '影片編號(hào),對(duì)應(yīng)mooc_film_t',
  actor_id INT COMMENT '演員編號(hào),對(duì)應(yīng)mooc_actor_t',
  role_name VARCHAR(100) COMMENT '角色名稱'
) COMMENT '影片與演員映射表' ENGINE = INNODB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;


role_name不是演員名字,是角色泡嘴,比如在電影里面的導(dǎo)演還是演員磺箕,這里也需要聯(lián)合查詢:


select actor.actor_name as directorName, actor_img as imgAddress, rela.role_name as roleName
from actor_t actor,
     film_actor_t rela
where actor.UUID = rela.actor_id
  and rela.film_id = 2;

這個(gè)查詢比上面的要簡(jiǎn)單多了,聯(lián)合幾個(gè)表抛虫,也不需要嵌套查詢松靡。這里要返回的json串:



這整個(gè)json作為一個(gè)對(duì)象FilmRequestVO,這個(gè)FilmRequestVO又包含幾個(gè)對(duì)象建椰,status和imgPre都是原來(lái)Respose就有的雕欺,只需要把剩下的加上就好,data那塊是一個(gè)對(duì)象棉姐,而data里面info04又做成一個(gè)對(duì)象屠列,director和actor又是一個(gè)對(duì)象,以此類推伞矩,賊多笛洛。



接下來(lái)就是控制器訪問的問題了:

ignore_url里面直接加film/films是不行的,因?yàn)樵贏uthfilter里面是equal乃坤,相等在能匹配苛让,只需要把Authfilter里面改成startwith即可:




調(diào)試成功。現(xiàn)在還有一個(gè)小小的問題湿诊,目前這種情況很快狱杰,數(shù)據(jù)量小一下子就出來(lái)了,假設(shè)每個(gè)接口都是200ms厅须,那么這個(gè)請(qǐng)求接口一共就演示1秒了浦旱,真是很大的延遲。而dubbo與一個(gè)特性九杂,即異步調(diào)用颁湖。

異步調(diào)用

dubbo的異步調(diào)用特性基于NIO的非阻塞實(shí)現(xiàn)并進(jìn)行調(diào)用,客戶端是不需要啟動(dòng)多線程例隆。

首先用戶線程調(diào)用服務(wù)甥捺,進(jìn)入IOThread,IOthread請(qǐng)求服務(wù)器獲取返回镀层,然后不等待服務(wù)器返回镰禾,立刻設(shè)置一個(gè)future對(duì)象皿曲,future對(duì)象標(biāo)識(shí)一個(gè)可能還沒有完成的任務(wù)結(jié)果,等到完成了在通知future吴侦,然后接受返回屋休。原本是發(fā)出請(qǐng)求,等待執(zhí)行完成后返回备韧,現(xiàn)在是已發(fā)出請(qǐng)求就返回劫樟,用future來(lái)接受,立刻返回后就跑去執(zhí)行其他的织堂,這就做到異步了叠艳。
比如啟動(dòng)異步調(diào)用之后,執(zhí)行方法:

fooService.findFoo(fooId);

這個(gè)方法直接返回一個(gè)null易阳,因?yàn)椴坏却Y(jié)果了附较,接著dubbo會(huì)自動(dòng)生成一個(gè)funture對(duì)象,因?yàn)樾枰胒uture對(duì)象存儲(chǔ)返回?cái)?shù)據(jù)潦俺,所以需要獲取到Future:

Future<Foo> fooFuture = RpcContext.getContext().getFuture();

再?gòu)倪@個(gè)future對(duì)象獲取信息:

Foo foo = fooFuture.get();

明白原理之后就要把異步調(diào)用用到業(yè)務(wù)上了拒课。剛剛返回電影信息詳細(xì)可以使用

    @Reference(interfaceClass = FilmServiceAPI.class, async = true)
    private FilmServiceAPI filmServiceAPI;

如果這樣使用,那么但凡是跟future沒有關(guān)系的都會(huì)報(bào)錯(cuò)事示,比如在剛剛獲取詳細(xì)信息過程中:

        FilmDetailVO filmDetail = filmServiceAPI.getFilmDetail(searchType, searchParam);

        if (filmDetail == null) {
            return ResponseVO.serviceFail("沒有可查詢影片");
        } else if (filmDetail.getFilmId() == null || filmDetail.getFilmId().trim().length() == 0) {
            return ResponseVO.serviceFail("沒有可查詢影片");

        }

        String filmId = filmDetail.getFilmId();

前面這部分是獲取ID的捕发,后面都是根據(jù)ID去查找其他的信息,那么如果異步處理后面的ID可能null值了很魂。把需要異步的方法全部封裝成另外一個(gè)API進(jìn)行處理扎酷,剩下的方法留著。

    @Reference(interfaceClass = FilmServiceAPI.class)
    private FilmServiceAPI filmServiceAPI;

    @Reference(interfaceClass = FilmAsyncServiceAPI.class, async = true)
    private FilmAsyncServiceAPI filmAsyncServiceAPI;

異步調(diào)用需要加上async = true遏匆。



        filmAsyncServiceAPI.getFilmDesc(filmId);
        Future<FilmDescVO> filmDescVOFuture = RpcContext.getContext().getFuture();

        filmAsyncServiceAPI.getImgs(filmId);
        Future<ImgVO> imgVOFuture = RpcContext.getContext().getFuture();

        filmAsyncServiceAPI.getDectInfo(filmId);
        Future<ActorVO> directorVOFuture = RpcContext.getContext().getFuture();

        filmAsyncServiceAPI.getActors(filmId);
        Future<List<ActorVO>> actorVOFuture = RpcContext.getContext().getFuture();

        InfoRequestVO infoRequestVO = new InfoRequestVO();

        ActorRequestVO actorRequestVO = new ActorRequestVO();
        actorRequestVO.setActors(actorVOFuture.get());
        actorRequestVO.setDirector(directorVOFuture.get());

        infoRequestVO.setActors(actorRequestVO);
        infoRequestVO.setBiography(filmDescVOFuture.get().getBiography());
        infoRequestVO.setFilmId(filmId);
        infoRequestVO.setImgVO(imgVOFuture.get());

需要用future對(duì)象來(lái)接收數(shù)據(jù)法挨。和示例一樣,注意異步需要手動(dòng)開啟幅聘,在啟動(dòng)類加上@EnableAsync

影院模塊

影院這塊相對(duì)簡(jiǎn)單一些凡纳,6張表,品牌字典表帝蒿,其實(shí)就是類似于map這樣的映射表荐糜,地域字典表,影廳字典表葛超,影院主表暴氏,影院的詳細(xì)信息,在熱映電影字典表绣张,放映場(chǎng)次信息表答渔。熱映電影表其實(shí)是不需要的,但是為了減少數(shù)據(jù)查詢的負(fù)荷侥涵,還是以空間換時(shí)間沼撕,數(shù)據(jù)庫(kù)往往是系統(tǒng)的瓶頸宋雏。
首先先給出接口大致框架:

@RestController
@RequestMapping("/cinema/")
public class CinemaController {
    @Reference(interfaceClass = CinemaServiceAPI.class, check = false)
    private CinemaServiceAPI cinemaServiceAPI;

    @RequestMapping(value = "getCinemas", method = RequestMethod.GET)
    public ResponseVO getCinemas(CinemaQueryVO cinemaQueryVO) {
        return null;
    }

    @RequestMapping(value = "getCondition", method = RequestMethod.GET)
    public ResponseVO getCondition(CinemaQueryVO cinemaQueryVO) {
        return null;
    }

    @RequestMapping(value = "getFields")
    public ResponseVO getFields(Integer cinemaId) {
        return null;
    }

    @RequestMapping(value = "getFieldInfo", method = RequestMethod.POST)
    public ResponseVO getFieldInfo(Integer cinemaId, Integer fieldId) {
        return null;
    }

}


接著還按照返回報(bào)文建立響應(yīng)的數(shù)據(jù)結(jié)構(gòu)。需要注意的就是已售座位這里务豺,只有下了訂單之后才能填上已售座位磨总,所以只能先寫死,等到訂單完成后在回來(lái)補(bǔ)齊笼沥。下面先分析一下所需要的接口蚪燕,首先第一個(gè)接口getCinemas接口,就是入?yún)⒊鰠⒓纯删赐兀凑瘴鍌€(gè)條件進(jìn)行帥選,判斷是否有滿足條件的影院裙戏,出現(xiàn)異常應(yīng)該怎么處理乘凸。入口參數(shù):CinemaQueryVO,出參就是cinemaVO對(duì)象累榜;然后是getCondition按照條件查詢营勤,1.需要獲取品牌列表,也就是影院是哪個(gè)公司的壹罚,2.所在區(qū)域葛作,3.影廳又是什么類型;getFields根據(jù)影院變化獲取影院信息猖凛,獲取所有電影信息和對(duì)應(yīng)的場(chǎng)次信息赂蠢,影院編號(hào)。接下來(lái)就是獲取場(chǎng)次詳細(xì)信息辨泳,根據(jù)編號(hào)獲取場(chǎng)次詳細(xì)信息虱岂,根據(jù)反映場(chǎng)次ID獲取反映信息,根據(jù)反映場(chǎng)次查詢播放電影菠红,根據(jù)電影編號(hào)獲取電影信息第岖,還有一個(gè)售賣座位的信息是需要通過訂單實(shí)現(xiàn)的,所以待實(shí)現(xiàn)试溯。
列出實(shí)現(xiàn)接口:

public interface CinemaServiceAPI {
    Page<CinemaVO> getCinemas(CinemaQueryVO cinemaQueryVO);

    List<BrandVO> getBrands(Integer brandId);

    List<AreaVO> getAreas(Integer areaId);

    List<HallTypeVO> getHallTypes(Integer hallType);

    CinemaInfoVO getCinemaInfoById(Integer cinemaId);

    FilmInfoVO getFilmInfoByCinemaId(Integer cinemaId);

    FilmFieldVO getFilmFieldInfo(Integer fieldId);

    FilmInfoVO getFilmInfoByFieldId(Integer fieldId);
}

這里嗎有點(diǎn)復(fù)雜的其實(shí)就是getFilmInfoByCinemaId()蔑滓;首先要根據(jù)影院ID去field_t表把這個(gè)影院的場(chǎng)次全部查出,還要去hall_film_t表把對(duì)于電影名字給出并篩選遇绞。這里可能有些困難的是hall_film_info_t和field_t的關(guān)系键袱,hall_film_info_t是影廳播放的電影,而field是場(chǎng)次信息摹闽,一個(gè)hall_film_t會(huì)對(duì)應(yīng)多個(gè)field杠纵,這個(gè)時(shí)候如果用聯(lián)合查詢只能查出一個(gè),查不出整個(gè)集合钩骇,所以只能用聯(lián)合查詢了:

select info.film_id,
       info.film_name,
       info.film_length,
       info.film_language,
       info.film_cats,
       info.actors,
       info.img_address,
       f.UUID,
       f.begin_time,
       f.begin_time,
       f.hall_name,
       f.price
from hall_film_info_t info
         left join field_t f
                   on f.film_id = info.film_id
                       and f.cinema_id = '1';


但是問題來(lái)了:

@Data
public class FilmInfoVO implements Serializable {
    private String filmId;
    private String filmName;
    private String filmLength;
    private String filmType;
    private String filmCats;
    private String actors;
    private String imgAddress;
    private List<FilmFieldVO> filmFields;
}

最后一個(gè)是一個(gè)集合對(duì)象比藻,怎么返回铝量?只能使用mybatis SQL自定義來(lái)處理了,首先mapper設(shè)置一個(gè)接口银亲,在mapper的xml文件里面進(jìn)行實(shí)現(xiàn)即可慢叨。

    <!-- 通用查詢映射結(jié)果 -->
    <resultMap id="BaseResultMap" type="com.stylefeng.guns.rest.common.persistence.model.FieldT">
        <id column="UUID" property="uuid"/>
        <result column="cinema_id" property="cinemaId"/>
        <result column="film_id" property="filmId"/>
        <result column="begin_time" property="beginTime"/>
        <result column="end_time" property="endTime"/>
        <result column="hall_id" property="hallId"/>
        <result column="hall_name" property="hallName"/>
        <result column="price" property="price"/>
    </resultMap>

    <resultMap id="getFilmInfoMap" type="com.stylefeng.guns.api.cinema.vo.FilmInfoVO">
        <result column="film_id" property="filmId"></result>
        <result column="film_name" property="filmName"></result>
        <result column="film_length" property="filmLength"></result>
        <result column="film_language" property="filmType"></result>
        <result column="film_cats" property="filmCats"></result>
        <result column="actors" property="actors"></result>
        <result column="img_address" property="imgAddress"></result>
        <collection property="filmFields" ofType="com.stylefeng.guns.api.cinema.vo.FilmFieldVO">
            <result column="UUID" property="fieldId"></result>
            <result column="begin_time" property="beginTime"></result>
            <result column="end_time" property="endTime"></result>
            <result column="film_language" property="language"></result>
            <result column="hall_name" property="hallName"></result>
            <result column="price" property="price"></result>
        </collection>
    </resultMap>
    <select id="getFilmInfos" parameterType="java.lang.Integer" resultMap="getFilmInfoMap">
select info.film_id,
       info.film_name,
       info.film_length,
       info.film_language,
       info.film_cats,
       info.actors,
       info.img_address,
       f.UUID,
       f.begin_time,
       f.end_time,
       f.hall_name,
       f.price
from hall_film_info_t info
         left join field_t f
                   on f.film_id = info.film_id
                       and f.cinema_id = #{cinemaId}
    </select>
</mapper>

resultMap定義自定義對(duì)象,這樣就可以返回自定義類型了务蝠。接著實(shí)現(xiàn)業(yè)務(wù)層什么的都很簡(jiǎn)單拍谐。簡(jiǎn)要提一下lombok做日志,平時(shí)做日志都是要private Logger log......馏段,可以加上@SLf4j注釋轩拨,就可以省略掉上述的過程。然后就是測(cè)試了院喜。

dubbo結(jié)果緩存

對(duì)于getCondition這個(gè)方法亡蓉,一般是熱點(diǎn)數(shù)據(jù),這個(gè)數(shù)據(jù)會(huì)被頻繁的使用喷舀,這種熱點(diǎn)數(shù)據(jù)一般處理都很簡(jiǎn)單砍濒,就是放到緩存,對(duì)于dubbo提供的結(jié)果緩存硫麻,是針對(duì)已經(jīng)存在的大量頻繁訪問的數(shù)據(jù)爸邢,存儲(chǔ)在本地緩存中,存在當(dāng)前是jvm里面拿愧,所以可能會(huì)存有多份緩存杠河,訪問也更快。緩存類型有三種浇辜,lru緩存感猛,和os的頁(yè)面置換類似,最近最少使用緩存奢赂,threadlocal當(dāng)前線程緩存陪白,還有一種是jcache,這種比較少見,基本都是前面兩種,但是注意threadlocal不適合大量數(shù)據(jù)刘离。配置其實(shí)很簡(jiǎn)單版确,在接口加上配置即可:

    @Reference(interfaceClass = CinemaServiceAPI.class, check = false, cache = "lru")
    private CinemaServiceAPI cinemaServiceAPI;

并發(fā)連接控制

同時(shí),dubbo可以對(duì)并發(fā)和連接數(shù)量進(jìn)行控制,可以在配置文件設(shè)置并發(fā)控制數(shù)量等等。首先明確,如果并發(fā)與連接數(shù)量超出了弛房,并不會(huì)等待,會(huì)以錯(cuò)誤的形式進(jìn)行返回而柑,dubbo本身雖然有服務(wù)降級(jí)文捶,但服務(wù)降級(jí)這個(gè)東西實(shí)現(xiàn)的并沒有特別好荷逞,其次,dubbo本身是有服務(wù)守恒的問題粹排,但是在以前dubbo防止雪崩是通過控制并發(fā)與連接數(shù)量來(lái)控制的种远,尤其是連接。雪崩:目前3個(gè)服務(wù)顽耳,其中一個(gè)服務(wù)不知道為什么原因并發(fā)量非常大坠敷,超出了他本身所能承受的力度,然后這個(gè)服務(wù)就只能被沖崩了射富,然后這些請(qǐng)求又全部被送到了二號(hào)膝迎,二號(hào)又雪崩了以此類推。所以就叫雪崩胰耗。以往對(duì)于這種控制是用控制連接與并發(fā)數(shù)限次。

訂單模塊

訂單模塊這玩意,之前都沒有碰過宪郊,訂單模塊主要是涉及一些dubbo特性或者是服務(wù)配置的問題掂恕,訂單本身的業(yè)務(wù)很簡(jiǎn)單拖陆,就兩個(gè)弛槐,下訂單,查看訂單依啰,沒了乎串,服務(wù)配置比如限流,服務(wù)降級(jí)速警,熔斷等等叹誉,dubbo本身有熔斷,但是這個(gè)實(shí)現(xiàn)不太好闷旧,所以使用其他的熔斷器來(lái)進(jìn)行處理长豁。首先是安裝ftp,10.13版本前的ISO是自帶的忙灼,Mac往后版本是沒有的了匠襟,我的恰好是10.14的,ftp沒有帶上自帶了sftp该园,sftp是ftp的變體酸舍,F(xiàn)TP另外一種是TFTP,F(xiàn)TP里初,TFTP啃勉,SFTP都是三種文件傳輸,區(qū)別就在于双妨,F(xiàn)TP是需要在可信賴網(wǎng)絡(luò)上傳輸淮阐,他沒有很高的安全加密叮阅,SFTP有,如果信息很銘感枝嘶,那就需要用SFTP了帘饶,增加了安全層進(jìn)行信息加密,TFTP即是簡(jiǎn)單文件傳輸群扶,基本適用于局域網(wǎng)及刻,而且與其他兩種協(xié)議不同就在于,F(xiàn)TP和SFTP使用TCP協(xié)議竞阐,而TFTP使用UDP協(xié)議缴饭,既然自帶了那就使用sftp充當(dāng)FTP吧。
首先對(duì)于購(gòu)票業(yè)務(wù)骆莹,后端要有一個(gè)原則颗搂,永遠(yuǎn)都不能相信前端給你的東西,因?yàn)槭强梢酝ㄟ^前端進(jìn)行更改的幕垦,所以要驗(yàn)證是否為真丢氢。影院列表是使用json文件,判斷座位id是不是正確先改,判斷在訂單里面有沒有座位id疚察,既然售出自然不能再買了,驗(yàn)證完這些后才能創(chuàng)建訂單信息仇奶。對(duì)于訂單業(yè)務(wù)貌嫡,獲取當(dāng)前登錄信息,獲取訂單即可该溯。由于座位信息是通過json文件傳輸岛抄,需要從ftp服務(wù)器獲取,但是我的Mac沒有ftp狈茉,就去阿里云找了一個(gè)夫椭。使用FileZilla用戶root發(fā)現(xiàn)登錄成功,但是獲取目錄失敗了氯庆,密碼也沒有錯(cuò)蹭秋,錯(cuò)誤原因有可能就是端口了,但是21 20端口開了点晴,21傳輸命令感凤,20端口傳輸文件。這里的問題確實(shí)實(shí)在端口粒督,但是是在ftp的傳輸方式上陪竿,ftp有兩種傳輸方式,一種是主動(dòng),一種是被動(dòng)族跛,主動(dòng):服務(wù)端來(lái)找客戶端闰挡,通過21傳輸命令,通過20傳輸數(shù)據(jù)礁哄;被動(dòng)方式:客戶端找服務(wù)端长酗,命令還是用21端口,但是文件使用1025-65535隨機(jī)一個(gè)桐绒,而FileZilla默認(rèn)使用passive mode夺脾,自然要開啟全部了。所以當(dāng)輸入賬戶密碼的時(shí)候茉继,即是命令登錄咧叭,使用21烁竭,沒有問題菲茬,但是文件目錄是使用隨機(jī)端口了,所以出現(xiàn)問題派撕。408歷年真題有一個(gè)選項(xiàng)就是這玩意婉弹,ftp任何情況下傳輸文件使用20端口,錯(cuò)誤的终吼,主動(dòng)才是镀赌。
然后就是配置阿里云服務(wù)器了,配置很簡(jiǎn)單衔峰,讀取信息的那些stream可能有點(diǎn)煩:


    public String getFileStrByAddress(String fileAddress) {
        initFTPClient();
        BufferedReader bufferedReader = null;
        try {
            bufferedReader = new BufferedReader(new InputStreamReader(ftpClient.retrieveFileStream(fileAddress)));
            StringBuffer stringBuffer = new StringBuffer();
            while (true) {
                String lnStr = bufferedReader.readLine();
                if (lnStr == null) {
                    break;
                }
                stringBuffer.append(lnStr);
            }
            ftpClient.logout();
            return stringBuffer.toString();
        } catch (Exception e) {
            log.error("獲取文件選項(xiàng)失敗");
        } finally {
            try {
                bufferedReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

測(cè)試通過之后佩脊,把ftp的相關(guān)信息配置到application.yml蛙粘,如果出現(xiàn)-u會(huì)自動(dòng)轉(zhuǎn)換成大寫垫卤。

訂單模塊業(yè)務(wù)實(shí)現(xiàn)

業(yè)務(wù)實(shí)現(xiàn)這塊下單有點(diǎn)復(fù)雜,存儲(chǔ)沒有什么問題出牧,返回的時(shí)候需要多個(gè)表的信息穴肘,影院名稱,電影名稱舔痕,電影價(jià)格评抚,訂單總價(jià)等等信息,但是還是很好寫伯复。因?yàn)橥ǔY?gòu)買完成之后慨代,需要返回已經(jīng)插入的訂單:



fieldTime的組織有點(diǎn)麻煩,首先要把orderTimestamp變成格式化拼接啸如,然后加上今天即可:

select o.UUID                                                                as orderId,
       h.film_name                                                           as filmName,
       concat('今天 ', DATE_FORMAT(o.order_time, '%m月%d日'), ' ', f.begin_time) as fieldTime,
       c.cinema_name                                                         as cinemaName,
       o.seats_name                                                          as seatsName,
       o.order_price                                                         as orderPrice,
       UNIX_TIMESTAMP(o.order_time)                                                          as orderTimestamp
from order_t o,
     field_t f,
     hall_film_info_t h,
     cinema_t c
where o.cinema_id = c.UUID
  and o.field_id = f.UUID
  and o.film_id = h.film_id
  and o.UUID = "415sdf58ew12ds5fe1";
    

業(yè)務(wù)層的實(shí)現(xiàn)都很簡(jiǎn)單侍匙,沒有什么問題。然后是實(shí)現(xiàn)得到已售出的座位叮雳,concat和group_concat的區(qū)別想暗,concat的實(shí)現(xiàn)對(duì)象是字符串妇汗,group_concat是可以在分組或不同表時(shí)間實(shí)現(xiàn)的,可以和group by和起來(lái)使用说莫。注意之前哎cinema模塊也要把原來(lái)沒有實(shí)現(xiàn)的已售出座位修改一下杨箭。

            hallFieldInfo.setSoldSeats(orderServiceAPI.getSoldSeatsByFieldId(fieldId));

cinema這塊信息加上,postman測(cè)試一下:


對(duì)于WiFi打開就連接不上的問題储狭,初步懷疑是虛擬ip的原因互婿,因?yàn)樵诰W(wǎng)上有人是因?yàn)殡娔X有兩張網(wǎng)卡,一張有線以太網(wǎng)網(wǎng)卡辽狈,一張是無(wú)線網(wǎng)卡擒悬,結(jié)果接到無(wú)線網(wǎng)卡去了,但是我的Mac又沒有以太網(wǎng)網(wǎng)卡稻艰,設(shè)置里面也查看了確實(shí)是802.11無(wú)線局域網(wǎng)懂牧,使用CSMA/CA協(xié)議,應(yīng)該是連接WiFi導(dǎo)致是ip發(fā)生變化尊勿。
測(cè)試完成后基本上業(yè)務(wù)這塊就完成了僧凤,但是訂單模塊的拓展還是比較多的。首先元扔,一般來(lái)說(shuō)在訂單這塊躯保,業(yè)務(wù)量是比較大的,每一部電影的訂單量是很大的澎语,比如戰(zhàn)狼2賣了50多個(gè)億途事,在這種情況下,訂單不會(huì)再數(shù)據(jù)庫(kù)用一張表存擅羞,這就涉及到了訂單模塊的橫向和縱向的拆分尸变;其次,還有服務(wù)限流减俏,服務(wù)限流不僅僅是局限于訂單系統(tǒng)召烂,其他的也有可能包含,就比如LOL比賽的觀看人數(shù)等等都需要限流娃承。還有熔斷和降級(jí)奏夫,這類主要解決服務(wù)器雪崩的情況。

訂單橫向縱向拆分

比如像京東天貓历筝,天貓一開始是賣小商品為主酗昼,那么運(yùn)行十年之后,有一部分是服裝梳猪,有一部分是家電麻削,這些在訂單表里面是混淆在一起的,一億條訂單表里面7000條服裝,3000條小商品碟婆,如果混在一起电抚,對(duì)于分析查找都是不方便的,這個(gè)時(shí)候就會(huì)引入橫向拆分竖共,比如家電表蝙叛,比如小商品表,縱向拆分就是按照時(shí)間拆分公给,比如2017年一張表借帘,2018年一張表等等。但是這樣拆分淌铐,業(yè)務(wù)也會(huì)復(fù)雜肺然,比如現(xiàn)在有兩張表,order_2017_t腿准,order_2018_t兩張表际起。這里涉及到一個(gè)dubbo的特性,服務(wù)分組吐葱,當(dāng)一個(gè) 接口有幾個(gè)實(shí)現(xiàn)街望,可以用group來(lái)區(qū)分,分組聚合弟跑,從每一個(gè)group中調(diào)用返回結(jié)果灾前,并合并返回結(jié)果。group關(guān)鍵字進(jìn)行分組孟辑,merge關(guān)鍵子進(jìn)行合并哎甲,合并也需要注意,只有返回了list或者collection一類的才能合并饲嗽,但是當(dāng)前使用的dubbo并不支持炭玫,所以要合并也只能手動(dòng)合并。但是如果這樣分組和并就要改業(yè)務(wù)了喝噪,所以知道怎么回事础嫡,但是就不改了霸琴。

服務(wù)限流

首先服務(wù)限流是系統(tǒng)高可用的一種手段座柱,對(duì)于業(yè)務(wù)上面沒有任何幫助沈善,完全是為了高并發(fā)。dubbo也有并發(fā)和控制連接數(shù)來(lái)限流控制晚唇。主要是用于服務(wù)之間的限流,限流算法有兩種盗似,漏桶法和令牌桶法哩陕。
漏桶法:


水龍頭的請(qǐng)求,下面的整個(gè)是業(yè)務(wù)系統(tǒng),無(wú)論來(lái)多少請(qǐng)求都會(huì)先裝載這個(gè)桶里面然后然后再處理悍及。所有的請(qǐng)求進(jìn)來(lái)都會(huì)被排列成一個(gè)隊(duì)列闽瓢,然后按照相同的速度進(jìn)行處理。
令牌桶算法:

arrival即請(qǐng)求心赶,在請(qǐng)求進(jìn)入的同時(shí)還有一個(gè)保護(hù)現(xiàn)場(chǎng)扣讼,這個(gè)保護(hù)線程擁有令牌,線程進(jìn)入之后只有拿到令牌才能被處理缨叫,否則會(huì)被丟棄或返回椭符,他與漏桶算法的最大區(qū)別就在于這玩意的業(yè)務(wù)請(qǐng)求峰值是有一定承載能力的,比如桶里面有1000個(gè)令牌耻姥,1000個(gè)業(yè)務(wù)進(jìn)來(lái)可以并發(fā)全部執(zhí)行完销钝,但是對(duì)于漏桶算法無(wú)論還有多少內(nèi)存或者是擠壓空間,處理速度都還是這么快琐簇。另外蒸健,令牌桶算法可以通過改變添加令牌的速度來(lái)控制請(qǐng)求的處理,防止業(yè)務(wù)崩掉婉商。
簡(jiǎn)單實(shí)現(xiàn)一下令牌桶算法:
首先需要準(zhǔn)備桶的數(shù)量:

    private int bucketNums = 100;
    private int rate = 1;
    private int nowTokens = 0;
    private long timestamp = getNowTime();

按照每毫秒添加一個(gè)令牌的速度進(jìn)行業(yè)務(wù)請(qǐng)求控制纵装。


    public boolean getToken() {
        long nowTime = getNowTime();
        nowTokens = nowTokens + (int) ((nowTime - timestamp) * rate);
        nowTokens = min(nowTokens);
        setTimestamp(nowTime);
        System.out.println("當(dāng)前令牌數(shù):" + nowTokens);
        if (nowTokens < 1) {
            return false;
        } else {
            nowTokens--;
            return true;
        }
    }

每一次請(qǐng)求看看時(shí)間離上一次申請(qǐng)令牌過去了多久,按照毫秒把令牌數(shù)補(bǔ)上据某,如果令牌數(shù)是大于0的橡娄,允許運(yùn)行,然后減一癣籽。



這樣就實(shí)現(xiàn)了挽唉,加入到工程里面。在訂單系統(tǒng)里面筷狼,getOrderInfo這個(gè)頻率不太高瓶籽,主要是下單的頻率很高,在下單處增加:


熔斷降級(jí)

服務(wù)的穩(wěn)定是公司可持續(xù)發(fā)展的重要基石埂材,隨著業(yè)務(wù)量的快速發(fā)展塑顺,一些平時(shí)正常運(yùn)行的服務(wù),會(huì)出現(xiàn)各種突發(fā)狀況俏险,而且在分布式系統(tǒng)中严拒,每個(gè)服務(wù)本身又存在很多不可控的因素,比如線程池處理緩慢竖独,導(dǎo)致請(qǐng)求超時(shí)裤唠,資源不足,導(dǎo)致請(qǐng)求被拒絕莹痢,又甚至直接服務(wù)不可用种蘸、宕機(jī)墓赴、數(shù)據(jù)庫(kù)掛了、緩存掛了航瞭、消息系統(tǒng)掛了...對(duì)于一些非核心服務(wù)诫硕,如果出現(xiàn)大量的異常,可以通過技術(shù)手段刊侯,對(duì)服務(wù)進(jìn)行降級(jí)并提供有損服務(wù)痘括,保證服務(wù)的柔性可用,避免引起雪崩效應(yīng)滔吠。實(shí)時(shí)監(jiān)控接口的健康值纲菌,在達(dá)到熔斷條件時(shí),自動(dòng)開啟熔斷疮绷,開啟熔斷之后翰舌,如何實(shí)現(xiàn)自動(dòng)恢復(fù)?每隔一段時(shí)間冬骚,釋放一個(gè)請(qǐng)求到服務(wù)端進(jìn)行探測(cè)椅贱,如果后端服務(wù)已經(jīng)恢復(fù),則自動(dòng)恢復(fù)只冻。比如庇麦,如果ServiceA調(diào)用ServiceD一直失敗,或者失敗率很高喜德,就可以采用“一種機(jī)制”確保后續(xù)請(qǐng)求不會(huì)調(diào)用ServiceD山橄,而是執(zhí)行降級(jí)邏輯。
使用Hystrix作為熔斷降級(jí)工具舍悯,Hystrix主要有兩種命令模式:



hystrix command模式有主要是單線程進(jìn)行航棱,而Observable Command可以使用線程池等等。請(qǐng)求從command進(jìn)入萌衬。



經(jīng)過toObservable之后進(jìn)入第三步饮醇,判斷目前的結(jié)果是不是在緩存里,不在的話繼續(xù)往下做秕豫,判斷斷路器是否開啟(第四步)朴艰。本來(lái)的業(yè)務(wù)線是A調(diào)用B,這是一條通路混移,熔斷就是把A到B切斷祠墅,熔斷器開啟的意思就是是不是把這條路切斷了,切斷了那就簡(jiǎn)單了沫屡,直接走返回饵隙。接下來(lái)判斷線程池狀態(tài),如果都是OK沮脖,那么繼續(xù)往下走金矛,到達(dá)第六步,如果執(zhí)行成功了勺届,啥事沒有驶俊,失敗了或者是超時(shí)了,注意在熔斷器機(jī)制下免姿,不僅僅是執(zhí)行失敗的饼酿,超時(shí)也算是失敗的。到達(dá)第8步胚膊,是失敗調(diào)用的故俐,這一步就叫降級(jí)返回,也叫服務(wù)降級(jí)紊婉。服務(wù)熔斷是判斷要不要把這條路干掉药版,一旦出現(xiàn)業(yè)務(wù)異常,就調(diào)用服務(wù)降級(jí)喻犁,把業(yè)務(wù)返回槽片。比如之前是A調(diào)用B,降級(jí)就是不調(diào)用B肢础,使用一種折中的方法返回还栓,比如今天想打游戲,電腦宕機(jī)了传轰,不會(huì)直接告訴你我炸機(jī)了剩盒,hystrix會(huì)返回電腦沒電了,游戲倒閉了等等比較折中的方案慨蛙。
首先導(dǎo)包了:
      <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>

接著設(shè)置注解開啟熔斷器:

    @EnableHystrixDashboard
    @EnableCircuitBreaker
    @EnableHystrix

在需要熔斷的方法上加上注解:

@HystrixCommand(fallbackMethod = "error", commandProperties = {
@HystrixProperty(name="execution.isolation.strategy", value = "THREAD"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value
= "4000"),//超時(shí)時(shí)間
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),//出現(xiàn)10次例外
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
}, threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "1"),
@HystrixProperty(name = "maxQueueSize", value = "10"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "1000"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "8"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1500")
})

加上熔斷器之后需要右一個(gè)一模一樣參數(shù)返回值的方法勃刨,對(duì)應(yīng)fallbackMethod即可。注解上的配置名字都很明顯了股淡,沒有上面需要解釋的身隐,超過4秒即為超時(shí),10次例外就開啟你熔斷機(jī)制等等唯灵。
添加了Hystrix注解之后贾铝,會(huì)發(fā)現(xiàn)Threadlocal用不了了,這是因?yàn)镠ystrix本身有線程隔離埠帕,線程池保護(hù)和信號(hào)量機(jī)制垢揩,所以會(huì)切換線程,這個(gè)時(shí)候ThreadLocal就有用了敛瓷,那就找一個(gè)可以緩存之前信息的叁巨,就是InheritableThreadLocal。

public class CurrentUser {
    private static final InheritableThreadLocal<String> threadlocal = new InheritableThreadLocal<>();

    public static void saveUserId(String userId) {
        threadlocal.set(userId);
    }

    public static String getCurrentUser() {
        return threadlocal.get();
    }

}

這樣就可以保存之前的信息呐籽。忘記開WiFi了:



服務(wù)降級(jí)成功了锋勺。感覺服務(wù)降級(jí)好像不止這些功能蚀瘸,上面實(shí)現(xiàn)的大概類似于安撫的一種做法,當(dāng)然庶橱,也可以設(shè)計(jì)一個(gè)隊(duì)列贮勃,把請(qǐng)求追加在隊(duì)列里面。

支付模塊

支付模塊做簡(jiǎn)單點(diǎn)苏章,對(duì)接支付寶即可寂嘉。支付流程:首先要獲取二維碼,用戶掃描支付二維碼之后枫绅,系統(tǒng)后臺(tái)是不知道泉孩,只能等支付寶回調(diào),然后修改訂單狀態(tài)并淋,最后還需要定期對(duì)賬寓搬。但是等待支付寶回調(diào)有點(diǎn)麻煩,現(xiàn)在還沒有一個(gè)公網(wǎng)的地址预伺,所以只能啟動(dòng)另外一個(gè)流程订咸,首先Consumer是服務(wù)端消費(fèi),客戶端就是前端操作系統(tǒng)酬诀,Consumer獲取二維碼送到前端客戶端上脏嚷,前端進(jìn)行掃描支付,這個(gè)時(shí)候后端會(huì)啟用請(qǐng)求調(diào)用來(lái)查詢前端支付狀態(tài)瞒御,同時(shí)修改數(shù)據(jù)庫(kù)父叙,把訂單修改成已支付,當(dāng)然了肴裙,支付流程不夠嚴(yán)謹(jǐn)趾唱,但是已經(jīng)能夠完成了。簡(jiǎn)單看一下開發(fā)文檔蜻懦,本身支付寶是需要身份驗(yàn)證甜癞,營(yíng)業(yè)執(zhí)照等等,這里肯定沒有了宛乃,所以只能用沙箱版的支付寶悠咱,也就是沙箱環(huán)境做測(cè)試,全部都是假的征炼。然后配置一下沙箱環(huán)境析既,按照文檔:


填寫配置文件信息。properties里面有公鑰和私鑰谆奥。現(xiàn)在有兩個(gè)環(huán)境眼坏,一個(gè)是商戶,一個(gè)是支付寶酸些,相互都持有一個(gè)公鑰宰译,這個(gè)公鑰是以明文傳輸檐蚜,沒有安全性,比如近代戰(zhàn)爭(zhēng)通信進(jìn)程會(huì)有一個(gè)秘密本囤屹,按照密碼本來(lái)進(jìn)行加密熬甚,這個(gè)密碼本就是公鑰逢渔,商戶還有一個(gè)私鑰肋坚,根據(jù)私鑰決定怎么讀密碼本,這個(gè)公鑰和私鑰是一對(duì)的肃廓,接著如果有數(shù)據(jù)智厌,那么就用私鑰把數(shù)據(jù)加密,當(dāng)然是根據(jù)公鑰加密了盲赊,而支付寶也會(huì)有一個(gè)私鑰铣鹏,私鑰是一樣的,那么支付寶也會(huì)用私鑰來(lái)找公鑰哀蘑,進(jìn)行解密即可诚卸。

簡(jiǎn)單弄一個(gè)demo玩一下,首先要下載一個(gè)沙箱app绘迁,把螞蟻金服上面給的demo搞下來(lái)合溺,把包全部導(dǎo)入,使用阿里云提供公鑰生成:

有兩種秘鑰的驗(yàn)證方式缀台,RSA和RSA2棠赛,代碼里面默認(rèn)選用了RSA2,那么把對(duì)應(yīng)的秘鑰填寫上去膛腐。


這句不去掉注釋是沒有圖片生成的睛约。然后運(yùn)行就可以了。
接下來(lái)就是業(yè)務(wù)環(huán)境搭建哲身,把SDK復(fù)制過來(lái)改好包辩涝。支付模塊的業(yè)務(wù)其實(shí)很簡(jiǎn)單,獲取二維碼勘天,存在FTP服務(wù)器怔揩,返回前端,獲取支付結(jié)果误辑。現(xiàn)在整個(gè)流程測(cè)試一下沧踏,首先是登錄,獲取jwt巾钉,然后使用jwt進(jìn)行購(gòu)票操作翘狱,發(fā)現(xiàn)居然觸發(fā)了熔斷器,但是這也發(fā)現(xiàn)熔斷器設(shè)置的一個(gè)不足砰苍,沒有配置是error日志的打印,但是項(xiàng)目趕工在即揭糕,后面高可用或者項(xiàng)目維護(hù)再說(shuō)其他的吧昆禽。這個(gè)錯(cuò)誤顯示

updateById這個(gè)方法出現(xiàn)異常,mybatis plus的update更新這里是用自帶的赤惊,默認(rèn)會(huì)根據(jù)主鍵來(lái)進(jìn)行更新,然而order表忘記建立主鍵了凰锡,所以找不到主鍵自然也就不存在什么根據(jù)主鍵更新了未舟,更新表結(jié)構(gòu)為帶主鍵方式,更新mapper文件掂为。更新完成之后還是出現(xiàn)異常

這條語(yǔ)句異常裕膀,這條語(yǔ)句是插入了之后再?gòu)膁atabase讀出來(lái),查閱數(shù)據(jù)庫(kù)發(fā)現(xiàn)勇哗,插入的uuid是null昼扛,也就是說(shuō)主鍵是沒有被插入的,查看log打印的日志欲诺,insert里面沒有插入uuid抄谐,我使用insert默認(rèn)提供的插入方法,orderT里面哪個(gè)字段不是null就插入進(jìn)去扰法,但是很明顯uuid有的蛹含,問題就出現(xiàn)在配置文件mapper或者是orderT自己生成的模型中,orderT可能性不大迹恐,mapper文件查閱后發(fā)現(xiàn)確實(shí)沒有上面問題挣惰,百度發(fā)現(xiàn)問題在orderT生成的模型上面

主鍵不添加策略,默認(rèn)是auto自增方式殴边,但是String又不能自增憎茂,就填不進(jìn)去了,加上type锤岸,變成手動(dòng)input方式竖幔,這樣理論上應(yīng)該是OK的了,然而還是觸發(fā)了熔斷器是偷,去掉熔斷器發(fā)現(xiàn)是沒有問題的拳氢,那么就是熔斷器的時(shí)間了,算了一下蛋铆,至少要10s馋评,一個(gè)請(qǐng)求10秒有點(diǎn)過分了,所以后面改進(jìn)可能要進(jìn)行分布式或者異步改進(jìn)刺啦,因?yàn)檫@個(gè)請(qǐng)求涉及到了FTP的數(shù)據(jù)傳送留特,但是總算還是OK了。

然后就是生成訂單二維碼了

用sandbox版的支付寶支付一下

顯示支付成功,那么這樣這個(gè)下單支付的后臺(tái)基本沒有問題了蜕青,下面就是要把二維碼上傳到FTP上去了苟蹈,這個(gè)時(shí)候生成二維碼服務(wù)可能會(huì)更加慢。另外打開WiFi就找不到服務(wù)的問題右核,今天突然可以了慧脱,這也是為什么今晚測(cè)試這么快的原因。練著WiFi能找到服務(wù)可能是因?yàn)槲胰チ思依锪硗庖惶幏孔雍睾龋抢锏腤iFi突然就可以了菱鸥;以往這個(gè)bug得到的結(jié)論是WiFi的開關(guān)和Provider能否被Consumer找到互相有因果關(guān)系,現(xiàn)在原因可能會(huì)與WiFi路由器的不同相關(guān)搜变,仍在觀查——2020.1.23 2:12分凌晨
配置上傳到ftp也很簡(jiǎn)單采缚,首先加入上傳的目錄:

配置路徑

    public boolean uploadFile(String fileName, File file){
        FileInputStream fileInputStream = null;
        try{
            fileInputStream = new FileInputStream(file);
            initFTPClient();
            ftpClient.setControlEncoding("utf-8");
            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
            ftpClient.enterLocalPassiveMode();

            boolean change = ftpClient.changeWorkingDirectory(this.getUploadPath());
            System.out.println(this.getUploadPath());
            System.out.println("切換目錄是否成功:" + change);
            ftpClient.storeFile(fileName, fileInputStream);
            return true;
        }catch (Exception e){
            log.error("上傳文件失斦肼挠他!", e);
            return false;

        }finally {
            try {
                assert fileInputStream != null;
                fileInputStream.close();
                ftpClient.logout();
            } catch (IOException e) {
                log.error("關(guān)閉流異常");
                e.printStackTrace();
            }
        }

設(shè)置編碼,上傳的是圖片篡帕,配置上傳格式二進(jìn)制殖侵,配置被動(dòng)模式,改變上傳目錄镰烧。**啟動(dòng)的時(shí)候發(fā)現(xiàn)拢军,上傳是成功了,但是上傳的路徑不對(duì)怔鳖,上傳到了根目錄茉唉,也就是說(shuō),改變目錄的位置失敗了结执,突然這個(gè)changeWorkDirectory類似于Linux里面的cd度陆,那么配置文件里面寫的/qrcode應(yīng)該要去掉/,然后就可以了献幔。
**

Dubbo特性——本地存根

本地存根其實(shí)就類似于靜態(tài)代理懂傀,按照以往的方式,接口在客戶端蜡感,實(shí)現(xiàn)在服務(wù)端蹬蚁,那么客戶端很受限制,比如參數(shù)的驗(yàn)證等等郑兴;在拿到返回調(diào)用結(jié)果之后犀斋,用戶可能需要緩存結(jié)果;或者是在調(diào)用失敗的時(shí)候構(gòu)造容錯(cuò)數(shù)據(jù)情连,而不是簡(jiǎn)單的拋出異常叽粹。找一個(gè)接口,做一個(gè)代理類,代理類訪問目標(biāo)對(duì)象球榆,當(dāng)用戶要訪問目標(biāo)對(duì)象需要先訪問代理朽肥。dubbo使用本地存根的時(shí)候會(huì)在客戶端生成一個(gè)代理,處理部分業(yè)務(wù)持钉,而且Stub必須傳入proxy函數(shù)衡招。


Consumer和Provider就是消費(fèi)者和服務(wù)提供者,api就是之前寫的服務(wù)接口每强,以往的操作是略過dubbo始腾,action訪問Service接口部分,接口通過Provider注入進(jìn)來(lái)空执,現(xiàn)在不一樣了浪箭,如果使用本地存根,首先先創(chuàng)建一個(gè)Stub辨绊,類似一個(gè)靜態(tài)代理奶栖,來(lái)實(shí)現(xiàn)service接口,消費(fèi)者訪問的時(shí)候门坷,先訪問Stub宣鄙,既然Stub是代理,那么action就不會(huì)直接訪問api接口默蚌,先訪問代理Stub冻晤,但是在邏輯上還是去訪問Servic接口,當(dāng)Stub接到請(qǐng)求之后绸吸,轉(zhuǎn)發(fā)請(qǐng)求到ServiceProxy對(duì)象鼻弧,如果Proxy滿足條件,就會(huì)路由到實(shí)現(xiàn)類Impl上锦茁,如果不滿足就到Mock中做返回攘轩,Mock也叫偽裝,proxy是遠(yuǎn)程服務(wù)的代理實(shí)例蜻势,保護(hù)目標(biāo)對(duì)象撑刺,提供間接訪問途徑。但是這么一看握玛,其實(shí)我感覺用攔截器也可以的够傍,而且Mock那個(gè)偽裝有點(diǎn)類似于Hystrix降級(jí),還不是很能理解這玩意作用在哪里挠铲。
我們現(xiàn)在的項(xiàng)目冕屯,客戶端是guns-gateway->API,服務(wù)端是guns-alipay還有其他的模塊訂單->Impl拂苹,這個(gè)時(shí)候?qū)τ谟脩舻膐rderId的驗(yàn)證安聘,就可以放在Stub里面,這樣做的好處很多,首先可以保護(hù)目標(biāo)對(duì)象浴韭,其次也可以減少一次緩存丘喻。本地存根理解為一種比較特殊的靜態(tài)代理模式, 用于對(duì)真實(shí)目標(biāo)的一種保護(hù)念颈,或者額外增加功能泉粉, 攔截器更適合進(jìn)行切面編程, 但是存根更適合對(duì)目標(biāo)對(duì)象進(jìn)行精準(zhǔn)打擊榴芳,或者其實(shí)可以把這兩個(gè)內(nèi)容變相理解為靜態(tài)代理和動(dòng)態(tài)代理之間的區(qū)別嗡靡。這里的容錯(cuò),也就是降級(jí)返回有點(diǎn)類似于Hystrix窟感,但是本地存根的核心在于服務(wù)端反向調(diào)用客戶端獲取一些信息讨彼, 但是熔斷的目標(biāo)是容錯(cuò),本質(zhì)上來(lái)講不是一個(gè)東西柿祈,服務(wù)端在調(diào)用的時(shí)候需要客戶端的一些信息就可以用本地存根哈误, 這個(gè)是Hystrix完全做不到的,有點(diǎn)像aop谍夭。
另外黑滴,本地存根還有一個(gè)本地偽裝的概念,本地偽裝是本地存根的一個(gè)子集紧索,其實(shí)就是Mock,當(dāng)失敗的時(shí)候就會(huì)走mock菜谣,但是用途反而是更多的珠漂。通常會(huì)使用本地偽裝來(lái)完成服務(wù)降級(jí),前面Hystrix也是可以做服務(wù)降級(jí)尾膊,但是Hystrix是在springboot和dubbo才能用媳危,如果不用springboot是使用不了的。一般在客戶端就可以實(shí)現(xiàn)冈敛。所以這玩意算作是一種補(bǔ)充把待笑,使用到業(yè)務(wù)上體驗(yàn)一下。比如想要在返回支付結(jié)果上做降級(jí)處理抓谴,只需要繼承這個(gè)類暮蹂,然后service加上配置即可。

這樣一個(gè)接口的方法都可以降級(jí)了癌压,而Hystrix相比之下只能是方法做降級(jí)仰泻,一個(gè)個(gè)方法的填上,本地偽裝相對(duì)簡(jiǎn)單一點(diǎn)滩届,但是也有本地偽裝只能是捕獲RPC的異常集侯,RpcException,其他的不行,比如超時(shí)棠枉,網(wǎng)絡(luò)問題浓体,找不到服務(wù)等等,而計(jì)算問題辈讶,除0異常等等都無(wú)法捕獲汹碱,所以各有優(yōu)劣把。
dubbo還有隱式參數(shù)的特性荞估,把參數(shù)放在RpcContext里面可以通過getAttachment獲取咳促,有些比較敏感的數(shù)據(jù)等等,正式業(yè)務(wù)系統(tǒng)里面勘伺,往往會(huì)有一個(gè)requestId跪腹,這個(gè)requestId是request唯一,而分布式鎖也是根據(jù)requestId生成飞醉,比如在獲取訂單狀態(tài)或者下單冲茸,可以把userId取出來(lái)對(duì)比防止偽造,這有點(diǎn)像spring里面getAttribute缅帘,類似于一個(gè)全局變量轴术,但是dubbo沒有全局變量這個(gè)說(shuō)法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末钦无,一起剝皮案震驚了整個(gè)濱河市逗栽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌失暂,老刑警劉巖彼宠,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異弟塞,居然都是意外死亡凭峡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門决记,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)摧冀,“玉大人,你說(shuō)我怎么就攤上這事系宫∷靼海” “怎么了?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵笙瑟,是天一觀的道長(zhǎng)楼镐。 經(jīng)常有香客問我,道長(zhǎng)往枷,這世上最難降的妖魔是什么框产? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任凄杯,我火速辦了婚禮,結(jié)果婚禮上秉宿,老公的妹妹穿的比我還像新娘戒突。我一直安慰自己,他們只是感情好描睦,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布膊存。 她就那樣靜靜地躺著,像睡著了一般忱叭。 火紅的嫁衣襯著肌膚如雪隔崎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天韵丑,我揣著相機(jī)與錄音爵卒,去河邊找鬼。 笑死撵彻,一個(gè)胖子當(dāng)著我的面吹牛钓株,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播陌僵,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼轴合,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了碗短?” 一聲冷哼從身側(cè)響起受葛,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎豪椿,沒想到半個(gè)月后奔坟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡搭盾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了婉支。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸯隅。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡疯攒,死狀恐怖蕊唐,靈堂內(nèi)的尸體忽然破棺而出剖张,到底是詐尸還是另有隱情庸娱,我是刑警寧澤哥谷,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布嚷辅,位于F島的核電站棚蓄,受9級(jí)特大地震影響晌块,放射性物質(zhì)發(fā)生泄漏溶推。R本人自食惡果不足惜徊件,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一奸攻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧虱痕,春花似錦睹耐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至新思,卻和暖如春窖梁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背夹囚。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工纵刘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人崔兴。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓彰导,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親敲茄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子位谋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容