Golang 基於單節點 Redis 實現的分散式鎖

phpdi發表於2020-07-05

原子性加鎖

加鎖問題,為避免死鎖,需要加鎖的時候為鎖設定一個存活時間,過了這個時間,鎖自動被釋放掉

安全釋放鎖

1.安全釋放鎖需要達到一個目的,A客戶端加鎖,必須由A客戶端釋放鎖或者鎖超時自動釋放
2.鎖重入問題,A執行緒由於業務處理時間過長,比存活時間還要長,鎖到期自動釋放,就會導致A程式去釋放掉其它程式產生的鎖
3.為了解決鎖重入問題,我們需要在A客戶端業務邏輯處理過程中,延長這個鎖的存活時間,防止業務未處理完成導致鎖自動釋放.(租約,自動續期)

程式碼

package dislock

import (
    "context"
    "errors"
    "github.com/gomodule/redigo/redis"
    "log"
    "time"
)

//基於redis的分散式鎖
type DisLockRedis struct {
    key       string             //鎖名稱
    ttl       int64              //鎖超時時間
    isLocked  bool               //上鎖成功標識
    cancelFun context.CancelFunc //用於取消自動續租攜程

    redis *redis.Pool
    debug bool
}

func NewDisLockRedis(key string, redis *redis.Pool) *DisLockRedis {
    this := &DisLockRedis{
        key:   key,
        ttl:   30,
        redis: redis,
    }

    return this
}

//上鎖
func (this *DisLockRedis) TryLock() (err error) {
    if err = this.grant(); err != nil {
        return
    }

    ctx, cancelFun := context.WithCancel(context.TODO())

    this.cancelFun = cancelFun
    //自動續期
    this.renew(ctx)

    this.isLocked = true

    return nil
}

//釋放鎖
func (this *DisLockRedis) Unlock() (err error) {
    var res int
    if this.isLocked {
        if res, err = redis.Int(this.redisConn().Do("DEL", this.key)); err != nil {

            if this.debug {
                log.Println(err.Error())
            }
            return errors.New("釋放鎖失敗")
        }

        if res == 1 {
            //釋放成功,取消自動續租
            this.cancelFun()
            return
        }
    }

    return errors.New("釋放鎖失敗")

}

//自動續期
func (this *DisLockRedis) renew(ctx context.Context) {

    go func() {

        for {
            select {
            case <-ctx.Done():
                return
            default:
                res, err := redis.Int(this.redisConn().Do("EXPIRE", this.key, this.ttl))
                if this.debug {
                    if err != nil {
                        log.Println("鎖自動續期失敗:", err)
                    }

                    if res != 1 {
                        log.Println("鎖自動續期失敗")
                    }
                }
            }

            time.Sleep(time.Duration(this.ttl/3) * time.Second)
        }
    }()

}

//建立租約
func (this *DisLockRedis) grant() (err error) {

    if res, err := redis.String(this.redisConn().Do("SET", this.key, "xxx", "NX", "EX", this.ttl)); err != nil {
        if this.debug {
            log.Println(err)
        }

    } else {
        if res == "OK" {
            return nil
        }
    }

    return errors.New("上鎖失敗")
}

func (this *DisLockRedis) redisConn() redis.Conn {
    return this.redis.Get()
}

func (this *DisLockRedis) Debug() *DisLockRedis {
    this.debug = true
    return this
}

注意

每次使用鎖,都必須呼叫NewDisLockRedis新建物件
如有問題歡迎指正

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章