redis分布式锁

今天发现之前有人分使用redis实现分布式锁,发现了一些问题,下面用一些用例来解释一下这些问题形成原因及解决办法。

Demo1

代码

public String redisLockDemo1() {
        String redisKey = "redis_key_001";
        try {
            //1.获取锁
            Boolean getLock = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "极简版redis锁");
            if (!getLock) {
                return "未获取到redis锁";
            }
            //2.获取到redis锁,进行业务逻辑
            System.out.println("获取到redis锁");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //3.最后进行删除锁
            stringRedisTemplate.delete(redisKey);
        }
        return "done";
    }

这个demo是我在正在运行的一些项目中发现的代码,这个代码是用的是stringRedisTemplate的setIfAbsent api,setIfAbsent这个api对应的就是redis中的setnx命令。

缺陷

这段代码有一个很明显的缺陷,没有设置锁的超时时间,当某个线程获取到锁时候,如果我们的机器宕机,就没办法执行finnally中的释放锁,锁就会一直存在,其他线程无法获取到锁,系统崩溃,这是我们不想看到的结果,解决这一问题,可以参照一下demo2;

Demo2

代码

public String redisLockDemo2() {
        String redisKey = "redis_key_002";
        try {
            Boolean getLock = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "redis锁带有失效时间", 10, TimeUnit.SECONDS);
            if (!getLock) {
                return "未获取到redis锁";
            }
            //获取到redis锁,进行业务逻辑
            System.out.println("获取到redis锁");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            stringRedisTemplate.delete(redisKey);
        }
        return "done";
    }

demo2这段代码就解决了机器宕机等其他异常情况导致的锁无法释放的问题,但是这个demo2存在着其他问题,下面我用一张图来解释一下代码这么写会导致什么问题。

缺陷

redis 实现分布式锁java redis分布式锁实现代码_redis

来解释一下这个流程:

1.线程1,线程2,线程3同时去获取锁

2.线程1获取到锁,线程2,线程3未获取到锁,进入阻塞

3.线程1获取到锁之后,需要执行一段耗时15s的代码,但是锁的超时时间只有10s

4.当10s后,线程1的锁被释放,但是线程1代码还在执行;

5.此时线程2获取到锁

6.这时候线程1,线程2同时执行一段逻辑(此处为bug)

7.当线程1到15s时,代码执行完成,线程1执行deletekey操作,

8.此时锁再次被释放

9.线程2,线程3同时执行业务逻辑

通过上面流程可以看到,在代码执行到15s后,线程2锁被线程1释放,导致锁失效,要解决此问题,可以参照demo3。

 

Demo3

代码

public String redisLockDemo3() {
        String redisKey = "redis_key_003";
        String value = UUID.randomUUID().toString();
        try {
            Boolean getLock = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, value, 10, TimeUnit.SECONDS);
            if (!getLock) {
                return "未获取到redis锁";
            }
            //获取到redis锁,进行业务逻辑
            System.out.println("获取到redis锁");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (value.equals(stringRedisTemplate.opsForValue().get(redisKey))) {
                stringRedisTemplate.delete(redisKey);
            }
        }
        return "done";
    }

Demo3中使用uuid获取到随机字符串作为value,在删除key的时候进行获取redis中的value,如果一样则删除key,这样做看似解决了redisLockDemo2中的缺点,但还存在其他缺陷,但是在不适用其他框架的情况下,这样写代码,出现bug的几率会下降了很多。

缺陷

1.获取锁后,机器宕机或其他异常,后面的锁不会被释放,会锁10s中

2.判断value相当不是原子操作,此处出现异常,也会出现bug

3.代码没执行完,锁失效,其他线程获取到锁,这个bug就比较严重了,可能会引起好几个线程同时执行一段代码。

其实Demo3基本可以满足大本分需求了,小的bug我们可以通过一些运营人员来支撑,而且缺陷3可以通过给锁设置较长时间的失效时间来解决。但是并不是很完美,如果想要继续解决问题,就得引入一个新的框架---Redisson。我们将在下面一篇文章中来介绍redisson的使用及原理。