三、详解Redis分布式锁&Redisson分布式锁

本文详细探讨了分布式锁的概念,重点介绍了Redis的setnx和expire命令实现分布式锁,以及Redisson的lua脚本和可重入性。还讨论了分布式锁的失效问题和解决方案,包括Redisson的看门狗机制。
摘要由CSDN通过智能技术生成

目录

一、什么是分布式锁?

二、常见的分布式锁实现方式

三、Redis分布式锁的实现

3.1 setnx和expire命令

3.2 setnx分布式锁的缺点

四、Redisson分布式锁

4.1 lua脚本

4.2 源码解析

4.2.1 redisson的demo

4.2.2 redisson分布式锁

如何实现可重入

如何实现超时时间过短或过长  (看门狗🐶)

五、分布式锁失效问题(连锁,multilock,redlock)


一、什么是分布式锁?

在传统的java进程中,我们常常用Synchronized三、详解Synchronized-CSDN博客或者ReentrantLock五、详解ReentrantLock-CSDN博客来对临界区进行加锁,防止多个线程之间并行访问,导致数据读写异常。但是这种锁的粒度仅限于当前jvm中,在工业生产环境下,往往一个web项目会部署多台机器,也就意味着会有多个jvm。那么这几个jvm是独立的,这就导致上述的锁失效。

分布式锁是一种在分布式系统环境下,通过多个节点对共享资源进行访问控制的机制。在分布式系统中,为了防止多个节点同时操作同一份数据,从而导致数据的不一致,需要使用分布式锁来确保在同一时刻只有一个节点能够操作数据。 

二、常见的分布式锁实现方式

分布式锁的实现方式有很多种,常见的有基于数据库的分布式锁、基于缓存(如Redis)的分布式锁、基于ZooKeeper的分布式锁等。

而在互联网工业生产环境中,由于对性能的要求,基本上都在使用Redis来进行分布式锁。

三、Redis分布式锁的实现

3.1 setnx和expire命令

在redis的命令 一、Redis常用命令-CSDN博客 中,我们学到过setnx命令。setnx命令常用于实现分布式锁。具体来说,多个客户端可以尝试使用setnx命令来创建相同的锁(key),只有一个客户端的操作会成功,成功的客户端将获得锁,失败的客户端将不会获得锁。这样可以确保同时只有一个客户端能够操作被锁住的资源,避免并发操作导致的数据不一致。

但是setnx的命令没有设置超时时间,因此当前的key会一直存放在redis中,除非有线程主动的del key。因此,在添加setnx key 的时候,需要在后面设置expire key 过期时间。

如果 setnx key  和 expire key 中间,服务挂了,那么当前key将一直存在Redis,导致其他线程无法加锁。

因此,可以使用set命令,因为set命令后面有很多参数:
set key value ex 10 nx。这样,设置值和设置过期时间将是一个原子的操作。

3.2 setnx分布式锁的缺点

1、setnx需要手动释放,即使设置了过期时间,这个时间也没有一个标准。如果时间过短,其他线程就可以获取锁,如果时间太长,其他线程就得等。

2、不支持重入。相同的线程重复进行加锁的时候,会被阻塞住。

3、锁误删。这个和redis过期时间有关,例如:a线程加了锁,然后执行逻辑,由于redis的过期时间很短,a还没执行完,锁就过期了。此时b线程加锁成功,开始处理逻辑。那么这个时候a线程执行结束,执行finally的锁释放。这就导致a线程释放了b线程加的锁。

下面给出一个简单的demo:

class LockTest{

	private RedisClient redisClient;

	private static final UUID = UUID.uuid();

	//加锁

	public boolean tryLock(String lockName, long expireTime){

		String value = UUID + System.currentThread(); //防止锁误删

		return redisClient.set(lockName, value, expireTime); //设置redis的key和value以及过期时间 
	}

	//解锁

	public void unLock(String lockName){
		String targetValue = UUID + System.currentThread(); 
		Strign value = redisClient.get(lockName);
		if(targetValue.equals(value)){
			redisClient.delete(locaName);
		}
	}

}

四、Redisson分布式锁

4.1 lua脚本

在上述demo中,尤其是在unlock的方法中,redisClient.get() 和redisClient.delete()方法是隔开的。如果get方法之后,线程被挂起,那么delete()方法还是可能被误删。

根本原因就是读写操作不是原子的。如果get和delete处于原子操作中,那么就不会出现误删的现象。

在redis中,支持lua脚本语言,而lua脚本在redis执行过程是一个原子的,也就意味着,要么整个lua脚本全执行,要么全不执行。

具体的lua脚本的语法这里不做多说明,大家可以自行查询。 

这里将上面的delete方法进行修改:

public void unLock(String lockName){
	String targetValue = UUID + System.currentThread(); 
		
   	redisClient.eval("local id = redis.call('get', KEYS[1]) 
   		local targetId = ARGV[1]if(id == targetId) then 
   		return redis.call('del', KEYS[1])end return 0", 
   		Lists.newArrayList(lockName), Lists.newArrayList(targetValue));
}

4.2 源码解析

4.2.1 redisson的demo

先配置redisson的依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.x.x</version>
</dependency>

创建config对象,配置要连接的redis

Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redisson = Redisson.create(config);

之后开始进行api的使用。

4.2.2 redisson分布式锁

如何实现可重入

当利用redisson进行lock的时候,实际上是可以进行可重入的,那么redisson是如何进行可重入的呢?我们可以先参考一下jvm的锁:

在reentrantlock中,是怎么实现可重入的呢?

1,判断是否加锁

2,判断加锁的线程是否为自己

3,如果是自己,则计数+1,然后放行

那么同样的在redisson中,我们也应该按照这个步骤来,因此我们需要在redis的中记录 线程id和重入的count。因此需要采用map结构来实现。

keymap
lock_namefieldvalue
thread_idcount

那么在上述描述中,需要经过3步骤才能加锁,如果我们利用java来操作redis,很可能导致中途中断的问题,导致数据不一致。因此我们需要使用lua脚本。下面给一个demo:

local lock_name = KEYS[1]; //获取锁的名称
local thread_name = ARGV[1]; //获取线程名称
local expire_time = ARGV[2]; //获取超时时间

//先判断锁是否存在
if(redis.call('exists', lock_name) == 0) then
	//如果不存在当前锁,那么就可以直接加锁,  其中field为当前线程id
	redis.call('hset', lock_name, thread_name, 1);
	//设置过期时间
	redis.call("expire", lock_name, expire_time);
	return 1; //加锁成功
end

//如果有锁,判断当前加锁的线程是否为自己
if(redis.call("hexists", lock_name, thread_name) == 1) then
	//如果是自己,则将value的值+1
	redis.call("hincrby", lock_name, thread_name, '1');
	redis.call("expire", lock_name, expire_time);
	return 1; //重入成功
end;

return 0; //加锁失败

如何实现超时时间过短或过长  (看门狗🐶)

在上面的lua脚本中,实现了加锁的过程。但是超时时间是自定义的。那么这时间过长或者过短都会有影响。我们从多线程的视角去分析。 

1、 存在A.B.C三个线程。这个三个线程都去加锁,也就是执行上面lua脚本。那么其中就会只有一个线程拿到锁,其他两个线程获取不到锁。我们假设A线程获取了锁,BC线程没有获取锁。

2、针对A线程来说,拿到锁了,就直接返回,去处理自己的事情。

3、但是针对BC线程,没有拿到锁,那么BC线程就应该等待,然后过一段时间在重试。那么BC需要等待多长时间呢? 很明显,BC不能一直等待,最好的等待时间就是锁有效期时间。因此上面的lua脚本应该修改,即如果获取锁成功,返回nil。如果获取锁失败,返回ttl。这样bc线程就等待ttl。

4、如果A线程提前释放了锁,也就是锁超时时间设置的比较大,A处理完了,那么释放锁。这次BC还在等待呢,这就浪费了资源。此时redis通过消息队列的方式通知BC线程唤醒,从而继续争夺锁。

5、如果A线程执行比较长,锁过期有设置的比较短。此时BC已经超时唤醒了,但是A还没结束。按理说bc应该继续等待,但是redis中的锁已经过期了,那么BC怎么能继续等待呢? 答案就是不让redis中的锁过期,利用watchdog。

6、在第2步的时候,A线程获取锁之后,在返回之前。会注入一个Timer,这个Timer经过一段时间(设置的超时时间/3,注意,如果手动设置超时时间,看门狗逻辑不生效,只有非手动设置,看门狗逻辑生效,默认超时时间30秒)就会来检查一下锁。如果A的锁存在,说明A线程还没释放锁,那么Timer就会将锁的过期重新设置,然后再继续添加一个新的Timer,重复操作。这样,会一直弥补锁的时间。这样BC就会一直等待。

五、分布式锁失效问题(连锁,multilock,redlock)

分布式锁的失效指的是,当我针对主redis机器进行加锁之后,从redis节点没有同步到锁信息。导致多个线程都加锁成功。

1、当主从发生延迟的时候,就会导致锁失效。

2、当主节点写入成功之后,宕机,也会导致锁失效。

那么redis是如何解决这个问题的呢?坦白讲,redis没有解决【主从之间的原因】导致锁失效的情况。因为根本解决不了,因为网络延迟,主宕机都是人为不可控。

但是redis从另一个角度进行了优化,既然主从存在延迟,和数据不一致问题。那么redis就不进行主从架构了,而是利用多节点node进行处理。这个有点类似于zk。

当client向redis写入锁的时候,会向所有redis节点都写入,如果超过一半的redis写入成功,那么就算加锁成功。但是这就需要部署多台redis,且加锁的时候,需要写入多个redis,这就导致效率问题。那这样一来还不如使用zk。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值