分布式锁

62 阅读4分钟

=====================

本篇为个人学习知识点笔记,方便随时看看

不保证你有收获

=====================

简单来说就是分布环境下面不同实例之间抢一把锁

基本上都和网络有关

redis

实现一个分布式锁,就是利用setnx,确保可以排他的设置一个键值对

只有一个实例能够设置成功,其他的实例都会阻塞在那里

问题

  • 如何加锁?

    • 加锁设置的值怎么确定?

    加锁的值应该具有唯一性,能够知道这个锁是谁加的,或者说是当前实例能判断这个锁是不是自己的

    • 加锁的时候要不要设置过期时间?

    加锁要设置过期时间,如果没有过期时间,前面的实例如果崩溃了,就没有办法再解锁了


type Client struct {
    Client redis.Cmdable
}
type Lock struct {
    client redis.Cmdable
    key    string
    value  string
}
var (
    errFailToPreemptLock = errors.New("抢锁失败")
    //go:embed lua/unlock.lua
    luaUnlock string
)

func NewClientAble(clien redis.Cmdable) *Client {
    return &Client{
       Client: clien,
    }
}
func (c *Client) TryLock(ctx context.Context, key string, expiration time.Duration) (*Lock, error) {
    val := uuid.New().String()
    ok, err := c.Client.SetNX(ctx, key, val, expiration).Result()

    if err != nil {
       return nil, err
    }
    if !ok {
       //代表有人抢到
       return nil, errFailToPreemptLock
    }
    return &Lock{
       client: c.Client,
       key:    key,
       value:  val,
    }, nil
}

  • 如何解锁?
    • 要注意当前的锁是不是自己的
    • 我在读取这个锁的时候别人是不能操作这个锁的,原子性使用lua

image.png


func (l *Lock) Unlock(ctx context.Context) error {
       // lua脚本不会,到时候学习一下
       // 这里是防止你在检查是不是自己的锁的时候别人就把锁给修改了
       // 所以当你检查的时候别人是不能操作的
    res, err := l.client.Eval(ctx, luaUnlock, []string{l.key}, l.value).Int64()
    if err != nil {
       return err
    }
    if res != 1 {
       return errors.New("解锁失败,锁不存在")

    }

    return nil
}



//rdb := redis.NewClient(&redis.Options{
//    Addr:     "",
//    Password: "",
//})

手动续约和自动续约

就是你设置了过期时间,但是你的业务并不会按照你的时间设置,在执行任务的时候,过期了

image.png

续约还是需要使用lua脚本,在续约的时候还是要判断是不是自己的锁

image.png

func (l *Lock) Refresh(ctx context.Context) error {
    res, err := l.client.Eval(ctx, luaRefresh, []string{l.key}, l.value, l.expirarion.Seconds()).Int64()
    if err != nil {
       return err
    }
    if res != 1 {
       return errors.New("解锁失败,锁不存在")
    }
    return nil
}

客户端怎么续约?

间隔多久续约?

续约超时怎么办?

如果出现错误怎么办?

func ExampleLock_Refresh() {
    var l *Lock

    ch := make(chan struct{})
    errch := make(chan struct{})
    timeoutCh := make(chan struct{}, 1)
    go func() {
       timer := time.NewTicker(time.Second * 10)
       var num int
       for {
          select {
          case <-timer.C:
             ctx, cancle := context.WithTimeout(context.Background(), time.Second*2)
             err := l.Refresh(ctx)
             cancle()
             if err == context.DeadlineExceeded {
                timeoutCh <- struct{}{}
                continue
             }
             if err != nil {
                errch <- struct{}{}
                return
             }
             num = 0
          case <-timeoutCh:
             //超时重试,限制次数 和次数归零 变量?
             if num > 10 {
                errch <- struct{}{}
                return
             }
             num++
             ctx, cancle := context.WithTimeout(context.Background(), time.Second*2)
             err := l.Refresh(ctx)
             cancle()
             if err == context.DeadlineExceeded {
                timeoutCh <- struct{}{}
                continue
             }
             if err != nil {
                errch <- struct{}{}
                return
             }
          case <-ch:
             ctx, cancle := context.WithTimeout(context.Background(), time.Second*2)
             l.Unlock(ctx)
             cancle()
          }

       }
    }()
    // 在业务中间怎么处理错误,如果给一个chan来进行检测错误信号
    // 如果业务没有循环都要检测
    // 如果有循环还好操作
    ch <- struct{}{}
}

redis自动续约

自动续约的问题和前面还是一样的

自动续约的可控性比较差

image.png

type Lock struct {
    client     redis.Cmdable
    key        string
    value      string
    expirarion time.Duration
    unlockChan chan struct{}
}

func (l *Lock) AutoRefresh(interval time.Duration, timeout time.Duration) error {
    timeoutCh := make(chan struct{}, 1)
    timer := time.NewTicker(interval)
    for {
       select {
       case <-timer.C:
          ctx, cancle := context.WithTimeout(context.Background(), timeout)
          err := l.Refresh(ctx)
          cancle()
          if err == context.DeadlineExceeded {
             timeoutCh <- struct{}{}
             continue
          }
          if err != nil {
             return err
          }
       case <-timeoutCh:
          ctx, cancle := context.WithTimeout(context.Background(), timeout)
          err := l.Refresh(ctx)
          cancle()
          if err == context.DeadlineExceeded {
             timeoutCh <- struct{}{}
             continue
          }
          if err != nil {
             return err
          }
       case <-l.unlockChan:
          return nil
       }

    }

}

加锁重试

加锁的时候可能会出现一些问题

  • 如果超时,直接加锁
  • 检查一下key对应的值是不是我们刚才超时加锁请求的值,
    • 如果是直接返回,前面一次加锁成功
    • 如果不是,直接返回加锁失败

一般都是在超时的时候进行再次尝试

image.png

image.png

type RetryStrategy interface {
    // 第一个值为重试间隔
    // 第二个值为要不要继续重试
    Next() (time.Duration, bool)
}
type FixedIntervalRetryStragety struct {
    Interval time.Duration
    MaxCnt   int
    Cnt      int
}

func (f *FixedIntervalRetryStragety) Next() (time.Duration, bool) {
    if f.Cnt > f.MaxCnt {
       return 0, false
    }
    return f.Interval, true
}

func NewClientAble(clien redis.Cmdable) *Client {
    return &Client{
       Client: clien,
    }
}
func (c *Client) Lock(ctx context.Context,
    key string, expiration time.Duration,
    retry RetryStrategy,
    timeout time.Duration) (*Lock, error) {
    // ctx可以控制整个链路
    val := uuid.New().String()
    var timer *time.Timer

    for {

       lctx, cancel := context.WithTimeout(ctx, timeout)
       res, err := c.Client.Eval(lctx, luaLock, []string{key}, val, expiration.Seconds()).Result()
       cancel()

       //不为bil并且不超时
       if err != nil && !errors.Is(err, context.DeadlineExceeded) {
          return nil, err
       }

       if res == "OK" {
          return &Lock{
             client:     c.Client,
             key:        key,
             value:      val,
             expirarion: expiration,
          }, nil
       }

       interval, ok := retry.Next()
       if ok {
          return nil, errors.New("redis lock:超出重试限制")
       }
       if timer == nil {
          timer = time.NewTimer(interval)
       } else {
          timer.Reset(interval)
       }

       select {
       case <-timer.C:
       case <-ctx.Done():
          return nil, ctx.Err()

       }
    }

}

如何使用singlefilght优化

  • 非常高并发
  • 非常热点集中

redis主从切换