一、引言
學習redis 也有一段時間了,該接觸的也差不多了。后來有一天,以前的同事問我,如何向redis中批量的增加數據,肯定是大批量的,為了這主題,我又重新找起了解決方案。目前的解決方案大都是從官網上查找和翻譯的,每個實例也都調試了,正確無誤。把結果告訴我同事的時候,我也更清楚這個主題如何操作了,里面的細節也更清楚了。當然也有人說可以通過腳本來做這個操作,沒錯,但是我對腳本語言還沒有研究很透,就不來班門弄斧了。
二、管道的由來
說起這個主題也是我同事幫的忙,關於批量增加增加數據到Redis服務器中,我已經寫了一篇文章了,那篇文章只是介紹的操作,我們學技術,就要做到知其然知其所以然,所以就有了這篇文章。如果想查看我的上一篇文章,可以點擊這里《Redis進階實踐之十六 Redis大批量增加數據》
1、請求/響應協議和RTT
Redis是使用 客戶端-服務器(Client-Server) 模型的TCP服務器,稱為請求/響應模式。
這意味着通過以下步驟才能完成請求:
1.1、客戶端向服務器發送查詢,並通常以阻塞的方式從套接字讀取服務器響應。
1.2、服務器處理命令並將響應發送回客戶端。
例如,這四個命令序列就是這樣的:
Client: INCR X Server: 1 Client: INCR X Server: 2 Client: INCR X Server: 3 Client: INCR X Server: 4
客戶端和服務器通過網絡鏈路進行連接。這樣的鏈接可以非常快(一個回送接口)或非常慢(通過互聯網在兩台主機之間建立很多跳轉的連接)。無論網絡延遲如何,數據包都會從客戶端傳輸到服務器,然后從服務器傳回客戶端以進行回復。
這個時間來回被稱為RTT(往返時間)。當客戶端需要連續執行多個請求時(例如,將許多元素添加到同一個列表或使用多個鍵填充數據庫),很容易看到這會很影響性能。例如,如果RTT時間為250毫秒(在因特網上連接速度非常慢的情況下),即使服務器能夠每秒處理100k個請求,此時我們也只能夠每秒最多處理四個請求。
如果使用的接口是本地回送接口(loopback),則RTT要短得多(例如,我的主機報告0.0,040毫秒ping 127.0.0.1),但如果您需要連續執行很多寫操作,則仍然需要很多的時間。
幸運的是,有一種方法可以改善這種做法。
2、Redis的管道
請求/響應服務器可以這樣實現,即使客戶端沒有閱讀上一條命令的回復,它也能夠處理新的請求。通過這種方式,可以發送多個命令到服務器而無需等待回復,最后一步讀取回復。
這被稱為管道技術,並且是被廣泛使用的技術。例如,許多POP3協議的實現已經支持這個功能,顯著加快了從服務器下載新電子郵件的過程。
Redis自從早期的版本開始就支持管道的操作,因此無論您運行哪種版本,都可以使用Redis進行管道的操作。這是使用原始netcat實用程序的示例:
[root@linux ~]# (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc 192.168.127.130 6379 +PONG +PONG +PONG
(如果執行nc命令,提示:command not found,安裝命令即可,即:yum install nc)
這次我們沒有為每次通話支付RTT的時間成本,只是把三命令作為了一個命令執行,最后只為這一次執行花費了時間。
非常明確地說,通過管道的操作,我們第一個例子的操作順序如下:
Client: INCR X Client: INCR X Client: INCR X Client: INCR X Server: 1 Server: 2 Server: 3 Server: 4
重要提示:當客戶端使用管道發送多條命令時,服務器將被迫使用內存排隊答復。所以如果你需要使用管道發送大量的命令,最好將這些命令以合理的數目進行分組來批量發送,例如10k命令,讀取回復,然后再發送另一個10k的命令,類似這樣。速度幾乎相同,所使用的額外內存的最大量將是將最大限度地排隊此10k命令的回復所需的數量。
3、這不僅僅是RTT的問題
管道不僅僅是為了減少往返時間所帶來的延遲成本,它實際上可以提高您在給定的Redis服務器上每秒執行的總操作量。這是事實,即在不使用管道的情況下,從訪問數據結構和生成答復的角度來看,每個命令的執行成本都不高的,但從執行套接字 I/O 操作的角度來看,這是非常昂貴的。當涉及調用read()和write()調用的時候,這個調用操作意味着要切換操作環境,要從用戶登陸切換到內核登陸。最后來看,其實上下文切換才是導致速度大幅度的降低的罪魁禍首。
當使用Redis的管道的時候,許多命令通常通過對一個read()函數的系統的調用來讀取,並且通過對一個write()函數的系統的調用來傳遞多個響應。因此,每秒執行的總查詢數量隨着管道的操作呈線性增加,並最終達到未使用管道的基線的10倍,如下圖所示:
4、一些真實世界的代碼示例
在以下基准測試中,我們將使用支持管道的Redis Ruby客戶端來測試由於管道而導致的速度提升:
require 'rubygems' require 'redis' def bench(descr) start = Time.now yield puts "#{descr} #{Time.now-start} seconds" end def without_pipelining r = Redis.new 10000.times { r.ping } end def with_pipelining r = Redis.new r.pipelined { 10000.times { r.ping } } end bench("without pipelining") { without_pipelining } bench("with pipelining") { with_pipelining }
運行上述簡單腳本將在我的Mac OS X系統中提供以下圖形,通過環回接口運行,其中管道將提供最小的改進,其他保持不變,因為RTT已經非常低:
without pipelining 1.185238 seconds with pipelining 0.250783 seconds
正如您所看到的,使用管道,我們將傳輸速度改提升五倍。
5、管道VS腳本
使用Redis腳本(Redis版本2.6或更高版本中可用),可以在服務器端更高效執行處理大量的管道用例的工作。 腳本的一大優點是它能夠以最小的延遲讀取和寫入數據,使得讀取,計算,寫入等操作非常快速(在這種情況下,管道操作不起作用,因為客戶端在調用寫入命令之前需要讀取命令的回復)。
有時,應用程序可能還想在管道中發送EVAL或EVALSHA命令。這是完全可能的,並且Redis通過SCRIPT LOAD命令明確是支持的(它保證可以在沒有失敗風險的情況下調用EVALSHA)。
6、 EVALSHA sha1 numkeys key [key ...] arg [arg ...]
Redis可以使用該命令的版本是2.6.0,或者更高的版本。
時間復雜度:取決於執行的腳本。
通過其SHA1摘要評估緩存在服務器端的腳本。使用SCRIPT LOAD命令將腳本緩存在服務器端。該命令在其他方面與EVAL相同。
7、附錄:為什么即使在回送接口上,一個繁忙的循環也很慢?
即使在本頁面介紹的所有背景下,您仍然可能想知道為什么如在下所示的Redis基准測試中(在偽代碼中),即使在回送接口中執行,並且服務器和客戶端在同一物理機器上運行時,也很慢:
FOR-ONE-SECOND: Redis.SET("foo","bar") END
畢竟,如果Redis進程和基准測試都在同一個框中運行,那么這不僅僅是通過內存將消息從一個地方復制到另一個地方,而沒有任何實際的延遲和實際網絡?
原因是系統中的進程並不總是在運行,實際上是內核調度器讓進程運行的,所以會發生如下的情況,例如,當基准測試程序被允許運行,從Redis服務器讀取回復(與最后執行的命令相關),並寫入新的命令。該命令現在位於回送接口緩沖區中,但為了被服務器讀取,內核調度器應該安排服務器進程(當前在系統調用中阻塞)運行,等等。 因此,實際上,由於內核調度程序的工作原理,回送接口仍然會有網絡延遲的。
基本上,使用一個繁忙的循環來執行基准測試是一件愚蠢的事情,可以在網絡服務器中測量性能時完成相關測試。明智的做法是避免以這種方式做基准測試。
三、結束
大批量插入數據的文章就寫到這里了,這篇文章也介紹了 管道的一些底層的機制,對大家,對我們以后使用Redis 會有好處。等以后我對腳本語言,ruby,或者python學有所成的時候,在通過這些工具來做一些腳本執行批量插入Redis 的實力吧,也會把相應的感受和心得寫出來。繼續努力吧。對了,如果大家想觀看英文,可以《點擊這里》。