JedisCluster JedisCluster是針對RedisCluster的JAVA用戶端,它封裝了java訪問redis叢集的各種操作,包括初始化串連,請求重新導向等操作。具體內部實現原理主要有如下兩個方面:
1.1. JedisCluster初始化時,所有的叢集串連資訊都是封裝在JedisClusterInfoCache這類中。
1.2. JedisClusterInfoCache類中有兩個非常重要的Map資料結構,分別是
/** **nodes存放叢集IP地址資訊和JedisPool。其中String是IP:Port資訊,叢集中有多少個節點,IP:Port就有多少個。 **/ Map<String, JedisPool> nodes = new HashMap<String, JedisPool>(); /** **slots中儲存key-value是叢集中資料槽的編號和jedisPool執行個體。即slots中有16384個索引值對。 **/ Map<Integer, JedisPool> slots = new HashMap<Integer, JedisPool>();
JedisCluster類圖
1. 從圖中可以看出,JedisCluster主要是繼承二進位的BinaryJedisCluster類,這個類中的各種操作都是基於位元組數組方式進行的。而且BinaryJedisCluster類實現的4個介面中有3個是基於位元組數組操作。
2. JedisCluster實現了JedisCommands,MultiKeyJedisClusterCommands,JedisClusterScriptingCommands介面。這三個介面提供的基於字串類型的操作,即key都是字串類型。
3. BasicCommands是關於redis服務本身基本操作,比如save,ping,bgsave等操作。
4. MultiKeyBinaryJedisClusterCommands和MultiKeyJedisClusterCommands介面一個位元組數組的大量操作,一個是字串的大量操作。 JedisClusterCommand 在JedisCluster用戶端中,JedisClusterCommand是一個非常重要的類,採用模板方法設計,高度封裝了操作Redis叢集的操作。對於叢集中各種儲存操作,提供了一個抽象execute方法。 JedisCluster各種具體操作Redis叢集方法,只需要通過匿名內部類的方式,靈活擴充Execute方法。 內部通過JedisClusterConnectionHandler封裝了Jedis的執行個體。 JedisClusterCommand源碼分析:
/****該類主要由兩個重點:採用模板方法設計,具體存取操作有子類實現**在叢集操作中,為了保證高可用方式,採用遞迴演算法進行嘗試發生的MOVED,ASK,資料移轉操作等。**/public abstract class JedisClusterCommand<T> { // JedisCluster的串連真正持有類 private JedisClusterConnectionHandler connectionHandler; // 嘗試次數,預設為5 private int maxAttempts; private ThreadLocal<Jedis> askConnection = new ThreadLocal<Jedis>(); public JedisClusterCommand(JedisClusterConnectionHandler connectionHandler, int maxAttempts) { this.connectionHandler = connectionHandler; this.maxAttempts = maxAttempts; } // redis各種操作的抽象方法,JedisCluster中都是匿名內部類實現。 public abstract T execute(Jedis connection); // public T run(String key) { if (key == null) { throw new JedisClusterException("No way to dispatch this command to Redis Cluster."); } return runWithRetries(SafeEncoder.encode(key), this.maxAttempts, false, false); } /*** *** 該方法採用遞迴方式,保證在往叢集中存取資料時,發生MOVED,ASKing,資料移轉過程中遇到問題,也是一種實現高可用的方式。 ***該方法中調用execute方法,該方法由子類具體實現。 ***/ private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) { if (attempts <= 0) { throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?"); } Jedis connection = null; try { /** *第一執行該方法,asking為false。只有發生JedisAskDataException *異常時,才asking才設定為true **/ if (asking) { connection = askConnection.get(); connection.asking(); // if asking success, reset asking flag asking = false; } else { // 第一次執行時,tryRandomNode為false。 if (tryRandomNode) { connection = connectionHandler.getConnection(); } else { /** 根據key擷取分配的嘈數,然後根據資料槽從JedisClusterInfoCache 中擷取Jedis的執行個體 **/ connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key)); } } /*** ** 調用子類方法的具體實現。 **/ return execute(connection); } catch (JedisNoReachableClusterNodeException jnrcne) { throw jnrcne; } catch (JedisConnectionException jce) { //釋放已有的串連 releaseConnection(connection); connection = null; /*** ***只是重建索引值對slot-jedis緩衝即可。已經沒有剩餘的redirection了。 ***已經達到最大的MaxRedirection次數,拋出異常即可。 ***/ if (attempts <= 1) { this.connectionHandler.renewSlotCache(); throw jce; } // 遞迴調用該方法 return runWithRetries(key, attempts - 1, tryRandomNode, asking); } catch (JedisRedirectionException jre) { // if MOVED redirection occurred, if (jre instanceof JedisMovedDataException) { // 發生MovedException,需要重建索引值對slot-Jedis的緩衝。 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, attempts - 1, false, asking); } finally { releaseConnection(connection); } } private void releaseConnection(Jedis connection) { if (connection != null) { connection.close(); } }}
JedisClusterInfoCache 這個緩衝類主要完成如下功能:
1.1. 緩衝索引值對IP:Port——>JedisPool,緩衝索引值對slot——>JedisPool。 將redisCluster中的每一個資料槽對應的jedis執行個體事先加入緩衝,每個資料節點的串連資訊緩衝到本地中。 該類中最重要的方法就是discoverClusterNodesAndSlots(Jedis),源碼如下:
public void discoverClusterNodesAndSlots(Jedis jedis) { w.lock(); try { reset(); /**根據當前redis執行個體,擷取叢集中master,slave節點資訊。包括每個master節點 上分配的資料嘈。slots結果如下: ****[10923, 16383, [[B@924fda2, 9000], [[B@5b879b5e, 9001]]] *** [[5461, 10922, [[B@3681fe9a, 7001], [[B@10724c6b, 8000]], *** [0, 5460, [[B@3ff70d3c, 7000], [[B@7485fef2, 8001]], ***/ List<Object> slots = jedis.clusterSlots(); // 遍曆List中的集合,總共就3個master節點資訊 for (Object slotInfoObj : slots) { // slotInfo指一個master,slave,分配槽的個數資訊 List<Object> slotInfo = (List<Object>) slotInfoObj; if (slotInfo.size() <= MASTER_NODE_INDEX) { continue; } // 擷取分配到每一個master節點的資料槽的個數 List<Integer> slotNums = getAssignedSlotArray(slotInfo); /**slotInfo的大小為4,其中前兩項為槽數的最小值和最大值。 *** 後兩項為master,slave執行個體資訊 ***[10923, 16383, [[B@924fda2, 9000], [[B@5b879b5e, 9001]]] ***/ int size = slotInfo.size(); for (int i = MASTER_NODE_INDEX; i < size; i++) { // hostInfos就是[B@924fda2, 9000]集合。就倆元素 List<Object> hostInfos = (List<Object>) slotInfo.get(i); if (hostInfos.size() <= 0) { continue; } //根據ip,port構建HostAndPort執行個體 HostAndPort targetNode = generateHostAndPort(hostInfos); /**根據HostAndPort解析出ip:port的key值, **再根據key從緩衝中查詢對應的jedisPool執行個體。如果沒有jedisPool執行個體, **就建立JedisPool執行個體,最後放入緩衝中。 ** key的值是 ip:port,value的值是jedisPool **/ setupNodeIfNotExist(targetNode); if (i == MASTER_NODE_INDEX) { // 將每一個資料槽對應的jedisPool緩衝起來。 // key的值是:資料槽的下標,value的值是 JedisPool。 // slots緩衝中總共有16384的key-value索引值對 assignSlotsToNode(slotNums, targetNode); } } } } finally { w.unlock(); } }