golang?基于?mysql?簡單實現(xiàn)分布式讀寫鎖
業(yè)務場景
因為項目剛上線,目前暫不打算引入其他中間件,所以打算通過 mysql 來實現(xiàn)分布式讀寫鎖;而該業(yè)務場景也滿足分布式讀寫鎖的場景,抽象后的業(yè)務場景是:特定資源 X,可以執(zhí)行 2 種操作:讀操作和寫操作,2種操作需要滿足下面條件:
- 執(zhí)行操作的機器分布式在不同的節(jié)點中,也就是分布式的;
- 讀操作是共享的,也就是說同時可以有多個 goroutine 對資源 X 執(zhí)行讀操作;
- 寫操作是互斥的,也就是說同一時刻只允許有一個 goroutine 對資源 X 執(zhí)行寫操作;
- 讀操作和寫操作是互斥的,也就是說寫操作和讀操作不能同時存在
既然需要如此實現(xiàn),下面我們看下什么是分布式讀寫鎖。
什么是分布式讀寫鎖
大家對于鎖肯定不陌生,在 golang 中 sync.Mutex 鎖是常見的,一般用在單節(jié)點多 goroutine 中對資源的并發(fā)訪問;但是分布式場景下,單節(jié)點 sync.Mutex 加鎖的方式就會失去作用,于是人們?yōu)榱嗽诜植际江h(huán)境中實現(xiàn)對共享資源的互斥訪問,實現(xiàn)了各種分布式鎖。
而分布式讀寫鎖是比分布式鎖粒度更小的鎖,對業(yè)務場景的加鎖會更加靈活,其中分布式讀寫鎖也遵循讀寫鎖的原則:
- 讀模式共享,寫模式互斥。
- 它三種模式狀態(tài): 讀加鎖狀態(tài)、寫加鎖狀態(tài)、無鎖狀態(tài)。
分布式讀寫鎖的訪問原則與讀寫鎖類似,下面我們具體看下。
分布式讀寫鎖的訪問原則
以下列表為讀寫鎖(也就是分布式讀寫鎖)的讀寫訪問原則
當前鎖狀態(tài) | 讀鎖請求 | 寫鎖請求 |
---|---|---|
無鎖狀態(tài) | 可以 | 可以 |
讀鎖狀態(tài) | 可以 | 不可以 |
寫鎖狀態(tài) | 不可以 | 不可以 |
讀鎖
- 只有在無鎖和讀鎖下可以獲取讀鎖。
- 讀鎖的模式下,任何請求讀鎖都可以。
- 讀鎖的模式下, 請求寫鎖不可以,直到所有讀鎖解鎖,寫鎖才能獲取到鎖。
寫鎖
- 只有在無鎖狀態(tài)下可以獲取寫鎖。
- 寫鎖的模式下,任何請求讀鎖和寫鎖都阻塞,直到寫鎖解鎖。
具體實現(xiàn)
如果本地沒有 mysql 數(shù)據(jù)庫,可以通過這篇文章快速搭建: 如何使用 docker 搭建一個 mysql 服務
通過 gorm 連接 mysql
gorm 是一個 golang 的 orm 框架,可以使用它快速連接數(shù)據(jù)庫,具體代碼如下:
package main import ( "fmt" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" ) var ( db *gorm.DB dbUsername = "kele" dbPassword = "baishi2020" dbHost = "127.0.0.1:7306" dbDatabase = "lingmo" stateReadLock = "ReadLock" stateWriteLock = "WriteLock" stateUnlock = "Unlock" ) type RWLock struct { LockMark string `gorm:"default:'Unlock'"` ReadLockCount uint32 `gorm:"default:0"` LockReason string } type Stock struct { gorm.Model RWLock Count int64 } func (Stock) TableName() string { return "stocks" } func init() { dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", dbUsername, dbPassword, dbHost, dbDatabase) mysqlConfig := mysql.Config{DSN: dsn} gormConfig := &gorm.Config{Logger: logger.Default.LogMode(logger.Info)} var err error if db, err = gorm.Open(mysql.New(mysqlConfig), gormConfig); err != nil { panic(err) } db.Set("db:table_options", "ENGINE = InnoDB DEFAULT CHARSET = utf8") // register tables if err = db.AutoMigrate(&Stock{}); err != nil { panic(err) } } func main() { if result := db.Model(&Stock{}).Save(&Stock{Model: gorm.Model{}, RWLock: RWLock{}, Count: 10}); result.Error != nil { panic(result.Error) } }
首先我們定義了一個庫存表 stocks,并且在其中添加三個和讀寫鎖相關(guān)的字段,三個字段的含義如下:
- LockMark: 表示某條數(shù)據(jù)加鎖的狀態(tài),只能是讀鎖、寫鎖、無鎖狀態(tài)中的一種。
- ReadLockCount: 首先讀模式是共享的,意味著可以有多個 goroutine 并發(fā)訪問,而 ReadLockCount 字段則記錄當前并發(fā)訪問的 goroutine 數(shù)量。
- LockReason: 記錄當前加鎖的原因;讀鎖是最新的 goroutine 的 lockReason,寫鎖則是寫鎖 goroutine 的 lockReason。
其余則是一些 gorm 連接 mysql 邏輯,這里不再多贅述。
實現(xiàn)讀鎖模式
具體代碼如下:
func (s Stock) RLock(db *gorm.DB, lockReason string) error { condition := "(id = ?) AND (lock_mark != ?)" fields := map[string]interface{}{ "lock_mark": stateReadLock, "read_lock_count": gorm.Expr("read_lock_count + ?", 1), "lock_reason": lockReason, } result := db.Model(&Stock{}).Where(condition, s.ID, stateWriteLock).Updates(fields) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return errors.New("failed to rlock Stock, RowsAffected=0") } return nil } func (s Stock) RUnlock(db *gorm.DB, UnLockReason string) error { sql := fmt.Sprintf(`UPDATE stocks SET read_lock_count=if(read_lock_count>0,read_lock_count-1,0), lock_mark=if(read_lock_count<1, 'Unlock', 'ReadLock'),lock_reason ='%s' where id= %d and lock_mark='%s'`, UnLockReason, s.ID, stateReadLock) result := db.Exec(sql) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return errors.New("failed to RUnlock Stock, RowsAffected=0") } return nil } func main() { if result := db.Model(&Stock{}).Save(&Stock{Model: gorm.Model{}, RWLock: RWLock{}, Count: 10}); result.Error != nil { panic(result.Error) } s := &Stock{Model: gorm.Model{ID: 1}} if result := db.Model(s).First(s); result.Error != nil { panic(result.Error) } if err := s.RLock(db, "readLock_reason_1"); err != nil { panic(err) } if err := s.RLock(db, "readLock_reason_2"); err != nil { panic(err) } if err := s.RUnlock(db, "readLock_unlock_1"); err != nil { panic(err) } if err := s.RUnlock(db, "readLock_unlock_2"); err != nil { panic(err) } }
執(zhí)行以上代碼是可以正常運行的, 下面我們分析下:
- 讀鎖的 sql 語句如下,只要在非寫鎖狀態(tài)下就能加讀鎖。
UPDATE `stocks` SET `lock_mark` = 'ReadLock', `lock_reason` = 'readLock_reason_1', `read_lock_count` = read_lock_count + 1, `updated_at` = '2022-09-25 14:58:45.693' WHERE (( id = 1 ) AND ( lock_mark != 'WriteLock' )) AND `stocks`.`deleted_at` IS NULL
- 解讀鎖的 sql 語句如下,只有在讀鎖狀態(tài)下才能解讀鎖,另外還要更新 read_lock_count 和 lock_reason 字段。
UPDATE stocks SET read_lock_count = IF ( read_lock_count > 0, read_lock_count - 1, 0 ), lock_mark = IF ( read_lock_count < 1, 'Unlock', 'ReadLock' ), lock_reason = 'readLock_unlock_1' WHERE id = 1 AND lock_mark = 'ReadLock'
實現(xiàn)寫鎖模式
具體代碼如下:
func (s Stock) WLock(db *gorm.DB, lockReason string) error { condition := "(id = ?) AND (lock_mark = ?)" fields := map[string]interface{}{ "lock_mark": stateWriteLock, "read_lock_count": 0, "lock_reason": lockReason, } result := db.Model(&Stock{}).Where(condition, s.ID, stateUnlock).Updates(fields) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return errors.New("failed to WLock Stock, RowsAffected=0") } return nil } func (s Stock) WUnlock(db *gorm.DB, UnLockReason string) error { condition := "(id = ?) AND (lock_mark = ?)" fields := map[string]interface{}{ "lock_mark": stateUnlock, "read_lock_count": 0, "lock_reason": UnLockReason, } result := db.Model(&Stock{}).Where(condition, s.ID, stateWriteLock).Updates(fields) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return errors.New("failed to WUnlock Stock, RowsAffected=0") } return nil } func main() { s := &Stock{Model: gorm.Model{ID: 1}} if result := db.Model(s).First(s); result.Error != nil { panic(result.Error) } if err := s.WLock(db, "writeLock_reason_1"); err != nil { panic(err) } if err := s.WUnlock(db, "unWriteLock_reason_1"); err != nil { panic(err) } }
執(zhí)行以上代碼也是可以運行,下面是分析結(jié)果
- 寫鎖的 sql 語句如下,只有在無鎖狀態(tài)下才能加鎖成功
UPDATE `stocks` SET `lock_mark` = 'WriteLock', `lock_reason` = 'writeLock_reason_1', `read_lock_count` = 0, `updated_at` = '2022-09-25 15:06:10.71' WHERE (( id = 1 ) AND ( lock_mark = 'Unlock' )) AND `stocks`.`deleted_at` IS NULL
- 解寫鎖的 sql 語句如下,只有在寫鎖狀態(tài)下才能解寫鎖
UPDATE `stocks` SET `lock_mark` = 'Unlock', `lock_reason` = 'unWriteLock_reason_1', `read_lock_count` = 0, `updated_at` = '2022-09-25 15:06:10.719' WHERE (( id = 1 ) AND ( lock_mark = 'WriteLock' )) AND `stocks`.`deleted_at` IS NULL
總結(jié)
分布式讀寫鎖的實現(xiàn)有多種方式,也可以通過 etcd、redisson 的方式進行實現(xiàn),而本文著重說明可通過 mysql 來實現(xiàn),這種方式的優(yōu)勢在于不必引入額外的組件且實現(xiàn)較為簡單,因此也有一定的應用場景,
到此這篇關(guān)于golang 基于 mysql 簡單實現(xiàn)分布式讀寫鎖的文章就介紹到這了,更多相關(guān)golang 讀寫鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用GoLang?Fiber進行高性能Web開發(fā)實例詳解
這篇文章主要為大家介紹了利用GoLang?Fiber進行高性能Web開發(fā)實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01解決Golang中g(shù)oroutine執(zhí)行速度的問題
這篇文章主要介紹了解決Golang中g(shù)oroutine執(zhí)行速度的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05Go?實現(xiàn)?WebSockets和什么是?WebSockets
這篇文章主要介紹了Go?實現(xiàn)?WebSockets和什么是?WebSockets,WebSockets?是構(gòu)建實時應用程序的第一大解決方案,在線游戲、即時通訊、跟蹤應用程序等,下文相關(guān)內(nèi)容介紹需要的小伙伴可以參考一下2022-04-04關(guān)于golang 字符串 int uint int64 uint64&
這篇文章主要介紹了golang 字符串 int uint int64 uint64 互轉(zhuǎn),本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-01-01