在微服务架构盛行的今天,数据一致性是每位架构师和开发者必须面对的挑战。当单体应用被拆分为多个独立部署的服务,且每个服务拥有独立的数据库时,传统的本地事务(@Transactional)便无法保证跨服务操作的数据一致性。
本教程将带你深入理解分布式事务的核心原理,并手把手教你使用阿里巴巴开源的 Seata 框架,在 Spring Boot/Cloud 环境中实现高可用的分布式事务解决方案。
一、 核心概念与理论基石
在编写代码之前,我们需要明确为什么需要分布式事务,以及它背后的理论支撑。
1. 为什么需要分布式事务?
想象一个经典的电商下单场景:
- 订单服务:创建订单记录。
- 库存服务:扣减商品库存。
- 账户服务:扣减用户余额。
在单体架构中,这三个操作在同一个数据库中,通过本地事务即可保证"要么全成功,要么全失败"。但在微服务架构下,这三个服务分别连接三个不同的数据库。如果订单创建成功,但库存扣减失败,就会导致"有订单无库存"的严重数据不一致问题。分布式事务就是为了解决这种跨进程、跨数据库的数据一致性问题而生的。
2. CAP 与 BASE 理论
分布式系统无法同时满足 CAP 定理中的三点(一致性、可用性、分区容错性)。由于分区容错性(P)是分布式环境的必然属性,我们通常需要在一致性(C)和可用性(A)之间做权衡。
这催生了 BASE 理论,它是目前大多数互联网分布式事务方案的基础:
- Basically Available(基本可用):系统允许在故障时损失部分可用性。
- Soft state(软状态):允许系统存在中间状态,不影响系统整体可用性。
- Eventually consistent(最终一致性):系统中的所有数据副本经过一段时间的同步后,最终将达到一个一致的状态。
结论 :大多数分布式事务方案不再追求强一致性(实时一致),而是转向最终一致性。
二、 主流方案选型
目前企业应用中,Seata 的 AT 模式和基于消息队列(MQ)的最终一致性方案最为流行。以下是主流方案的对比:
| 方案 | 原理 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| XA / 2PC | 两阶段提交,阻塞式 | 强一致 | 低 | 中 | 银行、金融等对一致性要求极高的场景 |
| TCC | Try-Confirm-Cancel,应用层补偿 | 强一致 | 中 | 高 | 核心交易、高并发,且需要精细控制资源的场景 |
| 可靠消息 | 本地事务+MQ | 最终一致 | 高 | 中 | 异步解耦、对实时性要求不高的场景(如注册送积分) |
| Seata AT | 自动补偿(基于Undo Log) | 强一致 | 中 | 低 | 希望无侵入改造的现有项目(首选推荐) |
本教程将重点讲解Seata AT 模式,因为它对业务代码几乎无侵入,且易于上手。
三、 Seata 核心架构解析
Seata 的设计非常优雅,主要由三个核心角色组成,理解它们是掌握 Seata 的关键:
-
TC (Transaction Coordinator) - 事务协调者
- 角色:独立部署的中间件服务(Seata Server)。
- 职责:它是分布式事务的"大脑",负责接收 TM 的请求,协调所有 RM 的执行状态,并最终决定是提交还是回滚全局事务。
-
TM (Transaction Manager) - 事务管理器
- 角色:全局事务的发起者(通常是业务入口服务,如订单服务)。
- 职责:向 TC 申请开启全局事务,获取全局事务ID(XID),并将 XID 传递给下游服务。最后向 TC 发起全局提交或回滚请求。
-
RM (Resource Manager) - 资源管理器
- 角色:分支事务的执行者(参与事务的每个微服务)。
- 职责:管理本地数据库资源,执行本地事务,并向 TC 注册分支事务。它接收 TC 的指令来提交或回滚本地事务。
工作流程简述 :
TM 发起全局事务 -> TC 生成 XID -> TM 将 XID 传递给 RM -> RM 执行本地事务并注册 -> TC 根据结果通知所有 RM 提交或回滚。
四、 实战:Spring Boot 集成 Seata AT 模式
我们将通过一个"订单服务调用库存服务"的案例,演示如何落地分布式事务。
技术栈:Spring Boot 2.7.x + Spring Cloud Alibaba + Seata 1.6.x + Nacos + MySQL
步骤 1:环境准备与 Seata Server 部署
首先,你需要启动 Seata Server(TC)。为了简化操作,推荐使用 Docker 快速部署:
bash
# 拉取 Seata 镜像
docker pull seataio/seata-server:latest
# 启动 Seata 容器,映射端口 8091
docker run -d --name seata-server \
-p 8091:8091 \
-e SEATA_PORT=8091 \
seataio/seata-server:latest
步骤 2:数据库配置(关键)
Seata 的 AT 模式依赖于数据库中的 undo_log 表来记录数据快照,以便回滚。所有参与分布式事务的微服务数据库(如订单库、库存库)都必须创建这张表:
sql
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL COMMENT '分支事务ID',
`xid` varchar(100) NOT NULL COMMENT '全局事务ID',
`context` varchar(128) NOT NULL COMMENT '上下文信息',
`rollback_info` longblob NOT NULL COMMENT '回滚信息',
`log_status` int NOT NULL COMMENT '日志状态:0-未提交,1-已提交',
`log_created` datetime NOT NULL COMMENT '创建时间',
`log_modified` datetime NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Seata 回滚日志表';
步骤 3:引入依赖
在你的 pom.xml 中引入 Seata 的 Starter 依赖。注意,如果你使用的是 Spring Cloud Alibaba,建议引入对应的 starter 以获得更好的集成体验。
xml
<!-- Seata Spring Boot Starter -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2021.0.4.0</version> <!-- 版本号需与你的 Spring Cloud 版本对应 -->
</dependency>
步骤 4:配置文件
在 application.yml 中配置 Seata 的连接信息,指向你的 Nacos 和 Seata Server。
yaml
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_test_tx_group # 事务组名称,需与 Seata Server 配置一致
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
步骤 5:代码实现
这是最关键的一步。Seata 的强大之处在于其无侵入性 。你只需要在全局事务的入口方法上添加 @GlobalTransactional 注解。
假设 OrderService 是事务发起方:
java
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryFeignClient inventoryFeignClient;
/**
* 创建订单并扣减库存
* 使用 @GlobalTransactional 开启分布式事务
*/
@GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
public void createOrder(OrderDTO orderDTO) {
log.info("开始创建订单,xid: {}", RootContext.getXID());
// 1. 本地事务:创建订单
Order order = new Order();
BeanUtils.copyProperties(orderDTO, order);
orderMapper.insert(order);
// 2. 远程调用:扣减库存 (Feign 会自动传递 XID)
// 如果这里发生异常,或者库存扣减失败,整个事务将回滚
inventoryFeignClient.deduct(orderDTO.getProductId(), orderDTO.getQuantity());
log.info("订单创建成功");
}
}
步骤 6:Feign 客户端配置
为了让下游服务(库存服务)也能感知到全局事务,必须将 XID 通过 HTTP 请求头传递过去。Spring Cloud Alibaba Seata 已经内置了 SeataFeignClient,或者你可以配置一个 RequestInterceptor 来自动注入 XID。
java
@FeignClient(name = "inventory-service")
public interface InventoryFeignClient {
@PostMapping("/inventory/deduct")
void deduct(@RequestParam("productId") Long productId, @RequestParam("quantity") Integer quantity);
}
五、 原理深挖:AT 模式是如何工作的?
Seata AT 模式之所以能做到无侵入,是因为它代理了数据源。
-
一阶段(Prepare):
- 业务 SQL 执行前,Seata 拦截 SQL,解析出修改前的数据(Before Image)和修改后的数据(After Image)。
- 将 Before Image 序列化为
rollback_info存入undo_log表。 - 立即提交本地事务,并释放本地锁(这是 Seata 优于传统 2PC 的地方,极大提高了并发性能)。
- 向 TC 注册分支事务。
-
二阶段(Commit/Rollback):
- 提交 :如果所有分支都成功,TC 异步通知 RM 删除
undo_log,过程非常快。 - 回滚 :如果有分支失败,TC 通知 RM 回滚。RM 读取
undo_log中的 Before Image,生成反向 SQL(如UPDATE变回原值)并执行,恢复数据。
- 提交 :如果所有分支都成功,TC 异步通知 RM 删除
六、 避坑指南与最佳实践
在实际生产环境中,使用 Seata 需要注意以下几点:
- 隔离级别问题:AT 模式在"一阶段"就提交了本地事务,这意味着在事务未完全结束前,数据修改对其他事务是可见的(读未提交)。虽然 Seata 通过"全局锁"来防止写写冲突,但在极高并发下仍需注意脏读风险。
- 多数据源支持 :如果你的一个服务需要操作多个数据源(例如订单库和日志库),Seata 同样支持,只需配置多个
DataSourceProxy。 - 超时设置:分布式事务涉及网络调用,务必合理设置 TM 的全局事务超时时间,避免因网络抖动导致事务悬挂。
- 异常处理 :
@GlobalTransactional默认只在遇到Exception时回滚。如果你的业务抛出的是Error或其他非受检异常,请确保配置rollbackFor = Throwable.class。
通过本教程,你已经掌握了使用 Seata 解决分布式事务的基本能力。在实际项目中,建议优先使用 AT 模式处理一般业务,对于核心资金类业务,可结合 TCC 模式或人工对账兜底,构建高可靠的微服务系统。