Redis并发问题及分布式锁

Redis并发问题

既然Redis是单线程,那么它为什么会有并发的问题。理论上来说,Redis是按顺序执行修改操作,不会有多个线程同时修改的情况。

但假如有这种场景下:
有一个字符串,key 为 a,value 为 1 。两个客户端同时对 a 进行加 1 。
他们同时获取到 a 的值为 1 ,同时向Redis发出请求,将 a 值改为 2 。

这样并发问题就产生了。
Redis本身是按顺序执行的,它本身不会有并发问题,但是对于客户端的请求来说,并非如此。

解决办法就是加锁。

Redis分布式锁

其实很简单,也就是针对需要修改的值设置一个字符串,key固定,用setNx。
所有需要修改该值的操作,都需要先setNx这个字符串成功,否则无法进行修改。
修改完成后,需要删除这个字符串,给其他线程使用。
如下图

    //1 加锁,设置一个只有本线程可以拿到的Redis的key-value,还要设置一下保存时间
    boolean isLock = cacheUtil.setNx(LOCK + "_" + id + "_" + bpin, "1", 2, TimeUnit.HOURS);
    if(!isLock){
        //获取锁失败后的操作
    }

完整示例如下:

try{
    //1 加锁,设置一个只有本线程可以拿到的Redis的key-value,还要设置一下保存时间
    boolean isLock = cacheUtil.setNx(LOCK + "_" + id + "_" + bpin, "1", 2, TimeUnit.HOURS);
    if(!isLock){
        //获取锁失败后的操作
    }

    //2 获取锁成功后要对某个key进行的操作
    String allStr = cacheUtil.hGet(ALL + "_" + id, bpin);
    cacheUtil.hSet(ALL + "_" + id, bpin, allStr + 1);

}catch (Exception e){
    e.printStackTrace();
}finally {
    //3 关闭Redis的锁
    cacheUtil.del(LOCK + "_" + id + "_" + bpin);
}

Redisson框架

Redisson是Redis官方给的分布式锁框架,非常简单:
在这里插入图片描述
第1点、加锁机制

客户端1发起加锁

  1. 客户端1发起加锁命令,对“myLock”进行加锁:RLock lock = redisson.getLock(“myLock”);lock.lock();
  2. 发出的信息包括:加锁的时长(默认是30s),客户端的ID——8743c9c0-0795-4907-87fd-6c719a6b4586:1
  3. Redis服务器收到消息,通过“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。
  4. 加锁命令如下:hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1。解释一下,这里是以客户端ID为hash表里的一个key,值为1。数据结构如下:
    在这里插入图片描述
  5. 接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。
  6. 加锁完成。

第2点、锁互斥机制

客户端2来尝试加锁:

  1. 客户端2来尝试加锁
  2. Redis服务器会执行“exists myLock”,发现myLock这个锁key已经存在了。
  3. 接着第二个判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。
  4. 客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的 剩余生存时间。 比如还剩15000毫秒的生存时间。
  5. 客户端2会进入一个while循环,不停的尝试加锁。

第3点、watch dog自动延期机制

客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗, 他是一个后台线程,会每隔10秒检查一下 ,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

第4点、可重入加锁机制

如果这样怎么办呢?
在这里插入图片描述
这就涉及到可重入锁机制

  1. 客户端1再次来尝试加锁
  2. Redis服务器会执行“exists myLock”,发现myLock这个锁key已经存在了。
  3. 接着第二个判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端1的ID,明显是包含的。进行下一步
  4. 通过命令:incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1,对客户端1的加锁次数,累加1。成为下面这样:
    在这里插入图片描述
  5. myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数

第5点、释放锁机制

  1. 执行lock.unlock(),就可以释放分布式锁。
  2. 对myLock数据结构中的那个加锁次数减1。
  3. 如果发现加锁次数是0了,说明这个客户端已经不再持有锁了。
  4. 此时就会用:“del myLock”命令,从redis里删除这个key。

缺点如下:

如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。

但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。

接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。

此时就会导致多个客户端对一个分布式锁完成了加锁。

这时系统在业务语义上一定会出现问题, 导致各种脏数据的产生 。

所以这个就是redis cluster,或者是redis master-slave架构的 主从异步复制 导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。

转自:https://www.cnblogs.com/daofaziran/p/11811510.html

发布了67 篇原创文章 · 获赞 32 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/weixin_43751710/article/details/104669437