2 基于Seata实现分布式事务
2.1 Seata简介
- Seata用于解决分布式事务
- Seata非常适合解决微服务分布式事务【dubbo、SpringCloud….】
- Seata性能高
- Seata使用简单
2.2 Seata事务模式-AT模式
Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
一个典型的分布式事务过程:
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
- XID 在微服务调用链路的上下文中传播。
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
- TM 向 TC 发起针对 XID 的全局提交或回滚决议。
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
AT模式使用前提:
- 基于支持本地 ACID 事务的关系型数据库。
- Java 应用,通过 JDBC 访问数据库。
AT模式机制:
基于两阶段提交协议的演变。
一阶段:
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成。
如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。
注意此处seata版本是0.7.0+ 增加字段 context
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, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
AT模式部分代码如下:不需要关注执行状态,对业务代码侵入较小。
/** * 此代码为示例代码, 不需要演示, 主要看AT和TCC代码的区别使用 */ @GlobalTransactional(timeoutMills = 300000, name = "dubbo-demo-tx") public void purchase(String userId, String commodityCode, int orderCount) { LOGGER.info("purchase begin ... xid: " + RootContext.getXID()); storageService.deduct(commodityCode, orderCount); orderService.create(userId, commodityCode, orderCount); throw new RuntimeException("AT 模式发生异常,回滚事务"); }
2.3 Seata Server端环境准备
(1)从官网上下载seata server端的程序包,解压到一个地方
下载地址:https://github.com/seata/seata/releases
(2)修改配置
我们是基于file的方式启动注册和承载配置的
打开conf/file.conf文件
修改service 节点目录内容如下:
service { #vgroup->rgroup vgroup_mapping.my_test_tx_group = "default" #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" }
说明:需要修改default.grouplist = “127.0.0.1:8091”,将该值设置为seata server向外提供服务ip及端口(或域名+端口)
(4)启动server
到bin目录下执行脚本启动seata server端,注:windows下执行seata-server.bat
启动;linux下执行seata-server.sh
启动
2.4 项目集成seata-仔细点
2.4.1 创建日志表undo_log
分别在leadnews_article、leadnews_user、leadnews_wemedia三个库中都创建undo_log表。已经做好。
2.4.2 导入依赖包
因为有多个工程都需要引入seata,所以新建一个工程oldlu-leadnews-seata专门来处理分布式事务
<dependencies> <dependency> <groupId>com.oldlu</groupId> <artifactId>oldlu-leadnews-common</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <version>2.1.0.RELEASE</version> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>0.9.0</version> <exclusions> <exclusion> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.21</version> </dependency> </dependencies>
2.4.3 创建代理数据源
(1)因为多个工程都需要依赖与seata,所以在oldlu-leadnews-seata模块下创建seata的配置类
package com.oldlu.seata.config; import com.alibaba.druid.pool.DruidDataSource; import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import io.seata.rm.datasource.DataSourceProxy; import org.apache.ibatis.plugin.Interceptor; import org.mybatis.spring.transaction.SpringManagedTransactionFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import javax.sql.DataSource; @Configuration @EnableConfigurationProperties({MybatisPlusProperties.class}) public class DataSourcesProxyConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() { return new DruidDataSource(); } //创建代理数据源 @Primary//@Primary标识必须配置在代码数据源上,否则本地事务失效 @Bean public DataSourceProxy dataSourceProxy(DataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } private MybatisPlusProperties properties; public DataSourcesProxyConfig(MybatisPlusProperties properties) { this.properties = properties; } //替换SqlSessionFactory的DataSource @Bean public MybatisSqlSessionFactoryBean sqlSessionFactory(DataSourceProxy dataSourceProxy, PaginationInterceptor paginationInterceptor) throws Exception { // 这里必须用 MybatisSqlSessionFactoryBean 代替了 SqlSessionFactoryBean,否则 MyBatisPlus 不会生效 MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean = new MybatisSqlSessionFactoryBean(); mybatisSqlSessionFactoryBean.setDataSource(dataSourceProxy); mybatisSqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); mybatisSqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath*:/mapper/*.xml")); MybatisConfiguration configuration = this.properties.getConfiguration(); if(configuration == null){ configuration = new MybatisConfiguration(); } mybatisSqlSessionFactoryBean.setConfiguration(configuration); //设置分页 Interceptor[] plugins = {paginationInterceptor}; mybatisSqlSessionFactoryBean.setPlugins(plugins); return mybatisSqlSessionFactoryBean; } }
(2)分别在oldlu-leadnews-article、oldlu-leadnews-user、oldlu-leadnews-wemedia引入oldlu-leadnews-seata工程
<dependency> <groupId>com.oldlu</groupId> <artifactId>oldlu-leadnews-seata</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
并且添加一下配置类:
@Configuration @ComponentScan("com.oldlu.seata.config") public class SeataConfig { }
2.4.4 配置seata-server链接和注册中心信息
修改注册中心配置,在每个项目中必须按照下方要求来
将配置文件file.conf和配置文件register.conf放到每个需要参与分布式事务项目的resources中。
- file.conf中的service.default.grouplist修改成seata-server的IP地址file.conf中的
- service.vgroup_mapping.xxx改成vgroup_mapping.#{spring.application.name}_tx_group = “default”
特别注意:#{spring.application.name}
是一个变量,指的是该项目的名称
如自媒体微服务名称的项目名称如下:
那么其配置就是vgroup_mapping.leadnews-wemedia_tx_group = "default"
其他项目也是这么依次配置
2.4.5 指定事务分组
分别在oldlu-leadnews-article、oldlu-leadnews-user、oldlu-leadnews-wemedia微服务的application.yml文件中添加如下配置:
spring: cloud: alibaba: seata: tx-service-group: ${spring.application.name}_tx_group
2.4.6 在分布式事务控制方法上添加注解@GlobalTransactional
在ApUserRealnameServiceImpl类的updateStatusById方法上加上@GlobalTransactional
注解
2.4.7 启动seata-server
运行:
/seata/bin/seata-server.bat
2.4.8 测试
(1)功能测试,看功能能否正常执行。
(2)异常测试,我们在方法中添加int x=1/0
,看认证信息和自媒体用户是否能够回滚。