图解Redis和Zookeeper分布式锁 redis和zookeeper实现分布式锁
wptr33 2024-12-22 21:13 50 浏览
1.基于Redis实现分布式锁?
??Redis分布式锁原理如上图所示,当有多个Set命令发送到Redis时,Redis会串行处理,最终只有一个Set命令执行成功,从而只有一个线程加锁成功
2:SetNx命令加锁
利用Redis的setNx命令在Redis数据库中创建一个<Key,Value>记录,这条命令只有当Redis中没有这个Key的时候才执行成功,当已经有这个Key的时候会返回失败?
??利用如上的setNx命令便可以简单的实现加锁功能,当多个线程去执行这个加锁命令时,只有一个线程执行成功,然后执行业务逻辑,其他线程加锁失败返回或者重试
3:死锁问题
上面的setNx命令实现了基本的加锁功能,但存在一个致命的问题是,当程序在执行业务代码崩溃时,无法再执行到下面的解锁指令,从而导致出现死锁问题?
??为了解决死锁问题,这里就需要引入过期时间的概念,过期时间是给当前这个key设置一定的存活时间,当存活时间到期后,Redis就会自动删除这个过期的Key,从而使得程序在崩溃时也能到期自动释放锁?
??如上图所示,使用Redis的expire命令来为锁设置过期时间,从而实现到期自动解锁的功能,但这里仍然还存在一个问题就是加锁与给锁设置过期时间这两个操作命令并不是原子命令
考虑下面这种情况:
当程序在加锁完成后,在设置过期时间前崩溃,这时仍然会造成锁无法自动释放,从而产生死锁现象?
4:使用原子命令
针对上面加锁与设置过期时间不是原子命令的问题,Redis为我们提供了一个原子命令如下:?
??通过SetNx(key,value,timeOut)这个结合加锁与设置过期时间的原子命令就能完整的实现基于Redis的分布式锁的加锁步骤
5:解锁原理
解锁原理就是基于Redis的del删除key指令?
6:错误删除锁问题
上面直接删除key来解锁方式会存在一个问题,考虑下面这种情况:
(1)线程1执行业务时间过长导致自己加的锁过期
(2)这时线程2进来加锁成功
(3)然后线程1业务逻辑执行完毕开始执行del key命令
(4)这时就会出现错误删除线程2加的锁
(5)错误删除线程2的锁后,线程3又可以加锁成功,导致有两个线程执行业务代码?
7:加入锁标识
为了解决这种错误删除其他线程的锁的问题,在这里需要对加锁命令进行改造,需要在value字段里加入当前线程的id,在这里可以使用uuid来实现。线程在删除锁的时候,用自己的uuid与Redis中锁的uuid进行比较,如果是自己的锁就进行删除,不是则不删除?
??如上图所示,加锁时在value字段中存入当前线程的id,然后在解锁时通过比较当前的锁是否是自己的来判断是否加锁成功,这样就解决了错误删除别人的锁的问题,但这里同样存在原子命令问题,比较并删除这个操作并不是原子命令,考虑下面这种情况
(1)线程1获取uuid并判断锁是自己的
(2)准备解锁时出现GC或者其他原因导致程序卡顿无法立即执行Del命令,导致线程1的锁过期
(3)线程2就会在这个时候加锁成功
(4)线程1卡顿结束继续执行解锁指令,就会错误删除线程2的锁?
??这个问题出现的根本原因还是比较并删除这两个操作并不是原子命令,只要两个命令被打断就有可能出现并发问题,如果将两个命令变为原子命令就能解决这个问题
8:引入lua脚本实现原子删除操作
lua脚本是一个非常轻量级的脚本语言,Redis底层天生支持lua脚本的执行,一个lua脚本中可以包含多条Redis命令,Redis会将整个lua脚本当作原子操作来执行,从而实现聚合多条Redis指令的原子操作,其原理如下图所示:?
??这里在解锁时,使用lua脚本将比较并删除操作变为原子操作
//lua脚本如下
luaScript = " if redis.call('get',key) == value then
return redis.call('del',key)
else
return 0
end;"
如上面的lua脚本所示,Redis会将整个lua脚本当作一个单独的命令执行,从而实现多个命令的原子操作,避免多线程竞争问题,最终结合lua脚本实现了一个完整的分布式的加锁和解锁过程,伪代码如下:
uuid = getUUID();
//加锁
lockResut = redisClient.setNx(key,uuid,timeOut);
if(!lockResult){
return;
}
try{
//执行业务逻辑
}finally{
//解锁
redisClient.eval(delLuaScript,keys,values)
}
//解锁的lua脚本
delLuaScript = " if redis.call('get',key) == value then
return redis.call('del',key)
else
return 0
end;"
到此,我们最终实现了一个加锁和解锁功能较为完整的redis分布式锁了,当然作为一个锁来说,还有一些其他的功能需要进一步完善,例如考虑锁失效问题,可重入问题等
9:自动续期功能
在执行业务代码时,由于业务执行时间长,最终可能导致在业务执行过程中,自己的锁超时,然后锁自动释放了,在这种情况下第二个线程就会加锁成功,从而导致数据不一致的情况发生,如下图所示:?
??对于上述的这种情况,原因是由于设置的过期时间太短或者业务执行时间太长导致锁过期,但是为了避免死锁问题又必须设置过期时间,那这就需要引入自动续期的功能,即在加锁成功时,开启一个定时任务,自动刷新Redis加锁key的超时时间,从而避免上诉情况发生,如下图所示:?
uuid = getUUID();
//加锁
lockResut = redisClient.setNx(key,uuid,timeOut);
if(!lockResult){
return;
}
//开启一个定时任务
new Scheduler(key,time,uuid,scheduleTime)
try{
//执行业务逻辑
}finally{
//删除锁
redisClient.eval(delLuaScript,keys,values)
//取消定时任务
cancelScheduler(uuid);
}
如上诉代码所示,在加锁成功后可以启动一个定时任务来对锁进行自动续期,定时任务的执行逻辑是:
(1)判断Redis中的锁是否是自己的
(2)如果存在的话就使用expire命令重新设置过期时间
这里由于需要两个Redis的命令,所以也需要使用lua脚本来实现原子操作,代码如下所示:
luaScript = "if redis.call('get',key) == value) then
return redis.call('expire',key,timeOut);
else
return 0;
end;"
10:可重入锁
对于一个功能完整的锁来说,可重入功能是必不可少的特性,所谓的锁可重入就是同一个线程,第一次加锁成功后,在第二次加锁时,无需进行排队等待,只需要判断是否是自己的锁就行了,可以直接再次获取锁来执行业务逻辑,如下图所示:?
实现可重入机制的原理就是在加锁的时候记录加锁次数,在释放锁的时候减少加锁次数,这个加锁的次数记录可以存在Redis中,如下图所示:?
如上图所示,加入可重入功能后,加锁的步骤就变为如下步骤:
(1)判断锁是否存在
(2)判断锁是否是自己的
(3)增加加锁的次数
由于增加次数以及减少次数是多个操作,这里需要再次使用lua脚本来实现,同时由于这里需要在Redis中存入加锁的次数,所以需要使用到Redis中的Map数据结构Map(key,uuid,lockCount),加锁lua脚本如下:
//锁不存在
if (redis.call('exists', key) == 0) then
redis.call('hset', key, uuid, 1);
redis.call('expire', key, time);
return 1;
end;
//锁存在,判断是否是自己的锁
if (redis.call('hexists', key, uuid) == 1) then
redis.call('hincrby', key, uuid, 1);
redis.call('expire', key, uuid);
return 1;
end;
//锁不是自己的,返回加锁失败
return 0;
加入可重入功能后的解锁逻辑就变为:
(1)判断锁是否是自己的
(2)如果是自己的则减少加锁次数,否则返回解锁失败
//判断锁是否是自己的,不是自己的直接返回错误
if (redis.call('hexists', key,uuid) == 0) then
return 0;
end;
//锁是自己的,则对加锁次数-1
local counter = redis.call('hincrby', key, uuid, -1);
if (counter > 0) then
//剩余加锁次数大于0,则不能释放锁,重新设置过期时间
redis.call('expire', key, uuid);
return 1;
else
//等于0,代表可以释放锁了
redis.call('del', key);
return 1;
end;
到此,我们在实现基本的加锁与解锁的逻辑上,又加入了可重入和自动续期的功能,自此一个完整的Redis分布式锁的雏形就实现了,伪代码如下:
uuid = getUUID();
//加锁
lockResut = redisClient.eval(addLockLuaScript,keys,values);
if(!lockResult){
return;
}
//开启一个定时任务
new Scheduler(key,time,uuid,scheduleTime)
try{
//执行业务逻辑
}finally{
//删除锁
redisClient.eval(delLuaScript,keys,values)
//取消定时任务
cancelScheduler(uuid);
}
11:Zookeeper实现分布式锁
Zookeeper是一个分布式协调服务,分布式协调主要是来解决分布式系统中多个应用之间的数据一致性,Zookeeper内部的数据存储方式类似于文件目录形式的存储结构,它的内存结果如下图所示:?
12:Zookeeper加锁原理
在Zookeeper中的指定路径下创建节点,然后客户端根据当前路径下的节点状态来判断是否加锁成功,如下图一种情况为例,线程1创建节点成功后,线程2再去创建节点就会创建失败?
13:Zookeeper节点类型
持久节点:在Zookeeper中创建后会进行持久储存,直到客户端主动删除
临时节点:以客户端会话Session维度创建节点,一旦客户端会话断开,节点就会自动删除
临时/持久顺序节点:在同一个路径下创建的节点会对每个节点按创建先后顺序编号?
zookeeper.exists("/watchpath",new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("进入监听器");
System.out.println("监听路径Path:"+event.getPath());
System.out.println("监听事件类型EventType:"+event.getType());
}
});
14:利用临时顺序节点和监听机制来实现分布式锁
实现分布式锁的方式有多种,我们可以使用临时节点和顺序节点这种方案来实现分布式锁:
1:使用临时节点可以在客户端程序崩溃时自动释放锁,避免死锁问题
2:使用顺序节点的好处是,可以利用锁释放的事件监听机制,来实现阻塞监听式的分布式锁
下面将基于这两个特性来实现分布式锁
15:加锁原理
1:首先在Zookeeper上创建临时顺序节点Node01、Node02等
2:第二步客户端拿到加锁路径下所有创建的节点
3:判断自己的序号是否最小,如果最小的话,代表加锁成功,如果不是最小的话,就对前一个节点创建监听器
4:如果前一个节点删除,监听器就会通知客户端来准备重新获取锁
加锁原理和代码入下图所示:?
//加锁路径
String lockPath;
//用来阻塞线程
CountDownLatch cc = new CountDownLatch(1);
//创建锁节点的路径
Sting LOCK_ROOT_PATH = "/locks"
//先创建锁
public void createLock(){
//lockPath = /locks/lock_01
lockPath = zkClient.create(LOCK_ROOT_PATH+"/lock_", CreateMode.EPHEMERAL_SEQUENTIAL);
}
//获取锁
public boolean acquireLock(){
//获取当前加锁路径下所有的节点
allLocks = zkClient.getChildren("/locks");
//按节点顺序大小排序
Collections.sort(allLocks);
//判断自己是否是第一个节点
int index = allLocks.indexOf(lockPath.substring(LOCK_ROOT_PATH.length() + 1));
//如果是第一个节点,则加锁成功
if (index == 0) {
System.out.println(Thread.currentThread().getName() + "获得锁成功, lockPath: " + lockPath);
return true;
} else {
//不是序号最小的节点,则监听前一个节点
String preLock = allLocks.get(index - 1);
//创建监听器
Stat status = zkClient.exists(LOCK_ROOT_PATH + "/" + preLockPath, watcher);
// 前一个节点不存在了,则重新获取锁
if (status == null) {
return acquireLock();
} else {
//阻塞当前进程,直到前一个节点释放锁
System.out.println(" 等待前一个节点锁释放,prelocakPath:"+preLockPath);
//唤醒当前线程,继续尝试获取锁
cc.await();
return acquireLock();
}
}
}
private Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
//监听到前一个节点释放锁,唤醒当前线程
cc.countDown();
}
}
16:可重入锁实现
Zookeeper实现可重入分布式锁的机制是在本地维护一个Map记录,因为如果在Zookeeper节点维护数据的话,Zookeeper的写操作是很慢,集群内部需要进行投票同步数据,所以在本地维护一个Map记录来记录当前加锁的次数和加锁状态,在释放锁的时候减少加锁的次数,原理如下图所示:?
//利用Map记录线程持有的锁
ConcurrentMap<Thread, LockData> lockMap = Maps.newConcurrentMap();
public Boolean lock(){
Thread currentThread = Thread.currentThread();
LockData lockData = lockMap.get(currentThread);
//LockData不为空则说明已经有锁
if (lockData != null)
{
//加锁次数加一
lockData.lockCount.increment();
return true;
}
//没有锁则尝试获取锁
Boolean lockResult = acquireLock();
//获取到锁
if (lockResult)
{
LockData newLockData = new LockData(currentThread,1);
lockMap.put(currentThread, newLockData);
return true;
}
//获取锁失败
return false;
}
17:解锁原理
解锁的步骤如下:
(1)判断锁是不是自己的
(2)如果是则减少加锁次数
(3)如果加锁次数等于0,则释放锁,删除掉创建的临时节点,下一个监听这个节点的客户端会感知到节点删除事件,从而重新去获取锁?
public Boolean releaseLock(){
LockData lockData = lockMap.get(currentThread);
//没有锁
if(lockData == null){
return false;
}
//有锁则加锁次数减一
lockCount = lockData.lockCount.decrement();
if(lockCount > 0){
return true;
}
//加锁次数为0
try{
//删除节点
zkClient.delete(lockPath);
//断开连接
zkClient.close();
finally{
//删除加锁记录
lockMap.remove(currentThread);
}
return true;
}
18:Redis和Zookeeper锁对比
Redis | Zookeeper | |
读性能 | 基于内存 | 基于内存 |
加锁性能 | 直接写内存加锁 | Master节点创建好后与其他Follower节点进行同步,半数成功后才能返回写入成功 |
数据一致性 | AP架构Redis集群之间的数据同步是存在一定的延迟的,当主节点宕机后,数据如果还没有同步到从节点上,就会导致分布式锁失效,会造成数据的不一致 | CP架构当Leader节点宕机后,会进行集群重新选举,如果此时只有一部分节点收到了数据的话,会在集群内进行数据同步,保证集群数据的一致性 |
19:总结
使用Redis还是Zookeeper来实现分布式锁,最终还是要基于业务来决定,可以参考以下两种情况:
(1)如果业务并发量很大,Redis分布式锁高效的读写性能更能支持高并发
(2)如果业务要求锁的强一致性,那么使用Zookeeper可能是更好的选择
相关推荐
- 开发者必看的八大Material Design开源项目
-
MaterialDesign是介于拟物和扁平之间的一种设计风格,自从它发布以来,便引起了很多开发者的关注,在这里小编介绍在Android开发者当中里最受青睐的八个MaterialDesign开源项...
- 另类插这么可爱,一定是…(另类t恤)
-
IT之家(www.ithome.com):另类插图:这么可爱,一定是…OSXMavericks和Yosemite打破了苹果对Mac操作系统传统的命名方式,使用加州的某些标志性景点来替换猫...
- Android常用ADB命令(安卓adb工具是什么)
-
杀死应用①根据包名获取APP的PIDadbshellps|grep应用包名②执行kill命令...
- 微软Mac版PowerPoint测试Reading Order Pane功能
-
IT之家5月20日消息,微软公司昨日(5月19日)发布博文,邀请Microsoft365Insiders成员,测试macOS新版PowerPoint演示文稿应用,重点引入...
- Visual Studio跨平台开发实战(4):Xamarin Android控制项介绍
-
前言不同于iOS,Xamarin在VisualStudio中针对Android,可以直接设计使用者界面.在本篇教学文章中,笔者会针对Android的专案目录结构以及基本控制项进行介绍,包...
- 用云存储30分钟快速搭建APP,你信吗?
-
背景不管你承认与否,移动互联的时代已经到来,这是一个移动互联的时代,手机已经是当今世界上引领潮流的趋势,大型的全球化企业和中小企业都把APP程序开发纳入到他们的企业发展策略当中。但随着手机APP上传的...
- 谷歌P图神器来了!不用学不用教,输入一句话,分分钟给结果
-
Pine发自凹非寺量子位|公众号QbitAI当你拍照片时,“模特不好好配合”怎么办?...
- iOS文本编辑控件UITextField和UITextVie
-
记录一个菜鸟的IOS学习之旅,如能帮助正在学习的你,亦枫不胜荣幸;如路过的大神如指教几句,亦枫感激涕淋!细心的朋友可能已经注意到了,IOS学习之旅系列教程在本篇公众号的文章中,封面已经换成美女图片了,...
- Android入门图文教程集锦(android 入门教程)
-
Android入门视频教程集锦AndroidStudio错误gradientandroid:endXattributenotfound...
- 如何使用Android自定义复合视图(如何使用android自定义复合视图)
-
在最近的一个客户应用中,我遇到了一个需求,根据选定的值来生成指定数量的编辑框字段,这样用户可以输入人物信息。最初我的想法是把这些逻辑放到Fragment中,只是根据选中值的变化来向线性布局容器中增加编...
- 原生安卓开发app的框架frida常用关键代码定位
-
前言有时候可能会对APP进行字符串加密等操作,这样的话你的变量名等一些都被混淆了,看代码就可能无从下手...
- 教程10 | 三分钟搞定一个智能输入法程序
-
一案例描述1、考核知识点网格布局线性布局样式和主题Toast2、练习目标掌握网格布局的使用掌握Toast的使用掌握线性布局的使用...
- (Android 8.1) 功能与新特性(android的功能)
-
和你一起终身学习,这里是程序员AndroidAndroid8.1(API级别27)为用户和开发人员引入了各种新特性和功能。本文档重点介绍了开发人员的新功能。通过本章阅读,您将获取到以下内容:Andr...
- 怎样设置EditText内部文字被锁定不可删除和修改
-
在做项目的时候,我曾经遇到过这样的要求,就是跟百度贴吧客户端上的一样,在回复帖子的时候,在EditText中显示回复人的名字,而且这个名字不可以修改和删除,说白了就是不可操作,只能在后面输入内容。在E...
- 如何阻止 Android 活动启动时 EditText 获得焦点
-
技术背景在Android开发中,当活动启动时,EditText有时会自动获得焦点并弹出虚拟键盘,这可能不是用户期望的行为。为了提升用户体验,我们需要阻止...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
git pull命令使用实例 git pull--rebase
-
面试官:git pull是哪两个指令的组合?
-
git 执行pull错误如何撤销 git pull fail
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
-
- 开发者必看的八大Material Design开源项目
- 另类插这么可爱,一定是…(另类t恤)
- Android常用ADB命令(安卓adb工具是什么)
- 微软Mac版PowerPoint测试Reading Order Pane功能
- Visual Studio跨平台开发实战(4):Xamarin Android控制项介绍
- 用云存储30分钟快速搭建APP,你信吗?
- 谷歌P图神器来了!不用学不用教,输入一句话,分分钟给结果
- iOS文本编辑控件UITextField和UITextVie
- Android入门图文教程集锦(android 入门教程)
- 如何使用Android自定义复合视图(如何使用android自定义复合视图)
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)