探討在Golang_gorm上使用樂觀鎖的做法

簡單介紹如何在Golang的gorm上面使用樂觀鎖的做法。

探討在Golang_gorm上使用樂觀鎖的做法

Gorm上如果要使用Optimistic locking的話,使用go-gorm optimisticlock會是一個好選擇。因為它只需要在Model定義好,就可以正常使用了。可以避免實作的時候忘記加上WHERE version = ?的條件。而變成沒有使用樂觀鎖保護資料的情況。

為什麼要使用樂觀鎖?

樂觀鎖實際上是沒有使用到Lock的,所以在沒有衝突的情況下,每筆資料的更新速度,會比有使用Lock的情況還要快速。 所以在資料庫更新頻率較低的時候,樂觀鎖是一個不錯的選擇。 樂觀鎖的缺點是,如果有兩個人同時更新同一筆資料,就會有一個人的更新會失敗。這時候就需要重新讀取資料,再重新更新一次。所以不適合在資料更新頻率很高的情況下使用。

另一方面,在產品還在MVP階段的時候,重點是趕快把Business Model 實作出來放到市場上,看看市場反應如何。能儘量快一點把功能做出來會越好。等確定這個功能是真的有市場,再來考慮優化效能方面的問題。 使用樂觀鎖+ORM,能簡化開發要寫的code,因為使用樂觀鎖+OR,就能讓大部分的code都變成,取出該筆資料->更新欄位的值->整筆存起來,並且可以避免資料更新的時候,用到別人更新的資料(race condition), 這樣幾乎每個table就只需要有GetUpdate就能完成所有更新的操作了。因此在MVP階段,使用樂觀鎖是一個不錯的選擇。

以下列出各種寫法,就能很清楚的看出來使用樂觀鎖的好處,還有為什麼要使用Optimisticlock套件了。

範例

  • table books
  • table 欄位有 id, title, count, updated_at;

每賣出一本書,就要讓count加1。 請實作出AddBookCount的方法。

方法1:使用Atomic Update

剛好這個例子只需要更新一個欄位的值,所以可以使用Atomic Update來實作。但在一些複雜的情境下,並不是單純的對原本的數值做加減的情況,就不能使用這個方法,而是需要使用下面其它的方法了。

type BookRepo struct{
    db *gorm.DB
}

func (repo* BookRepo) AddBookCount(bookID string, count int) error {
	err := repo.db.Model(&Book{}).
        Where("id = ?", bookID).
        Update("count = count + ?", count).
		Update("updated_at", time.Now()).
        Error
    if err != nil {
        return err
    }
	    return nil
}

方法2:使用lock

這個做法,在大部分的情況下可以正常運作。(例外:Phantom Read, Write Skew的情況)。

type BookRepo struct{
    db *gorm.DB
}

func(repo* BookRepo) AddBookCount(bookID string, count int) error{
    tx := repo.db.Begin()
    //lock the book record
	var b Book
    err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", bookID).First(&book).Error
    if err != nil{
        tx.Rollback()
        return err
    }
	//update the book record
	b.Count += count
    err = tx.Save(&b).Error
    if err != nil{
        tx.Rollback()
        return err
    }
	tx.Commit()
    return nil
}   

方法3:使用樂觀鎖

雖然看起來都有點複雜,但只要是針對一個record的更新,都可以簡化成同樣的流程:取出該筆資料->更新欄位的值->整筆存起來。這樣在一些比較複雜的情境裡面,就可以用相對簡單的方式實作出來。

用UpdatedAt來自己實作

常見的做法,使用updated_at欄位來做為樂觀鎖的版本號,但這個方法有一個缺點:如果更新在比updated_at記錄的時間顆粒度還小的時間內一起發生,就會發生問題。例如updated_at的顆粒度是秒,但有多個request在同一秒內更新,就會相撞。 所以這個做法要很小心,注意更新的頻率和updated_at資料的顆粒度。但通常來說,如果更新頻率偏高,通常就會開始改成使用Lock的方式,而不是樂觀鎖了。

type BookRepo struct{
    db *gorm.DB
}

func(repo* BookRepo) AddBookCount(bookID string, count int) error{
    //lock the book record
	var b Book
    err := tx.First(&book, "id = ?", bookID).Error
    if err != nil{
        return err
    }
	//update the book record
	b.Count += count
	err := tx.Model(&Book{}).Where("updated_at = ?", b.UpdatedAt).Updates(&b).Error
    if err != nil{
        return err
    }
    //檢查是否成功更新
    if tx.AffectedRows() != 1{
        return errors.New("更新失敗")
    }
    return nil
}   

用Version欄位來自己實作

能避免上面用updated_at來實作樂觀鎖提到的缺點。但是必須要在欄位上,多加上一個version的欄位

type BookRepo struct{
    db *gorm.DB
}

func (repo* BookRepo)AddBookCount(bookID string, count int) error{
    var b Book
    err := repo.db.First(&b, "id = ?", bookID).Error
    if err != nil{
        return err
    }
    b.Count += count
	currentVersion := b.Version
    b.Version += 1
    
    err = repo.db.Where("version = ?", currentVersion).Updates(&b).Error
    if err != nil{
        return err
    }
    // 檢查是否成功更新
    if repo.db.RowsAffected != 1{
        return errors.New("更新失敗")
    }
    return nil
}

使用go-gorm/optimisticlock

在Model使用optimisticlock.Version

type Book struct{
    ID         string
    Title      string
    Count      int
    UpdatedAt  time.Time
    Version    optimisticlock.Version
}
type BookRepo struct{
    db *gorm.DB
}

func (repo* BookRepo)AddBookCount(bookID string, count int) error{
    var b Book
    err := repo.db.First(&b, "id = ?", bookID).Error
    if err != nil{
        return err
    }
    b.Count += count
    
    err = repo.db.Save(&b).Error
    if err != nil{
        return err
    }
    // 檢查是否成功更新
    if repo.db.RowsAffected != 1{
        return errors.New("更新失敗")
    }
    return nil
}

完整範例在此

結論

  • 使用Atomic Update的方式,速度最快,但不一定適用於所有的情況。
  • 使用Lock的方式,能夠避免race condition的發生,但因為要取得、釋放lock,所以每個request執行所需要的時間會比較長一些。當然,在更新頻率較高的情況下,多等一下下,總是比全部重新來過還要快。
  • 如果要在Gorm上使用樂觀鎖,這個時候使用樂觀鎖套件會比較好,因為不論是哪種自己實作的樂觀鎖,都會需要多加一些檢查的條件,但在使用樂觀鎖套件的時候,只需要在Model上加上optimisticlock.Version,剩下的就是照常的使用Gorm的方法就好了。
comments powered by Disqus