分布式事务到底怎么选?Go语言实战指南

Debug到怀疑人生
2026-01-28 16:04
阅读 2748

大家好,我是一个开源项目维护者,也写过不少技术文档。最近很多刚入门后端开发的朋友问我:“分布式事务是不是特别难?有没有适合新手的方案?” 我当初学的时候也是一头雾水——看到“两阶段提交”“TCC”“Saga”这些词就发懵。更别提还要结合 Go 语言、微服务架构,甚至还要考虑和爬虫、书籍管理这类业务场景的结合。

所以今天,我决定用最直白的方式,带大家从零开始搞懂分布式事务的核心思路,并通过一个真实的 Go 项目,对比主流解决方案,帮你做出最适合自己的技术选型。这篇文章不讲空理论,全是可运行的代码和实用建议。


什么是分布式事务?为什么需要它?

想象一下你正在开发一个“电子书商城”系统:

  • 用户下单时,要扣减库存(库存服务)
  • 同时要生成订单(订单服务)
  • 还可能要记录用户积分(用户服务)

这三个操作分布在三个不同的服务中。如果只成功了前两个,第三个失败了,那用户付了钱却没拿到积分,系统就“不一致”了。

分布式事务:就是确保多个跨服务、跨数据库的操作,要么全部成功,要么全部失败,保持数据一致性。

在单体应用里,我们可以用数据库的 BEGIN...COMMIT 轻松搞定。但在分布式系统中,每个服务都有自己的数据库,传统事务失效了——这就是我们要解决的问题。


开发环境准备(Go + Docker)

我们用 Go 作为开发语言,因为它简洁、高性能,且生态完善。先准备好以下工具:

  1. Go 1.20+官网安装
  2. Docker + Docker Compose(用于启动多个数据库实例)
  3. 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:补偿事务

把一个大事务拆成多个小事务,每个都有对应的“补偿操作”。比如:

  1. 创建订单 → 补偿:删除订单
  2. 扣库存 → 补偿:加回库存

如果第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);

第六步:测试流程

  1. 启动库存服务:go run inventory/main.go
  2. 启动订单服务:go run order/main.go
  3. 浏览器访问:http://localhost:8080/buy

✅ 成功:订单创建 + 库存-1
❌ 失败(如库存为0):订单自动回滚

这就是最简版的 Saga 模式:失败时手动补偿(删除订单)。


进阶:用消息队列解耦(推荐生产环境使用)

上面的例子是“同步调用”,仍有耦合。更好的方式是:

  1. 订单服务发送“扣库存”消息到 NSQ
  2. 库存服务监听消息并执行
  3. 如果库存服务失败,消息会重试或进入死信队列

这样即使库存服务宕机,消息也不会丢,系统最终一致。


新手常见问题解答

Q1:分布式事务会不会影响性能?

会!尤其是 2PC 会锁住资源。建议:除非银行转账,否则优先考虑最终一致性方案(如 Saga、消息队列)。

Q2:Go 有没有现成的分布式事务框架?

有,但新手慎用:

  • DTM:支持 TCC、Saga、XA,国产优秀项目
  • Seata-Golang:阿里 Seata 的 Go 版

我建议:先手写一遍逻辑,再用框架,否则容易“黑盒”。

Q3:爬虫场景需要分布式事务吗?

一般不需要!爬虫通常是“只读”或“追加写”,很少涉及跨服务一致性。但如果爬虫结果要写入多个业务库(如书籍信息 + 价格库),那就需要考虑了。

Q4:书籍管理系统用哪种方案?

如果是内部系统,用 Saga + 消息队列 足够。如果涉及付费借阅,建议 TCC 保证资金安全。


学习建议与避坑指南

📚 推荐书籍(真的有用!)

  • 《数据密集型应用系统设计》(DDIA):第9章讲分布式事务,深入浅出
  • 《Go语言高级编程》:第8章讲 Go 与数据库事务
  • 《微服务架构设计模式》:第11章专门讲 Saga 模式

⚠️ 我踩过的坑

  1. 不要过度设计:90% 的业务用“本地事务 + 消息队列”就能解决。
  2. 幂等性必须做:补偿操作可能重复执行,确保 DELETE ORDER 多次调用不会出错。
  3. 日志要详细:记录每个步骤的状态,方便排查“卡在哪个环节”。

🔜 下一步学什么?

  1. 学习 DTM 框架,体验声明式事务
  2. 尝试用 Kafka 替代 NSQ,处理更高吞吐
  3. 研究“最大努力通知”模式,用于非关键业务(如发邮件)

结语

分布式事务听起来高大上,但核心思想很简单:要么一起成功,要么一起失败。通过今天的 Go 实战,你应该已经能区分不同方案的适用场景,并动手搭建一个基础系统。

记住:技术选型不是比谁更“先进”,而是看谁更“合适”。就像我当初做第一个开源项目时,用了最简单的补偿机制,反而比强行上 2PC 更稳定。

如果你觉得这篇教程有帮助,欢迎在 GitHub 上给我点个 star!也欢迎留言讨论你的分布式事务困惑。下期我们聊聊《如何用 Go 写一个高性能爬虫》,结合今天学到的事务知识,抓取百万本电子书信息!

评论 0

最热最新
暂无评论
Debug到怀疑人生Lv.1
0
影响力
0
文章
0
粉丝