部分參考鏈接
正文
Redis 是一種基於內存的單線程數據庫。意味着所有的命令是一個接一個的執行。
考慮只有一個Redis實例,也就是Redis本身沒有做分布式。
通過SETNX命令,set if not exist的縮寫。那么多個服務在調用的時候可以通過同一個key申請一個lock(也就是調用命令成功返回1),然后根據相應條件做釋放(比如時間到期,or手動釋放),也就是delete key。
Redis本身有MULTI命令,標記開啟一個事務。開啟之后后面的命令會在調用EXEC命令的時候以一個集合的方式整體執行,也就是原子性(不保證都成功)。
現在有個需求,用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還是有點意思的。