Redis學習一:管道技術

簡介

我們在項目中大量使用redis,卻很少停下腳步細細研究它到底是個什麼東西。有人說它是nosql的一種,有人說它是緩存。redis官網中說到Redis是一個開源的基於內存的數據結構存儲器,可用作數據庫、緩存、消息代理(Message Broker)。提供了字符串、hash表、list、set、有序集合等非常豐富的數據結構。Redis具有內置的版本複製、Lua腳本、LRU回收、事物和多層次的磁盤持久化,還通過Redis Sentinel 實現了高可用性、通過redis cluster實現自動分區。
總的來說,Redis是一個基於內存實現的key-value形式的存儲結構。接下來,小方想從管道技術、發佈訂閱、事物這幾個點入手學習。

Redis管道技術

redis中使用了管道技術(pipeline)來加速查詢。
Redis是一個使用客戶端/服務端模式的TCP服務,這也就是說一個請求要經歷如下的過程:
(1)客戶端發送查詢請求到服務端,通常以阻塞的方式等待從socket中讀取服務端的響應。
(2)服務端處理查詢的命令,發送響應給客戶端。
假如客戶端要執行如下的四條命令,

  • Client: INCR X
  • Server: 1
  • Client: INCR X
  • Server: 2
  • Client: INCR X
  • Server: 3
  • Client: INCR X
  • Server: 4

如果像http協議一樣每發一個請求都必須等待響應再發下一條,上面的例子至少需要8個tcp包。實際上對於redis來說沒有這個必要,使用管道技術,客戶端可以在未收到響應的時候繼續發送請求,可以將多個請求一起發送,服務端將最終的結果返回。
使用管道技術,上面的例子可以優化爲

  • Client: INCR X
  • Client: INCR X
  • Client: INCR X
  • Client: INCR X
  • Server: 1
  • Server: 2
  • Server: 3
  • Server: 4

使用java API測試管道技術,分別在使用管道和不使用管道的情況下進行10000次的自增操作。

public class RedisTest {

    @Test
    public void testRedis(){
        System.out.println();
        long start = System.currentTimeMillis();
        usePipeline();
        long end = System.currentTimeMillis();
        System.out.println("usePipeline:"+(end - start));

        start = System.currentTimeMillis();
        withoutPipeline();
        end = System.currentTimeMillis();
        System.out.println("withoutPipeline:"+(end - start));
    }

    private static void withoutPipeline() {
        try {
            Jedis jedis = new Jedis("127.0.0.1", 6379);
            for (int i = 0; i < 10000; i++) {
                jedis.incr("test2");
            }
            jedis.disconnect();
        } catch (Exception e) {
        }
    }

    private static void usePipeline() {
        try {
            Jedis jedis = new Jedis("127.0.0.1", 6379);
            Pipeline pipeline = jedis.pipelined();
            for (int i = 0; i < 10000; i++) {
                pipeline.incr("test2");
            }
            pipeline.sync();
            jedis.disconnect();
        } catch (Exception e) {
        }
    }
}

測試結果:
usePipeline:4
withoutPipeline:349
差別非常明顯。
作爲redis的使用者,服務端如何實現無須過多的關注(其實是因爲小方看不懂c),下面來看java中封裝的Jedis和Pipeline。
在使用Jedis的過程中,一般情況下我們調用的都是redis.clients.jedis.Jedis這個類中的方法,以set方法爲例

public String set(String key, String value) {
        this.checkIsInMultiOrPipeline();
        this.client.set(key, value);
        return this.client.getStatusCodeReply();
        }

在每次調用client的方法之前,都會進行事務和管道的檢查,因爲Jedis不能進行有事務的操作,帶事務的連接要用redis.clients.jedis.Transaction類,同樣Jedis也不能進行管道的操作,要用redis.clients.jedis.Pipeline類。也就是說Jedis中的操作都是無事務的不使用管道的???

protected void checkIsInMultiOrPipeline() {
        if (this.client.isInMulti()) {
            throw new JedisDataException("Cannot use Jedis when in Multi. Please use Transation or reset jedis state.");
        } else if (this.pipeline != null && this.pipeline.hasPipelinedResponse()) {
            throw new JedisDataException("Cannot use Jedis when in Pipeline. Please use Pipeline or reset jedis state .");
        }
    }

那如果要使用管道呢,調用jedis.pipelined()獲取管道的實例,之後通過Pipeline操作redis。

public Pipeline pipelined() {
        this.pipeline = new Pipeline();
        this.pipeline.setClient(this.client);
        return this.pipeline;
    }

再來看Pipeline 的set方法

public Response<String> set(String key, String value) {
        this.getClient(key).set(key, value);
        return this.getResponse(BuilderFactory.STRING);
    }

僅僅set key-value的過程中看不出什麼問題,因爲兩者調用的都是redis.clients.jedis.Client中的方法。那麼這個時候就想兩者的區別在於響應信息的讀取嗎?
Pipeline中也區分了兩種模式,如果是事務模式(即isInMulti爲true)使用ArrayList來暫存響應,不是則使用LinkedList來暫存響應。下面僅看非事務模式。
實際上,this.getResponse(BuilderFactory.STRING)這行代碼並不是獲取響應,而是將響應放入緩存,並返回響應的實例。
在這裏插入圖片描述
既然返回響應的方式不一樣了,那麼編碼方式也不一樣了。

		Jedis jedis = new Jedis("127.0.0.1",6379);
        Pipeline pipeline = jedis.pipelined();
        Response<String> response = pipeline.get("test2");
       	//System.out.println(response.get());//在未調用sync()之前,直接get會報JedisDataException
        pipeline.sync();
        System.out.println(response.get());

一定要在sync之後再讀取響應,因爲sync才真正將緩存的響應放到了Response中,也就是說我們可以在sync之前進行大量的操作,最後在sync一次性獲取所有響應。

public void sync() {
        if (this.getPipelinedResponseLength() > 0) {
            List<Object> unformatted = this.client.getAll();
            Iterator var2 = unformatted.iterator();

            while(var2.hasNext()) {
                Object o = var2.next();
                this.generateResponse(o);
            }
        }

    }

總結

畢竟緩存響應使用的是ArrayList或者LinkedList,存儲空間有限,不宜一次性發送過多的命令。個人認爲,管道技術適合於不需要關注中間結果的頻繁操作,在發送完命令之後,最後一次性讀取結果,僅操作單個key無需使用管道。

下次更新:

Redis發佈訂閱

Redis事物

面試中常見的問題

  1. 爲什麼單線程的redis會認爲比多線程的數據庫操作快?
  2. 高併發環境中如何解決資源競爭問題
  3. redis適用的場景,優缺點
  4. redis與memcached
  5. redis的事物
  6. redis的緩存失效策略和主鍵失效機制
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章