Redis分布式锁问题盘点,实战指南-内附Go-Redis分布式锁工具包

170 阅读1分钟

前言

诚然Redis作为分布式锁具备争议,但是在项目实践上,出于易用性、可维护性上考虑,还是有许多场景使用redis作为分布式锁。

本文主要介绍Redis作为分布式锁的一些坑和进阶实践,依次拆分现存问题,并给予解决方式及实现代码;

全文实践基于golang进行实践演示(语言不是桎梏,思维是互通的),后文附项目链接

基础锁

大部分人使用redis作为分布式锁,都是直接setnx操作,但是这里面就存在以下问题:

  1. set锁后,程序出现问题,导致长期未释放

image.png

  1. 执行时间长,redis锁超时释放后未自动续期

image.png

  1. 错误释放他人的锁,如果单纯依赖场景key,可能存在项目迭代过程中逻辑错误,导致释放同场景其他请求锁的情况,所以要增加请求的唯一标识(注意不是用户身份唯一标识)

image.png

对于上面存在的问题,首先要做保证程序或者机器发生故障后能自动释放锁,那么最好是给锁设置超时时间,而非单纯依赖程序去释放锁,其次在程序执行过程中需要保证锁存续。

具体实现代码如下(省略内部获取锁的过程,主要展示自动续期):

// 开启事务
func LockTransaction(key, reqID string, f func() error) (bool, error) {
   lock := NewRLock(key, reqID, 10*time.Second)

   result := lock.TryLock()
   if !result {
      log.Errorf("lock exist, key=%v, reqID=%v", key, reqID)
      return false, nil
   }

   stop := make(chan bool, 1)
   defer func() {
      stop <- true
      close(stop)
      err := lock.UnLock()
      if err != nil {
         log.Errorf("unlock failed, key=%v, reqID=%v", key, reqID)
         return
      }
      log.Infof("unlock succeed, key=%v, reqID=%v", key, reqID)
   }()

   // automatic renewal of additional maintenance lock
   go renewlRLock(lock, stop, key, reqID)
   return true, f()
}

// 自动续期锁
func renewlRLock(lock *RLock, stop chan bool, key, reqID string) {
   ticker := time.NewTicker(5 * time.Second)
   renewTime := 0
   defer func() {
      ticker.Stop()
      if err := recover(); err != nil {
         log.Errorf("renew RLock happened, key=%v, reqID=%v, err=%v", key, reqID, err)
      }
   }()

   for {
      select {
      case <-ticker.C:
         err := lock.RenewLock()
         if err != nil {
            log.Errorf("renew RLock failed, key=%v, reqID=%v, err=%v", key, reqID, err)
            return
         }
         renewTime++
         log.Infof("renew RLock succeed, key=%v, reqID=%v, renewTime=%v", key, reqID, renewTime)
      case <-stop:
         log.Infof("renew RLock over, key=%v, reqID=%v", key, reqID)
         return
      }
   }
}

测试效果:

共计开启5个任务say hello,每隔5秒启动一个,每个任务间隔5秒打印hello

具体执行时序:

image.png

测试代码:

func TestLockTransaction(t *testing.T) {
   err := InitRedisLock("localhost:6379", "", 0, 10)
   if err != nil {
      t.Fatalf("init redis failed, err=%v", err)
   }
   wait := &sync.WaitGroup{}
   for i := 0; i < 5; i++ {
      wait.Add(1)
      go SayHello(t, fmt.Sprint(i), wait)
      time.Sleep(5 * time.Second)
   }
}

func SayHello(t *testing.T, reqID string, wait *sync.WaitGroup) {
   defer func() {
      wait.Done()
   }()

   result, err := LockTransaction("say_hello", reqID, func() error {
      for i := 0; i < 3; i++ {
         time.Sleep(5 * time.Second)
         t.Logf("hello reqID=%v", reqID)
      }
      return nil
   })
   if !result {
      t.Logf("say hello fail, lock exist, reqID=%v", reqID)
      return
   }
   if err != nil {
      t.Logf("say hello fail, process err=%v, reqID=%v", err, reqID)
      return
   }
}

分段锁

如果一个资源都用一个锁来替换,那么很明显所有用户都要挤在一个通道,彼此竞争一个锁,所以我们可以将资源分段处理,让用户散列到多个区间,各自获取所属分段的资源

image.png

需要注意锁分段后,资源也要进行分段

可重入锁

对于一些特殊的业务场景,比如需要递归加锁的情况,那么需要考虑加入加锁层数,通过层数来判断来维持锁的存续,当层数为0的时候可以释放锁

image.png

需要注意的是层数的维护必须保证是原子性的,防止并发出现问题

一般分布式锁很少需要支持可重入的情况,建议在系统设计上避免可重入的情况

具体实现思路如下:

image.png

项目地址:

欢迎讨论,提出改进意见