redis 分布式锁(自己手动实现)

java中的锁,Synchronized是基于对象头部的锁标志位,Lock是基于volatile的一个变量,但是在微服务多个不同的进程之间这些标志位是不共享的,因此需要一个为分布式服务,存储共享锁标志。常见的分布式锁:redis分布式锁,zookeeper分布式锁,数据库的分布式锁等。

基于分布式锁现在已经有很多开源的实现,我们可以直接引用就行,基于redis的redission,基于zookeeper的 Curator框架,Spring框架也为此为我们提供了统一的分布式锁的定义接口。

参考学习链接: https://blog.csdn.net/qq_35529801/article/details/103878784

我们也可以手动实现下redis分布式锁,体验一下快感(参考下spring 框架的定义):

1. 先创建一个spring boot项目

引入依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

 

配置redis连接信息

 

server:
  port: 8888
spring:
  redis:
    host: 192.168.10.103
    port: 6379

 

2.实现

2.1 最简单的实现version1

在redis中判断key,如果存在表示已经有人持有锁了,没有则我们放入这个key去获取锁,执行完业务逻辑将这个key删除。

key我们自定义: lock:consume

value随便设置: hjj

@Component
public class LockDemo {
   @Autowired
   private RedisTemplate<String, String> redisTemplate;

   public boolean lockVersion1() {
      String lockValue = redisTemplate.opsForValue().get("lock:consume");

      if (lockValue == null) {
         redisTemplate.opsForValue().set("lock:consume", "hjj");
         return true;
      }

      return false;
   }

   public void unlockVersion1() {
      redisTemplate.delete("lock:consume");
   }
}

测试类

@RunWith(SpringRunner.class)
@SpringBootTest
public class LockDemoTest {
   @Autowired
   private LockDemo lockDemo;

   @Test
   public void lockVersion1() {
      System.out.println("第1个人获取锁" + lockDemo.lockVersion1());
      System.out.println("第2个人获取锁" + lockDemo.lockVersion1());
   }

   @Test
   public void unlockVersion1() {
      lockDemo.unlockVersion1();
   }
}

 

redis中的key已经存在

 

 

 

然后我们运行释放锁的测试方法,redis的键已经不存在了。

我们来模仿下实际应用

@Test
public void lockTest() {
   try {
      if (lockDemo.lockVersion1()) {
         System.out.println("执行业务逻辑,睡100秒钟");
         Thread.sleep(100000);
      } else {
         System.out.println("获取锁失败");
      }
   } catch (InterruptedException e) {
      e.printStackTrace();
   } finally {
      System.out.println("释放锁");
      lockDemo.unlockVersion1();
   }
}

@Test
public void lockTest2() {
   try {
      if (lockDemo.lockVersion1()) {
         System.out.println("执行业务逻辑,睡100秒钟");
         Thread.sleep(100000);
      } else {
         System.out.println("获取锁失败");
      }
   } catch (InterruptedException e) {
      e.printStackTrace();
   } finally {
      System.out.println("释放锁");
      lockDemo.unlockVersion1();
   }
}

测试1方法

 

测试2方法

 

你会发现他们使用同一个Key在没获取到锁的时候也会去释放锁,删除key,这样会使test1在执行业务逻辑期间,它的锁被test2获取失败后,释放掉了,这样再来test3用户又能去获取了,很明显是有问题的,我们需要一个标识来标记这个锁属于此人,如果不是它的,执行释放锁操作就不能进行操作。

 

2.1 实现version2(预防非法释放锁)

怎样去标识呢,UUID了解下,百度介绍,简而言之就是uuid全宇宙不会重复

@Component
public class LockDemo2 {
   @Autowired
   private RedisTemplate<String, String> redisTemplate;

   private String lockValue;

   public boolean lockVersion2() {
      String lockValue = redisTemplate.opsForValue().get("lock:consume");

      if (lockValue == null) {
         String uuid = UUID.randomUUID().toString();
         this.lockValue = uuid;
         redisTemplate.opsForValue().set("lock:consume", uuid);
         return true;
      }

      return false;
   }

   public void unlockVersion2() {
      if (lockValue != null && lockValue.equals(redisTemplate.opsForValue().get("lock:consume"))) {
         System.out.println("我的锁我自己释放了");
         redisTemplate.delete("lock:consume");
      } else {
         System.out.println("不是我的锁我不释放");
      }

   }
}

 测试方法

@RunWith(SpringRunner.class)
@SpringBootTest
public class LockDemoTest {
   @Autowired
   private LockDemo2 lockDemo2;


   @Test
   public void lockTest() {
      try {
         if (lockDemo2.lockVersion2()) {
            System.out.println("用户1执行业务逻辑,睡100秒钟");
            Thread.sleep(100000);
         } else {
            System.out.println("用户1获取锁失败");
         }
      } catch (InterruptedException e) {
         e.printStackTrace();
      } finally {
         System.out.println("用户1释放锁");
         lockDemo2.unlockVersion2();
      }
   }

   @Test
   public void lockTest2() {
      try {
         if (lockDemo2.lockVersion2()) {
            System.out.println("用户2执行业务逻辑,睡100秒钟");
            Thread.sleep(100000);
         } else {
            System.out.println("用户2获取锁失败");
         }
      } catch (InterruptedException e) {
         e.printStackTrace();
      } finally {
         System.out.println("用户2释放锁");
         lockDemo2.unlockVersion2();
      }
   }

   @Test
   public void lockTest3() {
      try {
         if (lockDemo2.lockVersion2()) {
            System.out.println("用户3执行业务逻辑,睡100秒钟");
            Thread.sleep(100000);
         } else {
            System.out.println("用户3获取锁失败");
         }
      } catch (InterruptedException e) {
         e.printStackTrace();
      } finally {
         System.out.println("用户3释放锁");
         lockDemo2.unlockVersion2();
      }
   }

}

redis的key还存在,并且其他用户获取锁失败了

 

 

 

 

这次解决了非法释放的问题,我们再来看加锁的代码

public boolean lockVersion2() {
      String lockValue = redisTemplate.opsForValue().get("lock:consume");

      if (lockValue == null) {
         String uuid = UUID.randomUUID().toString();
         this.lockValue = uuid;
         redisTemplate.opsForValue().set("lock:consume", uuid);
         return true;
      }

      return false;
   }

虽然redis是单线程的,但是如果两个人同时读到key为lock:consumer的没有设置值的情况

 

因此我们需要将查看redis的值是否存在和设置值弄成一个不可分割的操作,类似于事务,而redis也为我们提供了这个命令 setnx key value,只有在不存在的时候才会去设置值,存在就不设置值了。

 

 

2.3 version3(操作原子性)

将判断锁和加锁一步完成。

java代码

@Component
public class LockDemo3 {
   @Autowired
   private RedisTemplate<String, String> redisTemplate;

   private String lockValue;

   public boolean lockVersion2() {
      String uuid = UUID.randomUUID().toString();

      if (redisTemplate.opsForValue().setIfAbsent("lock:consume", uuid)) {
         this.lockValue = uuid;

         return true;
      }

      return false;
   }

   public void unlockVersion2() {
      if (lockValue != null && lockValue.equals(redisTemplate.opsForValue().get("lock:consume"))) {
         System.out.println("我的锁我自己释放了");
         redisTemplate.delete("lock:consume");
      } else {
         System.out.println("不是我的锁我不释放");
      }

   }
}

这次看似肯定没问题了,分布式服务有个最大的特点就是防止单点灾难,如果你在加锁期间你的服务挂了咋办,你的key一直不会被释放,这样大家一块服务不能使用了,肯定不行,redis也有设置键的过期命令set key value ex number nx 其中number就是时间,nx表示不存在才会执行。

但是超时时间设置多长呢,这是一个问题哦,设置的长了,没影响,运行完业务直接删除就行了,但 是设置的短了,你还在执行业务,锁没了,其他人直接用了,我们改咋整呢。

 

 

聪明的大家肯定能想到定时任务其周期刷新,如果我们设置一个定时任务去周期性的帮我们续费key的时间。如果这个线程一直在,就一直续费,感觉不错。

2.4 version4(续费)

大体思路是:

获取锁成功,启动一个定时任务去周期设置key的失效时间,当然在key不存在或者此线程已经没了,也就是执行完业务之后,我们应该停止此定时任务

@Component
public class LockDemo4 {
   @Autowired
   private RedisTemplate<String, String> redisTemplate;
   
   private String lockValue;

   public boolean lockVersion4() {
      String uuid = UUID.randomUUID().toString();

      if (redisTemplate.opsForValue().setIfAbsent("lock:consume", uuid)) {
         this.lockValue = uuid;
         renewKey(Thread.currentThread(), "lock:consume");

         return true;
      }

      return false;
   }

   public void unlockVersion4() {
      if (lockValue != null && lockValue.equals(redisTemplate.opsForValue().get("lock:consume"))) {
         System.out.println("我的锁我自己释放了");
         redisTemplate.delete("lock:consume");
      } else {
         System.out.println("不是我的锁我不释放");
      }
   }

   /**
    * 定时续费
    * @param thread
    * @param key
    */
   public void renewKey(Thread thread, String key) {
      ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);

      scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
         @Override
         public void run() {
            if (thread.isAlive() && redisTemplate.hasKey(key)) {
               System.out.println("线程还在,给key续30秒");
               redisTemplate.expire(key, 30, TimeUnit.SECONDS);
            } else {
               System.out.println("线程已经不存在,终止定时任务");
               throw new RuntimeException("终止定时任务");
            }
         }
      }, 10, 10, TimeUnit.SECONDS);
   }
}

测试类

@RunWith(SpringRunner.class)
@SpringBootTest
public class LockDemoTest {
   @Autowired
   private LockDemo4 lockDemo4;


   @Test
   public void lockTest() {
      try {
         if (lockDemo4.lockVersion4()) {
            System.out.println("用户1执行业务逻辑,睡50秒钟");
            Thread.sleep(50000);
         } else {
            System.out.println("用户1获取锁失败");
         }
      } catch (InterruptedException e) {
         e.printStackTrace();
      } finally {
         System.out.println("用户1释放锁");
         lockDemo4.unlockVersion4();
      }
   }

   @Test
   public void lockTest2() {
      try {
         if (lockDemo4.lockVersion4()) {
            System.out.println("用户2执行业务逻辑,睡100秒钟");
            Thread.sleep(100000);
         } else {
            System.out.println("用户2获取锁失败");
         }
      } catch (InterruptedException e) {
         e.printStackTrace();
      } finally {
         System.out.println("用户2释放锁");
         lockDemo4.unlockVersion4();
      }
   }

   @Test
   public void lockTest3() {
      try {
         if (lockDemo4.lockVersion4()) {
            System.out.println("用户3执行业务逻辑,睡100秒钟");
            Thread.sleep(100000);
         } else {
            System.out.println("用户3获取锁失败");
         }
      } catch (InterruptedException e) {
         e.printStackTrace();
      } finally {
         System.out.println("用户3释放锁");
         lockDemo4.unlockVersion4();
      }
   }

}

启动测试方法1和测试方法2

 

 

 

redis的key已经删除

 

测试一下异常情况,直接终止方法

 

 

 

 

实现到现在只能感觉自己太牛了,只需要改动下代码让其更符合使用的逻辑即可,比如说key让用户传进来,让用户自己设置过期时间,阻塞获取锁,或者定时一段时间内去获取锁。

 

2.5 version5(优化下接口)

实现逻辑

@Component
public class LockDemo5 {
   @Autowired
   private RedisTemplate<String, String> redisTemplate;

   private String lockValue;

   private ThreadLocal<String> keyMap = new ThreadLocal<>();

   @Autowired
   private ScheduledExecutorService scheduledExecutorService;


   public boolean tryLock(String key) {
      keyMap.set(key);
      String uuid = UUID.randomUUID().toString();

      if (redisTemplate.opsForValue().setIfAbsent(key, uuid)) {
         this.lockValue = uuid;
         renewKey(Thread.currentThread(), key);

         return true;
      }

      return false;
   }

   public boolean tryLock(String key, long time) {
      keyMap.set(key);
      String uuid = UUID.randomUUID().toString();
      Instant endTime = Instant.now().plusMillis(time);

      while(Instant.now().getEpochSecond() < endTime.getEpochSecond()) {

         if (redisTemplate.opsForValue().setIfAbsent(key, uuid)) {
            this.lockValue = uuid;
            renewKey(Thread.currentThread(), key);

            return true;
         }
      }
      keyMap.remove();
      return false;
   }

   public void lock(String key) {
      keyMap.set(key);
      String uuid = UUID.randomUUID().toString();

      while (true) {
         if (redisTemplate.opsForValue().setIfAbsent(key, uuid)) {
            this.lockValue = uuid;
            renewKey(Thread.currentThread(), key);

            break;
         }
      }
   }

   public void unlock() {
      String key = keyMap.get();
      System.out.println(lockValue);
      System.out.println(redisTemplate.opsForValue().get(key));

      if (lockValue != null && lockValue.equals(redisTemplate.opsForValue().get(key))) {
         System.out.println(LocalDateTime.now() + " 我的锁我自己释放了");
         redisTemplate.delete(key);
         keyMap.remove();
      } else {
         System.out.println(LocalDateTime.now() + " 不是我的锁我不释放");
      }
   }

   /**
    * 定时续费
    * @param thread
    * @param key
    */
   public void renewKey(Thread thread, String key) {
      scheduledExecutorService.scheduleAtFixedRate(() -> {
            if (thread.isAlive() && redisTemplate.hasKey(key)) {
               System.out.println(LocalDateTime.now() + " 线程还在,给key续30秒");
               redisTemplate.expire(key, 30, TimeUnit.SECONDS);
            } else {
               System.out.println("线程已经不存在,终止定时任务");
               throw new RuntimeException("终止定时任务");
            }
      }, 10, 10, TimeUnit.SECONDS);
   }

}

配置类

@Configuration
public class LockDemo5Config {
   @Bean
   public ConcurrentHashMap<Thread, String> map() {
      return new ConcurrentHashMap<>();
   }

   /**
    * 使用线程池优化新性能
    * @return
    */
   @Bean
   public ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() {
      return  new ScheduledThreadPoolExecutor(10);
   }
}

测试阻塞获取

@RunWith(SpringRunner.class)
@SpringBootTest
public class LockDemoTest {
   @Autowired
   private LockDemo5 lockDemo5;

   private String key = "lock";


   @Test
   public void lockTest() {
      try {
         System.out.println(LocalDateTime.now() + " 用户1开始获取锁");
         lockDemo5.lock(key);
         System.out.println(LocalDateTime.now() + " 用户1获取锁成功");
         System.out.println(LocalDateTime.now() + " 用户1执行业务逻辑,睡50秒钟");
         Thread.sleep(50000);
      } catch (InterruptedException e) {
         e.printStackTrace();
      } finally {
         System.out.println(LocalDateTime.now() + " 用户1释放锁");
         lockDemo5.unlock();
      }
   }

   @Test
   public void lockTest2() {
      try {
         System.out.println(LocalDateTime.now() + " 用户2开始获取锁");
         lockDemo5.lock(key);
         System.out.println(LocalDateTime.now() + " 用户2获取锁成功");
         System.out.println(LocalDateTime.now() + " 用户2执行业务逻辑,睡50秒钟");
         Thread.sleep(50000);
      } catch (InterruptedException e) {
         e.printStackTrace();
      } finally {
         System.out.println(LocalDateTime.now() + " 用户2释放锁");
         lockDemo5.unlock();
      }
   }

}

测试一直阻塞获取方法

 

 

两个用户的执行业务时间完美隔开,nice!

测试尝试获取,如果获取不到直接返回

@Test
public void lockTest3() {
   try {
      System.out.println(LocalDateTime.now() + " 用户1尝试开始获取锁");
      if (lockDemo5.tryLock(key)) {
         System.out.println(LocalDateTime.now() + " 用户1获取锁成功");
         System.out.println(LocalDateTime.now() + " 用户1执行业务逻辑,睡50秒钟");
         Thread.sleep(50000);
      } else {
         System.out.println(LocalDateTime.now() + " 用户1尝试获取锁失败");
      }
   } catch (InterruptedException e) {
      e.printStackTrace();
   } finally {
      System.out.println(LocalDateTime.now() + " 用户1释放锁");
      lockDemo5.unlock();
   }
}

@Test
public void lockTest4() {
   try {
      System.out.println(LocalDateTime.now() + " 用户2尝试开始获取锁");
      if (lockDemo5.tryLock(key)) {
         System.out.println(LocalDateTime.now() + " 用户2获取锁成功");
         System.out.println(LocalDateTime.now() + " 用户2执行业务逻辑,睡50秒钟");
         Thread.sleep(50000);
      } else {
         System.out.println(LocalDateTime.now() + " 用户2尝试获取锁失败");
      }
   } catch (InterruptedException e) {
      e.printStackTrace();
   } finally {
      System.out.println(LocalDateTime.now() + " 用户2释放锁");
      lockDemo5.unlock();
   }
}

 

测试等待多久获取,获取不到再返回

测试业务执行50秒,等待60秒能获取到的情况

 

 

 

测试业务执行50秒,等待40秒获取不到的情况

 

这个时候感觉自己已经做得很棒了,我们来百度下分布式锁的特性:

 

分布式锁实现有多种方式,其原理都基本类似,只要满足下列要求即可:

多进程可见:多进程可见,否则就无法实现分布式效果

互斥():同一时刻,只能有一个进程获得锁,执行任务后释放锁

可重入(可选):同一个任务再次获取改锁不会被死锁

阻塞锁(可选):获取失败时,具备重试机制,尝试再次获取锁

性能好(可选):效率高,应对高并发场景

高可用:避免锁服务宕机或处理好宕机的补救措施

高可用,我们好像不满足,这个需要redis集群来满足,可重入,感觉没用到多次获取这种情况,但是想想synchronized可以调用synchronized的方法并且不会出现问题,我们先来搭建个redis集群吧,我们现在的锁功能好像满足了,但是我们应该满足设计模式的面向接口编程,我们可以抄袭下spring 分布式锁的接口定义。

docker搭建redis集群: https://mp.csdn.net/postedit/103963576

2.6 最终版(提供可重入,优化接口设计)

synchronized是以一个标志位monitor如果是0表示,没有用,这个时候进行+1,当再次获取时(synchronized方法调用另一个synchronized方法)会判断这个线程是够持有这个锁,如果持有进行+1,运行完这个方法,进行-1,相对的我们可以使用redis中的hash来存储这个。

 

key 分布式锁 field uuid value count

 

 

 

获取锁的步骤:

1.先判断key是否存在

2.如果存在,判断是否是自己的锁,使用唯一的uuid表示,如果是,给count +1,如果不是表示锁已经被别人占有,加锁失败

3.如果不存在,表示锁还没有被持有,则添加hash,key为分布式锁的标识,field为uuid,唯一的锁身份标识,标识是谁的锁,value设置为1表示进入了一次

 

释放锁的步骤:

1.先判断key是否存在

2.如果存在,则判断是不是自己的锁,通过唯一的身份标识uuid,如果是,count进行-1操作,-1之后如果值为0,则删除这个hash。如果不是自己的锁,则不做任何操作

3.如果不存在,不做任何操作

 

这个时候你的注意大家如果都读取到那个能获取锁的时间,同时加锁咋整,虽然redis是单线程的,但是如果两个人读取Key是否存在刚好同时操作,就会出问题,为此我们需要将获取锁和释放锁以数据库的事务一样要么全部完成,要么都失败,但是很不幸redis的事务并不是数据库的事务,不过也相应的提供了lua脚本功能,你可以在脚本中,将执行的redis命令一次性执行完,对于redis而言他就是一条命令。需要专门去学这东西吗,我个人感觉用处不大,用的时候直接复制过来就行,而且看起来也不是很难懂。

 

LockRegistry接口

public interface LockRegistry {
   /**
    * 创建锁
    * @param key
    * @return
    */
   Lock obtain(String key);
}

Lock接口

public interface Lock {
   /**
    * 尝试加锁,如果获取不到,就阻塞
    */
   void lock();

   /**
    * 尝试加锁,如果获取到返回true,如获取不到返回false
    * @return
    */
   boolean tryLock();

   /**
    * 尝试加锁,如果获取指定时间内没有获取到返回true,如获取不到返回false
    * @param time
    * @return
    */
   boolean tryLock(long time);

   /**
    * 释放锁
    */
   void unlock();
}

RedisLockConfiguration

@Configuration
public class RedisLockConfiguration {
   @Bean
   public LockRegistry lockRegistry(StringRedisTemplate redisTemplate) {
      return  new RedisLockRegistry(redisTemplate,"hjj");
   }
}

RedisLockRegistry

public class RedisLockRegistry implements LockRegistry {
   protected String lockPrefix;
   protected StringRedisTemplate redisTemplate;

   public RedisLockRegistry(StringRedisTemplate redisTemplate,String lockPrefix) {
      this.redisTemplate = redisTemplate;
      this.lockPrefix = lockPrefix;
   }

   @Override
   public Lock obtain(String key) {
      return new RedisLockImpl(redisTemplate, lockPrefix + ":" + key);
   }
}

RedisLockImpl接口

public class RedisLockImpl implements Lock {
   private StringRedisTemplate redisTemplate;
   private String lockKey;
   private String lockKeyValue;
   private long DEFAULT_RELEASE_TIME = 30;
   private static final DefaultRedisScript<Long> LOCK_SCRIPT;
   private static final DefaultRedisScript<Object> UNLOCK_SCRIPT;
   private ScheduledExecutorService scheduledExecutorService  = new ScheduledThreadPoolExecutor(1);

   static {
      // 加载释放锁的脚本
      LOCK_SCRIPT = new DefaultRedisScript<>();
      LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new
            ClassPathResource("lock.lua")));
      LOCK_SCRIPT.setResultType(Long.class);
      // 加载释放锁的脚本
      UNLOCK_SCRIPT = new DefaultRedisScript<>();
      UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new
            ClassPathResource("unlock.lua")));
   }

   public RedisLockImpl(StringRedisTemplate redisTemplate, String lockKey) {
      this.redisTemplate = redisTemplate;
      this.lockKey = lockKey;
      this.lockKeyValue = UUID.randomUUID().toString();
   }



   @Override
   public boolean tryLock() {
      // 执行脚本
      Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),
            lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));
      // 判断结果
      return result != null && result.intValue() == 1;
   }

   @Override
   public boolean tryLock(long time) {
      Instant endTime = Instant.now().plusMillis(time);

      while(Instant.now().getEpochSecond() < endTime.getEpochSecond()) {
         Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),
               lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));

         if (result != null && result.intValue() == 1) {
            renewKey(Thread.currentThread());

            return true;
         }
      }

      return false;
   }

   @Override
   public void lock() {

      while (true) {
         Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),
               lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));

         if (result != null && result.intValue() == 1) {
            renewKey(Thread.currentThread());

            break;
         }
      }
   }

   @Override
   public void unlock() {
      // 执行脚本
      redisTemplate.execute(
            UNLOCK_SCRIPT,
            Collections.singletonList(lockKey),
            lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));
   }

   /**
    * 定时续费
    * @param thread
    */
   public void renewKey(Thread thread) {
      scheduledExecutorService.scheduleAtFixedRate(() -> {
         if (thread.isAlive() && redisTemplate.hasKey(lockKey)) {
            redisTemplate.expire(lockKey, DEFAULT_RELEASE_TIME, TimeUnit.SECONDS);
         } else {
            throw new RuntimeException("终止定时任务");
         }
      }, 10, 10, TimeUnit.SECONDS);
   }
}

lua脚本放在resource文件夹下

lock.lua

local key = KEYS[1]
local threadId = ARGV[1]
local releaseTime = ARGV[2]

if(redis.call('exists', key) == 0)
then
    redis.call('hset', key, threadId, '1')
    redis.call('expire', key, releaseTime)
    return 1
end

if(redis.call('hexists', key, threadId) == 1)
then
    redis.call('hincrby', key, threadId, '1')
    redis.call('expire', key, releaseTime)
    return 1
end
return 0

unlock.lua

local key = KEYS[1]
local threadId = ARGV[1]
local releaseTime = ARGV[2]

if (redis.call('HEXISTS', key, threadId) == 0) then
    return nil
end
local count = redis.call('HINCRBY', key, threadId, -1)

if (count > 0) then
    redis.call('EXPIRE', key, releaseTime)
    return nil
else
    redis.call('DEL', key)
    return nil
end

lua脚本有中文注释报这种奇怪的错误,有毒

 

 

 

测试阻塞获取 void lock()

 

测试尝试获取boolean tryLock()

 

 

测试一段时间内不停尝试获取 boolean tryLock(long time)

 

 

2.7 关于redis高可用的疑问

主从模式下的集群,都存在数据的复制延迟,可能在主节点用户1加锁成功,这个时候数据还没有复制到从节点,然后主节点挂掉了,然后从节点升级为主节点,这个时候就存在两人同时拥有锁的场景。

 

在Redis的分布式环境中,我们假设有5个Redis master。这些节点完全互相独立,不存在主从复制或

者其他集群协调机制。我们确保将在每个实例上使用之前介绍过的方法获取和释放锁,这样就能保

证他们不会同时都宕掉。实现高可用。

为了取到锁,客户端应该执行以下操作:

1. 获取当前Unix时间,以毫秒为单位。

2. 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端

应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自

动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉

的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应

该尽快尝试另外一个Redis实例。

3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当

且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。

4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结

果)。

5. 如果因为某些原因,获取锁失败(没有 在至少N/2+1个Redis实例取到锁或者取锁时间已经超

过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有

加锁成功)。

 

不过,这种方式并不能完全保证锁的安全性,因为我们给锁设置了自动释放时间,因此某些极端特例

下,依然会导致锁的失败,例如下面的情况:

 

  • 如果 Client 1 在持有锁的时候,发生了一次很长时间的 FGC 超过了锁的过期时间。锁就被释放了。
  • 这个时候 Client 2 又获得了一把锁,提交数据。
  • 这个时候 Client 1 从 FGC 中苏醒过来了,又一次提交数据。冲突发生了

 

还有一种情况也是因为锁的超时释放问题,例如:

  • Client 1 从 A、B、D、E五个节点中,获取了 A、B、C三个节点获取到锁,我们认为他持有了锁
  • 这个时候,由于 B 的系统时间比别的系统走得快,B就会先于其他两个节点优先释放锁。
  • Clinet 2 可以从 B、D、E三个节点获取到锁。在整个分布式系统就造成 两个 Client 同时持有锁了。

不过,这种因为时钟偏移造成的问题,我们可以通过延续超时时间、调整系统时间减少时间偏移(配置ntp时间同步)等方式

来解决。Redis作者也对超时问题给出了自己的意见:

在工作进行的过程中,当发现锁剩下的有效时间很短时,可以再次向redis的所有实例发送一个Lua脚

本,让key的有效时间延长一点(前提还是key存在并且value是之前设置的value)。客户端扩展TTL时必须像首次取得锁一样在大多数实例上扩展成功才算再次取到锁,并且是在有效时

间内再次取到锁(算法和获取锁是非常相似的)。

简单来说就是在获取锁成功后,监视锁的失效时间,如果即将到期,可以再次去申请续约,延长锁的有

效期。

我们可以采用看门狗(watch dog)解决锁超时问题,开启一个任务,这个任务在 获取锁之后10秒后,重

新向redis发起请求,重置有效期,重新执行expire(实际我们已经实现了)。

 

而Redission已经帮我们实现了这些功能,什么各种奇怪的安全问题,实现了各种锁,后面我们也会发文章介绍使用:

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值