在前面一篇我们介绍了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依赖
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.5.RELEASE
fun.faceless.dubboshop.api
parent
pom
1.0.0-SNAPSHOT
api-center
dubboshop api center
1.8
2.1.5.RELEASE
5.1.7.RELEASE
2.7.7
org.springframework.boot
spring-boot-starter-web
${spring-boot.version}
org.springframework.boot
spring-boot-devtools
${spring-boot.version}
runtime
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.3
org.mybatis.dynamic-sql
mybatis-dynamic-sql
1.1.4
mysql
mysql-connector-java
8.0.16
com.alibaba
druid
1.0.29
org.springframework.boot
spring-boot-starter-data-redis
${spring-boot.version}
org.apache.commons
commons-pool2
2.6.2
com.fasterxml.jackson.core
jackson-core
2.9.5
com.fasterxml.jackson.core
jackson-databind
2.9.5
org.apache.rocketmq
rocketmq-spring-boot-starter
2.1.0
org.springframework.boot
spring-boot-starter-cache
${spring-boot.version}
org.ehcache
3.6.3
ehcache
javax.cache
cache-api
1.1.0
io.seata
seata-spring-boot-starter
1.3.0
org.apache.dubbo
dubbo-bom
${dubbo.version}
pom
org.apache.dubbo
dubbo
${dubbo.version}
org.apache.thrift
libthrift
org.apache.dubbo
dubbo-spring-boot-starter
${dubbo.version}
org.apache.dubbo
dubbo-metadata-report-zookeeper
${dubbo.version}
org.apache.dubbo
dubbo-rpc-rest
${dubbo.version}
org.apache.tomcat.embed
tomcat-embed-core
8.5.32
org.apache.dubbo
dubbo-dependencies-zookeeper
2.7.1
pom
log4j
log4j
slf4j-log4j12
org.slf4j
fun.faceless.dubboshop
comms
1.0.0-SNAPSHOT
fun.faceless.dubboshop.inventory
dubboapi
1.0.0-SNAPSHOT
fun.faceless.dubboshop.account
dubboapi
1.0.0-SNAPSHOT
fun.faceless.dubboshop.order
dubboapi
1.0.0-SNAPSHOT
org.projectlombok
lombok
1.18.8
true
org.springframework.boot
spring-boot-starter-test
${spring-boot.version}
1.2 项目配置文件
在 application.yaml
中配置 seata:
spring:
# 这里省略spring相关的常规配置...
dubbo:
# 注册中心配置
registry:
id: dubboshop-registry
address: zookeeper://:2181
group: dubbo
simplified: true
metadata-report:
address: zookeeper://: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: :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("Welcome to %s", "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://:2181
group: dubbo
# 注册中心配置
registry:
id: dubboshop-registry
address: zookeeper://:2181
group: dubbo
simplified: true
check: true
metadata-report:
address: zookeeper://: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: :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;
}
// 省略其他业务代码
}