spring boot redis分布式鎖

一.?Redis?分布式鎖的實現以及存在的問題

鎖是針對某個資源,保證其訪問的互斥性,在實際使用當中,這個資源一般是一個字符串。使用?Redis?實現鎖,主要是將資源放到?Redis?當中,利用其原子性,當其他線程訪問時,如果?Redis?中已經存在這個資源,就不允許之后的一些操作。spring boot使用?Redis?的操作主要是通過?RedisTemplate?來實現,一般步驟如下:

將鎖資源放入?Redis?(注意是當key不存在時才能放成功,所以使用?setIfAbsent?方法):

redisTemplate.opsForValue().setIfAbsent("key","value");

設置過期時間

redisTemplate.expire("key", 30000,TimeUnit.MILLISECONDS);

釋放鎖

redisTemplate.delete("key");

一般情況下,這樣的實現就能夠滿足鎖的需求了,但是如果在調用?setIfAbsent?方法之后線程掛掉了,即沒有給鎖定的資源設置過期時間,默認是永不過期,那么這個鎖就會一直存在。所以需要保證設置鎖及其過期時間兩個操作的原子性,spring data的?RedisTemplate?當中并沒有這樣的方法。但是在jedis當中是有這種原子操作的方法的,需要通過?RedisTemplate?的?execute?方法獲取到jedis里操作命令的對象,代碼如下:

Stringresult = redisTemplate.execute(newRedisCallback() {? ? @OverridepublicStringdoInRedis(RedisConnection connection)throwsDataAccessException {? ? ? ? JedisCommands commands = (JedisCommands) connection.getNativeConnection();returncommands.set(key,"鎖定的資源","NX","PX", expire);? ? }});

注意:Redis?從2.6.12版本開始?set?命令支持?NX?、?PX?這些參數來達到?setnx?、?setex?、?psetex?命令的效果,文檔參見:http://doc.redisfans.com/string/set.html

NX:表示只有當鎖定資源不存在的時候才能?SET?成功。利用?Redis?的原子性,保證了只有第一個請求的線程才能獲得鎖,而之后的所有線程在鎖定資源被釋放之前都不能獲得鎖。

PX:expire?表示鎖定的資源的自動過期時間,單位是毫秒。具體過期時間根據實際場景而定

這樣在獲取鎖的時候就能夠保證設置?Redis?值和過期時間的原子性,避免前面提到的兩次?Redis?操作期間出現意外而導致的鎖不能釋放的問題。但是這樣還是可能會存在一個問題,考慮如下的場景順序:

線程T1獲取鎖

線程T1執行業務操作,由于某些原因阻塞了較長時間

鎖自動過期,即鎖自動釋放了

線程T2獲取鎖

線程T1業務操作完畢,釋放鎖(其實是釋放的線程T2的鎖)

按照這樣的場景順序,線程T2的業務操作實際上就沒有鎖提供保護機制了。所以,每個線程釋放鎖的時候只能釋放自己的鎖,即鎖必須要有一個擁有者的標記,并且也需要保證釋放鎖的原子性操作。

因此在獲取鎖的時候,可以生成一個隨機不唯一的串放入當前線程中,然后再放入?Redis?。釋放鎖的時候先判斷鎖對應的值是否與線程中的值相同,相同時才做刪除操作。

Redis?從2.6.0開始通過內置的?Lua?解釋器,可以使用?EVAL?命令對?Lua?腳本進行求值,文檔參見:http://doc.redisfans.com/script/eval.html

因此我們可以通過?Lua?腳本來達到釋放鎖的原子操作,定義?Lua?腳本如下:

ifredis.call("get",KEYS[1]) == ARGV[1]thenreturnredis.call("del",KEYS[1])elsereturn0end

具體意思可以參考上面提供的文檔地址

使用?RedisTemplate?執行的代碼如下:

// 使用Lua腳本刪除Redis中匹配value的key,可以避免由于方法執行時間過長而redis鎖自動過期失效的時候誤刪其他線程的鎖// spring自帶的執行腳本方法中,集群模式直接拋出不支持執行腳本的異常,所以只能拿到原redis的connection來執行腳本Longresult = redisTemplate.execute(newRedisCallback() {publicLongdoInRedis(RedisConnection connection) throws DataAccessException {? ? ? ? Object nativeConnection = connection.getNativeConnection();// 集群模式和單機模式雖然執行腳本的方法一樣,但是沒有共同的接口,所以只能分開執行// 集群模式if(nativeConnectioninstanceofJedisCluster) {return(Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);? ? ? ? }// 單機模式elseif(nativeConnectioninstanceofJedis) {return(Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);? ? ? ? }return0L;? ? }});

代碼中分為集群模式和單機模式,并且兩者的方法、參數都一樣,原因是spring封裝的執行腳本的方法中(?RedisConnection?接口繼承于?RedisScriptingCommands?接口的?eval?方法),集群模式的方法直接拋出了不支持執行腳本的異常(雖然實際是支持的),所以只能拿到?Redis?的connection來執行腳本,而?JedisCluster?和?Jedis?中的方法又沒有實現共同的接口,所以只能分開調用。

spring封裝的集群模式執行腳本方法源碼:

# JedisClusterConnection.java/** * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisScriptingCommands#eval(byte[], org.springframework.data.redis.connection.ReturnType, int, byte[][]) */@OverridepublicT eval(byte[] script, ReturnType returnType,intnumKeys, byte[]... keysAndArgs) {thrownewInvalidDataAccessApiUsageException("Eval is not supported in cluster environment.");}

至此,我們就完成了一個相對可靠的?Redis?分布式鎖,但是,在集群模式的極端情況下,還是可能會存在一些問題,比如如下的場景順序(本文暫時不深入開展):

線程T1獲取鎖成功

Redis?的master節點掛掉,slave自動頂上

線程T2獲取鎖,會從slave節點上去判斷鎖是否存在,由于Redis的master slave復制是異步的,所以此時線程T2可能成功獲取到鎖

為了可以以后擴展為使用其他方式來實現分布式鎖,定義了接口和抽象類,所有的源碼如下:

# DistributedLock.java 頂級接口/**

* @author fuwei.deng

* @date 2017年6月14日 下午3:11:05

* @version 1.0.0

*/publicinterface DistributedLock {publicstaticfinallongTIMEOUT_MILLIS =30000;publicstaticfinalintRETRY_TIMES = Integer.MAX_VALUE;publicstaticfinallongSLEEP_MILLIS =500;publicbooleanlock(Stringkey);publicbooleanlock(Stringkey,intretryTimes);publicbooleanlock(Stringkey,intretryTimes,longsleepMillis);publicbooleanlock(Stringkey,longexpire);publicbooleanlock(Stringkey,longexpire,intretryTimes);publicbooleanlock(Stringkey,longexpire,intretryTimes,longsleepMillis);publicbooleanreleaseLock(Stringkey);}

# AbstractDistributedLock.java 抽象類,實現基本的方法,關鍵方法由子類去實現/**

* @author fuwei.deng

* @date 2017年6月14日 下午3:10:57

* @version 1.0.0

*/publicabstractclass AbstractDistributedLock implements DistributedLock {? ? @Overridepublicbooleanlock(Stringkey) {returnlock(key, TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS);? ? }? ? @Overridepublicbooleanlock(Stringkey,intretryTimes) {returnlock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS);? ? }? ? @Overridepublicbooleanlock(Stringkey,intretryTimes,longsleepMillis) {returnlock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis);? ? }? ? @Overridepublicbooleanlock(Stringkey,longexpire) {returnlock(key, expire, RETRY_TIMES, SLEEP_MILLIS);? ? }? ? @Overridepublicbooleanlock(Stringkey,longexpire,intretryTimes) {returnlock(key, expire, retryTimes, SLEEP_MILLIS);? ? }}

# RedisDistributedLock.java Redis分布式鎖的實現importjava.util.ArrayList;importjava.util.List;importjava.util.UUID;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.dao.DataAccessException;importorg.springframework.data.redis.connection.RedisConnection;importorg.springframework.data.redis.core.RedisCallback;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.util.StringUtils;importredis.clients.jedis.Jedis;importredis.clients.jedis.JedisCluster;importredis.clients.jedis.JedisCommands;/**

* @author fuwei.deng

* @date 2017年6月14日 下午3:11:14

* @version 1.0.0

*/publicclass RedisDistributedLock extends AbstractDistributedLock {privatefinalLogger logger = LoggerFactory.getLogger(RedisDistributedLock.class);privateRedisTemplate redisTemplate;privateThreadLocal lockFlag =newThreadLocal();publicstaticfinalStringUNLOCK_LUA;static{? ? ? ? StringBuilder sb =newStringBuilder();? ? ? ? sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");? ? ? ? sb.append("then ");? ? ? ? sb.append("? ? return redis.call(\"del\",KEYS[1]) ");? ? ? ? sb.append("else ");? ? ? ? sb.append("? ? return 0 ");? ? ? ? sb.append("end ");? ? ? ? UNLOCK_LUA = sb.toString();? ? }publicRedisDistributedLock(RedisTemplate redisTemplate) {super();this.redisTemplate = redisTemplate;? ? }? ? @Overridepublicbooleanlock(Stringkey,longexpire,intretryTimes,longsleepMillis) {booleanresult = setRedis(key, expire);// 如果獲取鎖失敗,按照傳入的重試次數進行重試while((!result) && retryTimes-- >0){try{? ? ? ? ? ? ? ? logger.debug("lock failed, retrying..."+ retryTimes);? ? ? ? ? ? ? ? Thread.sleep(sleepMillis);? ? ? ? ? ? }catch(InterruptedException e) {returnfalse;? ? ? ? ? ? }? ? ? ? ? ? result = setRedis(key, expire);? ? ? ? }returnresult;? ? }privatebooleansetRedis(Stringkey,longexpire) {try{Stringresult = redisTemplate.execute(newRedisCallback() {? ? ? ? ? ? ? ? @OverridepublicStringdoInRedis(RedisConnection connection)throwsDataAccessException {? ? ? ? ? ? ? ? ? ? JedisCommands commands = (JedisCommands) connection.getNativeConnection();Stringuuid = UUID.randomUUID().toString();? ? ? ? ? ? ? ? ? ? lockFlag.set(uuid);returncommands.set(key, uuid,"NX","PX", expire);? ? ? ? ? ? ? ? }? ? ? ? ? ? });return!StringUtils.isEmpty(result);? ? ? ? }catch(Exception e) {? ? ? ? ? ? logger.error("set redis occured an exception", e);? ? ? ? }returnfalse;? ? }? ? ? ? @OverridepublicbooleanreleaseLock(Stringkey) {// 釋放鎖的時候,有可能因為持鎖之后方法執行時間大于鎖的有效期,此時有可能已經被另外一個線程持有鎖,所以不能直接刪除try{? ? ? ? ? ? List keys =newArrayList();? ? ? ? ? ? keys.add(key);? ? ? ? ? ? List args =newArrayList();? ? ? ? ? ? args.add(lockFlag.get());// 使用lua腳本刪除redis中匹配value的key,可以避免由于方法執行時間過長而redis鎖自動過期失效的時候誤刪其他線程的鎖// spring自帶的執行腳本方法中,集群模式直接拋出不支持執行腳本的異常,所以只能拿到原redis的connection來執行腳本Long result = redisTemplate.execute(newRedisCallback() {publicLong doInRedis(RedisConnection connection)throwsDataAccessException {ObjectnativeConnection = connection.getNativeConnection();// 集群模式和單機模式雖然執行腳本的方法一樣,但是沒有共同的接口,所以只能分開執行// 集群模式if(nativeConnectioninstanceofJedisCluster) {return(Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);? ? ? ? ? ? ? ? ? ? }// 單機模式elseif(nativeConnectioninstanceofJedis) {return(Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);? ? ? ? ? ? ? ? ? ? }return0L;? ? ? ? ? ? ? ? }? ? ? ? ? ? });returnresult !=null&& result >0;? ? ? ? }catch(Exception e) {? ? ? ? ? ? logger.error("release lock occured an exception", e);? ? ? ? }returnfalse;? ? }? ? }

二. 基于?AOP?的?Redis?分布式鎖

在實際的使用過程中,分布式鎖可以封裝好后使用在方法級別,這樣就不用每個地方都去獲取鎖和釋放鎖,使用起來更加方便。

首先定義個注解:

importjava.lang.annotation.ElementType;importjava.lang.annotation.Inherited;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;/**

* @author fuwei.deng

* @date 2017年6月14日 下午3:10:36

* @version 1.0.0

*/@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic@interfaceRedisLock {/** 鎖的資源,redis的key*/Stringvalue()default"default";/** 持鎖時間,單位毫秒*/long keepMills()default30000;/** 當獲取失敗時候動作*/LockFailAction action()defaultLockFailAction.CONTINUE;? ? ? ? publicenumLockFailAction{/** 放棄 */GIVEUP,/** 繼續 */CONTINUE;? ? }/** 重試的間隔時間,設置GIVEUP忽略此項*/long sleepMills()default200;/** 重試次數*/intretryTimes()default5;}

裝配分布式鎖的bean

importorg.springframework.boot.autoconfigure.AutoConfigureAfter;importorg.springframework.boot.autoconfigure.condition.ConditionalOnBean;importorg.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.core.RedisTemplate;importcom.itopener.lock.redis.spring.boot.autoconfigure.lock.DistributedLock;importcom.itopener.lock.redis.spring.boot.autoconfigure.lock.RedisDistributedLock;/**

* @author fuwei.deng

* @date 2017年6月14日 下午3:11:31

* @version 1.0.0

*/@Configuration@AutoConfigureAfter(RedisAutoConfiguration.class)public class DistributedLockAutoConfiguration {? ? ? ? @Bean@ConditionalOnBean(RedisTemplate.class)? ? public DistributedLock redisDistributedLock(RedisTemplateredisTemplate){returnnewRedisDistributedLock(redisTemplate);? ? }? ? }

定義切面(spring boot配置方式)

importjava.lang.reflect.Method;importjava.util.Arrays;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Pointcut;importorg.aspectj.lang.reflect.MethodSignature;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.autoconfigure.AutoConfigureAfter;importorg.springframework.boot.autoconfigure.condition.ConditionalOnClass;importorg.springframework.context.annotation.Configuration;importorg.springframework.util.StringUtils;importcom.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock;importcom.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock.LockFailAction;importcom.itopener.lock.redis.spring.boot.autoconfigure.lock.DistributedLock;/** *@authorfuwei.deng *@date2017年6月14日 下午3:11:22 *@version1.0.0 */@Aspect@Configuration@ConditionalOnClass(DistributedLock.class)@AutoConfigureAfter(DistributedLockAutoConfiguration.class)publicclassDistributedLockAspectConfiguration{privatefinalLogger logger = LoggerFactory.getLogger(DistributedLockAspectConfiguration.class);@AutowiredprivateDistributedLock distributedLock;@Pointcut("@annotation(com.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock)")privatevoid lockPoint(){? ? ? ? ? ? }@Around("lockPoint()")publicObject around(ProceedingJoinPoint pjp) throws Throwable{? ? ? ? Method method = ((MethodSignature) pjp.getSignature()).getMethod();? ? ? ? RedisLock redisLock = method.getAnnotation(RedisLock.class);? ? ? ? String key = redisLock.value();if(StringUtils.isEmpty(key)){? ? ? ? ? ? Object[] args = pjp.getArgs();? ? ? ? ? ? key = Arrays.toString(args);? ? ? ? }? ? ? ? int retryTimes = redisLock.action().equals(LockFailAction.CONTINUE) ? redisLock.retryTimes() :0;? ? ? ? boolean lock = distributedLock.lock(key, redisLock.keepMills(), retryTimes, redisLock.sleepMills());if(!lock) {? ? ? ? ? ? logger.debug("get lock failed : "+ key);returnnull;? ? ? ? }//得到鎖,執行方法,釋放鎖logger.debug("get lock success : "+ key);try{returnpjp.proceed();? ? ? ? }catch(Exception e) {? ? ? ? ? ? logger.error("execute locked method occured an exception", e);? ? ? ? }finally{? ? ? ? ? ? boolean releaseResult = distributedLock.releaseLock(key);? ? ? ? ? ? logger.debug("release lock : "+ key + (releaseResult ?" success":" failed"));? ? ? ? }returnnull;? ? }}

spring boot starter還需要在?resources/META-INF?中添加?spring.factories?文件

# Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.itopener.lock.redis.spring.boot.autoconfigure.DistributedLockAutoConfiguration,\com.itopener.lock.redis.spring.boot.autoconfigure.DistributedLockAspectConfiguration

這樣封裝之后,使用spring boot開發的項目,直接依賴這個starter,就可以在方法上加?RedisLock?注解來實現分布式鎖的功能了,當然如果需要自己控制,直接注入分布式鎖的bean即可

@AutowiredprivateDistributedLock distributedLock;

如果需要使用其他的分布式鎖實現,繼承?AbstractDistributedLock?后實現獲取鎖和釋放鎖的方法即可

歡迎歡迎學Java的朋友們加入java架構交流: 855835163

群內提供免費的Java架構學習資料(里面有高可用、高并發、高性能及分布式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容