Go與Redis實現(xiàn)分布式互斥鎖和紅鎖
前言
在項目中我們經(jīng)常有需要使用分布式鎖的場景,而Redis是實現(xiàn)分布式鎖最常見的一種方式,這篇文章主要是使用Go+Redis實現(xiàn)互斥鎖和紅鎖。
互斥鎖
Redis里有一個設(shè)置如果不存在的命令,我們可以通過這個命令來實現(xiàn)互斥鎖功能,在Redis官方文檔里面推薦的標(biāo)準(zhǔn)實現(xiàn)方式是SET resource_name my_random_value NX PX 30000這串命令,其中:
resource_name表示要鎖定的資源NX表示如果不存在則設(shè)置PX 30000表示過期時間為30000毫秒,也就是30秒my_random_value這個值在所有的客戶端必須是唯一的,所有同一key的鎖競爭者這個值都不能一樣。
值必須是隨機(jī)數(shù)主要是為了更安全的釋放鎖,釋放鎖的時候使用腳本告訴Redis:只有key存在并且存儲的值和我指定的值一樣才能告訴我刪除成功,避免錯誤釋放別的競爭者的鎖。
由于涉及到兩個操作,因此我們需要通過Lua腳本保證操作的原子性:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end舉個不用Lua腳本的例子:客戶端A取得資源鎖,但是緊接著被一個其他操作阻塞了,當(dāng)客戶端A運(yùn)行完畢其他操作后要釋放鎖時,原來的鎖早已超時并且被Redis自動釋放,并且在這期間資源鎖又被客戶端B再次獲取到。
因為判斷和刪除是兩個操作,所以有可能A剛判斷完鎖就過期自動釋放了,然后B就獲取到了鎖,然后A又調(diào)用了Del,導(dǎo)致把B的鎖給釋放了。
TryLock和Unlock實現(xiàn)
TryLock其實就是使用SET resource_name my_random_value NX PX 30000加鎖,這里使用UUID作為隨機(jī)值,并且在加鎖成功時把隨機(jī)值返回,這個隨機(jī)值會在Unlock時使用;
Unlock解鎖邏輯就是執(zhí)行前面說到的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實現(xiàn)
Lock是阻塞的獲取鎖,因此在加鎖失敗的時候,需要重試。當(dāng)然也可能出現(xiàn)其他異常情況(比如網(wǎng)絡(luò)問題,請求超時等),這些情況則直接返回error。
步驟如下:
- 嘗試加鎖,加鎖成功直接返回
- 加鎖失敗則不斷循環(huán)嘗試加鎖直到成功或出現(xiàn)異常情況
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
}
}
}
}實現(xiàn)看門狗機(jī)制
我們前面的例子中提到的互斥鎖有一個小問題,就是如果持有鎖客戶端A被阻塞,那么A的鎖可能會超時被自動釋放,導(dǎo)致客戶端B提前獲取到鎖。
為了減少這種情況的發(fā)生,我們可以在A持有鎖期間,不斷地延長鎖的過期時間,減少客戶端B提前獲取到鎖的情況,這就是看門狗機(jī)制。
當(dāng)然,這沒辦法完全避免上述情況的發(fā)生,因為如果客戶端A獲取鎖之后,剛好與Redis的連接關(guān)閉了,這時候也就沒辦法延長超時時間了。
看門狗實現(xiàn)
加鎖成功時啟動一個線程,不斷地延長鎖地過期時間;在Unlock時關(guān)閉看門狗線程。
看門狗流程如下:
- 加鎖成功,啟動看門狗
- 看門狗線程不斷延長鎖的過程時間
- 解鎖,關(guān)閉看門狗
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()
// 異常或鎖已經(jīng)不存在則不再續(xù)期
if err != nil || !ok {
return
}
case <-l.watchDog:
// 已經(jīng)解鎖
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:關(guān)閉看門狗
func (l *Lock) Unlock(ctx context.Context) error {
err := l.script.Run(ctx, l.client, []string{l.resource}, l.randomValue).Err()
// 關(guān)閉看門狗
close(l.watchDog)
return err
}紅鎖
由于上面的實現(xiàn)是基于單Redis實例,如果這個唯一的實例掛了,那么所有請求都會因為拿不到鎖而失敗,為了提高容錯性,我們可以使用多個分布在不同機(jī)器上的Redis實例,并且只要拿到其中大多數(shù)節(jié)點的鎖就能加鎖成功,這就是紅鎖算法。它其實也是基于上面的單實例算法的,只是我們需要同時對多個Redis實例獲取鎖。
加鎖實現(xiàn)
在加鎖邏輯里,我們主要是對每個Redis實例執(zhí)行SET resource_name my_random_value NX PX 30000獲取鎖,然后把成功獲取鎖的客戶端放到一個channel里(這里因為是多線程并發(fā)獲取鎖,使用slice可能有并發(fā)問題),同時使用sync.WaitGroup等待所有獲取鎖操作結(jié)束。
然后判斷成功獲取到的鎖的數(shù)量是否大于一半,如果沒有得到一半以上的鎖,說明加鎖失敗,釋放已經(jīng)獲得的鎖。
如果加鎖成功,則啟動看門狗延長鎖的過期時間。
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)
// 如果成功加鎖得客戶端少于客戶端數(shù)量的一半+1,表示加鎖失敗
if len(successClients) < len(l.clients)/2+1 {
// 就算加鎖失敗,也要把已經(jīng)獲得的鎖給釋放掉
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
}看門狗實現(xiàn)
我們需要延長所有成功獲取到的鎖的過期時間。
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:
// 已經(jīng)解鎖
return
}
}
}解鎖實現(xiàn)
我們需要解鎖所有成功獲取到的鎖。
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)
}
// 關(guān)閉看門狗
close(l.watchDog)
return nil
}到此這篇關(guān)于Go與Redis實現(xiàn)分布式互斥鎖和紅鎖的文章就介紹到這了,更多相關(guān)Go Redis分布式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
GoFrame框架數(shù)據(jù)校驗之校驗結(jié)果Error接口對象
這篇文章主要為大家介紹了GoFrame框架數(shù)據(jù)校驗之校驗結(jié)果Error接口對象示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
go-cqhttp權(quán)限管理系統(tǒng)的實現(xiàn)代碼
這篇文章主要介紹了go-cqhttp權(quán)限管理,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-09-09
Golang語言如何讀取http.Request中body的內(nèi)容
這篇文章主要介紹了Golang語言如何讀取http.Request中body的內(nèi)容問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03
go?打包運(yùn)行文件在windows,liunx運(yùn)行
這篇文章主要介紹了go?打包運(yùn)行文件在windows,liunx運(yùn)行的相關(guān)資料,需要的朋友可以參考下2023-11-11

