一、什麼是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這種方式,不然多線程各自都有自己的緩衝區,自己如果處理完就返回了,這樣是沒法玩的。