Buffalo 不自动回滚事务,必须显式调用 tx.Rollback() 并配合 recover 捕获 panic,且需在 commit 前手动 rollback 错误分支;pop.Transaction 中间件不处理 rollback,defer tx.Rollback() 单独使用无效;连接池未关闭会导致 rollback 失效,db.Close() 与 tx.Rollback() 必须成对使用。

Buffalo 本身不自动回滚事务,必须显式控制 —— 否则 panic、数据不一致、连接泄漏都可能发生。
pop.Transaction 中间件不会自动 rollback
Buffalo 默认启用 pop.Transaction 中间件,它会在每个请求开始时调用 tx := db.Transaction(),但只在 HTTP 响应成功(2xx)时 commit,其余情况一律不 rollback。这意味着:
- handler 中 panic 或返回 500,事务仍处于 open 状态,连接卡在 pool 里
- 数据库层可能靠超时回滚(如 PostgreSQL 的 idle_in_transaction_session_timeout),但时间不可控(默认 1 小时)
- 若用了
db.Transaction()手动开启事务,更不会被中间件接管,完全依赖你写defer tx.Rollback()
正确写法:用 defer + 显式判断
所有涉及事务的 handler 必须自己管理生命周期,典型模式是:
func MyHandler(c buffalo.Context) error {
tx, err := c.Tx().Transaction()
if err != nil {
return err
}
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p)
}
}()
// 执行业务逻辑
if err := doSomething(tx); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
关键点:
- 不要依赖
pop.Transaction中间件“兜底” -
defer tx.Rollback()单独写没用 —— 它会在函数结束时执行,不管是否已 commit - 必须在 commit 前手动 rollback 错误分支,且需 recover 捕获 panic
- 若用
c.Tx()(即 pop.Connection),它内部是sql.DB,rollback 不释放连接,只释放事务上下文
测试环境事务隔离靠 :memory: SQLite 不可靠
很多人用 sqlite3://:memory: 做单元测试,但要注意:
- SQLite 的 in-memory DB 是 per-connection 的,
tx.Rollback()只清空当前连接的变更,不影响其他并发 tx - pop v6+ 已移除 SQLite 驱动注册,
buffalo db命令根本无法识别dialect: sqlite3,连 migrate 都失败 - 真正可靠的测试方式是用 PostgreSQL 的
transactional fixtures:每个 test 开启事务,test 结束 rollback,不依赖内存 DB
连接池未关闭会导致 rollback 失效
如果进程退出前没调用 db.Close(),sql.DB 内部的 idle 连接不会立即释放,而事务回滚依赖连接状态清理:
- Kubernetes preStop hook 发送 SIGTERM 后,若没显式
db.Close(),连接可能卡住 180 秒(默认SetConnMaxIdleTime) - 容器重启时旧连接还在,新实例可能因连接数超限被拒绝
- rollback 成功 ≠ 连接释放,二者是不同层级的操作;不 close 连接池,rollback 后的资源仍被占用
最易被忽略的一点:事务回滚不是“开关”,而是连接上下文里的一个状态标记。一旦连接被复用、或连接池提前回收,rollback 的效果就不可见了 —— 所以必须把 db.Close() 和 tx.Rollback() 当作同一套清理流程来写,不能只做一半。











