Redis客戶端與服務器之間使用TCP協議進行通訊,而且很早就支持管道(pipelining)技術了。在某些高併發的場景下,網絡開銷成了Redis速度的瓶頸,因此須要使用管道技術來實現突破。java
在介紹管道以前,先來想一下單條命令的執行步驟:redis
按照這樣的描述,每一個命令的執行時間 = 客戶端發送時間+服務器處理和返回時間+一個網絡來回的時間bash
其中一個網絡來回的時間是不固定的,它的決定因素有不少,好比客戶端到服務器要通過多少跳,網絡是否擁堵等等。可是這個時間的量級也是最大的,也就是說一個命令的完成時間的長度很大程度上取決於網絡開銷。若是咱們的服務器每秒能夠處理10萬條請求,而網絡開銷是250毫秒,那麼實際上每秒鐘只能處理4個請求。最暴力的優化方法就是使客戶端和服務器在一臺物理機上,這樣就能夠將網絡開銷下降到1ms如下。可是實際的生產環境咱們並不會這樣作。並且即便使用這種方法,當請求很是頻繁時,這個時間和服務器處理時間比較仍然是很長的。服務器
爲了解決這種問題,Redis在很早就支持了管道技術。也就是說客戶端能夠一次發送多條命令,不用逐條等待命令的返回值,而是到最後一塊兒讀取返回結果,這樣只須要一次網絡開銷,速度就會獲得明顯的提高。管道技術其實已經很是成熟而且獲得普遍應用了,例如POP3協議因爲支持管道技術,從而顯著提升了從服務器下載郵件的速度。網絡
在Redis中,若是客戶端使用管道發送了多條命令,那麼服務器就會將多條命令放入一個隊列中,這一操做會消耗必定的內存,因此管道中命令的數量並非越大越好(太大容易撐爆內存),而是應該有一個合理的值。併發
管道並不僅是用來網絡開銷延遲的一種方法,它其實是會提高Redis服務器每秒操做總數的。在解釋緣由以前,須要更深刻的瞭解Redis命令處理過程。socket
一個完整的交互流程以下:高併發
write()
把消息寫入到操做系統內核爲Socket分配的send buffer中read()
讀取消息而後進行處理write()
把返回結果寫入到服務器端的send bufferread()
從recv buffer中讀取消息並返回如今咱們把命令執行的時間進一步細分:優化
命令的執行時間 = 客戶端調用write並寫網卡時間+一次網絡開銷的時間+服務讀網卡並調用read時間++服務器處理數據時間+服務端調用write並寫網卡時間+客戶端讀網卡並調用read時間spa
這其中除了網絡開銷,花費時間最長的就是進行系統調用write()
和read()
了,這一過程須要操做系統由用戶態切換到內核態,中間涉及到的上下文切換會浪費不少時間。
使用管道時,多個命令只會進行一次read()
和wrtie()
系統調用,所以使用管道會提高Redis服務器處理命令的速度,隨着管道中命令的增多,服務器每秒處理請求的數量會線性增加,最後會趨近於不使用管道的10倍。
對於管道的大部分應用場景而言,使用Redis腳本(Redis2.6及之後的版本)會使服務器端有更好的表現。使用腳本最大的好處就是能夠以最小的延遲讀寫數據。
有時咱們也須要在管道中使用EVAL和EVALSHA命令,這是徹底有可能的。所以Redis提供了SCRIPT LOAD命令來支持這種狀況。
多說無益,仍是眼見爲實。下面就來對比一下使用管道和不使用管道的速度差別。
public class JedisDemo {
private static int COMMAND_NUM = 1000;
private static String REDIS_HOST = "Redis服務器IP";
public static void main(String[] args) {
Jedis jedis = new Jedis(REDIS_HOST, 6379);
withoutPipeline(jedis);
withPipeline(jedis);
}
private static void withoutPipeline(Jedis jedis) {
Long start = System.currentTimeMillis();
for (int i = 0; i < COMMAND_NUM; i++) {
jedis.set("no_pipe_" + String.valueOf(i), String.valueOf(i), SetParams.setParams().ex(60));
}
long end = System.currentTimeMillis();
long cost = end - start;
System.out.println("withoutPipeline cost : " + cost + " ms");
}
private static void withPipeline(Jedis jedis) {
Pipeline pipe = jedis.pipelined();
long start_pipe = System.currentTimeMillis();
for (int i = 0; i < COMMAND_NUM; i++) {
pipe.set("pipe_" + String.valueOf(i), String.valueOf(i), SetParams.setParams().ex(60));
}
pipe.sync(); // 獲取全部的response
long end_pipe = System.currentTimeMillis();
long cost_pipe = end_pipe - start_pipe;
System.out.println("withPipeline cost : " + cost_pipe + " ms");
}
}
複製代碼
結果也符合咱們的預期:
withoutPipeline cost : 11791 ms
withPipeline cost : 55 ms
複製代碼
read()
和write()
系統調用,以此來節約時間。前面咱們提到,爲了解決網絡開銷帶來的延遲問題,能夠把客戶端和服務器放到一臺物理機上。可是有時用benchmark進行壓測的時候發現這仍然很慢。
這時客戶端和服務端實際是在一臺物理機上的,全部的操做都在內存中進行,沒有網絡延遲,按理來講這樣的操做應該是很是快的。爲何會出現上面的狀況的呢?
實際上,這是由內核調度致使的。好比說,benchmark運行時,讀取了服務器返回的結果,而後寫了一個新的命令。這個命令就在迴環接口的send buffer中了,若是要執行這個命令,內核須要喚醒Redis服務器進程。因此在某些狀況下,本地接口也會出現相似於網絡延遲的延遲。實際上是內核特別繁忙,一直沒有調度到Redis服務器進程。
Redis源碼
掘金小冊:《Redis 深度歷險:核心原理與應用實踐》