混用 db 和 tx 会导致部分提交错误:tx.commit() 返回 nil 但仅部分语句生效,根本原因是事务仅绑定单个物理连接,而 db.query()/exec() 会从连接池获取新连接,脱离事务上下文;必须统一使用 tx 对象执行所有操作。

混用 db 和 tx 会导致部分提交
错误现象:tx.Commit() 返回 nil,但查库发现只有部分语句生效。根本原因是:事务只绑定在单个物理连接上,而 db.Query() 或 db.Exec() 每次都可能从连接池拿到新连接,完全脱离当前事务上下文。
必须统一走 tx 对象:
- 所有 SQL 操作用
tx.QueryRow()、tx.Exec()、tx.Prepare() -
tx不是线程安全的,别跨 goroutine 复用;一个请求一个tx,用完即弃 - 别在事务里调
db.Begin()或嵌套tx—— MySQL 不支持嵌套事务,只会静默忽略
SELECT FOR UPDATE 必须配 context 超时与隔离级别
行级锁不是万能的。MySQL 默认 innodb_lock_wait_timeout=50s,而 Go 层若没设 context 超时,协程会卡死,拖垮整个连接池。
正确写法:
- 开启事务时必须用
db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead}) -
ctx超时建议设为 3–5 秒,比 DB 层锁等待时间短,避免“Go 放弃了,DB 还在等” -
SELECT FOR UPDATE只在 InnoDB 有效,MyISAM 不支持;测试环境别用错引擎 - WHERE 条件尽量走主键或唯一索引,锁住的行数越多,死锁概率越高
跨服务双写不能靠 tx.Commit() 回滚
database/sql 的 tx.Commit() 对第二个数据库完全无效。两个 *sql.DB 实例之间没有事务协调能力。硬写双写失败后手动回滚第一个库,既不可靠也不幂等。
务实路径:
- 先在本地库写操作日志(
status=pending),再发消息(Kafka / RabbitMQ) - 消费端成功后,再更新日志
status=done - 每个写入步骤带幂等 key(如
order_id + step_name),重试时不重复扣款 - 独立对账服务定时扫
status=pending超过 5 分钟的记录,触发补偿或告警
缓存更新顺序不能改,删缓存失败要解耦处理
go-zero 中 cachedConn 硬编码为“先更新 DB,再删缓存”,不支持配置。这个顺序之所以安全,是因为即使并发读到旧缓存值,也只是短暂脏读;而“先删缓存再更新 DB”可能导致旧值回填且无法自动收敛。
删缓存失败时,cachedConn 不重试、不抛异常,需上层兜底:
- 禁用方案:在事务内调
redis.Del()—— 若事务回滚,缓存已删但 DB 未改,造成“缓存空、DB 有值”的不可恢复不一致 - 推荐方案:DB 更新成功后,投递
cache_delete消息到 Kafka,由独立 job service 消费并重试 - 轻量替代:用
redis.SetEX写信号 key(如cache:order:123_signal),TTL 设为 300s;另起 goroutine 监听__keyevent@0__:expired事件触发真实删除
SELECT FOR UPDATE 没配 context 超时,会卡死;删缓存失败后直接重试,会放大延迟;双写没落日志,就等于没留退路。一致性不在代码多酷,而在每处失败路径是否可查、可重放、可兜底。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











