很多刚接触微服务的同学,包括我刚开始用 Django 构建中小型分布式系统时,都会陷入一个思维定式: "分布式事务不就是 2PC(两阶段提交)吗?要么全成功,要么全失败。"
但在真实的互联网业务场景下,尤其是当你把单体拆成几十个微服务后,你会发现:强一致性的分布式事务,往往是一个"性能毒药"和"架构陷阱"。
今天,我想结合自己的全栈实践,聊聊为什么我们最终都选择了最终一致性,以及 Python(Django)生态下是如何优雅落地这一方案的。
一、为什么 2PC 在微服务里"水土不服"?
我们先简单回顾一下 2PC(两阶段提交)的流程:
- 准备阶段:协调者问所有参与者"能不能提交?"
- 提交阶段:如果大家都说能,协调者下达"提交"指令;只要有一个人说不能,全体回滚。
听起来很完美,但在微服务架构下,它有三个致命伤:
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 来模拟这一流程。
- 本地事务 :在 Django 的
transaction.atomic()中,同时创建订单和一条TaskLog(相当于本地消息表)。 - 任务触发:事务提交后,立即触发 Celery 任务(发送消息)。
- 重试机制 :Celery 自带
retry机制,如果发送失败,会自动重试。 - 幂等处理 :在 Celery Task 中,通过 Redis 的
SETNX对task_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:适合对一致性要求极高、且业务逻辑清晰的金融场景,但开发成本高。
- 最终一致性(消息队列) :适合高并发、高可用的互联网业务,是目前最主流的方案。
作为开发者,我们不需要死记硬背这些名词,而是要理解它们背后的权衡逻辑。
强一致性牺牲了可用性和性能,而最终一致性牺牲了实时性,换取了系统的高并发和弹性。