启动TC(事务协调器) 1) 下载seata服务器(版本是0.7.1) 2) 微服务相关版本说明 3) 最后直接解压seata服务器通过下面的命令启动(进入到D:\seata-server-0.7.1\bin目录下) 命令:seata-server.bat -p 8888 -m file下面我们创建了下面的代码结构 seata-bank1 ---->服务1 seata-bank2 ---->服务2环境配置 nacos1.3.1 maven3.5.2 seata0.7.1 springboot2.1.12.RELEASE springcloud alibaba2.1.0.RELEASE springcloud Greenwich.SR6seata-parent代码结构 pom.xml文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.seata</groupId> <artifactId>seata-parent</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <modules> <module>../seata-bank1</module> <module>../seata-bank2</module> <module>../seata-common</module> </modules> <!--继承一个父模块,然后再引入相应的依赖 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.12.RELEASE</version> <!--relativePath是可选的,maven会首先搜索这个地址,在搜索本地远程repositories之前 --> <relativePath /> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java-version>1.8</java-version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.SR6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>
seata-common代码结构 pom.xml内容: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>seata-parent</artifactId> <groupId>com.seata</groupId> <version>1.0-SNAPSHOT</version> <relativePath>../seata-parent/pom.xml</relativePath> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>seata-common</artifactId> <properties> <commons.lang.version>2.6</commons.lang.version> <mybatis-plus.version>3.2.0</mybatis-plus.version> <lombok.version>1.18.8</lombok.version> <mysql.version>8.0.17</mysql.version> <druid.version>1.1.21</druid.version> </properties> <dependencies> <!--引入mybatis-plus依赖--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <!--引入lombok依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>${commons.lang.version}</version> </dependency> <!--引入mysql依赖--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!--引入nacos的注册中心依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--引入nacos的配置中心依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <exclusions> <!-- 去除springboot默认的logback配置--> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- alibaba的druid数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <!-- 去除springboot默认的logback配置--> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- 加入log4j2配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> </dependencies> </project>
seata-bank1代码结构 application.yml内容: spring: application: name: seata-bank1 cloud: nacos: discovery: server-addr: localhost:8848 alibaba: seata: tx-service-group: seata-bank1_tx_group datasource: url: jdbc:mysql://xxx.xx.xxx.xx:3306/seata-bank1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: xx password: xxx driver-class-name: com.mysql.cj.jdbc.Driver initial-size: 5 #初始连接数 min-idle: 10 #最小连接池数量 max-active: 20 #最大连接池数量 max-wait: 60000 #配置获取连接等待超时的时间 time-between-eviction-runs-millis: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 min-evictable-idle-time-millis: 300000 #配置一个连接在池中最小生存的时间,单位是毫秒 max-evictable-idle-time-millis: 900000 #配置一个连接在池中最大生存的时间,单位是毫秒 validation-query: SELECT 1 FROM DUAL #配置检测连接是否有效 test-while-idle: true #建议配置为true,不影响性能,并且保证安全性申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 test-on-borrow: false #这里建议配置为TRUE,防止取到的连接不可用 test-on-return: false #归还连接时执行validationQuery检测连接是否有效 web-stat-filter: enabled: true exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' #设置不统计哪些URL stat-view-servlet: enabled: true login-username: #控制台管理用户名和密码 login-password: allow: "" #默认允许所有访问 reset-enable: false #禁止手动重置监控数据 deny: #IP黑名单(共同存在时,deny优先于allow) url-pattern: /druid/* #servlet访问路径设置 filter: stat: #慢SQL记录 slow-sql-millis: 1000 merge-sql: true log-slow-sql: true #是否展示慢查询sql enabled: true db-type: mysql filters: stat,wall,slf4j #设置使用哪些插件 stat是统计,wall是SQL防火墙,防SQL注入的,log4j是用来输出统计数据的 pool-prepared-statements: false #是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 max-pool-prepared-statement-per-connection-size: 0 server: port: 7001 servlet: context-path: /seata-bank1 mybatis-plus: type-aliases-package: com.seata.bank1 mapper-locations: classpath:/mappers/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl feign: sentinel: enabled: true #服务降级必须加上这个才生效 httpclient: connection-timeout: 60000 enabled: true client: config: default: connectTimeout: 60000 readTimeout: 60000 loggerLevel: FULL logging: config: classpath:log4j2-dev.xml level: com.seata.bank1: debug
bootstrap.properties内容:
spring.application.name=seata-bank1 spring.cloud.nacos.config.namespace=c987cc45-4012-4ee2-9757-3662e830b3cd spring.cloud.nacos.config.server-addr=localhost:8848 spring.cloud.nacos.config.ext-config[0].data-id=seata-bank1-dev.yml spring.cloud.nacos.config.ext-config[0].group=dev spring.cloud.nacos.config.ext-config[0].refresh=true把压缩包里面的2个配置文件拿到resource下面,修改配置 file.conf内容: registry.conf内容: SeataBank1Application内容:
package com.seata.bank1; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @EnableDiscoveryClient @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) //取消数据源的自动创建。使用我们自己配置的seata代理的数据源 @MapperScan(value = "com.seata.bank1.mapper") @EnableFeignClients public class SeataBank1Application { public static void main(String[] args) { SpringApplication.run(SeataBank1Application.class,args); System.out.println("===================SeataBank1Application started============================"); } }配置类DatabaseConfiguration.java内容:
package com.seata.bank1.config; import com.alibaba.druid.pool.DruidDataSource; import io.seata.rm.datasource.DataSourceProxy; 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; @Configuration public class DatabaseConfiguration { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DruidDataSource druidDataSource() { DruidDataSource druidDataSource = new DruidDataSource(); return druidDataSource; } @Primary @Bean public DataSourceProxy dataSource(DruidDataSource druidDataSource){ return new DataSourceProxy(druidDataSource); } }bank1实现如下功能: 1) 张三账户减少金额,开启全局事务。 2) 远程调用bank2向李四转账。 web层:
package com.seata.bank1.controller; import com.seata.bank1.service.AccountInfoService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Slf4j public class AccountInfoController { @Autowired private AccountInfoService accountInfoService; @RequestMapping("transferAccount") public void transferAccount(Double amount){ log.info("transferAccount amount is {}",amount); accountInfoService.tranferAmount(amount); log.info("完成转账流程。。。。。。。。。。"); } }service层:
package com.seata.bank1.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.seata.bank1.feign.Bank2Feign; import com.seata.bank1.mapper.AccountInfoMapper; import com.seata.bank1.model.AccountInfo; import com.seata.bank1.service.AccountInfoService; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @Service @Slf4j public class AccountInfoServiceImpl extends ServiceImpl<AccountInfoMapper, AccountInfo> implements AccountInfoService { @Resource private Bank2Feign bank2Feign; @Resource private AccountInfoMapper accountInfoMapper; @GlobalTransactional//开启全局事务 @Transactional//开启本地事务 @Override public void tranferAmount(Double amount) { //张三先减去金额 accountInfoMapper.updateAccountBalanceByParams(amount); //为李四增加金额 String transfer = bank2Feign.tranferAmountToBank2(amount); if("fallback".equals(transfer)){ //调用李四微服务异常 throw new RuntimeException("调用李四微服务异常"); } if(amount == 2){ //人为制造异常 throw new RuntimeException("bank1 make exception.."); } } }dao层:
package com.seata.bank1.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.seata.bank1.model.AccountInfo; import org.springframework.web.bind.annotation.RequestParam; public interface AccountInfoMapper extends BaseMapper<AccountInfo> { void updateAccountBalanceByParams(@RequestParam("amount") Double amount); }FeignClient:
package com.seata.bank1.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @FeignClient(value = "seata-bank2",fallback = Bank2FeignFallback.class) public interface Bank2Feign { @RequestMapping(value = "/seata-bank2/tranferAmountToBank2",method = RequestMethod.POST) String tranferAmountToBank2(@RequestParam("amount") Double amount); }降级服务:
package com.seata.bank1.feign; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Component @Slf4j public class Bank2FeignFallback implements Bank2Feign{ @Override public String tranferAmountToBank2(Double amount) { log.info("tranferAmountToBank2 出错,进行服务降级。。。。。"); return "fallback"; } }AccountInfoMapperxml:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.seata.bank1.mapper.AccountInfoMapper"> <update id="updateAccountBalanceByParams"> update account_info set account_balance = account_balance-#{amount} where id = '2' </update> </mapper>seata-bank2代码结构 同样把压缩包里面的2个配置文件拿到resource下面,修改配置 file.conf内容: registry.conf内容: application.yml内容: spring: application: name: seata-bank2 cloud: nacos: discovery: server-addr: localhost:8848 alibaba: seata: tx-service-group: seata-bank2_tx_group datasource: url: jdbc:mysql://xxx.xx.xxx.xx:3306/seata-bank2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: xx password: xxxx driver-class-name: com.mysql.cj.jdbc.Driver initial-size: 5 #初始连接数 min-idle: 10 #最小连接池数量 max-active: 20 #最大连接池数量 max-wait: 60000 #配置获取连接等待超时的时间 time-between-eviction-runs-millis: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 min-evictable-idle-time-millis: 300000 #配置一个连接在池中最小生存的时间,单位是毫秒 max-evictable-idle-time-millis: 900000 #配置一个连接在池中最大生存的时间,单位是毫秒 validation-query: SELECT 1 FROM DUAL #配置检测连接是否有效 test-while-idle: true #建议配置为true,不影响性能,并且保证安全性申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 test-on-borrow: false #这里建议配置为TRUE,防止取到的连接不可用 test-on-return: false #归还连接时执行validationQuery检测连接是否有效 web-stat-filter: enabled: true exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' #设置不统计哪些URL stat-view-servlet: enabled: true login-username: #控制台管理用户名和密码 login-password: allow: "" #默认允许所有访问 reset-enable: false #禁止手动重置监控数据 deny: #IP黑名单(共同存在时,deny优先于allow) url-pattern: /druid/* #servlet访问路径设置 filter: stat: #慢SQL记录 slow-sql-millis: 1000 merge-sql: true log-slow-sql: true #是否展示慢查询sql enabled: true db-type: mysql filters: stat,wall,slf4j #设置使用哪些插件 stat是统计,wall是SQL防火墙,防SQL注入的,log4j是用来输出统计数据的 pool-prepared-statements: false #是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 max-pool-prepared-statement-per-connection-size: 0 server: port: 7002 servlet: context-path: /seata-bank2 mybatis-plus: type-aliases-package: com.seata.bank2 mapper-locations: classpath:/mappers/**/*.xml
bootstrap.properties:
spring.application.name=seata-bank2 spring.cloud.nacos.config.namespace=cd1f38e6-43d4-45ac-9b19-76435345c0e5 spring.cloud.nacos.config.server-addr=localhost:8848 spring.cloud.nacos.config.ext-config[0].data-id=seata-bank2-dev.yml spring.cloud.nacos.config.ext-config[0].group=dev spring.cloud.nacos.config.ext-config[0].refresh=true配置类:
package com.seata.bank2.config; import com.alibaba.druid.pool.DruidDataSource; import io.seata.rm.datasource.DataSourceProxy; 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; @Configuration public class DatabaseConfiguration { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DruidDataSource druidDataSource(){ return new DruidDataSource(); } @Primary @Bean public DataSourceProxy dataSource(DruidDataSource druidDataSource){ return new DataSourceProxy(druidDataSource); } }启动类:
package com.seata.bank2; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) //取消数据源的自动创建。使用我们自己配置的seata代理的数据源 @MapperScan(value = "com.seata.bank2.mapper") public class SeataBank2Application { public static void main(String[] args) { SpringApplication.run(SeataBank2Application.class,args); System.out.println("====================SeataBank2Application started=========================="); } }web层:
package com.seata.bank2.controller; import com.seata.bank2.service.AccountInfo2Service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @Slf4j public class AccountInfo2Controller { @Autowired private AccountInfo2Service accountInfo2Service; @RequestMapping("tranferAmountToBank2") public String tranferAmountToBank2(@RequestParam("amount") Double amount){ log.info("tranferAmountToBank2 amount is {}",amount); accountInfo2Service.tranferAmountToBank2(amount); log.info("完成转账流程。。。。。。。。。。"); return "bank2"+amount; } }service层:
package com.seata.bank2.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.seata.bank2.mapper.AccountInfoMapper; import com.seata.bank2.model.AccountInfo; import com.seata.bank2.service.AccountInfo2Service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @Service public class AccountInfo2ServiceImpl extends ServiceImpl<AccountInfoMapper, AccountInfo> implements AccountInfo2Service { @Resource private AccountInfoMapper accountInfoMapper; @Transactional//开启本地事务 @Override public void tranferAmountToBank2(Double amount) { accountInfoMapper.tranferAmountToBank2(amount); } }dao层:
package com.seata.bank2.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.seata.bank2.model.AccountInfo; import org.springframework.web.bind.annotation.RequestParam; public interface AccountInfoMapper extends BaseMapper<AccountInfo> { void tranferAmountToBank2(@RequestParam("amount") Double amount); }AccountInfoMapper.xml内容:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.seata.bank2.mapper.AccountInfoMapper"> <update id="tranferAmountToBank2"> update account_info set account_balance = account_balance+#{amount} where id = '3' </update> </mapper>seata分布式事务的流程
要点说明:
1、每个RM使用DataSourceProxy连接数据库,其目的是使用ConnectionProxy,使用数据源和数据连接代理的目
的就是在第一阶段将undo_log和业务数据放在一个本地事务提交,这样就保存了只要有业务操作就一定有
undo_log。
2、在第一阶段undo_log中存放了数据修改前和修改后的值,为事务回滚作好准备,所以第一阶段完成就已经将分
支事务提交,也就释放了锁资源。
3、TM开启全局事务开始,将XID全局事务id放在事务上下文中,通过feign调用也将XID传入下游分支事务,每个
分支事务将自己的Branch ID分支事务ID与XID关联。
4、第二阶段全局事务提交,TC会通知各各分支参与者提交分支事务,在第一阶段就已经提交了分支事务,这里各
各参与者只需要删除undo_log即可,并且可以异步执行,第二阶段很快可以完成。
5、第二阶段全局事务回滚,TC会通知各各分支参与者回滚分支事务,通过 XID 和 Branch ID 找到相应的回滚日
志,通过回滚日志生成反向的 SQL 并执行,以完成分支事务回滚到之前的状态,如果回滚失败则会重试回滚操
作。
测试场景 原数据 1)seata-bank1通过张三用户向李四账户转1元钱,成功场景 2)seata-bank1通过张三用户向李四账户转1元钱,将seata-bank2关掉,调用李四服务的时候挂掉 可以发现事务进行了回滚,张三的钱成功回滚