使用Jedis操作Redis Cluster,我们需要创建JedisCluster对象,再通过JedisCluster对象实例操作数据,代码一般如下:
// 初始化所有节点(例如6个节点)
Set jedisClusterNode = new HashSet() {{
add(new HostAndPort("127.0.0.1", 6379));
add(new HostAndPort("127.0.0.1", 6380));
add(new HostAndPort("127.0.0.1", 6381));
add(new HostAndPort("127.0.0.1", 6382));
add(new HostAndPort("127.0.0.1", 6383));
add(new HostAndPort("127.0.0.1", 6384));
}};
// 初始化commnon-pool连接池,并设置相关参数
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
// 初始化JedisCluster
JedisCluster jedisCluster = new JedisCluster(jedisClusterNode, 1000, 1000, 5, poolConfig);
下面我们详细了解下JedisCluster的创建逻辑,其构造函数核心功能为创建connectionHandler实例,JedisClusterConnectionHandler的构造函数如下:
public JedisClusterConnectionHandler(Set nodes,
final GenericObjectPoolConfig poolConfig, int connectionTimeout,
int soTimeout, String password, String clientName,
boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters,
HostnameVerifier hostnameVerifier, JedisClusterHostAndPortMap portMap) {
this.cache = new JedisClusterInfoCache(poolConfig, connectionTimeout, soTimeout, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, portMap);
initializeSlotsCache(nodes, connectionTimeout, soTimeout, password, clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
}
其中重点需要关注的是initializeSlotsCache方法,该方法主要初始化集群中槽和节点的映射关系,下面简单介绍下Redis Cluster的数据分区和路由方案。
Redis Cluster使用虚拟槽数据分区方案,每个节点维护部分槽和数据,当需要读取数据时,使用"CRC16(key) & 16383"哈希算法得到key所在的槽,Redis Cluster本身会维护节点和槽的信息。
当你使用Dummy型客户端(例如redis-cli)在某个不具备该槽的节点上执行读取指令时,Redis Cluster会返回"MOVED"重定向信息,告诉我们该槽所在节点的IP和端口信息,然后就可以到相应的节点上执行命令读取数据。该类型的客户端需要我们自行进行重定向操作,相对不方便。
而Jedis等Smart型客户端本身就维护了槽和节点的映射关系,通过"CRC16(key) & 16383"哈希算法得到key所在的槽后,就可以通过映射关系找到对应的节点,然后直接给该节点发送指令返回数据。
下面为initializeSlotsCache方法源码:
private void initializeSlotsCache(Set startNodes,
int connectionTimeout, int soTimeout, String password, String clientName,
boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters, HostnameVerifier hostnameVerifier) {
for (HostAndPort hostAndPort : startNodes) {
Jedis jedis = null;
try {
jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout, soTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
if (password != null) {
jedis.auth(password);
}
if (clientName != null) {
jedis.clientSetname(clientName);
}
cache.discoverClusterNodesAndSlots(jedis);
break;
} catch (JedisConnectionException e) {
// try next nodes
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
需要注意的是,由于discoverClusterNodesAndSlots内使用了WriteLock写锁,因此保证了只有一个连接会初始化节点和槽的映射关系。
重点分析下discoverClusterNodesAndSlots方法:
public void discoverClusterNodesAndSlots(Jedis jedis) {
//写锁保证只有一个连接进行初始化
w.lock();
try {
//清空旧的节点和槽数据,并关闭节点连接
reset();
//使用"cluster slots"指令获取Cluster节点和槽的映射关系
/**
* 返回结构如下:
* 127.0.0.1:6379> cluster slots
* 1) 1) (integer) 5462 -- 起始槽
* 2) (integer) 10922 -- 终止槽
* 3) 1) "127.0.0.1" -- 主节点
* 2) (integer) 6380
* 3) "85371dd3c2c11dbb1cd506ed028de10bb7fa2816"
* 4) 1) "127.0.0.1" -- 从节点
* 2) (integer) 6383
* 3) "01740d2eb2c9f89014e2b8b673d444753d0685cd"
* 2) 1) (integer) 10923
* 2) (integer) 16383
* 3) 1) "127.0.0.1"
* 2) (integer) 6381
* 3) "aacad0a5a2b37d4e11f2253849263575adb78740"
* 4) 1) "127.0.0.1"
* 2) (integer) 6384
* 3) "5e099198bba08597d695f6e2e3db2d6ec1494534"
* 3) 1) (integer) 0
* 2) (integer) 5461
* 3) 1) "127.0.0.1"
* 2) (integer) 6379
* 3) "672cc8350bb1ac1d94e9c511ea234f6b7f86cab1"
* 4) 1) "127.0.0.1"
* 2) (integer) 6382
* 3) "6bab67c3619c4b9cbdfd247ff3e446308a0c6326"
*/
List
维护节点和槽的映射关系主要通过JedisClusterInfoCache里面的两个Map类型的变量:
private final Map nodes = new HashMap();
private final Map slots = new HashMap();
该方法的主要流程如下:
1. 清空nodes和slots数据,并关闭关联的JedisPool。
2. 使用"cluster slots"指令获取Cluster节点和槽的映射关系,其返回结构如下:
127.0.0.1:6379> cluster slots
1) 1) (integer) 5462 . -- 起始槽
2) (integer) 10922 . -- 终止槽
3) 1) "127.0.0.1" . -- 主节点IP
2) (integer) 6380 -- 主节点端口
3) "85371dd3c2c11dbb1cd506ed028de10bb7fa2816" -- 主节点runId
4) 1) "127.0.0.1" . -- 从节点IP
2) (integer) 6383 -- 从节点端口
3) "01740d2eb2c9f89014e2b8b673d444753d0685cd" -- 从节点runId
2) 1) (integer) 10923
2) (integer) 16383
3) 1) "127.0.0.1"
2) (integer) 6381
3) "aacad0a5a2b37d4e11f2253849263575adb78740"
4) 1) "127.0.0.1"
2) (integer) 6384
3) "5e099198bba08597d695f6e2e3db2d6ec1494534"
3) 1) (integer) 0
2) (integer) 5461
3) 1) "127.0.0.1"
2) (integer) 6379
3) "672cc8350bb1ac1d94e9c511ea234f6b7f86cab1"
4) 1) "127.0.0.1"
2) (integer) 6382
3) "6bab67c3619c4b9cbdfd247ff3e446308a0c6326"
3. 解析出所负责的的所有槽,代码如下:
private List getAssignedSlotArray(List slotInfo) {
List slotNums = new ArrayList();
for (int slot = ((Long) slotInfo.get(0)).intValue(); slot <= ((Long) slotInfo.get(1))
.intValue(); slot++) {
slotNums.add(slot);
}
return slotNums;
}
slotNums的数量即为该主节点所负责的槽的数量。
4. 解析主节点和从节点信息,构建槽和节点的映射关系。
值得注意的是,nodes里面会保存主节点和从节点的信息,但是slots里面槽只对应主节点的连接池JedisPool,从节点并需要设置。
最后值得一提的是,Jedis连接Sentinel时,连接的是Sentinel集群,通过“masterName”参数获取Sentinel集群监控的主节点连接,后续的操作也是通过该主节点连接执行。但连接Cluster时,其实连接的是每个Redis实例,读取数据时会根据Jedis维护的槽和节点映射关系去对应的节点上读取。
JedisCluster关闭代码如下:
jedisCluster.close();
其关闭源码逻辑主要调用JedisClusterInfoCache类的reset方法,该方法关闭了所有集群节点的连接,并将nodes和slots数据清空。
//BinaryJedisCluster类
@Override
public void close() {
if (connectionHandler != null) {
connectionHandler.close();
}
}
//JedisClusterConnectionHandler类
@Override
public void close() {
cache.reset();
}
//JedisClusterInfoCache类
public void reset() {
w.lock();
try {
for (JedisPool pool : nodes.values()) {
try {
if (pool != null) {
pool.destroy();
}
} catch (Exception e) {
// pass
}
}
nodes.clear();
slots.clear();
} finally {
w.unlock();
}
}