ShardedJedisPipeline 源碼分析

一、什麼是pipeline?什麼是ShardedJedis?

由於pipeline和ShardedJedis的介紹和源碼分析在網上已經有了,本文就不再贅述,直接給出鏈接:

pipeline的介紹:
http://blog.csdn.net/freebird_lb/article/details/7778919

pipeline源碼分析:
http://blog.csdn.net/ouyang111222/article/details/50942893

ShardedJedis :
http://blog.csdn.net/ouyang111222/article/details/50958062

請讀者在繼續閱讀之前確保自己掌握了pipeline和shardedJedis的概念。

二、ShardedJedisPipeline源碼分析

1:怎麼使用?

如同名字一樣,ShardedJedisPipeline是分佈式異步調用的方式,即後端支持多臺Redis實例,並且可以從客戶端以pipeline的方式打包發送命令,先來看看怎麼使用:

    public static void main(String[] args) {
        List<JedisShardInfo> shards = Arrays.asList(
                new JedisShardInfo("IP1", 6379),
                new JedisShardInfo("IP2", 6379),
                new JedisShardInfo("IP3", 6379)
        );
        ShardedJedis shardedJedis = new ShardedJedis(shards);
        ShardedJedisPipeline shardedJedisPipeline = shardedJedis.pipelined();
        for (int i = 0; i < 10; i++) {
            shardedJedisPipeline.set("k" + i, "v" + i);
        }
        shardedJedisPipeline.sync();
    }

因爲客戶端有Hash算法,所以在for循環中set的k1~k9會被打散分配到三臺機器上(爲了模擬效果,也可以在同一臺機器上啓動三個Redis實例),下面是分別去三臺機器上查看key的分佈情況:

第一臺:
127.0.0.1:6379> keys k*
1) "k2"
2) "k0"

第二臺:
127.0.0.1:6379> keys k*
1) "k4"
2) "k5"
3) "k3"
4) "k9"
5) "k8"

第三臺:
127.0.0.1:6379> keys k*
1) "k1"
2) "k6"
3) "k7"

如上所示,k1 ~ k9 分別在不同的機器上,我們接下來把數據拿回來:

        for (int i = 0; i < 10; i++) {
            shardedJedisPipeline.get("k"+i);
        }
        List<Object> list = shardedJedisPipeline.syncAndReturnAll();
        for(Object obj:list) {
            System.out.println(obj);
        }

執行結果如下:

這裏寫圖片描述

這時候難道不應該思考一個問題嗎?

雖然我們get操作是依次 get k1 ~ k9 ,但是由於k1 ~ k9分別在不同的機器上,怎麼保證他們回來的順序呢?請在繼續往下看之前先思考這個問題你會怎麼解決。

2:開始分析

首先整一份Jedis的源碼下來,推薦用IDEA打開,因爲IDEA有功能可以生成類的調用圖http://blog.csdn.net/qq_27093465/article/details/52857307,我生成的類圖如下所示:

這裏寫圖片描述

可以看到ShardedJedisPipeline繼承自PipelineBase,繼續繼承自Queable。我們從get的代碼開始,注意看我的註釋,我保證以最簡單的方式解釋清楚這個問題:

shardedJedisPipeline.get("k"+i);

它的實現在PipelineBase中:

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

我們接着去看看getClient(key) :

    protected Client getClient(String key) {
        /*getShard對key做HASH,同時返回這個key對應的client對象,一個client對象就代表了一條連接,此時返回的對象和set的時候後端對應的Redis機器IP和PORT是一樣的,這樣才能保證這條get命令發出去能去正確的機器上拿回數據*/
        Client client = jedis.getShard(key).getClient();

        /*!!! 關鍵點
         private Queue<Client> clients = new LinkedList<Client>(); 
        上面是clients的定義,是一個隊列,它會按照client的使用順序把它入隊,相當於按照順序保存了每個命令對應的連接(保存的本地端口是關鍵),因爲回來的時候就按照這個順序依次去端口讀取數據了*/
        clients.add(client);
        results.add(new FutureResult(client));
        return client; //最後把client返回
    }

再回去看 this.getClient(key).get(key)其實相當於調用 client.get(key),這樣會把這條命令添加到outputstream,但是不會發送,(因爲是pipeline的方式,最後纔會統一刷新輸出流)this.getResponse(BuilderFactory.STRING)相當於爲每個回來的包準備一塊空間。

接下來我們調用了:

List<Object> list = shardedJedisPipeline.syncAndReturnAll();

去看看syncAndReturnAll()方法:

 public List<Object> syncAndReturnAll() {
    List<Object> formatted = new ArrayList<Object>();

    /* 遍歷clients 隊列,按照先進先出的規則,依次從每個client對象拿出一條(getOne())返回結果。看下面的圖解。
    */
    for (Client client : clients) {
      formatted.add(generateResponse(client.getOne()).get());
    }
    /*將結果添加到formatted返回*/
    return formatted;
  }

這裏寫圖片描述

說明:

  • 因爲有三臺Redis服務器,所以會有三條socket連接,假設他們對應的本地端口爲3333,6666,9999,後面是每個連接的接收緩衝區。
  • Redis服務器是單線程,所以每條連接上接收緩衝區返回的結果一定是按照順序的,比如發送按照getk0,getk2的順序,則結果也是按照這樣返回。
  • clients隊列中記錄了每個client對象,它能標識這條get命令應該去哪個本地端口讀取數據,getone按照Redis協議分隔讀取一條就是相應的結果

就這樣依次出隊,依次解析,現在我們假設隊列讀取到了最後的三條,則情況如下:

這裏寫圖片描述

3:總結

其實這種方法很巧妙的原因也得益於Redis是一個單線程的服務器,對於發送向它的命令,總是按照發送的順序返回,也正是這樣,纔能有pipeline這種方式,不然多線程各自都有自己的緩衝區,自己如果處理完就返回了,這樣是沒法玩的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章