如果没有环境,可以查看,这篇文章seata1.3.0整合nacos搭建
AT模式:
一阶段:2个微服务的数据库(Resource Manager 资源管理器,下面2个服务的事务简称为tx1 和tx2),会向seata服务器(Transaction Coordinator,事务协调器)注册一个分支事务会生成一个全局锁以Xid为识别,当双方代码执行完,无异常后,开始申请第二阶段的资源提交,并且锁定当前数据库资源
二阶段:此时tx1会优先尝试获取全局锁(拿不到 全局锁 ,不能提交本地事务),当tx1开启本地事务,拿到全局锁,提交本地事务,然后tx2开始获取全局锁,提交本地事务。如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录
(这里主要查看数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较)
弊端:从第二阶段可以看到,tx1提交事务之后,tx2才能开始提交事务,这里会形成一个阻塞现象,当有多个服务同时注册到一个分支事务,提交顺序会出现:tx1提交完之后,tx2提交,然后tx3…,tx4…并且AT模式会代理你的数据库
服务统一整合到nacos配置中心和注册中心
<properties>
<java.version>1.8</java.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<spring-boot-starter.version>2.2.10.RELEASE</spring-boot-starter.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>18.12</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${
spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${
spring-boot-starter.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<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.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.0</version>
</dependency>
</dependencies>
nacos上面的seata.yml的内容:
seata:
client:
rm:
async-commit-buffer-limit: 1000
report-retry-count: 5
table-meta-check-enable: false
report-success-enable: false
saga-branch-register-enable: false
saga-json-parser: fastjson
lock:
retry-interval: 10
retry-times: 30
retry-policy-branch-rollback-on-conflict: true
tm:
commit-retry-count: 5
rollback-retry-count: 5
default-global-transaction-timeout: 60000
degrade-check: false
degrade-check-period: 2000
degrade-check-allow-times: 10
undo:
data-validation: true
log-serialization: jackson
log-table: undo_log
only-care-update-columns: true
log:
exceptionRate: 100
service:
enable-degrade: false
disable-global-transaction: false
transport:
shutdown:
wait: 3
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
worker-thread-size: default
boss-thread-size: 1
type: TCP
server: NIO
heartbeat: true
serialization: seata
compressor: none
enable-client-batch-send-request: true
#seata的配置使用file指的是使用nacos注册中心上面的seata.yml配置
registry:
type: file
load-balance: RandomLoadBalance
load-balance-virtual-nodes: 10
二个服务的bootstrap.properties
spring.profiles.active=dev
spring.cloud.nacos.config.server-addr=ip:8848
#统一使用nacos上面的seata.yml配置文件的内容
spring.cloud.nacos.config.extension-configs[0].data-id=seata.yml
spring.cloud.nacos.config.extension-configs[0].group=dev
spring.cloud.nacos.config.extension-configs[0].refresh=true
2个服务都需要执行的sql
CREATE TABLE `表名`.`undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime(6) NOT NULL COMMENT 'create datetime',
`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Dynamic;
数据库sql
CREATE TABLE `表名`.`sys_user` (
`id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`ip` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`user_alias` varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`modify_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = DYNAMIC;
server:
port: 12000
spring:
application:
name:sys-service
main:
allow-bean-definition-overriding: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://ip:3306/表名?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: 账号
password: 密码
#初始化时建立物理连接的个数
initialSize: 2
#最小连接池数量
minIdle: 2
#最大连接池数量 maxIdle已经不再使用
maxActive: 2
#获取连接时最大等待时间,单位毫秒
maxWait: 60000
#申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle: true
#既作为检测的间隔时间又作为testWhileIdel执行的依据
timeBetweenEvictionRunsMillis: 60000
#销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接
minEvictableIdleTimeMillis: 30000
#用来检测连接是否有效的sql 必须是一个查询语句
#mysql中为 select 'x'
#oracle中为 select 1 from dual
validationQuery: select 'x'
#申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
testOnBorrow: false
#归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
testOnReturn: false
#是否缓存preparedStatement,mysql5.5+建议开启
poolPreparedStatements: true
#当值大于0时poolPreparedStatements会自动修改为true
maxPoolPreparedStatementPerConnectionSize: 20
#配置扩展插件
filters: stat,wall,slf4j,config
#通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;config.decrypt=true;config.decrypt.key=公钥
#合并多个DruidDataSource的监控数据
use-global-data-source-stat: true
mybatis-plus:
mapper-locations: classpath*:/mapper/*.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.violet.sys.entity
# 日志配置
configuration:
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 属性为true来开启驼峰功能。
map-underscore-to-camel-case: true
##是否控制台 print mybatis-plus 的 LOGO
global-config:
banner: false
feign:
hystrix:
enabled: true
hystrix:
command:
default: #default全局有效
execution:
timeout:
#是否开启超时熔断
enabled: true
isolation:
thread:
timeoutInMilliseconds: 6000 #断路器超时时间,默认1000ms
HystrixCommonKey: #HystrixCommandKey:要单独设置超时时间的方法
execution:
timeout:
#是否开启超时熔断,默认true
enabled: true
isolation:
thread:
timeoutInMilliseconds: 10000 #断路器超时时间
seata:
enabled: true
application-id: {spring.application.name}
tx-service-group: sys_tx_dev
enable-auto-data-source-proxy: true #这里使用自动注册
data-source-proxy-mode: AT
use-jdk-proxy: false
excludes-for-auto-proxying: firstClassNameForExclude,secondClassNameForExclude
service:
vgroupMapping:
sys_tx_dev: dev
grouplist:
dev: 127.0.0.1:8091
注意事项这里是seata的事务分组第一要和seata.vgroupMapping相对应,grouplist是注册那个seata服务的地址ip
Controller:
package com.violet.sys.controller;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.violet.sys.entity.SysUser;
import com.violet.sys.feign.NettyFeignService;
import com.violet.sys.service.SysUserService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("seata")
@Api(tags={
"分布式事务"})
public class SeataController {
@Autowired
private SysUserService sysUserService;
@Autowired
NettyFeignService nettyFeignService;
private Logger logger = LoggerFactory.getLogger(SeataController.class);
@RequestMapping(value = "/testTransactional", method = RequestMethod.GET)
@ApiOperation(value = "测试分布式事务", notes = "接口描述", httpMethod = "GET")
@ApiOperationSupport(author = "violet")
//GlobalTransactional调用方一定要加GlobalTransactional
@GlobalTransactional
public String testTransactional() throws NullPointerException {
SysUser sysUser = new SysUser();
sysUser.setUserName("1112");
sysUserService.save(sysUser);
String success = nettyFeignService.test("success");
//故意放一个异常
int i = 1/0;
return success;
}
}
feign
@FeignClient(name = "netty-service",fallback = HystrixMessage.class)
public interface NettyFeignService {
@GetMapping("/user/test")
String test(@RequestParam("name") String name);
}
然后启动上面添加@EnableFeignClients
server:
port: 13000
spring:
application:
name:netty-service
main:
allow-bean-definition-overriding: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://ip:3306/表名?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: 账号
password: 密码
#初始化时建立物理连接的个数
initialSize: 2
#最小连接池数量
minIdle: 2
#最大连接池数量 maxIdle已经不再使用
maxActive: 2
#获取连接时最大等待时间,单位毫秒
maxWait: 60000
#申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle: true
#既作为检测的间隔时间又作为testWhileIdel执行的依据
timeBetweenEvictionRunsMillis: 60000
#销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接
minEvictableIdleTimeMillis: 30000
#用来检测连接是否有效的sql 必须是一个查询语句
#mysql中为 select 'x'
#oracle中为 select 1 from dual
validationQuery: select 'x'
#申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
testOnBorrow: false
#归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
testOnReturn: false
#是否缓存preparedStatement,mysql5.5+建议开启
poolPreparedStatements: true
#当值大于0时poolPreparedStatements会自动修改为true
maxPoolPreparedStatementPerConnectionSize: 20
#配置扩展插件
filters: stat,wall,slf4j,config
#通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;config.decrypt=true;config.decrypt.key=公钥
#合并多个DruidDataSource的监控数据
use-global-data-source-stat: true
mybatis-plus:
mapper-locations: classpath*:/mapper/*.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.violet.sys.entity
# 日志配置
configuration:
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 属性为true来开启驼峰功能。
map-underscore-to-camel-case: true
#是否控制台 print mybatis-plus 的 LOGO
global-config:
banner: false
feign:
hystrix:
enabled: true
hystrix:
command:
default: #default全局有效
execution:
timeout:
#是否开启超时熔断
enabled: true
isolation:
thread:
timeoutInMilliseconds: 6000 #断路器超时时间,默认1000ms
HystrixCommonKey: #HystrixCommandKey:要单独设置超时时间的方法
execution:
timeout:
#是否开启超时熔断,默认true
enabled: true
isolation:
thread:
timeoutInMilliseconds: 10000 #断路器超时时间
seata:
enabled: true
application-id: {spring.application.name}
tx-service-group: netty_tx_dev
enable-auto-data-source-proxy: true #这里使用自动注册
data-source-proxy-mode: AT
use-jdk-proxy: false
excludes-for-auto-proxying: firstClassNameForExclude,secondClassNameForExclude
service:
vgroupMapping:
netty_tx_dev: dev
grouplist:
dev: 127.0.0.1:8091
数据库sql
CREATE TABLE `表名`.`friend` (
`id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`userid` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户id',
`friends_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '好友id',
`comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
`createtime` date NULL DEFAULT NULL COMMENT '添加好友日期',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `UK`(`userid`, `friends_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
controller:
import com.netty.violetnetty.entity.Friend;
import com.netty.violetnetty.service.IFriendService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
*
* 前端控制器
*
*
* @author violet
* @since 2020-06-27
*/
@RestController
@RequestMapping("/user")
public class FiendController {
@Autowired
FriendService friendService;
@GetMapping("/test")
//这里也可以添加GlobalTransactional ,当该服务报错会使用本地事务回滚,如果远程方报错会使用seata管理的事务回滚
@Transactional
public String test(@RequestParam("name") String name) {
Friend friend = new Friend();
friend.setComments("name");
this.friendService.save(friend);
return name;
}
}
然后启动上面添加@EnableFeignClients
查看seata日志会发现已经注册到了seata中,并且开启了数据库代理
调用http://127.0.0.1:12000/seata/testTransactional
查看2个服务器的日志你会发现,他们的xid相同,并且都执行的回滚,数据库数据未添加,到这里AT模式就搭建完了,并且seata也会刷新日志出现相同的xid
解析: 前面说过和最后的日志查看你会发现主要是通过xid来进行回滚,开启事务,整合seata依赖后,seata会拦截feign或者restTemplate的请求将请求方的Xid放到头中,到了服务方同样会拦截请求判断头中是否存在Xid,如果存在会开启事务并且绑定Xid,可以通过 RootContext.getXID() 获得当前方的绑定的Xid