Redis-10-分散式鎖.md

羊37發表於2024-06-08

參考:

分散式鎖介紹

1.概念

額,為什麼的話,建議先了解下我這篇文章。

Java-併發-併發的基本概念

我們在併發場景下,區分一個場景是否有併發問題,個人理解,鎖的場景需要考慮:

  • 共享:是否共享某個資源
  • 競態:如何構建競態關係

首先,我們得拎清楚它到底會不會共享,不是說多執行緒它就必然要有併發問題。

比如,上面連結文章中的例子,我開多個執行緒不停發請求,資料都不涉及到共享,它就不會有併發問題。

然後,對於併發場景,我們大體上是樂觀鎖和悲觀鎖兩種思想,他們都不是具體的鎖,只是一種思想。

  • 樂觀鎖:樂觀的態度,我覺得不一定會出現問題。

  • 悲觀鎖:悲觀的態度,我覺得一定有問題,我得預防好。

這裡,可以參考我這篇文章中的例子:Java-併發-synchronized

比如,我們線上編輯釘釘的表格,因為主管通知我們把手底下負責的廠商同事人天單價從一千更新到一千三。

我們剛準備改,此時,領導突然找我們有事情。

  • 樂觀鎖:沒想啥先去找領導了,回來一看,哇靠,剛才表裡一千誰給我更新成一千萬了,肯定誰動我電腦了。
  • 悲觀鎖:md我怕有人亂動我電腦,我找個人給我守著,休想亂動。

當然,樂觀悲觀一定有好壞嗎?不一定,都是結合場景來的。

然後,基於悲觀鎖,從例子中也可以思考到,我們的目的都是強制佔用某個資源

又回到了初學時經典的上廁所例子,對吧,我們必須要把門鎖了,才能安心。

Java中,實現鎖的機制有很多,比如基礎的synchronized,又比如後面來了個ReentrantLock

但是他們都存在一個問題,就是始終只能在我們的JVM中,比如一個是Java程式,一個是Python程式,我搞不了啊?

分散式場景下也是一樣的,核心原因就在於,咱們鎖這個資源,它共享不了了。

有辦法嗎?當然就是標題,分散式鎖,我們找一個同時能訪問的第三者,構建出一個共享+競態的條件。

就比如我們的Redis。嗯,終於扯出來了。

一個最基本的分散式鎖需要滿足:

  • 互斥(競態):任意一個時刻,鎖只能被一個執行緒持有。
  • 可重入:一個節點獲取了鎖之後,還可以再次獲取鎖。
  • 高可用:鎖服務是高可用的,當一個鎖服務出現問題,能夠自動切換到另外一個鎖服務。並且,即使客戶端的釋放鎖的程式碼邏輯出現問題,鎖最終一定還是會被釋放,不會影響其他執行緒對共享資源的訪問。這一般是透過超時機制實現的。

除了基本條件外,最好再具備以下兩點特性:

  • 高效能:獲取和釋放鎖的操作應該快速完成,並且不應該對整個系統的效能造成過大影響。
  • 非阻塞:如果獲取不到鎖,不能無限期等待,避免對系統正常執行造成影響。

常見的分散式鎖實現方案:

  • 基於Redis
  • 基於ZooKeeper
  • ...

本文著重介紹Redis下的分散式鎖。

2.實現

2.1 簡單原型

構建Redis分散式鎖,最簡單的場景即是使用String型別的SETNX命令。

SETNX 的全稱是"SET if Not eXists",即"如果不存在則設定"。

image-20240607140018568

語法:

SETNX key value
# key:要設定的鍵
# value:要設定的值

返回:

# 返回1:鍵不存在並且設定了鍵值對
# 返回0:鍵已經存在

image-20240607135415869

2.1.1 加鎖

直接使用setnx k v即可。

image-20240607135620648

2.1.2 解鎖

使用del命令刪除對應k即可。

image-20240607135633188

2.2 鎖有效期

為了避免鎖無法釋放,我們一般都要給鎖設定一個過期時間(注意原子性)。

redis的set命令支援這個場景

SET key value NX EX 3 
  • SET key value:設定鍵 Key 的值為 value

  • NX:僅在鍵不存在時設定鍵。這確保了只有在 lockKey 不存在時,才會設定它的值,從而實現互斥鎖的效果。

  • EX 3:設定鍵的過期時間為 3 秒。這確保了即使客戶端在持有鎖期間崩潰,鎖也會在 3 秒後自動釋放,防止死鎖。

SET命令語法詳細解釋

SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]

# 比較抽象是吧 []代表可選 可選引數名字對了就行 順序不重要
# [NX | XX]:表示可以是NX或者XX
# [GET]
# [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]
# 表示可以是:EX PX EXAT PXAT KEEPTTL
引數 說明 示例 結果
NX 僅在鍵不存在時設定鍵 SET mykey "value" NX 只有在 mykey 不存在時,才會設定值為 "value"
XX 僅在鍵已經存在時設定鍵 SET mykey "value" XX 只有在 mykey 已經存在時,才會設定值為 "value"
GET 設定新值並返回設定之前的舊值 SET mykey "newvalue" GET 返回設定前的值,並將 mykey 的值設定為 "newvalue"
EX seconds 設定鍵的過期時間為 seconds SET mykey "value" EX 10 mykey 將在 10 秒後過期
PX milliseconds 設定鍵的過期時間為 milliseconds 毫秒 SET mykey "value" PX 10000 mykey 將在 10000 毫秒(10 秒)後過期
EXAT unix-time-seconds 設定鍵的過期時間為 Unix 時間戳 unix-time-seconds SET mykey "value" EXAT 1672531199 mykey 將在指定的 Unix 時間戳過期
PXAT unix-time-milliseconds 設定鍵的過期時間為 Unix 時間戳 unix-time-milliseconds `SET mykey "value" PXAT 1672531199000 mykey 將在指定的 Unix 時間戳過期(以毫秒為單位)
KEEPTTL 保留鍵的現有 TTL(Time to Live) SET mykey "value" KEEPTTL mykey 的值被設定為 "value",但保持原有的過期時間不變

SpringBoot程式示例,接入的話,可以參考我這篇文章:Redis-12-SpringBoot整合Redis哨兵模式

package cn.yang37.za.controller;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Slf4j
@SpringBootTest
class RedisControllerTest {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Test
    void name1() {
        final String key = "yang37";

        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "123", 600, TimeUnit.SECONDS);
        log.info("flag: {}", flag);

        String value = stringRedisTemplate.opsForValue().get(key);
        log.info("value: {}", value);

        Long expire = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
        log.info("expire: {}", expire);
    }
}

image-20240607173007403

再次執行,因為key已經存在,set將返回false。

image-20240607173045412

image-20240607173108760

2.3 鎖的續期

上面講了為了避免鎖不能釋放,一般我們都會設定有效期。

  • 時間設定短

比如,我的事務平時執行10s妥妥夠了,我沒細想,給有效期設定的15s。

有天網比較卡,這個事務執行了30s之久,我都準備去走解鎖的程式碼了,結果,告訴我,鎖已經沒了。

沒了就沒了唄,反正它釋放了,我沒解鎖就算了。但是,但是!你忘了我們加鎖的本心了嗎?確保當前事務能獨佔資源。

在15s後,鎖自動釋放,別人搶到鎖了,它來搞你的的獨佔資源了!

  • 時間設定長

哈哈,那我直設定個三萬八千秒。嗯,那我們設定有效期的本心又又丟失了,假設當前執行緒出問題沒釋放。鎖又不釋放,其他人拿不到鎖,大家都別玩了?

所以,難就難在,這個有效期我們是很難把控的。

理想的狀態是,如果操作共享資源的操作還未完成,鎖過期時間能夠自己續期就好了

Java已經有了現成的解決方案:Redisson

2.3.1 Redisson

基本框架搭建可以參考:Redis-12-SpringBoot整合Redis哨兵模式(Lettuce或Redisson)

先來搭建一個基本demo。

@Slf4j
@RestController
@RequestMapping("/redis")
public class RedisController {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/set/{key}/{value}/{runTime}")
    public String do1(@PathVariable String key, @PathVariable String value, @PathVariable Integer runTime) {
        log.info("key: {},value: {}", key, value);
        Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30, TimeUnit.SECONDS);
        // 拿到鎖
        if (Boolean.TRUE.equals(ifAbsent)) {
            // 模擬業務執行n秒
            exec(runTime);
        }

        log.info("end...");
        return "ok";
    }

    private static void exec(Integer runTime) {
        try {
            for (int i = 0; i <= runTime; i++) {
                log.info("running {} s", i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            log.error("sleep exception!", e);
        }
    }

}

這個例子中,get請求的路徑為:

/redis/set/{key}/{value}/{runTime}

其中,鎖時間固定為30秒,runTime模擬實際業務執行,路徑傳入。

  • 正常情況下,我們傳入一個執行時間小於30s的,例如20s。

    image-20240608143730608

image-20240608143907053

http-nio-9595-exec-7執行的期間內,其他請求哪怕進來,如ttp-nio-9595-exec-6,由於拿不到鎖,也執行不了exce()業務n秒的操作。

image-20240608143946421

  • 鎖超時情況下,我們傳入一個執行時間大於30s的,例如50s。

image-20240608144142635

image-20240608144158174

在30s後,此時http-nio-9595-exec-3exec方法還未執行完成,再發起一個請求。

image-20240608144222762

由於鎖已經自動過期了,新請求http-nio-9595-exec-2將能拿到鎖,並開始執行exec方法,兩個請求的exec方法同時開始執行,產生潛在的併發問題。

image-20240608144430835

1.使用

使用Redission進行鎖續期的時候,無非就是我們把exec的執行時間設定的比較長,然後觀察執行期間,鎖是否會到期,即有沒有自動續期。

更新下程式碼,由於用到鎖,改為使用RedissonClient。

@Slf4j
@RestController
@RequestMapping("/redis")
public class RedisController {

    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/set/{key}/{value}/{runTime}")
    public String do1(@PathVariable String key, @PathVariable String value, @PathVariable Integer runTime) {
        RLock lock = redissonClient.getLock(key);

        try {
            // 嘗試獲取鎖,等待時間為10秒
            boolean isLocked = lock.tryLock(10, TimeUnit.SECONDS);
            if (isLocked) {
                try {
                    // 模擬業務執行n秒
                    exec(runTime);
                } finally {
                    lock.unlock();
                }
            } else {
                log.info("Unable to acquire lock");
            }
        } catch (InterruptedException e) {
            log.error("run error", e);
            Thread.currentThread().interrupt();
        }

        return "ok";
    }

    private static void exec(Integer runTime) {
        try {
            for (int i = 0; i <= runTime; i++) {
                log.info("running {} s", i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            log.error("sleep exception!", e);
            Thread.currentThread().interrupt();
        }
    }
}

使用很簡單啊,就是:

		RLock lock = redissonClient.getLock(key);
		// 注意啊,這裡根本沒傳redis中的過期時間,10s是咱們springboot里程式嘗試獲取鎖時的超時時間。
 		boolean isLocked = lock.tryLock(10, TimeUnit.SECONDS);

執行一下,我們把執行時間傳大一點。

image-20240608150647673

程式執行60s,開啟redis看下,注意這裡的過期時間25s。

image-20240608150701676

重新整理下,時間增加到29s了。

image-20240608150806866

可以看到,這個鎖的過期時間,確實是在自動的更新。

所以,你應該大概有個概念了。

  • 使用鎖續期的時候,沒有傳入鎖過期時間,Redission每次預設增加一個30s的過期時間。
  • 程式執行期間,Redission會不停的執行,幫我們不停的把過期時間重新整理到30s,直到我們的業務程式碼執行完成(完成了肯定就是觸發釋放鎖了唄)。

好,這裡要注意一下Redission的tryLock方法,是有好幾個過載方法的。

需要自動續期的話,我們要使用這一個。

image-20240608152621078

你可能疑惑,用鎖的時候,咋還有個嘗試獲取鎖的超時時間?

嗯,這不就是咱們synchronized的缺點之一嗎,鎖的獲取不能中斷,即後面的升級版ReentrantLock為什麼要有tryLock的形式。

假設我們是使用者,我發起一個請求,你的程式碼裡鎖拿不到就死等,然後呢,那邊拿到鎖的邏輯執行時間又長,就阻塞著?跟你傻等一小時?

image-20240608153144629

你可以閱讀下我這些文章:

Java-併發-synchronized

Java-併發-wait()、notify()和notifyAll()以及執行緒的狀態

Java-併發-ReentrantLock

最後,總結下Redission的lock和tryLock方法。

只有未指定redis中的鎖超時時間leaseTime,才會使用到自動續期機制。

方法簽名 引數 描述 鎖是否續期
lock() 立即獲取鎖,如果鎖不可用,則阻塞直到鎖可用。 是(透過 watchdog 自動續期,預設30秒)
lock(long leaseTime, TimeUnit unit) leaseTime:鎖時間
unit:時間單位
獲取鎖,並在指定租期時間後自動釋放。
tryLock() 立即嘗試獲取鎖,如果鎖不可用,則返回 false。 是(透過 watchdog 自動續期,預設30秒)
tryLock(long time, @NotNull TimeUnit unit) time:等待鎖最長時間
unit:時間單位
在指定等待時間內嘗試獲取鎖,鎖將一直保持直到顯式解鎖。 是(透過 watchdog 自動續期,預設30秒)
tryLock(long waitTime, long leaseTime, TimeUnit unit) waitTime:等待鎖最長時間
leaseTime:鎖時間
unit:時間單位
在指定等待時間內嘗試獲取鎖,並在獲得鎖後保持指定的租期。

2.原理

Redission的鎖續期是依賴一個看門狗(Watch Dog)的機制。

看門狗專門用來監控和續期鎖,如果操作共享資源的執行緒還未執行完成的話,看門狗會不斷地延長鎖的過期時間,進而保證鎖不會因為鎖自動到期而被釋放。

嗯,機制說起來很簡單哈,就是我不停的檢查你執行的程式碼,看有沒有完成,沒完成我就給你把對應的鎖時間更新掉。

  • 怎麼建立看門狗?
  • 怎麼續期?

image-20240608155558169

2.1 鎖的格式

注意我們前面的程式碼中的value欄位沒用上了,灰色的,我們獲取鎖的時候只傳了個鍵名。

		// yang37
		RLock lock = redissonClient.getLock(key);
        boolean isLocked = lock.tryLock(10, TimeUnit.SECONDS);

image-20240608173637436

嗯,可以看到,在Redis中,Redission就是根據我們傳入的key,自動建立了一個Hash型別的鍵。

是在 Redisson 中,每個 Java 程序(每個 Redisson 客戶端例項)都會生成一個唯一的 ID。這個 ID 是在客戶端啟動時生成的,用於唯一標識該客戶端例項。

哎呀,就是你程式不是起碼兩個節點嗎?都76號執行緒咋辦,機器1上的76執行緒和機器2上的76執行緒能一樣呀?那肯定要標明具體哪個呀,就這個用處。

# 客戶端例項ID + 執行緒 ID 
key: e0da9704-ebb4-4e9f-a6e4-ca490f176c42:76
# 可重入次數
value: 1
2.2 怎麼建立看門狗

Redisson 的看門狗機制並不是透過一個顯式的 "建立看門狗" 方法來實現的,而是透過在鎖的獲取和持有過程中自動啟動的定時任務來完成的。

定時任務由 scheduleExpirationRenewal 方法啟動。

好,我們以lock()方法為例。

lock -> tryAcquire

image-20240608171259398

tryAcquire -> tryAcquireAsync

image-20240608171315826

tryAcquireAsync -> scheduleExpirationRenewal

框框裡,也指明瞭自動續期的情況。

  • 固定租期:如果指定了 leaseTime,則鎖會在該固定時間後自動釋放,無需看門狗機制。
  • 自動續期:如果未指定 leaseTime(即else),則啟動看門狗機制,自動續期鎖的過期時間,確保鎖在持有期間不會因超時而被釋放。

image-20240608171353448

scheduleExpirationRenewal -> renewExpiration

最後,在scheduleExpirationRenewal方法中觸發了renewExpiration方法,即我們下一節講的,具體的續期操作。

image-20240608171725959

2.3 看門狗怎麼續期

續期的方法,主要是在RedissonBaseLock這個抽象類中的renewExpiration()方法。

上節中,我們的RedissonLock類在最後就是直接呼叫了這個來自父類的方法。

image-20240608171857146

大體上看下,我們知道,它是一個定時定時任務。

image-20240608164252259=

    private void renewExpiration() {
        ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (ee != null) {
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(
            new TimerTask() {/**/}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS
            );
            ee.setTimeout(task);
        }
    }

先留個印象。

  • EXPIRATION_RENEWAL_MAP :用來管理所有需要續期的鎖的一個資料結構,它儲存了每個鎖的續期資訊,即 ExpirationEntry物件。
  • ExpirationEntry物件:包含了鎖的續期任務和相關的執行緒資訊。

比如我們啟動的時候debug下:

image-20240608165517625

renewExpiration 方法透過從續期對映中獲取當前鎖的續期條目,並建立一個定時任務,每隔租期的三分之一時間執行一次,檢查並非同步續期鎖的過期時間。

  • 如果續期成功,遞迴呼叫自身;

  • 如果續期失敗,記錄錯誤日誌並取消續期任務,以確保鎖在持有期間不會因超時而被釋放。

private void renewExpiration() {
    // 從 EXPIRATION_RENEWAL_MAP 獲取鎖的過期條目
    ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
    if (ee != null) {
        // 建立一個定時任務,每隔 internalLockLeaseTime / 3 的時間執行一次
        Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            
            public void run(Timeout timeout) throws Exception {
                // 再次從 EXPIRATION_RENEWAL_MAP 獲取鎖的過期條目
                ExpirationEntry ent = (ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
                if (ent != null) {
                    // 獲取持有鎖的第一個執行緒ID
                    Long threadId = ent.getFirstThreadId();
                    if (threadId != null) {
                        // 非同步續期鎖的過期時間,續期具體的操作在這裡!!!
                        CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
                        future.whenComplete((res, e) -> {
                            if (e != null) {
                                // 如果續期失敗,記錄錯誤日誌並從 EXPIRATION_RENEWAL_MAP 移除該鎖條目
                                RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);
                                RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
                            } else {
                                if (res) {
                                    // 如果續期成功,遞迴呼叫 renewExpiration 方法,再次設定續期任務
                                    RedissonBaseLock.this.renewExpiration();
                                } else {
                                    // 如果續期失敗,取消續期任務
                                    RedissonBaseLock.this.cancelExpirationRenewal((Long)null);
                                }
                            }
                        });
                    }
                }
            }
            
        }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
        // 設定續期任務
        ee.setTimeout(task);
    }
}

嗯,上面看到了續期的具體操作。

if (threadId != null) {
    // 非同步續期鎖的過期時間,續期具體的操作在這裡!!!
    CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);                       					future.whenComplete((res, e) -> {
    // ...

好,那麼renewExpirationAsync(threadId)是啥樣?

image-20240608172350266

就是呼叫了一個lua指令碼命令。

return this.evalWriteAsync(
    this.getRawName(), // 鎖的鍵名,例如 "myLock"
    LongCodec.INSTANCE, 
    RedisCommands.EVAL_BOOLEAN, 
    "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", 
    Collections.singletonList(this.getRawName()), // 鎖的鍵名,傳遞給 KEYS[1]。例如:yang37
    this.internalLockLeaseTime, // 鎖的新的過期時間,傳遞給 ARGV[1]。例如:30000ms
    this.getLockName(threadId) // 鎖的欄位名,通常包含執行緒ID,傳遞給 ARGV[2]。例如:37f644b6-8e05-4eb0-afe2-a56bb2ce6fce:83
);

解釋下redis的lua指令碼中,基本的引數意義。

  • KEYS:鍵名,可以在指令碼中透過索引訪問。

  • ARGV:引數列表,可以在指令碼中透過索引訪問。

邏輯如下:

檢查 Redis 中是否存在特定鎖,例如:yang37 + 37f644b6-8e05-4eb0-afe2-a56bb2ce6fce:83

  • 如果存在,則更新該鎖的過期時間,並返回 1 表示續期成功;
  • 如果不存在,則返回 0 表示續期失敗。
-- KEYS[1] 是鎖的鍵名。例如:yang37
-- ARGV[1] 是鎖的新的過期時間,以毫秒為單位。例如:30000ms
-- ARGV[2] 是鎖的欄位名,通常包含執行緒ID,以唯一標識持有鎖的執行緒。例如:37f644b6-8e05-4eb0-afe2-a56bb2ce6fce:83

-- 如果 Redis 雜湊表 KEYS[1] 中存在欄位 ARGV[2],即檢查鎖是否存在
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
    -- 如果存在,則將雜湊表 KEYS[1] 的過期時間設定為 ARGV[1] 毫秒
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    -- 返回 1 表示鎖的續期操作成功
    return 1; 
end; 
-- 如果鎖不存在,返回 0 表示鎖的續期操作失敗
return 0;
2.4 鎖的可重入

image-20240608181516409

image-20240608181527240

image-20240608181535536

最後來到了tryLockInnerAsync

image-20240608181614084

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return this.evalWriteAsync(
    this.getRawName(), 
    LongCodec.INSTANCE, 
    command, 
    "if (redis.call('exists', KEYS[1]) == 0) then " + 
    "redis.call('hincrby', 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]);", 
    Collections.singletonList(this.getRawName()), 
    new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)}
);
}
-- KEYS[1]: 鎖的鍵名,即 this.getRawName()。例如:yang37
-- ARGV[1]: 鎖的過期時間(租期),以毫秒為單位,即 unit.toMillis(leaseTime)。例如:30000
-- ARGV[2]: 鎖的欄位名,包括執行緒ID,即 this.getLockName(threadId)。:例如:017adcbf-08d9-449d-8c93-c82819799842:84

以下邏輯:

  • 如果鎖不存在,建立鎖並設定過期時間。

    if (redis.call('exists', KEYS[1]) == 0) then 
        redis.call('hincrby', 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]);
    

Redis 命令解釋

# exists
語法:EXISTS key
功能:檢查指定的鍵是否存在。
返回值:如果鍵存在,返回 1;否則返回 0。
示例:
redis.call('exists', 'myKey')  -- 檢查鍵 'myKey' 是否存在

# hincrby
語法:HINCRBY key field increment
功能:為雜湊表中的指定欄位的整數值加上增量值(increment)。如果欄位不存在,則在執行加法操作前將其設為 0。
返回值:執行加法操作後,欄位的值。
示例:
redis.call('hincrby', 'myHash', 'myField', 1)  -- 將雜湊表 'myHash' 中欄位 'myField' 的值增加 1

# pexpire
語法:PEXPIRE key milliseconds
功能:設定鍵的過期時間,以毫秒為單位。
返回值:如果設定了過期時間,返回 1;如果鍵不存在或無法設定過期時間,返回 0。
示例:
redis.call('pexpire', 'myKey', 60000)  -- 設定鍵 'myKey' 的過期時間為 60000 毫秒(60 秒)

# hexists
語法:HEXISTS key field
功能:檢查雜湊表中的指定欄位是否存在。
返回值:如果欄位存在,返回 1;否則返回 0。
示例:
redis.call('hexists', 'myHash', 'myField')  -- 檢查雜湊表 'myHash' 中欄位 'myField' 是否存在

# pttl
語法:PTTL key
功能:返回鍵的剩餘生存時間,以毫秒為單位。
返回值:
  - 如果鍵存在且設定了過期時間,返回剩餘生存時間(以毫秒為單位)。
  - 如果鍵存在但沒有設定過期時間,返回 -1。
  - 如果鍵不存在,返回 -2。
示例:
redis.call('pttl', 'myKey')  -- 返回鍵 'myKey' 的剩餘生存時間(以毫秒為單位)

2.3.2 簡單版自實現

相關文章