3. 事务基础回顾:ACID特性、本地事务、分布式事务核心原理

聊数据一致性,绕不开事务。我见过不少同学一上来就追各种分布式事务框架,结果遇到诡异的数据对不上的问题,查半天发现是本地事务都没用好。说白了,事务是数据一致性的地基,地基不稳,上面盖什么都白搭。

这一节我们快速过一遍事务的核心概念。如果你已经很熟了,可以当复习;如果有些模糊,我建议你仔细看看,尤其是后面讲分布式事务那部分,跟决策回写场景直接相关。

3.1 ACID —— 事务的四个承诺

事务是什么?简单说,就是一组操作,要么全部成功,要么全部失败。数据库通过 ACID 四个特性来保证这个承诺。

特性 含义 我的一点体会
原子性(Atomicity) 事务里的操作,要么全做,要么全不做 不能出现「扣了钱但没下单」的情况
一致性(Consistency) 事务前后,数据必须满足所有约束 比如账户余额不能为负,这个得业务代码自己保证
隔离性(Isolation) 并发事务之间互不干扰 隔离级别选不对,脏读、幻读就来了
持久性(Durability) 事务一旦提交,数据就不会丢 靠的是 WAL(Write-Ahead Logging)机制

重点提醒: 一致性是业务层面的概念,数据库只保证原子性、隔离性、持久性。你写代码时得自己确保业务规则正确,数据库不会替你检查「转账金额不能超过余额」这种逻辑。

3.2 本地事务 —— 最基础也最容易被忽视

本地事务,就是单个数据库资源内的事务。比如你在 MySQL 里用 BEGIN 和 COMMIT,这就是本地事务。

-- 一个典型的本地事务示例
BEGIN;

UPDATE account SET balance = balance - 100 WHERE user_id = 1;
UPDATE account SET balance = balance + 100 WHERE user_id = 2;

COMMIT;

这段代码看起来简单,对吧?但我曾经在一个项目里遇到过,有人把 COMMIT 写成了 commit(小写),MySQL 默认配置下直接报语法错误,整个事务回滚了。嗯,这种低级错误其实挺常见的。

本地事务的优点是快、简单、可靠。缺点也很明显——它只能管一个数据库。一旦你的操作涉及多个数据库、多个服务,本地事务就无能为力了。

我的习惯: 能用本地事务解决的问题,绝不用分布式事务。分布式事务的代价比你想象的大得多。很多场景其实可以通过合理的数据库设计,把跨库操作变成单库操作。

3.3 分布式事务 —— 为什么这么难?

当你的系统拆成了微服务,订单在订单库,库存在库存库,用户积分在积分库……这时候一个操作要改三个库,本地事务就管不了了。你需要分布式事务。

分布式事务的核心难题是什么?是「网络不可靠」和「节点可能宕机」。你想想看,A 库提交成功了,B 库提交时网络断了,这时候怎么办?

为了解决这个问题,业界提出了几种经典方案。我按复杂度从低到高给你捋一遍。

3.4 2PC(两阶段提交)—— 简单但慢

2PC 把事务提交分成两个阶段:

  1. 准备阶段: 协调者问所有参与者「能提交吗?」参与者写日志,锁资源,然后回复 Yes/No。
  2. 提交阶段: 如果所有人都说 Yes,协调者通知大家 Commit;只要有一个人说 No,就通知所有人 Rollback。

听起来挺合理的,对吧?但实际用起来问题不少:

  • 同步阻塞: 准备阶段参与者要锁资源,直到第二阶段结束才能释放。如果协调者挂了,参与者就一直锁着。
  • 单点故障: 协调者挂了,整个事务卡住。
  • 数据不一致: 第二阶段协调者通知 Commit,但部分参与者没收到,就出现了部分提交、部分没提交的情况。

避坑指南: 我曾经在一个金融项目里看到有人用 2PC 做跨行转账,结果协调者宕机,一堆事务卡在准备阶段,数据库连接池直接爆了。后来我们改成了 Saga 模式。2PC 适合短事务、低并发场景,高并发下慎用。

3.5 3PC(三阶段提交)—— 改进但没完全解决

3PC 在 2PC 基础上加了一个 CanCommit 阶段,并且引入了超时机制。流程变成:

  1. CanCommit: 协调者问大家「能不能提交?」—— 这时候不锁资源,只是探询。
  2. PreCommit: 协调者通知「准备提交」,参与者锁资源、写日志。
  3. DoCommit: 协调者通知「正式提交」。

3PC 解决了协调者宕机后参与者无限等待的问题(参与者超时会自动提交),但数据不一致的问题依然存在。而且多了一次网络交互,性能更差了。实际生产中用 3PC 的很少,我基本没见过。

3.6 TCC(Try-Confirm-Cancel)—— 业务侵入性强

TCC 是另一种思路:不依赖数据库的锁,而是由业务代码来控制。三个阶段:

  • Try: 预留资源。比如冻结库存 10 件,不是直接扣减。
  • Confirm: 确认执行。把冻结的库存真正扣掉。
  • Cancel: 取消。把冻结的库存解冻。
// TCC 接口示例
public interface TccService {
    boolean tryReserve(Order order);      // 预留资源
    boolean confirm(Order order);         // 确认提交
    boolean cancel(Order order);          // 取消回滚
}

TCC 的好处是性能好,不长时间锁资源。坏处是——你得为每个操作写三个方法,业务代码量翻倍。而且 Cancel 逻辑必须能正确处理 Try 阶段的各种情况,幂等性也得自己保证。

我的建议: TCC 适合对一致性要求高、并发大的场景。但一定要做好幂等和防悬挂处理。我曾经因为 Cancel 接口没做幂等,导致同一笔订单被回滚了两次,库存变成了负数……嗯,那天的 on-call 电话我现在还记得。

3.7 Saga —— 长事务的救星

Saga 的核心思想是:把一个长事务拆成多个本地事务,每个本地事务都有一个对应的补偿操作。如果某个步骤失败了,就依次执行前面步骤的补偿操作。

Saga 有两种实现方式:

  • 编排(Choreography): 每个服务完成后发事件,下一个服务监听事件执行。没有中心协调者。
  • 协调(Orchestration): 有一个协调器来告诉每个服务「该做什么了」。
// Saga 编排模式示例(伪代码)
// 订单服务
orderService.createOrder() -> 发事件 "OrderCreated"
// 库存服务
eventBus.listen("OrderCreated") -> 扣库存 -> 发事件 "InventoryDeducted"
// 积分服务
eventBus.listen("InventoryDeducted") -> 加积分 -> 发事件 "PointsAdded"

// 如果加积分失败,触发补偿
eventBus.listen("PointsAddedFailed") -> 库存服务补偿: 恢复库存

Saga 的好处是没有长时间锁,适合长事务。坏处是补偿逻辑复杂,而且隔离性不好——别的服务可能看到中间状态的数据。

决策回写场景的启示: 我们后面要讲的决策回写,本质上就是一个 Saga 流程。决策引擎算出结果,然后要回写到多个系统。如果某个系统回写失败,就需要补偿。所以 Saga 是我们后续章节的核心理论基础。

3.8 四种分布式事务方案对比

方案 一致性 性能 业务侵入 适用场景
2PC 强一致 低(锁资源) 低 短事务、低并发
3PC 弱一致 更低 低 几乎不用
TCC 强一致 高 高 高并发、短事务
Saga 最终一致 高 中 长事务、跨服务

3.9 本章知识体系总览

下面这张图把本章的核心知识点串起来了。你可以看到,从本地事务到分布式事务,从强一致到最终一致,每种方案都有自己的位置。

事务知识体系总览 事务知识体系总览 事务(Transaction) 本地事务 分布式事务 ACID 特性 单库事务 2PC / 3PC TCC Saga 💡 核心思路 能用本地事务解决的问题,绝不引入分布式事务。 分布式事务选型:短事务高并发选 TCC,长事务跨服务选 Saga。

这张图把事务体系分成了两条路:左边是本地事务,简单可靠;右边是分布式事务,复杂但必要。你往后看决策回写的时候,会发现我们主要用的是 Saga 和 TCC 的思路。

好了,事务基础就复习到这里。记住一点:没有银弹。每种方案都有代价,关键是你得清楚自己的场景要什么——是要强一致,还是能接受最终一致?是要高性能,还是能容忍短暂阻塞?想清楚了,选型就不难了。