事务
简单介绍一下事务(Transaction),事务指的是一系列操作,作为一个单独的逻辑工作单元来执行,要么完全执行,要么完全不执行。在数据库系统中,事务通常由一组读写操作组成,在这样的系统中,事务具有四个基本特性,即ACID:
- 原子性(Atomicity):事务是一个原子操作,要么全部提交,要么全部回滚,不能只完成其中一部分。
- 一致性(Consistency):事务执行前后,数据库的状态必须保持一致,如果事务执行失败,则需要回滚到事务开始之前的状态。
- 隔离性(Isolation):并发执行的多个事务之间相互隔离,一个事务的执行不会影响其他事务的执行。
- 持久性(Durability):事务执行成功后,其结果必须永久保存到数据库中,即使发生系统故障也不能丢失。
通过使用事务,可以确保数据库的数据的一致性和可靠性,同时也可以防止数据的不一致和错误,提高了数据库系统的可靠性和安全性。
GORM中使用事务
为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,可以在初始化时禁用它,这将获得大约 30%+ 性能提升。
1// 全局禁用
2db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
3 SkipDefaultTransaction: true,
4})
在gorm中使用事务的一般流程如下:
1func CreateAnimals(db *gorm.DB) error {
2 // 开始事务
3 tx := db.Begin()
4 defer func() {
5 if r := recover(); r != nil {
6 // 遇到错误时回滚事务
7 tx.Rollback()
8 }
9 }()
10
11 if err := tx.Error; err != nil {
12 return err
13 }
14
15 // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
16 if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
17 tx.Rollback()
18 return err
19 }
20
21 if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
22 tx.Rollback()
23 return err
24 }
25
26 // 提交事务
27 return tx.Commit().Error
28}
从这个示例看似乎没什么问题,但结合显示的一些业务场景再来看一下会有什么问题。就拿在线购物来说,假设你正在购买一件商品,并且需要进行以下几个操作:
- 将商品添加到购物车中;
- 将订单创建并记录在系统中;
- 从用户账户中扣除相应金额;
- 更新库存。
根据第一个示例可能会这么写:
1func (d *Dao) Shop(product Product) error {
2 // 开始事务
3 tx := d.db.Begin()
4 defer func() {
5 if r := recover(); r != nil {
6 // 遇到错误时回滚事务
7 tx.Rollback()
8 }
9 }()
10
11 if err := tx.Error; err != nil {
12 return err
13 }
14
15 // 添加购物车
16 if err := tx.AddToCart(product).Error; err != nil {
17 tx.Rollback()
18 return err
19 }
20 // 创建订单
21 if err := tx.CreateOrder(product).Error; err != nil {
22 tx.Rollback()
23 return err
24 }
25 // 付款
26 if err := tx.Pay(product).Error; err != nil {
27 tx.Rollback()
28 return err
29 }
30 // 更新库存
31 if err := tx.UpdateInventory(product).Error; err != nil {
32 tx.Rollback()
33 return err
34 }
35
36 // 提交事务
37 return tx.Commit().Error
38}
这样看起来是没什么毛病,但是如果这个用户只添加了购物车呢,或者添加了购物车并创建了订单没有付款呢,又需要组装几个新的实现?这样就太繁琐了,重复的代码就会很多。也有的人会在service
层创建tx传到dao
层,在service层组装需求,类似这样:
1func (s *Service) Shop(product Product) error {
2 // 开始事务
3 tx := s.dao.Db.Begin()
4 defer func() {
5 if r := recover(); r != nil {
6 // 遇到错误时回滚事务
7 tx.Rollback()
8 }
9 }()
10
11 if err := tx.Error; err != nil {
12 return err
13 }
14
15 // 添加购物车
16 if err := s.dao.AddToCart(tx, product).Error; err != nil {
17 tx.Rollback()
18 return err
19 }
20 // 创建订单
21 if err := s.dao.CreateOrder(tx, product).Error; err != nil {
22 tx.Rollback()
23 return err
24 }
25 // 付款
26 if err := tx.Pay(product).Error; err != nil {
27 tx.Rollback()
28 return err
29 }
30 // 更新库存
31 if err := s.dao.UpdateInventory(tx, product).Error; err != nil {
32 tx.Rollback()
33 return err
34 }
35
36 // 提交事务
37 return tx.Commit().Error
38}
这样做会有两个问题,一个是tx被显示传递,不够优雅并且在service层出现了dao中的变量,破坏了我们的框架的分层原则;二是当业务的不确定性很大,不能灵活的满足需求,即使能满足也会出现冗余的代码。
优雅的封装事务
针对上述的问题,既然不想tx被显示传递,我们可以将它包到Context中,然后通过参数来决定事务需要处理的方法。使用Context还有一个好处就是,如果后续做链路追踪或者超时控制也非常方便。接下来先对事务进行封装:
1package transaction
2
3import (
4 "context"
5 "gorm.io/gorm"
6 "log"
7 "reflect"
8)
9
10type TransactionControl struct {
11 db *gorm.DB
12 transactionCtx struct{}
13}
14
15//在dao中创建事务控制器
16func NewTransactionControl(db *gorm.DB) TransactionControl {
17 return TransactionControl{db: db}
18}
19
20//从ctx中获取tx,此方法在dao中使用
21func (t *TransactionControl) GetCtxDB(ctx context.Context) *gorm.DB {
22 //使用结构体而不是字符串作为 ctx 的 key,可以保证 key 的唯一性
23 ctxDb := ctx.Value(t.transactionCtx)
24
25 if ctxDb != nil {
26 tx, ok := ctxDb.(*gorm.DB)
27 if !ok {
28 log.Panicf("unexpect context value type: %s", reflect.TypeOf(tx))
29 return nil
30 }
31 return tx
32 }
33 //如果tx不存在,则创建新的session
34 return t.db.WithContext(ctx)
35}
36
37//将tx包装到ctx中
38func (t *TransactionControl) CtxWithTransaction(ctx context.Context, tx *gorm.DB) context.Context {
39 if ctx == nil {
40 ctx = context.Background()
41 }
42 return context.WithValue(ctx, t.transactionCtx, tx)
43}
44
45//事务的处理,funcs是dao层的方法
46func (t *TransactionControl) Transaction(ctx context.Context, funcs ...func(txCtx context.Context) error) (err error) {
47 tx := t.db.Begin()
48 defer func() {
49 if r := recover(); r != nil {
50 tx.Rollback()
51 }
52 }()
53 //将事务包装到ctx中
54 txCtx := t.CtxWithTransaction(ctx, tx)
55 for _, f := range funcs {
56 if err = f(txCtx); err != nil {
57 tx.Rollback()
58 return
59 }
60 }
61 if err = tx.Commit().Error; err != nil {
62 tx.Rollback()
63 }
64 return
65}
我们总体的思路是将tx包装到Context中,通过GetCtxDB
方法获取tx并且实现db操作,然后将需要事务的方法通过参数传递,在Transaction
中真正的提交事务。
我们还是使用网购这个例子来看一下我们封装的事务,首先我们需要在dao层创建事务控制器,并且定义一些网购相关的方法:
1type Dao struct {
2 MDb *gorm.DB
3 transaction.TransactionControl
4}
5
6func New() *Dao {
7 mdb := db.GetMysqlDB()
8 return &Dao{
9 MDb: mdb,
10 TransactionControl: transaction.NewTransactionControl(mdb),
11 }
12}
13
14func (d *Dao) AddToCart(ctx context.Context, product Product) error {
15 return d.GetCtxDB(ctx).Table("product").Create(&product).Error
16}
17
18func (d *Dao) CreateOrder(ctx context.Context, product Product) error {
19 return d.GetCtxDB(ctx).Table("order").Create(&Product).Error
20}
21
22func (d *Dao) Pay(ctx context.Context, product Product) error {
23 //付款
24}
25
26func (d *Dao) UpdateInventory(ctx context.Context, product Product) error {
27 //更新库存
28}
我们在service层拼装我们的业务逻辑:
1func (s *Service) Shop(product Product) error {
2 var addToCartFunc = func(txCtx context.Context) error {
3 return s.dao.AddToCart(txCtx, product)
4 }
5 var createOrderFunc = func(txCtx context.Context) error {
6 return s.dao.CreateOrder(txCtx, product)
7 }
8 var payFunc = func(txCtx context.Context) error {
9 return s.dao.Pay(txCtx, product)
10 }
11 var updateInventoryFunc = func(txCtx context.Context) error {
12 return s.dao.UpdateInventory(txCtx, product)
13 }
14 return err := s.dao.Transaction(context.Background(), addToCartFunc, createOrderFunc, payFunc, updateInventoryFunc)
15}
即使只有添加购物车操作,我们只需要在service判断逻辑就可以了,不需要在修改dao层的数据获取,非常的灵活。