<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          寫了一個 gorm 樂觀鎖插件

          共 6697字,需瀏覽 14分鐘

           ·

          2021-03-31 12:19


          前言

          最近在用 Go 寫業(yè)務(wù)的時碰到了并發(fā)更新數(shù)據(jù)的場景,由于該業(yè)務(wù)并發(fā)度不高,只是為了防止出現(xiàn)并發(fā)時數(shù)據(jù)異常。

          所以自然就想到了樂觀鎖的解決方案。

          實現(xiàn)

          樂觀鎖的實現(xiàn)比較簡單,相信大部分有數(shù)據(jù)庫使用經(jīng)驗的都能想到。

          UPDATE `table` SET `amount`=100,`version`=version+1 WHERE `version` = 1 AND `id` = 1

          需要在表中新增一個類似于 version 的字段,本質(zhì)上我們只是執(zhí)行這段 SQL,在更新時比較當(dāng)前版本與數(shù)據(jù)庫版本是否一致。

          如上圖所示:版本一致則更新成功,并且將版本號+1;如果不一致則認(rèn)為出現(xiàn)并發(fā)沖突,更新失敗。

          這時可以直接返回失敗,讓業(yè)務(wù)重試;當(dāng)然也可以再次獲取最新數(shù)據(jù)進行更新嘗試。


          我們使用的是 gorm 這個 orm 庫,不過我查閱了官方文檔卻沒有發(fā)現(xiàn)樂觀鎖相關(guān)的支持,看樣子后續(xù)也不打算提供實現(xiàn)。

          不過借助 gorm 實現(xiàn)也很簡單:

          type Optimistic struct {
           Id      int64   `gorm:"column:id;primary_key;AUTO_INCREMENT" json:"id"`
           UserId  string  `gorm:"column:user_id;default:0;NOT NULL" json:"user_id"` // 用戶ID
           Amount  float32 `gorm:"column:amount;NOT NULL" json:"amount"`             // 金額
           Version int64   `gorm:"column:version;default:0;NOT NULL" json:"version"` // 版本
          }

          func TestUpdate(t *testing.T) {
           dsn := "root:abc123@/test?charset=utf8&parseTime=True&loc=Local"
           db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
           var out Optimistic
           db.First(&out, Optimistic{Id: 1})
           out.Amount = out.Amount + 10
           column := db.Model(&out).Where("id", out.Id).Where("version", out.Version).
            UpdateColumn("amount", out.Amount).
            UpdateColumn("version", gorm.Expr("version+1"))
           fmt.Printf("#######update %v line \n", column.RowsAffected)
          }

          這里我們創(chuàng)建了一張 t_optimistic 表用于測試,生成的 SQL 也滿足樂觀鎖的要求。

          不過考慮到這類業(yè)務(wù)的通用性,每次需要樂觀鎖更新時都需要這樣硬編碼并不太合適。對于業(yè)務(wù)來說其實 version 是多少壓根不需要關(guān)心,只要能滿足并發(fā)更新時的準(zhǔn)確性即可。

          因此我做了一個封裝,最終使用如下:


          var out Optimistic
          db.First(&out, Optimistic{Id: 1})
          out.Amount = out.Amount + 10
          if err = UpdateWithOptimistic(db, &out, nil00); err != nil {
            fmt.Printf("%+v \n", err)
          }
          • 這里的使用場景是每次更新時將 amount 金額加上 10

          這樣只會更新一次,如果更新失敗會返回一個異常。

          當(dāng)然也支持更新失敗時執(zhí)行一個回調(diào)函數(shù),在該函數(shù)中實現(xiàn)對應(yīng)的業(yè)務(wù)邏輯,同時會使用該業(yè)務(wù)邏輯嘗試更新 N 次。

          func BenchmarkUpdateWithOptimistic(b *testing.B) {
           dsn := "root:abc123@/test?charset=utf8&parseTime=True&loc=Local"
           db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
           if err != nil {
            fmt.Println(err)
            return
           }
           b.RunParallel(func(pb *testing.PB) {
            var out Optimistic
            db.First(&out, Optimistic{Id: 1})
            out.Amount = out.Amount + 10
            err = UpdateWithOptimistic(db, &out, func(model Lock) Lock {
             bizModel := model.(*Optimistic)
             bizModel.Amount = bizModel.Amount + 10
             return bizModel
            }, 30)
            if err != nil {
             fmt.Printf("%+v \n", err)
            }
           })
          }

          以上代碼的目的是:

          amount 金額 +10,失敗時再次依然將金額+10,嘗試更新 3 次;經(jīng)過上述的并行測試,最終查看數(shù)據(jù)庫確認(rèn)數(shù)據(jù)并沒有發(fā)生錯誤。

          面向接口編程

          下面來看看具體是如何實現(xiàn)的;其實真正核心的代碼也比較少:

          func UpdateWithOptimistic(db *gorm.DB, model Lock, callBack func(model Lock) LockretryCountcurrentRetryCount int32(err error) {
           if currentRetryCount > retryCount {
            return errors.WithStack(NewOptimisticError("Maximum number of retries exceeded:" + strconv.Itoa(int(retryCount))))
           }
           currentVersion := model.GetVersion()
           model.SetVersion(currentVersion + 1)
           column := db.Model(model).Where("version", currentVersion).UpdateColumns(model)
           affected := column.RowsAffected
           if affected == 0 {
            if callBack == nil && retryCount == 0 {
             return errors.WithStack(NewOptimisticError("Concurrent optimistic update error"))
            }
            time.Sleep(100 * time.Millisecond)
            db.First(model)
            bizModel := callBack(model)
            currentRetryCount++
            err := UpdateWithOptimistic(db, bizModel, callBack, retryCount, currentRetryCount)
            if err != nil {
             return err
            }
           }
           return column.Error

          }

          具體步驟如下:

          • 判斷重試次數(shù)是否達到上限。
          • 獲取當(dāng)前更新對象的版本號,將當(dāng)前版本號 +1。
          • 根據(jù)版本號條件執(zhí)行更新語句。
          • 更新成功直接返回。
          • 更新失敗 affected == 0  時,執(zhí)行重試邏輯。
            • 重新查詢該對象的最新數(shù)據(jù),目的是獲取最新版本號。
            • 執(zhí)行回調(diào)函數(shù)。
            • 從回調(diào)函數(shù)中拿到最新的業(yè)務(wù)數(shù)據(jù)。
            • 遞歸調(diào)用自己執(zhí)行更新,直到重試次數(shù)達到上限。

          這里有幾個地方值得說一下;由于 Go 目前還不支持泛型,所以我們?nèi)绻胍@取 struct 中的 version 字段只能通過反射。

          考慮到反射的性能損耗以及代碼的可讀性,有沒有更”優(yōu)雅“的實現(xiàn)方式呢?

          于是我定義了一個 interface:

          type Lock interface {
           SetVersion(version int64)
           GetVersion() int64
          }

          其中只有兩個方法,目的則是獲取 struct 中的 version 字段;所以每個需要樂觀鎖的 struct 都得實現(xiàn)該接口,類似于這樣:

          func (o *Optimistic) GetVersion() int64 {
           return o.Version
          }

          func (o *Optimistic) SetVersion(version int64) {
           o.Version = version
          }

          這樣還帶來了一個額外的好處:

          一旦該結(jié)構(gòu)體沒有實現(xiàn)接口,在樂觀鎖更新時編譯器便會提前報錯,如果使用反射只能是在運行期間才能進行校驗。

          所以這里在接收數(shù)據(jù)庫實體的便可以是 Lock 接口,同時獲取和重新設(shè)置 version 字段也是非常的方便。

          currentVersion := model.GetVersion()
          model.SetVersion(currentVersion + 1)

          類型斷言

          當(dāng)并發(fā)更新失敗時affected == 0,便會回調(diào)傳入進來的回調(diào)函數(shù),在回調(diào)函數(shù)中我們需要實現(xiàn)自己的業(yè)務(wù)邏輯。

          err = UpdateWithOptimistic(db, &out, func(model Lock) Lock {
             bizModel := model.(*Optimistic)
             bizModel.Amount = bizModel.Amount + 10
             return bizModel
            }, 20)
            if err != nil {
             fmt.Printf("%+v \n", err)
            }

          但由于回調(diào)函數(shù)的入?yún)⒅荒苤朗且粋€ Lock 接口,并不清楚具體是哪個 struct,所以在執(zhí)行業(yè)務(wù)邏輯之前需要將這個接口轉(zhuǎn)換為具體的 struct

          這其實和 Java 中的父類向子類轉(zhuǎn)型非常類似,必須得是強制類型轉(zhuǎn)換,也就是說運行時可能會出問題。

          Go 語言中這樣的行為被稱為類型斷言;雖然叫法不同,但目的類似。其語法如下:

          x.(T)
          x:表示 interface 
          T:表示 向下轉(zhuǎn)型的具體 struct

          所以在回調(diào)函數(shù)中得根據(jù)自己的需要將 interface 轉(zhuǎn)換為自己的 struct,這里得確保是自己所使用的 struct ,因為是強制轉(zhuǎn)換,編譯器無法幫你做校驗,具體能否轉(zhuǎn)換成功得在運行時才知道。

          總結(jié)

          有需要的朋友可以在這里獲取到源碼及具體使用方式:

          https://github.com/crossoverJie/gorm-optimistic

          最近工作中使用了幾種不同的編程語言,會發(fā)現(xiàn)除了語言自身的語法特性外大部分知識點都是相同的;

          比如面向?qū)ο蟆?shù)據(jù)庫、IO操作等;所以掌握了這些基本知識,學(xué)習(xí)其他語言自然就能觸類旁通了。

          瀏覽 62
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  午夜神马福利 | 水蜜桃一区 | 日日日av | 欧美综合网站 | a√天堂在线视频 |