SpringCloud Alibaba實戰(12:引入Dubbo實現RPC調用)
- 2021 年 12 月 23 日
- 筆記
- SpringCloud, SpringCloud Alibaba
源碼地址://gitee.com/fighter3/eshop-project.git
持續更新中……
大家好,我是老三,斷更了半年,我又滾回來繼續寫這個系列了,還有人看嗎……
在前面的章節中,我們使用Fegin完成了服務間的遠程調用,實際上,在更加註重性能的互聯網公司中,一般都會使用RPC框架,如Dubbo等,來實現遠程調用。
這一節,我們就來把我們的服務間調用從Feign改造成Dubbo。
1.Dubbo簡介
Apache Dubbo 是一款微服務開發框架,它提供了 RPC通信與微服務治理兩大關鍵能力。這意味着,使用 Dubbo 開發的微服務,將具備相互之間的遠程發現與通信能力, 同時利用 Dubbo 提供的豐富服務治理能力,可以實現諸如服務發現、負載均衡、流量調度等服務治理訴求。
這是Dubbo官網對Dubbo的簡介,Dubbo在國內是應用非常廣泛的服務治理框架,曾經一度停更,後來又重新維護,並從Apache畢業。
在這一節里,我們主要關注它的RPC通信的能力。
這裡再額外提一個老生常談的問題,Dubbo和我們前面用的Feign的區別:
Dubbo在性能上有優勢,Feign使用起來更便捷,接下來,我們來一步步學習Dubbo的使用。
2.Dubbo基本使用
在前面我們使用Feign遠程調用實現了一個業務添加商品
,接下來,我們把它改造成基於Dubbo遠程調用實現。
2.1.服務提供者
我們將原來的eshop-stock
拆成兩個子module,eshop-stock-api
和eshop-stock-service
,其中eshop-stock-api
是主要是RPC接口的定義,eshop-stock-service
則是完成庫存服務的主要業務。
2.1.1.eshop-stock-api
- 依賴引入,eshop-stock-api主要是接口和實體類的定義,所以只需要引入對common包的依賴和lombok的依賴
<!--對common的依賴-->
<dependency>
<groupId>cn.fighter3</groupId>
<artifactId>eshop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
- 接口和實體定義
StockApiService.java
:這個接口定義了兩個方法,在哪實現呢?往後看。
/**
* @Author 三分惡
* @Date 2021/11/14
* @Description 對外RPC接口定義
*/
public interface StockApiService {
/**
* 添加庫存
*
* @param stockAddDTO
* @return
*/
Integer addStock(StockAddDTO stockAddDTO);
/**
* 根據商品ID獲取庫存量
*
* @param goodsId
* @return
*/
Integer getAccountById(Integer goodsId);
}
StockAddDTO.java
:添加庫存實體類
/**
* @Author: 三分惡
* @Date: 2021/5/26
* @Description:
**/
@Data
@Builder
@EqualsAndHashCode(callSuper = false)
public class StockAddDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 商品主鍵
*/
private Integer goodsId;
/**
* 數量
*/
private Integer account;
}
2.1.2.eshop-stock-service
我們把原來eshop-stock的相關業務代碼都改到了這個module里。
同時,為了實現RPC服務的提供,我們需要:
- 導入依賴:主要需要導入兩個依賴
dubbo
的依賴,和eshop-stock-api
接口聲明的依賴,這裡的<scope> 設置為compile,這樣我們在編譯eshop-stock-service的時候,也會編譯相應的api依賴。
<!--Dubbo-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
<!--對api的依賴-->
<dependency>
<groupId>cn.fighter3</groupId>
<artifactId>eshop-stock-api</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
-
StockApiServiceImpl.java
:創建一個類,實現api中聲明的接口,其中@Service
是Dubbo提供的註解,表示當前服務會發佈成一個遠程服務,不要和Spring提供的搞混。/** * @Author 三分惡 * @Date 2021/11/14 * @Description 庫存服務提供RPC接口實現類 */ @org.apache.dubbo.config.annotation.Service @Slf4j public class StockApiServiceImpl implements StockApiService { @Autowired private ShopStockMapper stockMapper; /** * 添加庫存 * * @param stockAddDTO * @return */ @Override public Integer addStock(StockAddDTO stockAddDTO) { ShopStock stock = new ShopStock(); stock.setGoodsId(stockAddDTO.getGoodsId()); stock.setInventory(stockAddDTO.getAccount()); log.info("準備添加庫存,參數:{}", stock.toString()); this.stockMapper.insert(stock); Integer stockId = stock.getStockId(); log.info("添加庫存成功,stockId:{}", stockId); return stockId; } /** * 獲取庫存數量 * * @param goodsId * @return */ @Override public Integer getAccountById(Integer goodsId) { ShopStock stock = this.stockMapper.selectOne(Wrappers.<ShopStock>lambdaQuery().eq(ShopStock::getGoodsId, goodsId)); Integer account = stock.getInventory(); return account; } }
- 遠程調用配置:我們需要在
applicantion.yml
中進行dubbo相關配置,由於在之前,我們已經集成了nacos作為註冊中心,所以一些服務名、註冊中心之類的就不用配置。完整配置如下:
# 數據源配置 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/shop_stock?characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=GMT%2B8 username: root password: root application: name: stock-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 server: port: 8050 # dubbo相關配置 dubbo: scan: # dubbo服務實現類的掃描基準包路徑 base-packages: cn.fighter3.serv.service.impl #Dubbo服務暴露的協議配置 protocol: name: dubbo port: 1
- 遠程調用配置:我們需要在
2.2.服務消費者
我們的商品服務作為服務的消費者,為了後續開發的考慮,我也類似地把eshop-goods
拆成了兩個子moudule,服務消費放在了eshop-goods-service
里。
-
引入依賴:引入兩個依賴
dubbo
和eshop-stock-api
,因為在一個工程里,所以對api的依賴同樣用了<scope>為compile的方式,在實際的業務開發中,通常會把服務提供者的api打包上傳到私服倉庫,然後服務消費者依賴api包,這樣就可以直接調用api包里定義的方法。<!--Dubbo相關包--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-dubbo</artifactId> </dependency> <!--對api的依賴--> <dependency> <groupId>cn.fighter3</groupId> <artifactId>eshop-stock-api</artifactId> <version>1.0-SNAPSHOT</version> <scope>compile</scope> </dependency>
-
遠程調用:使用
@Reference
注入相應的service,就可以像調用本地jar包一樣,調用遠程服務。
ShopGoodsServiceImpl.java:
@Service
@Slf4j
public class ShopGoodsServiceImpl extends ServiceImpl<ShopGoodsMapper, ShopGoods> implements IShopGoodsService {
@org.apache.dubbo.config.annotation.Reference
StockApiService stockApiService;
/**
* 添加商品
*
* @param goodsAddDTO
* @return
*/
public CommonResult addGoods(GoodsAddDTO goodsAddDTO) {
ShopGoods shopGoods = new ShopGoods();
BeanUtils.copyProperties(goodsAddDTO, shopGoods);
this.baseMapper.insert(shopGoods);
log.info("添加商品,商品主鍵:{}", shopGoods.getGoodsId());
log.info(shopGoods.toString());
StockAddDTO stockAddDTO = StockAddDTO.builder().goodsId(shopGoods.getGoodsId()).account(goodsAddDTO.getAccount()).build();
log.info("準備添加庫存,參數:{}", stockAddDTO.toString());
Integer stockId = this.stockApiService.addStock(stockAddDTO);
log.info("添加庫存結束,庫存主鍵:{}", stockId);
return CommonResult.ok();
}
/**
* 獲取商品
*
* @param goodsId
* @return
*/
public CommonResult<GoodsVO> getGoodsById(Integer goodsId) {
GoodsVO goodsVO = new GoodsVO();
//獲取商品基本信息
ShopGoods shopGoods = this.baseMapper.selectById(goodsId);
BeanUtils.copyProperties(shopGoods, goodsVO);
//獲取商品庫存數量
Integer account = this.stockApiService.getAccountById(goodsId);
log.info("商品數量:{}", account);
goodsVO.setAccount(account);
return CommonResult.ok(goodsVO);
}
}
-
相關配置:需要在
applicantion.yml
里進行配置,主要配置了要訂閱的服務名。完整配置:# 數據源配置 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/shop_goods?characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=GMT%2B8 username: root password: root application: name: goods-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 server: port: 8020 #dubbo配置 #要訂閱的服務名,多個用,隔開 dubbo: cloud: subscribed-services: stock-service
2.3.調試
- 依次啟動
Nacos-Server
,庫存服務
,商品服務
,可以看到Nacos服務列表裡有兩個服務
-
打開我們商品服務的knife4j接口//localhost:8020/doc.html,調試添加商品接口
-
上圖可以看到,接口響應成功,查看控制台日誌,發現發生了遠程調用,查看數據庫,發現商品庫和庫存庫都新增了數據
到此,我們一個簡單的Dubbo遠程調用就完成了。
3.Dubbo進階使用
在Feign的使用中,它自身集成了Ribbon實現客戶端負載均衡,還需要額外繼承Hystrix來實現熔斷,我們接下來看看類似的一些能力Dubbo是怎麼做的。
3.1.集群容錯
網絡通信中存在很多不可控因素,例如網絡延遲、網絡中斷、服務異常等等,這時候就需要我們的服務消費者在調用服務提供者提供的接口是,對失敗的情況進行處理,儘可能保證服務調用成功。
Dubbo默認提供了6中容錯模式,默認為Failover 重試[1]。
- Failover Cluster:失敗自動切換,當出現失敗,重試集群中的其它服務。可通過
retries="2"
來設置重試次數,但重試會帶來更長延遲。一般用於讀操作,因為可能會帶來數據重複問題。 - Failfast Cluster:快速失敗,只發起一次調用,失敗立即報錯。通常用於非冪等性的寫操作,比如新增記錄。
- Failsafe Cluster:失敗安全,出現異常時,直接忽略。通常用於寫入審計日誌等操作。
- Failback Cluster:失敗自動恢復,後台記錄失敗請求,定時重發。通常用於消息通知操作。
- Forking Cluster:並行調用集群中的多個服務,只要一個成功即返回。通常用於實時性要求較高的讀操作,但需要浪費更多服務資源。可通過
forks="2"
來設置最大並行數。 - Broadcast Cluster:廣播調用所有提供者,逐個調用,任意一個服務報錯則報錯。通常用於通知所有提供者更新緩存或日誌等本地資源信息。
配置方式很簡單,只需要在指定服務的@Service註解上增加一個參數就行了——在@Service註解參數中增加cluster = "failfast"
。
@org.apache.dubbo.config.annotation.Service(cluster = "failfast")
@Slf4j
public class StockApiServiceImpl implements StockApiService {
在實際應用中,我們可以把讀寫操作接口分開定義和和實現,讀操作接口用默認的Failover Cluster
,寫操作用Failfast Cluster
。
3.2.負載均衡
Dubbo中內置了5種負載均衡策略,默認為random。
算法 | 特性 | 備註 |
---|---|---|
RandomLoadBalance | 加權隨機 | 默認算法,默認權重相同 |
RoundRobinLoadBalance | 加權輪詢 | 借鑒於 Nginx 的平滑加權輪詢算法,默認權重相同, |
LeastActiveLoadBalance | 最少活躍優先 + 加權隨機 | 背後是能者多勞的思想 |
ShortestResponseLoadBalance | 最短響應優先 + 加權隨機 | 更加關注響應速度 |
ConsistentHashLoadBalance | 一致性 Hash | 確定的入參,確定的提供者,適用於有狀態請求 |
配置方式也很簡單,在@Service
註解上增加參數loadbalance = "roundrobin"
:
@org.apache.dubbo.config.annotation.Service(cluster = "failfast",loadbalance = "roundrobin")
3.3.服務降級
Dubbo提供了一種Mock配置來實現服務降級,也就是說當服務提供方出現網絡異常無法訪問時,服務調用方不直接拋出異常,而是通過降級配置返回兜底數據。主要步驟如下:
- 在
eshop-goods-service
(服務消費者)中創建MockStockApiServiceImpl,實現StockApiServiceImpl,重寫接口方法,返回本地兜底的數據。
/**
* @Author 三分惡
* @Date 2021/11/14
* @Description 庫存服務降級兜底類
*/
@Slf4j
public class MockStockApiServiceImpl implements StockApiService {
@Override
public Integer addStock(StockAddDTO stockAddDTO) {
log.error("庫存服務添加庫存接口調用失敗!");
return 0;
}
@Override
public Integer getAccountById(Integer goodsId) {
log.error("庫存服務獲取庫存接口調用失敗!");
return 0;
}
}
- 使用也很簡單,在
ShopGoodsServiceImpl
(調用遠程服務的類)的@Reference
註解,增加mock
參數,設置降級類;我們同時設置設置集群容錯cluster="failfast"
快速失敗。
@Service
@Slf4j
public class ShopGoodsServiceImpl extends ServiceImpl<ShopGoodsMapper, ShopGoods> implements IShopGoodsService {
@org.apache.dubbo.config.annotation.Reference(mock = "cn.fighter3.serv.service.impl.MockStockApiServiceImpl",
cluster = "failfast")
StockApiService stockApiService;
- 不啟動服務提供者,我們就可以看到降級數據。
Dubbo實際上還有很多高級的功能,可以滿足很多場景的需求,更多內容可以查看官網://dubbo.apache.org/zh/docs/advanced/。
4.總結
在本節里,我們把遠程調用由Feign改成了Dubbo,學習了Dubbo的一些基礎和進階用法。經過Alibaba的操刀,Dubbo已經能比較快捷地融入SpringCloud的體系中,如果對性能有一定的要求,那妥妥地可以考慮採用Dubbo作為遠程調用框架。
實際上,這一節,經過我自己的遷移,Dubbo在應用上確實比Feign稍微麻煩一點點,我原本的計劃的是使用Feign作為主要的遠程調用組件,但實際上大部分真實電商項目基本都是使用Dubbo,或者自研RPC框架,所以這個項目後面的業務開發,決定改成Dubbo。
系列文章持續更新中,
點贊
、關注
不迷路,咱們下期見。
參考:
[1]. Dubbo官方文檔
[2]. 《Spring Cloud Alibaba 微服務原理與實戰》