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();
}
}