分布式事务:从“强一致”到“最终一致”,我为什么在微服务里放弃了 2PC?

很多刚接触微服务的同学,包括我刚开始用 Django 构建中小型分布式系统时,都会陷入一个思维定式: "分布式事务不就是 2PC(两阶段提交)吗?要么全成功,要么全失败。"

但在真实的互联网业务场景下,尤其是当你把单体拆成几十个微服务后,你会发现:强一致性的分布式事务,往往是一个"性能毒药"和"架构陷阱"。

今天,我想结合自己的全栈实践,聊聊为什么我们最终都选择了最终一致性,以及 Python(Django)生态下是如何优雅落地这一方案的。


一、为什么 2PC 在微服务里"水土不服"?

我们先简单回顾一下 2PC(两阶段提交)的流程:

  1. 准备阶段:协调者问所有参与者"能不能提交?"
  2. 提交阶段:如果大家都说能,协调者下达"提交"指令;只要有一个人说不能,全体回滚。

听起来很完美,但在微服务架构下,它有三个致命伤:

1. 同步阻塞,性能断崖

2PC 是同步阻塞协议。在提交阶段完成前,所有参与的微服务都必须锁定资源(比如数据库行锁)。在高并发场景下,这会让系统的 TPS(每秒事务处理量)呈指数级下降。

2. 协调者单点故障

如果那个发号施令的"协调者"挂了,所有参与者都会一直等待指令,导致整个系统卡死。虽然可以引入协调者集群,但这又引入了更复杂的选主问题。

3. 不适合长事务

微服务调用链通常很长(A 调 B,B 调 C)。如果 C 服务响应慢,A 和 B 的资源就会被长时间锁定,极易引发死锁

我的思考 :在 Django 单体应用中,@transaction.atomic() 之所以好用,是因为它只涉及一个数据库实例,锁的范围和时间是可控的。一旦跨服务、跨数据库,强一致性的代价就变得无法承受。


二、CAP 定理:我们不得不做的"取舍"

要理解为什么放弃 2PC,就必须提到分布式系统的基石------CAP 定理

  • C (Consistency) :强一致性,所有节点看到的数据时刻保持一致。
  • A (Availability) :可用性,每个请求都能得到响应(不保证是最新数据)。
  • P (Partition tolerance) :分区容错性,网络断了,系统还能跑。

在分布式环境下,网络分区(P)是必然存在的。所以我们只能在 C 和 A 之间做选择:

  • 选择 CP:保证数据绝对一致,但系统可能暂时不可用(如 2PC)。
  • 选择 AP:保证系统时刻可用,但允许数据在短时间内不一致。

对于电商、社交这类互联网应用,可用性(A)通常比强一致性(C)更重要。你愿意为了库存数据的绝对精确,让整个下单页面卡死吗?显然不愿意。

因此,我们选择了 AP ,并引入了 Base 理论 ,用最终一致性来替代强一致性。


三、最终一致性:微服务架构的"妥协"与"智慧"

最终一致性 的核心思想是:不追求数据的实时一致,只要保证在没有新的更新操作后,数据最终会达到一致状态。

这听起来有点"不靠谱",但它是目前微服务架构中最主流、最务实的解决方案。

实现最终一致性的"黄金搭档":消息队列 + 本地消息表

这是我目前在 Django 项目中实践最多、也是 Java 生态(如 RocketMQ)广泛采用的方案。

1. 核心流程(以"创建订单并扣减库存"为例)

步骤 动作 关键点
1 订单服务开启本地事务 在同一个数据库链接里操作
2 创建订单(状态为"待确认") 业务数据入库
3 本地消息表插入一条"扣减库存"消息 核心技巧:消息表和订单表在同一个本地事务中,保证了只要订单创建成功,消息就一定存在。
4 提交本地事务 此时消息还在本地,未发到 MQ
5 后台定时任务扫描本地消息表 找出状态为"待发送"的消息
6 将消息发送到消息队列(MQ) 发送成功后,更新本地消息状态为"已发送"
7 库存服务消费消息,扣减库存 必须保证幂等性(见下文)
8 扣减成功后,回调订单服务,更新订单状态为"已确认" 完成最终一致

2. 为什么一定要"本地消息表"?

如果没有本地消息表,直接发消息到 MQ,可能会出现这种情况:订单数据库事务提交了,但 MQ 由于网络抖动没发出去,导致数据不一致。本地消息表通过"本地事务 + 后台补偿"的机制,保证了消息的可靠性。

3. 幂等性:分布式系统的"安全带"

由于网络重试,消息可能会被重复消费。比如库存服务已经扣减了库存,但发送 ACK 确认给 MQ 时失败了,MQ 会再次推送这条消息。

解决方案

  • 唯一 ID :每条消息生成一个全局唯一的 message_id
  • 去重校验 :库存服务在处理消息前,先检查这个 message_id 是否已经处理过(可以通过 Redis 的 SETNX 或数据库的唯一约束实现)。

四、Django 实战:Celery 与 Redis 的完美配合

在 Python 生态中,我们虽然没有 RocketMQ 那样的分布式事务消息,但可以用 Celery + Redis​ 来模拟这一流程。

  1. 本地事务 :在 Django 的 transaction.atomic() 中,同时创建订单和一条 TaskLog(相当于本地消息表)。
  2. 任务触发:事务提交后,立即触发 Celery 任务(发送消息)。
  3. 重试机制 :Celery 自带 retry 机制,如果发送失败,会自动重试。
  4. 幂等处理 :在 Celery Task 中,通过 Redis 的 SETNXtask_id 进行加锁,防止重复执行。
python 复制代码
# Django 伪代码示例
from django.db import transaction
from celery import shared_task
import redis

@shared_task(bind=True, autoretry_for=(Exception,), retry_backoff=5)
def deduct_inventory_task(self, order_id, message_id):
    # 1. 幂等校验
    if redis_client.get(f"deduct:{message_id}"):
        return "Already processed"
    
    try:
        # 2. 扣减库存逻辑
        Inventory.objects.filter(...).update(count=F('count') - 1)
        # 3. 标记已处理
        redis_client.setex(f"deduct:{message_id}", 3600, "done")
    except Exception as e:
        # 4. 抛出异常,Celery 会自动重试
        raise self.retry(exc=e)

def create_order(request):
    with transaction.atomic():
        order = Order.objects.create(...)
        # 模拟本地消息表
        TaskLog.objects.create(task_id=message_id, status='PENDING')
        # 事务提交后发送任务
        transaction.on_commit(lambda: deduct_inventory_task.delay(order.id, message_id))

五、总结:架构师的权衡之道

分布式事务没有"银弹"。2PC、TCC、Saga 各有适用场景:

  • 2PC:适合传统单体数据库拆分初期,或内部后台管理系统,对性能要求不高。
  • TCC:适合对一致性要求极高、且业务逻辑清晰的金融场景,但开发成本高。
  • 最终一致性(消息队列) :适合高并发、高可用的互联网业务,是目前最主流的方案。

作为开发者,我们不需要死记硬背这些名词,而是要理解它们背后的权衡逻辑

强一致性牺牲了可用性和性能,而最终一致性牺牲了实时性,换取了系统的高并发和弹性。

相关推荐
再吃一根胡萝卜1 小时前
从 Django 到 Spring Cloud:一个全栈开发者的微服务思考
后端
程序员爱钓鱼1 小时前
Go 编程实战:切片 Slice——灵活的动态数据集合
后端·面试·go
再吃一根胡萝卜8 小时前
微服务治理的“四大护法”:从 Django 视角理解 Spring Cloud 核心组件
后端
再吃一根胡萝卜8 小时前
面试终极挑战:如何用 Django 经验,回答 Java 微服务实战问题?
后端
To_OC9 小时前
装完 ESLint 它一声不吭?我还以为代码写得多好
后端·node.js·eslint
凤山老林10 小时前
精细化流量治理:Spring Boot 动态特性开关与灰度发布体系
java·spring boot·后端
vipbic11 小时前
一个前端的 9 天重构:我是怎么用 Codex 重做航栈的
前端·javascript·后端
考虑考虑13 小时前
Excel导入时产生特殊字符处理
java·后端·java ee
IT_陈寒14 小时前
Redis的DEL命令竟然没删掉数据?我踩的这个坑你得知道
前端·人工智能·后端