本文主要是介绍springboot整合shardingsphere-jdbc+seata AT,实现分库分表+分布式事务的功能
准备
1.Nacos
docker安装latest版的nacos并启动
参考启动命令:
docker run -d --name nacos -p 8848:8848 -p 9848:9848 -p 9849:9849 \
-e JVM_XMS=256m -e JVM_XMX=256m -e MODE=standalone \
-e SPRING_DATASOURCE_PLATFORM=mysql -e MYSQL_SERVICE_HOST=127.0.0.1 \
-e MYSQL_SERVICE_PORT=3306 -e MYSQL_SERVICE_DB_NAME=nacos_config \
-e MYSQL_SERVICE_USER=root -e MYSQL_SERVICE_PASSWORD=root \
-v /usr/local/app/nacos/logs:/etc/nacos/logs \
-v /usr/local/app/nacos/init.d/custom.properties:/etc/nacos/init.d/custom.properties \
-e MYSQL_SERVICE_DB_PARAM="characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC" \
nacos/nacos-server
nacos_config的数据库sql不记得哪里搞的了,自己搜一搜
2.Seata server
docker方式启动
docker pull seataio/seata-server
## 运行容器
docker run --name seata-server -p 8091:8091 -d seataio/seata-server:latest
## 将容器中的配置拷贝到/usr/local/seata
docker cp seata-server:/seata-server /usr/local/seata
## 完成后就会在/usr/local/seata出现容器的配置,我们现在可以将原来容器停止并删除
docker stop seata-server
docker rm seata-server
##修改application.yml然后重启
docker run -d --name seata-server -p 8091:8091 -e SEATA_IP=外网ip -v /usr/local/seata:/seata-server seataio/seata-server
3.mysql
docker启动四个mysql8,两两主从同步
依赖
<?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>
<packaging>pom</packaging>
<groupId>com.sharding</groupId>
<artifactId>shardingsphere-jdbc-parent</artifactId>
<name>shardingsphere-jdbc-parent</name>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>1.8</java.version>
<common-dependency.version>1.0</common-dependency.version>
<spring-boot-maven-plugin.version>2.2.6.RELEASE</spring-boot-maven-plugin.version>
<maven-resources-plugin.version>3.1.0</maven-resources-plugin.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.boot.version>2.6.3</spring.boot.version>
<spring-cloud.version>2021.0.4</spring-cloud.version>
<spring.cloud.alibaba.version>2021.1</spring.cloud.alibaba.version>
<maven-javadoc-plugin.version>3.0.0</maven-javadoc-plugin.version>
<maven-source-plugin.version>3.1.0</maven-source-plugin.version>
</properties>
<modules>
<module>payment-service</module>
<module>storage-service</module>
<module>common</module>
</modules>
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.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>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
<exclusions>
<exclusion>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
</exclusion>
<exclusion>
<groupId>org.apiguardian</groupId>
<artifactId>apiguardian-api</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-typehandlers-jsr310</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.2</version>
<exclusions>
<exclusion>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-transaction-core</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>de.javakaffee</groupId>
<artifactId>kryo-serializers</artifactId>
<version>0.42</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.15</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>com.sharding</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>9.7.0</version>
</dependency>
</dependencies>
<build>
<filters>
<filter>../src/main/filters/config-${profileActive}.properties</filter>
</filters>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<!-- resources资源插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>${maven-resources-plugin.version}</version>
<configuration>
<delimiters>
<delimiter>@</delimiter>
</delimiters>
<useDefaultDelimiters>false</useDefaultDelimiters>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
<profiles>
<!--开发环境-->
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<profileActive>dev</profileActive>
</properties>
</profile>
<!--测试环境-->
<profile>
<id>test</id>
<properties>
<profileActive>test</profileActive>
</properties>
</profile>
<!--生产环境-->
<profile>
<id>prod</id>
<properties>
<profileActive>prod</profileActive>
</properties>
</profile>
</profiles>
</project>
核心依赖
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.2</version>
<exclusions>
<exclusion>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-transaction-core</artifactId>
<version>5.2.0</version>
</dependency>
配置
1.seata的nacos配置
在nacos上新建namespace为seata,然后使用github上seata的script下的config-center里的config,如下
#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#Transport configuration, for client and server
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none
#Transaction routing rules configuration, only for the client
service.vgroupMapping.sharding_tx_group=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
#Transaction rule configuration, only for the client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h
tcc.contextJsonParserType=fastjson
log.exceptionRate=100
store.mode=db
store.lock.mode=db
store.session.mode=db
store.publicKey=123456
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimeZone=Asia/Shanghai
store.db.user=userroot
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
server.xaerNotaRetryTimeout=60000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=false
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
导入sh在seata github源码里
项目配置
例如payment-service.yml
feign:
okhttp:
enable: true
client:
config:
#这里填具体的服务名称(也可以填default,表示对所有服务生效)
default:
#connectTimeout和readTimeout这两个得一起配置才会生效
connectTimeout: 600000
readTimeout: 600000
algorithm:
class-name: com.shard.springbootshardingjdbc.readwrite.algorithm.ConsistentShardingAlgorithm
server:
port: 10099
spring:
shardingsphere:
props:
sql-show: true
datasource:
names: ds-0-master,ds-0-follow,ds-1-master,ds-1-follow
ds-0-master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${db1.ip}:3306/${db1.schema}?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=UTF-8&zeroDateTimeBehavior=CONVERT_TO_NULL
username: ${db1.username}
password: ${db1.passwd}
ds-0-follow:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${db1.ip}:3307/${db1.schema}?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=UTF-8&zeroDateTimeBehavior=CONVERT_TO_NULL
username: ${db1.username}
password: ${db1.passwd}
ds-1-master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${db2.ip}:3306/${db2.schema}?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=UTF-8&zeroDateTimeBehavior=CONVERT_TO_NULL
username: ${db2.username}
password: ${db2.passwd}
ds-1-follow:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${db2.ip}:3307/${db2.schema}?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=UTF-8&zeroDateTimeBehavior=CONVERT_TO_NULL
username: ${db2.username}
password: ${db2.passwd}
rules:
transaction:
defaultType: BASE
providerType: Seata
readwrite-splitting:
load-balancers:
ms:
type: ROUND_ROBIN
props:
workId: 1
data-sources:
ds0:
staticStrategy:
writeDataSourceName: ds-0-master
readDataSourceNames: ds-0-follow
load-balancer-name: ms
ds1:
staticStrategy:
writeDataSourceName: ds-1-master
readDataSourceNames: ds-1-follow
load-balancer-name: ms
sharding:
key-generators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1
sharding-algorithms:
db-standard:
type: CLASS_BASED
props:
strategy: STANDARD
algorithmClassName: ${algorithm.class-name}
table-standard:
type: CLASS_BASED
props:
strategy: STANDARD
algorithmClassName: ${algorithm.class-name}
bindingTables:
- tb_pay,tb_pay_detail
# 表策略配置
tables:
tb_pay:
actualDataNodes: ds$->{0..1}.tb_pay$->{0..4}
databaseStrategy:
standard:
sharding-column: id
sharding-algorithm-name: db-standard
tableStrategy:
standard:
shardingColumn: id
sharding-algorithm-name: table-standard
tb_pay_detail:
actualDataNodes: ds$->{0..1}.tb_pay_detail$->{0..4}
databaseStrategy:
standard:
sharding-column: payment_id
sharding-algorithm-name: db-standard
tableStrategy:
standard:
shardingColumn: payment_id
sharding-algorithm-name: table-standard
algorithm.class-name为自定义分片算法实现类,具体代码参考末尾地址
seata.conf和registry.conf
每个子项目resources下新建以下两个文件
seata.conf
client {
## 应用唯一id
application.id=payment-service
## 所属事务组
transaction.service.group=sharding_tx_group
}
sharding {
transaction.seata.tx.timeout=6000
}
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = "seata"
cluster = "default"
username = "seata"
password = "seata"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "seata"
group = "SEATA_GROUP"
username = "seata"
password = "seata"
}
}
代码改造
-
原本整合seata,按照官网介绍只要加@Transactional和@@ShardingSphereTransactionType(value = TransactionType.BASE)即可
但实际测试过程中发现,子系统无法加入全局事务,或者子系统完全新建全局事务
这些一部分原因是pom依赖问题,首先seata-all我观察的是不会引入feign拦截器设置xid的,而seata-spring-boot-starter会自动注册拦截器,但是在整合过程中会遇到各种问题,故使用seata-all的依赖 -
由于未引入茫茫多人推荐的shardingsphere-transaction-base-seata-at:5.2.0的依赖,
原因是该包里的SeataTransactionHolder为package,外部无法调用其方法,故不引入此依赖,改为引入shardingsphere-transaction-core:5.2.0即可,然后项目java目录下新建org/apache/shardingsphere/transaction/base/seata/at文件夹,放入以下三个类
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.shardingsphere.transaction.base.seata.at;
import com.google.common.base.Preconditions;
import io.seata.config.FileConfiguration;
import io.seata.core.context.RootContext;
import io.seata.core.exception.TransactionException;
import io.seata.core.rpc.netty.RmNettyRemotingClient;
import io.seata.core.rpc.netty.TmNettyRemotingClient;
import io.seata.rm.RMClient;
import io.seata.rm.datasource.DataSourceProxy;
import io.seata.tm.TMClient;
import io.seata.tm.api.GlobalTransaction;
import io.seata.tm.api.GlobalTransactionContext;
import lombok.SneakyThrows;
import org.apache.shardingsphere.infra.database.type.DatabaseType;
import org.apache.shardingsphere.transaction.core.ResourceDataSource;
import org.apache.shardingsphere.transaction.core.TransactionType;
import org.apache.shardingsphere.transaction.spi.ShardingSphereTransactionManager;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* Seata AT transaction manager.
*/
public final class SeataATShardingSphereTransactionManager implements ShardingSphereTransactionManager {
private final Map<String, DataSource> dataSourceMap = new HashMap<>();
private final String applicationId;
private final String transactionServiceGroup;
private final boolean enableSeataAT;
private final int globalTXTimeout;
public SeataATShardingSphereTransactionManager() {
FileConfiguration config = new FileConfiguration("seata.conf");
enableSeataAT = config.getBoolean("sharding.transaction.seata.at.enable", true);
applicationId = config.getConfig("client.application.id");
transactionServiceGroup = config.getConfig("client.transaction.service.group", "default");
globalTXTimeout = config.getInt("sharding.transaction.seata.tx.timeout", 60);
}
@Override
public void init(final DatabaseType databaseType, final Collection<ResourceDataSource> resourceDataSources, final String providerType) {
if (enableSeataAT) {
initSeataRPCClient();
resourceDataSources.forEach(each -> dataSourceMap.put(each.getOriginalName(), new DataSourceProxy(each.getDataSource())));
}
}
private void initSeataRPCClient() {
Preconditions.checkNotNull(applicationId, "please config application id within seata.conf file.");
TMClient.init(applicationId, transactionServiceGroup);
RMClient.init(applicationId, transactionServiceGroup);
}
@Override
public TransactionType getTransactionType() {
return TransactionType.BASE;
}
@Override
public boolean isInTransaction() {
Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
return null != RootContext.getXID();
}
@Override
public Connection getConnection(final String databaseName, final String dataSourceName) throws SQLException {
Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
return dataSourceMap.get(databaseName + "." + dataSourceName).getConnection();
}
@Override
public void begin() {
begin(globalTXTimeout);
}
@Override
@SneakyThrows(TransactionException.class)
public void begin(final int timeout) {
if (timeout < 0) {
throw new TransactionException("timeout should more than 0s");
}
Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
GlobalTransaction globalTransaction = GlobalTransactionContext.getCurrentOrCreate();
globalTransaction.begin(timeout * 1000);
SeataTransactionHolder.set(globalTransaction);
}
@Override
@SneakyThrows(TransactionException.class)
public void commit(final boolean rollbackOnly) {
Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
try {
SeataTransactionHolder.get().commit();
} finally {
SeataTransactionHolder.clear();
RootContext.unbind();
}
}
@Override
@SneakyThrows(TransactionException.class)
public void rollback() {
Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
try {
SeataTransactionHolder.get().rollback();
} finally {
SeataTransactionHolder.clear();
RootContext.unbind();
}
}
@Override
public void close() {
dataSourceMap.clear();
SeataTransactionHolder.clear();
RmNettyRemotingClient.getInstance().destroy();
TmNettyRemotingClient.getInstance().destroy();
}
}
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.shardingsphere.transaction.base.seata.at;
import io.seata.tm.api.GlobalTransaction;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* Seata transaction holder.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SeataTransactionHolder {
private static final ThreadLocal<GlobalTransaction> CONTEXT = new ThreadLocal<>();
/**
* Set seata global transaction.
*
* @param transaction global transaction context
*/
public static void set(final GlobalTransaction transaction) {
CONTEXT.set(transaction);
}
/**
* Get seata global transaction.
*
* @return global transaction
*/
public static GlobalTransaction get() {
return CONTEXT.get();
}
/**
* Clear global transaction.
*/
public static void clear() {
CONTEXT.remove();
}
}
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.shardingsphere.transaction.base.seata.at;
import io.seata.core.context.RootContext;
import org.apache.shardingsphere.infra.database.metadata.DataSourceMetaData;
import org.apache.shardingsphere.infra.executor.kernel.model.ExecutorDataMap;
import org.apache.shardingsphere.infra.executor.sql.hook.SQLExecutionHook;
import java.util.List;
import java.util.Map;
/**
* Seata transactional SQL execution hook.
*/
public final class TransactionalSQLExecutionHook implements SQLExecutionHook {
private static final String SEATA_TX_XID = "SEATA_TX_XID";
private boolean seataBranch;
@Override
public void start(final String dataSourceName, final String sql, final List<Object> parameters,
final DataSourceMetaData dataSourceMetaData, final boolean isTrunkThread, final Map<String, Object> shardingExecuteDataMap) {
if (isTrunkThread) {
if (RootContext.inGlobalTransaction()) {
ExecutorDataMap.getValue().put(SEATA_TX_XID, RootContext.getXID());
}
} else if (!RootContext.inGlobalTransaction() && shardingExecuteDataMap.containsKey(SEATA_TX_XID)) {
RootContext.bind((String) shardingExecuteDataMap.get(SEATA_TX_XID));
seataBranch = true;
}
}
@Override
public void finishSuccess() {
if (seataBranch) {
RootContext.unbind();
}
}
@Override
public void finishFailure(final Exception cause) {
if (seataBranch) {
RootContext.unbind();
}
}
}
由于shardingsphere是通过SPI方式加载的该类,故在子系统的resources下新建META-INF/services目录,新建两个文件
名为org.apache.shardingsphere.infra.executor.sql.hook.SQLExecutionHook
值为org.apache.shardingsphere.transaction.base.seata.at.TransactionalSQLExecutionHook
名为org.apache.shardingsphere.transaction.spi.ShardingSphereTransactionManager
值为org.apache.shardingsphere.transaction.base.seata.at.SeataATShardingSphereTransactionManager
- 方法注解
在入口方法上加
@Transactional(rollbackFor = Exception.class)
@ShardingSphereTransactionType(value = TransactionType.BASE)
被feign调用的方法也需要加上相同的注解
其中ShardingSphereTransactionType的注解作用就是设置当前事务类型为BASE,这样shardingConnection里的transactionmanager就会放入SeataATShardingSphereTransactionManager,这一步会使本地事务转为分布式事务,并与seata通信,回滚全靠它了
总结
- 为什么不引入shardingsphere-transaction-base-seata-at:5.2.0
因为在引入feignInterceptor之后将xid传递到子系统,在
@Override
public void setAutoCommit(final boolean autoCommit) throws SQLException {
this.autoCommit = autoCommit;
if (connectionManager.getConnectionTransaction().isLocalTransaction()) {
processLocalTransaction();
} else {
processDistributeTransaction();
}
}
processDistributeTransaction
private void processDistributeTransaction() throws SQLException {
switch (connectionManager.getConnectionTransaction().getDistributedTransactionOperationType(autoCommit)) {
case BEGIN:
connectionManager.close();
connectionManager.getConnectionTransaction().begin();
getConnectionContext().getTransactionConnectionContext().setInTransaction(true);
break;
case COMMIT:
connectionManager.getConnectionTransaction().commit();
break;
default:
break;
}
}
getDistributedTransactionOperationType
public DistributedTransactionOperationType getDistributedTransactionOperationType(final boolean autoCommit) {
if (!autoCommit && !transactionManager.isInTransaction()) {
return DistributedTransactionOperationType.BEGIN;
}
if (autoCommit && transactionManager.isInTransaction()) {
return DistributedTransactionOperationType.COMMIT;
}
return DistributedTransactionOperationType.IGNORE;
}
-------------------------------
public boolean isInTransaction() {
Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
return null != RootContext.getXID();
}
如果它发现RootContext里的xid不为空,则返回DistributedTransactionOperationType.IGNORE,
这就会导致无法进到begin代码里,无法将已存在的GlobalTransaction放入当前线程
详见
public void begin(final int timeout) {
if (timeout < 0) {
throw new TransactionException("timeout should more than 0s");
}
Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
GlobalTransaction globalTransaction = GlobalTransactionContext.getCurrentOrCreate();
globalTransaction.begin(timeout * 1000);
SeataTransactionHolder.set(globalTransaction);
}
奇怪的是GlobalTransactionContext.getCurrentOrCreate();里又判断了xid不等于null,则new了一个DefaultGlobalTransaction(xid, GlobalStatus.Begin, GlobalTransactionRole.Participant);加入进去。个人认为前面那个isInTransaction()判断是有点问题的,感觉可用SeataTransactionHolder.get() == null来替换
回到上面,由于DistributedTransactionOperationType为ignore,但是shardingsphereConnect里的transactionmanager又为SeataATShardingSphereTransactionManager,当起事务提交是会调用
public void commit(final boolean rollbackOnly) {
Preconditions.checkState(enableSeataAT, "sharding seata-at transaction has been disabled.");
try {
SeataTransactionHolder.get().commit();
} finally {
SeataTransactionHolder.clear();
RootContext.unbind();
}
}
由于当前线程中没有GlobalTransaction对象的,所以这里会报空指针,那么如果子系统上面不加@ShardingSphereTransactionType(value = TransactionType.BASE)呢,那么恭喜你,报错是不报错了,但是子系统的事务只是LOCAL事务,不会加入全局事务,上面有解释
截止目前,shardingsphere-transaction-base-seata-at:5.3.1中的SeataTransactionHolder仍然是package,isInTransaction判断仍然是当前xid不为null
既然知道为什么会空指针后,那么就可以针对的修改,这里的修改参考了issues/22356的推荐代码。排除原依赖,加入修改后的java文件,放在相同package路径下即可
但是,由于硬件或软件配置问题,以下结果不一定准确,仅是表达一下观点
在接入seata后,以savePayment为里,A系统插入payment+detail,并循环调用2次B系统的reduce storage count接口
不加全局事务时执行大概300-400ms,性能尚可,加了全局事务后,测试最快1.6s,最慢5s+,平均2s以上