zoukankan      html  css  js  c++  java
  • 分布式锁-Redis实现

    1:Redis 分布式锁的原理

    利用NX 的原子性,多线程并发时,只有一个线程可以设置成功

    设置成功即获得锁,可以执行后续的业务处理

    如果出现异常,过了锁的有效期,锁自动释放

    释放锁用Redis 的delete 命令,然后释放锁的时候要校验锁的随机数,这个随机数相同才能释放,就是要证明Redis里面这个key 的值是你这个线程设置的,因为你这个线程在设置这个值得时候呢,是你的这个线程生成的这么一段随机数。删除的时候你要看程序设置的随机数和Redis 中的是不是相同的,如果相同就保证这个锁是你设置的。你才能够释放锁。确保你不会释放掉别人的锁。主要就是用作一个校验

    释放锁采用 Lua 脚本,因为这个 delete 校验并没有提供值校验这么一个功能

    获取锁的Redis命令:

    set resource_name my_random_value NX PX 30000
    • resource_name  资源名称,可根据不同业务区分不同的锁
    • my_random_value  随机值,每个线程的随机值都不同,用于释放锁时的校验,要保证每一个线程的随机值都不相同。
    • NX:key 不存在时设置成功,key 存在则设置不成功。我们就是用这个命令实现分布式锁,主要就是用 NX 这个特性。因为SET NX是一个原子性的操作,我们都知道Redis 是一个单线程的,当你多线程并发的给这个key设置值之后,那么这个时候只有第一个线程会设置成功。因为并发请求过来时这些命令呢,在Redis  里边都变成了顺序的了,因为Redis 是单线程的所有你的并行变成了串行,这个就是排队了,只有第一个执行这个命令的才可以设置成功
    • PX:自动失效时间 ,当出现异常情况,锁可以过期失效。由于设置成功之后,后边的程序执行完成,你要把这个锁给释放了,释放成功以后其他线程才可以再次获得这个锁。如果没有设置失效时间,或者释放锁的过程出现异常,那你Redis 这条记录永远存在的,那么其他线程就永远无法获取到锁

    为什么对值有这么高的要求?

    主要是为了校验值一样了才可以删除锁

    Redis分布式锁官方链接

    2:手写Redis分布式锁

    1. 新建一个项目,导入依赖jar包
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-data-redis</artifactId>
                  <version>2.3.4.RELEASE</version>
              </dependency>
    2. 配置账号密码

      ​
      spring.redis.host=xx
      spring.redis.password=xx
    3. 整个代码实现过程

      package com.example.redislock.controller;
      
      
      import lombok.extern.slf4j.Slf4j;
      import org.redisson.Redisson;
      import org.redisson.api.RLock;
      import org.redisson.api.RedissonClient;
      import org.redisson.config.Config;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.data.redis.connection.RedisStringCommands;
      import org.springframework.data.redis.core.RedisCallback;
      import org.springframework.data.redis.core.RedisTemplate;
      import org.springframework.data.redis.core.script.RedisScript;
      import org.springframework.data.redis.core.types.Expiration;
      import org.springframework.transaction.annotation.Transactional;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      import java.util.Arrays;
      import java.util.UUID;
      import java.util.concurrent.TimeUnit;
      
      /**
       * @Author: qiuj
       * @Description:
       * @Date: 2020-10-01 15:45
       */
      @Slf4j
      @RestController
      public class LockController {
      
          @Autowired
          private RedisTemplate redisTemplate;
      
          
      
          @Transactional(rollbackFor = Exception.class)
          @RequestMapping("/redisLock")
          public String redisLock() {
              log.info("进入方法");
      
              String key = "lock";
              String value = UUID.randomUUID().toString();
      
              RedisCallback<Boolean> redisCallback = connection -> {
                  RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
                  Expiration expiration = Expiration.seconds(10);
                  byte[] keyByte = redisTemplate.getKeySerializer().serialize(key);
                  byte[] valueByte = redisTemplate.getValueSerializer().serialize(value);
                  Boolean result = connection.set(keyByte, valueByte, expiration, setOption);
                  return result;
              };
              Boolean lock = (Boolean) redisTemplate.execute(redisCallback);
              if (lock) {
                  try {
                      log.info("获得了锁");
                      Thread.sleep(5000);
                  } catch (Exception e) {
                      e.printStackTrace();
                  } finally {
      
                      String script = "            if  redis.call("get",KEYS[1]) == ARGV[1] then
      " +
                              "                return  redis.call("del",KEYS[1])
      " +
                              "            else
      " +
                              "                return 0
      " +
                              "            end";
                      RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
                      Boolean result = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(key), value);
                      log.info("释放锁的结果:" + result);
                  }
              }
              log.info("方法执行完成");
              return "方法执行完成";
          }
      }
       
    4. 使用 redisTemplate.execute(RedisCallback<T> action)  方法
    5. RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
      需要实现  RedisCallback 回调接口, 设置选项为    SET_IF_ABSENT  也就是NX(如果没有这个key 则设置成功 ,如果已经存在则不成功)。           
    6. Expiration expiration = Expiration.seconds(10);
      设置锁失效时间为10秒,当持有锁超过10秒就会把锁释放。时间取决于你的业务代码块执行的时间进行相应的调整。
    7. byte[] keyByte = redisTemplate.getKeySerializer().serialize(key);
      byte[] valueByte = redisTemplate.getValueSerializer().serialize(value);                                                                           将key value 转为 Byte数组
    8. Boolean result = connection.set(keyByte, valueByte, expiration, setOption);                                   放入set 方法,返回结果true 就是设置key 成功,也就是获得到了锁。反之false 没有获得锁
              RedisCallback<Boolean> redisCallback = connection -> {
                  RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
                  Expiration expiration = Expiration.seconds(10);
                  byte[] keyByte = redisTemplate.getKeySerializer().serialize(key);
                  byte[] valueByte = redisTemplate.getValueSerializer().serialize(value);
                  Boolean result = connection.set(keyByte, valueByte, expiration, setOption);
                  return result;
              };
    9. 将我们实现的接口RedisCallback 放入 execute() 执行。获得到了锁执行业务代码当业务代码执行完需要释放锁。避免死锁
    10. 因为redis delete()并没有校验值这一方法,所以我们使用 Lua 脚本进行校验 value 匹配才允许删除 。并且使用脚本可以利用Redis 的原子性操作,取值、比较、删除是一个不可分割的操作。如果不使用脚本,3个步骤就分开了,会有并发的影响。例如我们在代码中获取值  if 判断 然后删除这并不是原子性操作
    11. if redis.call("get",KEYS[1]) == ARGV[1] then
          return redis.call("del",KEYS[1])
      else
          return 0
      end
    12. 运行两个实例,8080在44 秒时获得了锁,8088在45秒时请求获得锁,但是已经被8080获得了,所以导致无法获取锁。所以在多应用,跨JVM 中实现了分布式锁,只有一个客户端能获取到锁

     8080

    8088 

    3:使用Redisson分布式锁

    1:使用 api 实现

            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson</artifactId>
                <version>3.13.5</version>
            </dependency>
        @Transactional(rollbackFor = Exception.class)
        @RequestMapping("/redissonLock")
        public String redissonLock() {
            log.info("进入方法");
            Config config = new Config();
            config.useSingleServer().setAddress("redis://xxx:6379").setPassword("xxx");
            RedissonClient redissonClient = Redisson.create(config);
            RLock rLock = redissonClient.getLock("redissonLock");
            try {
                rLock.lock(10L, TimeUnit.SECONDS);
                log.info("获得了锁");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                rLock.unlock();
                log.info("释放锁");
            }
            log.info("方法执行完成");
            return "方法执行完成";
        }

    2:使用 Spring Xml实现

            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson</artifactId>
                <version>3.13.5</version>
            </dependency>
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:redisson="http://redisson.org/schema/redisson"
           xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
           http://redisson.org/schema/redisson
           http://redisson.org/schema/redisson/redisson.xsd
    ">
        <!-- minimal requirement -->
        <redisson:client>
            <!-- defaults to 127.0.0.1:6379 -->
            <redisson:single-server address="redis://xxx:6379" password="xxx"/>
        </redisson:client>
        <!-- or -->
    <!--    <redisson:client>-->
    <!--        <redisson:single-server address="${redisAddress}"/>-->
    <!--    </redisson:client>-->
    </beans>
    package com.example.redislock;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.ImportResource;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.annotation.Scheduled;
    
    @SpringBootApplication
    @EnableScheduling
    @ImportResource(locations = "classpath:redisson.xml")
    public class RedisLockApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(RedisLockApplication.class, args);
        }
    }
    
        @Autowired
        private RedissonClient redissonClient;
    
        @Transactional(rollbackFor = Exception.class)
        @RequestMapping("/redissonSpringLock")
        public String redissonSpringLock() {
            log.info("进入方法");
            RLock rLock = redissonClient.getLock("redissonLock");
            try {
                rLock.lock(10L, TimeUnit.SECONDS);
                log.info("获得了锁");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                rLock.unlock();
                log.info("释放锁");
            }
            log.info("方法执行完成");
            return "方法执行完成";
        }

    3:使用Spring Boot 实现

            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson-spring-boot-starter</artifactId>
                <version>3.13.5</version>
            </dependency>
    

    application.properties

    spring.redis.host=xxx
    spring.redis.password=xxx
        @Autowired
        private RedissonClient redissonClient;
    
        @Transactional(rollbackFor = Exception.class)
        @RequestMapping("/redissonSpringLock")
        public String redissonSpringLock() {
            log.info("进入方法");
            RLock rLock = redissonClient.getLock("redissonLock");
            try {
                rLock.lock(10L, TimeUnit.SECONDS);
                log.info("获得了锁");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                rLock.unlock();
                log.info("释放锁");
            }
            log.info("方法执行完成");
            return "方法执行完成";
        }

     4:实操-基于分布式锁解决定时任务重复执行问题

    1:redis 的实现

    需求:在集群环境下,每个应用的定时任务中,只能有一个应用执行这个方法。其他应用无法重复执行

    1. Application 启动类添加   @EnableScheduling 注解   ,用于开启 spring 定时任务
    2. 封装Lock
      package com.example.redislock.util;
      
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.data.redis.connection.RedisStringCommands;
      import org.springframework.data.redis.core.RedisCallback;
      import org.springframework.data.redis.core.RedisTemplate;
      import org.springframework.data.redis.core.script.RedisScript;
      import org.springframework.data.redis.core.types.Expiration;
      
      import java.util.Arrays;
      import java.util.UUID;
      
      /**
       * @Author: qiuj
       * @Description:
       * @Date: 2020-10-02 17:29
       */
      @Slf4j
      public class RedisLock implements AutoCloseable{
      
      
          public RedisLock(String key,Integer timeOutTime,RedisTemplate redisTemplate) {
              this.redisTemplate = redisTemplate;
              this.key = key;
              this.timeOutTime = timeOutTime;
              this.value = UUID.randomUUID().toString();
          }
      
          private RedisTemplate redisTemplate;
      
          String value;
          String key;
          Integer timeOutTime;
      
          public Boolean lock () {
              RedisCallback<Boolean> redisCallback = connection -> {
                  RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
                  Expiration expiration = Expiration.seconds(timeOutTime);
                  byte[] keyByte = redisTemplate.getKeySerializer().serialize(key);
                  byte[] valueByte = redisTemplate.getValueSerializer().serialize(value);
                  Boolean result = connection.set(keyByte, valueByte, expiration, setOption);
                  return result;
              };
              Boolean lock = (Boolean) redisTemplate.execute(redisCallback);
              return lock;
          }
      
          @Override
          public void close() throws Exception {
      
              String script = "            if  redis.call("get",KEYS[1]) == ARGV[1] then
      " +
                      "                return  redis.call("del",KEYS[1])
      " +
                      "            else
      " +
                      "                return 0
      " +
                      "            end";
              RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
              Boolean result = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(key), value);
              log.info("释放锁的结果:" + result);
          }
      }
      
    3. cron 设置为每隔5秒钟执行一次方法
      package com.example.redislock.task;
      
      import com.example.redislock.util.RedisLock;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.data.redis.core.RedisTemplate;
      import org.springframework.scheduling.annotation.Scheduled;
      import org.springframework.stereotype.Component;
      
      /**
       * @Author: qiuj
       * @Description:
       * @Date: 2020-10-02 17:26
       */
      @Slf4j
      @Component
      public class SendTextMessage {
      
          @Autowired
          private RedisTemplate redisTemplate;
      
          /*
          需求:每隔5秒发送一次短信 。但是在多应用情况下不能重复
           */
          @Scheduled(cron = "*/5 * * * * ?")
          public void send() {
              try (RedisLock redisLock = new RedisLock("textMessage",10,redisTemplate)){
                  if (redisLock.lock()) {
                      String message = "向13888888888发送一条短信";
                      log.info(message);
                  }
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
      }
    4. 开启两个应用 分别是 8080  8088
    5. 从  8080   从5秒-20秒   之间四次没有获得锁,8088则获取到锁。  8080从  20秒-35秒   之间成功获得锁,8088则没有获得锁

    2:Redisson 的实现 

        /*
            需求:每隔5秒发送一次短信 。但是在多应用情况下不能重复
        */
        @Scheduled(cron = "*/5 * * * * ?")
        public void send() throws InterruptedException {
            RLock rLock = redissonClient.getLock("redissonLock");
            //  尝试加锁,最多等待0秒,上锁以后30秒后自动释放锁
            if (rLock.tryLock(0,30L, TimeUnit.SECONDS)) {
                try {
                    String message = "向13888888888发送一条短信";
                    log.info(message);
                } finally {
                    rLock.unlock();
                    log.info("释放锁");
                }
            }
        }

    5:源码

    源码

     

  • 相关阅读:
    「HAOI2015」「LuoguP3178」树上操作(树链剖分
    「LuoguP3865」 【模板】ST表 (线段树
    「LuoguP3384」【模板】树链剖分
    「网络流24题」「Codevs1237」 餐巾计划问题
    「LuoguP1799」 数列_NOI导刊2010提高(06)
    「咕咕网校
    「数论」逆元相关
    「SHOI2007」「Codevs2341」 善意的投票(最小割
    「BZOJ3438」小M的作物(最小割
    「NOIP2005」「Codevs1106」篝火晚会
  • 原文地址:https://www.cnblogs.com/blogspring/p/14191738.html
Copyright © 2011-2022 走看看