分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小的操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
1. Seata介绍
Seata的设计目标是对业务无侵入,因此从业务无侵入的两阶段提交方案着手,在传统两阶段提交的基础上演进。他把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务。
Seata主要由三个重要组件组成,其中TC(Transaction Coordinator)事务协调器,用来管理全局的分布式事务的状态,用于全局性事务的提交和回滚。TM(Transaction Manager)事务管理器,用于开启全局、提交或者回滚全局事务。RM(Resource Manager)资源管理器,用户分支事务上的资源管理,向TC注册分支事务,上报分支事务的状态,接受TC的命令来提交或者回滚事务。
Seata的执行流程如下几个步骤:
1. A服务的TM向TC申请开启一个全局事务,TC就会创建一个全局事务并返回一个唯一的XID
2. A服务的RM向TC注册分支事务,并及其纳入XID对应全局事务的管辖
3. A服务执行分支事务,向数据库做操作
4. A服务开始远程调用B服务,此时XID会在微服务的调用链上传播
5. B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖
6. B服务执行分支事务,向数据库做操作
7. 全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚
8. TC协调其管辖之下的所有分支事务, 决定是否回滚
更直观的解释如下图所示:
2. 启动Seata服务端
Seata的服务端指的就是Seata的事务协调器(TC),它可以管理全局分支事务的状态,用于开启、提交或者回滚全局事务。
对于Seata的使用,我们首先需要下载一个Seata的服务器。我们可以从https://github.com/seata/seata/releases/v0.9.0/这个地址下载Seata服务器。然后需要在Seata的registry.conf文件中修改Seata的启动配置,具体的修改方法如下所示:
registry {
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
}
config {
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
}
然后我们需要在Seata的nacos-config.txt文件中修改配置,将我们需要实现分布式事务的微服务加入到该nacos-config.txt文件中,具体的格式如下所示,将我们的product微服务和order微服务都加入到该配置文件中:
service.vgroup_mapping.service-product=default
service.vgroup_mapping.service-order=default
然后我们在Seata的conf文件夹下,使用nacos-config.sh 127.0.0.1初始化seata在nacos中的配置,然后再seata的bin目录下使用seata-server.bat -p 9000 -m file启动seata服务,即可将seata服务注册到nacos中。
3. 使用Seata实现微服务的事务控制
首先我们需要在数据库中添加一个Seata事务实现所需的表,创建该表的语句如下所示,该表的作用主要是记录微服务的一些操作,如果在微服务接下来的工作中发生了一场需要回滚的话。就可以根据该表进行事务的回滚。
CREATE TABLE `undo_log`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL,
`context` VARCHAR(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
`ext` VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = INNODB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8;
然后我们就可以在我们需要实现分布式事务的微服务的pom.xml文件中添加seata的依赖,依赖的版本如下所示:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
每一个微服务都是一个分支事务,分支事务需要进行回滚。微服务的事务回滚是通过配置一个DataSourceProxyConfig类实现的,我们通过使用@Configuration和@Bean注解将该类以及类中的对象创建到我们的框架容器中。
@Configuration
public class DataSourceProxyConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
@Primary
@Bean
public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
在resources下添加Seata的配置文件registry.conf,具体的配置内容如下所示:
registry {
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
}
config {
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
}
bootstrap.yml的配置文件如下所示,注意这里的tx-service-group的地址应该与我们上面在nacos-config.txt配置的保持一致:
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: localhost:8848 # nacos的服务端地址
namespace: public
group: SEATA_GROUP
alibaba:
seata:
tx-service-group: ${spring.application.name}
最后我们在service接口的实现类中的方法上加入@GlobalTransactional注解,即可实现分布式事务。
//下面这个注解@GlobalTransactional是seata中的开启全局事务的注解
@GlobalTransactional
@Override
public void createrOrder(Order order) {
orderMapper.createrOrder(order);
}
4. seata client和seata server的通信流程
我们在上面启动的seata server就是我们常说的事务协调器(TC)。而微服务是事务管理器,对于事务管理器和事务协调器之间,可以进行通信。通信的第一步是将seata server向注册中心注册,然后seata client向注册中心注册。最后seata client(微服务)通过注册中心返回的seata server的地址与端口找到seata server然后进行通信。在2中我们配置了registry.conf指定了服务端的注册方式和配置。然后在nacos-config.txt文件中加入了需要使用seata分布式事务的微服务名称。
在第3个部分,我们设置了事务管理器也就是微服务也就是seata client的注册方式。首先在registry.conf文件中指定seata client的注册方式和配置。然后在bootstrap.yml中指定事务服务的组,这样seata server和seata client都注册到nacos了,两者可以通过nacos进行通信。最后在想实现的方法上加上@GlobalTransactional注解即可实现分布式事务。
用DataSourceProxy(java.sql.Datasource)这种包含关系,同时DataSourceProxy的父类AbstractDataSourceProxy实现了Datasource 接口,所以DataSourceProxy 也是Datasource 的一个实现,这使得DataSourceProxy有机会分析要执行的SQL 与生成对应的回滚SQL。那么我们只要把DataSourceProxy 注册成默认的java.sql.Datasource实现,并提供给其他使用框架(mybatis, jdbctemplate)装配,就达到我们的目的了。所以client端一切的配置都围绕着把DataSourceProxy 注册成默认的java.sql.Datasource,或者把数据库访问框架的Datasource配置DataSourceProxy 为来进行的。
对于数据库访问框架的Datasource配置DataSourceProxy可见下面的示例代码,下面的示例是以Mybatis框架为基础做的测试。
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:/mapper/*.xml"));
return factoryBean.getObject();
}
}
我们的application.yml的配置如下所示:
server:
port: 8081
spring:
zipkin:
base-url: http://192.168.10.20:9411/ #zipkin server的请求地址
discovery-client-enabled: false #这里是让nacos把他当成一个URLC,而不是当作服务名
sleuth:
sampler:
probability: 1.0 #采用的百分比 这里是测试 所示设置为百分之百 也就是所有的链路都会采样到
application:
name: service-product
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop?serverTimezone=UTC&useUnicode=true&characterEncodintg=utf-8&useSSL=false
username: root
password: root
cloud:
nacos:
discovery:
server-addr: 192.168.10.20:8848
mybatis:
mapper-locations: classpath:mapper/*.xml
5. seata实现分布式事务的测试
首先我们接收前端界面的uid(用户的id)和Pid(产品的id)这两个参数,代码如下所示:
@RequestMapping("/order/prod")
public Order order(Integer uid,Integer Pid){
然后在该微服务中我们进行了两个操作,第一个操作是远程调用产品微服务对产品的库存进行更新,代码如下所示:
// 下了订单之后,产品的库存将会降低
int id=product.getPid();
int stock=product.getStock();
stock=stock-1;
productService.update_stock(id,stock);
然后我们调用订单微服务的service写入新创建的订单,代码如下所示:
//下单(创建订单)这里是利用已经有的信息 组装出一个订单的类
Order order=new Order();
//下面是用户的一些属性,这里为了简便 直接进行模拟了
order.setUid(uid);
String name=user.getUsername();
order.setUsername(name);
//下面是商品的一些信息
order.setPid(Pid);
order.setPname(product.getPname());
order.setPprice(product.getPprice());
//下面是产品的数量 这里为了简单也直接模拟了
order.setNumber(1);
int a=1/0;
// 我们将这个创建的订单类加入到订单的数据库中
orderService.createrOrder(order);
如图所示,我们在创建订单的过程中,认为创造了一个异常,我们在产品微服务的service的实现类中,加上了@GlobalTransactional注解。我们观察是否实现了分布式微服务。
如下图所示,产品的表如下图所示:
通过前端URL,访问该方法,我们可以看到如下图所示,虽然执行了产品的更新库存的行为,但是因为后面发生了异常,所以发生了回滚,更新库存的行为没有实施:
我们将产品微服务中更新库存的方法上的@GlobalTransactional注解注释之后,然后再执行该操作,得到的结果如下所示,id为1的产品库存降低了一个,由此可以看到我们成功使用seata解决了简单的分布式微服务的事务。