从ACID走到BASE:分布式事务笔记

分布式事务笔记相关的坑,多半出在边界条件上。

单机环境下,事务很简单:

四个特性都满足:

问题出在分布式环境:这些数据可能在不同的数据库、不同的服务里。

单机事务的简单

单机环境下,事务很简单:

BEGIN TRANSACTION;

UPDATE inventory SET count = count - 1 WHERE product_id = 1;
INSERT INTO orders (user_id, product_id) VALUES (100, 1);
UPDATE users SET points = points + 10 WHERE id = 100;

COMMIT;

四个特性都满足:

  • 原子性:要么全做,要么全不做
  • 一致性:数据保持一致状态
  • 隔离性:并发事务互不干扰
  • 持久性:提交后永久保存

问题出在分布式环境:这些数据可能在不同的数据库、不同的服务里。

CAP 定理

Eric Brewer 在 2000 年提出的 CAP 定理告诉我们:一个分布式系统无法同时满足一致性(C)、可用性(A)和分区容错性(P)。

只能三选二:

  • CP:放弃可用性,保证一致性和分区容错
  • AP:放弃强一致性,保证可用性和分区容错
  • CA:放弃分区容错,这在分布式环境不现实
graph TB subgraph CAP定理 A[一致性 C] B[可用性 A] C[分区容错性 P] end subgraph 系统选择 D[CA系统<br/>放弃分区容错] E[CP系统<br/>放弃可用性] F[AP系统<br/>放弃强一致性] end A & B --> D A & C --> E B & C --> F style E fill:#FFB6C1 style F fill:#90EE90

大多数分布式系统选择 AP,也就是放弃强一致性,换取高可用。

两阶段提交(2PC)

传统的两阶段提交试图解决分布式事务问题。

准备阶段:协调者问所有参与者"准备好了吗?"

提交阶段:如果都准备好了,执行提交;否则回滚。

sequenceDiagram participant Coordinator as 协调者 participant P1 as 库存服务 participant P2 as 订单服务 participant P3 as 积分服务 Coordinator->>P1: 准备请求 Coordinator->>P2: 准备请求 Coordinator->>P3: 准备请求 P1->>P1: 执行事务<br/>锁定资源 P2->>P2: 执行事务<br/>锁定资源 P3->>P3: 执行事务<br/>锁定资源 P1-->>Coordinator: 同意 P2-->>Coordinator: 同意 P3-->>Coordinator: 同意 Coordinator->>P1: 提交 Coordinator->>P2: 提交 Coordinator->>P3: 提交 P1->>P1: 提交事务<br/>释放资源 P2->>P2: 提交事务<br/>释放资源 P3->>P3: 提交事务<br/>释放资源

2PC 的问题

实际用下来,2PC 有几个明显问题:

单点故障:协调者挂了,所有参与者都挂着等,资源一直被锁

阻塞协议:等待过程中,所有资源都被锁着,系统吞吐量下降

性能差:需要两轮网络通信,延迟高

对于互联网场景,2PC 基本不可用。

BASE 理论

既然强一致性这么难,那就接受最终一致性。

BASE 理论是针对 AP 系统的:

基本可用:系统保证基本可用,部分功能可能降级

软状态:状态可能随时间变化,允许中间状态

最终一致性:经过一段时间,所有节点最终一致

Saga 模式

Saga 模式通过补偿操作实现最终一致性。

正向操作:执行一系列局部事务

补偿操作:如果某步失败,执行之前步骤的补偿操作

def create_order(user_id, items):
    try:
        # 步骤1:创建订单
        order = OrderService.create(user_id, items)

        # 步骤2:扣库存
        InventoryService.deduct(items)

        # 步骤3:加积分
        UserService.add_points(user_id, order.total)

        # 完成
        order.complete()
        return order

    except Exception as e:
        # 补偿:按相反顺序回滚
        UserService.subtract_points(user_id, order.total)
        InventoryService.add_back(items)
        OrderService.cancel(order.id)

        raise

Saga 的挑战

补偿操作难设计:补偿逻辑本身可能很复杂,甚至需要人工介入

数据可见性:Saga 执行过程中,其他服务可能看到不一致的中间状态

并发问题:多个 Saga 并发执行,可能产生竞争条件

TCC 模式

TCC(Try-Confirm-Cancel)更加强调资源预留:

Try 阶段:检查资源并预留

Confirm 阶段:确认使用预留资源

Cancel 阶段:取消操作,释放预留资源

class OrderTCC:
    def try_reserve(self, order):
        # 预留库存
        inventory.reserve(order.items)
        # 预留资金
        payment.reserve(order.amount)
        # 预留积分
        points.reserve(order.user_id, order.points)

    def confirm(self, order):
        # 确认扣库存
        inventory.confirm_deduct(order.items)
        # 确认扣款
        payment.confirm_deduct(order.amount)
        # 确认加积分
        points.confirm_add(order.user_id, order.points)

    def cancel(self, order):
        # 释放预留
        inventory.release(order.items)
        payment.release(order.amount)
        points.release(order.user_id, order.points)

TCC 的优势是控制粒度细,但开发成本高,每个操作都要实现三套逻辑。

本地消息表

对于不需要强一致性的场景,本地消息表更实用:

def create_order(user_id, items):
    with db.transaction():
        # 创建订单
        order = Order.create(user_id, items)

        # 插入本地消息表
        OutboxMessage.create(
            type='order_created',
            payload={'order_id': order.id}
        )

# 定时任务扫描本地消息表,发送到消息队列
def process_outbox():
    messages = OutboxMessage.unsent()
    for msg in messages:
        try:
            message_queue.publish(msg.type, msg.payload)
            msg.mark_sent()
        except Exception as e:
            # 失败重试
            msg.increment_retry()

消息投递与业务操作在同一个事务里,保证了消息的可靠投递。

实践中的选择

不是所有场景都需要强一致性:

需要强一致性的

  • 金融交易
  • 库存扣减(超卖问题)
  • 积分兑换

可以接受最终一致性的

  • 日志统计
  • 数据同步
  • 消息通知
flowchart TB A[业务场景] --> B{对一致性要求} B -->|极高| C[TCC/2PC] B -->|高| D[Saga] B -->|中| E[本地消息表] B -->|低| F[直接调用] C --> G[金融交易] D --> H[电商订单] E --> I[数据同步] F --> J[日志记录] style C fill:#FFB6C1 style D fill:#FFD700 style E fill:#87CEEB style F fill:#90EE90

写在最后

分布式事务这东西,理论和实践是两回事。

理论上 2PC 能解决问题,但实际用起来性能太差。TCC 控制精确,但开发成本高。

最终发现:大多数场景根本不需要强一致性,最终一致性够用。关键是要设计好补偿机制和监控告警,出问题能及时发现和处理。


这次折腾下来,最大的收获是:不要为了一致性牺牲太多可用性和性能。根据业务需求选择合适的方案,而不是追求"完美"。

可用性说明:本文发布于 2018 年 7 月,距今已超过五年。文中涉及的软件版本、接口、下载地址、命令参数和操作界面可能已经发生变化,部分方案在当前环境下可能失效。请结合官方最新文档核对后再操作,生产环境使用前务必先行验证。

版权声明: 本文首发于 指尖魔法屋-从ACID走到BASE:分布式事务笔记https://blog.thinkmoon.cn/post/23-distributed-transactions-acid-base/) 转载或引用必须申明原指尖魔法屋来源及源地址!