Java 中利用 redis 實現一個分佈式鎖服務

Java 中利用 redis 實現一個分佈式鎖服務

在現代的編程語言中,接觸過多線程編程的程序員多多少少對鎖有必定的瞭解。簡單的說,多線程中的鎖就是在多線程環境下,多個線程對共享資源進行修改的時候,保證共享資源一致性的機制。這裏不展開說。在分佈式環境下,原來的多線程的鎖就無論用了,也就出現了分佈式鎖的需求。所謂分佈式鎖服務也就是在分佈式環境下,保證多個分佈式的服務共享的資源一致性的服務。
在分佈式環境下實現一個分佈式鎖服務並不太容易,須要考慮不少在單進程下的鎖服務不須要考慮的問題。分佈式鎖鎖的實現也有不少。這裏咱們討論在 Java 中經過 redis 來實現。在 GitHub 中的 redisson 項目中已經有開源的實現。可是那個太複雜了。如今咱們來基於單機的 redis 實現一個簡單的分佈式鎖服務。這個服務必須知足下面的要求java

  • 支持當即獲取鎖方式,若是獲取到返回true,獲取不到則返回false;
  • 支持等待獲取鎖方式,若是獲取到,直接返回true,獲取不到在等待一小段時間,在這一小段時間內反覆嘗試,若是嘗試成功,則返回true,等待時間事後還獲取不到則返回false;
  • 不能產生死鎖的狀況;
  • 不能釋放非本身加的鎖;

加鎖

經過 redis 來實現分佈式鎖的加鎖邏輯以下所示:程序員

Java 中利用 redis 實現一個分佈式鎖服務

根據這個邏輯,實現上鎖的核心代碼以下所示:redis

jedis.select(dbIndex);
String key = KEY_PRE + key;
String value = fetchLockValue();
if(jedis.exists(key)){
  jedis.set(key,value);
  jedis.expire(key,lockExpirseTime);
  return value;
}

表面上看這段代碼好像沒有什麼問題,實際上並不能在分佈式環境中正確的實現加鎖的操做。要可以正確的實現加鎖操做,「判斷 key 是否存在」「保存 key-value」「設置 key 的過時時間」這三步操做必須是原子操做。若是不是原子操做,那麼可能會出現下面兩種狀況:apache

  • 「判斷 key 是否存在」得出 key 不存在的結果步驟後,「保存 key-value」步驟前,另外一個客戶端執行一樣的邏輯,而且執行到了「判斷 key 是否存在」步驟,一樣得出了 key 不存在的結果。這樣回致使多個客戶端得到了同一把鎖;編程

  • 在客戶端執行完「保存 key-value」 步驟後,須要設置一個 key 的過時時間,防止客戶端由於代碼質量未解鎖,在或者進程崩潰未解鎖致使的死鎖狀況。在「保存 key-value」步驟以後,「設置 key 的過時時間」步驟以前,可能進程崩潰,致使「設置 key 的過時時間」步驟失敗;

redis 在2.6.12版本以後,對 set 命令進行了擴充,可以規避上面的兩個問題。新版的 redis set 命令的參數以下多線程

SET key value [EX seconds] [PX milliseconds] [NX|XX]

新版的 set 命令增長了 EX 、 PX 、 NX|XX 參數選項。他們的含義以下dom

EX seconds – 設置鍵 key 的過時時間,單位時秒
PX milliseconds – 設置鍵 key 的過時時間,單位時毫秒
NX – 只有鍵 key 不存在的時候纔會設置 key 的值
XX – 只有鍵 key 存在的時候纔會設置 key 的值

這樣,原來的三步操做就能夠在一個 set 的原子操做裏面來完成,規避了上面咱們提到的兩個問題。新版的 redis 加鎖核心代碼修改以下所示:編程語言

jedis = redisConnection.getJedis();
jedis.select(dbIndex);
String key = KEY_PRE + key;
String value = fetchLockValue();
if ("OK".equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {
    return value;
}

解鎖 解鎖的基本流程以下分佈式

Java 中利用 redis 實現一個分佈式鎖服務

根據這個邏輯,在 Java 中解鎖的核心代碼以下所示:ide

jedis.select(dbIndex);
String key = KEY_PRE + key;
if(jedis.exists(key) && value.equals(jedis.get(key))){
    jedis.del(key);
    return true;
}
return false;

和加鎖的時候同樣,key 是否存在、判斷是否本身持有鎖、刪除 key-value 這三步操做須要是原子操做,不然當一個客戶端執行完「判斷是否本身持有鎖」步驟後,得出本身持有鎖的結論,此時鎖的過時時間到了,自動被 redis 釋放了,同時另外一個客戶端又基於這個 key 加鎖成功,若是第一個客戶端還繼續執行刪除 key-value的操做,就將不屬於本身的鎖給釋放了。這顯然是不運行的。在這裏咱們利用 redis 執行 Lua 腳本的能力來解決原子操做的問題。修改後的解鎖核心代碼以下所示:

jedis.select(dbIndex);
String key = KEY_PRE + key;
String command = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
if (1L.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) {
    return true;
}

另外,判斷是否本身持有鎖的機制是用加鎖的時候的 key-value 來判斷當前的 key 的值是否等於本身持有鎖時得到的值。因此加鎖的時候的 value 必須是一個全局惟一的字符串。

完整的代碼以下所示

package com.x9710.common.redis.impl;

import com.x9710.common.redis.LockService;
import com.x9710.common.redis.RedisConnection;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import redis.clients.jedis.Jedis;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.UUID;

/**
 * 分佈式鎖 redis 實現
 *
 * @author 楊高超
 * @since 2017-12-14
 */
public class LockServiceRedisImpl implements LockService {

  private static Log log = LogFactory.getLog(LockServiceRedisImpl.class);

  private static String SET_SUCCESS = "OK";

  private static String KEY_PRE = "REDIS_LOCK_";

  private DateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");

  private RedisConnection redisConnection;

  private Integer dbIndex;

  private Integer lockExpirseTime;

  private Integer tryExpirseTime;

  public void setRedisConnection(RedisConnection redisConnection) {
      this.redisConnection = redisConnection;
  }

  public void setDbIndex(Integer dbIndex) {
      this.dbIndex = dbIndex;
  }

  public void setLockExpirseTime(Integer lockExpirseTime) {
      this.lockExpirseTime = lockExpirseTime;
  }

  public void setTryExpirseTime(Integer tryExpirseTime) {
      this.tryExpirseTime = tryExpirseTime;
  }

  public String lock(String key) {
      Jedis jedis = null;
      try {
          jedis = redisConnection.getJedis();
          jedis.select(dbIndex);
          key = KEY_PRE + key;
          String value = fetchLockValue();
          if (SET_SUCCESS.equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {
              log.debug("Reids Lock key : " + key + ",value : " + value);
              return value;
          }
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          if (jedis != null) {
              jedis.close();
          }
      }
      return null;
  }

  public String tryLock(String key) {

      Jedis jedis = null;
      try {
          jedis = redisConnection.getJedis();
          jedis.select(dbIndex);
          key = KEY_PRE + key;
          String value = fetchLockValue();
          Long firstTryTime = new Date().getTime();
          do {
              if (SET_SUCCESS.equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {
                  log.debug("Reids Lock key : " + key + ",value : " + value);
                  return value;
              }
              log.info("Redis lock failure,waiting try next");
              try {
                  Thread.sleep(100);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          } while ((new Date().getTime() - tryExpirseTime * 1000) < firstTryTime);
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          if (jedis != null) {
              jedis.close();
          }
      }
      return null;
  }

  public boolean unLock(String key, String value) {
      Long RELEASE_SUCCESS = 1L;
      Jedis jedis = null;
      try {
          jedis = redisConnection.getJedis();
          jedis.select(dbIndex);
          key = KEY_PRE + key;
          String command = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
          if (RELEASE_SUCCESS.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) {
              return true;
          }
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          if (jedis != null) {
              jedis.close();
          }
      }
      return false;
  }

/**
 * 生成加鎖的惟一字符串
 *
 * @return 惟一字符串
 */
  private String fetchLockValue() {
      return UUID.randomUUID().toString() + "_" + df.format(new Date());
  }
}

測試代碼

package com.x9710.common.redis.test;

import com.x9710.common.redis.RedisConnection;
import com.x9710.common.redis.impl.LockServiceRedisImpl;

public class RedisLockTest {

  public static void main(String[] args) {
      for (int i = 0; i < 9; i++) {
          new Thread(new Runnable() {
              public void run() {
                  RedisConnection redisConnection = RedisConnectionUtil.create();
                  LockServiceRedisImpl lockServiceRedis = new LockServiceRedisImpl();
                  lockServiceRedis.setRedisConnection(redisConnection);
                  lockServiceRedis.setDbIndex(15);
                  lockServiceRedis.setLockExpirseTime(20);
                  String key = "20171228";
                  String value = lockServiceRedis.lock(key);
                  try {
                      if (value != null) {
                          System.out.println(Thread.currentThread().getName() + " lock key = " + key + " success! ");
                          Thread.sleep(25 * 1000);
                      }else{
                          System.out.println(Thread.currentThread().getName() + " lock key = " + key + " failure! ");
                      }
                  } catch (Exception e) {
                      e.printStackTrace();
                  } finally {
                      if (value == null) {
                          value = "";
                      }
                      System.out.println(Thread.currentThread().getName() + " unlock key = " + key + " " + lockServiceRedis.unLock(key, value));

                  }
              }
          }).start();
      }
  }
}

測試結果

Thread-1 lock key = 20171228 failure! 
Thread-2 lock key = 20171228 failure! 
Thread-4 lock key = 20171228 failure! 
Thread-8 lock key = 20171228 failure! 
Thread-7 lock key = 20171228 failure! 
Thread-3 lock key = 20171228 failure! 
Thread-5 lock key = 20171228 failure! 
Thread-0 lock key = 20171228 failure! 
Thread-6 lock key = 20171228 success! 
Thread-1 unlock key = 20171228 false
Thread-2 unlock key = 20171228 false
Thread-4 unlock key = 20171228 false
Thread-8 unlock key = 20171228 false
Thread-3 unlock key = 20171228 false
Thread-5 unlock key = 20171228 false
Thread-0 unlock key = 20171228 false
Thread-7 unlock key = 20171228 false
Thread-6 unlock key = 20171228 true

從測試結果來看能夠看到,9個線程同時給一個 key 加鎖,只有一個可以成功獲取到鎖,其他的客戶端都沒法獲取到鎖。

後記 

這個代碼裏面還實現了一個 tryLock 的接口。這個主要是客戶端沒法獲取到鎖的時候會在一小段時間內反覆嘗試是否可以獲取到鎖。

寫在最後:

歡迎你們關注個人公衆號【風平浪靜如碼】,海量Java相關文章,學習資料都會在裏面更新,整理的資料也會放在裏面。

以爲寫的還不錯的就點個贊,加個關注唄!點關注,不迷路,持續更新!!!

Java 中利用 redis 實現一個分佈式鎖服務

相關文章
相關標籤/搜索