SpringCloud Alibaba實戰(12:引入Dubbo實現RPC呼叫)

三分惡發表於2021-12-22

原始碼地址:https://gitee.com/fighter3/eshop-project.git

持續更新中……

大家好,我是老三,斷更了半年,我又滾回來繼續寫這個系列了,還有人看嗎……

在前面的章節中,我們使用Fegin完成了服務間的遠端呼叫,實際上,在更加註重效能的網際網路公司中,一般都會使用RPC框架,如Dubbo等,來實現遠端呼叫。

這一節,我們就來把我們的服務間呼叫從Feign改造成Dubbo。

1.Dubbo簡介

Architecture-來自官網

Apache Dubbo 是一款微服務開發框架,它提供了 RPC通訊與微服務治理兩大關鍵能力。這意味著,使用 Dubbo 開發的微服務,將具備相互之間的遠端發現與通訊能力, 同時利用 Dubbo 提供的豐富服務治理能力,可以實現諸如服務發現、負載均衡、流量排程等服務治理訴求。

這是Dubbo官網對Dubbo的簡介,Dubbo在國內是應用非常廣泛的服務治理框架,曾經一度停更,後來又重新維護,並從Apache畢業。

在這一節裡,我們主要關注它的RPC通訊的能力。

這裡再額外提一個老生常談的問題,Dubbo和我們前面用的Feign的區別:

Dubbo和Feign主要區別

Dubbo在效能上有優勢,Feign使用起來更便捷,接下來,我們來一步步學習Dubbo的使用。

2.Dubbo基本使用

在前面我們使用Feign遠端呼叫實現了一個業務新增商品,接下來,我們把它改造成基於Dubbo遠端呼叫實現。

Dubbo遠端呼叫實現增加庫存

2.1.服務提供者

我們將原來的eshop-stock拆成兩個子module,eshop-stock-apieshop-stock-service,其中eshop-stock-api是主要是RPC介面的定義,eshop-stock-service則是完成庫存服務的主要業務。

stock拆分

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

eshop-goods拆分

  • 引入依賴:引入兩個依賴dubboeshop-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服務列表裡有兩個服務

Nacos註冊中心

  • 開啟我們商品服務的knife4j介面http://localhost:8020/doc.html,除錯新增商品介面

    介面除錯

  • 上圖可以看到,介面響應成功,檢視控制檯日誌,發現發生了遠端呼叫,檢視資料庫,發現商品庫和庫存庫都新增了資料

控制檯日誌

到此,我們一個簡單的Dubbo遠端呼叫就完成了。

3.Dubbo進階使用

在Feign的使用中,它自身整合了Ribbon實現客戶端負載均衡,還需要額外繼承Hystrix來實現熔斷,我們接下來看看類似的一些能力Dubbo是怎麼做的。

3.1.叢集容錯

網路通訊中存在很多不可控因素,例如網路延遲、網路中斷、服務異常等等,這時候就需要我們的服務消費者在呼叫服務提供者提供的介面是,對失敗的情況進行處理,儘可能保證服務呼叫成功。

Dubbo預設提供了6中容錯模式,預設為Failover 重試[1]。

cluster-來自官網

  • 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 微服務原理與實戰》

[3]. 遠端呼叫 Dubbo 與 Feign 的區別

相關文章