Seata 分佈式事務系列二: Dubbo分佈式事務

在前面一篇咱們介紹了Seata 分佈式事務系列一: Seata 服務端搭建.html

這一篇咱們使用Seata 進行一個 Dubbo 微服務分佈式事務的示例. 本示例包含 4 個項目:java

  • API Rest服務, 統一對外的業務模塊, 這裏示例一個購物服務 purchase(). 其 Dubbo 身份爲服務消費者.
  • Account 帳戶微服務, 下單前需扣除帳戶餘額. 其 Dubbo 身份爲服務提供者.
  • Order 訂單微服務, 建立訂單. 其 Dubbo 身份爲服務消費者 + 提供者.
  • Inventory 庫存微服務, 建立訂單前需扣除商品庫存. 其 Dubbo 身份爲服務提供者.

四者之間的調用關係以下圖所示:
image.pngmysql

一. API 服務

API 模塊爲一個普通的 SpringBoot Web Restful API服務, 爲 Dubbo 服務消費者. 在 purchase() 中, 啓動全局事務.web

1.1 maven依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>fun.faceless.dubboshop.api</groupId>
    <artifactId>parent</artifactId>
    <packaging>pom</packaging>
    <version>1.0.0-SNAPSHOT</version>
    <name>api-center</name>
    <description>dubboshop api center</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot.version>2.1.5.RELEASE</spring-boot.version>
        <spring.version>5.1.7.RELEASE</spring.version>
        <dubbo.version>2.7.7</dubbo.version>
    </properties>


    <dependencyManagement>
        <dependencies>
            <!-- springboot related -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${spring-boot.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <version>${spring-boot.version}</version>
                <scope>runtime</scope>
            </dependency>

            <!-- db data -->
            <!-- 對應 mybatis 3.5.5,從3.5.0開始支持mapper返回Optional -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.3</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/org.mybatis.dynamic-sql/mybatis-dynamic-sql -->
            <dependency>
                <groupId>org.mybatis.dynamic-sql</groupId>
                <artifactId>mybatis-dynamic-sql</artifactId>
                <version>1.1.4</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.16</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.0.29</version>
            </dependency>

            <!-- Redis -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
                <version>${spring-boot.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-pool2</artifactId>
                <version>2.6.2</version>
            </dependency>
            <!--jackson依賴-->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>2.9.5</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>2.9.5</version>
            </dependency>

            <!-- rocketmq -->
            <!--<dependency>
                <groupId>org.apache.rocketmq</groupId>
                <artifactId>rocketmq-client</artifactId>
                <version>4.7.0</version>
            </dependency>-->
            <dependency>
                <groupId>org.apache.rocketmq</groupId>
                <artifactId>rocketmq-spring-boot-starter</artifactId>
                <version>2.1.0</version>
            </dependency>

            <!-- caching -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-cache</artifactId>
                <version>${spring-boot.version}</version>
            </dependency>
            <dependency>
                <groupId>org.ehcache</groupId>
                <version>3.6.3</version>
                <artifactId>ehcache</artifactId>
            </dependency>
            <dependency>
                <groupId>javax.cache</groupId>
                <artifactId>cache-api</artifactId>
                <version>1.1.0</version>
            </dependency>

            <!-- Seata -->
            <dependency>
                <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
                <version>1.3.0</version>
            </dependency>

            <!-- Dubbo dependencies -->
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-bom</artifactId>
                <version>${dubbo.version}</version>
                <type>pom</type>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo</artifactId>
                <version>${dubbo.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.apache.thrift</groupId>
                        <artifactId>libthrift</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-spring-boot-starter</artifactId>
                <version>${dubbo.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-metadata-report-zookeeper</artifactId>
                <version>${dubbo.version}</version>
            </dependency>

            <!-- dubbo REST protocal -->
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-rpc-rest</artifactId>
                <version>${dubbo.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.tomcat.embed</groupId>
                <artifactId>tomcat-embed-core</artifactId>
                <version>8.5.32</version>
            </dependency>

            <!-- Zookeeper dependencies-->
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-dependencies-zookeeper</artifactId>
                <version>2.7.1</version>
                <type>pom</type>
                <exclusions>
                    <exclusion>
                        <artifactId>log4j</artifactId>
                        <groupId>log4j</groupId>
                    </exclusion>
                    <exclusion>
                        <artifactId>slf4j-log4j12</artifactId>
                        <groupId>org.slf4j</groupId>
                    </exclusion>
                </exclusions>
            </dependency>

            <!-- inter-project modules -->
            <dependency>
                <groupId>fun.faceless.dubboshop</groupId>
                <artifactId>comms</artifactId>
                <version>1.0.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>fun.faceless.dubboshop.inventory</groupId>
                <artifactId>dubboapi</artifactId>
                <version>1.0.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>fun.faceless.dubboshop.account</groupId>
                <artifactId>dubboapi</artifactId>
                <version>1.0.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>fun.faceless.dubboshop.order</groupId>
                <artifactId>dubboapi</artifactId>
                <version>1.0.0-SNAPSHOT</version>
            </dependency>

            <!-- other dependencies -->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.8</version>
                <optional>true</optional>
            </dependency>

            <!-- testing related -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <version>${spring-boot.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 將上面經過 dependencyManagement 指定版本號的依賴根據須要放到這裏... -->
    </dependencies>

1.2 項目配置文件

application.yaml中配置 seata:redis

spring:
  # 這裏省略spring相關的常規配置...
  
dubbo:
  # 註冊中心配置
  registry:
    id: dubboshop-registry
    address: zookeeper://<your_zk_host>:2181
    group: dubbo
    simplified: true
  metadata-report:
    address: zookeeper://<your_zk_host>:2181
    group: dubbo
  application:
    name: dubboshop-api
    id: dubboshop-api
    logger: slf4j

# 這裏 seata 未使用註冊中心. 若是使用過
seata:
  application-id: dubboshop-api         # Seata 應用編號,
  tx-service-group: dubboshop-api-group # Seata 事務組編號,用於 TC 集羣名
  # Seata 服務配置項,對應 ServiceProperties 類
  service:
    # 虛擬組和分組的映射
    vgroup-mapping:
      dubboshop-api-group: default      # 這裏 dubboshop-api-group 對應上面 tx-service-group 的值.
    # 分組和 Seata 服務的映射. 不適用 Nacos 註冊中心時, 配置grouplist, 不然不須要該項.
    grouplist:
      default: <your_seata_server_host>:8091

1.3 業務代碼

項目啓動WebApplication:spring

@SpringBootApplication
@RestController
@ComponentScan("fun.faceless.dubboshop.api")
public class WebApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
    }

    @GetMapping("/")
    public String home() {
        return String.format("<html>Welcome to %s</html>", "DubboShopApiCenter");
    }
}

業務代碼, 這裏爲了方便, 事務直接放在了OrderController中, 沒有單獨去寫一個Service了.sql

@RestController
@RequestMapping("/api")
@Slf4j
public class OrderController {

    // 在 dubbo 2.7.x 中, cluster="failfast" 存在bug, 沒法正常生效, 因此須要設置 retries=0.
    @Reference(protocol = "dubbo", cluster="failfast", retries=0)
    OrderProvider orderProvider;

    @Reference(protocol = "dubbo", cluster="failfast", retries=0)
    AccountCreditProvider accountCreditProvider;

    private String dubboConfigCenterAddr = "yyadmin:2181";

    @PostMapping(value = "purchase")
    @GlobalTransactional  // 注意這裏啓動全局事務
    public Result purchase(@RequestParam int userId, @RequestParam int goodsId, @RequestParam int goodsNum) {
        log.info("當前 XID: {}", RootContext.getXID());

        int unitCredit = 10;
        int creditAmount = goodsNum * unitCredit;
        accountCreditProvider.debit(userId, creditAmount);
        orderProvider.createOrder(userId, goodsId, goodsNum);
        return Result.succ("Order created successfully.");
    }
}

二. 其餘微服務

其餘微服務指 Account, Inventory, Order 3 個微服務項目. 三者的配置和方式都基本同樣, 核心就是在相關 Service 上使用 @Transactional 進行本地事務管理. 此外, 能夠經過 RootContext獲取全局事務ID: xid.數據庫

2.1 Maven依賴

微服務項目的 dubbo 和 seata 有關依賴跟 api項目的依賴基本一致. 能夠直接參考上面.apache

2.2 項目配置

各微服務項目 dubbo 和 seata 有關的配置也基本一致. 只不過這三者做爲微服務提供者, dubbo 相關的會多一些 protocol 配置. 這裏以 Order 項目爲例:segmentfault

# ========= Dubbo Provider ==============
dubbo:
  # 配置中心, see: http://dubbo.apache.org/zh-cn/docs/user/configuration/config-center.html
  config-center:
    address: zookeeper://<your_zk_host>:2181
    group: dubbo
  # 註冊中心配置
  registry:
    id: dubboshop-registry
    address: zookeeper://<your_zk_host>:2181
    group: dubbo
    simplified: true
    check: true
  metadata-report:
    address: zookeeper://<your_zk_host>:2181
    group: dubbo
  application:
    name: dubboshop-order
    id: dubboshop-order
    logger: slf4j
  # 示例多 Protocol, 這裏支持 dubbo 和 rest 兩種 Protocols.
  protocols:
    dubbo:
      name: dubbo
      server: netty4
      port: 20884
      accesslog: true
    rest:
      name: rest
      server: tomcat
      port: 8084
      accesslog: true
  scan:
    # dubbo 服務提供者實現類所在包
    base-packages: fun.faceless.dubboshop.order.dubboprovider


# Seata 配置項,對應 SeataProperties 類
seata:
  application-id: dubboshop-order           # Seata 應用編號,默認爲 ${spring.application.name}
  tx-service-group: dubboshop-order-group   # Seata 事務組編號,用於 TC 集羣名
  # Seata 服務配置項,對應 ServiceProperties 類
  service:
    # 虛擬組和分組的映射
    vgroup-mapping:
      dubboshop-order-group: default        # 這裏 dubboshop-order-group 對應 上面 tx-service-group 的值.
    # 分組和 Seata 服務的映射. 不適用 Nacos 註冊中心時, 配置grouplist, 不然不須要該項.
    grouplist:
      default: <your_seata_server_host>:8091

# 本來純dubbo服務不須要配置 server.port, 不過 Seata 會自動啓動 SpringBoot Web, 須要配置一個端口. 注意若是是同一主機上啓動這幾個服務, 這個端口取不一樣的值. 不然會報端口占用異常.
server:
  port: 28084

2.3 業務代碼

@Slf4j
@Service(cluster="failfast", retries=0, timeout = 2000)
public class OrderProviderImpl implements OrderProvider {

    // 本地持久化 DAO
    @Autowired
    private OrderDao orderDao;

    // Dubbo調用 InventoryProvider
    @Reference(protocol="dubbo", cluster="failfast", retries=0, timeout = 1500)
    private InventoryProvider inventoryProvider;

    /**
     * 調用Dubbo服務, 扣除庫存. 而後提交數據庫, 建立一個新訂單.
     *
     * @param userId
     * @param goodsId
     * @param orderCount
     * @return 返回受影響的記錄行數
     */
    @Override
    @Transactional      // <- 注意這裏須要本地事務
    public int createOrder(int userId, int goodsId, int orderCount) {
        log.info("當前 XID: {}", RootContext.getXID());

        Result debitResult = inventoryProvider.deduce(goodsId, orderCount);
        return this.saveOrder(userId, goodsId, orderCount);
    }

    private int saveOrder(int userId, int goodsId, int orderCount) {
        Order order = new Order();
        order.setUserId(userId);
        order.setState((short) 10);
        order.setGoodsInfo(prepareNewGoodsInfo(goodsId, orderCount));
        int recordsAffected = orderDao.createOrder(order);

        return recordsAffected;
    }
    // 省略其餘業務代碼
}
相關文章
相關標籤/搜索