分布式事务解决方案:最佳实践(零基础入门)
大家好,我是一名开源项目维护者,也是一名后端讲师。在过去的几年里,我参与和维护了多个高并发、高可用的 Go 语言后端系统。我当初学分布式事务的时候,被“两阶段提交”、“TCC”、“Saga”这些术语搞得晕头转向,文档晦涩难懂,示例又脱离实际产品场景。今天,我就用一个真实的电商下单案例,手把手带你理解分布式事务的核心思想,并用 Go 语言写出可运行的代码。
一、什么是分布式事务?为什么要关心它?
想象一下你正在开发一个电商产品:
- 用户点击“下单”
- 前端发送请求到订单服务(创建订单)
- 同时要扣减库存服务(减少商品库存)
- 还要通知积分服务(增加用户积分)
这三个操作必须全部成功,或者全部失败。如果只扣了库存但没生成订单,用户就“白拿”了商品!这种跨多个服务的数据一致性问题,就是分布式事务要解决的。
💡 关键点:单体应用用数据库事务就能搞定;微服务架构下,每个服务有自己的数据库,传统事务失效,必须用分布式事务方案。
二、环境准备
我们用 Go 构建一个极简 demo,只需以下工具:
| 工具 | 版本要求 | 安装方式 |
|---|---|---|
| Go | ≥1.20 | 官网下载 https://golang.org/dl/ |
| Docker | 最新稳定版 | 官网安装 |
| MySQL | 8.0 | docker run -d -p 3306:3306 --name mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0 |
创建项目结构:
mkdir distributed-tx-demo
cd distributed-tx-demo
go mod init demo
三、核心概念:三种主流方案对比
分布式事务没有银弹,但有适合不同场景的方案:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 2PC(两阶段提交) | 协调者先问所有参与者“能不能提交”,都同意后再正式提交 | 强一致性 | 性能差、阻塞风险高 | 金融等强一致场景 |
| TCC(Try-Confirm-Cancel) | 业务层实现预留/确认/取消逻辑 | 灵活、性能好 | 开发复杂 | 订单、支付等核心业务 |
| Saga | 每个服务执行本地事务,失败时逐个补偿 | 易实现、高吞吐 | 最终一致性、补偿逻辑复杂 | 长流程业务(如物流) |
🚨 新手误区:不要一上来就用 2PC!它在高并发下容易成为瓶颈。TCC 和 Saga 更适合大多数互联网产品。
四、实战:用 TCC 模式实现下单功能(Go 示例)
我们模拟一个简化版电商下单流程:
- 订单服务:创建“待支付”订单
- 库存服务:冻结商品库存
步骤 1:定义服务接口
// order_service.go
package main
type OrderService struct{}
// Try: 预创建订单(状态为"冻结")
func (s *OrderService) TryCreateOrder(userID, productID int) error {
// 插入订单表,status = 'frozen'
return nil
}
// Confirm: 真正创建订单
func (s *OrderService) ConfirmOrder(orderID int) error {
// 更新订单 status = 'paid'
return nil
}
// Cancel: 取消订单
func (s *OrderService) CancelOrder(orderID int) error {
// 删除或标记订单为'cancelled'
return nil
}
// inventory_service.go
package main
type InventoryService struct{}
// Try: 冻结库存
func (s *InventoryService) TryFreezeStock(productID, quantity int) error {
// 检查可用库存 >= quantity
// 减少 available_stock,增加 frozen_stock
return nil
}
// Confirm: 扣减冻结库存
func (s *InventoryService) ConfirmStock(productID, quantity int) error {
// frozen_stock -= quantity
return nil
}
// Cancel: 释放冻结库存
func (s *InventoryService) CancelStock(productID, quantity int) error {
// available_stock += quantity
// frozen_stock -= quantity
return nil
}
步骤 2:实现 TCC 协调器(核心逻辑)
// tcc_coordinator.go
package main
import "errors"
type TCCTransaction struct {
OrderSvc *OrderService
InventorySvc *InventoryService
}
func (t *TCCTransaction) PlaceOrder(userID, productID, quantity int) error {
// Step 1: Try 阶段 - 所有服务尝试预留资源
err := t.OrderSvc.TryCreateOrder(userID, productID)
if err != nil {
return errors.New("order try failed")
}
err = t.InventorySvc.TryFreezeStock(productID, quantity)
if err != nil {
// 如果库存冻结失败,必须回滚订单的 Try
t.OrderSvc.CancelOrder( /* 假设这里能获取到orderID */ )
return errors.New("inventory try failed")
}
// Step 2: Confirm 阶段 - 提交所有操作
// 注意:Confirm 必须保证幂等!
t.OrderSvc.ConfirmOrder( /* orderID */ )
t.InventorySvc.ConfirmStock(productID, quantity)
return nil
}
🔑 避坑指南:
- 幂等性:Confirm/Cancel 可能被重复调用(网络超时重试),必须设计成多次执行效果相同。
- 异步补偿:生产环境中,Confirm/Cancel 失败应加入重试队列,而非直接报错。
步骤 3:前端如何调用?
虽然本文聚焦后端,但前端也需要配合:
// 前端发起下单请求
fetch('/api/place-order', {
method: 'POST',
body: JSON.stringify({ userId: 123, productId: 456, quantity: 1 })
})
.then(res => {
if (res.ok) {
alert('下单成功!');
} else {
alert('下单失败,请重试');
// 前端无需关心具体哪个服务失败
}
});
💡 产品视角:对用户来说,要么看到“下单成功”,要么看到“失败”,绝不能出现“部分成功”的中间状态。
五、新手常见问题解答
Q1:为什么不用数据库事务直接跨库?
A:MySQL 的 InnoDB 事务只能作用于单个数据库实例。微服务中每个服务通常有独立数据库,物理上无法用 BEGIN...COMMIT 跨库。
Q2:TCC 的 Try 阶段失败了怎么办?
A:必须立即执行所有已成功的 Try 操作的 Cancel。例如:订单 Try 成功,库存 Try 失败 → 立即 Cancel 订单。
Q3:Go 中有没有现成的分布式事务框架?
A:有!推荐:
Q4:如何测试分布式事务?
A:用 Chaos Engineering 思路:
- 在 Try 后、Confirm 前 kill 服务进程
- 模拟网络延迟/超时
- 验证最终数据是否一致
六、下一步学习建议
- 动手改造:在 demo 中加入真正的 MySQL 操作,用
gorm实现数据库读写 - 引入消息队列:用 RabbitMQ/Kafka 实现 Saga 模式,对比 TCC 优劣
- 阅读源码:研究 DTM 的 TCC 实现,看它如何处理幂等和重试
- 压测验证:用
wrk或hey对下单接口做压力测试,观察 TCC 性能表现
🌟 最后的话:分布式事务是后端工程师的“成人礼”。我当初为了搞懂 TCC,在纸上画了几十遍流程图,还专门写了个玩具框架练手。记住:没有完美的方案,只有合适的权衡。先理解原理,再选择工具,你的产品才能既稳定又高效。
祝你编码愉快!如果有问题,欢迎在开源项目 issue 区讨论。

评论 0