一篇和Redis有關的鎖和事務的文章

Sheldon_Lou發表於2019-05-10

部分參考連結

Transaction

StackExchange.Redis Transaction

hashest

正文

Redis 是一種基於記憶體的單執行緒資料庫。意味著所有的命令是一個接一個的執行。


考慮只有一個Redis例項,也就是Redis本身沒有做分散式。


通過SETNX命令,set if not exist的縮寫。那麼多個服務在呼叫的時候可以通過同一個key申請一個lock(也就是呼叫命令成功返回1),然後根據相應條件做釋放(比如時間到期,or手動釋放),也就是delete key。

Redis本身有MULTI命令,標記開啟一個事務。開啟之後後面的命令會在呼叫EXEC命令的時候以一個集合的方式整體執行,也就是原子性(都成功or失敗)。

現在有個需求,用redis實現Check and Set,也就是先讀取裡面的值,然後設定(比如做個+=val);併發的問題是必須要考慮的。

用redis描述大致是這樣的。這裡假設redis沒有incr這個自增命令。

val = GET mykey
val = val + 1
SET mykey $val

直接這樣做,併發問題是肯定有的。所以,按照上面的知識,應該有2種方法來避免這個併發問題。

基於SENTX命令。

copy一下文件的demo

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis> 

第一次呼叫setnx,設定mykey的value為hello,返回1,表示成功。

第二次呼叫setnx,設定mykey的value為world,因為第一次呼叫並沒有釋放mykey,所以返回0,表示設定失敗。

最後獲取mykey的值,返回的是hello。

最後記得要去釋放mykey。

這其實是一個悲觀鎖,也就是一個程式獲取到鎖之後要等釋放別的程式才能繼續。

基於MULTI命令。

  1. 先看一個簡單的應用

    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> incr foo
    QUEUED
    127.0.0.1:6379> incr bar
    QUEUED
    127.0.0.1:6379> exec
    1) (integer) 1
    2) (integer) 1

    第一步呼叫MULTI命令,表示開始多個命令的輸入。返回OK,表示開始接收。

    第二步呼叫incr foo,給foo對應的值做自增。返回queued,表示已加入佇列。

    第二步呼叫incr bar,給bar對應的值做資政,返回queued,表示已加入佇列。

    最後呼叫exec命令,表示執行佇列中的命令。返回每個命令的結果。

  2. 有錯誤了怎麼辦

    首先錯誤分兩種

    • 在enqueue的時候出錯,最常見的就是引數錯誤。比如下面這個例子
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set a 1234
    QUEUED
    127.0.0.1:6379> set a 1 1 1 1 1 1 11
    QUEUED
    127.0.0.1:6379> exec
    1) OK
    2) (error) ERR syntax error
    127.0.0.1:6379>

    第二個set a 1 1 1 1 1 1 11命令是有語法錯誤,所以,在執行exec的時候會返回語法錯誤。第一個是成功的。所以,如果在後面get a是會返回1234,為成功的設定。

    假設報錯的命令在中間,後面的命令也是會執行的。

    • 還有就是直接命令就不對的。看個例子
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set a 11
    QUEUED
    127.0.0.1:6379> aaa
    (error) ERR unknown command `aaa`, with args beginning with:
    127.0.0.1:6379> exec
    (error) EXECABORT Transaction discarded because of previous errors.

    先set a,進入佇列。

    執行aaa命令,這個命令不存在。直接報錯。

    執行exec,事務因為之前的錯誤,exec中止。

  3. 為什麼沒有回滾

    通過上面的例子,看到redis對multi的操作是沒有回滾的,或許有點奇怪。根據文件描述,有兩個原因。

    • redis的命令執行只有在語法錯誤或者資料型別出錯的時候會失敗,而不是在enqueue的時候。這意味著失敗是由程式設定錯誤導致的。那麼,這種錯誤肯定是在開發環境中就應該容易被發現,而不是在生產環境。
    • 為了快。
  4. WATCH 命令的樂觀鎖

    結合watch命令我們也可以實現上面的需求。

    WATCH mykey
    --Begin---
    ##下面兩行是客戶端命令
    val = GET mykey
    val = val + 1
    --End---
    MULTI
    SET mykey $val
    EXEC

    解釋一下,先獲取一下mykey的監控。然後客戶端獲取mykey的值,(是客戶端,不是命令服務端)。然後賦值自增。然後服務端開啟MULTI, 設定新的值。執行。

    假設在MULTI和Exec之間,mykey的值被別的client修改,exec會返回(nil)。

    下面做個演示:

    先在redis-cli上執行以下命令

    127.0.0.1:6379> watch a
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set a 13
    QUEUED

    如上,已經開啟WATCH,然後設定a =13 進入佇列。

    然後在本地的redis desktop manager上去修改這個值。

    update

    然後再在伺服器上執行 exec,

    127.0.0.1:6379> exec
    (nil)

    返回的是nil,表示沒有成功。如果沒有客戶端去更新,執行exec是返回OK。

  5. redis-scripting-and-transactions

    在Redis 2.6之後,引入了Redis script來實現事務的功能。通常來說script方式速度會相對快一點(沒有做測試)。不過既然multi已經出來很久了,所以,不太可能會移除這個命令。

在StackExchange.Redis中使用

顯然,也分兩種,基於setnx 或者 MULTI + WATCH。分別對應的是IDatabaseAsync.LockTakeAsyncIDatabaseAsync.CreateTransaction這裡結合了Polly這個庫用於重試,畢竟,悲觀鎖,我多拿幾次總能拿到的;樂觀鎖,執行的命令,我多試幾次,總能成功的。

  • LockTakeAsync

    public async Task<T> TakeLockAsync<T>(string key, string token, Func<object, Task<T>> func, object obj)
        where T : class
    {
        var db = GetDb(redisConfigModel.LockDbIndex);//獲取IDatabaseAsync物件
        //定義獲取鎖的策略
        var policy = Policy
            .HandleResult<bool>(w => !w)
            .WaitAndRetryForeverAsync(
                sleepDurationProvider: attemp => TimeSpan.FromSeconds(3), //兩次重複嘗試的間隔
                onRetry: (delegeteRst, ts) =>
                {
                    //可以記錄日誌啥的
                }
            );
        //競爭獲取鎖。
        await policy.ExecuteAsync(async () => await db.LockTakeAsync(key, token, TimeSpan.MaxValue));  
        try
        {
            return await func(obj);//獲取到鎖之後的具體執行的方法。
        }
        finally
        {
            await db.LockReleaseAsync(key, token); //最後一定要釋放
        }
    }
    

    LockTakeAsync的時候根據key對應的token值是否已經被獲取來作為條件。

  • CreateTransaction

    StackExchange.Redis 用multiplexer類實現Redis的一些列命令。我們的程式碼不能直接簡單的對映到watch命令,因為,單純呼叫watch是肯定成功的,這樣會導致大家都"成功"(假的)。這裡用的Condition的方式來實現。

    public async Task AddAfterReadAsync(string key, int value, string hashField = "hash_field")
    {
          //處理policy的結果為false的情況,一直重試。
        var policy = Policy.HandleResult<bool>(w => !w).RetryForeverAsync();
          //執行
        await policy.ExecuteAsync(async () =>
        {
            var db = GetDb(redisConfigModel.LockDbIndex);
            var trans = db.CreateTransaction();
            var oldValue = Convert.ToInt32(await db.StringGetAsync(key));
            trans.AddCondition(Condition.HashNotExists(key,
                hashField)); //這裡確保hashField不存在。也可以用Condition.KeyNotExists(key)
            //這裡不能await,因為每個命令的結果只有在執行了execute後才知道。
            trans.StringSetAsync(key, (oldValue + value).ToString());
            var execSuccess = await trans.ExecuteAsync();
            return execSuccess;
        });
    }
    

小結

這是一篇和redis有關的鎖,事務的文章。寫了我一整個下午。看完,感覺也沒有多少東西。感覺開頭連結中關於hashset還是有點意思的。

相關文章