分布式事务到底怎么选?Go语言实战指南
大家好,我是一个开源项目维护者,也写过不少技术文档。最近很多刚入门后端开发的朋友问我:“分布式事务是不是特别难?有没有适合新手的方案?” 我当初学的时候也是一头雾水——看到“两阶段提交”“TCC”“Saga”这些词就发懵。更别提还要结合 Go 语言、微服务架构,甚至还要考虑和爬虫、书籍管理这类业务场景的结合。
所以今天,我决定用最直白的方式,带大家从零开始搞懂分布式事务的核心思路,并通过一个真实的 Go 项目,对比主流解决方案,帮你做出最适合自己的技术选型。这篇文章不讲空理论,全是可运行的代码和实用建议。
什么是分布式事务?为什么需要它?
想象一下你正在开发一个“电子书商城”系统:
- 用户下单时,要扣减库存(库存服务)
- 同时要生成订单(订单服务)
- 还可能要记录用户积分(用户服务)
这三个操作分布在三个不同的服务中。如果只成功了前两个,第三个失败了,那用户付了钱却没拿到积分,系统就“不一致”了。
分布式事务:就是确保多个跨服务、跨数据库的操作,要么全部成功,要么全部失败,保持数据一致性。
在单体应用里,我们可以用数据库的 BEGIN...COMMIT 轻松搞定。但在分布式系统中,每个服务都有自己的数据库,传统事务失效了——这就是我们要解决的问题。
开发环境准备(Go + Docker)
我们用 Go 作为开发语言,因为它简洁、高性能,且生态完善。先准备好以下工具:
- Go 1.20+(官网安装)
- Docker + Docker Compose(用于启动多个数据库实例)
- VS Code 或 Goland(推荐安装 Go 插件)
启动两个独立的 MySQL 实例
创建 docker-compose.yml:
version: '3'
services:
mysql-order:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: order_db
ports:
- "3306:3306"
command: --default-authentication-plugin=mysql_native_password
mysql-inventory:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: inventory_db
ports:
- "3307:3306"
command: --default-authentication-plugin=mysql_native_password
运行:
docker-compose up -d
现在你有两个独立的数据库:
localhost:3306→ 订单库localhost:3307→ 库存库
核心概念:四种主流方案对比
分布式事务没有“银弹”,不同场景适合不同方案。下面我用一张表帮你理清思路:
| 方案 | 全称 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 2PC | 两阶段提交 | 强一致 | 低 | 高 | 金融、支付等强一致性要求 |
| TCC | Try-Confirm-Cancel | 最终一致 | 中 | 高 | 高并发订单、库存 |
| Saga | 长活事务 | 最终一致 | 高 | 中 | 业务流程长(如电商下单) |
| 消息队列 | 基于可靠消息 | 最终一致 | 高 | 低 | 日志、通知、积分等弱一致性 |
💡 我当初学的时候,以为必须用 2PC,结果发现大多数业务用 Saga 或消息队列就够了!
重点解释 TCC 和 Saga(新手最常用)
TCC:三步走
- Try:预留资源(如冻结库存)
- Confirm:真正执行(扣减库存)
- Cancel:释放资源(解冻库存)
优点:性能较好,可控性强。
缺点:每个业务都要写三套逻辑,开发成本高。
Saga:补偿事务
把一个大事务拆成多个小事务,每个都有对应的“补偿操作”。比如:
- 创建订单 → 补偿:删除订单
- 扣库存 → 补偿:加回库存
如果第3步失败,就依次执行前面的补偿。
优点:实现简单,天然支持长流程。
缺点:补偿逻辑可能复杂(比如“已发货”怎么补偿?)。
实战:用 Go 实现一个书籍商城下单流程
我们模拟一个“买书”场景:
- 用户购买一本《Go语言实战》
- 需要:创建订单 + 扣减库存
我们将用 Saga 模式 + 消息队列 实现,因为这是对新手最友好的方案。
第一步:定义服务结构
bookstore/
├── order/ # 订单服务
│ ├── main.go
│ └── db.go
├── inventory/ # 库存服务
│ ├── main.go
│ └── db.go
└── go.mod
第二步:初始化 Go 模块
go mod init bookstore
go get github.com/go-sql-driver/mysql
go get github.com/nsqio/go-nsq # 轻量级消息队列
第三步:订单服务(order/main.go)
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
_ "github.com/go-sql-driver/mysql"
)
var orderDB *sql.DB
func init() {
var err error
orderDB, err = sql.Open("mysql", "root:root@tcp(localhost:3306)/order_db")
if err != nil {
log.Fatal(err)
}
}
func createOrder(w http.ResponseWriter, r *http.Request) {
// 1. 创建订单
_, err := orderDB.Exec("INSERT INTO orders (book_id, user_id) VALUES (?, ?)", 101, 123)
if err != nil {
http.Error(w, "创建订单失败", 500)
return
}
// 2. 发送“扣库存”消息(关键!)
// 这里简化:直接调用库存服务(实际应通过消息队列)
resp, err := http.Post("http://localhost:8081/decrease", "text/plain", nil)
if err != nil || resp.StatusCode != 200 {
// 如果失败,补偿:删除订单
orderDB.Exec("DELETE FROM orders WHERE book_id = ? AND user_id = ?", 101, 123)
http.Error(w, "下单失败,已回滚", 500)
return
}
fmt.Fprintf(w, "下单成功!")
}
func main() {
http.HandleFunc("/buy", createOrder)
log.Println("订单服务启动在 :8080")
http.ListenAndServe(":8080", nil)
}
第四步:库存服务(inventory/main.go)
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
_ "github.com/go-sql-driver/mysql"
)
var invDB *sql.DB
func init() {
var err error
invDB, err = sql.Open("mysql", "root:root@tcp(localhost:3307)/inventory_db")
if err != nil {
log.Fatal(err)
}
}
func decreaseInventory(w http.ResponseWriter, r *http.Request) {
// 模拟扣库存
_, err := invDB.Exec("UPDATE books SET stock = stock - 1 WHERE id = 101 AND stock > 0")
if err != nil {
http.Error(w, "库存不足", 400)
return
}
fmt.Fprintf(w, "库存扣减成功")
}
func main() {
http.HandleFunc("/decrease", decreaseInventory)
log.Println("库存服务启动在 :8081")
http.ListenAndServe(":8081", nil)
}
第五步:初始化数据库表
-- order_db
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
book_id INT,
user_id INT
);
-- inventory_db
CREATE TABLE books (
id INT PRIMARY KEY,
name VARCHAR(100),
stock INT
);
INSERT INTO books VALUES (101, 'Go语言实战', 10);
第六步:测试流程
- 启动库存服务:
go run inventory/main.go - 启动订单服务:
go run order/main.go - 浏览器访问:
http://localhost:8080/buy
✅ 成功:订单创建 + 库存-1
❌ 失败(如库存为0):订单自动回滚
这就是最简版的 Saga 模式:失败时手动补偿(删除订单)。
进阶:用消息队列解耦(推荐生产环境使用)
上面的例子是“同步调用”,仍有耦合。更好的方式是:
- 订单服务发送“扣库存”消息到 NSQ
- 库存服务监听消息并执行
- 如果库存服务失败,消息会重试或进入死信队列
这样即使库存服务宕机,消息也不会丢,系统最终一致。
新手常见问题解答
Q1:分布式事务会不会影响性能?
会!尤其是 2PC 会锁住资源。建议:除非银行转账,否则优先考虑最终一致性方案(如 Saga、消息队列)。
Q2:Go 有没有现成的分布式事务框架?
有,但新手慎用:
- DTM:支持 TCC、Saga、XA,国产优秀项目
- Seata-Golang:阿里 Seata 的 Go 版
我建议:先手写一遍逻辑,再用框架,否则容易“黑盒”。
Q3:爬虫场景需要分布式事务吗?
一般不需要!爬虫通常是“只读”或“追加写”,很少涉及跨服务一致性。但如果爬虫结果要写入多个业务库(如书籍信息 + 价格库),那就需要考虑了。
Q4:书籍管理系统用哪种方案?
如果是内部系统,用 Saga + 消息队列 足够。如果涉及付费借阅,建议 TCC 保证资金安全。
学习建议与避坑指南
📚 推荐书籍(真的有用!)
- 《数据密集型应用系统设计》(DDIA):第9章讲分布式事务,深入浅出
- 《Go语言高级编程》:第8章讲 Go 与数据库事务
- 《微服务架构设计模式》:第11章专门讲 Saga 模式
⚠️ 我踩过的坑
- 不要过度设计:90% 的业务用“本地事务 + 消息队列”就能解决。
- 幂等性必须做:补偿操作可能重复执行,确保
DELETE ORDER多次调用不会出错。 - 日志要详细:记录每个步骤的状态,方便排查“卡在哪个环节”。
🔜 下一步学什么?
- 学习 DTM 框架,体验声明式事务
- 尝试用 Kafka 替代 NSQ,处理更高吞吐
- 研究“最大努力通知”模式,用于非关键业务(如发邮件)
结语
分布式事务听起来高大上,但核心思想很简单:要么一起成功,要么一起失败。通过今天的 Go 实战,你应该已经能区分不同方案的适用场景,并动手搭建一个基础系统。
记住:技术选型不是比谁更“先进”,而是看谁更“合适”。就像我当初做第一个开源项目时,用了最简单的补偿机制,反而比强行上 2PC 更稳定。
如果你觉得这篇教程有帮助,欢迎在 GitHub 上给我点个 star!也欢迎留言讨论你的分布式事务困惑。下期我们聊聊《如何用 Go 写一个高性能爬虫》,结合今天学到的事务知识,抓取百万本电子书信息!

评论 0