当前位置:首页 > 文章列表 > Golang > Go问答 > Go database/sql Tx把事务边界放到业务操作外层的设计方法

Go database/sql Tx把事务边界放到业务操作外层的设计方法

来源:17golang原创 2026-09-15 22:31:26 0浏览 收藏

我在给“创建订单”补库存扣减时,最容易踩的坑不是 SQL 写错,而是每个仓储函数都觉得自己应该负责事务。结果是订单已经写入,库存失败后却没有统一回滚。Go database/sql 更稳妥的做法是:把一个业务用例需要的原子写入放在外层,用同一个 *sql.Tx 传给内部操作,最后由外层统一决定提交还是回滚。

官方文档:https://pkg.go.dev/database/sql

要点速览
  • 事务边界跟着业务用例走,不跟着某一条 SQL 走。
  • 仓储函数只使用传入的 Tx,错误交给外层处理。
  • Commit 失败不能简单当成“肯定没写入”,应按未知状态处理。

一、先把事务边界画在业务用例上

先列出必须一起成功的数据库动作。订单主表写入和库存扣减属于同一原子单元,就由 CreateOrder 创建事务;发短信、调用支付或写消息队列则不要硬塞进数据库事务,它们需要各自的可靠投递策略。

Go database/sql Tx 在业务用例、订单写入和库存扣减之间的静态结构说明图
图1:业务用例、Tx 与订单表/库存表之间的静态结构说明图,帮助确认事务边界属于用例外层。

边界确定后,外层函数只做四件事:创建 Tx、调用业务写入、处理错误、提交成功结果。这样评审时能直接看见“谁拥有事务”。

二、让仓储函数只接收 Tx 并返回错误

内部函数不要再调用 db.Begin,也不要自行 Commit。它们接收 *sql.Tx 和请求上下文,只执行属于自己的 SQL,并把错误返回给业务层。下面的例子把两次写入放在同一个事务对象上:

// CreateOrder 负责业务事务的生命周期,保证订单和库存一起提交。
func (s *Store) CreateOrder(ctx context.Context, orderID, sku string, qty int) error {
	// 用请求上下文创建事务,超时或取消可以传递到数据库驱动。
	tx, err := s.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	// 回滚是安全兜底;成功提交后它通常只会返回 sql.ErrTxDone。
	defer tx.Rollback()

	if err := insertOrder(ctx, tx, orderID, sku, qty); err != nil {
		return err
	}
	if err := deductStock(ctx, tx, sku, qty); err != nil {
		return err
	}
	// 提交只放在所有业务写入都成功之后。
	return tx.Commit()
}

// insertOrder 只负责订单写入,不拥有事务。
func insertOrder(ctx context.Context, tx *sql.Tx, orderID, sku string, qty int) error {
	_, err := tx.ExecContext(ctx,
		"INSERT INTO orders(id, sku, quantity) VALUES (?, ?, ?)",
		orderID, sku, qty,
	)
	return err
}

// deductStock 只负责库存写入,失败时把决定权交回 CreateOrder。
func deductStock(ctx context.Context, tx *sql.Tx, sku string, qty int) error {
	_, err := tx.ExecContext(ctx,
		"UPDATE stock SET quantity = quantity - ? WHERE sku = ? AND quantity >= ?",
		qty, sku, qty,
	)
	return err
}
Go Tx、仓储函数和 ExecContext 连接订单表与库存表的静态关系说明图
图2:Tx、仓储函数、ExecContext 与订单表/库存表之间的静态关系说明图,突出底层函数只执行 SQL 并返回错误。

这里的 defer tx.Rollback() 不是把回滚和提交并行执行,而是给提前返回留一个出口。提交成功后事务已经结束,兜底回滚不会再次撤销已提交的数据。

三、把提交与回滚写成一个清晰的出口

BeginTx 失败时还没有可用事务,直接返回即可;任一内部操作失败,函数提前返回并由 defer 回滚;只有全部写入成功才调用一次 Commit。不要在仓储函数里“顺手提交”,否则外层无法保证多个动作的原子性。

尤其要单独看待 Commit 错误:网络中断、驱动异常或数据库状态变化都可能让客户端无法确认最终结果。此时不要立即把同一组写入无条件重试,否则可能制造重复订单。应记录业务标识,查询订单和库存状态,再按业务幂等规则决定补偿。

位置允许做什么不应做什么
业务用例外层BeginTx、统一错误出口、Commit把外部慢调用放进长事务
仓储函数Tx.ExecContext、参数校验、返回 error自行 Begin、Commit 或吞掉错误
Commit 失败分支记录标识、查状态、走幂等补偿盲目重复提交整组写入

四、用边界清单复查并发与资源行为

事务越短,连接被占用的时间通常越可控。把计算、文件操作和外部 HTTP 请求放在事务外,事务内只保留必要的数据库读写。所有 SQL 使用同一个 context.Context,超时或取消后让驱动有机会终止操作;驱动是否支持取消仍需结合具体驱动文档判断。

最后做一次代码清单:是否只有用例层拥有 *sql.Tx 生命周期;是否每条 SQL 错误都返回;是否有 defer Rollback 兜底;是否把 Commit 错误记录为未知状态;提交后是否改用 *sql.DB 做普通查询。若这些答案都明确,事务边界就不会随着仓储函数继续扩散。

相关问题

为什么不用每个仓储函数自己开启事务?

因为多个写入可能需要同一原子单元。函数各自开事务时,外层无法让它们一起提交或一起回滚。

提交成功后还需要调用 Rollback 吗?

可以保留 defer 作为失败兜底;提交后事务已结束,回滚不会把已提交结果撤回,返回值也不应再被当作业务失败。

Commit 返回错误应该怎么重试?

先按业务主键查询最终状态,再依据幂等约束和补偿策略处理,不要对整段写入直接盲重试。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
专业视频创作者第一次用LibTV怎么做完整短片?从镜头清单到交付复盘专业视频创作者第一次用LibTV怎么做完整短片?从镜头清单到交付复盘
上一篇
专业视频创作者第一次用LibTV怎么做完整短片?从镜头清单到交付复盘
Redis Lua 脚本返回结构化状态码避免业务歧义的实现方法
下一篇
Redis Lua 脚本返回结构化状态码避免业务歧义的实现方法
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    43次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    140次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    75次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    42次使用
  • CMMLU中文大模型评估基准:功能、使用教程与应用场景解析
    CMMLU
    深入了解CMMLU中文评估基准,涵盖67个学科主题,提供数据集下载、Zero-shot/Five-shot评估方法及排行榜,助力优化中文语言模型性能。
    27次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码