部分參考連結
正文
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命令。
先看一個簡單的應用
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命令,表示執行佇列中的命令。返回每個命令的結果。
有錯誤了怎麼辦
首先錯誤分兩種
- 在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中止。
為什麼沒有回滾
通過上面的例子,看到redis對multi的操作是沒有回滾的,或許有點奇怪。根據文件描述,有兩個原因。
- redis的命令執行只有在語法錯誤或者資料型別出錯的時候會失敗,而不是在enqueue的時候。這意味著失敗是由程式設定錯誤導致的。那麼,這種錯誤肯定是在開發環境中就應該容易被發現,而不是在生產環境。
- 為了快。
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上去修改這個值。
然後再在伺服器上執行 exec,
127.0.0.1:6379> exec (nil)
返回的是nil,表示沒有成功。如果沒有客戶端去更新,執行exec是返回OK。
redis-scripting-and-transactions
在Redis 2.6之後,引入了Redis script來實現事務的功能。通常來說script方式速度會相對快一點(沒有做測試)。不過既然multi已經出來很久了,所以,不太可能會移除這個命令。
在StackExchange.Redis中使用
顯然,也分兩種,基於setnx
或者 MULTI + WATCH
。分別對應的是IDatabaseAsync.LockTakeAsync
和IDatabaseAsync.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還是有點意思的。