seata-快速使用
什么是分布式事务?
随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用:
传统单体Web应用
传统单体应用
拆分后的架构
微服务架构
分布式系统会把一个应用系统拆分为可独立部署的多个服务(通常一个服务对应着一个DB),因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络协作完成事务称为分布式事务。例如,商品添加,加库存,创建订单减库存等都是分布式事务。
分布式事务产生的场景
- 比较典型的场景就是微服务架构,微服务之间通过远程调用完成事务操作。上边的例子是订单服务和库存服务,再比如商品服务和库存服务。也同样会出现分布式事务问题。创建商品的同时,需要请求库存服务增加商品。简而言之就是:跨JVM进程产生分布式事务。
-
单体系统访问多个数据库实例,当单体系统需要访问多个数据库实例时就会产生分布式事务。比如用户管理系统,用户信息和订单信息分别在两个MySQL实例中存储,用户关系系统删除用户信息,需要同时删除用户个人信息以及订单信息。由于数据库在不同的数据库实例,需要通过不同的数据库连接去操作数据,此时产生分布式事务。简而言之就是:跨数据库实例产生分布式事务。
image.png
- 多服务访问同一数据库实例,比如商品微服务和库存微服务即使访问同一个数据库实例也会产生分布式事务,原因就是跨JVM进程,两个微服务持有了不同的数据库连接进行数据库操作,此时产生分布式事务。
分布式事务解决方案
-
刚性事务:
标准分布式事务(2pc/3pc) -
柔性事务:
可靠消息最终一致性
TCC
最大努力通知
纯补偿性
本篇着重介绍标准分布式事务(2pc)解决方案,seata。
在开始介绍之前,先来看一个例子。
案例
案例环境
我在本地创建了3个项目,分别是product商品服务, stock库存服务,admin统一的对外服务。每个服务单独对应一个数据库。在admin中进行调用product添加商品,然后调用库存服务进行添加库存。
三个服务分别对应着不同的数据库,由于是演示出分布式事务出现的问题,所以表结构相对简单。
pm_product表
CREATE TABLE `pm_product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`prod_name` varchar(255) NOT NULL COMMENT '商品名称',
`model` varchar(255) NOT NULL COMMENT '商品型号',
`price` decimal(20,10) NOT NULL '商品价格',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4;
pm_stock表
CREATE TABLE `pm_stock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`prod_id` bigint(20) NOT NULL COMMENT '商品ID',
`quantity` int(11) NOT NULL '商品数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4;
admin服务中核心调用逻辑如下:
public ServerResponse saveProduct(ProductAddReq productAddReq) {
ProductReq productReq = new ProductReq();
productReq.setModel(productAddReq.getModel());
productReq.setPrice(productAddReq.getPrice());
productReq.setProdName(productAddReq.getProdName());
// 添加商品 调用商品服务
ServerResponse<Long> serverResponse = productFeign.saveProduct(productReq);
if (!serverResponse.isSuccess() || serverResponse.getData() == null) {
return serverResponse;
}
// 添加库存 调用库存服务
StockReq stockReq = new StockReq();
stockReq.setProdId(serverResponse.getData());
stockReq.setQuantity(productAddReq.getQuantity());
ServerResponse stockResponse = stockFeign.saveStock(stockReq);
if (!stockResponse.isSuccess()) {
return stockResponse;
}
return ServerResponse.ok("商品添加成功");
}
然后启动项目进行运行,发现一切正常,表中对应的数据也都正常。
stock库中和product库中数据一致。
问题引出
然后我们开始制造一些意外,首先把表中数据情况,便于对比数据。比如此时我把stock服务关闭,然后调用admin中的接口进行添加商品,看看会发生什么情况。
POST http://localhost:8080/product
Content-Type: application/json
{
"prodName": "商品",
"price": 23,
"model": "",
"quantity": 100
}
很显然报错了,返回前端错误,但是此时数据库里面已经产生了脏数据。
报错
pm_product表
pm_product
pm_stock表
pm_stock
可以发现pm_product表中插入了一条数据,而pm_stock表中没有数据,此时明显是不正确的,pm_product表中数据属于脏数据。
产生这种情况的现象,还有很多种,例如,库存服务中有个异常,然后导致库存服务本地事务回滚,但是商品服务已经插进去了。还有就是admin服务中在调用完商品服务之后报异常,会导致报错,商品服务插入数据成功,但是库存服务没有被调用,也会产生脏数据。
解决方案
那么该如何解决这种情况呢?
解决这种情况的方案有很多种,这里只介绍基于两阶段提交的seata方案。
seata是由阿里中间件团队发起的开源项目Fescar,后更名为seata,它是一个开源的分布式事务框架、传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件,主要的优点是性能较好,且不长时间占用资源,它以高效并且对业务0侵入的方式解决微服务场景下面临的分布式事务问题。它的宗旨是像解决本地事务一样,来解决分布式事务。好,现在来看一下如何来快速使用seata。关于原理性的东西这里先不做过多介绍,先着重看一下如何将seata引入到项目中来。
seata方案
1.引入pom依赖。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
2.添加一个配置类
@Configuration
public class DataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Primary
@Bean("dataSourceProxy")
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
}
3.配置seata-server。将上述配置加入到每个工程中。admin服务可以不用加,只加在需要操作DB的服务中。好,下面还需要一个配置一下seata-server。可以把它当成一个全局事务的协调器。具体的下载地址如下:
https://seata.io/zh-cn/blog/download.html
下下来之后,其目录结构如下:
image.png
现在来简单说明一下每个目录下的文件,bin目录下主要是启动脚本,windows用户启动.bat文件,Linux和Mac用户启动.sh文件。conf目录主要是一些配置文件。
1. file.conf 主要是一些服务端和客户端的一些配置。
2. registry.conf 主要是seata-server的注册方式。它可以注册到
redis,zk,eureka中,来对它进行管理。
本案例主要使用eureka来进行注册。然后对这俩文件进行更改。
在registry.conf中仅需更改eureka地址,然后把应用名称也设置一下。
eureka {
serviceUrl = "http://127.0.0.1:8761/eureka"
application = "seata-server"
weight = "1"
}
之后更改file文件,首先更改store部分,这是seata-server在运行过程中,需要将运行时的一些临时数据存储的地方。这里我将它们存储在DB中,需要配置一下地址等。
store {
## store mode: file、db 存储模式,选择DB
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
}
## database store property 数据库的一些配置
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "chusen"
}
}
4.建库建表。然后需要在数据库中创建一个seata库,之后创建一些seata-server运行时需要的一些表。
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`resource_group_id` varchar(128) DEFAULT NULL,
`resource_id` varchar(256) DEFAULT NULL,
`lock_key` varchar(256) DEFAULT NULL,
`branch_type` varchar(8) DEFAULT NULL,
`status` tinyint(4) DEFAULT NULL,
`client_id` varchar(64) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `global_table` (
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(64) DEFAULT NULL,
`transaction_service_group` varchar(64) DEFAULT NULL,
`transaction_name` varchar(128) DEFAULT NULL,
`timeout` int(11) DEFAULT NULL,
`begin_time` bigint(20) DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(128) DEFAULT NULL,
`transaction_id` mediumtext,
`branch_id` mediumtext,
`resource_id` varchar(256) DEFAULT NULL,
`table_name` varchar(32) DEFAULT NULL,
`pk` varchar(128) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`row_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
seata库下一共三个表。
然后还需要在每个服务对应的库下创建一个undo_log表。用户记录分支事务数据。
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 DEFAULT CHARSET=utf8;
数据库建表到此为止。
5.下面再来简单配置一下每个服务。在每个服务的spring配置文件中加入一行配置
spring.cloud.alibaba.seata.tx-service-group = tx_group
然后进行更改seata-server中的file配置文件
service {
#transaction service group mapping
## 这里tx_group对应上面配置的tx——group。seata-server对应上面eureka中注册的应用名称
vgroup_mapping.tx_group = "seata-server"
#only support when registry.type=file, please don't set multiple addresses
seata-server.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
最后将seata-server中conf下更改过的file.conf和registry.conf这两个文件复制到每个服务的resources目录下面。大功告成。首先启动seata-server。
然后分别启动每个服务。启动之后,可以在seata-server控制台打印的日志可以看到。以admin服务为例。
image.png
然后重新访问下接口,发现一切正常,之后将stock服务关掉,看pm_product中是否还会产生脏数据。结果大失所望,不生效,这是为什么呢?
因为还差很关键的一步,因为你要让seata知道,要为哪个方法添加全局事务。很简单,在刚才的方法上面添加一个注解即可。这体现了seata对业务代码0侵入,真的像使用本地事务一样。
@GlobalTransactional
public ServerResponse saveProduct(ProductAddReq productAddReq) {
ProductReq productReq = new ProductReq();
productReq.setModel(productAddReq.getModel());
productReq.setPrice(productAddReq.getPrice());
productReq.setProdName(productAddReq.getProdName());
// 添加商品
ServerResponse<Long> serverResponse = productFeign.saveProduct(productReq);
if (!serverResponse.isSuccess() || serverResponse.getData() == null) {
return serverResponse;
}
// 添加库存
StockReq stockReq = new StockReq();
stockReq.setProdId(serverResponse.getData());
stockReq.setQuantity(productAddReq.getQuantity());
ServerResponse stockResponse = stockFeign.saveStock(stockReq);
if (!stockResponse.isSuccess()) {
return stockResponse;
}
return ServerResponse.ok("商品添加成功");
}
然后进行测试,大功告成!当运行时,出现异常时,分支事务会被回滚。可以在seata-server的控制台看到相关日志。
image.png
当没有异常时,可以看到全局事务会被提交。
image.png
小结
1.在使用微服务架构时,既带来了很多优势,同时也带来了很多新的挑战,这其中就包括分布式事务问题。
2.解决分布式事务问题,有很多种解决方案,本篇着重介绍了基于2pc提交的seata方案的使用。
3.需要注意的是,每个服务下面的配置文件需要和seata-server中的配置文件一致。
网友评论