對象池技術在服務器開發上應用廣泛。在各種對象池的實現中,尤其以數據庫的連接池最爲明顯,可以說是每個服務器必須實現的部分。
apache common pool 官方文檔可以參考:https://commons.apache.org/proper/commons-pool/。
結合JedisPool看Commons Pool對象池技術
結合JedisPool,我們來了解一下commons pool的整體設計:
面向用戶的往往是ObjectPool,用戶看到的是一個對象池,對於使用Redis連接的用戶來說,就是JedisPool。對象池ObjectPool提供了借用對象,返還對象,驗證對象等API,需要具體的配置GenericObjectPoolConfig來確定池的大小,以及創建具體池化對象的工廠接口PooledObjectFactory來根據需要創建,銷燬,激活,鈍化每個對象。
PooledObjectFactory接口,用來創建池對象(makeObject),將不用的池對象進行鈍化(passivateObject),對要使用的池對象進行激活(activateObject),對池對象進行驗證(valiateObject),將有問題的池對象銷燬(destroyObject)。
如果需要使用commons-pool,那麼就需要提供一個PooledObjectFactory接口的具體實現,一個比較簡單的辦法是使用BasePooledObjectFactory這個抽象類,只需要實現兩個方法:create()和wrap(T obj)。JedisFactory也就是用來創建每個Jedis連接的對象工廠類,其中直接實現了PooledObjectFactory,makeObject的過程中,直接創建了PooledObject<Redis>。
當我們使用JedisPool.getResource(),用於返回jedis連接時,實際調用的是其中GenericObjectPool的borrowObject方法,在Jedis連接池中借用一個對象。
借用對象時,先去idleObjects(LinkedBlockingDeque<Pooled<Jedis>>)列表中查看是否有空閒的對象,如果存在則直接使用;如果不存在,則需要考慮在沒有超出連接池最大數量的情況下,使用PooledObjectFactory進行初始化,這裏使用的是JedisFactory.makeObject來創建連接,並將其激活。
對於Jedis對象,不能總是重用同一個對象,在使用一段時間後其就會產生失效,連接出現異常。此時就需要使用JedisPool來獲取資源,注意在最後要回收資源,實際上就是returnObject,以下面的代碼作爲實例:
Jedis jedis = jedisPool.getResource();
try {
while (true) {
String productCountString = jedis.get("product");
if (Integer.parseInt(productCountString) > 0) {
if (acquireLock(jedis, "abc")) {
int productCount = Integer.parseInt(jedis.get("product"));
System.out.println(String.format("%tT --- Get product: %s", new Date(), productCount));
// System.out.println(productCount);
jedis.decr("product");
releaseLock(jedis, "abc");
return "Success";
}
Thread.sleep(1000L);
} else {
return "Over";
}
}
} finally {
jedis.close();
}
JedisCluster的連接/執行源碼研究
我們使用的JedisCluster(Redis集羣模式)需要初始化並使用JedisCluster對象,通過該對象來進行Redis的相關操作,下面就列舉出了JedisCluster的基本類圖結構:
在執行任務BinaryJedisCluster的相關命令 set/get/exist 等redis命令時,都採用回調的方式:
@Override
public String set(final byte[] key, final byte[] value) {
return new JedisClusterCommand<String>(connectionHandler, maxRedirections) {
@Override
public String execute(Jedis connection) {
return connection.set(key, value);
}
}.runBinary(key);
}
初始化一個JedisClusterCommand對象,執行runBinary方法,進行execute(Jedis connection)回調,其實可以看出執行回調之前的作用是將使用到的Jedis連接在內部統一管理起來。
可以猜想使用了JedisSlotBasedConnectionHandler中實現了父類定義的getConnection()獲取Redis連接的方法:
@Override
public Jedis getConnection() {
// In antirez's redis-rb-cluster implementation,
// getRandomConnection always return valid connection (able to
// ping-pong)
// or exception if all connections are invalid
List<JedisPool> pools = getShuffledNodesPool();
for (JedisPool pool : pools) {
Jedis jedis = null;
try {
jedis = pool.getResource();
if (jedis == null) {
continue;
}
String result = jedis.ping();
if (result.equalsIgnoreCase("pong")) return jedis;
pool.returnBrokenResource(jedis);
} catch (JedisConnectionException ex) {
if (jedis != null) {
pool.returnBrokenResource(jedis);
}
}
}
throw new JedisConnectionException("no reachable node in cluster");
}
其中調用的方法 getShuffledNodesPool(),就是從JedisClusterInfoCache中包含的所有JedisPool,執行shuffle操作,隨機拿到對應的JedisPool,去其中getResource拿到連接。
這屬於隨機去獲取connection,但事實上並不是這樣處理的,我們可以通過slot來獲得其對應的Connection,在JedisClusterCommand.run方法的最後一行中,其中第三個參數爲是否爲tryRandomMode,調用方式顯示爲非random Mode。
return runWithRetries(SafeEncoder.encode(keys[0]), this.redirections, false, false);
可以根據slot來定位到具體的JedisPool,getResource拿到對應的Jedis Connection,但該方法也標明瞭不能保證一定能夠拿到可用的連接。
@Override
public Jedis getConnectionFromSlot(int slot) {
JedisPool connectionPool = cache.getSlotPool(slot);
if (connectionPool != null) {
// It can't guaranteed to get valid connection because of node
// assignment
return connectionPool.getResource();
} else {
return getConnection();
}
}
在JedisClusterInfoCache緩存了Map<String,JedisPool>(host:port->JedisPool)和Map<Integer, JedisPool>(slot->JedisPool),用於查詢連接,那麼這兩個緩存是如何查詢出來的,這就需要用到Jedis.clusterNodes,它可以通過該Redis連接找到其他連接的相關配置,例如可以發現整個集羣的配置,其中三個master,三個slave,並且能夠識別出自身連接,可參考文檔:http://redis.io/commands/cluster-nodes:
5974ed7dd81c112d9a2354a0a985995913b4702c 192.168.1.137:6389 master - 0 1468809898374 26 connected 0-5640
d08dc883ee4fcb90c4bb47992ee03e6474398324 192.168.1.137:6390 master - 0 1468809898875 25 connected 5641-11040
ffb4db4e1ced0f91ea66cd2335f7e4eadc29fd56 192.168.1.138:6390 slave 5974ed7dd81c112d9a2354a0a985995913b4702c 0 1468809899376 26 connected
c69b521a30336caf8bce078047cf9bb5f37363ee 192.168.1.137:6388 master - 0 1468809897873 28 connected 11041-16383
532e58842d001f8097fadc325bdb5541b788a360 192.168.1.138:6389 slave c69b521a30336caf8bce078047cf9bb5f37363ee 0 1468809899876 28 connected
aa52c7810e499d042e94e0aa4bc28c57a1da74e3 192.168.1.138:6388 myself,slave d08dc883ee4fcb90c4bb47992ee03e6474398324 0 0 19 connected
分配slot只可能在master節點上發生,而不會在slave節點上發生,這意味着Redis集羣並未進行類似讀寫分離的形式。當Redis集羣的slot發生改變時,會重新初始化該Cache,重置slot。
而執行每個get/set等Redis操作時,真正的核心入口,其實是JedisClusterCommand.runWithRetries方法:
private T runWithRetries(byte[] key, int redirections, boolean tryRandomNode, boolean asking) {
if (redirections <= 0) {
throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");
}
Jedis connection = null;
try {
if (asking) {
// TODO: Pipeline asking with the original command to make it
// faster....
connection = askConnection.get();
connection.asking();
// if asking success, reset asking flag
asking = false;
} else {
if (tryRandomNode) {
connection = connectionHandler.getConnection();
} else {
connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
}
}
return execute(connection);
} catch (JedisConnectionException jce) {
if (tryRandomNode) {
// maybe all connection is down
throw jce;
}
// release current connection before recursion
releaseConnection(connection);
connection = null;
// retry with random connection
return runWithRetries(key, redirections - 1, true, asking);
} catch (JedisRedirectionException jre) {
// if MOVED redirection occurred,
if (jre instanceof JedisMovedDataException) {
// it rebuilds cluster's slot cache
// recommended by Redis cluster specification
this.connectionHandler.renewSlotCache(connection);
}
// release current connection before recursion or renewing
releaseConnection(connection);
connection = null;
if (jre instanceof JedisAskDataException) {
asking = true;
askConnection.set(this.connectionHandler.getConnectionFromNode(jre.getTargetNode()));
} else if (jre instanceof JedisMovedDataException) {
} else {
throw new JedisClusterException(jre);
}
return runWithRetries(key, redirections - 1, false, asking);
} finally {
releaseConnection(connection);
}
}
出現的Redis Retries問題
可以參考:http://carlosfu.iteye.com/blog/2251034,講的非常好。同樣,我們的出現的異常堆棧:
- 2016-06-04 00:02:51,911 [// - - ] ERROR xxx - Too many Cluster redirections?
redis.clients.jedis.exceptions.JedisClusterMaxRedirectionsException: Too many Cluster redirections?
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:97)
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:131)
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:152)
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:131)
直譯過來就是出現過多的redirections異常,出現過JedisConnectionException,完整的堆棧內容:
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:198)
at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
at redis.clients.jedis.Protocol.process(Protocol.java:141)
at redis.clients.jedis.Protocol.read(Protocol.java:205)
at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:297)
at redis.clients.jedis.Connection.getBinaryBulkReply(Connection.java:216)
at redis.clients.jedis.Connection.getBulkReply(Connection.java:205)
at redis.clients.jedis.Jedis.get(Jedis.java:101)
at redis.clients.jedis.JedisCluster$3.execute(JedisCluster.java:79)
at redis.clients.jedis.JedisCluster$3.execute(JedisCluster.java:76)
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:119)
at redis.clients.jedis.JedisClusterCommand.run(JedisClusterCommand.java:30)
at redis.clients.jedis.JedisCluster.get(JedisCluster.java:81)
at redis.RedisClusterTest.main(RedisClusterTest.java:30)
調試狀態下的異常信息:
jce = {redis.clients.jedis.exceptions.JedisConnectionException@1014} "redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream."
detailMessage = "Unexpected end of stream."
cause = {redis.clients.jedis.exceptions.JedisConnectionException@1014} "redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream."
stackTrace = {java.lang.StackTraceElement[0]@1017}
suppressedExceptions = {java.util.Collections$UnmodifiableRandomAccessList@1018} size = 0
關於這個問題,可以參考:http://blog.csdn.net/jiangguilong2000/article/details/45025355
客戶端buffer控制。在客戶端與server進行的交互中,每個連接都會與一個buffer關聯,此buffer用來隊列化等待被client接受的響應信息。如果client不能及時的消費響應信息,那麼buffer將會被不斷積壓而給server帶來內存壓力.如果buffer中積壓的數據達到閥值,將會導致連接被關閉,buffer被移除。
開發環境上執行查詢該參數的命令:config get client-output-buffer-limit
1) "client-output-buffer-limit"
2) "normal 0 0 0 slave 268435456 67108864 60 pubsub 33554432 8388608 60"
關於Redis上的所有參數詳解,可以參考:http://shift-alt-ctrl.iteye.com/blog/1882850
JedisMovedDataException
jre = {redis.clients.jedis.exceptions.JedisMovedDataException@2008} "redis.clients.jedis.exceptions.JedisMovedDataException: MOVED 8855 192.168.1.137:6390"
targetNode = {redis.clients.jedis.HostAndPort@2015} "192.168.1.137:6390"
slot = 8855
detailMessage = "MOVED 8855 192.168.1.137:6390"
cause = {redis.clients.jedis.exceptions.JedisMovedDataException@2008} "redis.clients.jedis.exceptions.JedisMovedDataException: MOVED 8855 192.168.1.137:6390"
stackTrace = {java.lang.StackTraceElement[0]@1978}
suppressedExceptions = {java.util.Collections$UnmodifiableRandomAccessList@1979} size = 0
日誌中出現超時異常:
4851:S 18 Jul 11:05:38.005 * Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.
可以參考github上關於redis的討論:https://github.com/antirez/redis/issues/641,關閉AOF,可以暫時解決問題。JedisCluster中應用的Apache Commons Pool對象池技術