https://blog.csdn.net/zhw21w/article/details/129563500

1\. 非原子操作(setnx + expire)

一说到实现Redis分布式锁,很多小伙伴马上就会想到setnx+ expire命令。也就是说,先用setnx来抢锁,如果抢到之后,再用expire给锁设置一个过期时间。

伪代码如下:

if(jedis.setnx(lock_key,lock_value) == 1){ //加锁
    jedis.expire(lock_key,timeout); //设置过期时间
    doBusiness //业务逻辑处理
}

这块代码是有坑的,因为setnxexpire两个命令是分开写的,并不是原子操作!如果刚要执行完setnx加锁,正要执行expire设置过期时间时,进程crash或者要重启维护了,那么这个锁就“长生不老”了,别的线程永远获取不到锁啦。

2.被别的客户端请求覆盖( setnx + value为过期时间)

为了解决:发生异常时,锁得不到释放的问题。有小伙伴提出,可以把过期时间放到setnxvalue里面。如果加锁失败,再拿出value值和当前系统时间校验一下是否过期即可。伪代码实现如下:

long expireTime = System.currentTimeMillis() + timeout; //系统时间+设置的超时时间
String expireTimeStr = String.valueOf(expireTime); //转化为String字符串

// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(lock_key, expireTimeStr) == 1) {
        return true;
}

// 如果锁已经存在,获取锁的过期时间
String oldExpireTimreStr = jedis.get(lock_key);

// 如果获取到的老的预期过期时间,小于系统当前时间,表示已经过期了
if (oldExpireTimreStr != null && Long.parseLong(oldExpireTimreStr) < System.currentTimeMillis()) {

     //锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
    String oldValueStr = jedis.getSet(lock_key, expireTimeStr);

    if (oldValueStr != null && oldValueStr.equals(oldExpireTimreStr)) {
      //考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
      return true;
    }
}

//其他情况,均返回加锁失败
return false;
}

这种实现的方案,也是有坑的:如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖

3\. 忘记设置过期时间

之前review代码的时候,看到这样实现的分布式锁,伪代码

try{
  if(jedis.setnx(lock_key,lock_value) == 1){//加锁
     doBusiness //业务逻辑处理
     return true; //加锁成功,处理完业务逻辑返回
  }
  return false; //加锁失败
} finally {
    unlock(lockKey);- //释放锁
}

这块有什么问题呢?是的,忘记设置过期时间了。如果程序在运行期间,机器突然挂了,代码层面没有走到finally代码块,即在宕机前,锁并没有被删除掉,这样的话,就没办法保证解锁,所以这里需要给lockKey加一个过期时间。注意哈,使用分布式锁,一定要设置过期时间哈

4\. 业务处理完,忘记释放锁

很多小伙伴,会使用Redisset指令扩展参数来实现分布式锁。

set指令扩展参数:SET key value[EX seconds][PX milliseconds][NX|XX]

- NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,
  而其他客户端请求只能等其释放锁,才能获取。
- EX seconds :设定key的过期时间,时间单位是秒。
- PX milliseconds: 设定key的过期时间,单位为毫秒
- XX: 仅当key存在时设置值

小伙伴会写出如下伪代码: