網站首頁 編程語言 正文
前言
在項目中我們經常有需要使用分布式鎖的場景,而Redis是實現分布式鎖最常見的一種方式,這篇文章主要是使用Go+Redis實現互斥鎖和紅鎖。
下面的代碼使用go-redis客戶端和gofakeit庫。
代碼地址
互斥鎖
Redis里有一個設置如果不存在
的命令,我們可以通過這個命令來實現互斥鎖功能,在Redis官方文檔里面推薦的標準實現方式是SET resource_name my_random_value NX PX 30000
這串命令,其中:
-
resource_name
表示要鎖定的資源 -
NX
表示如果不存在則設置 -
PX 30000
表示過期時間為30000毫秒,也就是30秒 -
my_random_value
這個值在所有的客戶端必須是唯一的,所有同一key的鎖競爭者這個值都不能一樣。
值必須是隨機數主要是為了更安全的釋放鎖,釋放鎖的時候使用腳本告訴Redis:只有key存在并且存儲的值和我指定的值一樣才能告訴我刪除成功,避免錯誤釋放別的競爭者的鎖。
由于涉及到兩個操作,因此我們需要通過Lua腳本保證操作的原子性:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
舉個不用Lua腳本的例子:客戶端A取得資源鎖,但是緊接著被一個其他操作阻塞了,當客戶端A運行完畢其他操作后要釋放鎖時,原來的鎖早已超時并且被Redis自動釋放,并且在這期間資源鎖又被客戶端B再次獲取到。
因為判斷和刪除是兩個操作,所以有可能A剛判斷完鎖就過期自動釋放了,然后B就獲取到了鎖,然后A又調用了Del,導致把B的鎖給釋放了。
TryLock和Unlock實現
TryLock
其實就是使用SET resource_name my_random_value NX PX 30000
加鎖,這里使用UUID
作為隨機值,并且在加鎖成功時把隨機值返回,這個隨機值會在Unlock
時使用;
Unlock
解鎖邏輯就是執行前面說到的lua腳本
。
func (l *Lock) TryLock(ctx context.Context) error { success, err := l.client.SetNX(ctx, l.resource, l.randomValue, ttl).Result() if err != nil { return err } // 加鎖失敗 if !success { return ErrLockFailed } // 加鎖成功 l.randomValue = randomValue return nil } func (l *Lock) Unlock(ctx context.Context) error { return l.script.Run(ctx, l.client, []string{l.resource}, l.randomValue).Err() }
Lock實現
Lock
是阻塞的獲取鎖,因此在加鎖失敗的時候,需要重試。當然也可能出現其他異常情況(比如網絡問題,請求超時等),這些情況則直接返回error
。
步驟如下:
- 嘗試加鎖,加鎖成功直接返回
- 加鎖失敗則不斷循環嘗試加鎖直到成功或出現異常情況
func (l *Lock) Lock(ctx context.Context) error { // 嘗試加鎖 err := l.TryLock(ctx) if err == nil { return nil } if !errors.Is(err, ErrLockFailed) { return err } // 加鎖失敗,不斷嘗試 ticker := time.NewTicker(l.tryLockInterval) defer ticker.Stop() for { select { case <-ctx.Done(): // 超時 return ErrTimeout case <-ticker.C: // 重新嘗試加鎖 err := l.TryLock(ctx) if err == nil { return nil } if !errors.Is(err, ErrLockFailed) { return err } } } }
實現看門狗機制
我們前面的例子中提到的互斥鎖有一個小問題,就是如果持有鎖客戶端A被阻塞,那么A的鎖可能會超時被自動釋放,導致客戶端B提前獲取到鎖。
為了減少這種情況的發生,我們可以在A持有鎖期間,不斷地延長鎖的過期時間,減少客戶端B提前獲取到鎖的情況,這就是看門狗機制。
當然,這沒辦法完全避免上述情況的發生,因為如果客戶端A獲取鎖之后,剛好與Redis的連接關閉了,這時候也就沒辦法延長超時時間了。
看門狗實現
加鎖成功時啟動一個線程,不斷地延長鎖地過期時間;在Unlock時關閉看門狗線程。
看門狗流程如下:
- 加鎖成功,啟動看門狗
- 看門狗線程不斷延長鎖的過程時間
- 解鎖,關閉看門狗
func (l *Lock) startWatchDog() { ticker := time.NewTicker(l.ttl / 3) defer ticker.Stop() for { select { case <-ticker.C: // 延長鎖的過期時間 ctx, cancel := context.WithTimeout(context.Background(), l.ttl/3*2) ok, err := l.client.Expire(ctx, l.resource, l.ttl).Result() cancel() // 異?;蜴i已經不存在則不再續期 if err != nil || !ok { return } case <-l.watchDog: // 已經解鎖 return } } }
TryLock:啟動看門狗
func (l *Lock) TryLock(ctx context.Context) error { success, err := l.client.SetNX(ctx, l.resource, l.randomValue, l.ttl).Result() if err != nil { return err } // 加鎖失敗 if !success { return ErrLockFailed } // 加鎖成功,啟動看門狗 go l.startWatchDog() return nil }
Unlock:關閉看門狗
func (l *Lock) Unlock(ctx context.Context) error { err := l.script.Run(ctx, l.client, []string{l.resource}, l.randomValue).Err() // 關閉看門狗 close(l.watchDog) return err }
紅鎖
由于上面的實現是基于單Redis實例,如果這個唯一的實例掛了,那么所有請求都會因為拿不到鎖而失敗,為了提高容錯性,我們可以使用多個分布在不同機器上的Redis實例,并且只要拿到其中大多數節點的鎖就能加鎖成功,這就是紅鎖算法。它其實也是基于上面的單實例算法的,只是我們需要同時對多個Redis實例獲取鎖。
加鎖實現
在加鎖邏輯里,我們主要是對每個Redis實例執行SET resource_name my_random_value NX PX 30000
獲取鎖,然后把成功獲取鎖的客戶端放到一個channel
里(這里因為是多線程并發獲取鎖,使用slice可能有并發問題),同時使用sync.WaitGroup
等待所有獲取鎖操作結束。
然后判斷成功獲取到的鎖的數量是否大于一半,如果沒有得到一半以上的鎖,說明加鎖失敗,釋放已經獲得的鎖。
如果加鎖成功,則啟動看門狗延長鎖的過期時間。
func (l *RedLock) TryLock(ctx context.Context) error { randomValue := gofakeit.UUID() var wg sync.WaitGroup wg.Add(len(l.clients)) // 成功獲得鎖的Redis實例的客戶端 successClients := make(chan *redis.Client, len(l.clients)) for _, client := range l.clients { go func(client *redis.Client) { defer wg.Done() success, err := client.SetNX(ctx, l.resource, randomValue, ttl).Result() if err != nil { return } // 加鎖失敗 if !success { return } // 加鎖成功,啟動看門狗 go l.startWatchDog() successClients <- client }(client) } // 等待所有獲取鎖操作完成 wg.Wait() close(successClients) // 如果成功加鎖得客戶端少于客戶端數量的一半+1,表示加鎖失敗 if len(successClients) < len(l.clients)/2+1 { // 就算加鎖失敗,也要把已經獲得的鎖給釋放掉 for client := range successClients { go func(client *redis.Client) { ctx, cancel := context.WithTimeout(context.Background(), ttl) l.script.Run(ctx, client, []string{l.resource}, randomValue) cancel() }(client) } return ErrLockFailed } // 加鎖成功,啟動看門狗 l.randomValue = randomValue l.successClients = nil for successClient := range successClients { l.successClients = append(l.successClients, successClient) } return nil }
看門狗實現
我們需要延長所有成功獲取到的鎖的過期時間。
func (l *RedLock) startWatchDog() { l.watchDog = make(chan struct{}) ticker := time.NewTicker(resetTTLInterval) defer ticker.Stop() for { select { case <-ticker.C: // 延長鎖的過期時間 for _, client := range l.successClients { go func(client *redis.Client) { ctx, cancel := context.WithTimeout(context.Background(), ttl-resetTTLInterval) client.Expire(ctx, l.resource, ttl) cancel() }(client) } case <-l.watchDog: // 已經解鎖 return } } }
解鎖實現
我們需要解鎖所有成功獲取到的鎖。
func (l *RedLock) Unlock(ctx context.Context) error { for _, client := range l.successClients { go func(client *redis.Client) { l.script.Run(ctx, client, []string{l.resource}, l.randomValue) }(client) } // 關閉看門狗 close(l.watchDog) return nil }
原文鏈接:https://juejin.cn/post/7148391514966589477
相關推薦
- 2022-09-25 MongoDB聚合管道 $lookup 與$mergeObjects配合使用 以及使用let,pip
- 2022-06-21 Windows下安裝Git_其它綜合
- 2022-12-09 python淺拷貝與深拷貝使用方法詳解_python
- 2022-03-17 Golang動態調用方法小結_Golang
- 2022-10-21 一文詳解?Compose?Navigation?的實現原理_Android
- 2022-09-04 python開發任意表達式求值全功能示例_python
- 2022-12-28 jquery點擊獲取動態數據進行傳參問題_jquery
- 2023-06-04 React中的合成事件是什么原理_React
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支