巧用 Redis pipeline 命令,解決真實的生產問題
本文轉載自微信公眾號「Java極客技術」,作者鴨血粉絲。轉載本文請聯系Java極客技術公眾號。
Hello,大家好,我是阿粉~
最近阿粉接到了一個業務需求,需要開發一個業務接口,批量刪除 Redis 中數據。
這個功能點其實很簡單,只要讓外部傳入需要刪除鍵信息,然后在接口內部遍歷調用刪除命令即可。
按照這個思路,功能很快就開發完成,然后順利的上線。
上線之后,運行一段時間,調用業務方反饋,當要刪除的數據很多的時候,這個接口響應時間就比較長,然后希望我們這邊優化一下,降低響應時間。
那優化辦法其實有很多,比如使用多線程刪除等,不過這一次并沒有采用這個,最終使用了 Redis pipeline(管道)命令進行了優化。
所以今天這篇文章就給大家介紹一下 Redis pipeline 命令,以及相關原理,文章涉及到知識點如下圖所示:
為什么多次調用 Redis 命令比較慢Redis 客戶端執行一個命令需要經歷流程如下圖所示:
總共需要經過四個流程:
- 客戶端發送命令
- Redis 服務收到命令等待處理
- Redis 服務端處理命令
- Redis 服務返回執行結果
Redis 的客戶端與服務可能部署在不同的機器上,這里我們假設 Redis 客戶端部署在北京,而 Redis 服務端在廣州,兩地的網絡延時為 50ms。
一次 Redis 命令,1 與 4 這兩個流程就需要耗費 100ms, 而 2 與 3 在由于是在 Redis 服務端執行,執行速度會很快,可以忽略不計。
此時客戶端如果需要執行 N 次 Redis 命令,我們就需要耗費 2N*100ms 時間,執行命令越多,耗時越長。
這就是文章開頭 Redis 刪除多個命令比較慢的主要原因。
Redis pipeline 流水線執行命令那如何解決這類問題了?
解決辦法有三種,第一種利用多線程機制,并行執行命令,提高執行速度。
第二種,調用 mget 這類命令,這類命令可以一次操作多個鍵,Redis 服務端收到命令之后,將會批量執行。
但是 mget這類批量命令畢竟是少數,很多情況下我們沒辦法直接使用,就像我們上面的例子。
這樣的話,只能使用最后一種辦法,使用 Redis pipeline命令。
開啟 Redis pipeline 之后,再執行 Redis 的其他命令,命令將不會發送給服務端,而是先暫存在客戶端,左后等到所有命令都執行完,然后再統一發送給服務端。
服務端會根據發送過來的命令的順序,依次運行計算。
然后同樣先將結果暫存服務端,等到命令都執行完畢之后,統一返回給客戶端。
通過這種方式,減少多個命令之間網絡交互,有效的提高多個命令執行的速度。
如上圖所示,開啟 Redis Pipeline 之后,客戶端運行的 5 個命令將會一起發送到服務端。服務依次運行命令,然后統一返回。
介紹完原理,我們來看下如何使用 Redis Pipeline ,下面代碼以 Jedis 為例。
- JedisPoolConfig poolConfig = new JedisPoolConfig();
- poolConfig.setMaxIdle(100);
- poolConfig.setTestOnBorrow(false);
- poolConfig.setTestOnReturn(false);
- JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", Integer.parseInt("6379"), 60*1000, "1234qwer");
- Jedis jedis = jedisPool.getResource();
- Pipeline pipelined = jedis.pipelined();
- for (int i = 0; i < 100; i++) {
- pipelined.set("key" + i, "value" + i);
- }
- pipelined.sync();
Jedis#pipelined 將會開啟 Redis Pipeline,而Pipeline 這個類提供所有 Redis 可以使用的命令:
當執行完所有的命令之后,調用 Pipelined#sync 命令,所有命令數據將會統一發送到到 Redis 中。
上面的例子中,Pipelined#sync 方法調用之后不會返回任何結果。
如果此時需要處理 Redis 的返回值,那么我們需要調用 Pipelined#syncAndReturnAll 方法,這個方法返回值將會是一個集合,返回結果按照 Redis 命令的順序排序。
解密 pipeline 實現原理
Redis pipeline 命令的實現,其實需要客戶端與服務端同時支持,并且實際執行過程中,Redis pipeline 會根據需要發送命令數據量大小進行拆分,拆分成多個數據包進行發送。
這么做主要原因是因為,如果一次組裝 pipeline 數據量過大,一方面會增加客戶端的等待時間,而另一方面會造成一定的網絡阻塞。
不同 Redis 客戶端 pipeline 發送的最大字節數不太相同,比如 jedis-pipeline 每次最大發送字節數為8192。
下面我們從源碼側,看下 jedis pipeline 實現機制。
Pipeline 所有命令方法,底層最終將會調用 Protocol#sendCommand方法,這個方法主要就是向 RedisOutputStream 輸出流中寫入數據。
RedisOutputStream#write方法如下圖所示:
這個方法內,一旦緩沖的數據大小超過指定大小,目前為 8192,就會立刻將數據全部寫入到真正輸出流中。
pipeline 多個命令實際發送流程圖如下所示:
一旦 Redis 客戶端將部分 pipeline 中執行命令的發送給 Redis 服務端,服務端就會立即運行這些命令,然后返回給客戶端。
但是此時客戶端并不會去讀取,所以返回的響應數據將會暫存在客戶端的 Socket 接收緩沖區中。
如果響應數據比較大,填滿緩沖區,此時客戶端會通過 TCP 流量控制機制,ACK 返回 WIN=0(接收窗口)來控制服務端不要再發送數據。
這時這些響應數據將會一直暫存在 Redis 服務端輸出緩存中,如果數據比較多,將會占用很多內存。
所以使用 Redis Pipeline 機制一定注意返回的數據量,如果數據很多,建議將包含大量命令的 pipeline 拆分成多次較小的 pipeline 來完成。
總結Redis 的 pipeline 命令可以批量執行多個 redis 命令,它通過減少網絡的調用次數,從而有效提高的多個命令執行的速度。
不過我們使用過程,一定主要執行數據的大小,如果數據過大,可以考慮將一個 pipeline 拆