RedisLock
packagetest.utils;importio.lettuce.core.RedisFuture;importio.lettuce.core.ScriptOutputType;importio.lettuce.core.SetArgs;importio.lettuce.core.api.async.RedisAsyncCommands;importio.lettuce.core.api.async.RedisScriptingAsyncCommands;importio.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;importlombok.extern.log4j.Log4j2;importorg.springframework.data.redis.connection.RedisConnection;importorg.springframework.data.redis.core.RedisCallback;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.util.Assert;importorg.springframework.util.StringUtils;importjava.nio.charset.StandardCharsets;importjava.util.UUID;importjava.util.concurrent.ExecutionException;importjava.util.concurrent.TimeUnit;@Log4j2publicclassRedisLock{privateStringRedisTemplate redisTemplate;publicstaticfinalString NX="NX";publicstaticfinalString EX="EX";publicstaticfinalString OK="OK";publicstaticfinalString UNLOCK_LUA="if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";privateString lockKey;privateString lockValue;privateint expireTime=120;privatelong timeOut=500;privatevolatileboolean locked=false;privatestaticfinalString REDIS_LIB_MISMATCH="Failed to convert nativeConnection. "+"Is your SpringBoot main version > 2.0 ? Only lib:lettuce is supported.";publicRedisLock(StringRedisTemplate redisTemplate,String lockKey){this.redisTemplate= redisTemplate;this.lockKey= lockKey;}publicRedisLock(StringRedisTemplate redisTemplate,String lockKey,int expireTime,long timeOut){this(redisTemplate, lockKey);this.expireTime= expireTime;this.timeOut= timeOut;}publicbooleantryLock(){
lockValue= UUID.randomUUID().toString();long timeout= timeOut*1000000;long nowTime=System.nanoTime();while((System.nanoTime()- nowTime)< timeout){if(OK.equalsIgnoreCase(this.set(lockKey, lockValue, expireTime))){
locked=true;returntrue;}try{TimeUnit.MILLISECONDS.sleep(100);}catch(InterruptedException e){
log.info("Sleep is interrupted", e);}}return locked;}publicbooleanlock(){
lockValue= UUID.randomUUID().toString();String result=set(lockKey, lockValue, expireTime);
locked= OK.equalsIgnoreCase(result);return locked;}publicbooleanlockBlock(){
lockValue= UUID.randomUUID().toString();while(true){String result=set(lockKey, lockValue, expireTime);if(OK.equalsIgnoreCase(result)){
locked=true;return locked;}try{TimeUnit.MILLISECONDS.sleep(100);}catch(InterruptedException e){
log.info("Sleep is interrupted", e);}}}publicBooleanunlock(){if(locked){try{return redisTemplate.execute((RedisConnection connection)->{Object nativeConnection= connection.getNativeConnection();Long result=0L;byte[] keyBytes= lockKey.getBytes(StandardCharsets.UTF_8);byte[] valueBytes= lockValue.getBytes(StandardCharsets.UTF_8);Object[] keyParam=newObject[]{keyBytes};if(nativeConnectioninstanceofRedisScriptingAsyncCommands){RedisScriptingAsyncCommands<Object,byte[]> command=(RedisScriptingAsyncCommands<Object,byte[]>) nativeConnection;RedisFuture future= command.eval(UNLOCK_LUA,ScriptOutputType.INTEGER, keyParam, valueBytes);
result=getEvalResult(future,connection);}else{
log.warn(REDIS_LIB_MISMATCH);}if(result==0L&&!StringUtils.isEmpty(lockKey)){
log.debug("Unlock failed! key={}, time={}", lockKey,System.currentTimeMillis());}
locked= result==0L;return result==1L;});}catch(Throwable e){if(log.isDebugEnabled()){
log.debug("The redis you are using dose NOT support EVAL. Use downgrade method to unlock. {}",
e.getMessage());}String value=this.get(lockKey,String.class);if(lockValue.equals(value)){
redisTemplate.delete(lockKey);returntrue;}returnfalse;}}returntrue;}privateLonggetEvalResult(RedisFuture future,RedisConnection connection){try{Object o= future.get();return(Long)o;}catch(InterruptedException|ExecutionException e){
log.error("Future get failed, trying to close connection.", e);closeConnection(connection);return0L;}}publicbooleanisLock(){return locked;}privateStringset(finalString key,finalString value,finallong expireSeconds){Assert.isTrue(!StringUtils.isEmpty(key),"Invalid key");return redisTemplate.execute((RedisCallback<String>) connection->{Object nativeConnection= connection.getNativeConnection();String result=null;byte[] keyByte= key.getBytes(StandardCharsets.UTF_8);byte[] valueByte= value.getBytes(StandardCharsets.UTF_8);if(nativeConnectioninstanceofRedisAsyncCommands){RedisAsyncCommands command=(RedisAsyncCommands) nativeConnection;
result= command.getStatefulConnection().sync().set(keyByte, valueByte,SetArgs.Builder.nx().ex(expireSeconds));}elseif(nativeConnectioninstanceofRedisAdvancedClusterAsyncCommands){RedisAdvancedClusterAsyncCommands clusterAsyncCommands=(RedisAdvancedClusterAsyncCommands) nativeConnection;
result= clusterAsyncCommands.getStatefulConnection().sync().set(keyByte, valueByte,SetArgs.Builder.nx().ex(expireSeconds));}else{
log.error(REDIS_LIB_MISMATCH);}return result;});}privatevoidcloseConnection(RedisConnection connection){try{
connection.close();}catch(Exception e2){
log.error("close connection fail.", e2);}}private<T>Tget(finalString key,Class<T> clazz){Assert.isTrue(!StringUtils.isEmpty(key),"Invalid key");return redisTemplate.execute((RedisConnection connection)->{Object nativeConnection= connection.getNativeConnection();Object result=null;byte[] keyByte= key.getBytes(StandardCharsets.UTF_8);if(nativeConnectioninstanceofRedisAsyncCommands){RedisAsyncCommands command=(RedisAsyncCommands) nativeConnection;
result= command.getStatefulConnection().sync().get(keyByte);}elseif(nativeConnectioninstanceofRedisAdvancedClusterAsyncCommands){RedisAdvancedClusterAsyncCommands clusterAsyncCommands=(RedisAdvancedClusterAsyncCommands) nativeConnection;
result= clusterAsyncCommands.getStatefulConnection().sync().get(keyByte);}return clazz.cast(result);});}}