go-zero-事务处理过程

在 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 连接归还连接池。