Integrating distributed transaction management using Seata in a Spring Cloud Alibaba microservices architecture—especially when combined with OpenFeign fallbacks—can be error-prone due to version mismatches, misconfigured registries, missing Nacos configurations, or context propagation issues. This guide provides a working setup for Seata 1.6.1 with Nacos 2.2.0 and Spring Boot 2.6.13, focusing on resolving the common problem where global transactions fail during Feign client fallbacks.
Environment Versions
- Spring Boot: 2.6.13
- Spring Cloud: 2021.0.5
- Spring Cloud Alibaba: 2021.0.5.0
- Nacos Server & Client: 2.2.0
- Seata: 1.6.1
- OpenFeign: 3.1.5
- LoadBalancer: 3.1.5
- MyBatis-Plus: 3.5.1
Nacos Server Setup
Configure nacos/conf/application.properties with MySQL support:
# Enable MySQL
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root
Initialize the databace using mysql-schema.sql from the Nacos distribution. Start in standalone mode:
startup.cmd -m standalone
Seata Server Configuration
Create the required Seata tables in your MySQL database using scripts from seata/script/server/db/.
Edit seata/conf/application.yml:
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: fbb5b0a8-7b5f-413e-80b9-4010646e6378
group: SEATA_GROUP
data-id: seataServer.properties
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
namespace: fbb5b0a8-7b5f-413e-80b9-4010646e6378
group: SEATA_GROUP
store:
mode: db
db:
url: jdbc:mysql://127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
user: root
password: root
Nacos Configuration for Seata
In the Nacos console:
- Create a namespace with ID:
fbb5b0a8-7b5f-413e-80b9-4010646e6378 - Switch to this namespace and create a configuration with Data ID:
seataServer.properties
Use the template from seata/script/config-center/config.txt. Ensure the following line is present:
service.vgroupMapping.default_tx_group=default
Also create a separate configuration with Data ID: service.vgroupMapping.default_tx_group and value: default.
Client-Side (Microservice) Configuration
In application.yml:
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: fbb5b0a8-7b5f-413e-80b9-4010646e6378
feign:
circuitbreaker:
enabled: true
seata:
tx-service-group: default_tx_group
enable-auto-data-source-proxy: true
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
application: seata-server
namespace: fbb5b0a8-7b5f-413e-80b9-4010646e6378
cluster: default
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
data-id: seataServer.properties
namespace: fbb5b0a8-7b5f-413e-80b9-4010646e6378
Required Database Table
Create the undo_log table in each participating service’s database:
CREATE TABLE undo_log (
id BIGINT AUTO_INCREMENT,
branch_id BIGINT NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
ext VARCHAR(100),
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
Fixing Seata Context Loss During Feign Fallback
When Feign fallbacks are enabled via Hystrix, the Seata XID (transaction ID) is lost because fallbacks run in separate threads. To propagate the XID:
1. Propagate XID to Feign Requests
@Component
@ConditionalOnProperty(name = "feign.circuitbreaker.enabled", havingValue = "true")
public class SeataFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String xid = RootContext.getXID();
if (xid != null) {
template.header(RootContext.KEY_XID, xid);
}
}
}
2. Preserve Request Context in Hystrix Threads
@Component
@ConditionalOnProperty(name = "feign.circuitbreaker.enabled", havingValue = "true")
public class SeataHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
String xid = RootContext.getXID();
return new Callable<T>() {
public T call() throws Exception {
try {
RequestContextHolder.setRequestAttributes(attrs);
if (xid != null) {
RootContext.bind(xid);
}
return callable.call();
} finally {
RootContext.unbind();
RequestContextHolder.resetRequestAttributes();
}
}
};
}
}
Register this strategy early in your application lifecycle (e.g., via a @PostConstruct method or static initializer).
3. Manual Transaction Control (Optional)
If automatic propagation isn't sufficient, use an aspect to manage transactions manually when XID is missing:
@Aspect
@Component
@ConditionalOnProperty(name = "feign.circuitbreaker.enabled", havingValue = "true")
public class SeataFallbackTransactionAspect {
@Before("execution(* com.example..service.*.*(..))")
public void beginTransactionIfNeeded() throws TransactionException {
if (RootContext.getXID() == null) {
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
tx.begin(300000);
}
}
@AfterThrowing(pointcut = "execution(* com.example..service.*.*(..))", throwing = "ex")
public void rollbackOnException(Throwable ex) throws TransactionException {
if (RootContext.getXID() != null) {
GlobalTransactionContext.reload(RootContext.getXID()).rollback();
}
}
@AfterReturning(pointcut = "execution(* com.example..service.*.*(..))", returning = "result")
public void commitIfSuccess(Object result) throws TransactionException {
if (result instanceof Boolean && (Boolean) result && RootContext.getXID() != null) {
GlobalTransactionContext.reload(RootContext.getXID()).commit();
}
}
}
Important Notes
- Avoid catching exceptions in business logic if you rely on automatic rollback.
- Do not use dynamic data sources (e.g.,
dynamic-datasource-spring-boot-starter) without additional Seata proxy configuration. - Ensure all services (including API Gateway) share the same Nacos namespace and group to discover eachother.
- If using Hystrix, configure timeouts via
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds, not Feign’s read timeout.
Dependency Management (Maven)
Include in parent POM:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
</dependencyManagement>
In service modules, include:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<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>