Golang可重入锁的实现

技术杂谈67

项目中遇到了可重入锁的需求和实现,具体记录下。

什么是可重入锁

*[En]*

**

Lock();
DoJob();
Unlock();

但是由于我们的系统都是分布式的,这个锁一般不会只放在某个进程中,我们会借用第三方存储,比如 Redis 来做这种分布式锁。但是一旦借助了第三方存储,我们就必须面对这个问题:Unlock是否能保证一定运行呢?

这个问题,我们面对的除了程序的bug之外,还有网络的不稳定,进程被杀死,服务器被down机等。我们是无法保证Unlock一定被运行的。

那么我们就一般在Lock的时候为这个锁加一个超时时间作为兜底。

LockByExpire(duration);
DoJob();
Unlock();

这个超时时间是为了一旦出现异常情况导致Unlock没有被运行,这个锁在duration时间内也会被自动释放。这个在redis中我们一般就是使用 set ex 来进行锁超时的设定。

但是有这个超时时间我们又遇上了问题,超时时间设置多久合适呢?当然要设置的比 DoJob 消耗的时间更长,否则的话,在任务还没结束的时候,锁就被释放了,还是有可能导致并发任务的存在。

但是实际上,同样由于网络超时问题,系统运行状况问题等,我们是无法准确知道DoJob这个函数要执行多久的。那么这时候怎么办呢?

有两个办法:

第一个方法,我们可以对DoJob做一个超时设置。让DoJob最多只能执行n秒,那么我的分布式锁的超时时长设置比n秒长就可以了。为一个任务设置超时时间在很多语言是可以做到的。比如golang 中的 TimeoutContext。

*[En]*

**

除了主线程之外,可重入锁必然有一个另外的线程(或者携程)可以对这个锁进行续期,我们叫这个额外的程序叫做watchDog(看门狗)。

具体实现

在Golang中,语言级别天生支持协程,所以这种可重入锁就非常容易实现:

// DistributeLockRedis 基于redis的分布式可重入锁,自动续租
type DistributeLockRedis struct {
    key       string             // 锁的key
    expire    int64              // 锁超时时间
    status    bool               // 上锁成功标识
    cancelFun context.CancelFunc // 用于取消自动续租携程
    redis     redis.Client       // redis句柄
}

// 创建可
func NewDistributeLockRedis(key string, expire int64) *DistributeLockRedis {
    return &DistributeLockRedis{
         key : key,
         expire : expire,
    }
}

// TryLock 上锁
func (dl *DistributeLockRedis) TryLock() (err error) {
    if err = dl.lock(); err != nil {
        return err
    }
    ctx, cancelFun := context.WithCancel(context.Background())
    dl.cancelFun = cancelFun
    dl.startWatchDog(ctx) // 创建守护协程,自动对锁进行续期
    dl.status = true
    return nil
}

// competition 竞争锁
func (dl *DistributeLockRedis) lock() error {
    if res, err := redis.String(dl.redis.Do(context.Background(), "SET", dl.key, 1, "NX", "EX", dl.expire)); err != nil {
        return err
    }
    return nil
}

// guard 创建守护协程,自动续期
func (dl *DistributeLockRedis) startWatchDog(ctx context.Context) {
    safeGo(func() error {
        for {
            select {
            // Unlock通知结束
            case <-ctx.done(): 1 2 return nil default: 否则只要开始了,就自动重入(续租锁) if dl.status { res, err :="redis.Int(dl.redis.Do(context.Background()," "expire", dl.key, dl.expire)); !="nil" } 续租时间为 expire 秒 time.sleep(time.duration(dl.expire 2) * time.second) }) unlock 释放锁 func (dl *distributelockredis) unlock() (err error) 这个重入锁必须取消,放在第一个地方执行 dl.cancelfun dl.cancelfun() 释放成功,取消重入锁 var res int "del", dl.key)); fmt.errorf("释放锁失败") < code></-ctx.done():>

这段代码的逻辑基本上都以注释的形式来写了。其中主要就在startWatchDog,对锁进行重新续期

ctx, cancelFun := context.WithCancel(context.Background())
dl.cancelFun = cancelFun
dl.startWatchDog(ctx) // &#x521B;&#x5EFA;&#x5B88;&#x62A4;&#x534F;&#x7A0B;&#xFF0C;&#x81EA;&#x52A8;&#x5BF9;&#x9501;&#x8FDB;&#x884C;&#x7EED;&#x671F;
dl.status = true

首先创建一个cancelContext,它的context函数cancelFunc是给Unlock进行调用的。然后启动一个goroutine进程来循环续期。

这个新启动的goroutine在主goroutine处理结束,调用Unlock的时候,才会结束,否则会在 过期时间/2 的时候,调用一次redis的expire命令来进行续期。

至于外部,在使用的时候如下

func Foo() error {
  key := foo

  // &#x521B;&#x5EFA;&#x53EF;&#x91CD;&#x5165;&#x7684;&#x5206;&#x5E03;&#x5F0F;&#x9501;
    dl := NewDistributeLockRedis(key, 10)
    // &#x4E89;&#x62A2;&#x9501;
    err := dl.TryLock()
    if err != nil {
        // &#x6CA1;&#x6709;&#x62A2;&#x5230;&#x9501;
        return err
    }

    // &#x62A2;&#x5230;&#x9501;&#x7684;&#x8BB0;&#x5F97;&#x91CA;&#x653E;&#x9501;
    defer func() {
        dl.Unlock()
    }

    // &#x505A;&#x771F;&#x6B63;&#x7684;&#x4EFB;&#x52A1;
    DoJob()
}

参考文章

如果还想了解更多,以下的参考文章值得阅读。

redisson

滴滴 曾奇:谈谈我所认识的分布式锁

Redis 分布式锁|从青铜到钻石的五种演进方案

分布式锁中的王者方案 - Redisson

redisson中的看门狗机制总结

Redis分布式锁如何自动续期

到底什么是重入锁,拜托,一次搞清楚!

Original: https://www.cnblogs.com/yjf512/p/16308469.html
Author: 轩脉刃
Title: Golang可重入锁的实现