分布式事务是构建现代分布式系统的关键技术之一,它解决了在多个独立服务或数据库间保持数据一致性的难题。本文将系统性地介绍分布式事务的必要性、技术演进历程以及当前主流解决方案的实现原理。我们将从最简单的单数据库事务开始,逐步深入到复杂的微服务场景下的分布式事务处理,涵盖 2PC、TCC、Saga、可靠消息、Seata AT 等主流技术,并结合实际案例和图示分析各种技术的优缺点及适用场景。
为什么需要分布式事务?
在单体应用架构中,我们通常使用数据库的 ACID 事务来保证数据一致性。然而随着系统规模扩大和微服务架构的普及,数据和服务被拆分到不同的节点上,传统的单机事务机制已无法满足需求,这就产生了对分布式事务的需求。
典型案例:银行转账
考虑一个经典的银行转账场景:程序员小张要向女友小丽转账 100 元。这个操作需要两个步骤:
从小张账户扣除 100 元
向小丽账户增加 100 元
在单体架构中,如果两个账户在同一个数据库中,我们可以简单地使用数据库事务来保证操作的原子性:
BEGIN TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE user_id = '小张';
UPDATE account SET balance = balance + 100 WHERE user_id = '小丽';
COMMIT;
复制代码
这种情况下,数据库事务能确保两个操作要么都成功,要么都失败,不会出现小张扣款而小丽未收款的情况。
然而在分布式系统中,情况变得复杂:
小张和小丽的账户可能存储在不同的数据库中
扣款和收款可能是两个独立的微服务
网络调用可能出现延迟或失败
此时,传统的数据库事务就无法保证跨服务的操作一致性了,我们需要分布式事务来解决这个问题。
分布式事务的典型场景
分布式事务主要出现在以下三种场景中:
跨 JVM 进程:微服务架构中,不同服务通过远程调用完成事务操作,如订单服务调用库存服务减库存。
跨数据库实例:单体系统访问多个数据库实例,如用户信息和订单信息分别存储在两个 MySQL 实例中。
多服务访问同一数据库:即使多个微服务访问同一个数据库,由于它们持有不同的数据库连接,也会产生分布式事务问题。
分布式事务的核心挑战
分布式事务面临的主要挑战包括:
这些挑战促使了分布式事务技术的不断演进,从早期的两阶段提交到现代的柔性事务解决方案。
分布式事务的技术演进过程
分布式事务技术随着系统架构的演变而不断发展。下面我们将按照技术演进的顺序,详细介绍各阶段的核心解决方案。
第一阶段:单数据库事务
适用场景:所有操作都在同一个数据库中完成。
实现原理:直接利用数据库的 ACID 事务特性,通过BEGIN TRANSACTION
、COMMIT
、ROLLBACK
等命令保证操作的原子性。
银行转账示例:
BEGIN TRANSACTION;
-- 扣除小张账户100元
UPDATE account SET balance = balance - 100 WHERE user_id = '小张';
-- 增加小丽账户100元
UPDATE account SET balance = balance + 100 WHERE user_id = '小丽';
COMMIT;
复制代码
异常处理:
优点:
实现简单,完全依赖数据库内置机制
性能高,没有跨节点协调开销
100%保证数据一致性
缺点:
仅适用于单数据库场景
无法满足微服务架构和分库分表的需求
[协调者] [数据库]
|-- BEGIN TRANSACTION -->|
|---- UPDATE 小张 ------->|
|---- UPDATE 小丽 ------->|
|------ COMMIT -------->|
复制代码
随着用户量增长,单数据库无法承受压力,于是产生了数据库垂直拆分的需求,将不同业务表拆分到不同数据库中,这就进入了分布式事务的领域。
第二阶段:基于后置提交的多数据库事务
当账户表和交易记录表被拆分到不同数据库后,简单的单数据库事务不再适用。最初的解决方案是后置提交策略。
实现原理:
在所有参与数据库上执行 SQL 但不提交
如果所有 SQL 执行成功,则逐个提交各数据库事务
如果任何 SQL 执行失败,则回滚所有数据库事务
银行转账示例:
// 数据库1:账户库
Connection conn1 = db1.getConnection();
conn1.setAutoCommit(false);
// 数据库2:交易库
Connection conn2 = db2.getConnection();
conn2.setAutoCommit(false);
try {
// 第一步:在所有数据库上执行SQL但不提交
stmt1 = conn1.prepareStatement("UPDATE account SET balance=balance-100 WHERE user_id='小张'");
stmt1.executeUpdate();
stmt2 = conn2.prepareStatement("INSERT INTO transaction(from_user,to_user,amount) VALUES('小张','小丽',100)");
stmt2.executeUpdate();
// 第二步:全部执行成功后,逐个提交
conn1.commit();
conn2.commit();
} catch (Exception e) {
// 任何一步失败则回滚所有
conn1.rollback();
conn2.rollback();
throw e;
}
复制代码
异常处理:
优点:
比简单的"执行-立即提交"模式更能保证一致性
实现相对简单
缺点:
提交阶段出现异常时无法保证一致性
事务持有时间较长,影响并发性能
[协调者] [DB1] [DB2]
|-- BEGIN -->|
|-- UPDATE小张-->|
|-- BEGIN -->|
|-- INSERT交易记录-->|
|-- COMMIT DB1-->| (成功)
|-- COMMIT DB2-->| (失败!)
// 此时DB1已提交无法回滚,数据不一致
复制代码
为解决后置提交的缺陷,计算机科学家们提出了两阶段提交协议(2PC),这成为分布式事务的经典解决方案。
第三阶段:两阶段提交(2PC/XA)
两阶段提交协议通过引入准备阶段来解决后置提交的问题。
2PC 基本流程
阶段一:准备阶段
协调者向所有参与者发送 prepare 请求
参与者执行事务操作但不提交,记录 undo/redo 日志
参与者回复是否可以提交
阶段二:提交/回滚阶段
XA 规范实现
XA 是 X/Open 组织提出的分布式事务规范,主流数据库如 MySQL、Oracle 等都支持 XA 协议。
Java 中使用 XA 示例(使用 Atomikos):
// 初始化XA数据源
AtomikosDataSourceBean ds1 = new AtomikosDataSourceBean();
ds1.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
// 配置略...
AtomikosDataSourceBean ds2 = new AtomikosDataSourceBean();
ds2.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
// 配置略...
// 获取连接
Connection conn1 = ds1.getConnection();
Connection conn2 = ds2.getConnection();
// 执行分布式事务
UserTransaction utx = com.atomikos.icatch.jta.UserTransactionManager();
try {
utx.begin();
PreparedStatement ps1 = conn1.prepareStatement("UPDATE account SET balance=balance-100 WHERE user_id='小张'");
ps1.executeUpdate();
PreparedStatement ps2 = conn2.prepareStatement("INSERT INTO transaction(from_user,to_user,amount) VALUES('小张','小丽',100)");
ps2.executeUpdate();
utx.commit(); // 两阶段提交
} catch (Exception e) {
utx.rollback();
throw e;
}
复制代码
优点:
标准化协议,主流数据库都支持
强一致性保证
对业务代码侵入较小
缺点 :
由于 2PC 的这些缺陷,在微服务架构流行后,出现了更适合服务化场景的 TCC 模式。
第四阶段:TCC 模式
当系统从直接操作数据库演进为服务化架构后,XA 协议不再适用。TCC(Try-Confirm-Cancel)模式成为服务化场景下分布式事务的主流解决方案。
TCC 核心思想
TCC 将业务操作分为三个阶段:
Try:尝试执行业务,完成所有一致性检查,预留必要资源
Confirm:确认执行业务,真正提交(使用 Try 阶段预留的资源)
Cancel:取消执行业务,释放 Try 阶段预留的资源
TCC 实现示例
以转账为例,我们需要改造原有服务,为每个操作提供三个接口:
账户服务:
// Try接口:冻结金额
@PostMapping("/account/freeze")
public boolean freeze(@RequestParam String userId,
@RequestParam BigDecimal amount) {
return accountService.freezeAmount(userId, amount);
}
// Confirm接口:扣除冻结金额
@PostMapping("/account/confirm")
public boolean confirm(@RequestParam String userId,
@RequestParam BigDecimal amount) {
return accountService.debitFrozenAmount(userId, amount);
}
// Cancel接口:解冻金额
@PostMapping("/account/cancel")
public boolean cancel(@RequestParam String userId,
@RequestParam BigDecimal amount) {
return accountService.unfreezeAmount(userId, amount);
}
复制代码
交易服务:
// Try接口:创建待确认交易记录
@PostMapping("/transaction/prepare")
public String prepare(@RequestBody TransactionDTO dto) {
return transactionService.prepare(dto);
}
// Confirm接口:确认交易
@PostMapping("/transaction/confirm")
public boolean confirm(@RequestParam String txId) {
return transactionService.confirm(txId);
}
// Cancel接口:取消交易
@PostMapping("/transaction/cancel")
public boolean cancel(@RequestParam String txId) {
return transactionService.cancel(txId);
}
复制代码
事务协调器:
public class TccTransferService {
@Autowired
private AccountClient accountClient;
@Autowired
private TransactionClient transactionClient;
public boolean transfer(String fromUserId, String toUserId, BigDecimal amount) {
// 生成全局事务ID
String xid = UUID.randomUUID().toString();
try {
// 阶段一:Try
boolean accountPrepared = accountClient.freeze(fromUserId, amount);
String txId = transactionClient.prepare(
new TransactionDTO(xid, fromUserId, toUserId, amount));
if (!accountPrepared || txId == null) {
throw new RuntimeException("Try阶段失败");
}
// 阶段二:Confirm
boolean accountConfirmed = accountClient.confirm(fromUserId, amount);
boolean txConfirmed = transactionClient.confirm(txId);
return accountConfirmed && txConfirmed;
} catch (Exception e) {
// 阶段二:Cancel
accountClient.cancel(fromUserId, amount);
transactionClient.cancel(txId);
throw e;
}
}
}
复制代码
异常情况处理:
空回滚:Try 未执行但收到了 Cancel 请求,需实现幂等性处理
幂等控制:Confirm/Cancel 可能会重试,需保证多次执行效果相同
悬挂问题:Cancel 比 Try 先到,需记录操作日志进行防护
优点 :
避免了长事务,性能较好
适用于跨服务的分布式事务
可以自定义业务逻辑的补偿操作
缺点:
对业务侵入性强,每个操作需要改造为三个接口
实现复杂度高,需要考虑各种异常情况
一致性较弱,Confirm 阶段仍可能失败
第五阶段:Saga 模式
对于长事务场景,TCC 模式的资源锁定时间仍然过长。Saga 模式通过事件驱动和补偿事务提供了另一种解决方案。
Saga 核心思想
Saga 将分布式事务拆分为一系列本地事务,每个本地事务:
执行实际业务操作
发布事件触发下一个本地事务
提供补偿操作用于回滚
Saga 有两种协调方式:
编排式(Choreography):通过事件总线自然流转,无中心协调者
编导式(Orchestration):由 Saga 协调器集中控制流程
Saga 实现示例
以订单创建为例,涉及订单服务、库存服务和支付服务:
订单服务:
public class OrderSaga {
@Autowired
private InventoryClient inventoryClient;
@Autowired
private PaymentClient paymentClient;
@Transactional
public void createOrder(Order order) {
// 1. 创建订单(待支付状态)
orderRepository.save(order);
// 2. 扣减库存
inventoryClient.reduceStock(order.getProductId(), order.getQuantity());
// 3. 发起支付
paymentClient.createPayment(order.getId(), order.getAmount());
}
// 补偿操作
@Transactional
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId);
if (order != null) {
order.setStatus("CANCELLED");
orderRepository.save(order);
}
}
}
复制代码
库存服务:
public class InventorySaga {
@Transactional
public void reduceStock(String productId, int quantity) {
inventoryRepository.reduceStock(productId, quantity);
}
// 补偿操作
@Transactional
public void compensateReduceStock(String productId, int quantity) {
inventoryRepository.addStock(productId, quantity);
}
}
复制代码
Saga 协调器(编导式):
public class OrderSagaOrchestrator {
public void execute(Order order) {
Saga saga = new Saga("create_order_" + order.getId());
try {
// 步骤1:创建订单
saga.addStep(
() -> orderService.createOrder(order),
() -> orderService.cancelOrder(order.getId())
);
// 步骤2:扣减库存
saga.addStep(
() -> inventoryService.reduceStock(order.getProductId(), order.getQuantity()),
() -> inventoryService.compensateReduceStock(order.getProductId(), order.getQuantity())
);
// 步骤3:创建支付
saga.addStep(
() -> paymentService.createPayment(order.getId(), order.getAmount()),
null // 最后一步无需补偿
);
saga.execute();
} catch (Exception e) {
saga.rollback();
throw e;
}
}
}
复制代码
优点 :
适用于长事务,不需要长期锁定资源
事件驱动架构,服务间耦合度低
性能较好,支持并行执行子事务
缺点:
编程模型复杂,需要设计补偿操作
不保证隔离性,可能出现脏读
调试困难,特别是编排式 Saga
第六阶段:可靠消息最终一致性
对于对实时一致性要求不高的场景,可靠消息最终一致性模式提供了更轻量级的解决方案。
核心思想
消息生产者与本地事务一起提交消息
消息中间件保证消息投递
消费者保证消息处理幂等
本地消息表示例
生产者端:
@Transactional
public void makePayment(Long orderId, BigDecimal amount) {
// 1. 业务操作:更新支付状态
paymentDao.updateStatus(orderId, "PAID");
// 2. 记录消息(与业务操作同库同事务)
messageDao.save(
new Message(UUID.randomUUID().toString(),
"payment_completed",
orderId.toString())
);
}
// 定时任务轮询发送消息
@Scheduled(fixedRate = 5000)
public void pollAndSendMessages() {
List<Message> messages = messageDao.findUnsent();
for (Message msg : messages) {
try {
rocketMQTemplate.send(msg.getTopic(), msg.getContent());
messageDao.markAsSent(msg.getId());
} catch (Exception e) {
log.error("发送消息失败", e);
}
}
}
复制代码
消费者端:
@RocketMQMessageListener(topic = "payment_completed", consumerGroup = "order_group")
public class PaymentCompletedConsumer implements RocketMQListener<String> {
@Override
@Transactional
public void onMessage(String orderId) {
// 幂等处理:检查是否已处理过
if (orderDao.isProcessed(orderId)) {
return;
}
// 更新订单状态
orderDao.updateStatus(orderId, "PAID");
// 记录处理标记
orderDao.markAsProcessed(orderId);
}
}
复制代码
优点 :
缺点:
只能保证最终一致性
需要处理幂等性问题
调试和问题排查困难
第七阶段:Seata AT 模式
Seata AT(Automatic Transaction)模式是阿里开源的分布式事务解决方案,结合了 XA 和 TCC 的优点。
核心思想
一阶段:执行业务 SQL,自动生成 undo log 并获取全局锁
二阶段:提交:异步删除 undo log 回滚:根据 undo log 生成反向 SQL 补偿
+----------+ +----------+ +----------+
| TM | | RM | | TC |
|(事务管理器)|-----|(资源管理器)|-----|(事务协调器)|
+----------+ +----------+ +----------+
| | |
v v v
业务应用 数据库代理 全局事务控制
复制代码
Seata AT 示例
配置:
@Configuration
public class SeataConfig {
@Bean
public GlobalTransactionScanner globalTransactionScanner() {
return new GlobalTransactionScanner("order-service", "my_test_tx_group");
}
}
复制代码
业务代码:
@GlobalTransactional
public void createOrder(Order order) {
// 1. 扣减库存
inventoryDao.reduceStock(order.getProductId(), order.getQuantity());
// 2. 创建订单
orderDao.insert(order);
// 3. 扣减账户余额
accountDao.reduceBalance(order.getUserId(), order.getAmount());
}
复制代码
工作原理:
业务方法开始时,Seata 会拦截并开启全局事务
每个 SQL 执行时,Seata 代理会:前置镜像:查询修改前的数据执行业务 SQL 后置镜像:查询修改后的数据生成 undo log 并注册分支事务
如果所有操作成功,全局事务提交,异步清理 undo log
如果任何操作失败,全局事务回滚,根据 undo log 执行补偿
优点 :
对业务代码几乎无侵入
性能优于 XA,不需要数据库支持 XA 协议
支持读已提交隔离级别
缺点:
需要部署 Seata Server
回滚时可能遇到数据冲突
全局锁可能成为性能瓶颈
现代分布式事务技术对比
根据不同的业务场景和一致性要求,我们可以选择合适的分布式事务解决方案:
表 1:主流分布式事务方案对比
选型建议:
必须强一致:XA/2PC(适用于同机房、事务量中等场景)
金融场景:TCC(需要精确控制每一步操作)
长业务流程:Saga(适合订单、审批等流程)
高并发最终一致:可靠消息+本地事务(电商下单等场景)
一站式解决方案:Seata AT(国内微服务常用)
分布式事务的未来发展
随着云原生和 Serverless 架构的兴起,分布式事务技术也在不断演进:
Service Mesh 集成:将分布式事务能力下沉到基础设施层
Saga 模式增强:结合事件溯源(Event Sourcing)提供更好的可观测性
混合事务:结合强一致和最终一致的优势
新数据库支持:如 Google Spanner 的 TrueTime API 提供全局一致性
总结
分布式事务技术的发展经历了从单数据库到多数据库,再到微服务架构的演进过程。从最初的 XA/2PC 强一致性方案,到后来的 TCC、Saga 等最终一致性方案,再到现在的 Seata AT 等混合方案,每一种技术都是为了解决特定场景下的分布式一致性问题。
在实际应用中,没有完美的解决方案,只有最适合业务场景的方案。理解各种技术的原理和优缺点,才能做出合理的架构决策。未来,随着新技术的出现,分布式事务领域还将继续演进,为构建可靠的分布式系统提供更多可能性。
文章转载自:佛祖让我来巡山
原文链接:https://www.cnblogs.com/sun-10387834/p/18923401
体验地址:http://www.jnpfsoft.com/?from=001YH
评论