Redis分布式鎖如何實現(xiàn)

這篇文章將為大家詳細講解有關redis分布式鎖如何實現(xiàn),小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。

成都創(chuàng)新互聯(lián)服務項目包括濱江網(wǎng)站建設、濱江網(wǎng)站制作、濱江網(wǎng)頁制作以及濱江網(wǎng)絡營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術優(yōu)勢、行業(yè)經(jīng)驗、深度合作伙伴關系等,向廣大中小型企業(yè)、政府機構等提供互聯(lián)網(wǎng)行業(yè)的解決方案,濱江網(wǎng)站推廣取得了明顯的社會效益與經(jīng)濟效益。目前,我們服務的客戶以成都為中心已經(jīng)輻射到濱江省份的部分城市,未來相信會繼續(xù)擴大服務區(qū)域并繼續(xù)獲得客戶的支持與信任!

什么是分布式鎖?

要介紹分布式鎖,首先要提到與分布式鎖相對應的是線程鎖、進程鎖。

線程鎖:主要用來給方法、代碼塊加鎖。當某個方法或代碼使用鎖,在同一時刻僅有一個線程執(zhí)行該方法或該代碼段。線程鎖只在同一JVM中有效果,因為線程鎖的實現(xiàn)在根本上是依靠線程之間共享內存實現(xiàn)的,比如synchronized是共享對象頭,顯示鎖Lock是共享某個變量(state)。

進程鎖:為了控制同一操作系統(tǒng)中多個進程訪問某個共享資源,因為進程具有獨立性,各個進程無法訪問其他進程的資源,因此無法通過synchronized等線程鎖實現(xiàn)進程鎖。

分布式鎖:當多個進程不在同一個系統(tǒng)中,用分布式鎖控制多個進程對資源的訪問。

前言

現(xiàn)在的業(yè)務場景越來越復雜,使用的架構也就越來越復雜,分布式、高并發(fā)已經(jīng)是業(yè)務要求的常態(tài)。像騰訊系的不少服務,還有cdn優(yōu)化、異地多備份等處理。

說到分布式,就必然涉及到分布式鎖的概念,如何保證不同機器不同線程的分布式鎖同步呢?

實現(xiàn)要點

  1. 互斥性,同一時刻,智能有一個客戶端持有鎖。

  2. 防止死鎖發(fā)生,如果持有鎖的客戶端崩潰沒有主動釋放鎖,也要保證鎖可以正常釋放及其他客戶端可以正常加鎖。

  3. 加鎖和釋放鎖必須是同一個客戶端。

  4. 容錯性,只有redis還有節(jié)點存活,就可以進行正常的加鎖解鎖操作。

正確的redis分布式鎖實現(xiàn)

錯誤加鎖方式

錯誤方式一

保證互斥和防止死鎖,首先想到的使用redis的setnx命令保證互斥,為了防止死鎖,鎖需要設置一個超時時間。

 public static void wrongLock(Jedis jedis, String key, String uniqueId, int expireTime) {
  Long result = jedis.setnx(key, uniqueId);
  if (1 == result) {
   //如果該redis實例崩潰,那就無法設置過期時間了
   jedis.expire(key, expireTime);
  }
 }

在多線程并發(fā)環(huán)境下,任何非原子性的操作,都可能導致問題。這段代碼中,如果設置過期時間時,redis實例崩潰,就無法設置過期時間。如果客戶端沒有正確的釋放鎖,那么該鎖(永遠不會過期),就永遠不會被釋放。

錯誤方式二

比較容易想到的就是設置值和超時時間為原子原子操作就可以解決問題。那使用setnx命令,將value設置為過期時間不就ok了嗎?

public static boolean wrongLock(Jedis jedis, String key, int expireTime) {
  long expireTs = System.currentTimeMillis() + expireTime;
  // 鎖不存在,當前線程加鎖成果
  if (jedis.setnx(key, String.valueOf(expireTs)) == 1) {
   return true;
  }

  String value = jedis.get(key);
  //如果當前鎖存在,且鎖已過期
  if (value != null && NumberUtils.toLong(value) < System.currentTimeMillis()) {
   //鎖過期,設置新的過期時間
   String oldValue = jedis.getSet(key, String.valueOf(expireTs));
   if (oldValue != null && oldValue.equals(value)) {
    // 多線程并發(fā)下,只有一個線程會設置成功
    // 設置成功的這個線程,key的舊值一定和設置之前的key的值一致
    return true;
   }
  }
  // 其他情況,加鎖失敗
  return true;
 }

乍看之下,沒有什么問題。但仔細分析,有如下問題:

value設置為過期時間,就要求各個客戶端嚴格的時鐘同步,這就需要使用到同步時鐘。即使有同步時鐘,分布式的服務器一般來說時間肯定是存在少許誤差的。

鎖過期時,使用 jedis.getSet雖然可以保證只有一個線程設置成功,但是不能保證加鎖和解鎖為同一個客戶端,因為沒有標志鎖是哪個客戶端設置的嘛。

錯誤解鎖方式

解鎖錯誤方式一

直接刪除key

public static void wrongReleaseLock(Jedis jedis, String key) {
  //不是自己加鎖的key,也會被釋放
  jedis.del(key);
 }

簡單粗暴,直接解鎖,但是不是自己加鎖的,也會被刪除,這好像有點太隨意了吧!

解鎖錯誤方式二

判斷自己是不是鎖的持有者,如果是,則只有持有者才可以釋放鎖。

 public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) {
  if (uniqueId.equals(jedis.get(key))) {
   // 如果這時鎖過期自動釋放,又被其他線程加鎖,該線程就會釋放不屬于自己的鎖
   jedis.del(key);
  }
 }

看起來很完美啊,但是如果你判斷的時候鎖是自己持有的,這時鎖超時自動釋放了。然后又被其他客戶端重新上鎖,然后當前線程執(zhí)行到jedis.del(key),這樣這個線程不就刪除了其他線程上的鎖嘛,好像有點亂套了哦!

正確加鎖釋放鎖方式

基本上避免了以上幾種錯誤方式之外,就是正確的方式了。要滿足以下幾個條件:

命令必須保證互斥

設置的key必須要有過期時間,防止崩潰時鎖無法釋放

value使用唯一id標志每個客戶端,保證只有鎖的持有者才可以釋放鎖

加鎖直接使用set命令同時設置唯一id和過期時間;其中解鎖稍微復雜些,加鎖之后可以返回唯一id,標志此鎖是該客戶端鎖擁有;釋放鎖時要先判斷擁有者是否是自己,然后刪除,這個需要redis的lua腳本保證兩個命令的原子性執(zhí)行。

下面是具體的加鎖和釋放鎖的代碼:

@Slf4j
public class RedisDistributedLock {
 private static final String LOCK_SUCCESS = "OK";
 private static final Long RELEASE_SUCCESS = 1L;
 private static final String SET_IF_NOT_EXIST = "NX";
 private static final String SET_WITH_EXPIRE_TIME = "PX";
 // 鎖的超時時間
 private static int EXPIRE_TIME = 5 * 1000;
 // 鎖等待時間
 private static int WAIT_TIME = 1 * 1000;
 private Jedis jedis;
 private String key;
 public RedisDistributedLock(Jedis jedis, String key) {
  this.jedis = jedis;
  this.key = key;
 }
 // 不斷嘗試加鎖
 public String lock() {
  try {
   // 超過等待時間,加鎖失敗
   long waitEnd = System.currentTimeMillis() + WAIT_TIME;
   String value = UUID.randomUUID().toString();
   while (System.currentTimeMillis() < waitEnd) {
    String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);
    if (LOCK_SUCCESS.equals(result)) {
     return value;
    }
    try {
     Thread.sleep(10);
    } catch (InterruptedException e) {
     Thread.currentThread().interrupt();
    }
   }
  } catch (Exception ex) {
   log.error("lock error", ex);
  }
  return null;
 }
 public boolean release(String value) {
  if (value == null) {
   return false;
  }
  // 判斷key存在并且刪除key必須是一個原子操作
  // 且誰擁有鎖,誰釋放
  String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  Object result = new Object();
  try {
   result = jedis.eval(script, Collections.singletonList(key),
     Collections.singletonList(value));
   if (RELEASE_SUCCESS.equals(result)) {
    log.info("release lock success, value:{}", value);
    return true;
   }
  } catch (Exception e) {
   log.error("release lock error", e);
  } finally {
   if (jedis != null) {
    jedis.close();
   }
  }
  log.info("release lock failed, value:{}, result:{}", value, result);
  return false;
 }
}

關于“Redis分布式鎖如何實現(xiàn)”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。

分享題目:Redis分布式鎖如何實現(xiàn)
網(wǎng)頁鏈接:http://muchs.cn/article36/pioisg.html

成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站收錄、用戶體驗、搜索引擎優(yōu)化品牌網(wǎng)站制作、小程序開發(fā)、關鍵詞優(yōu)化

廣告

聲明:本網(wǎng)站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經(jīng)允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)

成都定制網(wǎng)站網(wǎng)頁設計