分布式事务的学习

解决分布式事务的理论方案#

  • 全局事务XA规范
  • 最终一致性:2PC,TCC
  • 可靠消息服务(例如rocketmq的事务消息;rabbitmq基于ack的消息确认)
  • 最大努力通知

分布式事务场景有如下几种形式:

  • 一个应用中操作多个数据库需要保证数据一致性
  • 微服务不同模块操作同一个数据库需要保证数据一致性
  • 微服务不同模块操作不同数据库需要保证数据一致性

理论落地实现技术-Atomikos#

Atomikos是基于XA规范的Java实现版本JTA进行开发的一项技术,这项技术也比较成熟了。针对mysql数据库Atomikos只有在InnoDB存储引擎下才有效,针对MyISAM就无法控制事务了,开发的时候需要注意这点

XA是强一致性事务,性能不佳,互联网项目中中的应该不多,因此Atomikos更适合在一个单体应用中对多个数据源进行分布式事务

使用Atomikos开发,必须将不同数据源的mapper进行分包管理,也即不同数据源的Dao层放在不同的包下,这样mybatis的mapperScan扫描不同包下就可以获得正确的连接,建议将mapper的xml映射,实体类也进行分包管理

Atomikos和Spring整合#

1.引入依赖#

Copy
<dependency> <groupId>javax.transaction</groupId> <artifactId>jta</artifactId> <version>1.1</version> </dependency> <dependency> <groupId>com.atomikos</groupId> <artifactId>atomikos-util</artifactId> <version>4.0.6</version> </dependency> <dependency> <groupId>com.atomikos</groupId> <artifactId>transactions</artifactId> <version>4.0.6</version> </dependency> <dependency> <groupId>com.atomikos</groupId> <artifactId>transactions-api</artifactId> <version>4.0.6</version> </dependency> <dependency> <groupId>com.atomikos</groupId> <artifactId>transactions-jdbc</artifactId> <version>4.0.6</version> </dependency> <dependency> <groupId>com.atomikos</groupId> <artifactId>transactions-jta</artifactId> <version>4.0.6</version> </dependency>

2.多个数据源连接信息配置#

Copy
# 数据源1 jdbc.driverClass=com.mysql.jdbc.Driver jdbc.jdbcUrl=jdbc:mysql:///ssm_crud?useSSl=false&characterEncoding=utf-8 jdbc.user=root jdbc.password=root # 数据源2 jdbc.shop.driverClass=com.mysql.jdbc.Driver jdbc.shop.jdbcUrl=jdbc:mysql:///shop?useSSl=false&characterEncoding=utf-8 jdbc.shop.user=root jdbc.shop.password=root

3.在spring的application.properties文件中配置如下内容#

  • 配置dataSource,这里需要使用AtomikosDataSourceBean
Copy
<!-- 引入jdbc.properties文件 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!--这里配置一个抽象的bean 将数据源一些共有配置抽取出来,不是必须这样配置,只是为了抽出共有配置--> <bean id="abstractXADataSource" class="com.atomikos.jdbc.AtomikosDataSourceBean" init-method="init" destroy-method="close" abstract="true"> <property name="xaDataSourceClassName" value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"/> <property name="maxPoolSize" value="30"/> <property name="minPoolSize" value="10"/> <property name="borrowConnectionTimeout" value="60"/> <property name="reapTimeout" value="20"/> <property name="maxIdleTime" value="60"/> <property name="maintenanceInterval" value="60"/> </bean> <!--数据库1基本配置--> <bean id="dataSourceOne" parent="abstractXADataSource"> <property name="uniqueResourceName" value="dataSourceOne"/> <!--数据库驱动--> <property name="xaProperties"> <props> <prop key="url">${jdbc.jdbcUrl}</prop> <prop key="user">${jdbc.user}</prop> <prop key="password">${jdbc.password}</prop> </props> </property> </bean> <!--数据库2基本配置--> <bean id="dataSourceTwo" parent="abstractXADataSource"> <property name="uniqueResourceName" value="dataSourceTwo"/> <!--数据库驱动--> <property name="xaProperties"> <props> <prop key="url">${jdbc.shop.jdbcUrl}</prop> <prop key="user">${jdbc.shop.user}</prop> <prop key="password">${jdbc.shop.password}</prop> </props> </property> </bean>
  • 配置sqlSessionFactory (mapperLocations,typeAliasesPackage也可以配置不同的路径)
Copy
<!--有几个数据源就需要配置几个sqlSessionFactory--> <bean id="sqlSessionFactoryOne" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation" value="classpath:mybatis/sqlMapConfig.xml"></property> <property name="dataSource" ref="dataSourceOne"></property> <!-- 指定mybatis,mapper文件的位置 --> <property name="mapperLocations" value="classpath:mapper/*.xml"></property> <!-- 配置实体类别名 --> <property name="typeAliasesPackage" value="cn.lynu.model"></property> </bean> <bean id="sqlSessionFactoryTwo" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation" value="classpath:mybatis/sqlMapConfig.xml"></property> <property name="dataSource" ref="dataSourceTwo"></property> <!-- 指定mybatis,mapper文件的位置 --> <property name="mapperLocations" value="classpath:mapper/*.xml"></property> <!-- 配置实体类别名 --> <property name="typeAliasesPackage" value="cn.lynu.model"></property> </bean>
  • 配置mapper扫描器(注意这里basePackage扫描的路径)
Copy
<!--有几个数据源就需要配置几个mapper扫描器--> <bean id="mapperScannerConfigurerOne" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="cn.lynu.mapper"></property> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryOne"></property> </bean> <bean id="mapperScannerConfigurerTwo" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="cn.lynu.shopmapper"></property> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryTwo"></property> </bean>
  • 配置事务管理
Copy
<!--配置atomikos事务管理器--> <bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init" destroy-method="close"> <property name="forceShutdown" value="false"/> </bean> <!--配置atomikos事务--> <bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp"> <property name="transactionTimeout" value="300"/> </bean> <!--配置spring与jta事务整合--> <bean id="springTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManager" ref="atomikosTransactionManager"/> <property name="userTransaction" ref="atomikosUserTransaction"/> <property name="allowCustomIsolationLevels" value="true"/> </bean>
  • 配置事务切面,这里使用的是切面的方式配置事务,实际上使用注解配置也可以
Copy
<!-- 配置通知 --> <tx:advice id="txAdvice" transaction-manager="springTransactionManager"> <tx:attributes> <!-- 匹配业务类中的方法 --> <tx:method name="save*" propagation="REQUIRED"/> <tx:method name="insert*" propagation="REQUIRED"/> <tx:method name="update*" propagation="REQUIRED"/> <tx:method name="delete*" propagation="REQUIRED"/> <tx:method name="find*" propagation="SUPPORTS" read-only="true"/> <tx:method name="select*" propagation="SUPPORTS" read-only="true"/> <tx:method name="get*" propagation="SUPPORTS" read-only="true"/> </tx:attributes> </tx:advice> <!-- 配置aop --> <aop:config> <!-- 配置切点 --> <aop:pointcut expression="execution(* cn.lynu.service.*.*(..))" id="cut"/> <!-- 配置切面 --> <aop:advisor advice-ref="txAdvice" pointcut-ref="cut"/> </aop:config>

如果是注解方式就如下配置:

Copy
<tx:annotation-driven transaction-manager="springTransactionManager"/>

重点就是这里的transaction-manager

接下来就可以进行测试了:

Copy
package cn.lynu.service; import cn.lynu.mapper.EmployeeMapper; import cn.lynu.model.Employee; import cn.lynu.model.ShopProduct; import cn.lynu.shopmapper.ShopProductMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.math.BigDecimal; @Service public class AtomikosService { @Autowired private EmployeeMapper employeeMapper; @Autowired private ShopProductMapper shopProductMapper; public void saveMethod() { ShopProduct product = new ShopProduct(); product.setPname("测试1"); product.setPprice(new BigDecimal("100")); product.setStock(100); shopProductMapper.insertProduct(product); Employee employee = new Employee(); employee.setEmpName("测试1"); employee.setEmpSex("1"); employee.setEmpEmail("123@qq.com"); employee.setdId(1); employeeMapper.insertSelective(employee); int i = 1 / 0; } }

查看运行日志,出现如下内容就表示分布式事务正常回滚

Atomikos和SpringBoot整合#

这里我用的是Mybatis-Plus,Mybatis配置方法也类似,可以参考:springboot jta atomikos实现分布式事物管理

1.引入依赖#

Copy
<!--使用atomikos实现分布式事务--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jta-atomikos</artifactId> </dependency>

2.修改SpringBoot配置文件#

Copy
spring: # 数据源配置 datasource: databases1: # type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///ssm_crud?useSSl=false&characterEncoding=utf-8 username: root password: root databases2: # type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///shop?useSSl=false&characterEncoding=utf-8 username: root password: root mybatis-plus: #Mapper扫描在MyBatisPlusConfigDS.java配置类中进行了配置 #实体扫描,多个package用逗号或者分号分隔 typeAliasesPackage: com.ds.entity global-config: db-config: #主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID", ID_WORKER:"全局唯一ID (默认,数字类型唯一ID)", UUID:"全局唯一ID UUID"; id-type: AUTO #字段策略 IGNORED:"忽略判断", NOT_NULL:"非 NULL 判断"), NOT_EMPTY:"非空判断" field-strategy: NOT_NULL #是否开启大写命名,默认false不开启 #capital-mode: true configuration: #配置返回数据库(column下划线命名&&返回java实体是驼峰命名),自动匹配无需as(没开启这个,SQL需要写as: select user_id as userId) map-underscore-to-camel-case: true cache-enabled: false #配置JdbcTypeForNull, oracle数据库必须配置 jdbc-type-for-null: 'null' #打印sql日志 # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3.Mybatis-Plus配置类#

因为有两个数据源需要控制分布式事务,所以就需要写两个配置类,并且将basePackages扫描不同包下的mapper,这里也需要对不同数据源的Dao层进行分包,也建议将mapper的xml映射,实体类也进行分包管理

Copy
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; 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 // 扫描的Dao层 多个数据源对应的Mapper文件以及XML文件需要分开配置(放在不同的包下) 如果放在同一个文件夹会报错 @MapperScan(basePackages = "com.ds.mapper", sqlSessionTemplateRef = "ds1SqlSessionTemplate") public class MyBatisPlusConfigDS1 { /** * 数据源1配置信息(url password username...) */ @Bean(name = "ds1DataSourceProperties") @ConfigurationProperties(prefix = "spring.datasource.databases1") public DataSourceProperties ds1DataSourceProperties() { return new DataSourceProperties(); } /** * 配置数据源1 * @param config 数据源配置信息 * @return */ @Primary @Bean(name = "ds1DataSource") public DataSource ds1DataSource(@Qualifier("ds1DataSourceProperties") DataSourceProperties config) { // 创建Mysql实现XA规范的分布式数据源 MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource(); // 设置连接信息 mysqlXaDataSource.setUrl(config.getUrl()); mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); mysqlXaDataSource.setPassword(config.getPassword()); mysqlXaDataSource.setUser(config.getUsername()); mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); // 数据源改为Atomikos,将事务交给Atomikos统一管理 AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); xaDataSource.setXaDataSource(mysqlXaDataSource); // Druid对于XA的支持,将上面代码注释,打开以下注释 /*DruidXADataSource druidXADataSource = new DruidXADataSource(); druidXADataSource.setUrl(config.getUrl()); druidXADataSource.setPassword(config.getPassword()); druidXADataSource.setUsername(config.getUsername()); AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); xaDataSource.setXaDataSource(druidXADataSource);*/ xaDataSource.setUniqueResourceName("ds1DataSource"); // xaDataSource.setMinPoolSize(config.getMinPoolSize()); // xaDataSource.setMaxPoolSize(config.getMaxPoolSize()); // xaDataSource.setMaxLifetime(config.getMaxLifetime()); // xaDataSource.setBorrowConnectionTimeout(config.getBorrowConnectionTimeout()); // xaDataSource.setLoginTimeout(config.getLoginTimeout()); // xaDataSource.setMaintenanceInterval(config.getMaintenanceInterval()); // xaDataSource.setMaxIdleTime(config.getMaxIdleTime()); // xaDataSource.setTestQuery(config.getTestQuery()); return xaDataSource; } /** * 配置数据源1的sqlSessionFactory * 使用MybatisPlus的MybatisSqlSessionFactoryBean 替换Mybatis的SqlSessionFactoryBean * @param dataSource 数据源 * @return * @throws Exception */ @Bean(name = "ds1SqlSessionFactory") public SqlSessionFactory ds1SqlSessionFactory(@Qualifier("ds1DataSource") DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setPlugins(new Interceptor[]{ new PaginationInterceptor(), // 分页插件 new PerformanceInterceptor().setFormat(true) // SQL格式化插件 }); // 配置为扫描数据源1所有的mapper sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/com/ds/mapper/*.xml")); return sqlSessionFactoryBean.getObject(); } /** * 配置数据源1的SqlSessionTemplate * @param sqlSessionFactory 数据源1的sqlSessionFactory */ @Bean(name = "ds1SqlSessionTemplate") public SqlSessionTemplate ds1SqlSessionTemplate(@Qualifier("ds1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } }
Copy
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import javax.sql.DataSource; import java.sql.SQLException; @Configuration @MapperScan(basePackages = "com.ds.mapper2", sqlSessionTemplateRef = "ds2SqlSessionTemplate") public class MyBatisPlusConfigDS2 { @Bean(name = "ds2DataSourceProperties") @ConfigurationProperties(prefix = "spring.datasource.databases2") public DataSourceProperties ds1DataSourceProperties() { return new DataSourceProperties(); } // 配置数据源 @Bean(name = "ds2DataSource") public DataSource ds2DataSource(@Qualifier("ds2DataSourceProperties") DataSourceProperties config) throws SQLException { MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource(); mysqlXaDataSource.setUrl(config.getUrl()); mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); mysqlXaDataSource.setPassword(config.getPassword()); mysqlXaDataSource.setUser(config.getUsername()); mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); xaDataSource.setXaDataSource(mysqlXaDataSource); // Druid对于XA的支持,将上面代码注释,打开以下注释 /*DruidXADataSource druidXADataSource = new DruidXADataSource(); druidXADataSource.setUrl(config.getUrl()); druidXADataSource.setPassword(config.getPassword()); druidXADataSource.setUsername(config.getUsername()); AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); xaDataSource.setXaDataSource(druidXADataSource);*/ xaDataSource.setUniqueResourceName("ds2DataSource"); // xaDataSource.setMinPoolSize(config.getMinPoolSize()); // xaDataSource.setMaxPoolSize(config.getMaxPoolSize()); // xaDataSource.setMaxLifetime(config.getMaxLifetime()); // xaDataSource.setBorrowConnectionTimeout(config.getBorrowConnectionTimeout()); // xaDataSource.setLoginTimeout(config.getLoginTimeout()); // xaDataSource.setMaintenanceInterval(config.getMaintenanceInterval()); // xaDataSource.setMaxIdleTime(config.getMaxIdleTime()); // xaDataSource.setTestQuery(config.getTestQuery()); return xaDataSource; } @Bean(name = "ds2SqlSessionFactory") public SqlSessionFactory ds2SqlSessionFactory(@Qualifier("ds2DataSource") DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setPlugins(new Interceptor[]{ new PaginationInterceptor(), // 分页插件 new PerformanceInterceptor().setFormat(true) // SQL格式化插件 }); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/com/ds/mapper2/*.xml")); return sqlSessionFactoryBean.getObject(); } @Bean(name = "ds2SqlSessionTemplate") public SqlSessionTemplate ds2SqlSessionTemplate( @Qualifier("ds2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } }

不同数据库连接池对Atomikos的支持#

默认的MysqlXADataSource是支持XA规范的;而我们常用的数据库连接池如:HikariCP,druid,dbcp,c3p0等貌似有些还未支持,具体情况如下图:

理论落地实现技术-Seata#

Seata官网

Seata是阿里开源的分布式,比较适合在分布式微服务项目中使用,如SpringCloud,Dubbo,官方也给出一些整合的案例

Seata中有TC,TM,RM的概念解释如下:

TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。Seata提供实现好的seata-server作为TC。

TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。RM更多表示的就是每一个数据源。

TC已经由Seata提供了,我们只需要将从下载地址后配置运行即可,在项目中配置还需要我们配置TC和RM,这两者通过jar包的方式引入项目

Seata和SpringCloud整合#

1.修改seata-server配置并运行#

主要修改的是conf目录下的registry.conf,file.conf这两个文件。registry.conf只要配置注册中心和配置中心,Seata的注册中心可以是file 、nacos 、eureka、redis、zk、consul、etcd3、sofa类型,而配置中心可以是file、nacos 、apollo、zk、consul、etcd3这几种类型,如果我们将配置中心使用file类型,那么就需要修改file.conf这个文件了。这里记录的就是用nacos作为Seata注册中心,file作为配置中心的方案,如果需要将配置中心也使用nacos就需要修改conf/nacos-config.txt,修改后运行conf/nacos-config.sh脚本,更详细可以参看[文档](nacos (seata.io))

这里我们使用的是Seata 0.9.0版本的 seata-server-0.9.0

registry.conf:

Copy
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa # 这里将注册中心type改为naccos type = "nacos" # 配置正确的naocs地址 nacos { serverAddr = "localhost:8848" namespace = "" cluster = "default" } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" } zk { cluster = "default" serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } 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 # 使用file类型的配置中心 type = "file" nacos { serverAddr = "localhost" namespace = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { app.id = "seata-server" apollo.meta = "http://192.168.1.204:8801" } zk { serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } etcd3 { serverAddr = "http://localhost:2379" } # 因为使用了file类型的配置中心,所以接下来就需要修改file.conf文件 file { name = "file.conf" } }

file.conf:

这里我们使用数据库持久化配置信息,并且vgroup_mapping.xxx配置的值很关键,值的内容无所谓但是之后我们会用到,这里配置的my_tx_group并没什么特殊意义

Copy
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #vgroup->rgroup 这里的值很关键,值的内容无所谓但是之后我们会用到,这里用的my_tx_group并没什么特殊意义 vgroup_mapping.my_test_tx_group = "my_tx_group" #only support single node default.grouplist = "localhost: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" } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 tm.commit.retry.count = 1 tm.rollback.retry.count = 1 } ## transaction log store store { ## store mode: file、db # 使用数据库持久化配置信息 mode = "db" ## file store file { dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions max-branch-session-size = 16384 # globe session size , if exceeded throws exceptions max-global-session-size = 512 # file buffer size , if exceeded allocate new buffer file-write-buffer-cache-size = 16384 # when recover batch read size session.reload.read_size = 100 # async, sync flush-disk-mode = async } ## database store # 配置正确的数据库信息 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://localhost:3306/seata" user = "root" password = "root" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } } lock { ## the lock store mode: local、remote mode = "remote" local { ## store locks in user's database } remote { ## store locks in the seata's server } } recovery { #schedule committing retry period in milliseconds committing-retry-period = 1000 #schedule asyn committing retry period in milliseconds asyn-committing-retry-period = 1000 #schedule rollbacking retry period in milliseconds rollbacking-retry-period = 1000 #schedule timeout retry period in milliseconds timeout-retry-period = 1000 } transaction { undo.data.validation = true undo.log.serialization = "jackson" undo.log.save.days = 7 #schedule delete expired undo_log in milliseconds undo.log.delete.period = 86400000 undo.log.table = "undo_log" } ## metrics settings metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898 } support { ## spring spring { # auto proxy the DataSource bean datasource.autoproxy = false } }

因为需要在数据库持久化,所以就需要在数据库中提前生成表结构,建表语句在conf/db_store.sql,内容如下:

Copy
-- the table to store GlobalSession data drop table if exists `global_table`; create table `global_table` ( `xid` varchar(128) not null, `transaction_id` bigint, `status` tinyint not null, `application_id` varchar(32), `transaction_service_group` varchar(32), `transaction_name` varchar(128), `timeout` int, `begin_time` bigint, `application_data` varchar(2000), `gmt_create` datetime, `gmt_modified` datetime, primary key (`xid`), key `idx_gmt_modified_status` (`gmt_modified`, `status`), key `idx_transaction_id` (`transaction_id`) ); -- the table to store BranchSession data drop table if exists `branch_table`; create table `branch_table` ( `branch_id` bigint not null, `xid` varchar(128) not null, `transaction_id` bigint , `resource_group_id` varchar(32), `resource_id` varchar(256) , `lock_key` varchar(128) , `branch_type` varchar(8) , `status` tinyint, `client_id` varchar(64), `application_data` varchar(2000), `gmt_create` datetime, `gmt_modified` datetime, primary key (`branch_id`), key `idx_xid` (`xid`) ); -- the table to store lock data drop table if exists `lock_table`; create table `lock_table` ( `row_key` varchar(128) not null, `xid` varchar(96), `transaction_id` long , `branch_id` long, `resource_id` varchar(256) , `table_name` varchar(32) , `pk` varchar(36) , `gmt_create` datetime , `gmt_modified` datetime, primary key(`row_key`) );

当这两个配置文件修改完成之后运行bin/seata-server.sh/.bat就可以启动Seata-Server即可

2.在每一个涉及到分布式事务的项目中引入依赖#

因为spring-cloud-starter-alibaba-seata会自动引入Seata的依赖,但是自动引入的依赖版本可能不是我们需要的,就可以同故宫exclusion排除掉,这里我们用Seata 0.9.0版本

Copy
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>0.9.0</version> </dependency>

3.修改对应项目中修改SpringBoot配置文件#

我们在SpringBoot的配置文将中添加如下配置:

Copy
spring: cloud: alibaba: seata: # 这里自定义的事务组名必须与seata-Server file.conf文件中配置的vgroup_mapping.xxx值一样 tx-service-group: my_tx_group

4.将registry.conf和file.conf文件放到对应项目的resources目录下#

实际上registry.conf和file.conf文件中多余的配置项可以删除,这里就放一个删除后的版本

registry.conf:

Copy
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { serverAddr = "localhost:8848" namespace = "" cluster = "default" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" file { name = "file.conf" } }

file.conf:这个file文件配置的vgroup_mapping.xxx就不与Seata Serve中的配置不同了,xxx必须是Seata Serve file.conf中配置的值,也就是my_tx_group

Copy
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #vgroup->rgroup vgroup_mapping.my_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" } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 tm.commit.retry.count = 1 tm.rollback.retry.count = 1 } ## transaction log store store { ## store mode: file、db mode = "db" ## database store 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://localhost:3306/seata" user = "root" password = "root" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } } lock { ## the lock store mode: local、remote mode = "remote" remote { ## store locks in the seata's server } } recovery { #schedule committing retry period in milliseconds committing-retry-period = 1000 #schedule asyn committing retry period in milliseconds asyn-committing-retry-period = 1000 #schedule rollbacking retry period in milliseconds rollbacking-retry-period = 1000 #schedule timeout retry period in milliseconds timeout-retry-period = 1000 } transaction { undo.data.validation = true undo.log.serialization = "jackson" undo.log.save.days = 7 #schedule delete expired undo_log in milliseconds undo.log.delete.period = 86400000 undo.log.table = "undo_log" } ## metrics settings metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898 } support { ## spring spring { # auto proxy the DataSource bean datasource.autoproxy = false } }

5.在对应项目中配置Seata提供的代理数据源#

Copy
package com.lynu.config; import com.alibaba.druid.pool.DruidDataSource; import io.seata.rm.datasource.DataSourceProxy; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; /** * 使用seata配置数据源DataSourceProxy以及mybatis的SqlSessionFactory * 需要在启动类排除调用spring默认的mybatis的自动配置DataSourceAutoConfiguration */ @Configuration public class DataSourceProxyConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() { return new DruidDataSource(); } @Primary @Bean("dataSource") public DataSourceProxy dataSourceProxy(DataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } @Bean(name = "sqlSessionFactory") public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSourceProxy); SqlSessionFactory factory; try { factory = bean.getObject(); } catch (Exception e) { throw new RuntimeException(e); } return factory; } }

因为使用的Mybatis操作数据库,所以配置的就是SqlSessionFactory,其他的持久层框架与之类似,接下来需要我们将SpringBoot自带的数据源自动配置排除掉,因为我们需要使用自己的配置,在SpringBoot启动类上:

Copy
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

6.在每一个涉及到分布式事务的数据库中添加undo_log表#

undo_log表结构在conf/db_undo_log.sql

Copy
drop table `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;

接下来我们在需要开启分布式事务的Service方法上添加@GlobalTransactional注解即可

理论落地实现技术-TX-LCN#

TX-LCN官网文档

TX-LCN比较适合在分布式微服务项目中使用,如SpringCloud,Dubbo

TX-LCN 主要有两个模块,Tx-Client(TC) Tx-Manager(TM). TC作为微服务下的依赖,TM是独立的服务,这里的TM,TC与Seata中的概念有些相反

版本说明:该框架作者正在开发6.0版本,而本次记录的是以5.0.2版本为准,TM下载地址

TX-LCN和SpringCloud整合#

1.启动TM服务#

TX-LCN的TM需要依赖MySQL+Redis,所以在下载目录txlcn-tm/src/main/resources/tx-manager.sql有建库建表语句

Copy
CREATE DATABASE IF NOT EXISTS `tx-manager` DEFAULT CHARSET utf8 COLLATE utf8_general_ci; USE `tx-manager`; SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; DROP TABLE IF EXISTS `t_tx_exception`; CREATE TABLE `t_tx_exception` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `group_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `unit_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `mod_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `transaction_state` tinyint(4) NULL DEFAULT NULL, `registrar` tinyint(4) NULL DEFAULT NULL, `ex_state` tinyint(4) NULL DEFAULT NULL COMMENT '0 待处理 1已处理', `remark` varchar(10240) NULL DEFAULT NULL COMMENT '备注', `create_time` datetime(0) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 967 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;

修改配置信息(txlcn-tm/src/main/resources/application.properties)

Copy
spring.application.name=TransactionManager server.port=7970 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tx-manager?characterEncoding=UTF-8&useSSL=false spring.datasource.username=root spring.datasource.password=root spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect spring.jpa.hibernate.ddl-auto=update spring.redis.host=127.0.0.1 spring.redis.port=6379

修改pom.xml文件配置的plugins,注释掉配置的插件,添加SpringBoot-Maven插件

Copy
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> <addResources>true</addResources> </configuration> </plugin> </plugins> </build>

修改完这些文件之后就可以通过Maven构建了

Copy
mvn clean package -Dmaven.test.skip=true java -jar target/txlcn-tm-5.0.2.RELEASE.jar

2.在每一个涉及到分布式事务的项目中引入依赖#

Copy
<dependency> <groupId>com.codingapi.txlcn</groupId> <artifactId>txlcn-tc</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>com.codingapi.txlcn</groupId> <artifactId>txlcn-txmsg-netty</artifactId> <version>5.0.2.RELEASE</version> </dependency>

3.在对应项目开启分布式事务注解#

在每一个涉及到分布式事务的微服务主类上使用@EnableDistributedTransaction

Copy
@SpringBootApplication @EnableDistributedTransaction public class DemoAApplication { public static void main(String[] args) { SpringApplication.run(DemoDubboClientApplication.class, args); } }

接下来再在每一个涉及到分布式事务的业务方法配置@LcnTransaction,注意这里不同于Seata,只要涉及到分布式事务的每一个微服务中的业务方法上都需要添加@LcnTransaction注解。添加注解之后分布式事务就生效了

TX-LCN的默认TM的本机默认端口:127.0.0.1:8070,如果需要修改就在SpringBoot配置文件中修改:

Copy
tx-lcn: client: manager-address: 127.0.0.1:8070
posted @   OverZeal  阅读(340)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?

目录

目录

X
点击右上角即可分享
微信分享提示