用redis实现分布式锁

285 阅读1分钟

分布式系统中,多个执行进程间如何进行协作来保证共享数据的一致性,是一个常见的话题。通常我们可以通过redis提供的setnx命令来实现一个分布式的锁。setnx命令格式如下:

setnx  key value
  • 只在键 key 不存在的情况下, 将键 key 的值设置为 value 。
  • 若键 key 已经存在, 则 setnx 命令不做任何动作。
  • 命令执行成功时返回1,执行失败返回0

但是一个分布式的锁还需要考虑以下几种情况:

  • 某个执行进程在获取锁之后退出,如何保其他节点能正常获取锁
  • A进程获取的锁已经过期,在更新过期时间过程中,锁被其他执行进程获取

下面我用golang给出一个完整的分布式锁方案:

func lock(lockName string) (int64, error) {
   deadline := time.Now().Add(LockTimeout).Unix()


   redisConn := appconfig.RedisMiscConn.Get()
   defer redisConn.Close()


   // step 1:通过setnx尝试获取锁
   setnxResult, err := redis.Int(redisConn.Do("SETNX", lockName, deadline))
   // step 1.1 setnx执行失败,直接返回失败
   if err != nil {
      return -1, err
   }
   // step 1.2 setnx返回1,说明获取锁成功了
   if setnxResult == 1 {
      return deadline, nil
   }
   // step 2 前面setnx返回不为1,表示当前锁被占用,需要进一步检查锁是否过期
   getResult, err := redis.String(redisConn.Do("GET", lockName))
   if err != nil {
      return -1, err
   }
   
   // step 3 使用get获取lock的过期时间,没有过期直接返回获取锁失败
   getOld, _ := strconv.ParseInt(getResult, 10, 64)
   if getOld > time.Now().Unix() {
      return -1, ErrLockTaken
   }


   // step 4 如果过期了,尝试用getset命令更新锁的过期时间
   oldVal, err := redis.String(redisConn.Do("GETSET", lockName, strconv.FormatInt(deadline, 10)))
   if err != nil {
      return -1, err
   }
   getSetOld, _ := strconv.ParseInt(oldVal, 10, 64)
   // step 5 如果仍旧是未过期,可能是有其他节点拿到lock,返回获取锁失败
   if getSetOld > time.Now().Unix() {
      return -1, ErrLockTaken
   }
   
   return deadline, nil
}

扫码_搜索联合传播样式-白色版.png