【Redis】分布式锁存在的问题及解决方案

特别说明

下面是自己的思路过程,伪代码是自己用编辑器写的,只是大致写一下,不要太过纠结于方法名是不是完全正确。

需求

现在有一个需求,获取广告数据,并发量2000

synchronized(this){
    //1.获取缓存中的数据,存在返回.
    //2.查询数据库
    //3.存入缓存
    //4.返回数据
}

以上这种代码在单机部署中是可以使用,但是在集群部署的情况下,是有问题的。因为synchronized是本地锁,解决不了只有一个线程进入的问题。当然有些业务确实如果哪怕集群部署,多几个线程访问数据库也没大碍,但是对一些特定的或者要求性比较高的业务来说,还是锁不住的。

指望本地锁肯定是不行了,那么就只能找一个统一的地方来管理这个锁。了解到,Redis中有一个命令符合这个特性,set nx 命令,该命令的意思是set if not exist,顾名思义就是如果不存在就设置(返回值=1),否则设置不成功(返回值=0),接下来我们就用这个命令来改善上面的问题。

第一次改变
//使用setnx命令
boolean lock =redisTemplate.opsForValue().setIfAbsent("lockName",1);
if(lock){
    //1.获取缓存中的数据,存在返回
    //2.查询数据库
    //3.存入缓存
    //4.返回数据
    //5.删除锁
}

这一次看似好像解决了本地锁的问题,但是其实还有因此的问题。我们思考一下,如果这时候有一个线程拿到锁之后,在还没到第5步之前,程序突然中断,不管什么方式,反正就是没到第5步。那么这时候问题就出现了,Redis中存的lockName锁并没有被删除。所以我们又再次进行改善,在存锁的时候给他设置一个过期时间

第二次改变
//使用setnx命令 设置锁,过期时间30秒
boolean lock =redisTemplate.opsForValue().setIfAbsent("lockName",1,30,TimeUnit.SECONDS);
if(lock){
    //1.获取缓存中的数据,存在返回
    //2.查询数据库
    //3.存入缓存
    //4.返回数据
    //5.删除锁
}

这一次解决了我们第一次说的由于意外中断程序而导致锁没有被删除那个问题。但是还是存在问题,接下来我描述一个场景,当线程A拿到锁之后进入了if判断,假设线程A执行了1,2,3,4步,而走这四步由于各种各样的网络延迟等原因,他就是执行了31秒。注意我们的锁的过期时间是30秒,就在这个时候线程B也进行锁申请,它是可以申请到的,当线程B拿到锁成功的时候,线程A执行了第5步,这时候问题就出来了,线程B很无辜啊,我刚拿了锁,锁就被人给删了。所以我们再次改善,设置锁的时候,值给他设置一个不会重复的值,这样每个线程都是拿自己的那个设置的值去删锁。

第三次改变
//这里我们用UUID先做一个唯一值的设置
String uuid = UUID.randomUUID().toString();
//使用setnx命令 设置锁,过期时间30秒
boolean lock =redisTemplate.opsForValue().setIfAbsent("lockName",uuid,30,TimeUnit.SECONDS);
if(lock){
    //1.获取缓存中的数据,存在返回
    //2.查询数据库
    //3.存入缓存
    //4.返回数据
    //5.根据lockName获取值
    String value = redisTemplate.opsForValue().get("lockName");
    //比对两个值是否相等
    if(uuid.equal(value)){
        //6.删除锁
    }
}

这一次解决了我们第二次里面留下的误删了别人的锁问题。虽然上面已经解决了大部分问题,看似很好,但是还是存在问题,我再描述一个场景,还是有两个线程,线程A和线程B。当线程A成功设置了锁,正常执行到第5步的时候,它去redis中根据lockName取了值。我们知道取东西有两个动作,去redis的路上从redis拿到值回来的路上。问题就出现在了回来的路上,如果回来的路上因为各种各种的网络延迟等原因,他就是执行了31秒。注意我们的锁的过期时间是30秒,这时候锁过期了被redis自身删除了,就在这个时候线程B进行锁申请,它是可以申请到的,当线程B拿到锁之后,这时候线程A第5步成功回来了,比对了两个值,发现value跟uuid是一样的,然后线程A又把锁给删除了。所以我们这时候我们应该思考,第5步跟第6步应该是原子性的,它应该与setIfAbsent是一样的,必须原子性。

第四次改变
//这里我们用UUID先做一个唯一值的设置
String uuid = UUID.randomUUID().toString();
//使用setnx命令 设置锁,过期时间30秒
boolean lock =redisTemplate.opsForValue().setIfAbsent("lockName",uuid,30,TimeUnit.SECONDS);
if(lock){
    //1.获取缓存中的数据,存在返回
    //2.查询数据库
    //3.存入缓存
    //4.返回数据
    //5.使用lua脚本进行删除  先比较值是否相同,相同就删除
    String delKeyLuaScript="if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
    RedisScript redisScript =  RedisScript.of(delKeyLuaScript);
    //6.删除锁
    redisTemplate.execute(redisScript,Arrays.asList("lockName"),uuid);
}

至此,分布式锁基本要解决的问题都解决了,但是经过四次演变,还有一个问题并没有谈,也非常重要,就是锁的续期。因为我们的业务执行时间很有可能大于过期时间,如果没有续期,其他线程还是可以争夺到锁,进入业务代码内。

思考

所以经过上面的演变过程,我们可以知道作为分布式锁的四个关键点。

  • 原子性
  • 过期时间
  • 正确删除锁
  • 锁续期

在Java中Redis提供了分布式锁Redisson解决方案,有空的小伙伴可以学习一下。

https://github.com/redisson/redisson/wiki/Table-of-Content

你可能感兴趣的:(Redis,redis,缓存,java)