1. 为什么我们需要分布式事务
在单体应用时代,事务管理相对简单。我们使用本地事务就能保证数据一致性,比如Spring的@Transactional注解。但随着微服务架构的流行,一个业务操作往往需要跨多个服务完成,这就带来了分布式事务的挑战。
想象一下电商系统中的下单场景:订单服务创建订单、库存服务扣减库存、支付服务处理支付。这三个操作必须全部成功或全部失败,否则就会出现"订单创建成功但库存未扣减"或者"库存扣减了但支付失败"等数据不一致的情况。这就是典型的分布式事务问题。
分布式事务的核心难点在于CAP理论中的"一致性"与"可用性"的权衡。在分布式系统中,网络分区不可避免,如何在保证系统可用性的同时确保数据一致性,是分布式事务要解决的根本问题。
2. 主流分布式事务方案对比
目前业界主流的分布式事务解决方案主要有以下几种:
2.1 2PC/XA协议
两阶段提交是最经典的分布式事务协议,分为准备阶段和提交/回滚阶段。它的优点是强一致性保证,但缺点也很明显:
- 同步阻塞:参与者在准备阶段后必须等待协调者指令,期间资源被锁定
- 单点问题:协调者宕机会导致系统阻塞
- 数据不一致风险:在第二阶段协调者发送提交指令后,如果部分参与者收不到指令,会导致数据不一致
2.2 TCC模式
TCC(Try-Confirm-Cancel)是一种补偿型事务模型,将业务操作分为三个阶段:
- Try:预留业务资源
- Confirm:确认执行业务操作
- Cancel:取消业务操作,释放预留资源
TCC的优点是不锁定资源,性能较好。但缺点是实现复杂,需要为每个业务操作编写Try/Confirm/Cancel三个接口。
2.3 本地消息表
基本思路是将分布式事务拆分为多个本地事务,通过消息队列实现最终一致性。具体流程:
- 业务操作和消息写入本地数据库(同一个事务)
- 定时任务扫描消息表,将消息发送到MQ
- 消费者处理消息,完成后续操作
这种方案实现简单,但消息处理可能重复,需要保证幂等性。
2.4 SAGA模式
SAGA将长事务拆分为多个本地事务,每个本地事务都有对应的补偿操作。如果某个步骤失败,就执行前面步骤的补偿操作。SAGA适合业务流程长、参与者多的场景,但补偿逻辑的实现较为复杂。
3. Seata架构与核心概念
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案,支持AT、TCC、SAGA和XA四种模式。我们先了解它的核心架构:
3.1 Seata三大组件
Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager (TM):事务管理器,定义全局事务的范围:开始全局事务、提交或回滚全局事务。
Resource Manager (RM):资源管理器,管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
3.2 Seata的AT模式原理
AT(Auto Transaction)模式是Seata的默认模式,对业务无侵入,工作原理如下:
一阶段:
- 解析SQL,生成前置镜像(before image)和后置镜像(after image)
- 执行业务SQL
- 提交本地事务前,向TC注册分支事务
- 本地事务提交
二阶段:
- 如果全局事务成功,TC异步删除undo_log
- 如果全局事务失败,TC通知各分支根据undo_log进行补偿
3.3 Seata的事务ID设计
Seata的事务ID由三部分组成:
IP:端口@全局事务ID这种设计保证了事务ID在分布式环境中的唯一性。
4. Seata AT模式实战
下面我们通过一个订单-库存的分布式事务案例,演示如何使用Seata AT模式。
4.1 环境准备
首先需要部署Seata Server,可以使用Docker快速启动:
docker run --name seata-server \ -p 8091:8091 \ -e SEATA_IP=your_server_ip \ -e SEATA_PORT=8091 \ seataio/seata-server:1.4.24.2 项目配置
在Spring Boot项目中引入Seata依赖:
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.4.2</version> </dependency>配置application.yml:
seata: enabled: true application-id: order-service tx-service-group: my_tx_group service: vgroup-mapping: my_tx_group: default grouplist: default: 127.0.0.1:8091 registry: type: file4.3 业务代码实现
在订单服务中创建全局事务:
@GlobalTransactional public void createOrder(OrderDTO orderDTO) { // 1. 扣减库存 storageFeignClient.deduct(orderDTO.getCommodityCode(), orderDTO.getCount()); // 2. 创建订单 orderMapper.create(orderDTO); // 3. 扣减余额 accountFeignClient.debit(orderDTO.getUserId(), orderDTO.getMoney()); }在库存服务中实现分支事务:
@Transactional public void deduct(String commodityCode, int count) { // 检查库存 Storage storage = storageMapper.selectByCommodityCode(commodityCode); if(storage.getCount() < count) { throw new RuntimeException("库存不足"); } // 扣减库存 storageMapper.deduct(commodityCode, count); }4.4 数据库表设计
每个业务数据库都需要创建undo_log表:
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;5. Seata TCC模式实战
对于需要更高性能的场景,可以使用TCC模式。下面我们改造库存服务,实现TCC接口。
5.1 定义TCC接口
@LocalTCC public interface StorageTccService { @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel") boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "commodityCode") String commodityCode, @BusinessActionContextParameter(paramName = "count") int count); boolean confirm(BusinessActionContext actionContext); boolean cancel(BusinessActionContext actionContext); }5.2 实现TCC逻辑
@Service public class StorageTccServiceImpl implements StorageTccService { @Autowired private StorageMapper storageMapper; @Autowired private StorageFreezeMapper freezeMapper; @Transactional @Override public boolean prepare(BusinessActionContext actionContext, String commodityCode, int count) { // 检查库存 Storage storage = storageMapper.selectByCommodityCode(commodityCode); if(storage.getCount() < count) { throw new RuntimeException("库存不足"); } // 冻结库存 StorageFreeze freeze = new StorageFreeze(); freeze.setXid(actionContext.getXid()); freeze.setCommodityCode(commodityCode); freeze.setCount(count); freeze.setCreateTime(new Date()); freezeMapper.insert(freeze); return true; } @Transactional @Override public boolean confirm(BusinessActionContext actionContext) { // 获取冻结记录 String xid = actionContext.getXid(); StorageFreeze freeze = freezeMapper.selectByXid(xid); if(freeze == null) { return true; } // 扣减实际库存 storageMapper.deduct(freeze.getCommodityCode(), freeze.getCount()); // 删除冻结记录 freezeMapper.deleteByXid(xid); return true; } @Transactional @Override public boolean cancel(BusinessActionContext actionContext) { // 删除冻结记录 String xid = actionContext.getXid(); freezeMapper.deleteByXid(xid); return true; } }6. Seata性能优化与最佳实践
6.1 配置优化
在seata-server的file.conf中调整以下参数:
store { mode = "db" db { datasource = "druid" db-type = "mysql" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "root" password = "password" min-conn = 5 max-conn = 30 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } } transport { thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" worker-thread-size = "default" boss-thread-size = 1 } shutdown { wait = 3 } type = "TCP" server = "NIO" heartbeat = true serialization = "seata" compressor = "none" }6.2 常见问题解决
全局锁冲突:
- 现象:出现"Global lock wait timeout"错误
- 解决方案:
- 增加锁等待时间:
client.rm.lock.retryInterval=10(默认10ms) - 增加重试次数:
client.rm.lock.retryTimes=30(默认30次) - 优化业务逻辑,减少事务持有时间
- 增加锁等待时间:
AT模式不生效:
- 检查点:
- 确保数据源被Seata代理
- 检查undo_log表是否存在
- 确认@GlobalTransactional注解生效
- 检查点:
性能瓶颈:
- 优化建议:
- 使用TCC模式替代AT模式
- 分库分表减少单表数据量
- 调整TC的线程池大小
- 优化建议:
7. Seata与其他框架集成
7.1 与Spring Cloud集成
在Spring Cloud项目中,需要额外配置:
spring: cloud: alibaba: seata: tx-service-group: my_tx_group7.2 与Dubbo集成
在Dubbo服务中,需要添加过滤器:
<dubbo:reference filter="-exception" ... />7.3 与MyBatis集成
确保使用Seata的数据源代理:
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DruidDataSource druidDataSource() { return new DruidDataSource(); } @Primary @Bean("dataSource") public DataSource dataSource(DruidDataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } }8. 生产环境部署建议
高可用部署:
- TC服务部署至少3个节点
- 使用Nacos等注册中心做服务发现
- 配置负载均衡
监控与告警:
- 集成Prometheus监控
- 配置关键指标告警:
- 事务成功率
- 平均处理时间
- 失败事务数
灾备方案:
- 定期备份事务日志
- 准备手动干预脚本
- 设计降级方案
在实际项目中,我们团队发现Seata的AT模式在中小型系统中表现良好,但当并发量超过2000TPS时,建议考虑TCC模式。另外,全局锁的设计使得它不适合秒杀等高并发场景,这类场景更适合使用最终一致性方案。