背景
在业务开发过程中,你可能会碰到有任务形式的场景。比如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
}