原始碼地址:https://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介面http://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實際上還有很多高階的功能,可以滿足很多場景的需求,更多內容可以檢視官網:https://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 微服務原理與實戰》