服务器之家:专注于服务器技术及软件下载分享
分类导航

Mysql|Sql Server|Oracle|Redis|MongoDB|PostgreSQL|Sqlite|DB2|mariadb|Access|数据库技术|

服务器之家 - 数据库 - Redis - 详解redis分布式锁(优化redis分布式锁的过程及Redisson使用)

详解redis分布式锁(优化redis分布式锁的过程及Redisson使用)

2021-11-24 17:24一个还没入门的程序员 Redis

在分布式的开发中,以电商库存的更新功能进行讲解,在实际的应用中相同功能的消费者是有多个的,这篇文章主要介绍了redis分布式锁详解(优化redis分布式锁的过程及Redisson使用),需要的朋友可以参考下

1. redis在实际的应用中

不仅可以用来缓存数据,在分布式应用开发中,经常被用来当作分布式锁的使用,为什么要用到分布式锁呢?

在分布式的开发中,以电商库存的更新功能进行讲解,在实际的应用中相同功能的消费者是有多个的,假如多个消费者同一时刻要去消费一条数据,假如业务逻辑处理逻辑是查询出redis中的商品库存,而如果第一个进来的消费的消费者获取到库存了,还没进行减库存操作,相对晚来的消费者就获取了商品的库存,这样就导致数据会出错,导致消费的数据变多了。

例如:消费者A和消费者B分别去消费生产者C1和生产者C2的数据,而生产者都是使用同一个redis的数据库的,如果生产者C1接收到消费者A的消息后,先进行查询库存,然后当要进行减库存的时候,因为生产者C2接收到消费者B的消息后,也去查询库存,而因为生产者C1还没有进行库存的更新,导致生产者C2获取到的库存数是脏数据,而不是生产者C1更新后的数据,导致业务出错。

详解redis分布式锁(优化redis分布式锁的过程及Redisson使用)

如果不是分布式的应用,可以使用synchronized进行防止库存更新的问题的产生,但是synchronized只是基于JVM层面的,如果在不同的JVM中,就不能实现这样的功能。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@GetMapping("getInt0")
 public String test() {
     synchronized (this) {
         //获取当前商品的数量
         int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
         //然后对商品进行出库操作,即进行减1
         /*
          * a业务逻辑
          *
          * */
         if (productNum > 0) {
             stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
             int productNumNow = productNum - 1;
         } else {
             return "product=0";
         }
         int productNumNow = productNum - 1;
         return "success=" + productNumNow;
     }
 }

2.如何使用redis的功能进行实现分布式锁

2.1 redis分布式锁思想

如果对redis熟悉的话,我们能够想到redis中具有setnx的命令,该命令的功能宇set功能类似,但是setnx的命令在进行存数据前,会检查redis中是否已经存在相同的key,如存在的话就返回false,反之则返回true,因此我们可以使用该命令的功能,设计一个分布式锁。

2.1.1设计思想:

  • 在请求相同功能的接口时,使用redis的setnx命令,如果使用setnx命令后返回的是为true,说明此时没有其他的调用这个接口,就相当于获取到锁了,然后就可以继续执行接下来的业务逻辑了。当执行完业务逻辑后,在返回数据前,就把key删除了,然后其他的请求就能获取到锁了。
  • 如果使用setnx命令,返回的是false,说明此时有其他的消费者正在调用这个接口,因此需要等待其他消费者顺利消费完成后,才能获取到分布式的锁。

2.1.2 根据上面的设计思想进行代码实现

代码片段【1】

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@GetMapping("getInt1")
  public String fubushisuo(){
      //setIfAbsent的指令功能和redis命令中的setNx功能一样,如果redis中已经存在相同的key,则返回false
      String lockkey = "yigehaimeirumengdechengxuyuan";
      String lockvalue = "yigehaimeirumengdechengxuyuan";
      boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue);
      //如果能够成功的设置lockkey,这说明当前获取到分布式锁
      if (!opsForSet){
          return "false";
      }
      //获取当前商品的数量
      int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
      //然后对商品进行出库操作,即进行减1
      /*
      * a业务逻辑
      *
      * */
      if (productNum>0){
          stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
          int productNumNow = productNum - 1;
      }else {
          return "product=0";
      }
      //然后进行释放锁
      stringRedisTemplate.delete(lockkey);
      int productNumNow = productNum-1;
      return "success="+productNumNow;
  }
2.1.2.1反思代码片段【1】

如果使用这种方式,会产生死锁的方式:
死锁发生的情况:
(1) 如果在a业务逻辑出现错误时,导致不能执行delete()操作,使得其他的请求不能获取到分布式锁,业务lockkey一直存在于reids中,导致setnx操作一直失败,所以不能获取到分布式锁
(2) 解决方法,使用对业务代码进行try…catch操作,如果出现错误,那么使用finally对key进行删除

优化代码【2】

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@GetMapping("getInt2")
public String fubushisuo2(){
    //setIfAbsent的指令功能和redis命令中的setNx功能一样,如果redis中已经存在相同的key,则返回false
    String lockkey = "yigehaimeirumengdechengxuyuan";
    String lockvalue = "yigehaimeirumengdechengxuyuan";
    boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue);
    int productNumNow = 0;
    //如果能够成功的设置lockkey,这说明当前获取到分布式锁
    if (!opsForSet){
        return "false";
    }
    try {
        //获取当前商品的数量
        int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
        //然后对商品进行出库操作,即进行减1
        /*
         * b业务逻辑
         * */
        if (productNum>0){
            stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
            productNumNow = productNum-1;
        }else {
            return "product=0";
        }
 
    }catch (Exception e){
        System.out.println(e.getCause());
    }finally {
            //然后进行释放锁
        stringRedisTemplate.delete(lockkey);
    }
 
    return "success="+productNumNow;
}
2.1.2.2反思代码【2】

出现问题的情况:
如果这种情况也有会产生的情况,如果此时有多台服务器都在运行该方法,
其中有一个方法获取到了分布式锁,而在运行下面的业务代码时,此时该服务器突然宕机了,导致其他的不能获取到分布式锁,

解决方法:加上过期时间,但又服务宕机了,过了设置的时间后,redis会可以把key给删除,这样其他的的服务器就可以正常的进行上锁了。

优化代码【3】

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@GetMapping("getInt3")
   public String fubushisuo3(){
       //setIfAbsent的指令功能和redis命令中的setNx功能一样,如果redis中已经存在相同的key,则返回false
       String lockkey = "yigehaimeirumengdechengxuyuan";
       String lockvalue = "yigehaimeirumengdechengxuyuan";
      //[01] boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue);
       //设置过期时间为10秒,但是如果使用该命令,没有原子性,可能执行expire前宕机了,而不是设置过期时间,
      //[02] stringRedisTemplate.expire(lockkey, Duration.ofSeconds(10));
       //使用setIfAbsent(lockkey,lockvalue,10,TimeUnit.SECONDS);代码代替上面[01],[02]行代码
       Boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, lockvalue, 10, TimeUnit.SECONDS);
       int productNumNow = 0;
       //如果能够成功的设置lockkey,这说明当前获取到分布式锁
       if (!opsForSet){
           return "false";
       }
       try {
           //获取当前商品的数量
           int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
           //然后对商品进行出库操作,即进行减1
           /*
            * c业务逻辑
            * */
           if (productNum>0){
               stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
               productNumNow = productNum-1;
           }else {
               return "product=0";
           }
 
       }catch (Exception e){
           System.out.println(e.getCause());
       }finally {
       
       //然后进行释放锁
           stringRedisTemplate.delete(lockkey);
       }
       
       return "success="+productNumNow;
   }
2.1.2.3 反思优化代码【3】

出现问题的情况:
如果c业务逻辑持续超过了设置时间,导致redis中的lockkey过期了,
而其他的用户此时访问该方法时获取到锁了,而在此时,之前的的c业务逻辑也执行完成了,但是他会执行delete,把lcokkey删除了。导致分布式锁出错。
例子:在12:01:55的时刻,有一个A来执行该getInt3方法,并且成功获取到锁,但是A执行了10秒后还不能完成业务逻辑,导致redis中的锁过期了,而在11秒的时候有B来执行getint3方法,因为key被A删除了,导致B能够成功的获取redis锁,而在B获取锁后,A因为执行完成了,然后把reids中的key给删除了,但是我们注意的是,A删除的锁是B加上去的,而A的锁是因为过期了,才被redis自己删除了,因此这导致了C如果此时来时也能获取redis分布式锁

解决方法:使用UUID,产生一个随机数,当要进行delete(删除)redis中key时,判断是不是之前自己设置的UUID

代码优化【4】

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@GetMapping("getInt4")
  public String fubushisuo4(){
      //setIfAbsent的指令功能和redis命令中的setNx功能一样,如果redis中已经存在相同的key,则返回false
      String lockkey = "yigehaimeirumengdechengxuyuan";
      //获取UUID
      String lockvalue = UUID.randomUUID().toString();
      Boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, lockvalue, 10, TimeUnit.SECONDS);
      int productNumNow = 0;
      //如果能够成功的设置lockkey,这说明当前获取到分布式锁
      if (!opsForSet){
          return "false";
      }
      try {
          //获取当前商品的数量
          int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
          //然后对商品进行出库操作,即进行减1
          /*
           * c业务逻辑
           * */
 
          if (productNum>0){
              stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
              productNumNow = productNum-1;
          }else {
              return "product=0";
          }
 
      }catch (Exception e){
          System.out.println(e.getCause());
      }finally {
      
      //进行释放锁
          if (lockvalue==stringRedisTemplate.opsForValue().get(lockkey)){
              stringRedisTemplate.delete(lockkey);
          }
      }
      return "success="+productNumNow;
  }
2.1.2.4 反思优化代码【4】

出现问题的情况:
此时该方法是比较完美的,一般并发不是超级大的情况下都可以进行使用,但是关于key的过期时间需要根据业务执行的时间,进行设置,防止在业务还没执行完时,key就过期了.

解决方法:目前有很多redis的分布式锁的框架,其中redisson用的是比较多的

2.2 使用redisson进行实现分布式锁

先添加redisson的maven依赖

?
1
2
3
4
5
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.11.1</version>
</dependency>

redisson的bean配置

?
1
2
3
4
5
6
7
8
9
@Configuration
public class RedissonConfigure {
    @Bean
    public Redisson redisson(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://27.196.106.42:6380").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}

实现分布式锁代码如下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@GetMapping("getInt5")
   public String fubushisuo5(){
       //setIfAbsent的指令功能和redis命令中的setNx功能一样,如果redis中已经存在相同的key,则返回false
       String lockkey = "yigehaimeirumengdechengxuyuan";
       //获取UUID
       RLock lock = redisson.getLock(lockkey);
       lock.lock();
       int productNumNow = 0;
       //如果能够成功的设置lockkey,这说明当前获取到分布式锁
 
       try {
           //获取当前商品的数量
           int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
           //然后对商品进行出库操作,即进行减1
           /*
            * c业务逻辑
            * */
           if (productNum>0){
           stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
           productNumNow = productNum-1;
           }else {
               return "product=0";
           }
       }catch (Exception e){
           System.out.println(e.getCause());
       }finally {
          lock.unlock();
           }
 
       //然后进行释放锁
       return "success="+productNumNow;
   }

从面就能看到,redisson实现分布式锁是非常简单的,只要简单的几条命令就能实现分布式锁的功能的。
redisson实现分布式锁的只要原理如下:
redisson使用了Lua脚本语言使得命令既有原子性,redisson获取锁时,会给key设置30秒的过期是按,同时redisson会记录当前请求的线程编号,然后定时的去检查该线程的状态,如果还处于执行状态的话,而且key差不多要超期过时时,redisson会修改key的过期时间,一般增加10秒。这样就可以动态的设置key的过期时间了,弥补了优化代码【4】的片段

到此这篇关于redis分布式锁详解(优化redis分布式锁的过程及Redisson使用)的文章就介绍到这了,更多相关redis分布式锁内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/weixin_37541878/article/details/120059340

延伸 · 阅读

精彩推荐
  • Redis《面试八股文》之 Redis十六卷

    《面试八股文》之 Redis十六卷

    redis 作为我们最常用的内存数据库,很多地方你都能够发现它的身影,比如说登录信息的存储,分布式锁的使用,其经常被我们当做缓存去使用。...

    moon聊技术8182021-07-26
  • Redis详解三分钟快速搭建分布式高可用的Redis集群

    详解三分钟快速搭建分布式高可用的Redis集群

    这篇文章主要介绍了详解三分钟快速搭建分布式高可用的Redis集群,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,...

    万猫学社4502021-07-25
  • RedisRedis 6.X Cluster 集群搭建

    Redis 6.X Cluster 集群搭建

    码哥带大家完成在 CentOS 7 中安装 Redis 6.x 教程。在学习 Redis Cluster 集群之前,我们需要先搭建一套集群环境。机器有限,实现目标是一台机器上搭建 6 个节...

    码哥字节15752021-04-07
  • Redis如何使用Redis锁处理并发问题详解

    如何使用Redis锁处理并发问题详解

    这篇文章主要给大家介绍了关于如何使用Redis锁处理并发问题的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Redis具有一定的参考学习...

    haofly4522019-11-26
  • Redis关于Redis数据库入门详细介绍

    关于Redis数据库入门详细介绍

    大家好,本篇文章主要讲的是关于Redis数据库入门详细介绍,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下,方便下次浏览...

    沃尔码6982022-01-24
  • RedisRedis Template实现分布式锁的实例代码

    Redis Template实现分布式锁的实例代码

    这篇文章主要介绍了Redis Template实现分布式锁,需要的朋友可以参考下 ...

    晴天小哥哥2592019-11-18
  • Redisredis缓存存储Session原理机制

    redis缓存存储Session原理机制

    这篇文章主要为大家介绍了redis缓存存储Session原理机制详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪...

    程序媛张小妍9252021-11-25
  • RedisRedis集群的5种使用方式,各自优缺点分析

    Redis集群的5种使用方式,各自优缺点分析

    Redis 多副本,采用主从(replication)部署结构,相较于单副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。...

    优知学院4082021-08-10