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