StackExchange.Redis學習筆記(四) 事務控制和Batch批量操作
Redis事物
Redis命令實現事務
Redis的事物包含在multi和exec(執行)或者discard(回滾)命令中
和sql事務不同的是,Redis調用Exec只是將所有的命令變成一個單元一起執行,期間不會插入其他的命令。
這種方式不保證事務的一致性,即使中間有一條命令出錯了,其他命令仍然可以正常執行,並且無法回滾
下面的例子演示了一個基本的事務操作
127.0.0.1:6379> multi OK 127.0.0.1:6379> set name mike QUEUED 127.0.0.1:6379> set age 20 QUEUED 127.0.0.1:6379> exec 1) OK 2) OK 127.0.0.1:6379> get name "mike"
可以看到,直到調用Exec命令時,才開始執行之前的所有命令,同時會返回兩個結果,discard 命令類似,就不貼代碼了。
下面模擬一個會報錯的命令來看一下
127.0.0.1:6379> get age "20" 127.0.0.1:6379> get name "mike" 127.0.0.1:6379> multi OK 127.0.0.1:6379> incr name QUEUED 127.0.0.1:6379> incr age QUEUED 127.0.0.1:6379> exec 1) (error) ERR value is not an integer or out of range 2) (integer) 21 127.0.0.1:6379> get age "21" 127.0.0.1:6379>
我們同時將用戶name和age 進行自增1操作,然而name不是數字類型,結果執行失敗,但是age的自增操作仍然成功了。這無疑是個很令人不舒服的弊端,所以在寫相關代碼時要註意
樂觀鎖
前面說到通過multi命令只是保證一個事物中的所有命令可以在一起執行,顯然只是實現這一點的話對於大部分的業務都是無法滿足的。
所以Redis提供了Watch命令來監控一個key以達到樂觀鎖的效果。關於樂觀鎖的原理有不了解的小夥伴可以抽十分鐘去科普一下
下面展示一個樂觀鎖實例:
這裏模擬了兩個客戶端同時操作一個相同的鍵
左邊為client1,我們用watch監控了name和age兩個鍵,然後分別設置name和age的值。在exec命令之前,通過另一個客戶端client2設置了name的值。
client1執行exec命令時,Redis檢測到name的值已經被其他客戶端改過了,因此在事物中的所有命令都會回滾。
watch命令是對整個連接有效的,用完之後可以用discard、unwatch、exec命令清除監視
StackExchange.Redis中的事物控制
在StackExchange.Redis是無法用watch multi命令來執行的,因為在並發環境下,會產生多個watch multi命令,全混在一起就亂套了。
但是StackExchange.Redis提供了一套非常簡單易懂的創建事物的方式 ,下面為示例代碼
public void TestTran() { IDatabase db = StackExchangeRedisHelper.GetDatabase(); string name = db.StringGet("name"); string age = db.StringGet("age"); Console.WriteLine("NAME:" + name); Console.WriteLine("Age:" + age); var tran = db.CreateTransaction(); tran.AddCondition(Condition.StringEqual("name", name)); Console.WriteLine("tran begin"); tran.StringSetAsync("name", "leap"); tran.StringSetAsync("age", 12); Thread.Sleep(4000); bool result = tran.Execute(); Console.WriteLine("執行結果:" + result); Console.WriteLine("Age:" + db.StringGet("age")); Console.WriteLine("Name:" + db.StringGet("name")); }
這裏通過CreateTransaction函數(multi)來創建一個事物,調用其Execute函數(exec)提交事物,其中的 "Condition.StringEqual("name", name)" 就相當於Redis命令中的watch name。
其中睡眠四秒是我需要在事物提交之前打開另一個客戶端來修改name的值.最終的執行結果如下
NAME:leo Age:20 tran begin 執行結果:False Age:20 Name:mike
在程序睡眠期間我用另一個客戶端將name改成了mike,所以事物最終執行失敗
通過查詢Redis的慢日誌。其調用的命令也是watch multi exec。(慢日誌沒有記錄Exec命令,實際上是執行了的)。
我們可以通過設置redis.windows-service.conf文件中的slowlog-log-slower-than的值為0讓Redis記錄所有的命令日誌
127.0.0.1:6379> slowlog get 100 1) 1) (integer) 293 2) (integer) 1511257634 3) (integer) 1 4) 1) "GET" 2) "name" 2) 1) (integer) 292 2) (integer) 1511257634 3) (integer) 0 4) 1) "GET" 2) "age" 3) 1) (integer) 291 2) (integer) 1511257634 3) (integer) 3 4) 1) "SELECT" 2) "0" 4) 1) (integer) 290 2) (integer) 1511257634 3) (integer) 1 4) 1) "SET" 2) "age" 3) "12" 5) 1) (integer) 289 2) (integer) 1511257634 3) (integer) 1 4) 1) "SET" 2) "name" 3) "leap" 6) 1) (integer) 288 2) (integer) 1511257634 3) (integer) 1 4) 1) "MULTI" 7) 1) (integer) 287 2) (integer) 1511257634 3) (integer) 3 4) 1) "GET" 2) "name" 8) 1) (integer) 286 2) (integer) 1511257634 3) (integer) 11 4) 1) "WATCH" 2) "name" 9) 1) (integer) 285 2) (integer) 1511257634 3) (integer) 4 4) 1) "GET" 2) "age" 10) 1) (integer) 284 2) (integer) 1511257634 3) (integer) 6 4) 1) "GET" 2) "name"
這裏可能大家會有個疑惑,既然tran是直接調用的watch multi等命令,為什麽不會有並發的順序問題?
這是因為Tran開啟後,所做的watch,stringset等操作,都會再調用Exec函數時把相應的命令封裝成一個請求發送給Redis一起執行。這樣每個事務之間都是獨立的,就不會有問題了。
Batch批量操作
StackExchange.Redis中對於連續多次的緩存等請求,我們會多次調用相關的函數來執行Redis命令。然而這種方式有個弊端就是每一次的請求都需要等待返回結果
如果在網絡狀況不好的情況下,可能會造成不好的用戶體驗。
對於這種問題可以用StackExchange.Redis提供的CreateBatch()解決
public void TestPipeLine() { IDatabase db = StackExchangeRedisHelper.GetDatabase(); var batch = db.CreateBatch(); Task t1 = batch.StringSetAsync("name", "bob"); Task t2 = batch.StringSetAsync("age", 100); batch.Execute(); Task.WaitAll(t1, t2); Console.WriteLine("Age:" + db.StringGet("age")); Console.WriteLine("Name:" + db.StringGet("name")); }
batch會把所需要執行的命令打包成一條請求發到Redis,然後一起等待返回結果。這樣批量操作的速度就大大提升啦!
StackExchange.Redis學習筆記(四) 事務控制和Batch批量操作