偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

聊一下 Redis 實(shí)現(xiàn)分布式鎖的八大坑

數(shù)據(jù)庫(kù) Redis
在使用 Redis 實(shí)現(xiàn)分布式鎖時(shí)需要考慮很多的因素,以確保系統(tǒng)正確的使用還有程序的性能。下面我們將探討一下使用Redis實(shí)現(xiàn)分布式鎖時(shí)需要注意的關(guān)鍵點(diǎn)。

在分布式系統(tǒng)中,保證資源的互斥訪問(wèn)是一個(gè)關(guān)鍵的點(diǎn),而 Redis 作為高性能的鍵值存儲(chǔ)系統(tǒng),在分布式鎖這塊也被廣泛的應(yīng)用。然而,在使用 Redis 實(shí)現(xiàn)分布式鎖時(shí)需要考慮很多的因素,以確保系統(tǒng)正確的使用還有程序的性能。

下面我們將探討一下使用Redis實(shí)現(xiàn)分布式鎖時(shí)需要注意的關(guān)鍵點(diǎn)。

首先還是大家都知道,使用 Redis 實(shí)現(xiàn)分布式鎖,是兩步操作,設(shè)置一個(gè)key,增加一個(gè)過(guò)期時(shí)間,所以我們首先需要保證的就是這兩個(gè)操作是一個(gè)原子操作。

1.原子性

在獲取鎖和釋放鎖的過(guò)程中,要保證這個(gè)操作的原子性,確保加鎖操作與設(shè)置過(guò)期時(shí)間操作是原子的。Redis 提供了原子操作的命令,如SETNX(SET if Not eXists)或者 SET 命令的帶有NX(Not eXists)選項(xiàng),可以用來(lái)確保鎖的獲取和釋放是原子的。

String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
    returntrue;
}
returnfalse;

2.鎖的過(guò)期時(shí)間

為了保證鎖的釋放,防止死鎖的發(fā)生,獲取到的鎖需要設(shè)置一個(gè)過(guò)期時(shí)間,也就是說(shuō)當(dāng)鎖的持有者因?yàn)槌霈F(xiàn)異常情況未能正確的釋放鎖時(shí),鎖也會(huì)到達(dá)這個(gè)時(shí)間之后自動(dòng)釋放,避免對(duì)系統(tǒng)造成影響。

try{
  String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
  if ("OK".equals(result)) {
      returntrue;
  }
  returnfalse;
} finally {
    unlock(lockKey);
}

此時(shí)有些朋友可能就會(huì)說(shuō),如果釋放鎖的過(guò)程中,發(fā)生系統(tǒng)異常或者網(wǎng)絡(luò)斷線問(wèn)題,不也會(huì)造成鎖的釋放失敗嗎?

是的,這個(gè)極小概率的問(wèn)題確實(shí)是存在的。所以我們?cè)O(shè)置鎖的過(guò)期時(shí)間就是必須的。當(dāng)發(fā)生異常無(wú)法主動(dòng)釋放鎖的時(shí)候,就需要靠過(guò)期時(shí)間自動(dòng)釋放鎖了。

不管操作成功與否,都要釋放鎖,不能忘了釋放鎖,可以說(shuō)鎖的過(guò)期時(shí)間就是對(duì)忘了釋放鎖的一個(gè)兜底。

3.鎖的唯一標(biāo)識(shí)

在上面對(duì)鎖都加鎖正常的情況下,在鎖釋放時(shí),能正確的釋放自己的鎖嗎,所以每個(gè)客戶端應(yīng)該提供一個(gè)唯一的標(biāo)識(shí)符,確保在釋放鎖時(shí)能正確的釋放自己的鎖,而不是釋放成為其他的鎖。一般可以使用客戶端的ID作為標(biāo)識(shí)符,在釋放鎖時(shí)進(jìn)行比較,確保只有當(dāng)持有鎖的客戶端才能釋放自己的鎖。

如果我們加的鎖沒(méi)有加入唯一標(biāo)識(shí),在多線程環(huán)境下,可能就會(huì)出現(xiàn)釋放了其他線程的鎖的情況發(fā)生。

有些朋友可能就會(huì)說(shuō)了,在多線程環(huán)境中,線程A加鎖成功之后,線程B在線程A沒(méi)有釋放鎖的前提下怎么可以再次獲取到鎖呢?所以也就沒(méi)有釋放其他線程的鎖這個(gè)說(shuō)法。

下面我們看這么一個(gè)場(chǎng)景,如果線程A執(zhí)行任務(wù)需要10s,鎖的時(shí)間是5s,也就是當(dāng)鎖的過(guò)期時(shí)間設(shè)置的過(guò)短,在任務(wù)還沒(méi)執(zhí)行成功的時(shí)候就釋放了鎖,此時(shí),線程B就會(huì)加鎖成功,等線程A執(zhí)行任務(wù)執(zhí)行完成之后,執(zhí)行釋放鎖的操作,此時(shí),就把線程B的鎖給釋放了,這不就出問(wèn)題了嗎。

所以,為了解決這個(gè)問(wèn)題就是在鎖上加入線程的ID或者唯一標(biāo)識(shí)請(qǐng)求ID。對(duì)于鎖的過(guò)期時(shí)間短這個(gè)只能根據(jù)業(yè)務(wù)處理時(shí)間大概的計(jì)算一個(gè)時(shí)間,還有就是看門(mén)狗,進(jìn)行鎖的續(xù)期。

偽代碼如下

if (jedis.get(lockKey).equals(requestId)) {
    jedis.del(lockKey);
    returntrue;
}
returnfalse;

4.鎖非阻塞獲取

非阻塞獲取意味著獲取鎖的操作不會(huì)阻塞當(dāng)前線程或進(jìn)程的執(zhí)行。通常,在嘗試獲取鎖時(shí),如果鎖已經(jīng)被其他客戶端持有,常見(jiàn)的做法是讓當(dāng)前線程或進(jìn)程等待直到鎖被釋放。這種方式稱為阻塞獲取鎖。

相比之下,非阻塞獲取鎖不會(huì)讓當(dāng)前線程或進(jìn)程等待鎖的釋放,而是立即返回獲取鎖的結(jié)果。如果鎖已經(jīng)被其他客戶端持有,那么獲取鎖的操作會(huì)失敗,返回一個(gè)失敗的結(jié)果或者一個(gè)空值,而不會(huì)阻塞當(dāng)前線程或進(jìn)程的執(zhí)行。

非阻塞獲取鎖通常適用于一些對(duì)實(shí)時(shí)性要求較高、不希望阻塞的場(chǎng)景,比如輪詢等待鎖的釋放。當(dāng)獲取鎖失敗時(shí),可以立即執(zhí)行一些其他操作或者進(jìn)行重試,而不需要等待鎖的釋放。

在 Redis 中,可以使用 SETNX 命令嘗試獲取鎖,如果返回成功(即返回1),表示獲取鎖成功;如果返回失?。捶祷?),表示獲取鎖失敗。通過(guò)這種方式,可以實(shí)現(xiàn)非阻塞獲取鎖的操作。

try {
  Long start = System.currentTimeMillis();
  while(true) {
     String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
     if ("OK".equals(result)) {
        if(!exists(path)) {
           mkdir(path);
        }
        returntrue;
     }
     
     long time = System.currentTimeMillis() - start;
      if (time>=timeout) {
          returnfalse;
      }
      try {
          Thread.sleep(50);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
  }
} finally{
    unlock(lockKey,requestId);
}  
returnfalse;

在規(guī)定的時(shí)間范圍內(nèi),假如說(shuō)500ms,自旋不斷獲取鎖,不斷嘗試加鎖。

如果成功,則返回。如果失敗,則休息50ms然后在開(kāi)始重試獲取鎖。如果到了超時(shí)時(shí)間,也就是500ms時(shí),則直接返回失敗。

說(shuō)到了多次嘗試加鎖,在 Redis,分布式鎖是互斥的,假如我們對(duì)某個(gè) key 進(jìn)行了加鎖,如果 該key 對(duì)應(yīng)的鎖還沒(méi)有釋放的話,在使用相同的key去加鎖,大概率是會(huì)失敗的。

下面有這樣一個(gè)場(chǎng)景,需要獲取滿足條件的菜單樹(shù),后臺(tái)程序在代碼中遞歸的去獲取,知道獲取到所有的滿足條件的數(shù)據(jù)。我們要知道,菜單是可能隨時(shí)都會(huì)變的,所以這個(gè)地方是可以加入分布式鎖進(jìn)行互斥的。

后臺(tái)程序在遞歸獲取菜單樹(shù)的時(shí)候,第一層加鎖成功,第二層、第n層 加鎖不久加鎖失敗了嗎?

遞歸中的加鎖偽代碼如下:

privateint expireTime = 1000;

public void fun(int level,String lockKey,String requestId){
  try{
     String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
     if ("OK".equals(result)) {
        if(level<=10){
           this.fun(++level,lockKey,requestId);
        } else {
           return;
        }
     }
     return;
  } finally {
     unlock(lockKey,requestId);
  }
}

如果我們直接使用的話,看起來(lái)問(wèn)題不大,但是真正執(zhí)行程序之后,就會(huì)發(fā)現(xiàn)報(bào)錯(cuò)啦。

因?yàn)閺母?jié)點(diǎn)開(kāi)始,第一層遞歸加鎖成功之后,還沒(méi)有釋放這個(gè)鎖,就直接進(jìn)入到了第二層的遞歸之中。因?yàn)殒i名為lockKey,并且值為requestId的鎖已經(jīng)存在,所以第二層遞歸大概率會(huì)加鎖失敗,最后就是返回結(jié)果,只有底層遞歸的結(jié)果返回了。

所以,我們還需要一個(gè)可重入的特性。

5.可重入

redisson 框架中已經(jīng)實(shí)現(xiàn)了可重入鎖的功能,所以我們可以直接使用:

privateint expireTime = 1000;

public void run(String lockKey) {
  RLock lock = redisson.getLock(lockKey);
  this.fun(lock,1);
}

public void fun(RLock lock,int level){
  try{
      lock.lock(5, TimeUnit.SECONDS);
      if(level<=10){
         this.fun(lock,++level);
      } else {
         return;
      }
  } finally {
     lock.unlock();
  }
}

上述的代碼僅供參考,這也只是提供一個(gè)思路。

下面我們還是聊一下 redisson 可重入鎖的原理。

加鎖主要通過(guò)以下代碼實(shí)現(xiàn)的。

if (redis.call('exists', KEYS[1]) == 0) 
then  
   redis.call('hset', KEYS[1], ARGV[2], 1);        
   redis.call('pexpire', KEYS[1], ARGV[1]); 
   return nil; 
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) 
then  
  redis.call('hincrby', KEYS[1], ARGV[2], 1); 
  redis.call('pexpire', KEYS[1], ARGV[1]); 
  return nil; 
end;
return redis.call('pttl', KEYS[1]);
  • KEYS[1]:鎖名
  • ARGV[1]:過(guò)期時(shí)間
  • ARGV[2]:uuid + ":" + threadId,可認(rèn)為是requestId

(1) 先判斷如果加鎖的key不存在,則加鎖。

(2) 接下來(lái)判斷如果key和requestId值都存在,則使用hincrby命令給該key和requestId值計(jì)數(shù),每次都加1。注意一下,這里就是重入鎖的關(guān)鍵,鎖重入一次值就加1。

(3) 如果當(dāng)前 key 存在,但值不是 requestId ,則返回過(guò)期時(shí)間。

釋放鎖的腳本如下:

if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) 
then 
  return nil
end
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) 
then 
    redis.call('pexpire', KEYS[1], ARGV[2]); 
    return0; 
 else
   redis.call('del', KEYS[1]); 
   redis.call('publish', KEYS[2], ARGV[1]); 
   return1; 
end; 
return nil
  • 先判斷如果 鎖名key 和 requestId 值不存在,則直接返回。
  • 如果 鎖名key 和 requestId 值存在,則重入鎖減1。
  • 如果減1后,重入鎖的 value 值還大于0,說(shuō)明還有引用,則重試設(shè)置過(guò)期時(shí)間。
  • 如果減1后,重入鎖的 value 值還等于0,則可以刪除鎖,然后發(fā)消息通知等待線程搶鎖。

6.鎖競(jìng)爭(zhēng)

對(duì)于大量寫(xiě)入的業(yè)務(wù)場(chǎng)景,使用普通的分布式鎖就可以實(shí)現(xiàn)我們的需求。但是對(duì)于寫(xiě)入操作少的,有大量讀取操作的業(yè)務(wù)場(chǎng)景,直接使用普通的redis鎖就會(huì)浪費(fèi)性能了。所以對(duì)于鎖的優(yōu)化來(lái)說(shuō),我們就可以從業(yè)務(wù)場(chǎng)景,讀寫(xiě)鎖來(lái)區(qū)分鎖的顆粒度,盡可能將鎖的粒度變細(xì),提升我們系統(tǒng)的性能。

(1) 讀寫(xiě)鎖

對(duì)于降低鎖的粒度,上面我們知道了讀寫(xiě)鎖也算事在業(yè)務(wù)層面進(jìn)行降低鎖粒度的一種方式,所以下面我們以 redisson 框架為例,看看實(shí)現(xiàn)讀寫(xiě)鎖是如何實(shí)現(xiàn)的。

讀鎖:

RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.readLock();
try {
    rLock.lock();
    //業(yè)務(wù)操作
} catch (Exception e) {
    log.error(e);
} finally {
    rLock.unlock();
}

寫(xiě)鎖:

RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.writeLock();
try {
    rLock.lock();
    //業(yè)務(wù)操作
} catch (InterruptedException e) {
   log.error(e);
} finally {
    rLock.unlock();
}

通過(guò)講鎖分為讀鎖與寫(xiě)鎖,最大的提升之后就在與大大的提高系統(tǒng)的讀性能,因?yàn)樽x鎖與讀鎖之間是沒(méi)有沖突的,不存在互斥,然后又因?yàn)闃I(yè)務(wù)系統(tǒng)中的讀操作是遠(yuǎn)遠(yuǎn)多與寫(xiě)操作的,所以我們?cè)谔嵘俗x鎖的性能的同時(shí),系統(tǒng)整體鎖的性能都得到了提升。

讀寫(xiě)鎖特點(diǎn):

  • 讀鎖與讀鎖不互斥,可共享
  • 讀鎖與寫(xiě)鎖互斥
  • 寫(xiě)鎖與寫(xiě)鎖互斥

(2) 分段鎖

上面我們通過(guò)業(yè)務(wù)層面的讀寫(xiě)鎖進(jìn)行了鎖粒度的減小,下面我們?cè)谕ㄟ^(guò)鎖的分段減少鎖粒度實(shí)現(xiàn)鎖性能的提升。

如果你對(duì) concurrentHashMap 的源碼了解的話你就會(huì)知道分段鎖的原理了。是的就是你想的那樣,把一個(gè)大的鎖劃分為多個(gè)小的鎖。

舉個(gè)例子,假如我們?cè)诿霘?00個(gè)商品,那么常規(guī)做法就是一個(gè)鎖,鎖 100個(gè)商品,那么分段的意思就是,將100個(gè)商品分成10份,相當(dāng)于有 10 個(gè)鎖,每個(gè)鎖鎖定10個(gè)商品,這也就提升鎖的性能提升了10倍。

具體的實(shí)現(xiàn)就是,在秒殺的過(guò)程中,對(duì)用戶進(jìn)行取模操作,算出來(lái)當(dāng)前用戶應(yīng)該對(duì)哪一份商品進(jìn)行秒殺。

通過(guò)上述將大鎖拆分為小鎖的過(guò)程,以前多個(gè)線程只能爭(zhēng)搶一個(gè)鎖,現(xiàn)在可以爭(zhēng)搶10個(gè)鎖,大大降低了沖突,提升系統(tǒng)吞吐量。

不過(guò)需要注意的就是,使用分段鎖確實(shí)可以提升系統(tǒng)性能,但是相對(duì)應(yīng)的就是編碼難度的提升,并且還需要引入取模等算法,所以我們?cè)趯?shí)際業(yè)務(wù)中,也要綜合考慮。

7.鎖超時(shí)

在上面我們也說(shuō)過(guò)了,因?yàn)闃I(yè)務(wù)執(zhí)行時(shí)間太長(zhǎng),導(dǎo)致鎖自動(dòng)釋放了,也就是說(shuō)業(yè)務(wù)的執(zhí)行時(shí)間遠(yuǎn)遠(yuǎn)大于鎖的過(guò)期時(shí)間,這個(gè)時(shí)候 Redis 會(huì)自動(dòng)釋放該鎖。

針對(duì)這種情況,我們可以使用鎖的續(xù)期,增加一個(gè)定時(shí)任務(wù),如果到了超時(shí)時(shí)間,業(yè)務(wù)還沒(méi)有執(zhí)行完成,就需要對(duì)鎖進(jìn)行一個(gè)續(xù)期。

Timer timer = new Timer(); 
timer.schedule(new TimerTask() {
    @Override
    public void run(Timeout timeout) throws Exception {
      //自動(dòng)續(xù)期邏輯
    }
}, 10000, TimeUnit.MILLISECONDS);

獲取到鎖之后,自動(dòng)的開(kāi)啟一個(gè)定時(shí)任務(wù),每隔 10s 中自動(dòng)刷新一次過(guò)期時(shí)間。這種機(jī)制就是上面我們提到過(guò)的看門(mén)狗。

對(duì)于自動(dòng)續(xù)期操作,我們還是推薦使用 lua 腳本來(lái)實(shí)現(xiàn):

if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
   redis.call('pexpire', KEYS[1], ARGV[1]);
  return1; 
end;
return0;

需要注意的一點(diǎn)就是,鎖的續(xù)期不是一直續(xù)期的,業(yè)務(wù)如果一直執(zhí)行不完,到了一個(gè)總的超時(shí)時(shí)間,或者執(zhí)行續(xù)期的次數(shù)超過(guò)幾次,我們就不再進(jìn)行續(xù)期操作了。

上面我們講了這么幾個(gè)點(diǎn),下面我們來(lái)說(shuō)一下 Redis 集群中的問(wèn)題,如果發(fā)生網(wǎng)絡(luò)分區(qū),主從切換問(wèn)題,那么該怎么解決呢?

8.網(wǎng)絡(luò)分區(qū)

假設(shè) Redis 初始還是主從,一主三從模式。

Redis 的加鎖操作都是在 master 上操作,成功之后異步不同到 slave上。

當(dāng) master 宕機(jī)之后,我們就需要在三個(gè)slave中選舉一個(gè)出來(lái)當(dāng)作 master ,假如說(shuō)我們選了slave1。

現(xiàn)在有一個(gè)鎖A進(jìn)行加鎖,正好加鎖到 master上,然后 master 還沒(méi)有同步到 slave 上,master 就宕機(jī)了,此時(shí),后面在來(lái)新的線程獲取鎖A,也是可以加鎖成功的,所以分布式鎖也就失效了。

Redisson 框架為了解決這個(gè)問(wèn)題,提供了一個(gè)專門(mén)的類,就是 RedissonRedLock,使用 RedLock 算法。

RedissonRedLock 解決問(wèn)題的思路就是多搭建幾個(gè)獨(dú)立的 Redisson 集群,采用分布式投票算法,少數(shù)服從多數(shù)這種。假如有5個(gè) Redisson 集群,只要當(dāng)加鎖成功的集群有5/2+1個(gè)節(jié)點(diǎn)加鎖成功,意味著這次加鎖就是成功的。

  • 搭建幾套相互獨(dú)立的 Redis 環(huán)境,我們這里搭建5套。
  • 每套環(huán)境都有一個(gè) redisson node 節(jié)點(diǎn)。
  • 多個(gè) redisson node 節(jié)點(diǎn)組成 RedissonRedLock。
  • 環(huán)境包括單機(jī)、主從、哨兵、集群,可以一種或者多種混合都可以。

我們這個(gè)例子以主從為例來(lái)說(shuō)

RedissonRedLock 加鎖過(guò)程如下:

  • 向當(dāng)前5個(gè) Redisson node 節(jié)點(diǎn)加鎖。
  • 如果有3個(gè)節(jié)點(diǎn)加鎖成功,那么整個(gè) RedissonRedLock 就是加鎖成功的。
  • 如果小于3個(gè)節(jié)點(diǎn)加鎖成功,那么整個(gè)加鎖操作就是失敗的。
  • 如果中途各個(gè)節(jié)點(diǎn)加鎖的總耗時(shí),大于等于設(shè)置的最大等待時(shí)間,直接返回加鎖失敗。

通過(guò)上面這個(gè)示例可以發(fā)現(xiàn),使用 RedissonRedLock 可以解決多個(gè)示例導(dǎo)致的鎖失效的問(wèn)題。但是帶來(lái)的也是整個(gè) Redis 集群的管理問(wèn)題:

  • 管理多套 Redis 環(huán)境
  • 增加加鎖的成本。有多少個(gè) Redisson node就需要加鎖多少次。

由此可見(jiàn)、在實(shí)際的高并發(fā)業(yè)務(wù)中,RedissonRedLock 的使用并不多。

在分布式系統(tǒng)中,CAP 理論應(yīng)該都是知道的,所以我們?cè)谶x擇分布式鎖的時(shí)候也可以參考這個(gè)。

  • C(Consistency) 一致性
  • A(Acailability) 可用性
  • P(Partition tolerance)分區(qū)容錯(cuò)性

所以如果我們的業(yè)務(wù)場(chǎng)景,更需要數(shù)據(jù)的一致性,我們可以使用 CP 的分布式鎖,例子 zookeeper。

如果我們更需要的是保證數(shù)據(jù)的可用性,那么我們可以使用 AP 的分布式鎖,例如 Redis。

其實(shí)在我們絕大多數(shù)的業(yè)務(wù)場(chǎng)景中,使用Redis已經(jīng)可以滿足,因?yàn)閿?shù)據(jù)的不一致,我們還可以使用 BASE 理論的最終一致性方案解決。因?yàn)槿绻到y(tǒng)不可用了,對(duì)用戶來(lái)說(shuō)體驗(yàn)肯定不是那么好的。

責(zé)任編輯:趙寧寧 來(lái)源: 醉魚(yú)Java
相關(guān)推薦

2021-09-26 09:16:45

RedisGeo 類型數(shù)據(jù)類型

2022-12-18 20:07:55

Redis分布式

2017-07-21 07:37:20

2022-01-06 10:58:07

Redis數(shù)據(jù)分布式鎖

2023-08-21 19:10:34

Redis分布式

2020-07-30 09:35:09

Redis分布式鎖數(shù)據(jù)庫(kù)

2019-06-19 15:40:06

分布式鎖RedisJava

2020-01-17 09:07:14

分布式系統(tǒng)網(wǎng)絡(luò)

2019-02-26 09:51:52

分布式鎖RedisZookeeper

2024-10-07 10:07:31

2022-11-14 07:23:32

RedisJedis分布式鎖

2023-03-01 08:07:51

2024-04-01 05:10:00

Redis數(shù)據(jù)庫(kù)分布式鎖

2020-04-09 10:25:37

Redis分布式算法

2019-12-25 14:35:33

分布式架構(gòu)系統(tǒng)

2020-07-15 16:50:57

Spring BootRedisJava

2023-10-11 09:37:54

Redis分布式系統(tǒng)

2021-11-01 12:25:56

Redis分布式

2024-11-28 15:11:28

2024-05-08 10:20:00

Redis分布式
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)