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秒这个锁就被清理调了,外面又不断有新的请求进来了… …

RedLock & Redisson分布式锁_第1张图片

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

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脚本实现
RedLock & Redisson分布式锁_第2张图片

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的实现

你可能感兴趣的:(分布式锁,redisson,redis,经验分享,java)