一.?Redis?分布式鎖的實(shí)現(xiàn)以及存在的問題
鎖是針對某個資源,保證其訪問的互斥性,在實(shí)際使用當(dāng)中,這個資源一般是一個字符串。使用?Redis?實(shí)現(xiàn)鎖,主要是將資源放到?Redis?當(dāng)中,利用其原子性,當(dāng)其他線程訪問時(shí),如果?Redis?中已經(jīng)存在這個資源,就不允許之后的一些操作。spring boot使用?Redis?的操作主要是通過?RedisTemplate?來實(shí)現(xiàn),一般步驟如下:
將鎖資源放入?Redis?(注意是當(dāng)key不存在時(shí)才能放成功,所以使用?setIfAbsent?方法):
redisTemplate.opsForValue().setIfAbsent("key","value");
設(shè)置過期時(shí)間
redisTemplate.expire("key", 30000,TimeUnit.MILLISECONDS);
釋放鎖
redisTemplate.delete("key");
一般情況下,這樣的實(shí)現(xiàn)就能夠滿足鎖的需求了,但是如果在調(diào)用?setIfAbsent?方法之后線程掛掉了,即沒有給鎖定的資源設(shè)置過期時(shí)間,默認(rèn)是永不過期,那么這個鎖就會一直存在。所以需要保證設(shè)置鎖及其過期時(shí)間兩個操作的原子性,spring data的?RedisTemplate?當(dāng)中并沒有這樣的方法。但是在jedis當(dāng)中是有這種原子操作的方法的,需要通過?RedisTemplate?的?execute?方法獲取到j(luò)edis里操作命令的對象,代碼如下:
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?這些參數(shù)來達(dá)到?setnx?、?setex?、?psetex?命令的效果,文檔參見:http://doc.redisfans.com/string/set.html
NX:表示只有當(dāng)鎖定資源不存在的時(shí)候才能?SET?成功。利用?Redis?的原子性,保證了只有第一個請求的線程才能獲得鎖,而之后的所有線程在鎖定資源被釋放之前都不能獲得鎖。
PX:expire?表示鎖定的資源的自動過期時(shí)間,單位是毫秒。具體過期時(shí)間根據(jù)實(shí)際場景而定
這樣在獲取鎖的時(shí)候就能夠保證設(shè)置?Redis?值和過期時(shí)間的原子性,避免前面提到的兩次?Redis?操作期間出現(xiàn)意外而導(dǎo)致的鎖不能釋放的問題。但是這樣還是可能會存在一個問題,考慮如下的場景順序:
線程T1獲取鎖
線程T1執(zhí)行業(yè)務(wù)操作,由于某些原因阻塞了較長時(shí)間
鎖自動過期,即鎖自動釋放了
線程T2獲取鎖
線程T1業(yè)務(wù)操作完畢,釋放鎖(其實(shí)是釋放的線程T2的鎖)
按照這樣的場景順序,線程T2的業(yè)務(wù)操作實(shí)際上就沒有鎖提供保護(hù)機(jī)制了。所以,每個線程釋放鎖的時(shí)候只能釋放自己的鎖,即鎖必須要有一個擁有者的標(biāo)記,并且也需要保證釋放鎖的原子性操作。
因此在獲取鎖的時(shí)候,可以生成一個隨機(jī)不唯一的串放入當(dāng)前線程中,然后再放入?Redis?。釋放鎖的時(shí)候先判斷鎖對應(yīng)的值是否與線程中的值相同,相同時(shí)才做刪除操作。
Redis?從2.6.0開始通過內(nèi)置的?Lua?解釋器,可以使用?EVAL?命令對?Lua?腳本進(jìn)行求值,文檔參見:http://doc.redisfans.com/script/eval.html
因此我們可以通過?Lua?腳本來達(dá)到釋放鎖的原子操作,定義?Lua?腳本如下:
ifredis.call("get",KEYS[1]) == ARGV[1]thenreturnredis.call("del",KEYS[1])elsereturn0end
具體意思可以參考上面提供的文檔地址
使用?RedisTemplate?執(zhí)行的代碼如下:
// 使用Lua腳本刪除Redis中匹配value的key,可以避免由于方法執(zhí)行時(shí)間過長而redis鎖自動過期失效的時(shí)候誤刪其他線程的鎖// spring自帶的執(zhí)行腳本方法中,集群模式直接拋出不支持執(zhí)行腳本的異常,所以只能拿到原redis的connection來執(zhí)行腳本Longresult = redisTemplate.execute(newRedisCallback() {publicLongdoInRedis(RedisConnection connection) throws DataAccessException {? ? ? ? Object nativeConnection = connection.getNativeConnection();// 集群模式和單機(jī)模式雖然執(zhí)行腳本的方法一樣,但是沒有共同的接口,所以只能分開執(zhí)行// 集群模式if(nativeConnectioninstanceofJedisCluster) {return(Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);? ? ? ? }// 單機(jī)模式elseif(nativeConnectioninstanceofJedis) {return(Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);? ? ? ? }return0L;? ? }});
代碼中分為集群模式和單機(jī)模式,并且兩者的方法、參數(shù)都一樣,原因是spring封裝的執(zhí)行腳本的方法中(?RedisConnection?接口繼承于?RedisScriptingCommands?接口的?eval?方法),集群模式的方法直接拋出了不支持執(zhí)行腳本的異常(雖然實(shí)際是支持的),所以只能拿到?Redis?的connection來執(zhí)行腳本,而?JedisCluster?和?Jedis?中的方法又沒有實(shí)現(xiàn)共同的接口,所以只能分開調(diào)用。
spring封裝的集群模式執(zhí)行腳本方法源碼:
# 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?分布式鎖,但是,在集群模式的極端情況下,還是可能會存在一些問題,比如如下的場景順序(本文暫時(shí)不深入開展):
線程T1獲取鎖成功
Redis?的master節(jié)點(diǎn)掛掉,slave自動頂上
線程T2獲取鎖,會從slave節(jié)點(diǎn)上去判斷鎖是否存在,由于Redis的master slave復(fù)制是異步的,所以此時(shí)線程T2可能成功獲取到鎖
為了可以以后擴(kuò)展為使用其他方式來實(shí)現(xiàn)分布式鎖,定義了接口和抽象類,所有的源碼如下:
# 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 抽象類,實(shí)現(xiàn)基本的方法,關(guān)鍵方法由子類去實(shí)現(xiàn)/**
* @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分布式鎖的實(shí)現(xiàn)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);// 如果獲取鎖失敗,按照傳入的重試次數(shù)進(jìn)行重試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) {// 釋放鎖的時(shí)候,有可能因?yàn)槌宙i之后方法執(zhí)行時(shí)間大于鎖的有效期,此時(shí)有可能已經(jīng)被另外一個線程持有鎖,所以不能直接刪除try{? ? ? ? ? ? List keys =newArrayList();? ? ? ? ? ? keys.add(key);? ? ? ? ? ? List args =newArrayList();? ? ? ? ? ? args.add(lockFlag.get());// 使用lua腳本刪除redis中匹配value的key,可以避免由于方法執(zhí)行時(shí)間過長而redis鎖自動過期失效的時(shí)候誤刪其他線程的鎖// spring自帶的執(zhí)行腳本方法中,集群模式直接拋出不支持執(zhí)行腳本的異常,所以只能拿到原redis的connection來執(zhí)行腳本Long result = redisTemplate.execute(newRedisCallback() {publicLong doInRedis(RedisConnection connection)throwsDataAccessException {ObjectnativeConnection = connection.getNativeConnection();// 集群模式和單機(jī)模式雖然執(zhí)行腳本的方法一樣,但是沒有共同的接口,所以只能分開執(zhí)行// 集群模式if(nativeConnectioninstanceofJedisCluster) {return(Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);? ? ? ? ? ? ? ? ? ? }// 單機(jī)模式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?分布式鎖
在實(shí)際的使用過程中,分布式鎖可以封裝好后使用在方法級別,這樣就不用每個地方都去獲取鎖和釋放鎖,使用起來更加方便。
首先定義個注解:
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";/** 持鎖時(shí)間,單位毫秒*/long keepMills()default30000;/** 當(dāng)獲取失敗時(shí)候動作*/LockFailAction action()defaultLockFailAction.CONTINUE;? ? ? ? publicenumLockFailAction{/** 放棄 */GIVEUP,/** 繼續(xù) */CONTINUE;? ? }/** 重試的間隔時(shí)間,設(shè)置GIVEUP忽略此項(xiàng)*/long sleepMills()default200;/** 重試次數(shù)*/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;? ? ? ? }//得到鎖,執(zhí)行方法,釋放鎖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開發(fā)的項(xiàng)目,直接依賴這個starter,就可以在方法上加?RedisLock?注解來實(shí)現(xiàn)分布式鎖的功能了,當(dāng)然如果需要自己控制,直接注入分布式鎖的bean即可
@AutowiredprivateDistributedLock distributedLock;
如果需要使用其他的分布式鎖實(shí)現(xiàn),繼承?AbstractDistributedLock?后實(shí)現(xiàn)獲取鎖和釋放鎖的方法即可
歡迎歡迎學(xué)Java的朋友們加入java架構(gòu)交流: 855835163
群內(nèi)提供免費(fèi)的Java架構(gòu)學(xué)習(xí)資料(里面有高可用、高并發(fā)、高性能及分布式、Jvm性能調(diào)優(yōu)、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點(diǎn)的架構(gòu)資料)合理利用自己每一分每一秒的時(shí)間來學(xué)習(xí)提升自己,不要再用"沒有時(shí)間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!