Redis分布式锁实例分析

Redis分布式锁实例分析

这篇文章主要介绍“Redis分布式锁实例分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Redis分布式锁实例分析”文章能帮助大家解决问题。

分布式锁概览

在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问,Java中我们一般可以使用synchronized语法和ReetrantLock去保证,这实际上是本地锁的方式。但是现在公司都是流行分布式架构,在分布式环境下,如何保证不同节点的线程同步执行呢?因此就引出了分布式锁,它是控制分布式系统之间互斥访问共享资源的一种方式。

在一个分布式系统中,多台机器上部署了多个服务,当客户端一个用户发起一个数据插入请求时,如果没有分布式锁机制保证,那么那多台机器上的多个服务可能进行并发插入操作,导致数据重复插入,对于某些不允许有多余数据的业务来说,这就会造成问题。而分布式锁机制就是为了解决类似这类问题,保证多个服务之间互斥的访问共享资源,如果一个服务抢占了分布式锁,其他服务没获取到锁,就不进行后续操作。大致意思如下图所示:

分布式锁的特点

分布式锁一般有如下的特点:

  • 互斥性: 同一时刻只能有一个线程持有锁

  • 可重入性: 同一节点上的同一个线程如果获取了锁之后能够再次获取锁

  • 锁超时:和J.U.C中的锁一样支持锁超时,防止死锁

  • 高性能和高可用: 加锁和解锁需要高效,同时也需要保证高可用,防止分布式锁失效

  • 具备阻塞和非阻塞性:能够及时从阻塞状态中被唤醒

分布式锁的实现方式

我们一般实现分布式锁有以下几种方式:

  • 基于数据库

  • 基于Redis

  • 基于zookeeper

Redis普通分布式锁存在的问题

说到Redis分布式锁,大部分人都会想到:setnx+lua(redis保证执行lua脚本时不执行其他操作,保证操作的原子性),或者知道set key value px milliseconds nx。后一种方式的核心实现命令如下:

-获取锁(unique_value可以是UUID等)SETresource_nameunique_valueNXPX30000-释放锁(lua脚本中,一定要比较value,防止误解锁)ifredis.call("get",KEYS[1])==ARGV[1]thenreturnredis.call("del",KEYS[1])elsereturn0end

这种实现方式有3大要点(也是面试概率非常高的地方):

  1. set命令要用set key value px milliseconds nx

  2. value要具有唯一性;

  3. 释放锁时要验证value值,不能误解锁;

事实上这类锁最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:

  1. 在Redis的master节点上拿到了锁;

  2. 但是这个加锁的key还没有同步到slave节点;

  3. master故障,发生故障转移,slave节点升级为master节点;

  4. 导致锁丢失。

为了避免单点故障问题,Redis作者antirez基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock。Redlock也是Redis所有分布式锁实现方式中唯一能让面试官高潮的方式。

Redis高级分布式锁:Redlock

antirez提出的redlock算法大概是这样的:

在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。

为了取到锁,客户端应该执行以下操作:

  • 获取当前Unix时间,以毫秒为单位。

  • 依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间TTL为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。

  • 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功

  • 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。

  • 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

  • 此处不讨论时钟漂移

Redlock源码

redisson已经有对redlock算法封装,接下来对其用法进行简单介绍,并对核心源码进行分析(假设5个redis实例)。

1. Redlock依赖

<!--https://mvnrepository.com/artifact/org.redisson/redisson--><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.3.2</version></dependency>

2. Redlock用法

首先,我们来看一下redission封装的redlock算法实现的分布式锁用法,非常简单,跟重入锁(ReentrantLock)有点类似:

Configconfig=newConfig();config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379","127.0.0.1:6389").setMasterName("masterName").setPassword("password").setDatabase(0);RedissonClientredissonClient=Redisson.create(config);//还可以getFairLock(),getReadWriteLock()RLockredLock=redissonClient.getLock("REDLOCK_KEY");booleanisLock;try{isLock=redLock.tryLock();//500ms拿不到锁,就认为获取锁失败。10000ms即10s是锁失效时间。isLock=redLock.tryLock(500,10000,TimeUnit.MILLISECONDS);if(isLock){//TODOifgetlocksuccess,dosomething;}}catch(Exceptione){}finally{//无论如何,最后都要解锁redLock.unlock();}

3. Redlock唯一ID

实现分布式锁的一个非常重要的点就是set的value要具有唯一性,redisson的value是怎样保证value的唯一性呢?答案是UUID+threadId。入口在redissonClient.getLock(“REDLOCK_KEY”),源码在Redisson.java和RedissonLock.java中:

protectedfinalUUIDid=UUID.randomUUID();StringgetLockName(longthreadId){returnid+":"+threadId;}

4. Redlock获取锁

获取锁的代码为redLock.tryLock()或者redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS),两者的最终核心源码都是下面这段代码,只不过前者获取锁的默认租约时间(leaseTime)是LOCK_EXPIRATION_INTERVAL_SECONDS,即30s:

<T>RFuture<T>tryLockInnerAsync(longleaseTime,TimeUnitunit,longthreadId,RedisStrictCommand<T>command){internalLockLeaseTime=unit.toMillis(leaseTime);//获取锁时向5个redis实例发送的命令returncommandExecutor.evalWriteAsync(getName(),LongCodec.INSTANCE,command,//首先分布式锁的KEY不能存在,如果确实不存在,那么执行hset命令(hsetREDLOCK_KEYuuid+threadId1),并通过pexpire设置失效时间(也是锁的租约时间)"if(redis.call('exists',KEYS[1])==0)then"+"redis.call('hset',KEYS[1],ARGV[2],1);"+"redis.call('pexpire',KEYS[1],ARGV[1]);"+"returnnil;"+"end;"+//如果分布式锁的KEY已经存在,并且value也匹配,表示是当前线程持有的锁,那么重入次数加1,并且设置失效时间"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]);"+"returnnil;"+"end;"+//获取分布式锁的KEY的失效时间毫秒数"returnredis.call('pttl',KEYS[1]);",//这三个参数分别对应KEYS[1],ARGV[1]和ARGV[2]Collections.<Object>singletonList(getName()),internalLockLeaseTime,getLockName(threadId));}

获取锁的命令中,

  • KEYS[1]就是Collections.singletonList(getName()),表示分布式锁的key,即REDLOCK_KEY;

  • ARGV[1]就是internalLockLeaseTime,即锁的租约时间,默认30s;

  • ARGV[2]就是getLockName(threadId),是获取锁时set的唯一值,即UUID+threadId:

5. Redlock释放锁

释放锁的代码为redLock.unlock(),核心源码如下:

protectedRFuture<Boolean>unlockInnerAsync(longthreadId){//向5个redis实例都执行如下命令returncommandExecutor.evalWriteAsync(getName(),LongCodec.INSTANCE,RedisCommands.EVAL_BOOLEAN,//如果分布式锁KEY不存在,那么向channel发布一条消息"if(redis.call('exists',KEYS[1])==0)then"+"redis.call('publish',KEYS[2],ARGV[1]);"+"return1;"+"end;"+//如果分布式锁存在,但是value不匹配,表示锁已经被占用,那么直接返回"if(redis.call('hexists',KEYS[1],ARGV[3])==0)then"+"returnnil;"+"end;"+//如果就是当前线程占有分布式锁,那么将重入次数减1"localcounter=redis.call('hincrby',KEYS[1],ARGV[3],-1);"+//重入次数减1后的值如果大于0,表示分布式锁有重入过,那么只设置失效时间,还不能删除"if(counter>0)then"+"redis.call('pexpire',KEYS[1],ARGV[2]);"+"return0;"+"else"+//重入次数减1后的值如果为0,表示分布式锁只获取过1次,那么删除这个KEY,并发布解锁消息"redis.call('del',KEYS[1]);"+"redis.call('publish',KEYS[2],ARGV[1]);"+"return1;"+"end;"+"returnnil;",//这5个参数分别对应KEYS[1],KEYS[2],ARGV[1],ARGV[2]和ARGV[3]Arrays.<Object>asList(getName(),getChannelName()),LockPubSub.unlockMessage,internalLockLeaseTime,getLockName(threadId));}

Redis实现的分布式锁轮子

下面利用SpringBoot + Jedis + AOP的组合来实现一个简易的分布式锁。

1. 自定义注解

自定义一个注解,被注解的方法会执行获取分布式锁的逻辑

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic@interfaceRedisLock{/***业务键**@return*/Stringkey();/***锁的过期秒数,默认是5秒**@return*/intexpire()default5;/***尝试加锁,最多等待时间**@return*/longwaitTime()defaultLong.MIN_VALUE;/***锁的超时时间单位**@return*/TimeUnittimeUnit()defaultTimeUnit.SECONDS;}

2. AOP拦截器实现

在AOP中我们去执行获取分布式锁和释放分布式锁的逻辑,代码如下:

@Aspect@ComponentpublicclassLockMethodAspect{@AutowiredprivateRedisLockHelperredisLockHelper;@AutowiredprivateJedisUtiljedisUtil;privateLoggerlogger=LoggerFactory.getLogger(LockMethodAspect.class);@Around("@annotation(com.redis.lock.annotation.RedisLock)")publicObjectaround(ProceedingJoinPointjoinPoint){Jedisjedis=jedisUtil.getJedis();MethodSignaturesignature=(MethodSignature)joinPoint.getSignature();Methodmethod=signature.getMethod();RedisLockredisLock=method.getAnnotation(RedisLock.class);Stringvalue=UUID.randomUUID().toString();Stringkey=redisLock.key();try{finalbooleanislock=redisLockHelper.lock(jedis,key,value,redisLock.expire(),redisLock.timeUnit());logger.info("isLock:{}",islock);if(!islock){logger.error("获取锁失败");thrownewRuntimeException("获取锁失败");}try{returnjoinPoint.proceed();}catch(Throwablethrowable){thrownewRuntimeException("系统异常");}}finally{logger.info("释放锁");redisLockHelper.unlock(jedis,key,value);jedis.close();}}}

3. Redis实现分布式锁核心类

@ComponentpublicclassRedisLockHelper{privatelongsleepTime=100;/***直接使用setnx+expire方式获取分布式锁*非原子性**@paramkey*@paramvalue*@paramtimeout*@return*/publicbooleanlock_setnx(Jedisjedis,Stringkey,Stringvalue,inttimeout){Longresult=jedis.setnx(key,value);//result=1时,设置成功,否则设置失败if(result==1L){returnjedis.expire(key,timeout)==1L;}else{returnfalse;}}/***使用Lua脚本,脚本中使用setnex+expire命令进行加锁操作**@paramjedis*@paramkey*@paramUniqueId*@paramseconds*@return*/publicbooleanLock_with_lua(Jedisjedis,Stringkey,StringUniqueId,intseconds){Stringlua_scripts="ifredis.call('setnx',KEYS[1],ARGV[1])==1then"+"redis.call('expire',KEYS[1],ARGV[2])return1elsereturn0end";List<String>keys=newArrayList<>();List<String>values=newArrayList<>();keys.add(key);values.add(UniqueId);values.add(String.valueOf(seconds));Objectresult=jedis.eval(lua_scripts,keys,values);//判断是否成功returnresult.equals(1L);}/***在Redis的2.6.12及以后中,使用setkeyvalue[NX][EX]命令**@paramkey*@paramvalue*@paramtimeout*@return*/publicbooleanlock(Jedisjedis,Stringkey,Stringvalue,inttimeout,TimeUnittimeUnit){longseconds=timeUnit.toSeconds(timeout);return"OK".equals(jedis.set(key,value,"NX","EX",seconds));}/***自定义获取锁的超时时间**@paramjedis*@paramkey*@paramvalue*@paramtimeout*@paramwaitTime*@paramtimeUnit*@return*@throwsInterruptedException*/publicbooleanlock_with_waitTime(Jedisjedis,Stringkey,Stringvalue,inttimeout,longwaitTime,TimeUnittimeUnit)throwsInterruptedException{longseconds=timeUnit.toSeconds(timeout);while(waitTime>=0){Stringresult=jedis.set(key,value,"nx","ex",seconds);if("OK".equals(result)){returntrue;}waitTime-=sleepTime;Thread.sleep(sleepTime);}returnfalse;}/***错误的解锁方法—直接删除key**@paramkey*/publicvoidunlock_with_del(Jedisjedis,Stringkey){jedis.del(key);}/***使用Lua脚本进行解锁操纵,解锁的时候验证value值**@paramjedis*@paramkey*@paramvalue*@return*/publicbooleanunlock(Jedisjedis,Stringkey,Stringvalue){StringluaScript="ifredis.call('get',KEYS[1])==ARGV[1]then"+"returnredis.call('del',KEYS[1])elsereturn0end";returnjedis.eval(luaScript,Collections.singletonList(key),Collections.singletonList(value)).equals(1L);}}

4. Controller层控制

定义一个TestController来测试我们实现的分布式锁

@RestControllerpublicclassTestController{@RedisLock(key="redis_lock")@GetMapping("/index")publicStringindex(){return"index";}}

关于“Redis分布式锁实例分析”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注恰卡编程网行业资讯频道,小编每天都会为大家更新不同的知识点。

发布于 2022-03-29 22:28:49
收藏
分享
海报
0 条评论
22
上一篇:SpringBoot集成Redis开启缓存机制的方法 下一篇:如何搭建fastdfs服务及单机redis服务
目录

    0 条评论

    本站已关闭游客评论,请登录或者注册后再评论吧~

    忘记密码?

    图形验证码