一、redis介绍 Redis应该是目前最受欢迎的高性能的缓存数据库了,在五一期间看到一则Redis7。0发布的消息后,回想起多年前学习黄健宏老师《Redis从入门到精通》2。x的月伴时光,不由得感慨Reids发展之迅速。搜集了一下3。0及之后各版本的知名特性,整理出来方便读者朋友们有个简单了解(感兴趣的朋友还需自行深入研究),情况大致如下:3。0开始支持cluster集群模式4。0开发的lazyfree和PSYNC2解决了Redis长久的大key删除阻塞问题及同步中断无法续传的问题5。0新增了stream数据结构使Redis具备功能完整的轻量级消息队列能力6。0更是发布了诸多企业级特性如threadedio、TLS和ACL等,大幅提升了Redis的性能和安全性7。0Function彻底解决了过去Lua脚本同步丢失的问题;MultiPartAOF增强了Redis的数据持久化的可靠性1。1特性介绍 为满足本篇目标所需,这里着重介绍以下几个关键特性:数据组织:Redis中支持多种数据结构,将他们灵活组合搭配即可满足分布式锁在不同场景下的功能需求:Jedis和Lettuce这类框架中常使用String来做简易的锁信息存储Redisson中使用Hash结构来存储更多维度的锁信息,如:业务名称作为key,uuid线程id作为field,加锁次数作为valueRedisson中在公平锁的场景下引入List和ZSet,List类型用于线程排队,Zset类型存放等待线程的顺序,分数score是等待线程的超时时间戳。 Redis的数据结构(来自网络)集群模式:Redis采用集群模式分片存储数据,整个集群拥有固定的2的32次方个槽位,数据被分配到这些槽位中,每个实例只分管一部分槽位,而非如etcd、ZK这种每个实例中的数据都一致;集群模式提供的是数据规模扩大后的横向AP能力,应对单节点的风险需再加上主从模式,但当某个master节点挂之后,slave节点可能还未同步到全部数据,会导致数据丢失;一致性保障能力偏弱 Redis的集群模式(来自网络)顺序变更:一种简单的抢锁逻辑是判断key是否已存在,Redis中没有给变更操作附加顺序信息(如etcd中的Revision),但服务端以串行方式处理数据的变更,那就可以结合其他数据结构来记录请求顺序信息,如公平锁的实现也会依赖其他数据结构存储信息,用于判断锁状态;但当用到的数据类型和指令变多后,由于是非原子性操作,自然就会遇到结果与预期不一致这类问题,Redis提供的lua脚本机制可用于解决此类问题,用户在客户端编排自定义脚本逻辑:可用多个指令操控多个数据,然后将脚本发送给服务端,服务端执行lua脚本,并保障一个lua脚本内的所有操作是原子性的 Redislua脚本的工作机制(来自网络)TTL机制:TTL(TimeToLive)机制是给单个key设置存活时间,超过时间后Redis自动删除这个key1。2特性总结 Redis的分布式锁正是基于以上特性来实现的,简单来说是:TTL机制:用于支撑异常情况下的锁自动释放的能力顺序变更:用于支撑获取锁和排队等待的能力集群主从模式:用于支撑锁服务的高可用 Redis没有提供对分布式锁亲和的监听机制,需要客户端主动轮询感知数据变更。二。加锁解锁的流程描述 使用Jedis指令实现分布式锁的核心流程如下图所示: 准备客户端、key和value若key不存在,指定过期时间成功写入KeyValue则抢锁成功,并定时推后key的过期时间若key已存在,则采用重试策略间歇性抢锁。解锁时,删除key并撤销推后key过期时间的逻辑 其中第2和第4是核心环节,有几个版本的演进很有趣味:插入key和设置过期时间并非原子操作:setnxexpire加锁和设置过期是两个分开的独立操作;若发生异常,导致设置过期操作未执行,则此锁就成了永恒锁,其他客户端就再也抢不到了以原子性操作完成插入key和设置过期时间:使用set的扩展指令,如下:SETkeyvalue〔EXseconds〕〔PXmilliseconds〕〔NXXX〕复制代码NX:当key不存在时,才插入KeyXX:当插入key时,指定值为固定的lockValueEXsecond:设置key的过期时间单位秒(PXEX二选一)PXmillisecond:设置键的过期时间单位毫秒(PXEX二选一)if(jedis。set(key,lockValue,NX,EX,100)1){加锁成功try{dowork执行业务这里缺点什么?}catch(Exceptione){。。。}finally{jedis。del(key);释放锁,这里可能误删其他client的锁key}}复制代码引入lockValue的随机值校验,避免误释放其它客户端的锁,场景如下:client1加锁成功,key10s后过期,完成逻辑后,删除key之前,因GC导致持锁超过10s,Redis自动删除了key,之后其他客户端可以抢锁假如是client2接下来成功抢锁,开始处理持锁后的逻辑。而此时client1GC结束了会继续执行删除key的操作,但此时释放的其实是client2的key 解决办法是:加锁时指定的lockValue为随机值,每次加锁时的值都是唯一的,释放锁时若lockValue与加锁时的值一致才可释放,否则什么都不做,逻辑如下:if(jedis。set(key,randomLockValue,NX,EX,100)1){加锁try{dosomething业务处理}catch(){}finally{判断是不是当前线程加的锁,是才释放但判断和释放锁两个操作不是原子性的if(randomLockValue。equals(jedis。get(key))){jedis。del(key);释放锁}}}复制代码 以上代码遗留的问题是判断randomlockValue和释放锁两个操作不是原子性的。引入lua脚本,保障判断randomlockValue和删除key这两个操作的原子性,逻辑如下:Stringscriptifredis。call(get,KEYS〔1〕)ARGV〔1〕thenreturnredis。call(del,KEYS〔1〕)elsereturn0end;Objectresultjedis。eval(script,Collections。singletonList(key),Collections。singletonList(randomLockValue));if(1。equals(result。toString())){returntrue;}复制代码 至此依然存在的一个问题是:若持锁后,业务逻辑执行耗时超过了key的过期时间,则锁Key会被Reids主动删除。引入watchDog定时推后key的过期时间,避免业务未执行完时,key过期被Redis删除。if(jedis。set(key,randomLockValue,NX,EX,100)1){加锁成功try{dowork执行业务watchDog定时延后Key的过期时间}catch(Exceptione){。。。}finally{Stringscriptifredis。call(get,KEYS〔1〕)ARGV〔1〕thenreturnredis。call(del,KEYS〔1〕)elsereturn0end;try{Objectresultjedis。eval(script,Collections。singletonList(key),Collections。singletonList(randomLockValue));if(1。equals(result。toString())){returntrue;}returnfalse;}catch(Exceptione){。。。}}}复制代码三。Jedis分布式锁的能力 可能读者是单篇阅读,这里引入第一篇《分布式锁上初探》中的一些内容,一个分布式锁应具备这样一些功能特点:互斥性:在同一时刻,只有一个客户端能持有锁安全性:避免死锁,如果某个客户端获得锁之后处理时间超过最大约定时间,或者持锁期间发生了故障导致无法主动释放锁,其持有的锁也能够被其他机制正确释放,并保证后续其它客户端也能加锁,整个处理流程继续正常执行可用性:也被称作容错性,分布式锁需要有高可用能力,避免单点故障,当提供锁的服务节点故障(宕机)时不影响服务运行,这里有两种模式:一种是分布式锁服务自身具备集群模式,遇到故障能自动切换恢复工作;另一种是客户端向多个独立的锁服务发起请求,当某个锁服务故障时仍然可以从其他锁服务读取到锁信息(Redlock)可重入性:对同一个锁,加锁和解锁必须是同一个线程程,即不能把其他线程持有的锁给释放了高效灵活:加锁、解锁的速度要快;支持阻塞和非阻塞;支持公平锁和非公平锁 基于上文对Jedis分布式锁的介绍,这里简单总结一下Jedis的能力矩阵,ZK请看《分布式锁中基于Zookeeper的实现》,etcd请看《分布式锁中基于etcd的实现很优雅》,表格中标题使用Redis简单锁,主要是跟RedLock做区分,这种简单锁使用Jedis、Lettuce、Redisson都能实现,任何一把锁的信息只保存在一个Redismaster实例中,而RedLock是Redisson提供的高阶分布式锁,它需要客户端同时跟多个Redismaster实例协作才能完成,即一把锁的信息同时存在于多个master实例中。它的情况会在后续文章中补充(感兴趣的读者可以关注本号【架构染色】,文章完成时会主动推送给你) 能力 ZK etcd Redis简单锁 Redlock MySql 互斥 是 是 是 安全 链接异常时,session丢失自动释放锁 基于租约,超时自动释放锁 基于TTL,超时自动释放锁 可用性 相对可用性还好 好 好 可重入 服务端非可重入,本地线程可重入 服务端非可重入,Resission本地线程可重入 服务端非可重入,本地线程可重入需自研 加解锁速度 速度不算快 速度快,GRPC协议优势以及服务端能力的优势 速度快 阻塞非阻塞 客户端两种能力都提供 jetcdcore中,阻塞非阻塞由Futureget支撑 Jedis非阻塞,Redission提供阻塞能力 公平非公平 公平锁 公平锁 非公平锁,Redission提供公平锁 可续期 天然支持 天然支持 Jedis需自研watchDog,Redission自带 其他因素 技术栈偏老,性能不佳 多数公司不熟悉 容易受业务缓存操作干扰 四、Jedis库实现分布式锁 Jedis是Redis官方推出的用于通过Java连接Redis客户端的一个工具包,提供了Redis的各种命令支持。4。1pom依赖dependencygroupIdredis。clientsgroupIdjedisartifactIdversion4。3。0versiondependency复制代码4。2相关的API介绍使用SET的扩展指令加锁(SETkeyvalue〔EXseconds〕〔pxmilliseconds〕〔NXXX〕)SetParamsparamsSetParams。setParams()。nx()。ex(lockState。getLeaseTTL());Stringresultclient。set(lockState。getLockKey(),lockState。getLockValue(),params);复制代码使用lua解锁Stringscriptifredis。call(get,KEYS〔1〕)ARGV〔1〕thenreturnredis。call(del,KEYS〔1〕)elsereturn0end;Objectresultclient。eval(script,1,lockState。getLockKey(),lockState。getLockValue());复制代码4。3分布式锁示例锁的封装packagecom。rock。dlock。jedis;importcom。rock。dlock。common。DtLockException;importcom。rock。dlock。common。KeepAliveAction;importcom。rock。dlock。common。KeepAliveTask;importorg。slf4j。Logger;importorg。slf4j。LoggerFactory;importredis。clients。jedis。JedisPooled;importredis。clients。jedis。params。SetParams;importjava。net。SocketTimeoutException;importjava。util。concurrent。TimeUnit;authorzsdate202211134:44PMpublicclassDemoJedisLock{privatefinalstaticLoggerlogLoggerFactory。getLogger(DemoJedisLock。class);privateJedisPooledclient;privateLockStatelockState;privateKeepAliveTaskkeepAliveTask;privateintsleepMillisecond;privatefinalstaticStringRESULTOKOK;privatestaticfinalLongUNLOCKSUCCESS1L;classLockState{privateStringlockKey;privateStringlockValue;privateStringerrorMsg;privateintleaseTTL;privatelongleaseId;privatebooleanlockSuccess;publicLockState(StringlockKey,intleaseTTL){this。lockKeylockKey;this。leaseTTLleaseTTL;}publicLockState(StringlockKey,Stringvalue,intleaseTTL){this。lockKeylockKey;this。lockValuevalue;this。leaseTTLleaseTTL;}publicStringgetLockKey(){returnlockKey;}publicvoidsetLockKey(StringlockKey){this。lockKeylockKey;}publicStringgetLockValue(){returnlockValue;}publicvoidsetLockValue(StringlockValue){this。lockValuelockValue;}publicStringgetErrorMsg(){returnerrorMsg;}publicvoidsetErrorMsg(StringerrorMsg){this。errorMsgerrorMsg;}publiclonggetLeaseId(){returnleaseId;}publicvoidsetLeaseId(longleaseId){this。leaseIdleaseId;}publicbooleanisLockSuccess(){returnlockSuccess;}publicvoidsetLockSuccess(booleanlockSuccess){this。lockSuccesslockSuccess;}publicintgetLeaseTTL(){returnleaseTTL;}publicvoidsetLeaseTTL(intleaseTTL){this。leaseTTLleaseTTL;}}publicDemoJedisLock(JedisPooledclient,Stringkey,Stringvalue,intttlSeconds){1。准备客户端this。clientclient;this。lockStatenewLockState(key,value,ttlSeconds);this。sleepMillisecond(ttlSeconds1000)3;抢锁的重试间隔可由用户指定}publicbooleantryLock(longwaitTime,TimeUnitwaitUnit)throwsDtLockException{longtotalMillisSecondswaitUnit。toMillis(waitTime);longstartSystem。currentTimeMillis();重试,直到成功或超过指定时间while(true){抢锁try{SetParamsparamsSetParams。setParams()。nx()。ex(lockState。getLeaseTTL());Stringresultclient。set(lockState。getLockKey(),lockState。getLockValue(),params);if(RESULTOK。equals(result)){manualKeepAlive();log。info(〔jedislock〕locksuccess线程:{}加锁成功,key:{},value:{},Thread。currentThread()。getName(),lockState。getLockKey(),lockState。getLockValue());lockState。setLockSuccess(true);returntrue;}else{if(System。currentTimeMillis()starttotalMillisSeconds){returnfalse;}Thread。sleep(sleepMillisecond);}}catch(Exceptione){Throwablecausee。getCause();if(causeinstanceofSocketTimeoutException){忽略网络抖动等异常}log。error(〔jedislock〕lockfailed:e);thrownewDtLockException(〔jedislock〕lockfailed:e。getMessage(),e);}}}此实现中忽略,网络通信异常部分的处理,可参考tryLockpublicvoidunlock()throwsDtLockException{try{首先停止续约if(keepAliveTask!null){keepAliveTask。close();}Stringscriptifredis。call(get,KEYS〔1〕)ARGV〔1〕thenreturnredis。call(del,KEYS〔1〕)elsereturn0end;Objectresultclient。eval(script,1,lockState。getLockKey(),lockState。getLockValue());if(UNLOCKSUCCESS。equals(result)){log。info(〔jedislock〕unlocksuccess线程:{}解锁成功,锁key:{},路径:{},Thread。currentThread()。getName(),lockState。getLockKey(),lockState。getLockValue());}else{log。info(〔jedislock〕unlockdelkeyfailed,线程:{}解锁成功,锁key:{},路径:{},Thread。currentThread()。getName(),lockState。getLockKey(),lockState。getLockValue());}}catch(Exceptione){log。error(〔jedislock〕unlockfailed:e。getMessage(),e);thrownewDtLockException(〔jedislock〕unlockfailed:e。getMessage(),e);}}定时将Key的过期推迟privatevoidmanualKeepAlive(){finalStringtkeylockState。getLockKey();finalinttttllockState。getLeaseTTL();keepAliveTasknewKeepAliveTask(newKeepAliveAction(){Overridepublicvoidrun()throwsDtLockException{刷新值try{client。expire(tkey,tttl);}catch(Exceptione){e。printStackTrace();}}},tttl);keepAliveTask。start();}}复制代码异常类的简单实现packagecom。rock。dlock。common;publicclassDtLockExceptionextendsRuntimeException{publicDtLockException(Stringmessage){super(message);}publicDtLockException(Stringmessage,Throwablecause){super(message,cause);}publicstaticDtLockExceptionclientException(){returnnewDtLockException(clientisempty);}}复制代码watchDog的任务抽象packagecom。rock。dlock。common;publicinterfaceKeepAliveAction{voidrun()throwsDtLockException;}复制代码watchDog的简单实现packagecom。rock。dlock。common;importorg。slf4j。Logger;importorg。slf4j。LoggerFactory;importjava。util。concurrent。TimeUnit;authorzsdate20221174:20PMpublicclassKeepAliveTaskextendsThread{privatestaticfinalLoggerLOGGERLoggerFactory。getLogger(KeepAliveTask。class);publicvolatilebooleanisRunningtrue;过期时间,单位sprivatelongttlSeconds;privateKeepAliveActionaction;publicKeepAliveTask(KeepAliveActionaction,longttlSeconds){this。ttlSecondsttlSeconds;this。actionaction;this。setDaemon(true);}Overridepublicvoidrun(){finallongsleepthis。ttlSeconds10003;每隔三分之一过期时间,续租一次while(isRunning){try{1、续租,刷新值action。run();LOGGER。debug(续租成功!);TimeUnit。MILLISECONDS。sleep(sleep);}catch(InterruptedExceptione){close();}catch(DtLockExceptione){close();}}}publicvoidclose(){isRunningfalse;this。interrupt();}}复制代码4。4测试锁importcom。rock。dlock。jedis。DemoJedisLock;importredis。clients。jedis。JedisPooled;importjava。util。UUID;importjava。util。concurrent。TimeUnit;authorzsdate202211134:51PMpublicclassTestJedisLock{publicstaticvoidmain(String〔〕args){JedisPooledjedisnewJedisPooled(127。0。0。1,6379);DemoJedisLockdemoEtcdLock1newDemoJedisLock(jedis,rock,UUID。randomUUID()。toString(),10);DemoJedisLockdemoEtcdLock2newDemoJedisLock(jedis,rock,UUID。randomUUID()。toString(),10);booleanlock1demoEtcdLock1。tryLock(20,TimeUnit。SECONDS);if(lock1){try{System。out。printf(dosomething);}finally{demoEtcdLock1。unlock();}}demoEtcdLock1。tryLock(20,TimeUnit。SECONDS);demoEtcdLock2。tryLock(20,TimeUnit。SECONDS);等待锁,超时后放弃}}复制代码五、使用Jedis的一些注意事项 通常分布式锁服务会和业务逻辑使用同一个Redis集群,自然也使用同一个Jedis客户端;当业务逻辑侧对Redis的读写并发提高时,会给Redis集群和Jedis客户度带来压力;为应对一些异常情况,我们除了解功能层面的API,还需要了解一下客户端的一些配置调优,主要是池化管理和网络通信两个方面5。1池化管理 在使用Jedis时可以配置JedisPool连接池,池化处理有许多好处,如:提高响应的速度、降低资源的消耗、方便管理和维护;JedisPool配置参数大部分是由JedisPoolConfig的对应项来赋值的,在生产中我们需要关注它的配置并合理的赋值,如此能够提升Redis的服务性能,降低资源开销。下边是对一些重要参数的说明、默认及设置建议: 参数 说明 默认值 建议 maxTotal 资源池中的最大连接数 8hrmaxIdle 资源池允许的最大空闲连接数 8hrminIdle 资源池确保的最少空闲连接数 0hrblockWhenExhausted 当资源池用尽后,调用者是否要等待。只有当值为true时,下面的maxWaitMillis才会生效。 true 建议使用默认值。 maxWaitMillis 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)。 1(表示永不超时) 不建议使用默认值。 testOnBorrow 向资源池借用连接时是否做连接有效性检测(ping)。检测到的无效连接将会被移除。 false 业务量很大时候建议设置为false,减少一次ping的开销。 testOnReturn 向资源池归还连接时是否做连接有效性检测(ping)。检测到无效连接将会被移除。 false 业务量很大时候建议设置为false,减少一次ping的开销。 jmxEnabled 是否开启JMX监控 true 建议开启,请注意应用本身也需要开启。 空闲Jedis对象的回收检测由以下四个参数组合完成,testWhileIdle是该功能的开关。 名称 说明 默认值 建议 testWhileIdle 是否开启空闲资源检测。 false true timeBetweenEvictionRunsMillis 空闲资源的检测周期(单位为毫秒) 1(不检测) 建议设置,周期自行选择,也可以默认也可以使用下方JedisPoolConfig中的配置。 minEvictableIdleTimeMillis 资源池中资源的最小空闲时间(单位为毫秒),达到此值后空闲资源将被移除。 180000(即30分钟) 可根据自身业务决定,一般默认值即可,也可以考虑使用下方JeidsPoolConfig中的配置。 numTestsPerEvictionRun 做空闲资源检测时,每次检测资源的个数。 3hr可根据自身应用连接数进行微调,如果设置为1,就是对所有连接做空闲监测。 通过源码可以发现这些配置是GenericObjectPoolConfig对象的属性,这个类实际上是rg。apache。commons。pool2。implapache提供的,也就是说jedis的连接池是依托于apache提供的对象池来,这个对象池的声明周期如下图,感兴趣的可以看下: 5。2网络调优maxredirects:这个是集群模式下,重定向的最大数量;举例说明,比如第一台挂了,连第二台,第二台挂了连第三台,重新连接的次数不能超过这个值timeout:客户端超时时间,单位是毫秒 Rsdis节点故障或者网络抖动时,这两个值如果不合理可能会导致很严重的问题,比如timeout设置为1000,maxRedirect为2,一旦出现redis连接问题,将会导致请求阻塞3s左右。而这个3秒的阻塞在可能导致常规业务流量下的线程池耗尽,需根据业务场景调整。 原文链接:https:juejin。cnpost7166131629348356104