thread safe of gorm

gorm的并发安全问题

1
2
3
4
最近负责一个小项目的后端,需要用到sql数据库,
本着能偷懒就绝不多写一行代码的精神,我决定用gorm帮助我偷懒。
由于代码逻辑十分简单所以entity、dao等等各层写的不亦乐乎,
但是多次高并发程序的开发经历告诉我事情没有这么简单,果然gorm并没有说自己对多线程操作保证安全。。。

下面是gorm关于并发安全的叙述
https://gorm.io/zh_CN/docs/method_chaining.html
alt gorm中并发安全的叙述

链式方法

1
2
3
链式方法是将 Clauses 修改或添加到当前 Statement 的方法,例如:

Where, Select, Omit, Joins, Scopes, Preload, Raw
1
2
3
终结(方法) 是会立即执行注册回调的方法,然后生成并执行 SQL,比如这些方法:

Create, First, Find, Take, Save, Update, Delete, Scan, Row, Rows

新建会话方法特指

1
gorm.DB.Session(&gorm.Session{})

链式方法一般都是返回当前类一个实例指针的方法,比如gorm源码中这两句:

1
2
func (db *DB) Session(config *Session) *DB
func (db *DB) Create(value interface{}) (tx *DB)

对 *DB 类型实例的调用返回了一个*DB指针
如果我要对 Session方法的返回值调用 Create方法,
可以这么写:

1
2
d:=db.Session(balabalabala)
d.Create(balabalabala)

但是既然这Session返回了一个*DB指针,那么可以这样调用

1
db.Session(balabala).Create(balabala)

这样写出的代码更优雅一些,但是
这不是本文章的重点,算是前置知识吧

gorm中并发安全的关键

gorm定义了一个 *DB 类型
这个类型里面有一个很重要的成员: DB.clone

1
2
3
4
5
6
7
type DB struct {
*Config
Error error
RowsAffected int64
Statement *Statement
clone int
}

gorm中还有另一个函数,大多数的链式方法首先会执行下面这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
}

我们把这个函数的逻辑简化一下:
clone < 0: 返回原来的 *DB
clone = 1: 新建一个 *DB 并且新的 *DB 的clone字段为0
clone > 1: 复制原来 *DB 并返回,那么新 *DB 的clone字段和原来相同,也会 > 1

我们每对 *DB 的方法进行一次调用都有可能对它 *Statement 字段进行修改,比如Limit方法修改了一次Statement( tx.Statement.AddClause(args) )

1
2
3
4
5
func (db *DB) Limit(limit int) (tx *DB) {
tx = db.getInstance()
tx.Statement.AddClause(clause.Limit{Limit: &limit})
return
}

Where方法也会修改

1
2
3
4
5
6
7
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) {
tx = db.getInstance()
if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {
tx.Statement.AddClause(clause.Where{Exprs: conds})
}
return
}

如果两次条件不同的查询都使用同一个 *DB,那么就会并发地对同一个Statement进行修改,就会出现安全问题,比如

1
2
select * from table1 where id = 1
select * from table1 where id = 2

他们向同一个statement写入了不同查询条件,最终的结果几乎一定会出错。
因此问题的关键就在于, 对不同的查询要使用不同的 *DB,有一个很实用的方法 *DB.Session(), 如果传入的*gorm.Session{}结构体指针中NewDB字段为true,那么就会返回一个clone为1的新 *DB,对这个 *DB 的调用就不会影响到原来的 *DB

1
newDB := oldDB.Session(&gorm.Session{NewDB: true})

或者诸位可以自己查看各个链式方法和终结方法的源码,确定该方法对老的 *DB 会进行什么操作,只要能够保证不同的sql操作条件作用在不同的 *DB 上就好了