電影售票系統開發流程及其bug修復日誌--業務開發(1)

傳統業務

傳統業務應用基本把所有的東西都集合在一起,傳統應用帶來的問題,單一業務的開發和迭代困難,這個時候牽扯到兩個部分,第一是有可能只是針對用戶模塊增加了許多需求,其他模塊沒有變更,這種情況下,第一不談開發難度,我們把所以的用戶模塊的內容都開發完了,在測試的時候有兩種測試,一種是冒煙測試,一種是迴歸測試,除了要測試用戶還要測試其他的。這樣問他來了,如果用戶有一點要修改,那麼其他的測試也工作量也很大。而且用戶模塊修改也有可能修改公共類了。第三當我們有一種新的技術準備應用的時候,比如發現數據庫是瓶頸了,想要提升系統,這個時候就可能要用到緩存了,這個時候就要用原有代碼進行全量修改了,在調整一個模塊的時候可能會與其他的包進行衝突。其次,擴容困難,現在有一個業務系統,這個業務系統包含了我們的其他模塊,比如影院模塊,訂單模塊等等,這種情況下,用戶模塊的併發量不大,影院的也不大,但是訂單模塊就不一定了,比如說內容可能是256G,訂單模塊不夠了,要512G,但是要知道,傳統業務他的內存是統一分配的,在部署一臺256G內存機器,那麼訂單可能就只有一點點內存,所以這個時候擴容可能要1T纔有可能達到要求。 部署和回滾,傳統在部署是,比如用戶模塊要ES,影院模塊要redis,那麼部署的時候就要全部都部署上才能跑的起來,回滾就很簡單了,比如現在針對訂單模塊的一修改,這種情況下訂單模塊出問題,其他模塊沒有問題,這個時候全部做回滾。
#微服務發展歷程
很久以前就有提出過面向服務開發——SOA,在EJB的時代就提出了,然後到微服務開發。SOA,原先可能有一個大的系統,現在拆了他,拆成權限系統和用戶系統,現在他們需要通信,可以用webservice,當然這個技術是很老的技術了。微服務和soa最主要就差在微字,首先,微服務是一種將業務系統進一步拆分的架構風格,將業務系統進一步拆分的架構風格,微服務強調每一個單一業務都獨立運行,比如原來有一個系統,有登錄,退出等等一系列業務,這個用戶底下有很多個業務模塊,每一個業務模塊佔用一個進程,或者說一個JVM,這樣就做了一個資源拆分,這個業務就是一個應用,這就是微服務強調的,資源獨立,業務獨立,這就有點像進程進化到線程一樣,共享相同資源,但是又有自己獨立的棧地址;同時每一個單一服務都應該使用更輕量級的機制保持通信。在微服務裏面一般會使用更輕量級的協議而不是像webservice這樣這麼沉重的。而且每一個服務不強調環境,可以用不同語言或數據源,只是要求提供好的服務即可。

微服務核心概念

Provider:服務提供者,提供服務實現。Consumer:服務調用者,調用Provider提供服務的人。 同一個服務可以既是Provider也可以是Consumer。

環境搭建

Spring + dubbo

在idea選擇quickstart,只是服務之間的調用,完全可以滿足了。

接下來還要兩個子工程,一個Provider,一個Consumer, 至少一個吧。按照相同的方法建立兩個子工程。在src下面建立resource文件夾,設置成resource root文件。然後把依賴引進了,依賴這兩個Provider和Consumer都需要用,所以直接引近父工程的即可,由於是spring,不是springboot,還要application.xml配置文件,所以引入application-hello.xml:

注意第七行和第十三行就是引入了dubbo的命名空間。這個時候環境基本完成,現在就是要引入dubbo集成了。簡單寫下測試,在Provider裏面新建立一個服務接口以及服務實現:

public class QuickStartServiceImpl implements ServiceAPI {
    @Override
    public String sendMessage(String message) {
        return "quickstart-provider-message=" + message;
    }
}

實現的接口。這就是Provider要提供的一個接口,這個接口是要Consumer來調用的,然後就要在application-hello-Provider.xml裏面進行配置:

	<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就是服務的名稱,也是唯一標識,protocol就是服務的地址,bean其實就是接口的實現類,service就是接口本身,通過這個接口本身去調用服務。
接着就是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的服務,URL爲dubbo://localhost:20880,和

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

相對應,可能serviceAPI會報錯,但是創建一個就好了。

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));
        }
    }
}

啓動的時候兩個都要啓動。運行的時候遇到一個問題:會發現dubbo://localhost:20880找不到,顯示這個服務已經關閉,調了好久,然後重啓一下idea,完了好了,啥事都沒有。爲什麼會出現這個問題,看到網上大多數問題都是ZX沒有註冊或者是內外網的問題,所以不了了之了。其實整個流程是這樣:Consumer先在xml裏面配置得到了Provider的bean,quickstart,然後用Provider的這個服務給自己返回了一個消息,再輸出。

SpringBoot + dubbo

這個就比較簡單了,idea就自帶了spring initial,直接建立即可,不需要任何依賴。這就是基本目錄。和spring是一樣的,只不過把大部分配置變成了註解。接口按照spring的一樣,但是實現類需要把配置文件的信息變成註解:

注意Service不要倒錯包。然後在Provider啓動類加上另外一個註解:

這就是Provider的編碼,Consumer就簡單許多了:

注意ServiceAPI的路徑在Provider和Consumer的路徑要一樣,剛剛就犯了這個錯誤。Provider
Consumer
路徑要一樣。
結果:

還是出現是配置spring+dubbo的問題,超時問題,這個問題是不定時出現的,上一次的配置是重啓了idea就好了,這次是重啓電腦好的。懷疑的電腦配置問題導致的超時問題,因爲dubbo默認是1000ms就會報超時,於是把他調到了50000ms,可能與內存電腦配置有關,具體原因尚未知。

Zookeeper

上面這種就是直連提供者了,要求Consumer知道Provider的服務地址,直接找到服務,這種方式太固定了,不利於擴展。所以延伸出了用一個註冊中心來註冊所有的服務,zookeeper就是這樣一個註冊中心。

invoke就所用的直連提供者。安裝zookeeper還是很簡單的,下載下來解壓即可,然後進入bin,./zkServer.sh start運行即可。
在spring+dubbo中的配置比較簡單,加上幾個依賴即可,把registry=N/A改成zookeeper://localhost:2181即可,N/A就是什麼都不用。
springboot+dubbo配置需要在父工程加上依賴:

注意子工程的parent要改成父工程,否則依賴是無法引入的。
父工程加上:
表明這兩個是parent的兒子。

子工程加上,表面父母座標,這樣才能繼承依賴。
springboot的依賴最好不要加在子工程上,會出現日誌衝突。接着和原來一樣:

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

N/A去掉,接上服務器的位置。Provider就改完了。Consumer也是改一樣的地方,把註解改了:

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

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

還有一個需要注意的問題,springboot的application只會默認掃描同路徑下或者是子路徑的包:

application在Provider下面,那麼它只會掃描和她同級的包和Provider的子路徑,所以impl放在quickstart放在了Provider下面是可以掃描到的,如果把implement放在dubbo就掃碼不到了,這個時候就要加註解:

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

就會從dubbo這個包下面開始掃描。

構建業務環境

API網關

首先介紹一下API網關,API網關有點像設計模式中的Facade模式,比如現在有很多個服務,用戶服務,產品服務,訂單服務,如果一個網站,想要訪問產品服務,這個時候可能就要檢查是不是登錄了,或者權限等等,那麼就要先訪問用戶服務看看你登錄了沒有,登錄了,才訪問產品,但是這樣會帶來問題,因爲客戶端本身不安全,所以微服務這塊對外相當於是暴露了,都知道是什麼參數了,對外來說相當於是透明,所以安全比較難做;其次,在提交訂單的時候要做三步操作,檢查登錄,產品是否足夠,下訂單,其實是在點擊下訂單的時候,這三個步驟要一起完成,而這三個操作是三個不同的微服務,有順序性的了,所以時間長。打個比方,現在想看新聞,一會兒想看搜狐新聞,一會兒想看頭條新聞,每次都要輸入就很麻煩,那麼這個時候就會出現一個公共網站,比如hao123,這些網站,既可以看到搜狐,也可以看到頭條新聞,其他的新聞網站對於我們老師都是透明的,甚至有時候新聞來源都是不知道的。介於這種情況,就會出現一個網關的東西,gateway,這東西就相當於後臺服務與前端客戶端的一個接口,那麼至於怎麼訪問,異步同步等等前端都不需要管。都只需要面向一個接口,其他都只是後端的,所以API網關就相當於是微服務中的一個門面。

API網關作用:既然已經充當門面了,首要就是需要驗證你身份合不合格了,就相當於一個防火牆;審查與監察,網關有一個比較特殊的作用,類似於之前的攔截器,審查和分發,回來還要經過網關,再返回。這種情況下,就可以把邊緣信息統計一下,比如執行時間,調用了啥服務,響應時間等等。其次還可以做動態路由,dubbo做好了,但是springcloud沒有,springcloud需要處理。壓力測試也有可能,一般是接替測試,負載均衡,靜態響應分離。整個業務結構大概就是客戶端訪問API網關(服務聚合,熔斷降級,身份安全),然後網關再把請求分發下去。
#guns環境搭建
guns這裏使用還是v3.1,現在已經到了v6了,很早之前就下載過3.1版本,懶得換了。直接啓動會有一些錯誤:

首先是log4j問題,下一個包就好了。

這個是時區的問題,另外他也沒有識別出關鍵字zeroDateTimeBehavior,但是查了一下這個確實是MySQL的一個參數,也不知道爲什麼,修改一下連接路徑:

      url: jdbc:mysql://127.0.0.1:3306/guns_rest?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8

去掉即可。

admin是主要操作,也是主要業務,core是核心實現,generator是代碼生成,rest是連接數據庫的。原來還有一個parent,我把他搞在外面了。啓動guns-rest,在application.yml裏面有一個auth-path,就是路徑,http://localhost/auth?userName=admin&password=admin訪問,默認賬號密碼admin(這個要看官網,我自己改的),出現:

就表示成功了。接下來就是配置dubbo環境了,和springboot一樣,加依賴即可,使用guns-rest作爲後端模板往常網關,所以直接複製一個就好了,對於zookeeper這些和原來的一樣,啓動之後./zkServer.sh start,./zkCli.sh打開客戶端查看zookeeper註冊符號:

可以看到當前服務已經註冊進去了。那麼環境基本就是這樣了,剩下就是業務開發了。

抽離業務接口

就我們現在的工程,每一個模塊一個實現類就一個接口,在微服務中這些接口各個工程都要有,就如前面實現的spring/springboot+dubbo直連一樣,serviceAPI各個工程都要有,provide要有,Consumer也要有,很麻煩。如果是這樣的話,爲什麼不可以把這些接口全部獨立出來,做成一個工程呢?然後把實現類也扔到一個子工程裏面,然後就當成是一個依賴一樣引入pom.xml文件中即可。所以複製一份guns-core改名,在project structure改名,可能會出現can not contain source root這些錯誤,這個時候要把原來的module的重複source root刪除:

複製完成後右邊那一列(Add Content Root)會出現兩個source root 刪除一個即可change name了。之前測試使用的UserAPI移到這裏。

打包這個模塊,maven裏面lifecycle install,打包完成之後引入到guns-gateway做測試:

@Component
@Service(interfaceClass = UserAPI.class)
public class UserImpl implements UserAPI {
    @Override
    public boolean login(String userName, String password) {
        return true;
    }
}

這裏的UserAPI導入guns-api裏面即可。

可以看到,這個時候註冊的又是guns.api.user.UserAPI的接口了。

Dubbo調用流程

dubbo有兩種調用方式,直連提供者和註冊中心,兩種都在剛剛的環境搭建中就簡單測試過了。首先是直連提供者:在開發和測試環境,常常是需要繞過註冊中心,直接指定提供者是誰,其實就是點對點直連,類似數據鏈路層吧,如果需要動態擴容,每一個地址都要讓Consumer知道,缺點就是全寫在代碼裏面了,寫的好點的可以扔在配置文件,但是都要重新啓動,所以這種方式僅僅用於開發和測試,如果使用註冊中心,就簡單許多了。基於註冊中心:首先先要有Provider,dubbo提供了一個容器,用這個容器來裝載Provider,當啓動系統時,這個Provider就會在註冊中心留下地址,其實就是發現服務的過程,也叫register;而Consumer也會subscribe一下注冊中心,把服務地址下載下來,同時註冊中心一有變化就notify Consumer,Consumer又更新一次,dubbo本身就有moniter,用於監控檢測等等

dubbo多協議

dubbo默認支持阿里開源的dubbo協議,同時也支持rmi,hessian等等協議。Dubbo協議特點: 傳入傳出參數數據包較小(建議小於100K),消費者比提供者個數多,單一消費者無法壓滿提供者,儘量不要用dubbo協議傳輸大文件或超大字符串,基於以上描述,我們一般建議Dubbo用於小數據量大併發的服務調用,以及服務消費者機器數遠大於服務提供者機器數的情況。
RMI協議特點: 傳入傳出參數數據包大小混合,消費者與提供者個數差不多,可傳文件。基於以上描述,我們一般對傳輸管道和效率沒有那麼高的要求,同時又有傳輸文件這一類的要求時,可以嘗試採用RMI協議。
Hessian協議特點: 傳入傳出參數數據包大小混合,提供者比消費者個數多,可用瀏覽器查看,可用表單或URL傳入參數,暫不支持傳文件。比較適用於需同時給應用程序和瀏覽器JS使用的服務,Hessian協議的相關內容與HTTP基本差不多,這裏就不再贅述了。
WebService協議特點: 基於CXF的frontend-simple和transports-http實現,適用於系統集成,跨語言調用。 不過如非必要,強烈不推薦使用這個方式,WebService是一個相對比較重的協議傳輸類型,無論從性能、效率和安全性上都不太能滿足微服務的需要,如果確實存在異構系統的調用,建議可以採用其他的形式。http協議也支持。

用戶模塊開發

JWT驗證

遠離Java太久了,之前使用的方式都是使用cookie+session,或者用上sso單點登錄吧,jwt有點不一樣,不需要存儲session,用戶從客戶端輸入賬號密碼訪問服務器,服務器給他個token,結構如下圖所示:

header是加密方式,就是用什麼方式加密把,payload就是承載信息了,用戶還是管理員等等,簽名就是利用前面兩個信息進行兩次哈希加密得到的。然後每一次客戶端就帶着這個token,服務器只需要驗證這個token是不是正確的就好了。
還是先要建表:
user_t的字段屬性。guns_gateway基本已經完成了,只需要相互調通即可。重新複製一個guns-gateway修改名字爲guns-user,注意是gateway這個門戶去調用服務,所以這兩個東西都要啓動起來,所以端口肯定要不一樣,所以dubbo端口要變一下,而且權限驗證jwt這是在gateway門戶做的,通過之後再跑到gate-user調用服務。所以端口和jwt要設置一下:


啓動測試一下。
現在的流程是,客戶端調用服務,是直接調用gateway門戶裏面的api,gateway再根據各種需求調用後面各種模塊的服務,測試一把。
登錄進去一定會調用jwt做驗證,就直接在jwt上面做測試吧。

@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模塊的服務,也就是gateway要調用的服務,如果能打印出來語句就OK了。

在gateway的auth的controller加上測試。

注意,打印是打印在UserApplication裏面,因爲這個服務是在user模塊完成的。出現打印那麼說明互相調用就OK了。

配置可以忽略的URL

有些URL是可以忽略的,比如註冊,/user/register,登錄,/user/login,這些很明顯是不需要的,所以還是需要配置一下。首先理解一下guns的jwt:

gateway裏面的配置文件有一項就是jwt,在springboot中有一項就是要把其內容全部讀進去。
這是屬於springboot的註解配置,configuration就是讀取在yml配置文件所有前綴是JWT_PREFIX,下面配置了JWT_PREFIX = “jwt”,就是讀取yml中jwt:中的內容。所以在yml配置文件jwt中我配置了ignore-url,作爲忽略的URL,那麼在JwrProperties就要讀進來了。
**注意在springboot中,讀取配置這裏默認會把-u變成大寫U,ignore-url就變成ignoreUrl,讀取進來記得加上getset方法。**既然添加完路徑了,首要就是做處理了,處理這種東西肯定是在讀取進來的權限做限制,那麼在module auth filter裏面修改即可,有一個AuthFilter:

這裏的chain.doFilter是按照鏈式過濾的意思,如果多個filter,那麼按照filter1->filter2->filter3…以此類推,但是下面沒有其他的filter了,所以直接返回頁面,所以這裏也代表着直接通過的意思。那麼仿照它把ignore-url路徑加上:

這樣就配置好。

用戶模塊API以及相應的類

用戶模塊api肯定是添加在guns-api這塊,在添加兩個類,一個類是UserModel,這是用戶註冊的類,這個類是不能被修改的,僅僅作爲註冊使用,因爲註冊的內容有一些敏感內容,所以需要一個新類,也就是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;
    }
}

按照返回格式來修改登錄之後要返回的數據:

修改api中的返回數據,然後讓其返回用戶的uid,因爲返回的token使用的就是用戶的UID來構建,構建一個返回model,泛型使用M,因爲這裏的data是其他類型:

然後在AuthController修改一下返回類型和內容即可。

用戶信息保存

ThredLocal用戶信息保存的方法代替把信息保存到session中,threadlocal每一個線程分開使用,不同線程的threadlocal不一樣,好比線程之間的棧地址不可共享,同一個進程的線程資源與空間可以共享。可以使用threadlocal直接保存用戶信息,也可以只保存UID或者某些關鍵信息,這裏使用保存UID的方法:

有了方法還得維護他,每一次登陸或者是註冊完之後,都會發放一個jwt,每一次客戶端進入頁面帶來點jwt中也有uid,也要把UID拿出來放在threadlocal中,也就是再filter那裏,在登陸進來,訪問URL進來的時候都需要進過的一個過濾器,在AuthFilter裏面修改:

這裏使用getUsernameFromToken是因爲在AuthController中我們保存的就是uid,只不過uid替代了username這個位置。那麼用戶的jwt就修改完成了。既然都改完了,做一個測試吧!
首先準備一個控制器:

如果能通過這個控制器驗證成功就可以打印出uid,返回請求成功。
回看一下請求流程,首先登陸,登陸成功就會返回一個uid,每一次客戶端就會拿着這個Uid組成的token登陸,查看一下filter的AuthController:

顯示把前面7個固定字符取出來,然後再驗證後面的token,使用postman測試:
先登陸得到token,然後用token登陸,注意token在header裏面:

測試的時候還是遇到了一些問題,一開始啓動不了gateway,後來嘗試了很多遍,把gateway的target刪掉就可以運行了。既然都已經調通了,那麼開始把數據層對接上吧,啓用代碼生成工具直接生成就好了。
額,直接生成就好了,guns這個框架很快的。

注意區別一下目前存在的三個模型:

UserModel和UserInfoModel都是模型之間交互的,但是區別就在於UserModel只是用於註冊,而UserInfoModel是用於各模塊之間的交互以及修改,UserT就是guns-user自用的Dao層而已,不會跨越模塊。然後就實現各種服務了,這個很簡單,沒得說,注意密碼不能明文存儲即可。需要注意的主要就是用戶退出這個功能,一般會把用戶的信息存兩份,前端先存jwt,一般存7天,在這種情況下就會存在一個問題,jwt的刷新;那麼這個時候後端就起着刷新作用,服務端就存儲活動用戶信息,一般30分鐘,如果在30分鐘之內能查到用戶,那麼就認爲是活躍用戶,如果沒有,哪怕你有jwt也認爲你需要重新登錄,所以logout要做兩件事,首先刪除前端jwt,然後刪除後端活動緩存即可。
接着就是測試了,遇到一個很牛逼的bug:

**這個問題吧,就是前面提到好幾次的問題,我當時解決對了一半,確實是機器問題,但是不是性能超時時延的問題,而是WiFi問題,說我信息發不出去,通道關閉了,那就是鏈路問題,但是我ping了一下127.0.0.1,可以通,那麼tcp/ip就沒有問題了,ping了一下另外一臺電腦,可以通,那麼網卡或者說網關就沒有問題了,然後看到網上很多人說WiFi問題,然後我把WiFi關了,然後就可以通了。不過打開WiFi在測試的過程中還是有某幾次是可以連上的,但關閉WiFi就一定可以連上。關鍵是他這個錯誤,也就是cause:message can not be send,channal is closed.這個錯誤不是一下就提出來了,還是我把check=false設置了之後纔出現。**接着就是測試接口了,注意一些model裏面的值最好使用對象,比如使用Integer或者String對象,不要使用int這樣的,因爲有可能會出現null值。
**測試完成後,基本上用戶模塊差不多了,但是現在還有一個小問題,就是啓動的時候必須要有順序,要不然gateway會找不到服務,其次還有負載均衡的問題。啓動順序那個就是check=false的問題,負載均衡策略dubbo有四種,Random,按權重設置隨機概率,RoundRobin,按公約後的權重設置輪循比率,LeastActive,最少活躍調度數,如果活躍數相同,那麼隨機,不同就按照排序,ConsistentHash,相同參數就到同一個提供者,不同參數到另外一個。一般多用輪循,但是有可能受到機器影響,如果三臺機器的效率並不相同,如果第三個請求到了第三臺機器,但是第三臺機器炸毛了,沒有返回,那麼就一直卡在這,因爲輪循第三個request一定是到第三個,但是第三個一直不能返回,就造成了dubbo的雪崩。**負載均衡這裏有兩種配置方式:

客戶端端配置意思是訪問服務提供者是一種什麼形式,而服務端服務是所有的客戶端訪問這個服務的訪問形式是什麼樣的,簡單點說,就是客戶端級別就是當前這個客戶端訪問是怎麼來的,其他客戶端沒有影響,而服務端是影響到了所有客戶端,所以在Impl配上roundrobin就好了。
dubbo的多協議之前提到過,簡單再提一下,dubbo支持多協議中,最主要的區別就是鏈接方式,dubbo協議建立的是長鏈接,一旦建立就會建立一個管道,不需要每一次都要進行建立,類似HTTP的長短鏈接,但是dubbo本身不是一種協議,只是封裝了TCP,然後在TCP的基礎上變成了dubbo這個協議,那麼dubbo的傳輸協議就是TCP了,另外,dubbo用到是NIO的異步傳輸。

影片模塊

影片模塊有點複雜,數據庫設計也有點多,首頁內容比較多,首頁每一個人都是一樣的,所以直接搞了一個數據庫表給他,這樣直接取出來就好了,首頁需要實現:

這麼多功能需要用一個功能完成,這就是網關的功能聚合,前端只調用一次接口,全部加載出來,不需要這麼多次HTTP請求。

actor_t是演員表,banner是首頁圖片的表,其實就是滑動窗口的圖片,cat_dict_t就是字典了,分類字典,比如懸疑,犯罪,動作,愛情等等,film_info電影信息表,這個表存儲電影的少量核心信息,這類信息是經常要使用的,信息量不大,可以加快查詢速度;film_t就是電影信息全的表了,所有信息都在裏面了,接下來就來源表,年份表。這些表都沒有進行外鍵關聯。

@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;
}

前端準備要返回的模型,通過一個接口全部裝進去。

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();

}

要實現的接口。裏面很多交互的模型都有共同點,直接使用同一個模型即可。實現網關裏面的首頁控制器,別忘了把前端地址加到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);
    }

很簡單,就是裝進去直接返回即可。測試一下:

寫到這裏基本調通了。接下來就是快速的業務開發。業務開發這塊複雜點的其實也就按條件查詢這裏有點複雜,其他還算OK。類似於電影天堂裏面那些按照國籍查詢,按照演員查詢等等:
前端接口如此,isActive就是是否是選中的,如果是選中就是true,其他的就是false了,前端會返回選中的分類,接着後端會對比選中的分類,選中的返回true,其他返回false,如果是’全選’,標識是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);
    }


有點複雜,先遍歷一次,遇到99了先存下來,如果沒有匹配到的,說明傳回來的就是99,那麼把99全選傳回去就好了。沒有用到什麼特別的算法,如果想快點用上二分可能好點。測試結果:

這樣就測試成功了。(WiFi斷掉才能練上這個問題還是存在,WiFi連着,可能可以找到服務,WiFi斷開是一定可以找到服務,問題還是message can not send,channel is closed.)

影片查詢接口

影片查詢接口順便實現一次重構, 比如說在首頁的時候:

islimit = true,即爲首頁,islimit爲false則爲列表,但是這樣是完全不能滿足功能需求的。影片查詢需要7個參數,影片類型,排序方式,來源,分類,年份,當前第幾頁,總頁數,如果使用參數直接散開傳到控制器是可以的,但是很麻煩,所以使用一個模型來接收。那麼控制器主要做幾個事情:首先是根據showType判斷類型,接着根據sortID排序,然後添加各種查詢條件,判斷當前是第幾頁。之前有實現過geHotFilms等等類似功能的函數,重構這些函數,改成可用的,先修改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);

修改查詢接口的時候sourceId和yearId是一樣的,catId可能有點麻煩,一個電影可能有多個標籤,可能既是動作,又是愛情,所以數據庫裏面是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);

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

            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加入忽略列表裏面,首頁也是可以查詢的,並且不需要登錄處理。如果出現一下:

那麼這就調通了。

影片詳情接口

套路都很簡單,首先在控制器添加一個方法,查詢詳細信息本身是很簡單的,只是簡單的調用接口就好了,但是這裏會使用到dubbo的一個特性——異步特性,那麼這個時候就需要重新定義API了。影片這裏的詳細信息查詢關係到關聯查詢,在Mappering裏面添加API:

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


}

然後在Mapper添加基本語句,然後重寫SQL語句了。主要是在film_t和film_info_t這兩張表進行查詢,查詢的結果後還需要進行拼接,其實還是挺簡單的。

version1版本,簡單實現一下。

拼接拼的差不多了,info1那裏的轉換還有點問題,還有上映時間日期也沒有弄好:

組織好後面的上映日期。現在就是要處理#1#2#4#5#這類字符串了,先去掉收尾的#號再把中間的#變成,號:

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

both就是收尾都要去掉。接着就是替換:

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

這樣就替換成功了。那麼只需要看看哪一個是IN (選擇語句)就好了,按道理:

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

這樣就好了,但是結果只是出現一個:

所以這種做法是不可以的,FIND_IN_SET函數可以做到,可以用find_in_set函數把他們套起來,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));


差不多了,但是還有一個問題,就是如何拼接的問題,自己需要在中間加上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));


那麼接下來組織一下就好了,先簡單的讀出數據:

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;

接下來就是按照上面的做法先解析分類:

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;

接下來就是後面的拼接:


這玩意有點嚇人,講道理,還沒寫過這麼長的,如果把那些分類全部放程序裏面的話有不好改,不靈活。接下來就是補充基本的控制器即可,但是這上面完成的只是電影基本信息,電影的介紹,演員表,截圖等等還沒有完成。接下來就是電影的演員,電影與演員的對應關係是一對多的關係,如果直接寫不太好些,所以用一個演員映射表來實現:

CREATE TABLE mooc_film_actor_t(
  UUID INT PRIMARY KEY AUTO_INCREMENT COMMENT '主鍵編號',
  film_id INT COMMENT '影片編號,對應mooc_film_t',
  actor_id INT COMMENT '演員編號,對應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不是演員名字,是角色,比如在電影裏面的導演還是演員,這裏也需要聯合查詢:


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;

這個查詢比上面的要簡單多了,聯合幾個表,也不需要嵌套查詢。這裏要返回的json串:

這整個json作爲一個對象FilmRequestVO,這個FilmRequestVO又包含幾個對象,status和imgPre都是原來Respose就有的,只需要把剩下的加上就好,data那塊是一個對象,而data裏面info04又做成一個對象,director和actor又是一個對象,以此類推,賊多。

接下來就是控制器訪問的問題了:

ignore_url裏面直接加film/films是不行的,因爲在Authfilter裏面是equal,相等在能匹配,只需要把Authfilter裏面改成startwith即可:


調試成功。現在還有一個小小的問題,目前這種情況很快,數據量小一下子就出來了,假設每個接口都是200ms,那麼這個請求接口一共就演示1秒了,真是很大的延遲。而dubbo與一個特性,即異步調用。

異步調用

dubbo的異步調用特性基於NIO的非阻塞實現並進行調用,客戶端是不需要啓動多線程。
首先用戶線程調用服務,進入IOThread,IOthread請求服務器獲取返回,然後不等待服務器返回,立刻設置一個future對象,future對象標識一個可能還沒有完成的任務結果,等到完成了在通知future,然後接受返回。原本是發出請求,等待執行完成後返回,現在是已發出請求就返回,用future來接受,立刻返回後就跑去執行其他的,這就做到異步了。
比如啓動異步調用之後,執行方法:

fooService.findFoo(fooId);

這個方法直接返回一個null,因爲不等待結果了,接着dubbo會自動生成一個funture對象,因爲需要用future對象存儲返回數據,所以需要獲取到Future:

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

再從這個future對象獲取信息:

Foo foo = fooFuture.get();

明白原理之後就要把異步調用用到業務上了。剛剛返回電影信息詳細可以使用

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

如果這樣使用,那麼但凡是跟future沒有關係的都會報錯,比如在剛剛獲取詳細信息過程中:

        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的,後面都是根據ID去查找其他的信息,那麼如果異步處理後面的ID可能null值了。把需要異步的方法全部封裝成另外一個API進行處理,剩下的方法留着。

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

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

異步調用需要加上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對象來接收數據。和示例一樣,注意異步需要手動開啓,在啓動類加上@EnableAsync

影院模塊

影院這塊相對簡單一些,6張表,品牌字典表,其實就是類似於map這樣的映射表,地域字典表,影廳字典表,影院主表,影院的詳細信息,在熱映電影字典表,放映場次信息表。熱映電影表其實是不需要的,但是爲了減少數據查詢的負荷,還是以空間換時間,數據庫往往是系統的瓶頸。
首先先給出接口大致框架:

@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;
    }

}


接着還按照返回報文建立響應的數據結構。需要注意的就是已售座位這裏,只有下了訂單之後才能填上已售座位,所以只能先寫死,等到訂單完成後在回來補齊。下面先分析一下所需要的接口,首先第一個接口getCinemas接口,就是入參出參即可,按照五個條件進行帥選,判斷是否有滿足條件的影院,出現異常應該怎麼處理。入口參數:CinemaQueryVO,出參就是cinemaVO對象;然後是getCondition按照條件查詢,1.需要獲取品牌列表,也就是影院是哪個公司的,2.所在區域,3.影廳又是什麼類型;getFields根據影院變化獲取影院信息,獲取所有電影信息和對應的場次信息,影院編號。接下來就是獲取場次詳細信息,根據編號獲取場次詳細信息,根據反映場次ID獲取反映信息,根據反映場次查詢播放電影,根據電影編號獲取電影信息,還有一個售賣座位的信息是需要通過訂單實現的,所以待實現。
列出實現接口:

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);
}

這裏嗎有點複雜的其實就是getFilmInfoByCinemaId();首先要根據影院ID去field_t表把這個影院的場次全部查出,還要去hall_film_t表把對於電影名字給出並篩選。這裏可能有些困難的是hall_film_info_t和field_t的關係,hall_film_info_t是影廳播放的電影,而field是場次信息,一個hall_film_t會對應多個field,這個時候如果用聯合查詢只能查出一個,查不出整個集合,所以只能用聯合查詢了:

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';


但是問題來了:

@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;
}

最後一個是一個集合對象,怎麼返回?只能使用mybatis SQL自定義來處理了,首先mapper設置一個接口,在mapper的xml文件裏面進行實現即可。

    <!-- 通用查詢映射結果 -->
    <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定義自定義對象,這樣就可以返回自定義類型了。接着實現業務層什麼的都很簡單。簡要提一下lombok做日誌,平時做日誌都是要private Logger log…,可以加上@SLf4j註釋,就可以省略掉上述的過程。然後就是測試了。

dubbo結果緩存

對於getCondition這個方法,一般是熱點數據,這個數據會被頻繁的使用,這種熱點數據一般處理都很簡單,就是放到緩存,對於dubbo提供的結果緩存,是針對已經存在的大量頻繁訪問的數據,存儲在本地緩存中,存在當前是jvm裏面,所以可能會存有多份緩存,訪問也更快。緩存類型有三種,lru緩存,和os的頁面置換類似,最近最少使用緩存,threadlocal當前線程緩存,還有一種是jcache,這種比較少見,基本都是前面兩種,但是注意threadlocal不適合大量數據。配置其實很簡單,在接口加上配置即可:

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

併發連接控制

同時,dubbo可以對併發和連接數量進行控制,可以在配置文件設置併發控制數量等等。首先明確,如果併發與連接數量超出了,並不會等待,會以錯誤的形式進行返回,dubbo本身雖然有服務降級,但服務降級這個東西實現的並沒有特別好,其次,dubbo本身是有服務守恆的問題,但是在以前dubbo防止雪崩是通過控制併發與連接數量來控制的,尤其是連接。雪崩:目前3個服務,其中一個服務不知道爲什麼原因併發量非常大,超出了他本身所能承受的力度,然後這個服務就只能被衝崩了,然後這些請求又全部被送到了二號,二號又雪崩了以此類推。所以就叫雪崩。以往對於這種控制是用控制連接與併發數。

訂單模塊

訂單模塊這玩意,之前都沒有碰過,訂單模塊主要是涉及一些dubbo特性或者是服務配置的問題,訂單本身的業務很簡單,就兩個,下訂單,查看訂單,沒了,服務配置比如限流,服務降級,熔斷等等,dubbo本身有熔斷,但是這個實現不太好,所以使用其他的熔斷器來進行處理。首先是安裝ftp,10.13版本前的ISO是自帶的,Mac往後版本是沒有的了,我的恰好是10.14的,ftp沒有帶上自帶了sftp,sftp是ftp的變體,FTP另外一種是TFTP,FTP,TFTP,SFTP都是三種文件傳輸,區別就在於,FTP是需要在可信賴網絡上傳輸,他沒有很高的安全加密,SFTP有,如果信息很銘感,那就需要用SFTP了,增加了安全層進行信息加密,TFTP即是簡單文件傳輸,基本適用於局域網,而且與其他兩種協議不同就在於,FTP和SFTP使用TCP協議,而TFTP使用UDP協議,既然自帶了那就使用sftp充當FTP吧。
首先對於購票業務,後端要有一個原則,永遠都不能相信前端給你的東西,因爲是可以通過前端進行更改的,所以要驗證是否爲真。影院列表是使用json文件,判斷座位id是不是正確,判斷在訂單裏面有沒有座位id,既然售出自然不能再買了,驗證完這些後才能創建訂單信息。對於訂單業務,獲取當前登錄信息,獲取訂單即可。由於座位信息是通過json文件傳輸,需要從ftp服務器獲取,但是我的Mac沒有ftp,就去阿里雲找了一個。使用FileZilla用戶root發現登錄成功,但是獲取目錄失敗了,密碼也沒有錯,錯誤原因有可能就是端口了,但是21 20端口開了,21傳輸命令,20端口傳輸文件。這裏的問題確實實在端口,但是是在ftp的傳輸方式上,ftp有兩種傳輸方式,一種是主動,一種是被動,主動:服務端來找客戶端,通過21傳輸命令,通過20傳輸數據;被動方式:客戶端找服務端,命令還是用21端口,但是文件使用1025-65535隨機一個,而FileZilla默認使用passive mode,自然要開啓全部了。所以當輸入賬戶密碼的時候,即是命令登錄,使用21,沒有問題,但是文件目錄是使用隨機端口了,所以出現問題。408歷年真題有一個選項就是這玩意,ftp任何情況下傳輸文件使用20端口,錯誤的,主動纔是。
然後就是配置阿里雲服務器了,配置很簡單,讀取信息的那些stream可能有點煩:


    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("獲取文件選項失敗");
        } finally {
            try {
                bufferedReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

測試通過之後,把ftp的相關信息配置到application.yml,如果出現-u會自動轉換成大寫。

訂單模塊業務實現

業務實現這塊下單有點複雜,存儲沒有什麼問題,返回的時候需要多個表的信息,影院名稱,電影名稱,電影價格,訂單總價等等信息,但是還是很好寫。因爲通常購買完成之後,需要返回已經插入的訂單:

fieldTime的組織有點麻煩,首先要把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";
	

業務層的實現都很簡單,沒有什麼問題。然後是實現得到已售出的座位,concat和group_concat的區別,concat的實現對象是字符串,group_concat是可以在分組或不同表時間實現的,可以和group by和起來使用。注意之前哎cinema模塊也要把原來沒有實現的已售出座位修改一下。

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

cinema這塊信息加上,postman測試一下:

對於WiFi打開就連接不上的問題,初步懷疑是虛擬ip的原因,因爲在網上有人是因爲電腦有兩張網卡,一張有線以太網網卡,一張是無線網卡,結果接到無線網卡去了,但是我的Mac又沒有以太網網卡,設置裏面也查看了確實是802.11無線局域網,使用CSMA/CA協議,應該是連接WiFi導致是ip發生變化。
測試完成後基本上業務這塊就完成了,但是訂單模塊的拓展還是比較多的。首先,一般來說在訂單這塊,業務量是比較大的,每一部電影的訂單量是很大的,比如戰狼2賣了50多個億,在這種情況下,訂單不會再數據庫用一張表存,這就涉及到了訂單模塊的橫向和縱向的拆分;其次,還有服務限流,服務限流不僅僅是侷限於訂單系統,其他的也有可能包含,就比如LOL比賽的觀看人數等等都需要限流。還有熔斷和降級,這類主要解決服務器雪崩的情況。

訂單橫向縱向拆分

比如像京東天貓,天貓一開始是賣小商品爲主,那麼運行十年之後,有一部分是服裝,有一部分是家電,這些在訂單表裏面是混淆在一起的,一億條訂單表裏面7000條服裝,3000條小商品,如果混在一起,對於分析查找都是不方便的,這個時候就會引入橫向拆分,比如家電錶,比如小商品表,縱向拆分就是按照時間拆分,比如2017年一張表,2018年一張表等等。但是這樣拆分,業務也會複雜,比如現在有兩張表,order_2017_t,order_2018_t兩張表。這裏涉及到一個dubbo的特性,**服務分組,**當一個 接口有幾個實現,可以用group來區分,**分組聚合,**從每一個group中調用返回結果,併合並返回結果。group關鍵字進行分組,merge關鍵子進行合併,合併也需要注意,只有返回了list或者collection一類的才能合併,但是當前使用的dubbo並不支持,所以要合併也只能手動合併。但是如果這樣分組和並就要改業務了,所以知道怎麼回事,但是就不改了。

服務限流

首先服務限流是系統高可用的一種手段,對於業務上面沒有任何幫助,完全是爲了高併發。dubbo也有併發和控制連接數來限流控制。主要是用於服務之間的限流,限流算法有兩種,漏桶法和令牌桶法
漏桶法:
水龍頭的請求,下面的整個是業務系統,無論來多少請求都會先裝載這個桶裏面然後然後再處理。所有的請求進來都會被排列成一個隊列,然後按照相同的速度進行處理。
令牌桶算法:

arrival即請求,在請求進入的同時還有一個保護現場,這個保護線程擁有令牌,線程進入之後只有拿到令牌才能被處理,否則會被丟棄或返回,他與漏桶算法的最大區別就在於這玩意的業務請求峯值是有一定承載能力的,比如桶裏面有1000個令牌,1000個業務進來可以併發全部執行完,但是對於漏桶算法無論還有多少內存或者是擠壓空間,處理速度都還是這麼快。另外,令牌桶算法可以通過改變添加令牌的速度來控制請求的處理,防止業務崩掉。
簡單實現一下令牌桶算法:
首先需要準備桶的數量:

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

按照每毫秒添加一個令牌的速度進行業務請求控制。


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

每一次請求看看時間離上一次申請令牌過去了多久,按照毫秒把令牌數補上,如果令牌數是大於0的,允許運行,然後減一。

這樣就實現了,加入到工程裏面。在訂單系統裏面,getOrderInfo這個頻率不太高,主要是下單的頻率很高,在下單處增加:

熔斷降級

服務的穩定是公司可持續發展的重要基石,隨着業務量的快速發展,一些平時正常運行的服務,會出現各種突發狀況,而且在分佈式系統中,每個服務本身又存在很多不可控的因素,比如線程池處理緩慢,導致請求超時,資源不足,導致請求被拒絕,又甚至直接服務不可用、宕機、數據庫掛了、緩存掛了、消息系統掛了…對於一些非核心服務,如果出現大量的異常,可以通過技術手段,對服務進行降級並提供有損服務,保證服務的柔性可用,避免引起雪崩效應。實時監控接口的健康值,在達到熔斷條件時,自動開啓熔斷,開啓熔斷之後,如何實現自動恢復?每隔一段時間,釋放一個請求到服務端進行探測,如果後端服務已經恢復,則自動恢復。比如,如果ServiceA調用ServiceD一直失敗,或者失敗率很高,就可以採用“一種機制”確保後續請求不會調用ServiceD,而是執行降級邏輯。
使用Hystrix作爲熔斷降級工具,Hystrix主要有兩種命令模式:

hystrix command模式有主要是單線程進行,而Observable Command可以使用線程池等等。請求從command進入。

經過toObservable之後進入第三步,判斷目前的結果是不是在緩存裏,不在的話繼續往下做,判斷斷路器是否開啓(第四步)。本來的業務線是A調用B,這是一條通路,熔斷就是把A到B切斷,熔斷器開啓的意思就是是不是把這條路切斷了,切斷了那就簡單了,直接走返回。接下來判斷線程池狀態,如果都是OK,那麼繼續往下走,到達第六步,如果執行成功了,啥事沒有,失敗了或者是超時了,注意在熔斷器機制下,不僅僅是執行失敗的,超時也算是失敗的。到達第8步,是失敗調用的,這一步就叫降級返回,也叫服務降級。服務熔斷是判斷要不要把這條路幹掉,一旦出現業務異常,就調用服務降級,把業務返回。比如之前是A調用B,降級就是不調用B,使用一種折中的方法返回,比如今天想打遊戲,電腦宕機了,不會直接告訴你我炸機了,hystrix會返回電腦沒電了,遊戲倒閉了等等比較折中的方案。
首先導包了:

	  <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>

接着設置註解開啓熔斷器:

	@EnableHystrixDashboard
	@EnableCircuitBreaker
	@EnableHystrix

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

@HystrixCommand(fallbackMethod = "error", commandProperties = {
@HystrixProperty(name="execution.isolation.strategy", value = "THREAD"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value
= "4000"),//超時時間
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),//出現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")
})

加上熔斷器之後需要右一個一模一樣參數返回值的方法,對應fallbackMethod即可。註解上的配置名字都很明顯了,沒有上面需要解釋的,超過4秒即爲超時,10次例外就開啓你熔斷機制等等。
添加了Hystrix註解之後,會發現Threadlocal用不了了,這是因爲Hystrix本身有線程隔離,線程池保護和信號量機制,所以會切換線程,這個時候ThreadLocal就有用了,那就找一個可以緩存之前信息的,就是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了:

服務降級成功了。感覺服務降級好像不止這些功能,上面實現的大概類似於安撫的一種做法,當然,也可以設計一個隊列,把請求追加在隊列裏面。

支付模塊

支付模塊做簡單點,對接支付寶即可。**支付流程:首先要獲取二維碼,用戶掃描支付二維碼之後,系統後臺是不知道,只能等支付寶回調,然後修改訂單狀態,最後還需要定期對賬。**但是等待支付寶回調有點麻煩,現在還沒有一個公網的地址,所以只能啓動另外一個流程,首先Consumer是服務端消費,客戶端就是前端操作系統,Consumer獲取二維碼送到前端客戶端上,前端進行掃描支付,這個時候後端會啓用請求調用來查詢前端支付狀態,同時修改數據庫,把訂單修改成已支付,當然了,支付流程不夠嚴謹,但是已經能夠完成了。簡單看一下開發文檔,本身支付寶是需要身份驗證,營業執照等等,這裏肯定沒有了,所以只能用沙箱版的支付寶,也就是沙箱環境做測試,全部都是假的。然後配置一下沙箱環境,按照文檔:

填寫配置文件信息。properties裏面有公鑰和私鑰。現在有兩個環境,一個是商戶,一個是支付寶,相互都持有一個公鑰,這個公鑰是以明文傳輸,沒有安全性,比如近代戰爭通信進程會有一個祕密本,按照密碼本來進行加密,這個密碼本就是公鑰,商戶還有一個私鑰,根據私鑰決定怎麼讀密碼本,這個公鑰和私鑰是一對的,接着如果有數據,那麼就用私鑰把數據加密,當然是根據公鑰加密了,而支付寶也會有一個私鑰,私鑰是一樣的,那麼支付寶也會用私鑰來找公鑰,進行解密即可。

簡單弄一個demo玩一下,首先要下載一個沙箱app,把螞蟻金服上面給的demo搞下來,把包全部導入,使用阿里雲提供公鑰生成:

有兩種祕鑰的驗證方式,RSA和RSA2,代碼裏面默認選用了RSA2,那麼把對應的祕鑰填寫上去。


這句不去掉註釋是沒有圖片生成的。然後運行就可以了。
接下來就是業務環境搭建,把SDK複製過來改好包。支付模塊的業務其實很簡單,獲取二維碼,存在FTP服務器,返回前端,獲取支付結果。現在整個流程測試一下,首先是登錄,獲取jwt,然後使用jwt進行購票操作,發現居然觸發了熔斷器,但是這也發現熔斷器設置的一個不足,沒有配置是error日誌的打印,但是項目趕工在即,後面高可用或者項目維護再說其他的吧。這個錯誤顯示

updateById這個方法出現異常,mybatis plus的update更新這裏是用自帶的,默認會根據主鍵來進行更新,然而order表忘記建立主鍵了,所以找不到主鍵自然也就不存在什麼根據主鍵更新了,更新表結構爲帶主鍵方式,更新mapper文件。更新完成之後還是出現異常

這條語句異常,這條語句是插入了之後再從database讀出來,查閱數據庫發現,插入的uuid是null,也就是說主鍵是沒有被插入的,查看log打印的日誌,insert裏面沒有插入uuid,我使用insert默認提供的插入方法,orderT裏面哪個字段不是null就插入進去,但是很明顯uuid有的,問題就出現在配置文件mapper或者是orderT自己生成的模型中,orderT可能性不大,mapper文件查閱後發現確實沒有上面問題,百度發現問題在orderT生成的模型上面

主鍵不添加策略,默認是auto自增方式,但是String又不能自增,就填不進去了,加上type,變成手動input方式,這樣理論上應該是OK的了,然而還是觸發了熔斷器,去掉熔斷器發現是沒有問題的,那麼就是熔斷器的時間了,算了一下,至少要10s,一個請求10秒有點過分了,所以後面改進可能要進行分佈式或者異步改進,因爲這個請求涉及到了FTP的數據傳送,但是總算還是OK了。

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

用sandbox版的支付寶支付一下

顯示支付成功,那麼這樣這個下單支付的後臺基本沒有問題了,下面就是要把二維碼上傳到FTP上去了,這個時候生成二維碼服務可能會更加慢。另外打開WiFi就找不到服務的問題,今天突然可以了,這也是爲什麼今晚測試這麼快的原因。練着WiFi能找到服務可能是因爲我去了家裏另外一處房子,那裏的WiFi突然就可以了;以往這個bug得到的結論是WiFi的開關和Provider能否被Consumer找到互相有因果關係,現在原因可能會與WiFi路由器的不同相關,仍在觀查——2020.1.23 2:12分凌晨
配置上傳到ftp也很簡單,首先加入上傳的目錄:

配置路徑

    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("關閉流異常");
                e.printStackTrace();
            }
        }

設置編碼,上傳的是圖片,配置上傳格式二進制,配置被動模式,改變上傳目錄。**啓動的時候發現,上傳是成功了,但是上傳的路徑不對,上傳到了根目錄,也就是說,改變目錄的位置失敗了,突然這個changeWorkDirectory類似於Linux裏面的cd,那麼配置文件裏面寫的/qrcode應該要去掉/,然後就可以了。
**

Dubbo特性——本地存根

本地存根其實就類似於靜態代理,按照以往的方式,接口在客戶端,實現在服務端,那麼客戶端很受限制,比如參數的驗證等等;在拿到返回調用結果之後,用戶可能需要緩存結果;或者是在調用失敗的時候構造容錯數據,而不是簡單的拋出異常。找一個接口,做一個代理類,代理類訪問目標對象,當用戶要訪問目標對象需要先訪問代理。dubbo使用本地存根的時候會在客戶端生成一個代理,處理部分業務,而且Stub必須傳入proxy函數。

Consumer和Provider就是消費者和服務提供者,api就是之前寫的服務接口,以往的操作是略過dubbo,action訪問Service接口部分,接口通過Provider注入進來,**現在不一樣了,如果使用本地存根,首先先創建一個Stub,類似一個靜態代理,來實現service接口,消費者訪問的時候,先訪問Stub,既然Stub是代理,那麼action就不會直接訪問api接口,先訪問代理Stub,但是在邏輯上還是去訪問Servic接口,當Stub接到請求之後,轉發請求到ServiceProxy對象,如果Proxy滿足條件,就會路由到實現類Impl上,如果不滿足就到Mock中做返回,Mock也叫僞裝,proxy是遠程服務的代理實例,保護目標對象,提供間接訪問途徑。**但是這麼一看,其實我感覺用攔截器也可以的,而且Mock那個僞裝有點類似於Hystrix降級,還不是很能理解這玩意作用在哪裏。
我們現在的項目,客戶端是guns-gateway->API,服務端是guns-alipay還有其他的模塊訂單->Impl,這個時候對於用戶的orderId的驗證,就可以放在Stub裏面,這樣做的好處很多,首先可以保護目標對象,其次也可以減少一次緩存。本地存根理解爲一種比較特殊的靜態代理模式, 用於對真實目標的一種保護,或者額外增加功能, 攔截器更適合進行切面編程, 但是存根更適合對目標對象進行精準打擊,或者其實可以把這兩個內容變相理解爲靜態代理和動態代理之間的區別。這裏的容錯,也就是降級返回有點類似於Hystrix,但是本地存根的核心在於服務端反向調用客戶端獲取一些信息, 但是熔斷的目標是容錯,本質上來講不是一個東西,服務端在調用的時候需要客戶端的一些信息就可以用本地存根, 這個是Hystrix完全做不到的,有點像aop。
另外,本地存根還有一個本地僞裝的概念,本地僞裝是本地存根的一個子集,其實就是Mock,當失敗的時候就會走mock,但是用途反而是更多的。通常會使用本地僞裝來完成服務降級,前面Hystrix也是可以做服務降級,但是Hystrix是在springboot和dubbo才能用,如果不用springboot是使用不了的。一般在客戶端就可以實現。所以這玩意算作是一種補充把,使用到業務上體驗一下。比如想要在返回支付結果上做降級處理,只需要繼承這個類,然後service加上配置即可。

這樣一個接口的方法都可以降級了,而Hystrix相比之下只能是方法做降級,一個個方法的填上,本地僞裝相對簡單一點,但是也有本地僞裝只能是捕獲RPC的異常,RpcException,其他的不行,比如超時,網絡問題,找不到服務等等,而計算問題,除0異常等等都無法捕獲,所以各有優劣把。
dubbo還有隱式參數的特性,把參數放在RpcContext裏面可以通過getAttachment獲取,有些比較敏感的數據等等,正式業務系統裏面,往往會有一個requestId,這個requestId是request唯一,而分佈式鎖也是根據requestId生成,比如在獲取訂單狀態或者下單,可以把userId取出來對比防止僞造,這有點像spring裏面getAttribute,類似於一個全局變量,但是dubbo沒有全局變量這個說法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章