在 go-zero 基于 sqlx 的数据库事务体系中,普通 SQL 执行和事务 SQL 执行的核心区别在于数据库 TCP 连接是否独占。普通单条 SQL 每次都会从连接池随机取用空闲 TCP 连接,执行完立即归还,每次连接不固定;而数据库事务要求多条 SQL 必须在同一条 TCP 连接上执行,才能保证原子性、一致性。整个事务的连接独占、生命周期管控、自动提交回滚,全部依托底层 Tx 结构体实现。
普通 SQL 与事务 SQL 的核心本质区别
绝大多数业务开发者只会使用TransactCtx 编写事务逻辑,但并不清楚 go-zero 事务的底层保障机制。在基于 sqlx 的数据库体系中,二者的本质差异不在语法,而在 TCP 连接复用规则:
普通单条 SQL:每次从连接池随机获取空闲 driverConn,执行完毕立即归还连接池,每次 TCP 连接不固定,无法保证多条 SQL 在同一个连接执行。
事务 SQL:必须保证多条操作复用同一条物理 TCP 连接,全程独占不归还,以此满足事务的原子性、一致性、隔离性,事务结束后才释放连接。
go-zero 所有事务能力,本质都是围绕连接独占 + 生命周期自动管控封装实现,底层完全依托标准库 Tx 结构体完成。
一:Tx结构体
Tx 结构体作用:事务场景中,临时持有并独占一条 TCP 数据库连接,同时管控整个事务生命周期的工具对象。
type Tx struct {
db *DB
dc *driverConn
..............
keepConnOnRollback bool
stmts struct {
sync.Mutex
v []*Stmt
}
cancel func()
ctx context.Context
}
核心字段 dc *driverConn 详解
Tx结构体字段【dc *driverConn】。在*sql.DB 内部维护连接池中,存放大量 *driverConn,每个driverConn 绑定唯一一条 TCP 物理连接。
在非事务场景下,DB 从池子里随机取一个空闲 driverConn,用它内部的TCP 发包执行 SQL,SQL 跑完立刻归还池子,这条 TCP 可被下次请求复用。每次 SQL 可能拿到不同driverConn,对应不同 TCP。
但是在事务场景下,就不能这么用了,从sql.DB对象也是随机地取出一条 driverConn,存在Tx结构体的dc字段,事务内所有 tx.Exec/tx.Query等sql执行只用 tx.dc,整条事务固定同一条 TCP。Commit/Rollback 后才把该 driverConn 归还池子。
二:db.begin方法创建Tx结构体
BeginTx 支持重试机制
go-zero 底层不直接使用原生Begin,而是封装了带重试能力的 BeginTx,解决连接池偶发获取连接失败、坏连接问题。通过 BeginTx 开启事务,核心动作就是从连接池抢占一条专属连接 *driverConn,封装为 Tx 对象。
\core\stores\sqlx\tx.go
func (db *DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error) {
var tx *Tx
var err error
err = db.retry(func(strategy connReuseStrategy) error {
tx, err = db.begin(ctx, opts, strategy)
return err
})
return tx, err
}
db.retry重试策略作用:规避连接池老化、连接失效、临时连接耗尽等偶发异常,提升事务创建成功率。
统一 begin 封装
go-zero 进一步封装了统一的 begin 方法,注册在 commonSqlConn,对外统一创建事务:【 见 go-zero-如何封装数据库使用? 】
func begin(db *sql.DB) (trans, error) {
tx, err := db.Begin()
if err != nil {
return nil, err
}
return txSession{
Tx: tx,
}, nil
}
这里统一三个容易混淆的概念(重点解惑):
原生 *sql.Tx:Go 标准库事务结构体,真实操作事务的底层对象。
trans 接口:go-zero 抽象的事务通用接口,统一事务行为。
txSession:go-zero 包装结构体,内嵌原生 *sql.Tx,适配框架 Session 接口。
为什么事务回调传递 Session 而不是原生 *sql.Tx呢?核心目的:统一抽象,抹平普通DB操作与事务操作的差异,普通 DB 操作中的Session 底层是 sql.DB,而事务操作中的Session 底层是 sql.Tx。这样子用户的业务层只需依赖 Session 接口,无需感知当前是普通SQL还是事务SQL,降低业务侵入性。
三:核心函数transactOnConn 自动事务模板
框架封装了通用事务执行模板 transactOnConn,实现:自动开启事务、执行业务多条 SQL、自动提交 / 回滚、panic 兜底,业务层无需手动写 Commit、Rollback。
func transactOnConn(ctx context.Context, conn *sql.DB, b beginnable,
fn func(context.Context, Session) error) (err error) {
var tx trans
// b 就是 begin函数,创建事务对象,独占一条TCP连接
tx, err = b(conn)
if err != nil {
return
}
defer func() {
if p := recover(); p != nil {
// sql事务执行失败,强制回滚
if e := tx.Rollback(); e != nil {
err = fmt.Errorf("recover from %#v, rollback failed: %w", p, e)
} else {
err = fmt.Errorf("recover from %#v", p)
}
} else if err != nil {
if e := tx.Rollback(); e != nil {
err = fmt.Errorf("transaction failed: %s, rollback failed: %w", err, e)
}
} else {
// sql事务执行成功,提交事务
err = tx.Commit()
}
}()
// 利用事务工具tx ,可以事务执行多条sql语句
return fn(ctx, tx)
}
调用 BeginTx 创建事务(生成 Tx 对象,独占一条 driverConn/TCP),然后执行业务回调函数 fn(此时所有事务 多条SQL insert/update/delete均在 fn 里),最后通过defer统一自动处理提交 / 回滚、panic 捕获兜底,不用吭呲吭吃业务手动写 Commit / Rollback。
四、go-zero 上层封装:TransactCtx 对外事务入口
1:model 层对外方法
func(context.Context, sqlx.Session) error) error {
return m.conn.TransactCtx(ctx, fn)
}
customXsUsersModel是goctl 自动生成的数据库表操作模型结构体,其中之一持有数据库连接实例sql.DB赋值于字段conn。业务层操作从普通sql提速事务多条sql执行时,可以直接通过 customXsUsersModel开启事务,不用手动拿底层 conn。
m.conn就是结构体commonSqlConn,内置 sql.DB、begin 事务方法、熔断器能力。
2:commonSqlConn 事务入口方法
func (db *commonSqlConn) TransactCtx(ctx context.Context, fn func(context.Context, Session) error) (err error) {
ctx, span := startSpan(ctx, "Transact")
defer func() {
endSpan(span, err)
}()
err = db.brk.DoWithAcceptableCtx(ctx, func() error {
return transact(ctx, db, db.beginTx, fn)
}, db.acceptable)
if errors.Is(err, breaker.ErrServiceUnavailable) {
metricReqErr.Inc("Transact", "breaker")
}
return
}
TransactCtx函数,经过熔断器审核通过,调用 transact(),并传入实参,其中db,当前数据库连接实例 db,就是前面说的结构体commonSqlConn; db.beginTx,就是之前的beign函数,用来创建事务对象,即之前所说的Tx结构体; fn:就是业务写的那一堆 insert/update SQL 回调(自定义业务逻辑)
3:中转方法 transact
func transact(ctx context.Context, db *commonSqlConn, b beginnable,
fn func(context.Context, Session) error) (err error) {
conn, err := db.connProv(ctx)
if err != nil {
db.onError(ctx, err)
return err
}
return transactOnConn(ctx, conn, b, fn)
}
db.connProv(ctx)执行后赋值于conn,就是sql.DB对象。然后在transactOnConn函数中,从sql.DB对象中随机地取出一条 driverConn中(conn实参调用begin函数),执行事务多条sql语句,并自动处理事务提交或回滚。
五、整体完整链路总结
业务层调用 model.TransactCtx 开启事务,经过框架熔断器校验、链路追踪埋点,获取底层 sql.DB 真实连接,执行 begin 方法,从连接池取出一条 唯一 driverConn/TCP 连接,创建 Tx 事务对象。事务内所有 SQL 固定复用该 TCP 连接,保证事务一致性,然后执行业务回调中的多条数据库操作,框架自动根据结果提交 / 回滚 /panic 兜底回滚,事务结束之后,TCP 连接归还连接池。