gorm不并发安全

gorm是并发安全的吗?

我们先来看一些例子,这是真实可以跑的代码:

版本:

 module mygo
 ​
 go 1.16
 ​
 require (
     gorm.io/driver/mysql v1.3.6
     gorm.io/gorm v1.23.8
 )

基础代码:

 package main
 ​
 import (
     "log"
     "os"
     "strconv"
     "sync"
 ​
     "golang.org/x/sync/errgroup"
     "gorm.io/driver/mysql"
     "gorm.io/gorm"
     "gorm.io/gorm/clause"
     "gorm.io/gorm/logger"
 )
 ​
 func main() {
     conn, _ := gorm.Open(mysql.Open("root:@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local"), &gorm.Config{
         Logger: logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{
             LogLevel: logger.Info,
         }),
     })
 ​
     Case1(conn)
     //Case2(conn)
     //Case3(conn)
     //Case4(conn)
 }

查询条件污染

 // Case1 : 查询条件污染
 func Case1(conn *gorm.DB) {
     query := conn.Where("id = ?", 1)
     eg := errgroup.Group{}
     for i := 0; i < 3; i++ {
         i := i
         eg.Go(func() error {
             m := &Student{}
             return query.Where("id = ?", i).Find(m).Error
         })
     }
 ​
     if err := eg.Wait(); err != nil {
         panic(err)
     }
 }

输出:

 Error 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SELECT * FROM `students` students` WHERE id = id = SELECT * FROM `students` WHER' at line 1
 [2.944ms] [rows:0] SELECT * FROM SELECT * FROM `students` students` WHERE id = id = SELECT * FROM `students` WHERE id = 1 AND id = 1 AND id = 2 AND id = 01 AND id = 2 AND id = 0

查询条件丢失

 // Case2 : 查询条件丢失
 func Case2(conn *gorm.DB) {
     query := conn.Table("students")
     wg := sync.WaitGroup{}
     for i := 0; i < 10; i++ {
         wg.Add(1)
         go func(id int) { // 0-1000都有数据
             defer wg.Done()
             query.Or("id = ?", id)
         }(i)
     }
     wg.Wait()
 ​
     var c int64
     if err := query.Count(&c).Error; err != nil {
         panic(err)
     }
     print(c)
 }

输出:

 [2.017ms] [rows:1] SELECT count(*) FROM `students` WHERE id = 9 OR id = 3 OR id = 8 OR id = 0 OR id = 2 OR id = 5 OR id = 6
 6

导致panic1

 // Case3 : 并发设置查询条件,导致panic
 func Case3(conn *gorm.DB) {
     query := conn.Where("id = ?", 1)
     wg := sync.WaitGroup{}
     for i := 0; i < 64; i++ {
         wg.Add(1)
         go func() {
             defer wg.Done()
             query.Where("id = ?", 1).Where("id = ?", 1).Where("id = ?", 1)
         }()
     }
     wg.Wait()
 }

输出:

 fatal error: concurrent map read and map write

导致panic2

 // Case4 : 新建session的同时设置查询条件,导致panic
 func Case4(conn *gorm.DB) {
     query := conn.Select("abc").Table("abc").Where("id = ?", 1).
         Joins("cba").Offset(1).Limit(1).Group("111").Order("bbb").
         Clauses(clause.OnConflict{}).Clauses(clause.Locking{})
     wg := sync.WaitGroup{}
     for i := 0; i < 1024; i++ {
         wg.Add(1)
         go func() {
             defer wg.Done()
             query.Session(&gorm.Session{Context: context.Background()})
         }()
         query.Where("id = ?", i) // 没有并发设置查询条件。设置查询条件是串行的
     }
     wg.Wait()
 }

输出:

 fatal error: concurrent map read and map write

前置知识

Chain Method

比如WhereLimitSelectTablesJoinClauses等等,这些在语句执行被执行前,设置和修改语句内容的,都叫 Chain Method

Finisher Method

比如CreateFirstFindTakeSaveUpdate``DeleteScanRowRows等等,会设置和修改语句内容,并执行语句的,都叫 Finisher Method。

New Session Method

比如SessionWithContextDebug 这三个方法,他们会新建一个Session。WithContextDebug 都只是Session方法特定调用的简写,底层都是调用的Session方法。

Statement

每个*gorm.DB 实例都会有一个Statement的字段,Statement就是我们真正要执行的语句,我们的 Chain Method 和 Finisher Method,事实上都是在修改Statement这个结构体。最后这个结构体会被渲染为SQL语句。

gorm的并发模型

首先,我们需要先去理解几乎每个方法中都会调用的函数:tx = db.getInstance()

 func (db *DB) getInstance() *DB {
     if db.clone > 0 {
         tx := &DB{Config: db.Config, Error: db.Error}
 ​
         if db.clone == 1 {
             // clone with new statement
             tx.Statement = &Statement{
                 DB:       tx,
                 ConnPool: db.Statement.ConnPool,
                 Context:  db.Statement.Context,
                 Clauses:  map[string]clause.Clause{},
                 Vars:     make([]interface{}, 0, 8),
             }
         } else {
             // with clone statement
             tx.Statement = db.Statement.clone()
             tx.Statement.DB = tx
         }
         return tx
     }
 ​
     return db
 }

将上述改写并简化一下,大概是这么个逻辑:

 func (db *DB) getInstance() *DB {
     switch db.clone:
     case 0:
         return db
     case 1:
         return newStatement() // 一个全新的,空白的Statement
     case 2:
         return db.cloneStatement() // 将之前的Statement复制一份
 }

当clone=1时,这个*gorm.DB 实例总是并发安全的,因为它总是会返回一个全新的*gorm.DB 实例,不会对老*gorm.DB 实例有什么读写。

当clone=2时,这个*gorm.DB 实例也总是并发安全的,因为任何的 Chain Method 和 Finisher Method 都只会去读和复制当前*gorm.DB 实例的值,而不会修改,因此只会对这个*gorm.DB 实例并发读,那么当然是并发安全的。

当clone=0时,这个*gorm.DB 实例就不并发安全

那clone字段分别会在什么情况下等于0、1、2呢?

  • 在使用gorm.Open()之后,新建出来的*gorm.DB 实例clone字段总是1。
  • 在调用(*gorm.Gorm).Session()时,如果Session{}.NewDBfalse,则为返回的*gorm.DB 实例clone字段是2,如果为true,则为1。
  • 在调用(*gorm.Gorm).Session()时,如果Session{}.Initializedtrue,则返回的*gorm.DB 实例clone字段是0。这条规则优先级高于Session.NewDB
  • 在调用了任意Chain Method、Finisher Method之后,返回的Gorm对象clone字段是0。

这也就符合文档中的说法:

After a Chain method, Finisher Method, GORM returns an initialized *gorm.DB instance, which is NOT safe to reuse anymore.

在调用过了 Chain Method 和 Finisher Method 后,GORM返回一个初始化好的 *gorm.DB 实例,这个实例不再能被安全重用。

更近一步,在抛开(*gorm.Gorm).Session()那些复杂的配置项后,我们可以得出这些非常实用的结论:

  • 使用gorm.Open()创建出来的对象,完全无法被修改。因为对他调用任何方法,最后都只会创建出新的*gorm.DB 实例。所以不妨称为connection,简称为conn。
  • 使用conn.Session()新建一个*gorm.DB 实例来查询,和直接使用conn来查询,效果是一样的。因为conn.Session()也只会clone一个空的Statement。
  • 如果想让之后的查询都带上特定的条件,那么需要先设定好初始的条件,再使用 New Session Method 来创建新的*gorm.DB 实例,并且之后的查询都使用这个*gorm.DB 实例。比如db.Unscoped().Session(&gorm.Session{})
  • 无论如何不能并发调用 Chain Method 和 Finisher Method,要么会查询条件污染,要么查询条件丢失,那么还可能会panic。

gorm的并发哲学

gorm没说自己是并发安全的,从主页上看不到任何对并发的描述。网上有一些说法,说gorm是并发安全的,但是从一开始的例子大家也就知道,gorm不是并发安全的。

gorm不并发安全

gorm使用复制来新建对象,避免了锁的开销,一定程度上保证了并发安全。我个人感觉思路上有些像多版本控制(不同的*gorm.DB实例就是不同的版本),但是学疏才浅,想不到特别准确的描述。

因此,gorm并没有承诺并发安全,gorm只是提供了一套符合使用习惯的并发范式,来兼顾性能和并发安全。如果你的使用习惯不符合gorm预想的习惯,则可能会出现并发安全。