1 分布式事务在 Java Web 项目中的实践
好的,分布式事务在 Java Web 项目中的实践是一个非常核心且常见的问题。我会从理论到实践,为你系统地讲解常见的解决方案和具体技术实现。
1. 理解分布式事务的核心问题
在分布式系统中,一个业务操作往往需要调用多个独立的服务(例如:订单服务、库存服务、账户服务),每个服务都有自己的数据库(即资源管理器)。要保证所有服务的数据要么全部成功,要么全部失败,这就是分布式事务要解决的问题,其核心是满足 ACID 特性,尤其是原子性(Atomicity)。
传统的单机数据库事务(通过 JDBC 的 commit
/rollback
)无法直接跨越多个数据库。
2. 主流实践方案
Java Web 项目中处理分布式事务主要有以下几种主流方案,各有其适用场景。
方案一:两阶段提交 (2PC - Two-Phase Commit) - 强一致性
这是一种重量级的解决方案,追求强一致性。
- 角色:包含一个协调者 (Coordinator)(通常是事务管理器)和多个参与者 (Participants)(即各个微服务/资源管理器)。
- 阶段一:准备阶段 (Prepare Phase)
- 协调者向所有参与者发送事务内容,询问是否可以提交。
- 每个参与者执行事务,但不提交,将
Undo
和Redo
信息记入事务日志。 - 参与者回复协调者:“可以提交” (Yes) 或“不可以提交” (No)。
- 阶段二:提交/回滚阶段 (Commit/Rollback Phase)
- 如果所有参与者都回复 “Yes”:协调者向所有参与者发送 Commit 命令,参与者正式提交事务。
- 如果任何一个参与者回复 “No” 或超时:协调者向所有参与者发送 Rollback 命令,参与者回滚事务。
- Java 实现:
- JTA (Java Transaction API):Java EE 的标准 API。
- 实现框架:如 Atomikos, Narayana。
- 使用方式:通常与 Spring 的
JtaTransactionManager
集成,使用@Transactional
注解即可管理分布式事务。
- 优点:强一致性,ACID 保障严格。
- 缺点:
- 同步阻塞:在准备阶段后,所有参与者都在等待协调者的指令,资源会被锁定。
- 性能差:通信次数多,延迟高,不适合高并发场景。
- 协调者单点问题:协调者宕机会导致参与者一直处于不确定状态。
方案二:TCC (Try-Confirm-Cancel) - 最终一致性
这是一种补偿型的解决方案,通过业务逻辑来实现最终一致性。它将一个业务操作分成三个步骤:
- Try 阶段:尝试执行,完成所有业务的检查,并预留必要的业务资源。
订单服务
:创建一个状态为 “待确认” 的订单。库存服务
:检查库存,并预扣库存(将库存锁定,而不是实际减少)。账户服务
:检查账户余额,并冻结要支付的金额。
- Confirm 阶段:确认执行。如果所有服务的 Try 都成功了,则进入 Confirm 阶段。Confirm 使用 Try 阶段预留的资源,真正执行业务操作。
订单服务
:将订单状态更新为 “已确认”。库存服务
:扣减预扣的库存。账户服务
:扣减冻结的金额。
- Cancel 阶段:取消执行。如果任何一个服务的 Try 失败,则进入 Cancel 阶段,释放 Try 阶段预留的资源。
订单服务
:将订单状态更新为 “已取消”。库存服务
:释放预扣的库存。账户服务
:释放冻结的金额。
- Java 实现:
- 框架:Seata (TCC 模式), Hmily, ByteTCC。
- 使用方式:你需要为每个服务编写
try
,confirm
,cancel
三个接口方法,并通过框架注解(如@TwoPhaseBusinessAction
)将其关联。框架会负责调用这些方法。
- 优点:性能比 2PC 好,保证了最终一致性。
- 缺点:
- 代码侵入性强:需要为每个业务逻辑编写 Try/Confirm/Cancel 三个方法,开发量大。
- 业务模型复杂:需要考虑如何预留和释放资源。
方案三:基于消息队列的最终一致性 - 最常用
这是目前互联网公司最常用的方案,利用消息队列的可靠性来实现最终一致性。其核心思想是:将分布式事务拆分成本地事务+异步消息。
经典场景:下单扣库存
- 本地事务(订单服务):
- 在订单服务的数据库中创建订单,状态为 “待支付”。
- 在同一个本地事务中,向消息表(与订单表在同一个数据库)插入一条记录,内容是“要发送扣减库存的消息”。或者,直接向 MQ 发送一条 “半消息”/“预备消息”(RocketMQ 支持)。
- 消息投递:
- 有一个定时任务扫描消息表,将消息发送给 MQ。或者,MQ 回调确认后正式投递消息(RocketMQ 事务消息机制)。
- 消费者(库存服务):
- 库存服务监听 MQ,收到扣减库存的消息。
- 执行本地事务:扣减数据库库存。
- 如果执行成功,向 MQ 返回
ACK
,消息被消费;如果失败,MQ 会重投消息(需要保证接口幂等性)。
- Java 实现:
- MQ:RocketMQ(原生支持事务消息),Kafka 和 RabbitMQ 需要结合数据库消息表自己实现。
- 框架:Spring Cloud Stream, RocketMQ-Spring-Boot-Starter。
- 优点:
- 性能高,吞吐量好。
- 实现了业务的解耦。
- 通用性强,适用于很多最终一致性场景。
- 缺点:
- 只保证最终一致性,存在短暂的数据不一致窗口。
- 需要处理消息重试和幂等性问题。
方案四:Seata 的 AT 模式 (Automatic Transaction) - 无侵入
Seata 是阿里开源的分布式事务解决方案,其 AT 模式对代码侵入性低,类似于“增强版的 2PC”。
- 原理:
- 一阶段:
- Seata 的 JDBC 数据源代理会拦截业务 SQL。
- 解析 SQL,生成查询快照(
before image
)。 - 执行 SQL,更新数据库。
- 生成更新后的快照(
after image
)。 - 将行锁和快照信息保存到 Seata 的全局锁表(
global_table
,lock_table
)中。
- 二阶段提交:
- 如果所有分支事务成功,TM 通知 TC,TC 通知所有 RM 删除快照和锁信息,完成提交。
- 二阶段回滚:
- 如果有分支事务失败,TM 通知 TC,TC 通知 RM 回滚。
- RM 比较当前数据与
after image
,如果一致,用before image
回滚数据;如果不一致,说明有脏写,需要人工介入。
- Java 实现:
- 框架:Seata。
- 使用方式:
- 部署 Seata Server (TC)。
- 在每个微服务中引入 Seata Client 依赖。
- 配置数据源代理。
- 在全局事务的入口方法上添加
@GlobalTransactional
注解。
- 优点:使用简单,代码侵入性低,性能不错。
- 缺点:
- 需要部署和维护 Seata Server。
- AT 模式默认读未提交隔离级别,可能存在脏读(但通过全局锁一定程度上避免)。
实践总结与选型建议
方案 | 一致性 | 性能 | 侵入性 | 复杂度 | 适用场景 |
2PC (JTA) | 强一致 | 低 | 低 | 中 | 传统企业级应用,内部系统 |
TCC | 最终一致 | 中 | 高 | 高 | 对一致性要求高,资金、交易核心场景 |
本地消息表 | 最终一致 | 高 | 中 | 中 | 异步场景,如订单、库存、积分 |
Seata AT | 最终一致 | 中 | 低 | 低 | 希望快速上手,对一致性要求非强一致的业务 |
如何选择?
- 追求强一致性:银行、支付等金融核心系统,可考虑 TCC 或 2PC(但后者现在较少)。
- 追求高可用和最终一致性:绝大多数互联网业务(如电商、社交),首选 基于消息队列的最终一致性 方案。
- 希望快速开发且无侵入:可以选择 Seata AT 模式,它能解决大部分分布式事务问题,且对代码影响最小。
示例代码 (Seata AT 模式)
以 Spring Cloud Alibaba + Seata 为例:
- 订单服务入口方法:
@Service
public class OrderServiceImpl implements OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate AccountFeignClient accountFeignClient; // 通过Feign调用账户服务@Override@GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class) // 开启全局事务public void createOrder(Order order) {// 1. 本地操作:创建订单orderMapper.insert(order);// 2. 远程调用:扣减账户余额ResponseEntity<String> result = accountFeignClient.decrease(order.getUserId(), order.getMoney());if (!result.getStatusCode().is2xxSuccessful()) {throw new RuntimeException("调用账户服务失败");}// 如果发生异常,全局事务会自动回滚:订单记录会被删除,账户服务的扣款也会回滚}
}
- 账户服务方法:
@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountMapper accountMapper;@Override@Transactional // 本地事务注解public void decrease(Long userId, BigDecimal money) {// 本地操作:扣减账户余额accountMapper.decrease(userId, money);// 这里如果抛出异常,会先回滚本地事务,然后通知Seata TC,TC会通知订单服务回滚}
}
只需要一个 @GlobalTransactional
注解,Seata 就会自动帮你协调订单服务和账户服务的事务,使其成为一个分布式事务。
希望这个详细的解答能帮助你在 Java Web 项目中更好地实践分布式事务!