在微服务架构中,一个业务操作往往涉及多个服务的数据库修改(如下单→扣库存→减余额)。分布式事务的核心是保证这些跨服务操作的原子性:要么全部成功,要么全部失败。Seata 作为阿里开源的分布式事务框架,提供了简单易用的解决方案。本文将从部署到完整业务代码,带你掌握基于 Seata 的分布式事务实现。
一、核心概念与原理
1. Seata 核心角色
- TC(Transaction Coordinator):事务协调者(独立服务),负责协调全局事务的提交或回滚。
- TM(Transaction Manager):事务管理器(在发起全局事务的服务中),负责开启和结束全局事务。
- RM(Resource Manager):资源管理器(在参与事务的每个服务中),负责分支事务的提交/回滚。
2. 事务模式
本文重点讲解两种常用模式:
- AT 模式:无侵入式,基于 undo log 实现自动回滚(推荐生产使用,性能好)。
- XA 模式:基于数据库原生 XA 协议,强一致性但性能较低(适合对一致性要求极高的场景)。
二、部署 Seata TC 服务
TC 是 Seata 的核心协调服务,必须先部署。
1. 下载 Seata 服务器
从 Seata 官网 下载 1.6.1 版本,解压后目录结构如下:
seata-server-1.6.1/
├── bin/ # 启动脚本
├── conf/ # 配置文件
├── lib/ # 依赖包2. 配置 TC 服务
修改 conf/application.yml,配置注册中心(Nacos)和存储模式:
server:
port: 8091 # TC 服务端口
spring:
application:
name: seata-server
# 注册中心配置(使用 Nacos)
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848 # Nacos 地址
group: SEATA_GROUP # 分组(默认即可)
application: seata-server # TC 在 Nacos 中的服务名
# 配置中心(从 Nacos 读取配置)
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
dataId: seataServer.properties # 配置文件名
# 存储模式(file 适合测试,生产用 db)
store:
mode: file3. 在 Nacos 中添加 Seata 配置
登录 Nacos 控制台(http://localhost:8848,账号密码 nacos/nacos),创建配置:
- Data ID:
seataServer.properties - Group:
SEATA_GROUP 配置内容:
# 事务组映射(微服务需与之一致) service.vgroupMapping.my_test_tx_group=default # TC 服务地址(由注册中心自动发现) service.default.grouplist=127.0.0.1:8091 metrics.enabled=false # 关闭监控,减少干扰
4. 启动 TC 服务
- Windows:双击
bin/seata-server.bat - Linux/Mac:执行
sh bin/seata-server.sh
启动成功后,在 Nacos 服务列表中能看到 seata-server,说明 TC 部署完成。
三、环境准备与数据库设计
1. 微服务架构
我们将实现一个经典的分布式事务场景:
- 订单服务(order-service):创建订单
- 库存服务(storage-service):扣减商品库存
- 账户服务(account-service):扣减用户余额
业务流程:用户下单 → 订单服务创建订单 → 调用库存服务扣库存 → 调用账户服务扣余额 → 全部成功则提交,任一失败则回滚。
2. 数据库设计
为三个服务分别创建数据库,并添加 undo_log 表(AT 模式必需)。
(1)订单库(order_db)
-- 订单表
CREATE TABLE `order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '用户ID',
`product_id` bigint NOT NULL COMMENT '商品ID',
`count` int NOT NULL COMMENT '购买数量',
`money` decimal(10,2) NOT NULL COMMENT '总金额',
`status` int NOT NULL COMMENT '订单状态:0-创建中,1-已完成',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- AT 模式回滚日志表
CREATE TABLE `undo_log` (
`id` bigint NOT NULL 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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;(2)库存库(storage_db)
-- 库存表
CREATE TABLE `storage` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_id` bigint NOT NULL COMMENT '商品ID',
`total` int NOT NULL COMMENT '总库存',
`used` int NOT NULL COMMENT '已使用库存',
`residue` int NOT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 初始化数据(商品1库存100)
INSERT INTO `storage` VALUES (1, 1, 100, 0, 100);
-- 回滚日志表(同上,复制 order_db 的 undo_log 表结构)(3)账户库(account_db)
-- 账户表
CREATE TABLE `account` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '用户ID',
`total` decimal(10,2) NOT NULL COMMENT '总余额',
`used` decimal(10,2) NOT NULL COMMENT '已使用余额',
`residue` decimal(10,2) NOT NULL COMMENT '剩余余额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 初始化数据(用户1余额1000)
INSERT INTO `account` VALUES (1, 1, 1000, 0, 1000);
-- 回滚日志表(同上)四、微服务集成 Seata
1. 统一依赖(所有服务)
在 pom.xml 中添加依赖:
<dependencies>
<!-- Spring Boot 基础 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Seata 核心依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
<!-- Nacos 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- OpenFeign(服务间调用) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- MyBatis(数据库操作) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>2. 订单服务(order-service)
(1)配置文件(application.yml)
server:
port: 8081
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos 地址
alibaba:
seata:
tx-service-group: my_test_tx_group # 事务组名(与 TC 一致)
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/order_db?useSSL=false
username: root
password: 123456 # 替换为你的数据库密码
# MyBatis 配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.order.entity
# Seata 配置
seata:
registry:
type: nacos
nacos:
server-addr: localhost:8848
group: SEATA_GROUP
tx-service-group: my_test_tx_group # 与上面一致(2)实体类(Order.java)
package com.example.order.entity;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; // 0-创建中,1-已完成
}(3)Mapper 接口与 XML
// OrderMapper.java
package com.example.order.mapper;
import com.example.order.entity.Order;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderMapper {
// 创建订单
void create(Order order);
// 更新订单状态
void updateStatus(@Param("id") Long id, @Param("status") Integer status);
}<!-- resources/mapper/OrderMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.order.mapper.OrderMapper">
<insert id="create">
INSERT INTO `order` (user_id, product_id, count, money, status)
VALUES (#{userId}, #{productId}, #{count}, #{money}, 0)
</insert>
<update id="updateStatus">
UPDATE `order` SET status = #{status} WHERE id = #{id}
</update>
</mapper>(4)Feign 客户端(调用其他服务)
// StorageFeignClient.java
package com.example.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "storage-service") // 目标服务名
public interface StorageFeignClient {
// 扣减库存
@PostMapping("/storage/decrease")
String decrease(
@RequestParam("productId") Long productId,
@RequestParam("count") Integer count
);
}
// AccountFeignClient.java
package com.example.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
@FeignClient(name = "account-service")
public interface AccountFeignClient {
// 扣减余额
@PostMapping("/account/decrease")
String decrease(
@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money
);
}(5)Service 层(核心业务逻辑)
package com.example.order.service;
import com.alibaba.fastjson.JSONObject;
import com.example.order.entity.Order;
import com.example.order.feign.AccountFeignClient;
import com.example.order.feign.StorageFeignClient;
import com.example.order.mapper.OrderMapper;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StorageFeignClient storageFeignClient;
@Autowired
private AccountFeignClient accountFeignClient;
/**
* 创建订单(全局事务入口)
* @GlobalTransactional:标记为全局事务,name 为事务名(唯一)
*/
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public String createOrder(Order order) {
log.info("全局事务ID:{}", RootContext.getXID()); // 打印全局事务ID
// 1. 创建订单(本地事务)
log.info("开始创建订单...");
orderMapper.create(order);
// 2. 扣减库存(调用库存服务)
log.info("开始扣减库存...");
String storageResult = storageFeignClient.decrease(order.getProductId(), order.getCount());
if (!"扣减库存成功".equals(storageResult)) {
throw new RuntimeException("扣减库存失败:" + storageResult);
}
// 3. 扣减余额(调用账户服务)
log.info("开始扣减余额...");
String accountResult = accountFeignClient.decrease(order.getUserId(), order.getMoney());
if (!"扣减余额成功".equals(accountResult)) {
throw new RuntimeException("扣减余额失败:" + accountResult);
}
// 4. 更新订单状态为“已完成”
log.info("更新订单状态...");
orderMapper.updateStatus(order.getId(), 1);
// 模拟异常(测试回滚时打开注释)
// int i = 1 / 0;
return "订单创建成功,ID:" + order.getId();
}
}(6)Controller 层
package com.example.order.controller;
import com.example.order.entity.Order;
import com.example.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/order/create")
public String createOrder(@RequestBody Order order) {
return orderService.createOrder(order);
}
}(7)启动类
package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients // 开启 Feign 客户端
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}3. 库存服务(storage-service)
(1)配置文件(application.yml)
server:
port: 8082
spring:
application:
name: storage-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
alibaba:
seata:
tx-service-group: my_test_tx_group
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/storage_db?useSSL=false
username: root
password: 123456
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.storage.entity
seata:
registry:
type: nacos
nacos:
server-addr: localhost:8848
group: SEATA_GROUP
tx-service-group: my_test_tx_group(2)实体类(Storage.java)
package com.example.storage.entity;
import lombok.Data;
@Data
public class Storage {
private Long id;
private Long productId;
private Integer total;
private Integer used;
private Integer residue;
}(3)Mapper 接口与 XML
// StorageMapper.java
package com.example.storage.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface StorageMapper {
// 扣减库存
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}<!-- resources/mapper/StorageMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.storage.mapper.StorageMapper">
<update id="decrease">
UPDATE storage
SET used = used + #{count}, residue = residue - #{count}
WHERE product_id = #{productId}
</update>
</mapper>(4)Service 层
package com.example.storage.service;
import com.example.storage.mapper.StorageMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class StorageService {
@Autowired
private StorageMapper storageMapper;
// 扣减库存(本地事务)
@Transactional
public void decrease(Long productId, Integer count) {
log.info("扣减库存:productId={}, count={}", productId, count);
storageMapper.decrease(productId, count);
}
}(5)Controller 层
package com.example.storage.controller;
import com.example.storage.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StorageController {
@Autowired
private StorageService storageService;
@PostMapping("/storage/decrease")
public String decrease(
@RequestParam("productId") Long productId,
@RequestParam("count") Integer count
) {
storageService.decrease(productId, count);
return "扣减库存成功";
}
}(6)启动类
package com.example.storage;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StorageServiceApplication {
public static void main(String[] args) {
SpringApplication.run(StorageServiceApplication.class, args);
}
}4. 账户服务(account-service)
(1)配置文件(application.yml)
server:
port: 8083
spring:
application:
name: account-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
alibaba:
seata:
tx-service-group: my_test_tx_group
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/account_db?useSSL=false
username: root
password: 123456
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.account.entity
seata:
registry:
type: nacos
nacos:
server-addr: localhost:8848
group: SEATA_GROUP
tx-service-group: my_test_tx_group(2)实体类(Account.java)
package com.example.account.entity;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class Account {
private Long id;
private Long userId;
private BigDecimal total;
private BigDecimal used;
private BigDecimal residue;
}(3)Mapper 接口与 XML
// AccountMapper.java
package com.example.account.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
@Mapper
public interface AccountMapper {
// 扣减余额
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}<!-- resources/mapper/AccountMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.account.mapper.AccountMapper">
<update id="decrease">
UPDATE account
SET used = used + #{money}, residue = residue - #{money}
WHERE user_id = #{userId}
</update>
</mapper>(4)Service 层
package com.example.account.service;
import com.example.account.mapper.AccountMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
@Slf4j
public class AccountService {
@Autowired
private AccountMapper accountMapper;
// 扣减余额(本地事务)
@Transactional
public void decrease(Long userId, BigDecimal money) {
log.info("扣减余额:userId={}, money={}", userId, money);
accountMapper.decrease(userId, money);
}
}(5)Controller 层
package com.example.account.controller;
import com.example.account.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@RestController
public class AccountController {
@Autowired
private AccountService accountService;
@PostMapping("/account/decrease")
public String decrease(
@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money
) {
accountService.decrease(userId, money);
return "扣减余额成功";
}
}(6)启动类
package com.example.account;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AccountServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AccountServiceApplication.class, args);
}
}五、AT 模式实战
AT 模式是 Seata 的默认模式,无需额外配置,直接使用即可。
1. 测试正常流程
- 启动 Nacos、Seata TC 服务。
- 启动订单服务、库存服务、账户服务。
发送请求创建订单:
curl -X POST http://localhost:8081/order/create \ -H "Content-Type: application/json" \ -d '{"userId":1,"productId":1,"count":10,"money":100.00}'检查数据库:
- 订单表新增记录,状态为 1(已完成)。
- 库存表:used=10,residue=90。
- 账户表:used=100,residue=900。
→ 所有操作成功提交。
2. 测试回滚流程
- 在订单服务的
createOrder方法中打开异常注释:int i = 1 / 0;。 - 再次发送请求,会触发异常。
检查数据库:
- 订单表无新增记录(或状态仍为 0,但最终会回滚)。
- 库存和账户数据未变化。
→ 所有操作成功回滚。
六、XA 模式实战
XA 模式基于数据库原生 XA 协议,需修改配置开启。
1. 切换为 XA 模式
在三个服务的 application.yml 中添加:
seata:
data-source-proxy-mode: XA # 默认为 AT,改为 XA2. 测试 XA 模式
重复 AT 模式的测试步骤,结果一致。区别在于:
- XA 模式下,事务执行过程中会锁定数据(如扣库存时其他事务无法修改该记录)。
- 性能略低于 AT 模式(因资源锁定时间更长)。
七、AT 与 XA 模式对比
| 特性 | AT 模式 | XA 模式 |
|---|---|---|
| 侵入性 | 无(自动生成 undo log) | 无(依赖数据库原生支持) |
| 性能 | 高(异步提交,无锁等待) | 低(锁定资源直到全局提交) |
| 一致性 | 最终一致性 | 强一致性 |
| 适用场景 | 大多数微服务场景 | 金融核心交易等强一致性场景 |
八、常见问题解决
事务不回滚?
- 检查
@GlobalTransactional是否添加在订单服务的createOrder方法上。 - 确保所有服务的
tx-service-group与 TC 配置一致(my_test_tx_group)。 - 检查 undo_log 表是否创建(AT 模式必需)。
- 检查
服务间调用失败?
- 检查 Nacos 服务列表,确认三个服务和 Seata 均已注册。
- 检查 Feign 客户端的服务名是否正确(
storage-service、account-service)。
XA 模式下数据库死锁?
- 尽量缩短事务执行时间,减少资源锁定时间。
- 避免多个事务同时修改同一批数据。
九、总结
本文通过完整的代码示例,讲解了基于 Seata 的分布式事务方案:
- 部署 Seata TC 服务作为事务协调者。
- 实现三个微服务(订单、库存、账户),通过 Feign 实现服务间调用。
- 在订单服务中用
@GlobalTransactional标记全局事务入口。 - 分别测试了 AT 模式(默认,高性能)和 XA 模式(强一致性)。
通过 Seata,我们可以在微服务架构中轻松保证跨服务操作的原子性,无需手动编写复杂的回滚逻辑。实际项目中,建议优先选择 AT 模式,仅在强一致性场景下使用 XA 模式。