0%

Seata使用

Seata 分布式事务使用指南:从集成到实战

Seata 作为分布式事务解决方案,其使用流程简洁高效,核心通过注解标记全局事务、代理数据源实现自动回滚。本文基于 Spring Cloud Alibaba 生态,详细介绍 Seata 的集成步骤与实战要点。

环境准备

在使用 Seata 前,需确保:

  1. Seata Server 已启动:作为事务协调者(TC),需提前配置并启动(参考前文 Seata Server 配置);

  2. 数据库准备:所有参与分布式事务的微服务数据库需创建undo_log表(AT 模式用于回滚日志存储),SQL 脚本如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    CREATE TABLE `undo_log` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id` bigint(20) NOT NULL,
    `xid` varchar(100) NOT NULL,
    `context` varchar(128) NOT NULL,
    `rollback_info` longblob NOT NULL,
    `log_status` int(11) NOT NULL,
    `log_created` datetime NOT NULL,
    `log_modified` datetime NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

微服务集成 Seata

1. 引入依赖

在各参与分布式事务的微服务(如订单服务、库存服务、账户服务)中添加 Seata 依赖,注意版本需与 Seata Server 一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- Spring Cloud Alibaba Seata  starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<!-- 排除默认Seata依赖,避免版本冲突 -->
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- 引入与Seata Server一致的版本(如1.1.0) -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.1.0</version>
</dependency>

2. 配置文件(application.yml)

核心配置事务分组与注册中心,确保微服务能关联到 Seata Server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
spring:
application:
name: springcloudalibaba-seata-order # 服务名
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group # 事务分组(需与Seata Server的vgroupMapping一致)
nacos:
discovery:
server-addr: localhost:8848 # 注册中心地址(与Seata Server一致)
datasource:
druid: # 本地数据库配置
username: root
password: 123456
url: jdbc:mysql://localhost:3306/seata_order # 订单库(示例)
driver-class-name: com.mysql.jdbc.Driver

# MyBatis配置(可选,根据实际ORM框架调整)
mybatis:
config-location: classpath:mybatis/mybatis.cfg.xml
type-aliases-package: com.zhanghe.study.springcloudalibaba.model
mapper-locations: classpath:mybatis/mapper/**/*.xml

关键说明

  • tx-service-group需与 Seata Server 的file.confvgroupMapping.my_test_tx_group配置一致,用于关联 TC 集群;
  • 若使用 Nacos 作为注册中心,需确保微服务与 Seata Server 的registry.conf配置一致。

3. 拷贝 Seata 配置文件

将 Seata Server 的conf目录下的file.confregistry.conf拷贝到微服务的src/main/resources目录下:

  • file.conf:配置事务分组、存储模式等(需与 Server 端保持一致);
  • registry.conf:配置注册中心类型(如 Nacos),确保微服务能发现 Seata Server。

核心配置:数据源代理

Seata 通过代理数据源实现 SQL 拦截与 undo 日志生成(AT 模式核心),需替换默认数据源为 Seata 的DataSourceProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.Resource;
import javax.sql.DataSource;
import java.io.IOException;

@Configuration
@EnableConfigurationProperties(MybatisProperties.class) // 导入MyBatis配置
public class DataSourceConfiguration {

// 1. 配置原始数据源(如Druid)
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid") // 绑定yml中的数据源配置
public DataSource dataSource() {
return new DruidDataSource();
}

// 2. 用Seata的DataSourceProxy代理数据源
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}

// 3. 配置MyBatis使用代理后的数据源
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSourceProxy dataSourceProxy,
MybatisProperties mybatisProperties) throws IOException {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy); // 设置代理数据源

// 绑定MyBatis配置(mapper路径、config位置等)
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] mapperLocations = resolver.getResources(mybatisProperties.getMapperLocations()[0]);
sqlSessionFactoryBean.setMapperLocations(mapperLocations);

if (mybatisProperties.getConfigLocation() != null) {
Resource[] configResources = resolver.getResources(mybatisProperties.getConfigLocation());
sqlSessionFactoryBean.setConfigLocation(configResources[0]);
}
return sqlSessionFactoryBean;
}
}

为什么需要代理数据源?

  • Seata 通过代理数据源拦截 SQL 执行,自动生成 undo 日志(记录数据修改前的状态);
  • 全局回滚时,通过 undo 日志恢复数据,实现无侵入式事务回滚。

启动类配置

需排除 Spring Boot 默认的数据源自动配置,避免与 Seata 代理数据源冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 排除默认数据源配置
@EnableDiscoveryClient // 启用服务发现
@EnableFeignClients // 启用Feign(用于服务间调用)
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class, args);
}
}

标记全局事务:@GlobalTransactional

在分布式事务的发起方(TM)的核心业务方法上添加@GlobalTransactional注解,标记为全局事务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

@Autowired
private OrderDao orderDao; // 订单DAO

@Autowired
private StorageClient storageClient; // Feign客户端:调用库存服务

@Autowired
private AccountClient accountClient; // Feign客户端:调用账户服务

/**
* 创建订单(全局事务入口)
*/
@GlobalTransactional(
name = "create-order", // 事务名称(唯一标识)
rollbackFor = Exception.class // 发生任何异常均回滚
)
public void create(Order order) {
// 1. 创建订单(本地事务)
System.out.println("创建订单");
orderDao.addOrder(order);

// 2. 调用库存服务:扣减库存(远程事务)
System.out.println("扣减库存");
storageClient.decrease(order.getProductId(), order.getCount());

// 3. 调用账户服务:扣减余额(远程事务)
System.out.println("扣减账户余额");
accountClient.decrease(order.getUserId(), order.getMoney());

// 4. 修改订单状态为“完成”(本地事务)
System.out.println("修改订单状态");
orderDao.updateStatus(order.getId(), 1); // 1表示成功
}
}

注解参数说明

  • name:全局事务名称(建议唯一,便于追踪);
  • rollbackFor:指定触发回滚的异常类型(默认仅回滚RuntimeException);
  • timeoutMills:全局事务超时时间(默认 60000ms)。

服务间调用:Feign 客户端

远程服务调用需通过 Feign 实现(确保 XID 全局事务 ID 在调用链路中传播):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

// 库存服务Feign客户端
@FeignClient(name = "springcloudalibaba-seata-storage") // 目标服务名
public interface StorageClient {

// 扣减库存
@PostMapping("/storage/decrease")
void decrease(@RequestParam("productId") Long productId,
@RequestParam("count") Integer count);
}

// 账户服务Feign客户端(类似)
@FeignClient(name = "springcloudalibaba-seata-account")
public interface AccountClient {
@PostMapping("/account/decrease")
void decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money);
}

XID 传播机制
Seata 通过ThreadLocal存储 XID(全局事务 ID),Feign 调用时会自动将 XID 放入请求头,远程服务接收后通过拦截器提取 XID 并绑定到当前线程,确保分支事务关联到全局事务。

测试分布式事务

  1. 正常流程:所有服务调用成功,全局事务提交,undo_log表记录被删除;
  2. 异常流程:任意远程服务抛出异常(如库存不足),全局事务回滚,所有操作(创建订单、扣减库存、扣减余额)均被撤销,undo_log表记录用于回滚恢复。

注意事项

  1. 版本一致性:微服务的seata-all版本必须与 Seata Server 版本一致,否则可能出现通信异常;
  2. 事务分组匹配tx-service-group需与 Seata Server 的vgroupMapping严格一致(大小写敏感);
  3. undo_log 表:所有参与事务的数据库必须创建undo_log表,否则 AT 模式回滚会失败;
  4. 数据源代理:必须使用DataSourceProxy代理数据源,否则 Seata 无法拦截 SQL 生成 undo 日志;
  5. 全局事务范围@GlobalTransactional仅需添加在事务发起方(TM),参与方(RM)无需额外注解。

总结

Seata 通过@GlobalTransactional注解、数据源代理和 XID 传播机制,实现了分布式事务的无侵入式管理。核心步骤包括:集成依赖、配置事务分组、代理数据源、标记全局事务

欢迎关注我的其它发布渠道