redis分布式锁与redsync库源码分析

redsync是redis官方推荐的go版本分布式锁实现,标准的官方redlock算法实现,阅读了下源码并顺便复习一下redis分布式锁原理。

一. redlock算法
单点场景
首先来看单redis实例的场景,这是集群模式的基础。这种场景下实现分布式锁比较简单


加锁
各节点通过set key value nx ex即可,如果set执行成功,则表明加锁成功,否则失败,其中value为随机串,用来判断是否是当前应用实例加的锁;nx用来判断该key是否存在以实现排他特性,ex用来指定锁的过期时间,避免死锁。

解锁
向redis服务发送并执行一段lua脚本,脚本如下,也很好理解,如果是自己加的锁,那么安全释放,否则什么也不做。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
1
2
3
4
5
如果redis采用了主备的部署方式,存在一种场景,master上set成功后宕机,而set的key没有来得及同步到slave的话,会存在不一致的场景,可以通过redis持久化和fsync=always的方式来保持一致,但是有性能损耗。

集群场景
设集群有N个redis节点,那么,redlock算法约定,任意应用实例在半数以上(N/2 + 1)的redis节点上执行set成功,就认为当前应用实例成功持有锁。

这里面有几个问题需要考虑:网络延迟、超时处理、节点宕机、新增节点
网络延迟
由于set时指定了ex参数,官方称为TTL,所以锁本身就是有生命周期的。而应用实例又需要与多个redis实例通信,网络io的耗时不能无视,官方给出的建议值是,如果ex参数设置为10s,那么请求单个实例的超时时间应在5-50ms以内,换算下来,就是5‰ - 0.5‰

超时处理
由于TTL中包含了网络传输耗时、各及节点的耗时差异,所以加锁成功后,应用实例有效的持有锁时长 = TTL - (最晚执行set成功的response时间 - 最早执行set成功的response时间) - Clock drift,讲真,这里clock drift我没理解,网上讲这是时钟频率的差异?或者可能是部署在不同时区时,服务之间的时区差值。

节点宕机
当一个应用实例持有锁时,如果一个持有key的redis实例宕机了,且没有配置主备同步策略,那么锁状态依然可能会出现不一致情形。官方有两个解决方案:一个是像单redis实例一样,对每个实例配置主备同步持久化,并采用fsync=always策略进行主从同步,这会带来性能损耗。另一个不依赖持久化策略,令宕机redis实例延迟启动,延迟启动的作用,就是使宕机节点已经持有的key超时掉,迫使这个节点变为一个未持有key的节点,但这引入一个风险,就是当大多数redis节点同时宕机时,会使分布式锁不可用。

新增节点
官方文档没有提及,但是这里有坑,我的理解是,用于实现分布式锁的redis集群,需要显式的配置节点地址,如果采用动态的redis服务发现策略,那么追加节点可能会导致锁状态的不一致。

以上基本就是redlock的核心思路。

二. redsync库
redsync库中定义的mutex结构如下:

// A Mutex is a distributed mutual exclusion lock.
type Mutex struct {
    name         string                 // 锁在redis上的key
    expiry       time.Duration          // 超时时间
    tries        int                    // 重试次数
    delayFunc    DelayFunc              // 延时函数,用于在每两次重试之间的休眠期,避免大量请求拥塞
    factor       float64                // 时钟偏移因子
    quorum       int                    // 成功获取锁需要set成功的最少redis节点数,N/2+1
    genValueFunc func() (string, error) // 用于生成随机value的方法
    value        string                 // 锁在热地上的value值
    until        time.Time              // 持有锁的deadline时间
    pools        []redis.Pool           // redis连接池
}
1
2
3
4
5
6
7
8
9
10
11
12
13
Mutex类定义了四组共八个方法,分别是

func (m *Mutex) Lock() error                                         // 
func (m *Mutex) Unlock() (bool, error)
func (m *Mutex) LockContext(ctx context.Context) error
func (m *Mutex) UnlockContext(ctx context.Context) (bool, error)
func (m *Mutex) Extend() (bool, error)
func (m *Mutex) ExtendContext(ctx context.Context) (bool, error)
func (m *Mutex) Valid() (bool, error)
func (m *Mutex) ValidContext(ctx context.Context) (bool, error)
1
2
3
4
5
6
7
8
带有context的可以通过应用层控制获取或释放锁的过程。Extend簇函数用来重置key的超时时间,Valid用来验证当前节点是否持有锁。

与redis通信
redsync与redis集群通信时,采用了并发访问方式,并发过程在actOnPoolsAsync函数中,其参数传入的是与单个节点通信的实现函数地址。

func (m *Mutex) actOnPoolsAsync(actFn func(redis.Pool) (bool, error)) (int, error) {
    type result struct {
        Status bool
        Err    error
    }

    // 创建用于收集所有redis节点返回值的chan
    ch := make(chan result)
    for _, pool := range m.pools {
        // 并发请求所有redis节点,结果写入chan
        go func(pool redis.Pool) {
            r := result{}
            r.Status, r.Err = actFn(pool)
            ch <- r
        }(pool)
    }
    // 校验所有redis节点的返回值,并返回成功节点数量
    n := 0
    var err error
    for range m.pools {
        r := <-ch
        if r.Status {
            n++
        } else if r.Err != nil {
            err = multierror.Append(err, r.Err)
        }
    }
    return n, err
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
获取锁
func (m *Mutex) LockContext(ctx context.Context) error {
    // 生成随机value
    value, err := m.genValueFunc()
    if err != nil {
        return err
    }

    // 循环重试
    for i := 0; i < m.tries; i++ {
        if i != 0 {
            time.Sleep(m.delayFunc(i))
        }

        start := time.Now()

        // 并发在所有redis节点上获取锁
        n, err := m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {
            return m.acquire(ctx, pool, value)
        })
        if n == 0 && err != nil {
            return err
        }

        now := time.Now()
        until := now.Add(m.expiry - now.Sub(start) - time.Duration(int64(float64(m.expiry)*m.factor)))
        // 如果成功在半数以上节点set成功,并且在锁的有效时间内,则说明加锁成功
        if n >= m.quorum && now.Before(until) {
            m.value = value
            m.until = until
            return nil
        }

        // 加锁失败,清除所有set成功的节点上的key
        _, _ = m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {
            return m.release(ctx, pool, value)
        })
    }

    return ErrFailed
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
释放锁
func (m *Mutex) UnlockContext(ctx context.Context) (bool, error) {
    // 并发执行delete lua脚本
    n, err := m.actOnPoolsAsync(func(pool redis.Pool) (bool, error) {
        return m.release(ctx, pool, m.value)
    })
    // 执行成功的节点数小于约定的加锁成功节点数,则说明有节点删除失败了,那么释放锁就会失败
    if n < m.quorum {
        return false, err
    }
    return true, nil
}
1
2
3
4
5
6
7
8
9
10
11
至于Mutex的acquire和release方法的实现,以及Extend、Valid方法的实现没什么难度,就不贴代码了。
需要注意的是,在分布式锁场景中,无论获取还是释放锁,与操作系统的锁相比,执行失败会是常态,所以一定要检查Lock、Unlock的返回值。

其他彩蛋
Option可变参数用法
将Option定义为interface,而OptionFunc作为函数指针的别名,实现Option接口,所以实际传入的options数组就是一组函数指针,这组函数的执行将会把参数的修改应用到Mutex上,这是一种优雅的变参函数实现方式,在gopherChina上被左哥提到过。

func (r *Redsync) NewMutex(name string, options ...Option) *Mutex {
    // ...
    for _, o := range options {
        o.Apply(m)
    }
    return m
}

// An Option configures a mutex.
type Option interface {
    Apply(*Mutex)
}

// OptionFunc is a function that configures a mutex.
type OptionFunc func(*Mutex)

// Apply calls f(mutex)
func (f OptionFunc) Apply(mutex *Mutex) {
    f(mutex)
}

// WithExpiry can be used to set the expiry of a mutex to the given value.
func WithExpiry(expiry time.Duration) Option {
    return OptionFunc(func(m *Mutex) {
        m.expiry = expiry
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
multierror库
在actOnPoolsAsync方法中,在处理所有redis节点的返回时,引用了multierror库,这个库自定义了Error结构,用于保存多个error,当你的处理过程中在多个位置可能会返回不同error信息,但是返回值又只有一个error时,可以通过multierror.Append方法将这些error合成一个返回。内部创建了一个[]error来保存这些error,保留了层层弹栈返回时,各层的错误信息。代码很少但却很实用。

for range语法糖
还在写for _,v :=range m.pools或for i:=0; i<len(m.pools); i++的我,学到了这个

for range m.pools {
    // ... 
}
————————————————
版权声明:本文为CSDN博主「一只coding猪」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44446512/article/details/114338612

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值