背景
在業務開發過程中,你可能會碰到有任務形式的場景。比如Client請求Proxy GateWay(同時後端有若干機器), 要求執行某個(執行時間不定)的任務。如何去保證任務被執行一次。或者你的第一反應是,根據請求參數Hash取模似的統一請求只打到特定機器,但是如上場景明顯不能做到,因爲Proxy不受你控制。下面提供兩種可行,簡單的方式。a. MySQL行鎖(FOR UPDATE)
b. Redis分佈式鎖
c. 當然你也可以使用MQ這種重量級應用
d. …..(方法很多,選擇合適的)思考
如果併發不打,使用DB鎖,也是特別方便,但是碰到有一定併發的場景,流量自然直接穿透到後端,對DB造成一定壓力。所以這裏詳細介紹下使用Redis鎖的一種實現方式,當然值得注意的是,如果你的任務時間不定長,你需要去reset過期時間,防止任務還在,但是鎖釋放了。
下面,提供一個簡版的Redis鎖,原因是廠裏目前只有一主多從並且根據Key哈希取模形式的Slave,所以無法使用redlock-rb算法來實現更加可靠的鎖。當然了,如果業務能夠容忍,下面這種實現,也是沒什麼問題。實現(Show me your code)
// NOTICE
//
// Mi redis集羣基於Key分片,故在Key確定情況下可認爲集羣爲一主多從模式
// 此Redis鎖有一個前提:即 Redis單點的、保證永不宕機(https://github.com/antirez/redis-doc/blob/master/topics/distlock.md)
package utils
import (
"errors"
"fmt"
"sync"
"time"
xredis "path/to/redis"
)
var (
//ErrLockerExisted 重複加鎖
ErrLockerExisted = errors.New("Locker Existed")
//ErrLockerLost 鎖丟失
ErrLockerLost = errors.New("Locker Lost")
)
var (
//lockerQueue 全局鎖集合,loops with keys that are stable over time
lockerQueue *sync.Map
redisImp xredis.XRedisInterface
)
//addScript Lua腳本加鎖
var addScript = `return redis.call("set", KEYS[1], ARGV[1], "nx", "px", ARGV[2])`
//delScript Lua腳本刪除
var delScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return "ERR"
end`
//touchScript Lua腳本刷新過期時間
var touchScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("set", KEYS[1], ARGV[1], "xx", "px", ARGV[2])
else
return "ERR"
end`
//注: 以上Lua腳本僅僅是爲了保證鎖操作方爲加鎖方,無特別作用(可用if Get then Cmd 替代)
//InitLocker 初始化
func InitLocker(redis xredis.XRedisInterface) {
lockerQueue = &sync.Map{}
redisImp = redis
go func() {
ticker := time.NewTicker(10 * time.Second) //每10秒刷新locker Key的過期時間
for {
select {
case <-ticker.C:
removeExpiredKey()
}
}
}()
}
func removeExpiredKey() {
var expireKeys []string
lockerQueue.Range(
func(k, v interface{}) bool {
key := k.(string)
val := v.(string)
reset := int64(15 * time.Second / time.Millisecond) //刷新時間爲15秒
res, err := redisImp.Eval(touchScript, []interface{}{key}, []interface{}{val, reset})
if err != nil {
return true
}
if res != "ERR" && res != "OK" {
expireKeys = append(expireKeys, key)
}
return true
})
//刪除過期key
for _, key := range expireKeys {
lockerQueue.Delete(key)
}
}
//Locker 鎖實例
type Locker struct {
key interface{}
}
//NewLocker 創建鎖
func NewLocker(key interface{}) *Locker {
locker := &Locker{
key: key,
}
return locker
}
//GetKey ..
func (locker *Locker) GetKey() interface{} {
return locker.key
}
//Lock 加Redis鎖
func (locker *Locker) Lock() error {
key := locker.key
hash := ToMd5(key.(string))
val := fmt.Sprintf("PulseAPI_Global_locker_%v_%v", time.Now().UnixNano(), hash)
reset := int64(15 * time.Second / time.Millisecond) //刷新時間爲15秒
res, err := redisImp.Eval(addScript, []interface{}{key}, []interface{}{val, reset})
if err != nil {
err = fmt.Errorf("[LOCKER] Fail to exec Lua-addScript [Key: %v][Val: %v][Err: %v]", key, val, err)
return err
}
if res != "OK" {
return ErrLockerExisted
}
lockerQueue.Store(key, val)
return nil
}
//UnLock 解鎖
func (locker *Locker) UnLock() error {
key := locker.key
val, ok := lockerQueue.Load(key)
if !ok {
err := fmt.Errorf("[LOCKER] Key[%v] is lost", key)
return err
}
lockerQueue.Delete(key) // 清除刷新隊列
res, err := redisImp.Eval(delScript, []interface{}{key}, []interface{}{val})
if err != nil {
err = fmt.Errorf("[LOCKER] Fail to exec Lua-delScript [Key: %v][Val: %v][Err: %v]", key, val, err)
return err
}
if res != int64(1) {
return ErrLockerLost
}
return nil
}
// Remove 清除, 即不立刻Unlock
func (locker *Locker) Remove() error {
key := locker.key
lockerQueue.Delete(key) // 清除刷新隊列
return nil
}