1 解决问题
当在Spring Cloud搭建的分布式系统中,如果某个业务涉及到多个服务的事务,无法保证当某一个服务异常时,其他所有业务服务都进行事务的回滚,就会导致业务数据不一致的问题
2 解决方案
使用阿里巴巴开源的分布式事务框架Seata,目前支持的注册中心有nacos、eureka、zk、consul、etcd3、sofa等。
2.1 优点
1、Seata基于SQL解析实现了事务回滚的自动补偿,无需开发者自己实现,降低了框架对业务的侵入性
2、事务协调者独立部署成一个微服务,同样降低了对业务的侵入性
2.2 缺点
1、Seata的日志回滚表,有一个字段用来存储事务修改数据前后的数据镜像,为了存储内容很大的镜像选择了longblob类型,所以回滚表的插入性能不是很好,即使数据镜像很小
2.3 Seata支持的分布式事务模式
1、AT模式:在传统的二阶段提交协议上进行优化,普通的二阶段提交,执行过程中所有节点时同步阻塞的,导致速度很慢,AT模式进行了优化:在第一阶段,节点直接提交本地事务,并记录undo log,然后提交结果到Seata全局的事务管理器;在第二阶段,如果其他服务异常,全局事务管理器异步发送通过undo log记录发送回滚请求到各节点,进行事务的补偿回滚,完毕后删除undo log记录。此模式的前提是本地数据源必须是jdbc连接的支持本地事务的数据库
2、TCC模式:此模式可以自定义准备、提交、回滚的策略,相对于AT模式,可以支持任何数据源,在第一个准备阶段会将数据加锁,能保证隔离性,需要自己去开发各个阶段的处理逻辑,对业务的侵入性很大,适合不支持事务的数据库,比如impala
3、Saga模式:本质是二阶段提交的实现版本之一,在第一阶段直接提交本地事务,释放锁,保证高性能,代价是不保证事务的隔离性,适合业务流程长的事务,第二阶段出现异常执行补偿逻辑
4、XA模式:XA模式需要数据源支持XA协议
3 搭建流程
3.1 技术选型
使用eureka+feign+mybatis,Seata使用AT模式
3.2 Seata服务端搭建
1、https://github.com/seata/seata/releases
下载最新发布版本的压缩包,并解压
2、配置config目录下的registry.conf
修改三个地方
registry.type:指定注册中心,这里指定为eureka
registry.eureka.serviceUrl:eureka注册地址
registry.eureka.application:Seata注册到eureka的服务名
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "eureka" loadBalance = "RandomLoadBalance" loadBalanceVirtualNodes = 10 nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "" cluster = "default" username = "" password = "" } eureka { serviceUrl = "http://127.0.0.1:8080/eureka/" application = "seata-server" weight = "1" } redis { serverAddr = "localhost:6379" db = 0 password = "" cluster = "default" timeout = 0 } zk { cluster = "default" serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } consul { cluster = "default" serverAddr = "127.0.0.1:8500" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { appId = "seata-server" apolloMeta = "http://192.168.1.204:8801" namespace = "application" apolloAccesskeySecret = "" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }
2、配置config目录下的file.conf
service.vgroupMapping.{事务群组}:Seata注册到eureka的服务名
(注意这里的事务群组,后面要与客户端配置的事务群组一致)
Seata注册到eureka的服务名.grouplist
store.mode:指定事务信息存储方式,这里指定为数据库存储
db下面的配置指定一些数据源的配置,要注意如果使用mysql5和mysql8的driverClassName是不同的
数据库需要导入三张表,建表语句:https://github.com/seata/seata/tree/develop/script/server/db
service { #transaction service group mapping vgroupMapping.my_test_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 } ## transaction log store, only used in seata-server store { ## store mode: file、db、redis mode = "db" ## file store property file { ## store location dir dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions maxBranchSessionSize = 16384 # globe session size , if exceeded throws exceptions maxGlobalSessionSize = 512 # file buffer size , if exceeded allocate new buffer fileWriteBufferCacheSize = 16384 # when recover batch read size sessionReloadReadSize = 100 # async, sync flushDiskMode = async } ## 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.cj.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=UTC" user = "root" password = "root" minConn = 5 maxConn = 100 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 maxWait = 5000 } ## redis store property redis { host = "127.0.0.1" port = "6379" password = "" database = "0" minConn = 1 maxConn = 10 maxTotal = 100 queryLimit = 100 } }
3、如果需要覆盖Seata服务的eureka配置,可以手动增加eureka-client.properties文件进行配置
4、前往bin目录,启动Seata服务端,Linux下使用seata-server.sh启动,Windows下使用seata-server.bat启动
3.3客户端服务搭建
客户端搭建有两种方式,一种是直接将服务器的配置文件放到客户端项目中,另一种是使用Springboot starter的方式引入。starter的方式更便于配置的统一管理,但是目前有一些starter属性无效,还是要加入一点文件配置,比如disableGlobalTransaction
3.3.1 引入配置文件方式
1、客户端为标准的Springboot项目,请自行配置好注册eureka,feign调用,mybatis等配置
2、引入Seata依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-seata</artifactId> <version>2.2.0.RELEASE</version> <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>
3、配置bootstrap.yml
这里指定事务群组,与上方的服务器配置保持一致
spring:
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group
4、配置数据源
每个业务数据库需要创建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 AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
注意使用Seata的数据源代理DataSourceProxy
@Configuration public class DataSourceConfiguration { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ DruidDataSource druidDataSource = new DruidDataSource(); return druidDataSource; } @Primary @Bean("dataSource") public DataSourceProxy dataSource(DataSource druidDataSource){ return new DataSourceProxy(druidDataSource); } @Bean public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy)throws Exception{ SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath*:/mapper/*.xml")); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
5、将服务端的配置文件file.conf和registry.conf直接放到客户端新项目的resource目录下
6、启动客户端项目,查看日志是否register success
7、在业务的调用函数上加上@GlobalTransactional注解,即可实现分布式事务
3.3.2 Springboot starter方式
1、客户端为标准的Springboot项目,请自行配置好注册eureka,feign调用,mybatis等配置
2、引入Seata依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-seata</artifactId> <version>2.2.0.RELEASE</version> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>
3、配置bootstrap.yml
seata: enabled: true # 开启seata registry: # 指定注册中心 type: eureka eureka: service-url: ${eureka.client.service-url.defaultZone} application: seata-server weight: 1 service: vgroup-mapping: seata-server # seata服务端的服务名 tx-service-group: my_test_tx_group # 事务群组,与服务端配置一致
4、配置数据源
每个业务数据库需要创建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 AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
由于starter默认是开启自动数据源代理的,所有不需要额外配置数据源代理
5、由于部分starter配置不生效,需要将是否禁止全局事务的属性通过配置file.conf文件引入,将此文件放入resource目录下即可
service { disableGlobalTransaction = false }
6、启动客户端项目,查看日志是否register success
7、在业务的调用函数上加上@GlobalTransactional注解,即可实现分布式事务