SpringBoot 整合 Zookeeper 接入Starring微服務平臺
背景
最近接的一個專案是基於公司產品Starring做的微服務支付平臺,純後臺專案,實現三方支付公司和銀行介面來完成使用者賬戶扣款,整合成通用支付介面釋出給前端呼叫。
但是扯蛋了,這邊前端什麼都不想做,只想我們提供一個連結,使用者可以選擇支付方式進行支付,這樣的話相當於咱們又得起一個WEB版的收銀臺Project。
最近SpringBoot挺流行的,那就單獨給起一個H5專案跑幾個頁面,呼叫後臺的支付介面就完事了,如下?
最終的系統架構成了這樣吧,隨便畫一畫,請客官別吐槽。
公司的產品的服務都是釋出到Zookeeper註冊中心的,結果我們SpringBoot收銀臺成了直連某個IP埠,要是交易量一起來把直連的12001壓垮了怎麼辦?
這樣顯然會存在問題,就因為一個收銀臺專案把整個微服務支付平臺變成了單節點,所以我們收銀臺SpringBoot專案也必須連到上面的ZK中去查詢平臺服務。
環境
SpringBoot 2.2.1.Release
解決思路
從單web專案轉成基於zookeeper呼叫的微服務專案:
1、Registry:服務註冊,公司產品Starring 採取Zookeeper 作為我們的註冊中心,我們現在要做的就是訂閱服務。
2、Provider:服務提供者(生產者),提供具體的服務實現,這個是支付後臺提供的服務。
3、Consumer:消費者,從註冊中心中訂閱服務,這個就是我們這邊收銀臺要實現的功能啦。
4、Monitor:監控中心,RPC呼叫次數和呼叫時間監控,這塊公司存在
從上圖中我們可以看出RPC 服務呼叫的過程主要為:
1、生產者釋出服務到服務註冊中心
2、消費者在服務註冊中心中訂閱服務
3、消費者呼叫已註冊的服務
操作步驟
A、配置檔案
B、建立自己的Zookeeper連線
C、查詢自己需要的服務
D、服務呼叫
A、配置檔案
1、Maven 配置檔案 pom.xml,引入zookeeper和zkclient兩個包。
<dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.5.6</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.101tec</groupId> <artifactId>zkclient</artifactId> <version>0.11</version> </dependency>排除slf4j是因為和其他jar包衝突,啟動時檢查報錯。
2、SpringBoot配置檔案 application.yml 增加zookeeper配置
zookeeper: address: serverhost:2181,serverhost:2182,serverhost:2183 timeout: 20000
B、建立Zookeeper連線
SpringBoot專案啟動後,自動連線Zookeeper配置中心,並獲取到zookeeper例項,只需要連線一次,所以使用的單例。
關注SpringBoot平臺啟動後執行事件【@PostConstruct 】
這裡需要注意,嘗試過多種平臺後執行事件來執行connect方法,只有這種方式在平臺載入完所有Bean後執行。其他的方式下,無法獲取Zookeeper中的配置。放在主函式後面執行,也不行。
package com.adtec.pay.util; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.ZooKeeper; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.io.IOException; import java.util.concurrent.CountDownLatch; @Component public class ZKWatcher implements Watcher { @Value("${zookeeper.address}") public String ZK_ADDRESS; @Value("${zookeeper.timeout}") public int ZK_TIMEOUT; private static ZKWatcher instance = null; private CountDownLatch latch = new CountDownLatch(1); private ZooKeeper zooKeeper; public ZKWatcher() { } public static ZKWatcher getInstance() { if (instance == null) { instance = new ZKWatcher(); } return instance; } // 平臺啟動後加載 @PostConstruct public void connect() throws IOException { zooKeeper = new ZooKeeper(ZK_ADDRESS, ZK_TIMEOUT, this); try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } setZooKeeper(zooKeeper); System.out.println("Zookeeper已連線成功:" + ZK_ADDRESS); } @Override public void process(WatchedEvent event) { if (event.getState() == Event.KeeperState.SyncConnected) { latch.countDown(); } } public ZooKeeper getZooKeeper() { return zooKeeper; } public void setZooKeeper(ZooKeeper zooKeeper) { this.zooKeeper = zooKeeper; } }
C、查詢自己的服務
好了,啟動專案,到這裡zookeeper已經連線上了。
現在咱們要發請求到後臺,該怎麼在註冊中心找到自己需要的服務呢 ?
上面也已經提到整個微服務執行模式,由生產者(Starring支付平臺)釋出服務到 註冊中心(Zookeeper),我們收銀臺專案是消費者要去訂閱服務的。也就是我們得去註冊中心搜服務。
所以我們首先得知道生產者釋出的服務到註冊中心是一個什麼路徑,就是生產者釋出到 Zookeeper的目錄節點。
稍微要懂一點Zookeeper知識,你才知道怎麼查節點。不懂的話,百度一下,或者看一下別人的: https://blog.csdn.net/java_66666/article/details/81015302,如果還看不會,那勸你洗洗睡吧。
這裡可以推薦一個圖形介面查Zookeeper的工具,如下圖:
通過工具檢視到我們的服務目錄節點路徑:/Inst/cty/800002/V1.0/IcpPayReq/V1.0
我們要呼叫的服務是:【IcpPayReq】,也就是我們定義的服務碼。
既然知道路徑,知道服務碼,事情就和把大象塞進冰箱需要幾步一樣。
1、獲取Zookeeper連線例項。
2、根據目錄節點獲取服務例項。
3、隨機選擇其中一個例項,獲取URL。
獲取請求的類如下:
package com.adtec.pay.util; import org.apache.zookeeper.ZooKeeper; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.util.LinkedList; import java.util.List; import java.util.Random; @Component public class ZKListener { // private static String SERVER_PATH = "/Inst/cty/800002/V1.0/IcpPayReq/V1.0"; private String SERVER_PATH = ""; private ZooKeeper zooKeeper; private List<String> paths = new LinkedList<>(); public void findTranUrl(String tranCode) { if (!StringUtils.isEmpty(tranCode)) { SERVER_PATH = "/Inst/cty/800002/V1.0/" + tranCode + "/V1.0"; } getChilds(); } private void getChilds() { List<String> ips = new LinkedList<>(); zooKeeper = ZKWatcher.getInstance().getZooKeeper(); try { //獲取子節點 List<String> childs = zooKeeper.getChildren(SERVER_PATH, false); for (String child : childs) { byte[] data = zooKeeper.getData(SERVER_PATH + "/" + child, false, null); String path = new String(data, "UTF-8"); ips.add(path); } this.paths = ips; } catch (Exception e) { e.printStackTrace(); } } public String getPath() { if (paths.isEmpty()) { return null; } //這裡我們隨機獲取一個ip埠使用 int index = new Random().nextInt(paths.size()); return paths.get(index); } }這樣就能找到真實請求地址了,愉快的傳送請求吧。
D、服務呼叫
這裡我寫了一個通用類,因為呼叫的服務不會只有一個,服務目錄路徑相同服務碼不同就可以通用了。
package com.adtec.pay.entity; import com.adtec.pay.util.CommUtil; import com.adtec.pay.util.ZKListener; import com.alibaba.fastjson.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; public class Request { @Autowired private ZKListener zkListener; @Value("${spring.profiles.active}") private String env; protected String url; protected Class<? extends Response> responseClass; public Request(String tranCode, Class<? extends Response> responseClass) { if (env.equals("dev")){ if (tranCode.equals("HosOrderQuery")) { this.url = "http://serverhost:13008/HttpServer/MEDICAL_MNG_SVR/QryOrderDetail"; } else if (tranCode.equals("IcpPayReq")) { this.url = "http://serverhost:13008/HttpServer/MEDICAL_MNG_SVR/IcpPayReq"; } else if (tranCode.equals("QryOrderDetail")) { this.url = "http://serverhost:13008/HttpServer/MEDICAL_MNG_SVR/QryOrderDetail"; } } else { zkListener.findTranUrl(tranCode); String path = zkListener.getPath(); ZKStatusEntity zkStatus = JSONObject.parseObject(path, ZKStatusEntity.class); this.url = zkStatus.getCOM_HTTP().getURL() + "/" + tranCode; } this.responseClass = responseClass; } public void setUrl(String url) { this.url = url; } public void setResponseClass(Class<? extends Response> responseClass) { this.responseClass = responseClass; } public <T> Response send(Request request) { return CommUtil.httpRequestJSON(url, request, responseClass); } }
總結
這次通過做這個專案,摸索了很多SpringBoot的細節,遇到了很多看著很小又很影響進度的問題。
1、專案啟動後加載所有Bean檔案後啟動,嘗試了很多種方式。
2、通用的請求類整合,泛型確實用的不太熟悉,需要再多理解。
3、新的HttpClient包包名 org.apache.httpcomponents 的請求方法除錯,網上很多都是老的方法。
搞定,收工。