在后面一篇咱们介绍了 Seata 分布式事务系列一: Seata 服务端搭建.
这一篇咱们应用 Seata 进行一个 Dubbo 微服务分布式事务的示例. 本示例蕴含 4 个我的项目:
- API Rest 服务, 对立对外的业务模块, 这里示例一个购物服务 purchase(). 其 Dubbo 身份为服务消费者.
- Account 账户微服务, 下单前需扣除账户余额. 其 Dubbo 身份为服务提供者.
- Order 订单微服务, 创立订单. 其 Dubbo 身份为服务消费者 + 提供者.
- Inventory 库存微服务, 创立订单前需扣除商品库存. 其 Dubbo 身份为服务提供者.
四者之间的调用关系如下图所示:
一. API 服务
API 模块为一个一般的 SpringBoot Web Restful API 服务, 为 Dubbo 服务消费者. 在 purchase() 中, 启动全局事务.
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:
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
:
@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 了.
@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 我的项目的依赖基本一致. 能够间接参考下面.
2.2 我的项目配置
各微服务项目 dubbo 和 seata 无关的配置也基本一致. 只不过这三者作为微服务提供者, dubbo 相干的会多一些 protocol 配置. 这里以 Order 我的项目为例:
# ========= 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;
}
// 省略其余业务代码
}