redis分散式鎖的實現方式
前言:分散式鎖的實現方式一般有三種,1:基於資料庫的樂觀鎖。2:基於redis的分散式鎖。3:基於zk的分散式鎖,本文主要介紹第二種實現,由於以前一直是單機寫筆記,所以第一次寫有寫的不好的地方歡迎大家指正。
網上對於redis分散式鎖的實現各有不同,今天分享的這種,不確定是不是最好的,但是個人覺得最易懂,好了廢話不多說,貼公司錯誤例子跟正確寫法。
//錯誤例子
public class RedisLock {
private static final Logger logger = LoggerFactory.getLogger(RedisLockNew.class);
private static final String REDIS_KEY_PREFIX = "redis_key_";
private static final String SET_IF_NOT_EXIST = "NX";//key不存在時,我們進行set操作;若key已經存在,則不做任何操作
private static final String SET_WITH_EXPIRE_TIME = "PX";//給key加一個過期的設定
@Autowired
private JedisCluster jedisCluster;
/**
* @description redis鎖
* @param key
* @param seconds 鎖過期時間(單位:秒)
* @return
*/
public Boolean getLock(String key,Integer seconds) {
if(StringUtils.isBlank(key)){
logger.warn("key({}) cann't be blank(null or empty)!", key);
return false;
}
String redis_key = REDIS_KEY_PREFIX+key;
String value = "1";//value可以任意,但是如果解鎖方式是基於value的,可以設定成唯一標識
Boolean res;
try{
Integer failedStaus = jedisCluster.ttl(redis_key).intValue();//判斷是否過期失效
if(-1 == failedStaus){
jedisCluster.expire(redis_key, seconds);//重新設定過期時間
}
String result = jedisCluster.set(redis_key, value,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,(long) seconds);
res = JedisUtils.isStatusOk(result);
}catch(Exception e){
logger.error("setnx key:{},value:{},seconds:{}", new Object[]{redis_key, value, seconds, e});
return false;
}
return res;
}
/**
* @description 釋放鎖
* @param key
* @param seconds 鎖過期時間(單位:秒)
* @return
*/
public void releaseLock(String key){
String redis_key = REDIS_KEY_PREFIX+key;
jedisCluster.del(redis_key);
logger.info("delete redis_key={}", redis_key);
}
}
上面這段程式碼可以說寫的很不嚴謹,而且有點冗餘,相信細心的朋友已經發現一個問題了,如果存在兩個客戶端,客戶端a在釋放鎖之前剛好鎖失效了,客戶端b獲取了鎖,然後a再執行del操作會導致a刪了b的鎖,所以這裡刪除鎖的操作是不對的,還有一段超時的判斷的程式碼並無實際意義,至少我看不出到底是什麼意思。。。而且缺少重新獲取鎖的操作。。。,下面是我改造後的程式碼。
public class RedisLock {
private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);
private static final String LOCK_SUCCESS = "OK";
private static final String REDIS_KEY_PREFIX = "redis_key_";
private static final String SET_IF_NOT_EXIST = "NX";//key不存在時,我們進行set操作;若key已經存在,則不做任何操作
private static final String SET_WITH_EXPIRE_TIME = "PX";//給key加一個過期的設定
@Autowired
private JedisCluster jedisCluster;
/**
* @description redis鎖
* @param key
* @param seconds 鎖過期時間(單位:秒)
* @return
*/
public Boolean getNonBlockLock(String key,Integer seconds,String requestId) {
if(StringUtils.isBlank(key)){
logger.warn("key({}) cann't be blank(null or empty)!", key);
return false;
}
String redis_key = REDIS_KEY_PREFIX+key;
String result = null;
try{
result = jedisCluster.set(redis_key, requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,(long) seconds);
if(LOCK_SUCCESS != result){//內部嘗試獲取鎖
logger.info(">>> 沒有獲取到鎖,正在嘗試 。。。 <<<");
result = innerTryLock(redis_key, requestId,seconds);
}
}catch(Exception e){
logger.error("setnx key:{},value:{},seconds:{}", new Object[]{redis_key, requestId, seconds, e});
return false;
}
return JedisUtils.isStatusOk(result);
}
/**
* @description 內部嘗試獲取鎖
* @param redis_key
* @param value
* @param seconds
* @return
*/
public String innerTryLock(String redis_key,String requestId,Integer seconds){
String oldVal = jedisCluster.get(redis_key);
String result = "";
if(!StringUtils.isNotBlank(oldVal)){
result = jedisCluster.set(redis_key, requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,(long) seconds);
logger.info(">>> 嘗試之後獲取到了鎖! <<<");
}else{
logger.info(">>> 嘗試之後沒有獲取到鎖! <<<");
}
return result;
}
/**
* @description 釋放鎖
* @param key
* @param seconds 鎖過期時間(單位:秒)
* @return
*/
public void releaseLock(String key,String requestId){
/*String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(requestId));//luna指令碼方式*/
String redis_key = REDIS_KEY_PREFIX+key;
if(requestId.equals(jedisCluster.get(redis_key))){
jedisCluster.del(redis_key);
logger.info("delete redis_key={}", redis_key);
}else{
logger.info("can not del lock cause it is being used,redis_key={}", redis_key);
}
}
}
如果有朋友想在本地做測試的話,我寫了段粗略的測試程式碼,大家可以配合redis客戶端進行測試。
public class testRedisLock {
public static void main(String[] args) {
Jedis jedisCluster=new Jedis("127.0.0.1",6379);
//30 60
Long setnx = jedisCluster.setnx("LOCK_NAME", String.valueOf(System.currentTimeMillis() + 60*1000));
//獲取鎖成功
if(setnx !=null && setnx.intValue()==1){
System.out.println(">>>Task02成功獲取到redis分散式鎖<<<");
System.out.println("執行業務>>>");
}else {
System.out.println("Task02>>>沒有獲取到鎖,正在嘗試<<<");
String lockVal = jedisCluster.get("LOCK_NAME");//獲取當前LOCK_NAME的時間
//如果老的lockVal不為空,並且當前時間已經大於過期鎖過期時間,則證明可以獲取鎖
//100
if(lockVal !=null && System.currentTimeMillis()>Long.parseLong(lockVal)){
//設定LOCK_NAME新的值,返回的是LOCK_NAME老的值
String getOldSetVal = jedisCluster.getSet("LOCK_NAME", String.valueOf(System.currentTimeMillis() +60*1000));
if(getOldSetVal ==null ||(getOldSetVal!=null && StringUtils.equals(lockVal,getOldSetVal))){
System.out.println("Task02>>>嘗試之後獲取到了鎖<<<");
//如果返回的getOldSetVal 為空則證明以前的鎖已經被釋放,則可以重新獲取鎖
//如果返回的getOldSetVal 不為空且lockVal == getOldSetVal ,返回的值和查詢的時間過期值確實相等,那麼證明我確實拿到這把鎖
System.out.println("執行業務>>>2<<<");
}else{
System.out.println("Task02>>>嘗試之後沒有獲取到了鎖>>>");
}
}else{
System.out.println("Task02>>>嘗試之後沒有獲取到了鎖>>>");
}
}
jedisCluster.close();
}
}
第一次寫部落格,還有很多不足,希望大家多多指正,一起進步。