当前位置: 首页 > 乐活 > 内容页

通讯!图解Redis和Zookeeper分布式锁

2023-05-31 11:24:02 来源:博客园
1.基于Redis实现分布式锁

Redis分布式锁原理如上图所示,当有多个Set命令发送到Redis时,Redis会串行处理,最终只有一个Set命令执行成功,从而只有一个线程加锁成功

2:SetNx命令加锁

利用_Redis的setNx命令在Redis数据库中创建一个记录,这条命令只有当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;//锁是自己的,则对加锁次数-1local 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 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可能是更好的选择

作者:京东物流 钟磊来源:京东云开发者社区

标签:

潮流

更多

猜你喜欢

更多
《我和我们在一起》在哪个平台播出?《我和我们在一起》林皓结局是什么? 怎么禁止电脑安装游戏软件 相关内容如下-消息 《十五年等待候鸟》原著小说是什么?《十五年等待候鸟》结局是什么? 《玉昭令》展颜身世是什么?《玉昭令》温孤的结局是什么? 《为了你我愿意热爱整个世界》讲的什么内容?《为了你我愿意热爱整个世界》结局是什么? 【环球聚看点】中央气象台:川渝陕等地较强降雨将持续多日 华南及四川南部等地高热不断 5月30日信用债曲线收益率小幅波动 《最后的赢家》是什么综艺?《最后的赢家》幕后的x是谁? 《芳华》的导演是谁?《芳华》萧穗子结局是什么? 今日热闻!电费户号查询95598 电费户号查询 《夏天19岁的肖像》是什么影片?《夏天19岁的肖像》夏颖颖扮演者是谁? 世界微速讯:5月31日 10:45分 新余国科(300722)股价快速拉升 《青春创世纪》根据什么改编?《青春创世纪》结局是什么? “六一”儿童节的N种打开方式 《月半爱丽丝》主要讲述了什么?《月半爱丽丝》黄可是谁演的? 当前要闻:ip67防水等级标准_ip67级防水什么意思 《检察风云》讲的什么内容?《检察风云》陈鑫结局是什么? 招商蛇口增发获批 房企股权融资的“第三支箭”首单即将落地_全球看点 《春日暖阳》丁沫沫最后和谁在一起了?《春日暖阳》孙怡演的角色是什么? 《指尖少年》讲的什么?《指尖少年》淼淼扮演者是谁? 3人→6人 空间站如何快速调整保障航天员在轨生活需求? 10名!济宁干部政德教育学院引进急需紧缺人才 热门:转换插头选购技巧,转换器可以当插座用吗 《情满九道弯》杨树茂和谁在一起了?《情满九道弯》赵亚静结局怎么了? 【机构调研记录】国金基金调研五洲特纸、中煤能源-每日视讯 《橙红年代》主要讲了什么?《橙红年代》的大结局是什么? 新增21种普通本科专业 2023年高考招生有变化|全球热点 5月30日基金净值:易方达科技创新混合最新净值2.7407,涨1.53% 《可惜不是你》原著小说是什么?《可惜不是你》叶子最后和谁在一起? 【研报推荐】东方证券:5月首二套房贷利率差进一步扩大 集中供地热度持续分化 “三品一械”广告审查管理办法征求意见 《风暴舞》牟川是好人吗?《风暴舞》中阿尔法是谁? 全球热讯:不锈钢材质,苹果早期 Apple Watch 原型曝光 《孤独的野兽》是什么剧情?《孤独的野兽》顾南结局怎么了? 《温德瑞拉日记》讲了什么内容?《温德瑞拉日记》温媃和温欣是什么关系? 美国众议院民主党领袖:将尽力确保债务上限法案通过-热讯 《恩爱两不疑》是什么改编的?《恩爱两不疑》结局是什么? 消息!会议接待果盘摆几样水果 《做家务的男人第二季》的嘉宾有谁?《做家务的男人第三季》观察员是谁? 科技早报|英伟达市值破1万亿美元 苹果618将在天猫开启全球首次直播 快播 四川重庆陕西等地有较强降雨 华南江南等地有高温天气 《这!就是街舞第三季》的队长是哪四位?《这!就是街舞第三季》冠军是谁? 《漫游记》是什么节目?《漫游记》郭碧婷是哪一期? 5月30日基金净值:南方医药保健灵活配置混合A最新净值2.472,跌0.2%|焦点速看 央行适度加大逆回购操作规模 资金面平稳跨月无虞 整治涉企违规收费行动启动|实时焦点 智利:预计2030年代后半段锂将出现供应过剩 需求到2035年平均年增逾16%|环球短讯 算力需求迸发,AI产业链显现新投资机会 世界最资讯 泽库县气象台发布冰雹橙色预警信号【2023-05-30】|当前热闻 环球新消息丨北京科协App全新上线——打造科技工作者专属的服务平台 “占芭花开”文化之夜在昆明举行 热点评!营造适老化消费环境(市场漫步) 全球播报:朝鲜宣布将发射卫星,日韩紧盯! 山西前4月抽采煤层气34.8亿立方米 关注:普联软件(300996):5月30日北向资金增持16.5万股 全球视讯!剁椒鱼头怎么腌制? 《七龙珠:破界斗士》第三季新增基纽特种部队 全球今日报丨天猫店(天猫旗舰店是什么意思) 媒体:快播宣告破产 微头条丨引入“离线权”,让劳动者不再困于“网”中 从金晨到景甜再到张蕊,张继科是眼光变了,还是光环褪了 每日播报 债务缠身 金科股份新增重大诉讼成被告 mid函数提取出生年月_mid是什么函数 世界热议:山东省艺术类本科院校排名 山东省艺术类院校排名 世界速看:最新!漳州市发布混凝土预制构件市场参考价!助力装配式建筑发展 今日诫勉谈话记录放入档案吗_诫勉谈话放个人档案吗|快讯 最新消息:黄道吉日忌诸事不宜什么意思_忌诸事不宜什么意思 时间都去哪儿了表达了什么意思_时间都去哪儿了 荷兰公开赛第三轮拉拉扎巴尔领先 丁文一T48 短讯!用手机优酷下载视频怎么导出_手机优酷下载的视频怎么导出来 焦点信息:仅需2899元!JBL KTV 350让您在家随心所欲K歌! 上海22号线地铁线路图最新消息_上海22号线|天天时快讯 2023就是宿迁市泗洪县面向社会招聘村卫生室工作人员80人公告 【世界独家】国家卫健委:要求各地高度重视流感、诺如病毒感染救治。面对来势汹汹的诺如病毒,你应该了解 每日看点!板六_关于板六概略 全球热门:曲水纹(关于曲水纹介绍) 当前看点!净水器会不会把水中矿物质去掉_净水器会过滤掉矿物质吗 每日速看!牛膝草图(牛膝草) 打造“醇甜香山地烟叶”特色IP,桑植烟叶跑出高质量发展“加速度”|天天热议 张予曦斗罗大陆2路透,白色长裙美若仙子下凡,吴宣仪造型被完爆_世界微头条 【原】为什么大力推崇医养结合的智慧养老?|环球新要闻 天天简讯:三大专项行动!赣州公安这样干…… 去英国留学读研需要哪些条件呢-天天热门 种植烟草无益农民增收,世卫组织呼吁:种粮食不种烟草 【丝路话语】要求员工签自愿放弃社保承诺书是自欺欺人 焦点热议 谢依霖发文感谢杨幂帮她找工作:真的好感人,我爱你 周宏伟 pico个人资料(360周宏伟) 剑道独尊小说在线阅读_剑道独尊小说_快报 2023新一线城市名单:成都、重庆、杭州、武汉等15座城市入选 天天最新:柯尔鸭和普通鸭的区别是什么(柯尔鸭幼苗和普通鸭幼苗的区别) 【天天时快讯】美国枪击事件接连发生 枪支暴力难消 微信刷掌支付落户深圳大学 今日快讯 世界热讯:“中植系”拟退出宇顺电子,上海一实业公司老板接盘 常按这个穴位,可以防治冠心病! 调戏女佣小游戏网址(调教女佣小游戏攻略)-消息 日本歌曲排行榜前十名_日本歌 世界焦点!巧克力银行韩剧网资源_巧克力银行 丽尚国潮(600738.SH):部分股东解除一致行动关系_环球热点评 环球短讯!Mission E领衔 2018日内瓦车展新能源汽车汇总 阳光城被叠加实施其他风险警示 已连续十二交易日股价低于1元 全球时讯

时尚

  1. 又一只宣布:清盘!

    又一只宣布:清盘!

  2. 【环球聚看点】海正生材:5月30日融资买入163.84万元,融资融券余额1959.15万元

    【环球聚看点】海正生材:5月30日融资买入163.84万元,融资融券余额1959.15万元

  3. 5月31日华北苯酐市场行情走势下滑 动态焦点

    5月31日华北苯酐市场行情走势下滑 动态焦点

  4. 粤港澳大湾区轨道交通加速融合-全球热资讯

    粤港澳大湾区轨道交通加速融合-全球热资讯

  5. 2023年5月钢铁PMI显示: 供需两端继续收缩 钢市运行较为低迷 环球微头条

    2023年5月钢铁PMI显示: 供需两端继续收缩 钢市运行较为低迷 环球微头条

  6. 5月31日生意社PP(拉丝)基准价为7100.00元/吨

    5月31日生意社PP(拉丝)基准价为7100.00元/吨

  7. 【全球独家】讲武谈兵|大举进军东南亚,兰卡威防务展上的日本军工企业

    【全球独家】讲武谈兵|大举进军东南亚,兰卡威防务展上的日本军工企业

  8. 环球信息:台风“玛娃”确定不登陆我国,却成绝顶坏台风!分析:带两种极端天气,蓝色多雨区确定覆盖西南……

    环球信息:台风“玛娃”确定不登陆我国,却成绝顶坏台风!分析:带两种极端天气,蓝色多雨区确定覆盖西南……

  9. “舌尖”带动“脚尖” 小吃消费释放更大潜力-今日视点

    “舌尖”带动“脚尖” 小吃消费释放更大潜力-今日视点

  10. 来凤县气象台发布雷电黄色预警【III级/较重】【2023-05-30】 当前滚动

    来凤县气象台发布雷电黄色预警【III级/较重】【2023-05-30】 当前滚动