gorm是如何保證協程安全的

Mr_houzi發表於2022-04-02
Gorm 官方文件提供瞭如何正確使用鏈式呼叫的例子以及會引起協程不安全的反例,知道了如何正確使用,也要知道原理才能用的更安心。下面以文件示例和原始碼切入,淺析 Gorm 在鏈式呼叫時時如何保證協程安全的?

原始碼分析

下面是一段 gorm 常見的使用程式碼,先初始化連線,然後根據鏈式呼叫進行增刪改。

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
    panic("failed to connect database")
}

tx := db.Where("age = 22").Where("name = '小明'").Find(&user)

通過 gorm.Open() 初始化連線,將拿到一個 *gorm.DB 結構體指標 db。結構體的 clone 屬性為 1

type DB struct {
    *Config
    Error        error
    RowsAffected int64
    Statement    *Statement
    clone        int
}

func Open(dialector Dialector, opts ...Option) (db *DB, err error) {
    // ……
    db = &DB{Config: config, clone: 1}
    // ……
}

通過 db.Where() 進行鏈式查詢,Where() 方法也將返回一個*gorm.DB結構體指標 tx。

// Where add conditions
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
}

檢視原始碼可以發現,像Where()、Select()、Limit() 等每個鏈式方法中,都要先去獲取 *gorm.DB結構體指標 tx,也就是 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
}

當傳入的 *gorm.DB指標指向的結構體屬性 clone 為 1時,將會克隆一份 DB 結構體,並返回一個新的指標指向這個 DB 結構體,也即是建立一個新的會話。clone 為 0 時,返回原來的指標指向地址。

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
    panic("failed to connect database")
}

db.Where().Where()

// 等同於
tx1 = db.Where()
tx2 = tx.Where()

基於上面的分析,在此例程式碼中,由於 db 指標指向 gorm.DB 結構體的 clone 屬性為1。tx1 將指向一個複製的新的 gorm.DB 結構體,它的 clone 屬性為 0。所以,tx.Where() 時將不會複製一個新的結構體(不會建立新會話),即 tx2 與 tx1 都指向同一個 gorm.DB 結構體,與 db 指向不同。

例子

經過上面的剖析,再看官方文件的例子,就能理解怎樣使用是協程安全的鏈式呼叫。

例子1

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

// 安全的使用新初始化的 *gorm.DB
for i := 0; i < 100; i++ {
  go db.Where(...).First(&user)
}

在 100 個協程中,db.Where() 會分別複製 gorm.DB 結構體,返回其指向指標,即建立了新會話,然後繼續進行鏈式呼叫。所以,在 100 個協程中,鏈式呼叫拼接的 sql 查詢是不會相互干擾的。

例子2

tx := db.Where("name = ?", "jinzhu")
// 不安全的複用 Statement
for i := 0; i < 100; i++ {
  go tx.Where(...).First(&user)
}

本例中,tx 指向了一個新的 gorm.DB 結構體,且 clone 為 0,所以 tx. Where() 將不會產生新的結構體,即不會建立新會話。那麼,在100個協程中,共用 tx 指向的 gorm.BD,這樣就會產生協程間相互干擾的問題。

例子3

tx := db.Where("name = ?", "jinzhu").Session(&gorm.Session{})
// 在 `新建會話方法` 之後是安全的
for i := 0; i < 100; i++ {
  go tx.Where(...).First(&user) // `name = 'jinzhu'` 會應用到查詢中
}

通過 Session() 方法,建立新會話,將 tx 指向的新結構體 的 clone 屬性置為 1。“tx 便有了例子一中 db 的效果”。

例子4

ctx, _ := context.WithTimeout(context.Background(), time.Second)
ctxDB := db.WithContext(ctx)
// 在 `新建會話方法` 之後是安全的
for i := 0; i < 100; i++ {
  go ctxDB.Where(...).First(&user)
}

ctx, _ := context.WithTimeout(context.Background(), time.Second)
ctxDB := db.Where("name = ?", "jinzhu").WithContext(ctx)
// 在 `新建會話方法` 之後是安全的
for i := 0; i < 100; i++ {
  go ctxDB.Where(...).First(&user) // `name = 'jinzhu'` 會應用到查詢中
}

這兩種方式是複用了 Session 方法。

finish!

參考
鏈式方法 - GORM

文章來自 gorm是如何保證協程安全的

相關文章