Apache ShardingSphere provides BASE transactions that integrate the Seata implementation. All references to Seata integration in this article refer to Seata AT mode.
ShardingSphere’s Seata integration is only available in apache/incubator-seata:v2.1.0
or higher.
For Seata Client corresponding to the org.apache.seata:seata-all
Maven module, this limitation applies to both HotSpot VM and GraalVM Native Image.
Introduce Maven dependencies and exclude the outdated Maven dependency of org.antlr:antlr4-runtime:4.8
in org.apache.seata:seata-all
.
<project>
<dependencies>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc</artifactId>
<version>${shardingsphere.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-transaction-base-seata-at</artifactId>
<version>${shardingsphere.version}</version>
</dependency>
<dependency>
<groupId>org.apache.seata</groupId>
<artifactId>seata-all</artifactId>
<version>2.1.0</version>
<exclusions>
<exclusion>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
Affected by Calcite,
commons-lang:commons-lang
and org.apache.commons:commons-pool2
used by ShardingSphere JDBC have dependency conflicts with Seata Client.
Users need to consider whether to resolve dependency conflicts based on actual scenarios.
When using ShardingSphere’s Seata integration module,
the database instance connected to ShardingSphere should implement both ShardingSphere’s dialect parsing support and Seata AT mode’s dialect parsing support.
Such databases include but are not limited to mysql
, gvenzl/oracle-free
, gvenzl/oracle-xe
, postgres
, mcr.microsoft.com/mssql/server
and other Docker Images.
Follow the steps in one of the links below to download and start Seata Server.
The proper way to start Seata Server is to instantiate it through the Docker Image of apache/seata-server
in Docker Hub.
Create the undo_log
table in each real database instance involved in ShardingSphere.
The SQL content is based on the corresponding database in https://github.com/apache/incubator-seata/tree/v2.1.0/script/client/at/db .
The following content takes MySQL as an example.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) 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 KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
Write the following content in the YAML configuration file of ShardingSphere of your own project, refer to Distributed Transaction.
If Java API is used when initializing ShardingSphere JDBC DataSource, refer to Distributed Transaction.
transaction:
defaultType: BASE
providerType: Seata
Add the seata.conf
file to the root directory of the classpath.
The configuration file format refers to the JavaDoc of org.apache.seata.config.FileConfiguration
.
There are four properties in seata.conf
,
shardingsphere.transaction.seata.at.enable
, when this value is true
, enable ShardingSphere’s Seata AT integration. The default value is true
shardingsphere.transaction.seata.tx.timeout
, global transaction timeout (seconds). The default value is 60
client.application.id
, application unique primary key, used to set applicationId
of Seata Transaction Manager Client and Seata Resource Manager Clientclient.transaction.service.group
, transaction group, used to set transactionServiceGroup
of Seata Transaction Manager Client and Seata Resource Manager Client.
The default value is default
A fully configured seata.conf
is as follows,
shardingsphere.transaction.seata.at.enable = true
shardingsphere.transaction.seata.tx.timeout = 60
client {
application.id = example
transaction.service.group = default_tx_group
}
A minimally configured seata.conf
is as follows.
In seata.conf
managed by ShardingSphere, the default value of client.transaction.service.group
is set to default
for historical reasons.
Assuming that in the registry.conf
of Seata Server and Seata Client used by the user, registry.type
and config.type
are both file
,
then for the .conf
file configured by config.file.name
of registry.conf
,
the default transaction group name is default_tx_group
in apache/incubator-seata:v1.5.1
and later,
and my_test_tx_group
before apache/incubator-seata:v1.5.1
.
client.application.id = example
Modify the registry.conf
file of Seata as required.
ShardingSphere’s Seata integration does not support isolation levels.
ShardingSphere’s Seata integration places the obtained Seata global transaction into the thread’s local variables.
And org.apache.seata.spring.annotation.GlobalTransactionScanner
uses Dynamic Proxy to enhance the method.
This means that when using ShardingSphere’s Seata integration, users should avoid using the Java API of org.apache.seata:seata-all
,
unless the user is mixing ShardingSphere’s Seata integration with the TCC mode feature of Seata Client.
For ShardingSphere data source, discuss 6 situations,
Manually obtain the java.sql.Connection
instance created from the ShardingSphere data source,
and manually calling the setAutoCommit()
, commit()
and rollback()
methods is allowed.
Using the Jakarta EE 8 javax.transaction.Transactional
annotation on the function is allowed.
Using Jakarta EE 9/10’s jakarta.transaction.Transactional
annotation on functions is allowed.
Using Spring Framework’s org.springframework.transaction.annotation.Transactional
annotation on functions is allowed.
Using the org.apache.seata.spring.annotation.GlobalTransactional
annotation on the function is not allowed.
Manually create org.apache.seata.tm.api.GlobalTransaction
instance from org.apache.seata.tm.api.GlobalTransactionContext
,
calling the begin()
, commit()
and rollback()
methods of an org.apache.seata.tm.api.GlobalTransaction
instance is not allowed.
In actual scenarios where Spring Boot is used,
com.alibaba.cloud:spring-cloud-starter-alibaba-seata
and org.apache.seata:seata-spring-boot-starter
are often transitively imported by other Maven dependencies.
To avoid transaction conflicts, users need to set the property seata.enable-auto-data-source-proxy
to false
in the Spring Boot configuration file.
A possible dependency relationship is as follows.
<project>
<dependencies>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc</artifactId>
<version>${shardingsphere.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-transaction-base-seata-at</artifactId>
<version>${shardingsphere.version}</version>
</dependency>
<dependency>
<groupId>org.apache.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.1.0</version>
<exclusions>
<exclusion>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
The corresponding application.yml
under classpath needs to contain the following configuration.
In this case, the equivalent configuration of Seata’s registry.conf
defined in Spring Boot’s application.yaml
is still valid.
When downstream projects use the Maven module of org.apache.shardingsphere:shardingsphere-transaction-base-seata-at
,
it is always encouraged to use registry.conf
to configure Seata Client.
seata:
enable-auto-data-source-proxy: false
For the case of setting up ShardingSphere’s Seata integration,
In business functions unrelated to ShardingSphere JDBC DataSource, if you need to use Seata Client’s Seata TCC mode-related features in business functions,
you can instantiate a non-proxy ordinary TCC interface implementation class, and then use org.apache.integration.tx.api.util.ProxyUtil
to create a proxy TCC interface class,
and call the functions corresponding to the three stages of the TCC interface implementation class Try
, Confirm
, and Cancel
.
For the org.apache.seata.spring.annotation.GlobalTransactional
annotation introduced by the Seata TCC mode or the business functions involved in the Seata TCC mode that need to interact with the database instance,
ShardingSphere JDBC DataSource should not be used in the business functions marked by this annotation. Instead,
a javax.sql.DataSource
instance should be created manually or obtained from a custom Spring Bean.
Transactional propagationn in cross-service call scenarios is not as out-of-the-box as transaction operations within a single microservice.
For Seata Server, transactional propagation in cross-service call scenarios requires passing XID to the service provider through service calls and binding it to org.apache.seata.core.context.RootContext
.
Refer to https://seata.apache.org/docs/user/api/ . This requires discussing two situations,
In the scenario of using ShardingSphere JDBC,
transaction scenarios across multiple microservices need to consider using org.apache.seata.core.context.RootContext.getXID()
to obtain Seata XID in the context of the starting microservice,
and passing it to the end microservice through HTTP or RPC, and processing it in the Filter or Spring WebMVC HandlerInterceptor of the end microservice.
Spring WebMVC HandlerInterceptor is only applicable to Spring Boot microservices and is invalid for Quarkus, Micronaut Framework and Helidon.
In the scenario of using ShardingSphere Proxy, multiple microservices operate local transactions against the logical data source of ShardingSphere Proxy. This will be converted into distributed transaction operations on the server side of ShardingSphere Proxy, without considering additional Seata XID.
Introduce a simple scenario to continue discussing the transactional propagation across service calls in the scenario of using ShardingSphere JDBC.
a-mysql
, all databases have created UNDO_LOG
table and business table.b-mysql
, all databases have created UNDO_LOG
table and business table.a-seata-server
using file
as configuration center and registration center.a-service
. This microservice creates a ShardingSphere JDBC DataSource that only configures the database instance a-mysql
.
This ShardingSphere JDBC DataSource configuration uses the Seata AT integration connected to the Seata Server instance a-seata-server
,
whose Seata Application Id is service-a
, whose Seata transaction group is default_tx_group
,
and the Seata Transaction Coordinator cluster group pointed to by its Virtual Group Mapping
is default
.
This microservice instance a-service
exposes a single Restful API GET endpoint as /hello
,
and the business function aMethod
of this Restful API endpoint uses a common local transaction annotation.
If this microservice is based on Spring Boot 2,import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@Transactional
@GetMapping("/hello")
public String aMethod() {
// ... Perform an UPDATE operation on the database instance `a-mysql`
return "Hello World!";
}
}
b-service
. This microservice creates a ShardingSphere JDBC DataSource that only configures the database instance b-mysql
.
This ShardingSphere JDBC DataSource configuration uses the Seata AT integration connected to the Seata Server instance a-seata-server
,
whose Seata Application Id is service-b
, whose Seata transaction group is default_tx_group
,
and whose Virtual Group Mapping
points to the Seata Transaction Coordinator cluster group as default
.
The business function bMethod
of this microservice instance b-service
uses a normal local transaction annotation,
and calls the /hello
Restful API endpoint of the microservice instance a-service
through the HTTP Client in bMethod
.
If this microservice is based on Spring Boot 2,import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
@Service
public class DemoService {
@Transactional
public void bMethod() {
RestTemplate restTemplate = new RestTemplateBuilder().build();
restTemplate.getForEntity("http://a-service/hello", String.class);
// ... Perform an UPDATE operation on the database instance `b-mysql`
}
}
For this simple scenario, there is a single Seata Server Cluster, which contains a single Virtual Group
as default
.
This Virtual Group
contains a single Seata Server instance as a-seata-server
.
Discuss transaction propagation for single service calls. When the business function aMethod
of the microservice instance a-service
throws an exception,
the changes to the MySQL database instance a-mysql
in the business function will be rolled back normally.
Discuss transaction propagation for cross-service calls. When the business function bMethod
of the microservice instance b-service
throws an exception,
the changes to the MySQL database instance b-mysql
in the business function will be rolled back normally,
and the org.apache.seata.core.context.RootContext
of the microservice instance a-service
is not bound to the Seata XID of the business function bMethod
of the microservice instance b-service
,
so the changes to the MySQL database instance a-mysql
in the business function will not be rolled back.
In order to achieve that when the business function bMethod
of the microservice instance b-service
throws an exception,
the changes to the MySQL database instances a-mysql
and b-mysql
in the business function are rolled back normally,
discuss the common processing solutions in different scenarios.
a-service
and b-service
are both Spring Boot 2 microservices based on Jakarta EE 8.
Users can use org.springframework.web.client.RestTemplate
in the business function bMethod
of the microservice instance b-service
to pass the XID to the microservice instance a-service
through the service call.
The possible transformation logic is as follows.import org.apache.seata.core.context.RootContext;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
@Service
public class DemoService {
@Transactional
public void bMethod() {
RestTemplate restTemplate = new RestTemplateBuilder().additionalInterceptors((request, body, execution) -> {
String xid = RootContext.getXID();
if (null != xid) {
request.getHeaders().add(RootContext.KEY_XID, xid);
}
return execution.execute(request, body);
})
.build();
restTemplate.getForEntity("http://a-service/hello", String.class);
// ... Perform an UPDATE operation on the database instance `b-mysql`
}
}
At this time, custom org.springframework.web.servlet.config.annotation.WebMvcConfigurer
implementations need to be added to the microservice instances a-service
and b-service
.
import org.apache.seata.integration.http.TransactionPropagationInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CustomWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TransactionPropagationInterceptor());
}
}
At this time, when the business function bMethod
of the microservice instance b-service
throws an exception,
the changes to the MySQL database instances a-mysql
and b-mysql
in the business function are rolled back normally.
a-service
and b-service
are both Spring Boot 3 microservices based on Jakarta EE 9/10.
Users can use org.springframework.web.client.RestClient
in the business function bMethod
of the microservice instance b-service
to pass the XID to the microservice instance a-service
through a service call.
The possible transformation logic is as follows.import org.apache.seata.core.context.RootContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestClient;
@Service
public class DemoService {
@Transactional
public void bMethod() {
RestClient restClient = RestClient.builder().requestInterceptor((request, body, execution) -> {
String xid = RootContext.getXID();
if (null != xid) {
request.getHeaders().add(RootContext.KEY_XID, xid);
}
return execution.execute(request, body);
})
.build();
restClient.get().uri("http://a-service/hello").retrieve().body(String.class);
// ... Perform an UPDATE operation on the database instance `b-mysql`
}
}
At this time, custom org.springframework.web.servlet.config.annotation.WebMvcConfigurer
implementations need to be added to the microservice instances a-service
and b-service
.
import org.apache.seata.integration.http.JakartaTransactionPropagationInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CustomWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JakartaTransactionPropagationInterceptor());
}
}
At this time, when the business function bMethod
of the microservice instance b-service
throws an exception,
the changes to the MySQL database instances a-mysql
and b-mysql
in the business function are rolled back normally.
The microservice instances a-service
and b-service
are both Spring Boot microservices,
but the API gateway middleware used blocks all HTTP requests containing the HTTP Header of TX_XID
.
The user needs to consider changing the HTTP Header used to pass XID to the microservice instance a-service
through service calls,
or use the RPC framework to pass XID to the microservice instance a-service
through service calls.
Refer to https://github.com/apache/incubator-seata/tree/v2.1.0/integration .
The microservice instances a-service
and b-service
are both microservices such as Quarkus, Micronaut Framework and Helidon.
In this case, Spring WebMVC HandlerInterceptor cannot be used.
You can refer to the following Spring Boot 3 custom WebMvcConfigurer implementation to implement Filter.
import org.apache.seata.common.util.StringUtils;
import org.apache.seata.core.context.RootContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CustomWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
String rpcXid = request.getHeader(RootContext.KEY_XID);
String xid = RootContext.getXID();
if (StringUtils.isBlank(xid) && StringUtils.isNotBlank(rpcXid)) {
RootContext.bind(rpcXid);
}
return true;
}
@Override
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception ex) {
if (RootContext.inGlobalTransaction()) {
String rpcXid = request.getHeader(RootContext.KEY_XID);
String xid = RootContext.getXID();
if (StringUtils.isNotBlank(xid)) {
String unbindXid = RootContext.unbind();
if (!StringUtils.equalsIgnoreCase(rpcXid, unbindXid)) {
if (StringUtils.isNotBlank(unbindXid)) {
RootContext.bind(unbindXid);
}
}
}
}
}
});
}
}
a-service
and b-service
are Spring Boot microservices, but the components used are Spring WebFlux instead of Spring WebMVC.
ShardingSphere JDBC cannot handle R2DBC DataSource under the reactive programming API, only JDBC DataSource.
Avoid creating ShardingSphere JDBC DataSource in Spring Boot microservices using WebFlux components.