分佈式任務解決方案

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