【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

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
回答: Redis分布式锁是一种常见的解决方案,用于在分布式环境下实现互斥访问共享资源的机制。在面试中,可能会问到一些与Redis分布式锁相关的问题。以下是一些可能的问题和答案: 1. 请简要介绍一下Redis分布式锁的原理和实现方式。 Redis分布式锁的实现方式有多种,常见的包括使用SETNX命令和使用Redlock算法。使用SETNX命令可以通过在Redis中设置一个特定的键值对来实现锁的获取和释放。Redlock算法是一种多实例的分布式锁方案,它通过在多个Redis实例上获取锁来实现分布式锁的功能。 2. Redis分布式锁有哪些常见的问题和挑战? Redis分布式锁面临的常见问题包括锁的竞争条件、死锁和误删锁等。在高并发场景下,多个线程同时尝试获取锁可能导致竞争条件的发生。死锁是指当一个线程获取到锁后,由于某种原因无法释放锁,导致其他线程无法获取锁的情况。误删锁是指当一个线程释放锁时,由于某种原因导致其他线程误认为锁已经释放,从而导致并发问题的发生。 3. Redis分布式锁的实现方式有哪些优缺点? 使用SETNX命令实现的Redis分布式锁简单易用,但可能存在死锁和误删锁的问题。Redlock算法是一种更为复杂的实现方式,可以解决死锁和误删锁的问题,但在网络分区等异常情况下可能会导致锁的不一致性。 4. Redis分布式锁的性能如何? Redis分布式锁的性能取决于多个因素,包括网络延迟、Redis实例的性能和并发访问的情况等。在高并发场景下,使用SETNX命令实现的简单锁可能会导致性能瓶颈,而Redlock算法则可以提供更好的性能。 总结起来,Redis分布式锁是一种常见的解决方案,用于在分布式环境下实现互斥访问共享资源的机制。在面试中,可能会涉及到Redis分布式锁的原理、实现方式、常见问题和性能等方面的问题。 #### 引用[.reference_title] - *1* *2* *3* [2022年Redis最新面试题- Redis分布式锁](https://blog.csdn.net/q66562636/article/details/124739036)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杰肥啊

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值