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
比如Where
、Limit
、Select
、Tables
、Join
、Clauses
等等,这些在语句执行被执行前,设置和修改语句内容的,都叫 Chain Method
Finisher Method
比如Create
、First
、Find
、Take
、Save
、Update``Delete
、Scan
、Row
、Rows
等等,会设置和修改语句内容,并执行语句的,都叫 Finisher Method。
New Session Method
比如Session
、WithContext
、Debug
这三个方法,他们会新建一个Session。WithContext
和Debug
都只是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{}.NewDB
为false
,则为返回的*gorm.DB
实例clone字段是2,如果为true
,则为1。 - 在调用
(*gorm.Gorm).Session()
时,如果Session{}.Initialized
为true
,则返回的*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.DB
实例就是不同的版本),但是学疏才浅,想不到特别准确的描述。
因此,gorm并没有承诺并发安全,gorm只是提供了一套符合使用习惯的并发范式,来兼顾性能和并发安全。如果你的使用习惯不符合gorm预想的习惯,则可能会出现并发安全。
参考资料
www.slideshare.net/JinzhuZhang…
转载自:https://juejin.cn/post/7134002645651439630