分布式任务解决方案

  1. 背景
    在业务开发过程中,你可能会碰到有任务形式的场景。比如Client请求Proxy GateWay(同时后端有若干机器), 要求执行某个(执行时间不定)的任务。如何去保证任务被执行一次。或者你的第一反应是,根据请求参数Hash取模似的统一请求只打到特定机器,但是如上场景明显不能做到,因为Proxy不受你控制。下面提供两种可行,简单的方式。

    a. MySQL行锁(FOR UPDATE)
    b. Redis分布式锁
    c. 当然你也可以使用MQ这种重量级应用
    d. …..(方法很多,选择合适的)

  2. 思考
    如果并发不打,使用DB锁,也是特别方便,但是碰到有一定并发的场景,流量自然直接穿透到后端,对DB造成一定压力。所以这里详细介绍下使用Redis锁的一种实现方式,当然值得注意的是,如果你的任务时间不定长,你需要去reset过期时间,防止任务还在,但是锁释放了。
    下面,提供一个简版的Redis锁,原因是厂里目前只有一主多从并且根据Key哈希取模形式的Slave,所以无法使用redlock-rb算法来实现更加可靠的锁。当然了,如果业务能够容忍,下面这种实现,也是没什么问题。

  3. 实现(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
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章