分布式锁:单节点redis实现

一、线程之间的并发

如果我们想要两个线程同步执行,那么只需要加上sychronzied或者使用Reetrantlock类保证同一个jvm进程内多个线程同步执行。

那么如果这个系统是分布式的,例如:两个服务器上的线程想要并发执行该怎么办呢?

而我们想要实现的是这个亚子滴:

实际上最主要的就是对这个锁的操作,获得锁和释放锁。

二、redis实现

1、获得锁

set(key,线程的id,30,NX)

通过向redis中放入一个数据来实现锁,这个set方法:如果当前redis中没有这个key,返回1,如果有返回0。这样就可以通过。set方法来控制不同线程之间的并发,只有执行这个方法返回1的时候才算获得锁。

后面两个参数是设置过期时间。如果不设置会怎么样?例如线程A获得锁,然后程序发生故障死了,那么这个锁就一直不会被释放,其他服务器的线程也就获得不了这个锁。所以呢,设置过期时间的意义在于:当到达过期时间后,那么这个数据值自动失效(当然redis要设置好内存淘汰机制),也就是释放了锁,并不影响其他线程set后可以获得这个锁。

有人说可不可以这样实现呢:(以下都是伪代码)

setnx(key,1)
expire(key,30)

利用redis的这两个命令来实现:答案是NO

我们必须要保证加锁的过程是一个原子操作,例如,线程A的客户端在执行完setnx(key,1)之后就死掉了,那么这个锁依然不会被其他线程获取了。

扫描二维码关注公众号,回复: 10419420 查看本文章

2、释放锁

在线程A执行完同步代码块后,就释放锁执行

del(key)

这个方法行不行呢?NO,NO,NO。绝对不可以

考虑这样一种情况:如果当前设置的过期时间是30秒,那么线程A获得锁之后在执行同步代码块中的内容,过了30秒,redis一看到时间了,就把这个key删除了,锁释放了,这个时候呢A还没执行完呢,所以线程Bset了一下,发现可以拿到锁,线程B在愉快的玩耍,但是呢线程A这个时候执行完了(redis没法通知它锁释放了),这哥们以为自己还有锁呢,那就释放一下吧,这个时候,线程B的锁也被拿掉了,关键是这哥们还不知道自己锁被拿掉了。这样就出现了并发的问题。就像被戴绿帽子了还不知道一样。

我们可以把set中存储的value设置成线程的id(个人想法是:服务器唯一标识+线程的id = value)

加锁:
String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)

解锁:
if(threadId .equals(redisClient.get(key))){
del(key)
}

但是呢又出现一个问题,释放锁也不是原子操作。我靠肯定会出问题啊。

我们来看这样一个情景:

还是上个例子的情况,线程A在判断完是不是当前线程id后,那么一瞬间key到期了。线程B拿到了锁,而A接着执行依然出现了上述的情况。

我们应该替换掉del,改用lua脚本来实现,就扣1了。

String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] 
then return redis.call('del', KEYS[1]) else return 0 end";

redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));

有小伙伴又要说了,那么线程A先到期,还在执行,线程B获得锁也在执行,这也不符合规矩啊。我们下面来解决这个问题。

我们可以为线程设置一个带刀侍卫,就像JR守护詹姆斯一样(几年前的事了)。来一个御前侍卫,在java里就是守护线程!当然你必须要知道啥是守护线程。

这个守护线程只有一个作用,

当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。

当线程A执行完之后,这个守护线程也就结束了。

发布了134 篇原创文章 · 获赞 91 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/weixin_44588495/article/details/104563365
今日推荐