Go + GORM 实现支持嵌套事务的中间件(含事务计数器与日志开关)

1.背景

在使用 GORM 做事务管理时,常见的写法是直接调用:

go 复制代码
db.Transaction(func(tx *gorm.DB) error {
	// 事务逻辑
	return nil
})

但是,这种方式有两个问题:

  1. 嵌套事务问题 :如果在一个事务中再次调用 Transaction,GORM 会新建一个事务,而不是复用已有事务,可能造成事务混乱。
  2. 调试困难:事务调用链嵌套时,很难知道当前处于第几层事务,调试不方便。

为了解决这些问题,我们封装了一个 支持嵌套事务 + 事务计数器 + 日志开关 的中间件插件。

2. 核心思路

  1. 使用 context.Context 存储事务信息

    • 每次开启事务时,将 *gorm.DB 和当前事务层级存入 context
  2. 检测已有事务

    • 如果 context 中已有事务,就直接复用,而不是新开。
  3. 事务计数器

    • 层级 level 从 1 开始,内层每进入一层 ExecTx 就加 1。
  4. 日志开关

    • 通过 debugLog 控制是否输出事务进入/退出的调试日志。

3. 插件实现

go 复制代码
package transaction

import (
	"context"
	"fmt"

	"gorm.io/gorm"
)

// 存储事务信息
type txContext struct {
	tx    *gorm.DB
	level int
}

type contextTxKey struct{}

type TransactionPlugin struct {
	db       *gorm.DB
	debugLog bool // 日志开关
}

// New 创建事务插件
func New(debug bool) *TransactionPlugin {
	return &TransactionPlugin{
		debugLog: debug,
	}
}

func (p *TransactionPlugin) Name() string {
	return "transaction_plugin"
}

func (p *TransactionPlugin) Initialize(db *gorm.DB) error {
	p.db = db
	return nil
}

// ExecTx 在事务中执行函数(支持嵌套事务 + 事务计数器)
func (p *TransactionPlugin) ExecTx(ctx context.Context, fn func(ctx context.Context) error) error {
	// 检查是否已有事务
	if txData, ok := ctx.Value(contextTxKey{}).(txContext); ok {
		newCtx := context.WithValue(ctx, contextTxKey{}, txContext{
			tx:    txData.tx,
			level: txData.level + 1,
		})
		p.logf("[事务插件] 进入嵌套事务,层级: %d", txData.level+1)
		return fn(newCtx)
	}

	// 开启新事务
	return p.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		newCtx := context.WithValue(ctx, contextTxKey{}, txContext{
			tx:    tx,
			level: 1,
		})
		p.logf("[事务插件] 开启事务,层级: 1")
		err := fn(newCtx)
		if err != nil {
			p.logf("[事务插件] 事务回滚,层级: 1")
		} else {
			p.logf("[事务插件] 事务提交,层级: 1")
		}
		return err
	})
}

// GetDB 根据 ctx 获取事务 DB
func GetDB(ctx context.Context, fallback *gorm.DB) *gorm.DB {
	if txData, ok := ctx.Value(contextTxKey{}).(txContext); ok {
		return txData.tx
	}
	return fallback.Session(&gorm.Session{})
}

// logf 日志输出(根据 debugLog 控制)
func (p *TransactionPlugin) logf(format string, args ...interface{}) {
	if p.debugLog {
		fmt.Printf(format+"\n", args...)
	}
}

4.使用示例

go 复制代码
package main

import (
	"context"
	"fmt"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"

	"your_project/transaction"
)

type User struct {
	ID   uint
	Name string
}

func main() {
	dsn := "root:password@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	// 创建事务插件(true=调试模式,false=生产模式)
	txPlugin := transaction.New(true)
	db.Use(txPlugin)

	err := txPlugin.ExecTx(context.Background(), func(ctx context.Context) error {
		tx := transaction.GetDB(ctx, db)
		if err := tx.Create(&User{Name: "Tom"}).Error; err != nil {
			return err
		}

		// 嵌套事务
		return txPlugin.ExecTx(ctx, func(ctx context.Context) error {
			tx2 := transaction.GetDB(ctx, db)
			if err := tx2.Create(&User{Name: "Jerry"}).Error; err != nil {
				return err
			}

			// 再嵌套一层
			return txPlugin.ExecTx(ctx, func(ctx context.Context) error {
				tx3 := transaction.GetDB(ctx, db)
				return tx3.Create(&User{Name: "Spike"}).Error
			})
		})
	})

	if err != nil {
		fmt.Println("事务失败:", err)
	} else {
		fmt.Println("事务成功")
	}
}

5.运行效果

开启调试模式(debug=true

ini 复制代码
[事务插件] 开启事务,层级: 1
[事务插件] 进入嵌套事务,层级: 2
[事务插件] 进入嵌套事务,层级: 3
[事务插件] 事务提交,层级: 1
事务成功

关闭调试模式(debug=false

复制代码
事务成功

6.执行流程图

ini 复制代码
ExecTx(level=1)  ──> 开启事务
    ├── ExecTx(level=2) ──> 复用事务
    │       └── ExecTx(level=3) ──> 复用事务
    └── 提交 / 回滚(只在 level=1 处理)
相关推荐
码事漫谈11 分钟前
《C语言点滴》——笑着入门,扎实成长
后端
Tony Bai28 分钟前
【Go模块构建与依赖管理】09 企业级实践:私有仓库与私有 Proxy
开发语言·后端·golang
咖啡教室1 小时前
每日一个计算机小知识:ICMP
后端·网络协议
间彧1 小时前
OpenStack在混合云架构中通常扮演什么角色?
后端
咖啡教室1 小时前
每日一个计算机小知识:IGMP
后端·网络协议
间彧1 小时前
云原生技术栈中的核心组件(如Kubernetes、Docker)具体是如何协同工作的?
后端
清空mega1 小时前
从零开始搭建 flask 博客实验(3)
后端·python·flask
努力的小郑2 小时前
Elasticsearch 避坑指南:我在项目中总结的 14 条实用经验
后端·elasticsearch·性能优化
August_._2 小时前
【MySQL】SQL语法详细总结
java·数据库·后端·sql·mysql·oracle
间彧2 小时前
云原生,与云计算、云服务的区别与联系
后端