部分參考連接git
Transactiongithub
StackExchange.Redis Transactionredis
hashest數據庫
Redis 是一種基於內存的單線程數據庫。意味着全部的命令是一個接一個的執行。c#
考慮只有一個Redis實例,也就是Redis自己沒有作分佈式。bash
經過SETNX命令,set if not exist的縮寫。那麼多個服務在調用的時候能夠經過同一個key申請一個lock(也就是調用命令成功返回1),而後根據相應條件作釋放(好比時間到期,or手動釋放),也就是delete key。服務器
Redis自己有MULTI命令,標記開啓一個事務。開啓以後後面的命令會在調用EXEC命令的時候以一個集合的方式總體執行,也就是原子性(不保證都成功)。併發
如今有個需求,用redis實現Check and Set,也就是先讀取裏面的值,而後設置(好比作個+=val);併發的問題是必需要考慮的。async
用redis描述大體是這樣的。這裏假設redis沒有incr這個自增命令。分佈式
val = GET mykey val = val + 1 SET mykey $val
直接這樣作,併發問題是確定有的。因此,按照上面的知識,應該有2種方法來避免這個併發問題。
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。
這實際上是一個悲觀鎖,也就是一個進程獲取到鎖以後要等釋放別的進程才能繼續。
先看一個簡單的應用
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命令,表示執行隊列中的命令。返回每一個命令的結果。
有錯誤了怎麼辦
首先錯誤分兩種
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的操做是沒有回滾的,或許有點奇怪。根據文檔描述,有兩個緣由。
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已經出來好久了,因此,不太可能會移除這個命令。
顯然,也分兩種,基於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仍是有點意思的。