专业的JAVA编程教程与资源

网站首页 > java教程 正文

如何用Redisson框架实现分布式锁?

temp10 2024-11-22 20:55:35 java教程 13 ℃ 0 评论

为什么需要分布式锁

在单机环境下,多个线程同时访问临界资源,需要使用Java并发相关的API或语法实现互斥,比如如ReentrantLock 或 Synchronized等。

但是在多机部署的分布式场景下,这些单机的同步互斥机制就不够用了。这种情况我们可以使用Redis分布式锁的机制,来达到和单机多线程相同的互斥效果。分布式锁也是Java开发面试经常遇到的问题。Redis分布式锁,一般使用Redisson框架来使用。本文介绍如何使用Redisson框架实现Redis分布式锁。

如何用Redisson框架实现分布式锁?

Redisson框架

Redis提供了非常丰富的指令集,包括了200多个命令。但是有些场景下,比较复杂的原子性操作,仅使用原生命令无法完成。Redis 为这样的特殊场景提供了 Lua 脚本支持。Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放。

用户可以向服务器发送 Lua 脚本来执行自定义操作并获得Redis的响应,Redis 服务器将会原子性地执行 Lua 脚本,保证脚本在执行过程中不会被打断。

应用Redisson框架的客户端程序,先把Lua脚本发送到Redis上,然后就可以进行加锁和解锁操作。当应用准备对某个临界资源加锁时,就在Redis中创建一个键值对,表示这个资源已经被某一线程获取了。要解锁时,就从Redis上将键值对删除。服务端加锁和解锁的逻辑都是由Lua脚本来实现的。

Lua脚本

"if(redis.call('exists', KEYS[1])== 0 then " +
    "redis.call('hset', KEYS[1], ARGV[2], 1); " +
    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
     "return nil; " +
"end; " + 
"if (redis.call('hexists', KEYS[1], ARGV[2] == 1) then " + 
    "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + 
    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
    "return nil" +
"end; " + 
"return redis.call('pttl', KEYS[1]);"

在上面字符串拼接的Lua脚本中,KEYS[1]代表的是要加锁的key,ARGV[1]代表的就是锁key的生存时间,默认30秒。ARGV[2]代表的是加锁的客户端的ID。

(1)加锁

假如有一个客户端准备加锁,在客户端的Java代码中:

RLock lock = redisson.getLock("myLock");

锁key的名字就是“myLock”,即对应的是Lua脚本的KEYS[1]。客户端的ID为

8743c9c0-0795-4907-87fd-6c719a6b4586:1

第1行脚本是if判断语句:“exists myLock”,如果要加锁的键myLock不存在的话,那么就可以加锁。

第2行脚本创建key,实现加锁:

hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1

通过这个命令在Redis中设置了一个hash数据结构:

myLock:
{
    "8743c9c0-0795-4907-87fd-6c719a6b4586:1": 1
}

上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。

第3行脚本执行“pexpire myLock 30000”命令,设置myLock这个key的生存时间是30秒。最后返回。

(2)锁互斥

如果在客户端1加锁以后,客户端2也来尝试加锁,执行了同样的脚本,情况会是怎样?

第一个if判断语句,会发现 myLock 这个锁key已经存在了。

接着执行第二个if判断语句(第6行脚本),判断在myLock键的hash数据结构中,是否包含当前客户端2的ID,发现没有,包含的是客户端1的ID。判断失败,直接执行最后一行脚本。

第11行脚本,客户端2获取到 pttl myLock 命令返回的一个数值,这个数值表示myLock这个key的剩余生存时间

接下来 客户端2 进入锁的循环等待中,不停地尝试加锁。

(3)锁延期机制

客户端1 加锁的锁key默认生存时间只有30秒,如果时间超过了30秒,客户端1还想继续持有这把锁,怎么办?此时就需要对锁进行延期。

客户端1一旦加锁成功,就启动一看门狗(watch dog),这是一个后台线程,会每隔10秒检查一下,如果客户端1 还持有锁key,那么就会不断地延长锁key的生存时间。

(4)可重入锁

如果客户端1 再次获取锁,也就是可重入锁场景,这种情况的脚本会如何处理?

首先第一个if判断不成立,“exists myLock”会显示锁key已经存在了。

然后第二个if判断成立,因为myLock的hash数据结构中已经包含了客户端1的ID,此时执行第7行,进入可重入加锁的逻辑:

incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1

通过这个命令,对客户端1的加锁次数加1。

(5)释放锁

如果执行 lock.unlock(),就可以释放分布式锁,每次执行解锁都会对myLock的hash数据结构中的加锁次数减1。

当发现加锁次数变为0,说明这个客户端已经不再持有锁了,就会执行 “del myLock” 命令,从redis里删除这个key。这样,其他的客户端就可以加锁了。

总结

在主从架构的Redis集群中,这个方案存在一点缺陷。考虑这样一种情况,客户端A从master节点获取到锁,在master节点将锁同步到slave节点前,master节点恰好宕机了。

随后slave节点升级为master节点,另外客户端B此时仍然可以获取到锁,互斥失效。如果为了保证一致性,可以考虑使用基于ZooKeeper的分布式锁方案,ZK的方案介绍可以参考前文《Java面试必考问题:如何实现分布式锁》。

参考资料:《石杉的架构笔记--Redis分布式锁的实现原理

我会持续更新关于物联网、云原生以及数字科技方面的文章,用简单的语言描述复杂的技术,也会偶尔发表一下对IT产业的看法,欢迎大家关注,谢谢。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表