golang?基于?mysql?簡(jiǎn)單實(shí)現(xiàn)分布式讀寫鎖
業(yè)務(wù)場(chǎng)景
因?yàn)轫?xiàng)目剛上線,目前暫不打算引入其他中間件,所以打算通過 mysql 來實(shí)現(xiàn)分布式讀寫鎖;而該業(yè)務(wù)場(chǎng)景也滿足分布式讀寫鎖的場(chǎng)景,抽象后的業(yè)務(wù)場(chǎng)景是:特定資源 X,可以執(zhí)行 2 種操作:讀操作和寫操作,2種操作需要滿足下面條件:
- 執(zhí)行操作的機(jī)器分布式在不同的節(jié)點(diǎn)中,也就是分布式的;
- 讀操作是共享的,也就是說同時(shí)可以有多個(gè) goroutine 對(duì)資源 X 執(zhí)行讀操作;
- 寫操作是互斥的,也就是說同一時(shí)刻只允許有一個(gè) goroutine 對(duì)資源 X 執(zhí)行寫操作;
- 讀操作和寫操作是互斥的,也就是說寫操作和讀操作不能同時(shí)存在
既然需要如此實(shí)現(xiàn),下面我們看下什么是分布式讀寫鎖。
什么是分布式讀寫鎖
大家對(duì)于鎖肯定不陌生,在 golang 中 sync.Mutex 鎖是常見的,一般用在單節(jié)點(diǎn)多 goroutine 中對(duì)資源的并發(fā)訪問;但是分布式場(chǎng)景下,單節(jié)點(diǎn) sync.Mutex 加鎖的方式就會(huì)失去作用,于是人們?yōu)榱嗽诜植际江h(huán)境中實(shí)現(xiàn)對(duì)共享資源的互斥訪問,實(shí)現(xiàn)了各種分布式鎖。
而分布式讀寫鎖是比分布式鎖粒度更小的鎖,對(duì)業(yè)務(wù)場(chǎng)景的加鎖會(huì)更加靈活,其中分布式讀寫鎖也遵循讀寫鎖的原則:
- 讀模式共享,寫模式互斥。
- 它三種模式狀態(tài): 讀加鎖狀態(tài)、寫加鎖狀態(tài)、無鎖狀態(tài)。
分布式讀寫鎖的訪問原則與讀寫鎖類似,下面我們具體看下。
分布式讀寫鎖的訪問原則
以下列表為讀寫鎖(也就是分布式讀寫鎖)的讀寫訪問原則
| 當(dāng)前鎖狀態(tài) | 讀鎖請(qǐng)求 | 寫鎖請(qǐng)求 |
|---|---|---|
| 無鎖狀態(tài) | 可以 | 可以 |
| 讀鎖狀態(tài) | 可以 | 不可以 |
| 寫鎖狀態(tài) | 不可以 | 不可以 |
讀鎖
- 只有在無鎖和讀鎖下可以獲取讀鎖。
- 讀鎖的模式下,任何請(qǐng)求讀鎖都可以。
- 讀鎖的模式下, 請(qǐng)求寫鎖不可以,直到所有讀鎖解鎖,寫鎖才能獲取到鎖。
寫鎖
- 只有在無鎖狀態(tài)下可以獲取寫鎖。
- 寫鎖的模式下,任何請(qǐng)求讀鎖和寫鎖都阻塞,直到寫鎖解鎖。
具體實(shí)現(xiàn)
如果本地沒有 mysql 數(shù)據(jù)庫,可以通過這篇文章快速搭建: 如何使用 docker 搭建一個(gè) mysql 服務(wù)
通過 gorm 連接 mysql
gorm 是一個(gè) 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)
}
}首先我們定義了一個(gè)庫存表 stocks,并且在其中添加三個(gè)和讀寫鎖相關(guān)的字段,三個(gè)字段的含義如下:
- LockMark: 表示某條數(shù)據(jù)加鎖的狀態(tài),只能是讀鎖、寫鎖、無鎖狀態(tài)中的一種。
- ReadLockCount: 首先讀模式是共享的,意味著可以有多個(gè) goroutine 并發(fā)訪問,而 ReadLockCount 字段則記錄當(dāng)前并發(fā)訪問的 goroutine 數(shù)量。
- LockReason: 記錄當(dāng)前加鎖的原因;讀鎖是最新的 goroutine 的 lockReason,寫鎖則是寫鎖 goroutine 的 lockReason。
其余則是一些 gorm 連接 mysql 邏輯,這里不再多贅述。
實(shí)現(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í)行以上代碼是可以正常運(yùn)行的, 下面我們分析下:
- 讀鎖的 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'實(shí)現(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í)行以上代碼也是可以運(yùn)行,下面是分析結(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é)
分布式讀寫鎖的實(shí)現(xiàn)有多種方式,也可以通過 etcd、redisson 的方式進(jìn)行實(shí)現(xiàn),而本文著重說明可通過 mysql 來實(shí)現(xiàn),這種方式的優(yōu)勢(shì)在于不必引入額外的組件且實(shí)現(xiàn)較為簡(jiǎn)單,因此也有一定的應(yīng)用場(chǎng)景,
到此這篇關(guān)于golang 基于 mysql 簡(jiǎn)單實(shí)現(xiàn)分布式讀寫鎖的文章就介紹到這了,更多相關(guān)golang 讀寫鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用GoLang?Fiber進(jìn)行高性能Web開發(fā)實(shí)例詳解
這篇文章主要為大家介紹了利用GoLang?Fiber進(jìn)行高性能Web開發(fā)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
解決Golang中g(shù)oroutine執(zhí)行速度的問題
這篇文章主要介紹了解決Golang中g(shù)oroutine執(zhí)行速度的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-05-05
Go?實(shí)現(xiàn)?WebSockets和什么是?WebSockets
這篇文章主要介紹了Go?實(shí)現(xiàn)?WebSockets和什么是?WebSockets,WebSockets?是構(gòu)建實(shí)時(shí)應(yīng)用程序的第一大解決方案,在線游戲、即時(shí)通訊、跟蹤應(yīng)用程序等,下文相關(guān)內(nèi)容介紹需要的小伙伴可以參考一下2022-04-04
關(guān)于golang 字符串 int uint int64 uint64&
這篇文章主要介紹了golang 字符串 int uint int64 uint64 互轉(zhuǎn),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-01-01

