寫了一個 gorm 樂觀鎖外掛
前言
最近在用Go寫業務的時碰到了併發更新資料的場景,由於該業務併發度不高,只是為了防止出現併發時資料異常。
所以自然就想到了樂觀鎖的解決方案。
實現
樂觀鎖的實現比較簡單,相信大部分有資料庫使用經驗的都能想到。
UPDATE`table`SET`amount`=100,`version`=version+1 WHERE`version`=1 AND`id`=1
需要在表中新增一個類似於version的欄位,本質上我們只是執行這段SQL,在更新時比較當前版本與資料庫版本是否一致。
如上圖所示:版本一致則更新成功,並且將版本號+1;如果不一致則認為出現併發衝突,更新失敗。
這時可以直接返回失敗,讓業務重試;當然也可以再次獲取最新資料進行更新嘗試。
我們使用的是gorm這個orm庫,不過我查閱了官方文件卻沒有發現樂觀鎖相關的支援,看樣子後續也不打算提供實現。
不過藉助gorm實現也很簡單:
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 linen",column.RowsAffected)
}
這裡我們建立了一張t_optimistic表用於測試,生成的SQL也滿足樂觀鎖的要求。
不過考慮到這類業務的通用性,每次需要樂觀鎖更新時都需要這樣硬編碼並不太合適。對於業務來說其實version是多少壓根不需要關心,只要能滿足併發更新時的準確性即可。
因此我做了一個封裝,最終使用如下:
var out Optimistic
db.First(&out,Optimistic{Id:1})
out.Amount=out.Amount+10
if err=UpdateWithOptimistic(db,&out,nil,0,0);err!=nil{
fmt.Printf("%+vn",err)
}
這裡的使用場景是每次更新時將amount金額加上10。
這樣只會更新一次,如果更新失敗會返回一個異常。
當然也支援更新失敗時執行一個回撥函式,在該函式中實現對應的業務邏輯,同時會使用該業務邏輯嘗試更新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
},3,0)
if err!=nil{
fmt.Printf("%+vn",err)
}
})
}
以上程式碼的目的是:
將amount金額+10,失敗時再次依然將金額+10,嘗試更新3次;經過上述的並行測試,最終檢視資料庫確認資料並沒有發生錯誤。
面向介面程式設計
下面來看看具體是如何實現的;其實真正核心的程式碼也比較少:
func UpdateWithOptimistic(db*gorm.DB,model Lock,callBack func(model Lock)Lock,retryCount,currentRetryCount 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
}
具體步驟如下:
判斷重試次數是否達到上限。
獲取當前更新物件的版本號,將當前版本號+1。
根據版本號條件執行更新語句。
更新成功直接返回。
更新失敗affected==0時,執行重試邏輯。
重新查詢該物件的最新資料,目的是獲取最新版本號。
執行回撥函式。
從回撥函式中拿到最新的業務資料。
遞迴呼叫自己執行更新,直到重試次數達到上限。
這裡有幾個地方值得說一下;由於Go目前還不支援泛型,所以我們如果想要獲取struct中的version欄位只能透過反射。
考慮到反射的效能損耗以及程式碼的可讀性,有沒有更”優雅“的實現方式呢?
於是我定義了一個interface:
type Lock interface{
SetVersion(version int64)
GetVersion()int64
}
其中只有兩個方法,目的則是獲取struct中的version欄位;所以每個需要樂觀鎖的struct都得實現該介面,類似於這樣:
func(o*Optimistic)GetVersion()int64{
return o.Version
}
func(o*Optimistic)SetVersion(version int64){
o.Version=version
}
這樣還帶來了一個額外的好處:
一旦該結構體沒有實現介面,在樂觀鎖更新時編譯器便會提前報錯,如果使用反射只能是在執行期間才能進行校驗。
所以這裡在接收資料庫實體的便可以是Lock介面,同時獲取和重新設定version欄位也是非常的方便。
currentVersion:=model.GetVersion()
model.SetVersion(currentVersion+1)
型別斷言
當併發更新失敗時affected==0,便會回撥傳入進來的回撥函式,在回撥函式中我們需要實現自己的業務邏輯。
err=UpdateWithOptimistic(db,&out,func(model Lock)Lock{
bizModel:=model.(*Optimistic)
bizModel.Amount=bizModel.Amount+10
return bizModel
},2,0)
if err!=nil{
fmt.Printf("%+vn",err)
}
但由於回撥函式的入參只能知道是一個Lock介面,並不清楚具體是哪個struct,所以在執行業務邏輯之前需要將這個介面轉換為具體的struct。
這其實和Java中的父類向子類轉型非常類似,必須得是強制型別轉換,也就是說執行時可能會出問題。
在Go語言中這樣的行為被稱為型別斷言;雖然叫法不同,但目的類似。其語法如下:
x.(T)
x:表示interface
T:表示向下轉型的具體struct
所以在回撥函式中得根據自己的需要將interface轉換為自己的struct,這裡得確保是自己所使用的struct,因為是強制轉換,編譯器無法幫你做校驗,具體能否轉換成功得在執行時才知道。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69995861/viewspace-2762855/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 悲觀鎖和樂觀鎖
- 自己寫一個Babel外掛Babel
- laravel樂觀鎖和悲觀鎖Laravel
- mysql悲觀鎖以樂觀鎖MySql
- 理解樂觀鎖和悲觀鎖
- 寫了一個簡單好用的彈出層外掛
- MySQL鎖(樂觀鎖、悲觀鎖、多粒度鎖)MySql
- MySQL 樂觀鎖MySql
- 樂觀鎖CAS
- java-樂觀鎖與悲觀鎖Java
- MybatisPlus - [03] 樂觀鎖&悲觀鎖MyBatis
- 如何寫一個Vue的外掛Vue
- 如何編寫一個Jquery外掛jQuery
- 面試官:你說說互斥鎖、自旋鎖、讀寫鎖、悲觀鎖、樂觀鎖的應用場景面試
- MySQL樂觀鎖和悲觀鎖介紹MySql
- Java中的鎖之樂觀鎖與悲觀鎖Java
- 【mybatis-plus】什麼是樂觀鎖?如何實現“樂觀鎖”MyBatis
- 面試必備之悲觀鎖與樂觀鎖面試
- Redis的事務、樂觀鎖和悲觀鎖Redis
- MySQL 悲觀鎖與樂觀鎖的詳解MySql
- 面試必備之樂觀鎖與悲觀鎖面試
- Java彌散系列 - 樂觀鎖與悲觀鎖Java
- SQLServer樂觀鎖定和悲觀鎖定例項SQLServer
- Java鎖?分散式鎖?樂觀鎖?行鎖?Java分散式
- 自己動手編寫一個Mybatis外掛:Mybatis脫敏外掛MyBatis
- 【鎖機制】共享鎖、排它鎖、悲觀鎖、樂觀鎖、死鎖等等
- Java併發:樂觀鎖Java
- 樂觀鎖與悲觀鎖及應用舉例
- 資料庫中的悲觀鎖和樂觀鎖資料庫
- JPA和Hibernate的樂觀鎖與悲觀鎖
- 教練我想寫一個 HelloWorld Babel 外掛Babel
- 如何編寫一個 Pulsar Broker Interceptor 外掛
- 編寫一個簡單的babel外掛Babel
- 給Ionic寫一個cordova(PhoneGap)外掛
- 如何從零編寫一個vite外掛 建立 vite 外掛通用模板Vite
- 造了一個 protoc-gen-fieldmask 外掛
- 開發了一個 JATO for JBuilder 的外掛UI
- 自旋鎖、阻塞鎖、可重入鎖、悲觀鎖、樂觀鎖、讀寫鎖、偏向所、輕量級鎖、重量級鎖、鎖膨脹、物件鎖和類鎖物件