RedLock & Redisson分布式锁

前言

Redis为什么可以做分布式锁

大家都知道有个setNx指令,set if not exist 。但是分布式锁从设计角度来讲,我🉐️有三个前提:

  • 1、必须有个标记,一般通过String字符串标识是否拿到了锁,然后我才能去开展我的业务
  • 2、去拿锁的时候必须保证只有一个人拿到,也就是说第二个线程进来的时候它会阻塞
  • 3、这把锁对所有线程都是可见的,当我拿到这把锁的时候,后面的人都知道这把锁被我拿了

那么Redisson客户端的出现解决了什么问题?

  • 1、解决了setNx会导致死锁的问题
  • 2、解决了锁可重入的问题(对比setNx,会存储加锁的线程信息,加锁的次数信息 - 加了几次就要释放几次,通过hash数据结构进行存储)
  • 3、解决了业务还没执行完,锁被释放的问题(时间轮 + 看门狗)

应用场景:

1、秒杀
2、抢优惠券
3、接口幂等性校验

总结:抢资源

1、单机锁 --> 到分布式锁

synchronized ()就不再这里做阐述了

1.1、基于opsForValue().setIfAbsent()实现

格式:setnx key value
将key的值设为value,当且仅当key不存在
若给定的key已存在,则setnx不做任何操作
setnx是【set if not exists】(若果不存在,则set)的简写
可用版本 >= 1.0.0

String lockKey = "lockKey";
Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
if(!result){
return "something exception";
}
// 大量业务代码
...
redisTemplate. delete(lockKey);

return "program End ...";

存在的问题,如果业务代码抛异常了,他还能delete吗?
删除不了,就死锁了

1.1.1 改进:加入try … finally. 无论如何,它最后都会delete

try{
    String lockKey = "lockKey";
    Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
    if(!result){
    return "something exception";
    }
    // 大量业务代码
    ...
    }
finally{ 
    redisTemplate. delete(lockKey);
}
return "program End ...";

继续看看现在存在的问题 ?
释放锁的问题解决了,但是执行到业务代码宕机了怎么办?finally就没用了,又会出现死锁问题。因为redis里的key始终存在。

1.1.2 改进,设置key的过期时间

try{
    String lockKey = "lockKey";
    Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
    // 加超时时间
    redisTemplate.expire(lockKey,10,TimeUnit.SECONDS);
    if(!result){
    return "something exception";
    }
    // 大量业务代码
    ...
    }
finally{ 
    redisTemplate. delete(lockKey);
}
return "program End ...";

但是我们这么写,就咩问题了吗?

Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
    // 会出现的问题
redisTemplate.expire(lockKey,10,TimeUnit.SECONDS);

如果这个key刚刚执行完setIfAbsent(), 结果程序挂了,还没到expire(), 那怎么办??这两行代码有原子性的问题

1.1.3 改进,解决原子性的问题

还别说,redis原生就有这两个合二为一的,在底层,redis会帮我们保证原子性

// Boolean result = redisTemplate. opsForValue().setIfAbsent(lockKey, "hello");
// redisTemplate.expire(lockKey,10,TimeUnit.SECONDS);
redisTemplate. opsForValue().setIfAbsent(lockKey, "hello",10,TimeUnit.SECONDS);

这么写的问题是什么呢??

如果放到超高并发场景那就很可怕了,就不小问题了。
假设过来个请求执行了15秒,结果到10秒这个锁就被清理调了,外面又不断有新的请求进来了… …

在这里插入图片描述

如此恶性循环下去会有怎样的后果??高并发不断有请求进来... ... 这样会导致什么问题呢?可能这把锁会永久失效!

1.1.4 针对高并发的问题,该怎么解决??

本质在于我自己的加的锁,结果会被别的线程给删掉,对不对。

接下来我们能想到的办法就是给这个锁加一个唯一标志。把id加到锁里边去。

String uniqueId = UUID.randomUUID.toString() + threadId;

当我们要解锁的时候判断一下,加锁的id是否是我本人加的

String uniqueId = UUID.randomUUID.toString() + threadId;
try{
    String lockKey = "lockKey";
    Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey,uniqueId, "hello",10,TimeUnit.SECONDS);
    
    if(!result){
    	return "something exception";
    }
    	// 大量业务代码
    	...
}
finally{ 
    if(uniqueId.equals(redisTemplate.opsForValue().get(lockKey))){
    	redisTemplate. delete(lockKey);
    }
}
return "program End ...";

当执行到这儿,还是有点问题哈,如果程序执行15秒才结束,但是10秒锁才释放。还是会有并发的问题。

时间不是解决问题的方案,还有没有别的思路?
在redis的处理方式上确实有实现方法:在此现况的情景下,通过定时器去扫,分线程每过10秒扫一次,判断一下主线程是否持有锁,key是否还存在,如果存在,把超时时间再设置长一点,跟token续期类似。

2、Redisson

现在市面上都有很多开源框架,来帮我们实现这些问题。而且人家都已经封装好了。

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.6.5</version>
</dependency>
// 初始化客户端
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
    // 注入到 Spring IOC 中
    @Bean
    public Redisson redissonClient(){
        // 此为单机模式
        Config  config = new Config();
        // 当然,这儿有很多模式可选择,主从、集群、复制、哨兵 等等 ... ...
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);    
    }
}

两个常用参数:
waitTime:等待时间 - 当锁拿不到的时候最多等你多久,通过自旋等待锁
leaseTime:锁释放时间,如果不传,默认是30秒,这个参数可配置

// 业务端
@Autowire
private Redisson redisson;

String lockKey = String.format(CommonConstant.RedisKey.EXAMPLE_KEY);

// 执行加锁的操作
// 可以看成 setIfAbsent(lockKey,uniqueId, "hello",10,TimeUnit.SECONDS);
boolean lockIs = redissonClient.tryLock(lockKey, 0L, 10L, TimeUnit.SECONDS);
if (lockIs) {
   		log.info("================== 当前已获取到锁,进行业务处理 ==============================");
	try{
    	// 业务代码
    	... ...
    }
	catch (Exception e) {
        log.error("================== 异常 ====================", e);
    }
}

以下为源码底层加锁逻辑:lua脚本实现
在这里插入图片描述

Redisson就不会有问题了吗??

如果redis是做的主从、哨兵,那么依然有问题

主节点写入了key,他是异步的再把数据同步到slave,只要主节点的key写成功了,马上返回给客户端,告诉客户端你这把锁加成功了,key设置成功,客户端线程1开始做业务逻辑了,这个时候,redis主节点才会往从节点去同步。

也就是说:同步在后

  • 在Redis的master节点上拿到了锁;
  • 但是这个加锁的key还没有同步到slave节点;
  • master故障,发生故障转移,slave节点升级为master节点;
  • 导致锁丢失。

假设,他刚准备同步给从节点,然后主节点挂了,怎么办?假设要同步的这个从节点被选举为master,高并发场景下新的线程(线程3)又进来,他请求的时候发现新的master他没有key,对于这种问题怎么解决呢?

3、Redis主从架构锁失效问题?

其实用redis去解决相对麻烦,当然也是有方法的。但是redis我们都已经做到极致了此时可以换种思路,比如用zk,他也是key-value结构,不过是树形的,CAP中满足CP,他不会马上告诉你成功(C:表示一致性),牺牲时间做同步。所以不存在这种问题。

怎么选择redis或者zookeeper,对并发要求比较高还是要选择redis(单机可支持10W的QPS),锁失效也只有手动对数据进行补偿,出错的概率是比较小。

4、RedLock实现

redis官方推荐使用 底层实现手段跟zk很类似

4.1、RedLock 简介

在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段。实现高效的分布式锁有三个属性需要考虑:

  • 安全属性:互斥,不管什么时候,只有一个客户端持有锁
  • 效率属性A:不会死锁
  • 效率属性B:容错,只要大多数redis节点能够正常工作,客户端端都能获取和释放锁。

Redlock是redis官方提出的实现分布式锁管理器的算法。这个算法会比一般的普通方法更加安全可靠。关于这个算法的讨论可以看下官方文档

超过半数redis节点(没有任何的依赖关系,就是单独节点)加锁成功才算加锁成功

4.2、RedLock的弊端?

  • 性能,原来只要主节点写成功了就行了,现在要很多台机器都加锁成功。
  • 都不能100%解决锁失效问题

详细实现可参考 方志朋大佬好文:RedLock的实现

  • 24
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redisson分布式锁是通过Redisson实现的一种分布式锁。它与普通的Redis分布式锁相比具有可重入的特点,即同一个线程可以多次获取同一个锁。这意味着在同一个线程中可以多次调用获取锁的方法,而不会造成死锁。 在使用Redisson实现的分布式锁中,可以使用tryLock方法来尝试获取锁,该方法会在指定的时间内尝试获取锁,如果获取成功则返回true,否则返回false。获取锁后,需要使用unlock方法来释放锁。 与普通的Redis分布式锁相比,Redisson分布式锁使用了RedLock的思想,即在多个Redis节点之间获取多个锁来提高锁的稳定性。这样可以避免单点故障导致的锁失效问题。然而,需要注意的是,RedLock并不能完全避免分布式锁的失效问题。 总结来说,Redisson分布式锁是一种基于Redis实现的可重入的分布式锁,它能够解决多个应用程序并发访问共享资源的问题,并通过RedLock的思想提高了锁的稳定性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Redis面试题,分布式锁](https://blog.csdn.net/qq_53868937/article/details/130760550)[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^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值