Redis:事务、管道、Lua脚本
1. Redis事務定義
Redis中的事務(transaction)是一組命令的集合。事務同命令一樣都是Redis的最小執行單位,一個事務中的命令要么都執行,要么都不執行。
事務的原理是先將屬于一個事務的命令發送給Redis,然后再讓Redis依次執行這些命令。
1.????Redis保證一個事務中的所有命令要么都執行,要么都不執行。如果在發送EXEC命令前客戶端斷線了,則Redis會清空事務隊列,事務中的所有命令都不會執行。而一旦客戶端發送了EXEC命令,所有的命令就都會被執行,即使此后客戶端斷線也沒關系,因為Redis中已經記錄了所有要執行的命令。
2.????除此之外,Redis的事務還能保證一個事務內的命令依次執行而不被其他命令插入。試想客戶端A需要執行幾條命令,同時客戶端B發送了一條命令,如果不使用事務,則客戶端B的命令可能會插入到客戶端A的幾條命令中執行。如果不希望發生這種情況,也可以使用事務。
2. Redis事務的錯誤和回滾
Redis的事務沒有隔離級別的概念(CAID中的I),在事務執行前所有的命令都未執行。對于執行過程中的錯誤按照類型分為兩種。
1. 語法錯誤
語法錯誤指命令不存在或者命令參數的個數不對,這個命令可能會有語法錯誤(參數的數量錯誤、命令名稱錯誤,等等),或者可能會有某些臨界條件(例如:如果使用maxmemory指令,為Redis服務器配置內存限制,那么就可能會有內存溢出條件)。?
可用Redis客戶端檢測第一種類型的錯誤,在調用EXEC命令之前,這些客戶端可以檢查被放入隊列的命令的返回值:如果命令的返回值是QUEUE字符串,那么就表示已經正確地將這個命令放入隊列;否則,Redis將返回一個錯誤。如果將某個命令放入隊列時發生錯誤,那么大多數客戶端將會中止事務,并且丟棄這個事務。
在Redis 2.6.5版本之前,如果發生了上述的錯誤,那么在客戶端調用了EXEC命令之后,Redis還是會運行這個出錯的事務,執行已經成功放入事務隊列的命令,而不會關心先前發生的錯誤。從2.6.5版本開始,Redis在遭遇上述錯誤時,服務器會記住事務積累命令期間發生的錯誤。然后,Redis會拒絕執行這個事務,在運行EXEC命令之后,便會返回一個錯誤消息。最后,Redis會自動丟棄這個事務,這樣便能輕松地混合使用事務和管道。在這種情況下,客戶端可以一次性地將整個事務發送至Redis服務器,稍后再一次性地讀取所有的返回值。
2.?運行錯誤
運行錯誤指在命令執行時出現的錯誤,比如使用散列類型的命令操作集合類型的鍵,這種錯誤在實際執行之前Redis是無法發現的,所以在事務里這樣的命令是會被Redis接受并執行的。如果事務里的一條命令出現了運行錯誤,事務里其他的命令依然會繼續執行(包括出錯命令之后的命令)。
Redis的回滾機制
Redis的事務沒有關系數據庫事務提供的回滾(rollback)功能。為此開發者必須在事務執行出錯后自己收拾剩下的攤子(將數據庫復原回事務執行前的狀態等,這里我們一般采取日志記錄然后業務補償的方式來處理,但是一般情況下,在redis做的操作不應該有這種強一致性要求的需求,我們認為這種需求為不合理的設計)。
3.?Redis的樂觀鎖和Watch
Watch命令描述:
WATCH命令可以監控一個或多個鍵,一旦其中有一個鍵被修改(或刪除),之后的事務就不會執行。監控一直持續到EXEC命令(事務中的命令是在EXEC之后才執行的(事先都是存儲在隊列里),所以在MULTI命令后可以修改WATCH監控的鍵值)
在Redis的事務中,WATCH命令可用于提供CAS(check-and-set)功能,且是基于樂觀鎖的思想。假設我們通過WATCH命令在事務執行之前監控了多個Keys,倘若在WATCH之后有任何Key的值發生了變化,EXEC命令執行的事務都將被放棄,同時返回Null multi-bulk應答以通知調用者事務執行失敗。例如,我們再次假設Redis中并未提供incr命令來完成鍵值的原子性遞增,如果要實現該功能,我們只能自行編寫相應的代碼。其偽碼如下:
[java]?view plain?copy
1.??val?=?GET?mykey??
2.??val?=?val?+?1??
3.??SET?mykey?$val??
以上代碼只有在單連接的情況下才可以保證執行結果是正確的,因為如果在同一時刻有多個客戶端在同時執行該段代碼,那么就會出現多線程程序中經常出現的一種錯誤場景--競態爭用(race condition)。比如,客戶端A和B都在同一時刻讀取了mykey的原有值,假設該值為10,此后兩個客戶端又均將該值加一后set回Redis服務器,這樣就會導致mykey的結果為11,而不是我們認為的12。為了解決類似的問題,我們需要借助WATCH命令的幫助,見如下代碼:
[java]?view plain?copy
1.??WATCH?mykey??
2.??val?=?GET?mykey??
3.??val?=?val?+?1??
4.??MULTI??
5.??SET?mykey?$val??
6.??EXEC??
和此前代碼不同的是,新代碼在獲取mykey的值之前先通過WATCH命令監控了該鍵,此后又將set命令包圍在事務中,這樣就可以有效的保證每個連接在執行EXEC之前,如果當前連接獲取的mykey的值被其它連接的客戶端修改,那么當前連接的EXEC命令將執行失敗。這樣調用者在判斷返回值后就可以獲悉val是否被重新設置成功。
由于WATCH命令的作用只是當被監控的鍵值被修改后阻止之后一個事務的執行,而不能保證其他客戶端不修改這一鍵值,所以在一般的情況下我們需要在EXEC執行失敗后重新執行整個函數。執行EXEC命令后會取消對所有鍵的監控,如果不想執行事務中的命令也可以使用UNWATCH命令來取消監控。
實現一個hsetNX函數
我們實現的hsetNX這個功能是:僅當字段存在時才賦值。為了避免競態條件我們使用watch和事務來完成這一功能(偽代碼):
[java]?view plain?copy
1.??WATCH?key????
2.??isFieldExists?=?HEXISTS?key,?field????
3.??if?isFieldExists?is?1????
4.??MULTI????
5.??HSET?key,?field,?value????
6.??EXEC????
7.??else????
8.??UNWATCH????
9.??return?isFieldExists??
在代碼中會判斷要賦值的字段是否存在,如果字段不存在的話就不執行事務中的命令,但需要使用UNWATCH命令來保證下一個事務的執行不會受到影響。
?
4. Jedis的事務
Jedis對Redis的事務機制給出了具體實現,示例代碼如下:
[java]?view plain?copy
1.??public?static?void?testWach(){??
2.???????Jedis?jedis?=?RedisCacheClient.getInstrance().getClient();??
3.???????String?watch?=?jedis.watch("testabcd");??
4.???????System.out.println(Thread.currentThread().getName()+"--"+watch);??
5.???????Transaction?multi?=?jedis.multi();??
6.???????multi.set("testabcd",?"23432");??
7.???????try?{??
8.???????????Thread.sleep(3000);??
9.???????}?catch?(InterruptedException?e)?{??
10. ?????????e.printStackTrace();??
11. ?????}??
12. ?????List<Object>?exec?=?multi.exec();??
13. ?????System.out.println("---"+exec);??
14. ?????jedis.unwatch();??
15. ?}??
16. ?public?static?void?testWatch2(){??
17. ?????Jedis?jedis?=?RedisCacheClient.getInstrance().getClient();??
18. ?????String?watch?=?jedis.watch("testabcd2");??
19. ?????System.out.println(Thread.currentThread().getName()+"--"+watch);??
20. ?????Transaction?multi?=?jedis.multi();??
21. ?????multi.set("testabcd",?"125");??
22. ?????List<Object>?exec?=?multi.exec();??
23. ?????System.out.println("--->>"+exec);??
24. ?}??
三Redis的管道
Redis是一個響應式的服務,當客戶端發送一個請求后,就處于阻塞狀態等待Redis返回結果。這樣一次命令消耗的時間就包括三個部分:請求從客戶端到服務器的時間、結果從服務器到客戶端的時間和命令真正執行時間,前兩個部分消耗的時間總和稱為RTT(Round Trip Time),當客戶端與服務器存在網絡延時,RTT就可能會很大,這樣就會導致性能問題。
管道(Pipeline)就是為了改善這個情況的,利用管道,客戶端可以一次性發送多個請求而不用等待服務器的響應,待所有命令都發送完后再一次性讀取服務的響應,這樣可以極大的降低RTT時間從而提升性能。需要注意到是用pipeline方式打包命令發送,redis必須在處理完所有命令前先緩存起所有命令的處理結果。打包的命令越多,緩存消耗內存也越多。所以并不是打包的命令越多越好。(占內存)
pipeline和“事務”是兩個完全不同的概念,pipeline只是表達“交互”中操作的傳遞的方向性,pipeline也可以在事務中運行,也可以不在。無論如何,pipeline中發送的每個command都會被server立即執行,如果執行失敗,將會在此后的相應中得到信息;也就是pipeline并不是表達“所有command都一起成功”的語義(有的成功有的失敗);但是如果pipeline的操作被封裝在事務中,那么將有事務來確保操作的成功與失敗(只允許成功或者失敗)。Pipeline的示例代碼如下:
[java]?view plain?copy
1.??private?static?void?usePipeline(int?count){??
2.??????Jedis?jr?=?null;??
3.??????try?{??
4.??????????jr?=?new?Jedis("10.10.224.44",?6379);??
5.??????????Pipeline?pl?=?jr.pipelined();??
6.??????????for(int?i?=0;?i<count;?i++){??
7.???????????????pl.incr("testKey2");??
8.??????????}??
9.??????????????pl.sync();??
10. ????}?catch?(Exception?e)?{??
11. ????????e.printStackTrace();??
12. ????}??
13. ????finally{??
14. ????????if(jr!=null){??
15. ????????????jr.disconnect();??
16. ????????}??
17. ????}??
18. }??
使pipeline完成操作需要更低的耗時即可。
四Redis Lua腳本
Redis在2.6推出了腳本功能,允許開發者使用Lua語言編寫腳本傳到Redis中執行。使用腳本的好處如下:
1.減少網絡開銷:本來5次網絡請求的操作,可以用一個請求完成,原先5次請求的邏輯放在redis服務器上完成。使用腳本,減少了網絡往返時延。
2.原子操作:Redis會將整個腳本作為一個整體執行,中間不會被其他命令插入。
3.復用:客戶端發送的腳本會永久存儲在Redis中,意味著其他客戶端可以復用這一腳本而不需要使用代碼完成同樣的邏輯。
在實際工作過程中,可以使用lua腳本來解決一些需要保證原子性的問題,而且lua腳本可以緩存在redis服務器上,勢必會增加性能。
Lua語法
見Lua語言模型與 Redis應用中關于Lua語法的詳述。
Eval命令
從Redis2.6.0版本開始,通過內置的Lua解釋器,可以使用EVAL命令對Lua腳本進行求值。EVAL命令的格式如下:
[java]?view plain?copy
1.??EVAL?script?numkeys?key?[key?...]?arg?[arg?...]??
script參數是一段Lua腳本程序,它會被運行在Redis服務器上下文中,這段腳本不必(也不應該)定義為一個Lua函數。numkeys參數用于指定鍵名參數的個數。鍵名參數 key [key ...] 從EVAL的第三個參數開始算起,表示在腳本中所用到的那些Redis鍵(key),這些鍵名參數可以在 Lua中通過全局變量KEYS數組,用1為基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推)。
在命令的最后,那些不是鍵名參數的附加參數 arg [arg ...] ,可以在Lua中通過全局變量ARGV數組訪問,訪問的形式和KEYS變量類似( ARGV[1] 、ARGV[2] ,諸如此類)。例如
[java]?view plain?copy
1.??>?eval?"return?{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"?2?key1?key2?first?second??
2.??1)?"key1"??
3.??2)?"key2"??
4.??3)?"first"??
5.??4)?"second"??
其中 "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 是被求值的Lua腳本,數字2指定了鍵名參數的數量,key1和key2是鍵名參數,分別使用 KEYS[1] 和 KEYS[2] 訪問,而最后的 first 和 second 則是附加參數,可以通過 ARGV[1] 和 ARGV[2] 訪問它們。
Call和pcall
在 Lua 腳本中,可以使用兩個不同函數來執行Redis命令,它們分別是:
1.????redis.call()
2.????redis.pcall()
這兩個函數的唯一區別在于它們使用不同的方式處理執行命令所產生的錯誤。當redis.call() 在執行命令的過程中發生錯誤時,腳本會停止執行,并返回一個腳本錯誤,錯誤的輸出信息會說明錯誤造成的原因,redis.pcall() 出錯時并不引發(raise)錯誤,而是返回一個帶 err 域的 Lua 表(table),用于表示錯誤。
Jedis調用
Jedis中調用腳本需要以字符串形式給出腳本主體,并遵從EVAL的數據規范,示例代碼如下:
[java]?view plain?copy
1.??String?script?="local?result={}?"?+???
2.??????????????????"?for?i,v?in?ipairs(KEYS)?do?"?+???
3.??????????????????"?result[i]?=?redis.call('get',v)?"?+???
4.??????????????????"?end?"?+???
5.??????????????????"?return?result?";??
6.????
7.??Jedis?jedis?=?new?Jedis(ip,port);??
8.????
9.??jedis.eval(script,keyCount,String?…?params);??
注意,不要再Lua腳本中出現死循環和耗時的運算,否則redis將不接受其他的命令,這個redis就掛了,只能script kill,如果有寫入的話,只能shutdown nosave。?所以使用時要注意不能出現死循環、耗時的運算。redis是單進程、單線程執行腳本。
五 Redis事務、管道和腳本的區別
1. 事務和腳本從原子性上來說都能滿足原子性的要求,其區別在于腳本可借助Lua語言可在服務器端存儲的便利性定制和簡化操作,但腳本無法處理長耗時的操作。
2. 管道是無狀態操作集合,使用管道可能在效率上比使用script要好,但是有的情況下只能使用script。因為在執行后面的命令時,無法得到前面命令的結果,就像事務一樣,所以如果需要在后面命令中使用前面命令的value等結果,則只能使用script或者事務+watch。
?
總結
以上是生活随笔為你收集整理的Redis:事务、管道、Lua脚本的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis : redis事务
- 下一篇: Redis:事件驱动(IO多路复用)