解决方案
框架
从使用成本的角度来看,直接使用开源的分布式事务框架是比较容易上手的,所以寻找了比较流行的分布式事务框架seata(阿里开源组件)、Hmily(TCC模式)、 tcc-transaction(TCC模式)
seata | hmily | tcc-transaction | |
---|---|---|---|
文档 | 全 | 全 | 不全 |
支持模式 | AT/tcc/saga/XA | tcc | tcc |
star | 18.6k | 3.4k | 4.9k |
对比了几款开源框架,阿里的开源框架seata使用人数最多,支持模式最全,文档也相对较多,所以选用seata进行了试用,下面记录下seata的使用步骤。
seata
原理
Seata的分布式事务解决方案是业务层面的解决方案,只依赖于单台数据库的事务能力。Seata框架中一个分布式事务包含3中角色:
-
Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
(就是我们的Seata-server,用来保存全局事务,分支事务,全局锁等记录,然后会通知各个RM进行回滚或者提交.)
-
Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
( 也是我们的一个微服务,但是该微服务是一个带头大哥,充当全局事务的发起者(决定了全局事务的开启,回滚,提交等)凡是我们的微服务中标注了@GlobalTransactional ,那么该微服务就会被看成一个TM,同时也是一个RM。)
-
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
(可以理解为我们的一个个微服务,也叫事务的参与者)
其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。如下图所示:
下面是一个分布式事务在Seata中的执行流程:
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
- XID 在微服务调用链路的上下文中传播。
- RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。
- TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
使用
1.提前搭建nacos作为配置中心和注册中心
2.seata-server搭建
- 下载最新版本1.4.0 seata-server Releases · seata/seata (github.com)
- 修改配置,打开\seata-server-1.4.0\seata\conf文件夹,对file.conf进行修改(主要是对事务日志的配置,mode改为db,并配好的数据库配置,sql语句可在seata/script/server/db at develop · seata/seata (github.com)下载,自行创建数据库导入sql)
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://ip:3306/seata"
user = username
password = password
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
- 对registry.conf进行修改,这里主要进行注册中心和配置中心的配置,此处均选择nacos作为注册中心和配置中心,注册中心和配置中心需要使用其它的组件请自行研究Seata 是什么
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
application = "seata-server"
serverAddr = "http://ip:8848"
group = "SEATA_GROUP"
#nacos中的命名空间ID
namespace = "a86602c7-0172-4348-9821-e8f28fe4b80e"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "http://ip:8848"
#nacos中的命名空间ID
namespace = "a86602c7-0172-4348-9821-e8f28fe4b80e"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}
- 修改完配置后,可以启动seata-server,启动脚本在\seata-server-1.4.0\seata\bin 文件夹下面,
- 启动脚本:nohup sh seata-server.sh -p 8091 -h nacosip -m db &
- 可以在nacos上看到,seata-server已经成功注册上了
3.导入配置中心配置
下载配置文件config.txtseata/config.txt at develop · seata/seata (github.com)
下载nacos导入脚本seata/script/config-center/nacos at develop · seata/seata (github.com)
修改配置文件,只需修改service.vgroupMapping.tx-service-group和数据库配置即可
#最需要关注的事务分组——tx-service-group 这值会在我们客户端对应,需要注意
service.vgroupMapping.tx-service-group=default
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
#数据库地址和之前file.conf中的数据库配置一致
store.db.url=jdbc:mysql://ip:3306/seata?useUnicode=true
store.db.user=username
store.db.password=password
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
在windoes下执行脚本,sh nacos-config.sh -h nacosip -p 8848,把配置成功导入到nacos中
4.搭建微服务
一、创建事务日志表
测试工程由3个微服务构成,数据库也是三个,每个数据库需要提前创建一个事务日志表,sql可在此处下载seata/script/client/at/db at develop · seata/seata (github.com)
二、添加pom依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.0</version>
</dependency>
三、修改启动类注解
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
四、添加代理数据源配置
@Configuration
@MapperScan(basePackages = {"com.demo.seata.mapper"})
public class MyBatisConfig {
/**
* 从配置文件获取属性构造datasource,注意前缀,这里用的是druid,根据自己情况配置,
* 原生datasource前缀取"spring.datasource"
*
* @return
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public DataSource hikariDataSource() {
return new HikariDataSource();
}
/**
* 构造datasource代理对象,替换原来的datasource
*
* @param hikariDataSource
* @return
*/
@Primary
@Bean("dataSource")
public DataSourceProxy dataSourceProxy(DataSource hikariDataSource) {
return new DataSourceProxy(hikariDataSource);
}
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:/mybatis/mapper/**/*.xml"));
sqlSessionFactoryBean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:/mybatis/mybatis-config.xml"));
sqlSessionFactoryBean.setTypeAliasesPackage("com.demo.seata.domin");
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
return sqlSessionFactoryBean;
}
}
五、添加及修改配置
#seata配置(配置事务组 需要和seata‐server的配置一样)
spring.cloud.alibaba.seata.tx-service-group=tx-service-group
下载client的配置文件file.conf和register.conf,下载地址seata/script/client/conf at develop · seata/seata (github.com)
client端的file.conf、register.conf配置和seata-server的file.conf、register.conf一致,请按照自身情况进行修改
六、微服务发起者加上全局事务注解@GlobalTransactional
@GlobalTransactional(name = "prex-create-order",timeoutMills = 30000,rollbackFor = Exception.class)
//@Transactional
@Override
public void createOrder(Order order) {
log.info("当前 XID: {}", RootContext.getXID());
log.info("下单开始,用户:{},商品:{},数量:{},金额:{}", order.getUserId(), order.getProductId(), order.getCount(), order.getPayMoney());
//创建订单
order.setStatus(0);
orderMapper.saveOrder(order);
log.info("保存订单{}", order);
//远程调用库存服务扣减库存
log.info("扣减库存开始");
remoteStorageService.reduceCount(order.getProductId(), order.getCount());
log.info("扣减库存结束");
//远程调用账户服务扣减余额
log.info("扣减余额开始");
remoteAccountService.reduceBalance(order.getUserId(), order.getPayMoney());
log.info("扣减余额结束");
//System.out.println(1/0);
//修改订单状态为已完成
log.info("修改订单状态开始");
orderMapper.updateOrderStatusById(order.getId(),1);
log.info("修改订单状态结束");
log.info("下单结束");
}
六、测试
经测试,在调用链的任意一处出现异常,都可以成功回滚 出现异常的服务及所有上游服务的sql
至此,已经完成了seata的一个简单试用,可以看到seata的搭建是比较复杂的,但是使用起来比较简单,只需要一个注解就可以达到解决分布式事务的问题,此处试用的是seata最简单的AT模式,seata还支持tcc/saga/xa模式,此处不再介绍。
当然实际应用中业务可能更复杂,需要进一步测试进行验证,本文仅做分布式事务方案的一个初步试用,具体使用什么解决方案和框架还要做深入探讨。