【GORM】事务,嵌套事务,保存点事务的使用,简单电商平台go案例

GORM 中的嵌套事务与 SavePoint 事务

1. 嵌套事务

嵌套事务是指一个事务中包含另一个事务。例如,在一个数据库操作中,你可能需要在一个主事务中嵌套执行子事务。

GORM 对嵌套事务的支持:

  • GORM 中通过 DB.Transaction 方法嵌套事务。
  • 内部事务出错时,会回滚到父事务的状态。
  • 如果外部事务提交了,嵌套的事务也会生效。

示例:

go 复制代码
package main

import (
	"fmt"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func main() {
	db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

	// 外层事务
	err := db.Transaction(func(tx *gorm.DB) error {
		// 内层事务
		err := tx.Transaction(func(tx2 *gorm.DB) error {
			if err := tx2.Create(&User{Name: "Nested Transaction"}).Error; err != nil {
				return err // 内层回滚
			}
			return nil // 内层提交
		})

		if err != nil {
			return err // 外层回滚
		}

		if err := tx.Create(&User{Name: "Outer Transaction"}).Error; err != nil {
			return err // 外层回滚
		}

		return nil // 外层提交
	})

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

type User struct {
	ID   uint
	Name string
}

注意:

  • 如果子事务回滚,父事务仍然可以正常执行。
  • 子事务的回滚不会影响父事务中的其他操作。

2. SavePoint 事务

SavePoint 是数据库事务的一种控制方式,它允许你在事务中创建检查点(SavePoint),并在发生错误时回滚到特定的检查点,而不是完全回滚整个事务。

GORM 中 SavePoint 的使用:

  • 使用 SavePoint 创建检查点。
  • 使用 RollbackTo 回滚到某个检查点。

示例:

go 复制代码
package main

import (
	"fmt"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func main() {
	db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	tx := db.Begin()

	// SavePoint 检查点
	tx.SavePoint("sp1")

	if err := tx.Create(&User{Name: "First SavePoint"}).Error; err != nil {
		tx.RollbackTo("sp1") // 回滚到 sp1
	}

	// SavePoint 检查点
	tx.SavePoint("sp2")

	if err := tx.Create(&User{Name: "Second SavePoint"}).Error; err != nil {
		tx.RollbackTo("sp2") // 回滚到 sp2
	}

	// 提交事务
	if err := tx.Commit().Error; err != nil {
		fmt.Println("提交失败:", err)
	} else {
		fmt.Println("事务成功提交")
	}
}

type User struct {
	ID   uint
	Name string
}

注意:

  • SavePointRollbackTo 需要底层数据库支持。例如,SQLite 和 MySQL 支持 SavePoint。
  • 如果底层数据库不支持 SavePoint,使用时会报错。

区别和选择
功能 嵌套事务 SavePoint 事务
实现方式 通过 GORM 提供的 Transaction 方法封装。 显式创建检查点并回滚到特定检查点。
适用场景 子事务相对独立,但需要依赖主事务的提交或回滚。 需要在事务中多次设置状态点,灵活回滚到某点。
复杂度 比较简单,直接使用 Transaction 方法嵌套。 较复杂,需要手动管理 SavePoint 和回滚点。

以下是一个企业级案例,一个电商平台需要创建订单并更新库存,同时提供对操作失败的容错机制。

案例背景

  1. 场景描述:

    • 用户提交订单时,需要:
      • 在数据库中创建订单。
      • 扣减库存。
      • 记录操作日志。
    • 如果任意一步失败,需要保证事务的一致性。
  2. 技术需求:

    • 使用嵌套事务确保不同模块的独立性。
    • 使用 SavePoint 处理中间步骤的可回滚。
  3. 数据库表结构:

    • orders 表:存储订单信息。
    • products 表:存储商品库存。
    • logs 表:记录操作日志。

代码实现

go 复制代码
package main

import (
	"errors"
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type Order struct {
	ID         uint   `gorm:"primaryKey"`
	UserID     uint   // 用户ID
	ProductID  uint   // 商品ID
	Quantity   int    // 购买数量
	Status     string // 订单状态
}

type Product struct {
	ID       uint   `gorm:"primaryKey"`
	Name     string
	Stock    int // 库存数量
}

type Log struct {
	ID      uint   `gorm:"primaryKey"`
	Message string // 日志信息
}

func main() {
	// 初始化数据库连接(假设使用 MySQL)
	dsn := "user:password@tcp(127.0.0.1:3306)/ecommerce?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("数据库连接失败")
	}

	// 自动迁移(仅用于演示,实际生产中建议手动管理表结构)
	db.AutoMigrate(&Order{}, &Product{}, &Log{})

	// 调用企业级事务流程
	err = createOrder(db, 1, 1, 2) // 用户 1 购买商品 1,购买数量为 2
	if err != nil {
		fmt.Println("订单创建失败:", err)
	} else {
		fmt.Println("订单创建成功")
	}
}

func createOrder(db *gorm.DB, userID, productID, quantity int) error {
	return db.Transaction(func(tx *gorm.DB) error {
		// 创建 SavePoint 检查点
		tx.SavePoint("start_order")

		// Step 1: 扣减库存
		if err := reduceStock(tx, productID, quantity); err != nil {
			tx.RollbackTo("start_order")
			return err
		}

		// Step 2: 创建订单
		order := Order{
			UserID:    uint(userID),
			ProductID: uint(productID),
			Quantity:  quantity,
			Status:    "Created",
		}
		if err := tx.Create(&order).Error; err != nil {
			tx.RollbackTo("start_order")
			return err
		}

		// Step 3: 记录日志
		log := Log{
			Message: fmt.Sprintf("用户 %d 创建订单 %d", userID, order.ID),
		}
		if err := tx.Create(&log).Error; err != nil {
			tx.RollbackTo("start_order")
			return err
		}

		// 提交事务
		return nil
	})
}

func reduceStock(tx *gorm.DB, productID, quantity int) error {
	var product Product
	if err := tx.First(&product, productID).Error; err != nil {
		return errors.New("商品不存在")
	}

	if product.Stock < quantity {
		return errors.New("库存不足")
	}

	// 扣减库存
	product.Stock -= quantity
	if err := tx.Save(&product).Error; err != nil {
		return errors.New("库存更新失败")
	}

	return nil
}

功能讲解

  1. 事务嵌套:

    • 订单创建逻辑封装在 createOrder 函数中,库存更新 (reduceStock) 是一个嵌套的子操作。
    • db.Transaction 保证了 reduceStock 和日志记录的原子性。
  2. SavePoint 使用:

    • 在事务开始时设置检查点 SavePoint("start_order")
    • 任意步骤失败时,通过 RollbackTo 回滚到检查点,避免影响全局事务。
  3. 模块化设计:

    • 每个业务逻辑拆分为独立的函数,如 reduceStock 和日志记录。
    • 确保代码可读性和可维护性。
  4. 数据库层的幂等性:

    • 通过事务机制保证多次调用不会产生脏数据。
    • 在高并发场景下可结合数据库锁进一步优化。

扩展建议

  1. 并发控制:

    • 针对库存扣减,可以结合数据库的乐观锁或悲观锁机制,避免超卖。
  2. 日志的异步化:

    • 日志记录可以通过消息队列(如 Kafka 或 RabbitMQ)异步处理,减少事务时长。
  3. 监控和告警:

    • 在生产环境中,可通过 APM 工具(如 Prometheus 或 Jaeger)监控事务耗时和失败情况。

练习案例

go 复制代码
package _case

import (
	"gorm.io/gorm"
	"log"
)

// Transaction 事务
func Transaction() {
	t := Teacher{
		Name:   "nick",
		Age:    40,
		Salary: 12345.123,
		Email:  "nick@gmail.com",
	}
	c := Course{
		Name:  "golang",
		Price: 12345.1234,
	}
	DB.Transaction(func(tx *gorm.DB) error {
		if err := tx.Create(&t).Error; err != nil {
			return err
		}
		c.UserID = t.ID
		if err := tx.Create(&c).Error; err != nil {
			return err
		}
		return nil
	})
}

// NestTransaction 嵌套事务指的是再一个外部事务内启动一或者多个内部事务,
// 每个内部事务可以独立回滚
// 外部事务提交成功,内部事务才能成功
// 内部事务失败,不影响其他同级或者高级事务,但是外部事物回滚,所有内部也会回滚
// 嵌套事务不支持预编译(Prepared Statements)
func NestTransaction() {
	t := Teacher{
		Name:   "nick",
		Age:    40,
		Salary: 12345.123,
		Email:  "nick@gmail.com",
	}
	t1 := Teacher{
		Name:   "king",
		Age:    40,
		Salary: 12345.123,
		Email:  "nick@gmail.com",
	}
	t2 := Teacher{
		Name:   "mark",
		Age:    40,
		Salary: 12345.123,
		Email:  "nick@gmail.com",
	}
	DB.Transaction(func(tx *gorm.DB) error {
		if err := tx.Create(&t).Error; err != nil {
			return err
		}
		tx.Transaction(func(tx1 *gorm.DB) error {
			if err := tx1.Create(&t1).Error; err != nil {
				return err
			}
			return nil
		})
		tx.Transaction(func(tx2 *gorm.DB) error {
			if err := tx2.Create(&t2).Error; err != nil {
				return err
			}
			return nil
		})
		return nil
	})
}

// ManualTransaction 手动事务,模拟平常手写事务
func ManualTransaction() {
	t := Teacher{
		Name:   "nick",
		Age:    40,
		Salary: 12345.123,
		Email:  "nick@gmail.com",
	}
	c := Course{
		Name:  "golang",
		Price: 12345.1234,
	}

	tx := DB.Begin()
	defer func() { // 确保在发生未处理的 panic 时,能够及时回滚事务,避免数据不一致的问题。
		if err := recover(); err != nil {
			tx.Rollback()
		}
	}()
	if tx.Error != nil {
		log.Fatalln(tx.Error)
		return
	}
	if err := tx.Create(&t).Error; err != nil {
		tx.Rollback()
		return
	}
	c.UserID = t.ID
	if err := tx.Create(&c).Error; err != nil {
		tx.Rollback()
		return
	}
	tx.Commit()
}

// SavePointTransaction 保存点事务,即事务中可以创建多个保存点,可以回滚到指定保存点
func SavePointTransaction() {
	t := Teacher{
		Name:   "nick",
		Age:    40,
		Salary: 12345.123,
		Email:  "nick@0voice.com",
	}
	c := Course{
		Name:  "golang 云原生",
		Price: 12345.1234,
	}

	tx := DB.Begin()
	defer func() {
		if err := recover(); err != nil {
			tx.Rollback()
		}
	}()
	if tx.Error != nil {
		log.Fatal(tx.Error)
		return
	}

	if err := tx.Create(&t).Error; err != nil {
		tx.Rollback()
		return
	}
	tx.SavePoint("teacher") // SavePoint 设置回滚点
	c.UserID = t.ID
	if err := tx.Create(&c).Error; err != nil {
		tx.RollbackTo("teacher") // 指定回滚到
		return
	}
	tx.Commit()
}

https://github.com/0voice

相关推荐
java1234_小锋2 分钟前
Java中如何安全地停止线程?
java·开发语言
siy23334 分钟前
[c语言日寄]结构体的使用及其拓展
c语言·开发语言·笔记·学习·算法
Archy_Wang_118 分钟前
ASP.NET Core 中的 JWT 鉴权实现
后端·ui·asp.net
Archy_Wang_122 分钟前
ASP.NET Core中 JWT 实现无感刷新Token
后端·asp.net
行路见知26 分钟前
3.1 Go函数调用过程
golang
一只会飞的猪_30 分钟前
国密加密golang加密,java解密
java·开发语言·golang
m0_7482309438 分钟前
SpringBoot实战(三十二)集成 ofdrw,实现 PDF 和 OFD 的转换、SM2 签署OFD
spring boot·后端·pdf
雾里看山39 分钟前
【MySQL】数据库基础知识
数据库·笔记·mysql·oracle
四念处茫茫44 分钟前
【C语言系列】深入理解指针(2)
c语言·开发语言·visual studio
好像是个likun1 小时前
spring Ioc 容器的简介和Bean之间的关系
java·后端·spring