redis中的事务、lua脚本和管道的使用场景
https://blog.csdn.net/fangjian1204/article/details/50585080
事務
redis中的事務并不像mysql中那么完美,只是簡單的保證了原子性。redis中提供了四個命令來實現事務,MULTI:類似于mysql中的BEGIN;EXEC:類似于COMMIT;DISCARD類似于ROLLBACK;WATCH則是用于來實現mysql中類似鎖的功能。具體的使用方法非常簡單,例如:
127.0.0.1:6379> multi OK 127.0.0.1:6379> incr count QUEUED 127.0.0.1:6379> incr count QUEUED 127.0.0.1:6379> exec 1) (integer) 1 2) (integer) 2redis事務的實現原理是把事務中的命令先放入隊列中,當client提交了exec命令后,redis會把隊列中的每一條命令按序執行一遍。如果在執行exec之前事務中斷了,那么所有的命令都不會執行;如果執行了exec命令之后,那么所有的命令都會按序執行。但如果在事務執行期間redis被強制關閉,那么則需要使用redis-check-aof 工具對redis進行修復,刪除那些部分執行的命令。下面分幾種情況討論下redis事務中需要注意的地方:
1)入隊命令語法錯誤,此時還沒有執行exec命令
雖然redis在碰到exec命令之前不會執行事務中的命令,但是,它會對每個命令進行適當的檢查,當發現有某些明顯的語法錯誤時,如參數個數不正確,則會在入隊時,返回錯誤信息,并當看到exec命令調用discard命令進行回滾。例如:
127.0.0.1:6379> get name "Jeff" 127.0.0.1:6379> multi OK 127.0.0.1:6379> set name Kate QUEUED 127.0.0.1:6379> set name (error) ERR wrong number of arguments for 'set' command 127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get name "Jeff"2) 當exec執行完畢后,執行其它命令時發生錯誤
當redis在執行命令時,如果出現了錯誤,那么redis不會終止其它命令的執行。即只要是正確的命令,無論在錯誤命令之前還是之后,都會順利執行。例如:
127.0.0.1:6379> lpush visited "name1" (integer) 1 127.0.0.1:6379> get name "Kate" 127.0.0.1:6379> get count "5" 127.0.0.1:6379> multi OK 127.0.0.1:6379> set name Jeff QUEUED 127.0.0.1:6379> get visited QUEUED 127.0.0.1:6379> set count 10 QUEUED 127.0.0.1:6379> exec 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 3) OK 127.0.0.1:6379> get name "Jeff" 127.0.0.1:6379> get count "10"redis沒有實現真正的回滾是因為redis只是一個key-value緩存數據庫,如果加上日志回滾,將會影響其效率。
3)事務間的相互影響
事務中最長出現的影響就是同時修改一條記錄,而redis中的事務默認沒有對此進行處理,如果兩個事務同時修改一條記錄,首先執行exec的事務的結果將會被覆蓋。這里我們可以使用watch命令,該命令用于監控某些具體的key,如果這些key被其它事務修改了,那么本事務再修改時就不會成功,然后返回失敗的提示。
T1:watch namemultiset name Jeffexec T2:watch namemultiset name Kateexec如果T2先提交exec,那么T1提交時則更新失敗,此時name依舊是Kate,然后在應用層決定是否需要重新執行該事務。
由于redis事務中的命令在遇到exec命令之前并沒有真正的執行,所以我們無法在事務中的命令中使用前面命令的查詢結果。我們唯一可以做的就是通過watch保證在我們進行修改時,如果其它事務剛好進行了修改,則我們的修改停止,然后應用層做相應的處理。比如:如果get key 返回的值是true,那么我們set otherkey value,否則什么也不做。這種情況下,雖然可以用事務+watch實現原子操作,但是不免有點太僵硬,很明顯這是一個if……else語句。正是因為這個局限,使得lua腳本派上了大的用場。
參考文獻:http://redis.io/topics/transactions
lua腳本(2.6.0及以后版本)
原先沒有注意lua腳本的用法,上次還是請教了同事才知道redis中lua腳本的強大之處,然后果斷在項目中用了一下,感覺非常完美。其使用方法非常簡單,例如:
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second其中eval是lua腳本的解釋器;eval的第一個參數是腳本的內容,第二個參數是腳本里面KEYS數組的長度(不包括ARGV參數的個數),這里是兩個;緊接著就會有兩個參數,用于傳遞個KEYS數組;后面剩下的參數全部傳遞給ARGV數組,相當于命令行參數。
如果我們想在lua腳本中調用redis的命令該如何操作?可以在腳本中使用redis.call()或redis.pcall()直接調用,兩者用法類似,只是在遇到錯誤時,返回錯誤的提示方式不同。例如:
eval "return redis.call('set',KEYS[1],'bar')" 1 fooredis確保正一條script腳本執行期間,其它任何腳本或者命令都無法執行。正是由于這種原子性,script才可以替代MULTI/EXEC作為事務使用。當然,官方文檔也說了,正是由于script執行的原子性,所以我們不要在script中執行過長開銷的程序,否則會驗證影響其它請求的執行。
另外,redis為了減少每次客戶端發送來的數據帶寬(如果script太長,則發送來的內容可能非常多),會把每次新出現的腳本的sha1摘要保存下來,這樣后續如果script不變的話,只需要調用evalsha命令+script摘要即可,而不需要重復傳遞過長的腳本內容。例如:
127.0.0.1:6379> set foo bar OK 127.0.0.1:6379> eval "return redis.call('get','foo')" 0 "bar" 127.0.0.1:6379> evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0 "bar"從這里可以看出把key和arg以參數的形式傳遞而不是直接寫在script中的好處,因為這樣可以把變量提取出來,使得script的sha1摘要保持不變,提高命中率。在應用程序中,可以先使用evalsha進行調用,如果失敗,再使用eval進行操作,這樣可以在一定程度上提高效率。
有了上面的知識,我們就可以使用lua腳本來靈活的使用redis的事務,這里舉幾個簡單的例子。
場景1:我們要判斷一個IP是不是第一次訪問,如果是第一次訪問,那么返回狀態1,否則插入該ip,并返回狀態0.
127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) then return 1 else redis.call('set', KEYS[1], 'test') return 0 end" 1 test_127.0.0.1 (integer) 0 127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) then return 1 else redis.call('set', KEYS[1], 'test') return 0 end" 1 test_127.0.0.1 (integer) 1場景2:使用redis限制30分鐘內一個IP只允許訪問5次
思路:每次想把當前的時間插入到redis的list中,然后判斷list長度是否達到5次,如果大于5次,那么取出隊首的元素,和當前時間進行判斷,如果在30分鐘之內,則返回-1,其它情況返回1.
eval "redis.call('rpush', KEYS[1],ARGV[1]);if (redis.call('llen',KEYS[1]) >tonumber(ARGV[2])) then if tonumber(ARGV[1])-redis.call('lpop', KEYS[1])<tonumber(ARGV[3]) then return -1 else return 1 end else return 1 end" 1 'test_127.0.0.1' 1451460590 5 1800通過上面兩個場景可以看到,我們僅僅使用了lua的if語句,就可以實現這么方便的操作,如果使用其它的lua語法,肯定更加方便。
官網文檔上有這樣一段話:
A Redis script is transactional by definition, so everything you can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.
由此可以看出,官方還是支持大家盡量使用lua script來代替transaction的。
參考文獻:http://redis.io/commands/eval
管道
大家都知道redis是基于TCP連接進行通信的,每一個request/response都需要經歷一個RTT往返時間,如果需要執行很多短小的命令,這些往返時間的開銷是很大的,在此情形下,redis提出了管道來提高執行效率。管道的思想是:如果client執行一些相互之間無關的命令或者不需要獲取命令的返回值,那么redis允許你連續發送多條命令,而不需要等待前面命令執行完畢。比如我們執行3條INCR命令,如果使用管道,理論上只需要一個RTT+3條命令的執行時間即可,如果不適用管道,那么可能需要額外的兩個RTT時間。因此,管道相當于批處理腳本,相當于是命令集,例如:
with r.pipeline(transaction=False) as pipe:pipe.set('key1', 'value1')pipe.set('key2', 'value2')pipe.set('key3', 'value3')pipe.execute()Pipeline在某些場景下非常有用,比如有多個command需要被“及時的”提交,而且他們對相應結果沒有互相依賴,而且對結果響應也無需立即獲得,那么pipeline就可以充當這種“批處理”的工具;而且在一定程度上,可以較大的提升性能,性能提升的原因主要是TCP鏈接中較少了“交互往返”的時間。例如:因為業務需要,我們需要把用戶的操作過程記錄在日志中以方便以后的統計,每隔3個小時生成一個新的日志文件,那么后臺處理線程,將會掃描日志文件并將每條日志輸出為“operation”:1,即表示操作次數為1;如果每個operation都發送一個command,事實上性能是很差的,而且是沒有必要的;那么我們就可以使用pipeline批量提交即可。
管道和事務是不同的,pipeline只是表達“交互”中操作的傳遞的方向性,pipeline也可以在事務中運行,也可以不在。無論如何,pipeline中發送的每個command都會被server立即執行,如果執行失敗,將會在此后的相應中得到信息;也就是pipeline并不是表達“所有command都一起成功”的語義,管道中前面命令失敗,后面命令不會有影響,繼續執行。簡單來說就是管道中的命令是沒有關系的,它們只是像管道一樣流水發給server,而不是串行執行,僅此而已;但是如果pipeline的操作被封裝在事務中,那么將有事務來確保操作的成功與失敗。
使用管道可能在效率上比使用script要好,但是有的情況下只能使用script。因為在執行后面的命令時,無法得到前面命令的結果,就像事務一樣,所以如果需要在后面命令中使用前面命令的value等結果,則只能使用script或者事務+watch。
參考文獻:http://redis.io/topics/pipelining
轉載于:https://www.cnblogs.com/davidwang456/articles/9388449.html
總結
以上是生活随笔為你收集整理的redis中的事务、lua脚本和管道的使用场景的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: No module named Cryp
- 下一篇: python 字符串转字节数组