Redis分布式事務(wù)鎖的原理(上)

我們在單機服務(wù)器,出現(xiàn)資源的競爭,一般使用synchronized 就可以解決,但是在分布式的服務(wù)器上,synchronized 就無法解決這個問題,這就需要一個分布式事務(wù)鎖。

除此之外面試,基本會問springboot、Redis,然后都會一路再聊到分布式事務(wù)、分布式事務(wù)鎖的實現(xiàn)。

1、常見的分布式事務(wù)鎖

1、數(shù)據(jù)庫級別的鎖

  • 樂觀鎖,基于加入版本號實現(xiàn)
  • 悲觀鎖,基于數(shù)據(jù)庫的 for update 實現(xiàn)

2、Redis ,基于 SETNX、EXPIRE 實現(xiàn)

3、Zookeeper,基于InterProcessMutex 實現(xiàn)

4、Redisson,lcok、tryLock(背后原理也是Redis)

本文主要介紹一下Redis和Redisson的分布式事務(wù)鎖的原理。

2、Redis 搭建模式

Redis 的搭建方式:

  • 單機
  • 主從
  • 哨兵
  • 集群

單機,只要一臺Redis服務(wù)器,掛了就無法工作了

主從,是備份關(guān)系, 數(shù)據(jù)也會同步到從庫,還可以讀寫分離

哨兵:master掛了,哨兵就行選舉,選出新的master,作用是監(jiān)控主從,主從切換

集群:高可用,分散請求。目的是將數(shù)據(jù)分片存儲,節(jié)省內(nèi)存。

單機:

單機模式

主從:

主從模式

哨兵:

哨兵模式

集群:

集群模式

3、幾個概念

分布式:簡單來說就是將業(yè)務(wù)進行拆分,部署到不同的機器來協(xié)調(diào)處理。比如用戶在網(wǎng)上買東西,大致分為:訂單系統(tǒng)、庫存系統(tǒng)、支付系統(tǒng)、、、、這些系統(tǒng)共同來完成用戶買東西這個業(yè)務(wù)操作。

集群:同一個業(yè)務(wù),通過部署多個實例來完成,保證應(yīng)用的高可用,如果其中某個實例掛了,業(yè)務(wù)仍然可以正常進行,通常集群和分布式配合使用。來保證系統(tǒng)的高可用、高性能。

分布式事務(wù):按照傳統(tǒng)的系統(tǒng)架構(gòu),下單、扣庫存等等,這一系列的操作都是一在一個應(yīng)用一個數(shù)據(jù)庫中完成的,也就是說保證了事務(wù)的ACID特性。如果在分布式應(yīng)用中就會涉及到跨應(yīng)用、跨庫。這樣就涉及到了分布式事務(wù),就要考慮怎么保證這一系列的操作要么都成功要么都失敗。保證數(shù)據(jù)的一致性。

分布式鎖:因為資源有限,要通過互斥來保持一致性,引入分布式事務(wù)鎖。

4、Redis分布式鎖原理

簡單的來說,其實現(xiàn)原理如下:

  • 互斥性

    • 保證同一時間只有一個客戶端可以拿到鎖。
  • 安全性

    • 只有加鎖的服務(wù)才能有解鎖權(quán)限,也就是不能讓客戶端A加的鎖,客戶端B、C 都可以解鎖。
  • 避免死鎖

  • 保證加鎖與解鎖操作是原子性操作

    • 這個其實屬于是實現(xiàn)分布式鎖的問題,假設(shè)a用redis實現(xiàn)分布式鎖
    • 假設(shè)加鎖操作,操作步驟分為兩步:1,設(shè)置key set(key,value) 2,給key設(shè)置過期時間
    • 假設(shè)現(xiàn)在a剛實現(xiàn)set后,程序崩了就導(dǎo)致了沒給key設(shè)置過期時間就導(dǎo)致key一直存在就發(fā)生了死鎖。

講了這么多,Redis實現(xiàn)分布式鎖的核心就是:

加鎖:

SET key value NX EX timeOut

參數(shù)解釋:

NX:只有這個key不存才的時候才會進行操作,即 if not exists;
EX:設(shè)置key的過期時間為秒,具體時間由第5個參數(shù)決定
timeOut:設(shè)置過期時間保證不會出現(xiàn)死鎖【避免宕機死鎖】

代碼實現(xiàn):

 public Boolean lock(String key,String value,Long timeOut){
     String var1 = jedis.set(key,value,"NX","EX",timeOut); //加鎖,設(shè)置超時時間 原子性操作
     if(LOCK_SUCCESS.equals(var1)){
         return true;
     }
     return false;
 }

總的來說,執(zhí)行上面的set()方法就只會導(dǎo)致兩種結(jié)果:

  1. 當(dāng)前沒有鎖(key不存在),那么就進行加鎖操作,并對鎖設(shè)置個有效期,同時value表示加鎖的客戶端。
  2. 已有鎖存在,不做任何操作。

注:從2.6.12版本后, 就可以使用set來獲取鎖、Lua 腳本來釋放鎖。setnx是以前剛開始的實現(xiàn)方式,set命令nx、xx等參數(shù),,就是為了實現(xiàn) setnx 的功能。

解鎖:

代碼實現(xiàn):

public Boolean redisUnLock(String key, String value) {
    String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else  return 0 end";

    Object var2 = jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value));
    if (UNLOCK_SUCCESS == var2) {
        return true;
    }
    return false;
}

這段lua代碼的意思:首先獲取鎖對應(yīng)的value值,檢查是否與輸入的value相等,如果相等則刪除鎖(解鎖)。

上面加鎖、解鎖,看著是挺麻煩的,所以就出現(xiàn)了Redisson。

5、Redisson 分布式鎖原理

官方介紹:

Redisson是一個在Redis的基礎(chǔ)上實現(xiàn)的Java駐內(nèi)存數(shù)據(jù)網(wǎng)格。

就是在Redis的基礎(chǔ)上封裝了很多功能,以便于我們更方便的使用。

只需要三行代碼:

RLock lock = redisson.getLock("myLock");
lock.lock(); //加鎖
lock.unlock(); //解鎖

(1)加鎖機制

加鎖流程:

redisson的lock過程

redisson的lock()、tryLock()方法 底層 其實是發(fā)送一段lua腳本到一臺服務(wù)器:

if (redis.call('exists' KEYS[1]) == 0) then  +  --  exists 判斷key是否存在
       redis.call('hset' KEYS[1] ARGV[2] 1);  +   --如果不存在,hset存哈希表
       redis.call('pexpire' KEYS[1] ARGV[1]);  + --設(shè)置過期時間
       return nil;  +                            -- 返回null 就是加鎖成功
          end;  +
          if (redis.call('hexists' KEYS[1] ARGV[2]) == 1) then  + -- 如果key存在,查看哈希表中是否存在(當(dāng)前線程)
              redis.call('hincrby' KEYS[1] ARGV[2] 1);  + -- 給哈希中的key加1,代表重入1次,以此類推
              redis.call('pexpire' KEYS[1] ARGV[1]);  + -- 重設(shè)過期時間
              return nil;  +
          end;  +
          return redis.call('pttl' KEYS[1]); --如果前面的if都沒進去,說明ARGV[2]的值不同,也就是不是同一線程的鎖,這時候直接返回該鎖的過期時間

參數(shù)解釋:

KEYS[1]:即加鎖的key,RLock lock = redisson.getLock("myLock"); 中的myLock

ARGV[1]:即 TimeOut 鎖key的默認生存時間,默認30秒

ARGV[2]:代表的是加鎖的客戶端的ID,類似于這樣的:99ead457-bd16-4ec0-81b6-9b7c73546469:1

其中l(wèi)ock()默認是30秒的生存時間。

(2)鎖互斥

假如客戶端A已經(jīng)拿到了 myLock,現(xiàn)在 有一客戶端(未知) 想進入:

1、第一個if判斷會執(zhí)行“exists myLock”,發(fā)現(xiàn)myLock這個鎖key已經(jīng)存在了。
2、第二個if判斷,判斷一下,myLock鎖key的hash數(shù)據(jù)結(jié)構(gòu)中, 如果是客戶端A重新請求,證明當(dāng)前是同一個客戶端同一個線程重新進入,所以可從入標(biāo)志+1,重新刷新生存時間(可重入); 否則進入下一個if。
3、第三個if判斷,客戶端B 會獲取到pttl myLock返回的一個數(shù)字,這個數(shù)字代表了myLock這個鎖key的剩余生存時間。比如還剩15000毫秒的生存時間。

此時客戶端B會進入一個while循環(huán),不停的嘗試加鎖。

(3)watch dog 看門狗自動延期機制

官方介紹:

lockWatchdogTimeout(監(jiān)控鎖的看門狗超時,單位:毫秒)

默認值:30000

監(jiān)控鎖的看門狗超時時間單位為毫秒。該參數(shù)只適用于分布式鎖的加鎖請求中未明確使用leaseTimeout參數(shù)的情況。(如果設(shè)置了leaseTimeout那就會自動失效了呀~)

看門狗的時間可以自定義設(shè)置:

config.setLockWatchdogTimeout(30000);

看門狗有什么用呢?

假如客戶端A在超時時間內(nèi)還沒執(zhí)行完畢怎么辦呢? redisson于是提供了這個看門狗,如果還沒執(zhí)行完畢,監(jiān)聽到這個客戶端A的線程還持有鎖,就去續(xù)期,默認是 LockWatchdogTimeout/ 3 即 10 秒監(jiān)聽一次,如果還持有,就不斷的延長鎖的有效期(重新給鎖設(shè)置過期時間,30s)

可以在lock的參數(shù)里面指定:

lock.lock(); //如果不設(shè)置,默認的生存時間是30s,啟動看門狗 
lock.lock(10, TimeUnit.SECONDS);//10秒以后自動解鎖,不啟動看門狗,鎖到期不續(xù)

如果是使用了可重入鎖( leaseTimeout):

lock.tryLock(); //如果不設(shè)置,默認的生存時間是30s,啟動看門狗 
lock.tryLock(100, 10, TimeUnit.SECONDS);//嘗試加鎖最多等待100秒,上鎖以后10秒自動解鎖,不啟動看門狗

這里的第二個參數(shù)leaseTimeout 設(shè)置為 10 就會覆蓋 看門狗的設(shè)置(看門狗無效),在10秒后鎖就自動失效,不會去續(xù)期;如果是 -1 ,就表示 使用看門狗的默認值。

(4)釋放鎖機制

lock.unlock(),就可以釋放分布式鎖。就是每次都對myLock數(shù)據(jù)結(jié)構(gòu)中的那個加鎖次數(shù)減1。

如果發(fā)現(xiàn)加鎖次數(shù)是0了,說明這個客戶端已經(jīng)不再持有鎖了,此時就會用:“del myLock”命令,從redis里刪除這個key。

為了安全,會先校驗是否持有鎖再釋放,防止

  • 業(yè)務(wù)執(zhí)行還沒執(zhí)行完,鎖到期了。(此時沒占用鎖,再unlock就會報錯)
  • 主線程異常退出、或者假死
finally {
            if (rLock.isLocked()) {
                if (rLock.isHeldByCurrentThread()) {
                    rLock.unlock();
                }
            }
        }

(5) 缺點

如果是 主從、哨兵模式,當(dāng)客戶端A 把 myLock這個鎖 keyvalue寫入了 master,此時會異步復(fù)制給slave實例。

萬一在這個主從復(fù)制的過程中 master 宕機了,主備切換,slave 變成了master。

那么這個時候 slave還沒來得及加鎖,此時 客戶端A的myLock的 值是沒有的,客戶端B在請求時,myLock卻成功為自己加了鎖。這時候分布式鎖就失效了,就會導(dǎo)致數(shù)據(jù)有問題。

所以說Redis分布式說最大的缺點就是宕機導(dǎo)致多個客戶端加鎖,導(dǎo)致臟數(shù)據(jù),不過這種幾率還是很小的。


參考:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容