業務背景: 後臺定時任務刷新Redis的數據到數據庫中,有多臺機器開啓了此定時同步的任務,但是需要其中一臺工作,其他的作爲備用,提高可用性。使用Redis分佈式鎖進行限制,拿到鎖的機器去執行具體業務,拿不到鎖的繼續輪詢。
分佈式鎖原理
分佈式鎖:當多個進程不在同一個系統中,多個進程共同競爭同一個資源,用分佈式鎖控制多個進程對資源的互斥訪問。採用Redis服務器存儲鎖信息(即SET一個Key表示已加鎖),可以實現多進程的併發讀鎖的狀態,如果沒有鎖,則只允許一個進程加鎖。
Redis分佈式鎖實現的關鍵點:
問題 | 問題描述 | 解決方案 |
---|---|---|
互斥性 | 保證只有一個client可以獲取資源 | 加鎖 |
原子性 | 如果鎖不存在則執行加鎖操作,必須是原子性操作 | 原子性命令或者執行Lua腳本 |
避免死鎖 | 當拿到鎖的Client因宕機或網絡原因斷線後,如果鎖不能釋放就會產生死鎖 | 爲鎖加超時時間 |
鎖超時時間設定 | 鎖超時時間到了,業務沒執行完問題 | 心跳線程,不斷更新鎖超時時間 |
鎖的所屬權 | 解鈴還須繫鈴人,加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。 | Client與鎖進行一一對應,使用UUID作爲鎖的值 |
自動重連 | 網絡故障導致Client連接Redis失敗的情況,網絡恢復後可以自動重連 | 輪詢 |
實現方案
方案一:採用Redis的原子性命令“SET key value EX expire-time NX”可以實現分佈式鎖的基本功能,其中的NX(Not Exist)即判斷是否已存在鎖,如果不存在key則可進行操作,SET key value 等同於加鎖,EX expire-time即設置超時時間,可以避免死鎖,但是超時時間的設置需要根據具體業務設置一個合理的經驗值,避免鎖超時時間到了,業務沒執行完的問題。
方案二:採用Lua腳本實現,Redis會將整個腳本作爲一個整體執行,因此Lua腳本可以實現原子性操作。相較於方案一,此處增加了心跳線程,不斷更新鎖超時時間,解決鎖超時時間設置不合理的問題;生成UUID(或者是隨機數字符串)作爲鎖的值,用於保證鎖與Client的一一對應;採用輪詢來實現斷線自動重連。
Talk is cheap. Show me the code.
實現方案1:SET EX NX
加鎖流程圖:
定義鎖的變量名爲lock,那麼對應Redis命令:
判斷是否加鎖的命令:GET lock
加鎖的命令:SET lock
設置超時時間的命令:EXPIRE expire-time
三條命令分開執行是不具有原子性的,比如可能會出現一個進程執行GET lock得到的結果爲nil即尚未加鎖,在其執行SET lock前另一個進程也執行了SET lock,導致兩個進程都認爲是可以加鎖的,失去互斥性。
因此判斷尚未加鎖、加鎖、設置超時時間必須原子操作,使用Redis的命令“SET key value EX expire-time NX”可以實現該原子操作。
package main
import (
"fmt"
"time"
"github.com/gomodule/redigo/redis"
)
func main() {
rds, err := redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
fmt.Println("Connect to redis error", err)
return
}
defer rds.Close()
for true {
//檢查是否有所與加鎖必須是原子性操作
result, err := rds.Do("SET", "lock", 1, "EX", 5, "NX")
if err != nil {
fmt.Println("redis set error.", err)
return
}
result, err = redis.String(result, err)
// 加鎖失敗,繼續輪詢
if result != "OK" {
fmt.Println("SET lock failed.")
time.Sleep(5 * time.Second)
continue
}
// 加鎖成功
fmt.Println("work begining...")
// 此處處理業務
fmt.Println("work end");
// 業務處理結束後釋放鎖
result, err := rds.Do("del", "lock")
break;
}
}
此方法弊端是對超時時間的設置有要求,需要根據具體業務設置一個合理的經驗值,避免鎖超時時間到了,業務沒執行完的問題。
實現方案2:Lua腳本
package main
import (
"fmt"
"time"
"github.com/satori/go.uuid"
"github.com/gomodule/redigo/redis"
)
var uuidClient uuid.UUID
const (
SCRIPT_LOCK = `
local res=redis.call('GET', KEYS[1])
if res then
return 0
else
redis.call('SET',KEYS[1],ARGV[1]);
redis.call('EXPIRE',KEYS[1],ARGV[2])
return 1
end
`
SCRIPT_EXPIRE = `
local res=redis.call('GET', KEYS[1])
if not res then
return -1
end
if res==ARGV[1] then
redis.call('EXPIRE', KEYS[1], ARGV[2])
return 1
else
return 0
end
`
SCRIPT_DEL = `
local res=redis.call('GET', KEYS[1])
if not res then
return -1
end
if res==ARGV[1] then
redis.call('DEL', KEYS[1])
else
return 0
end
`
)
func ResetExpire() {
fmt.Println("Reset expire begin...")
rds, err := redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
fmt.Println("Connect to redis server error.", err)
return
}
for true {
luaExpire := redis.NewScript(1, SCRIPT_EXPIRE)
result, err := redis.Int(luaExpire.Do(rds, "lock", uuidClient.String(), 5))
if err != nil {
fmt.Println("luaExpire exec error", err)
break
}
if result != 1 {
fmt.Println("Reset expire failed.")
break
} else {
fmt.Println("Reset expire succeed.")
}
time.Sleep(3 * time.Second)
}
fmt.Println("Reset expire end.")
}
func main() {
for true {
rds, err := redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
fmt.Println("Connect to redis server error.", err)
time.Sleep(5 * time.Second)
continue
}
defer rds.Close()
// 生成UUID標識鎖與Client的對應關係
uuidClient,err = uuid.NewV4() //也可以生成隨機數字符串來代替
if err != nil {
fmt.Println("New uuid error.", err)
return
}
luaLock := redis.NewScript(1, SCRIPT_LOCK)
luaDel:= redis.NewScript(1, SCRIPT_DEL)
for true {
result, err := redis.Int(luaLock.Do(rds, "lock", uuidClient.String(), 5))
if err != nil {
fmt.Println("luaLock exec error.", err)
break
}
if result == 0 {
fmt.Println("Set lock failed.")
time.Sleep(5 * time.Second)
continue
}
fmt.Println("Set lock succeed.")
go ResetExpire()
// 加鎖成功
fmt.Println("work begining...")
// 此處處理業務
fmt.Println("work end");
// 業務處理結束後釋放鎖
result, err = redis.Int(luaDel.Do(rds, "lock", uuidClient.String()))
return
}
}
}
Redis採用Lua腳本可以執行更多的個性化的原子操作,在我項目中就採用這種容錯性更高的方式。