从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:放弃分区容错,这在分布式环境不现实
大多数分布式系统选择 AP,也就是放弃强一致性,换取高可用。
两阶段提交(2PC)
传统的两阶段提交试图解决分布式事务问题。
准备阶段:协调者问所有参与者"准备好了吗?"
提交阶段:如果都准备好了,执行提交;否则回滚。
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()
消息投递与业务操作在同一个事务里,保证了消息的可靠投递。
实践中的选择
不是所有场景都需要强一致性:
需要强一致性的:
- 金融交易
- 库存扣减(超卖问题)
- 积分兑换
可以接受最终一致性的:
- 日志统计
- 数据同步
- 消息通知
写在最后
分布式事务这东西,理论和实践是两回事。
理论上 2PC 能解决问题,但实际用起来性能太差。TCC 控制精确,但开发成本高。
最终发现:大多数场景根本不需要强一致性,最终一致性够用。关键是要设计好补偿机制和监控告警,出问题能及时发现和处理。
这次折腾下来,最大的收获是:不要为了一致性牺牲太多可用性和性能。根据业务需求选择合适的方案,而不是追求"完美"。
可用性说明:本文发布于 2018 年 7 月,距今已超过五年。文中涉及的软件版本、接口、下载地址、命令参数和操作界面可能已经发生变化,部分方案在当前环境下可能失效。请结合官方最新文档核对后再操作,生产环境使用前务必先行验证。
版权声明: 本文首发于 指尖魔法屋-从ACID走到BASE:分布式事务笔记(https://blog.thinkmoon.cn/post/23-distributed-transactions-acid-base/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。