1. 写入数据
JedisCluster设置string类型数据的set方法如下:
@Override
public String set(final String key, final String value) {
return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {
@Override
public String execute(Jedis connection) {
return connection.set(key, value);
}
}.run(key);
}
JedisClusterCommand为抽象类,execute为抽象方法,因此需要重写该方法,核心逻辑run方法代码如下:
public T run(String key) {
return runWithRetries(JedisClusterCRC16.getSlot(key), this.maxAttempts, false, null);
}
(1)通过哈希算法获取key对应的slot。
public static int getSlot(String key) {
if (key == null) {
throw new JedisClusterOperationException("Slot calculation of null is impossible");
}
//提取hash_tag
key = JedisClusterHashTagUtil.getHashTag(key);
// optimization with modulo operator with power of 2 equivalent to getCRC16(key) % 16384
return getCRC16(key) & (16384 - 1);
}
- JedisClusterHashTagUtil.getHashTag(key) 用于提取hash_tag,我们简单介绍下Redis的hash_tag,key内部使用大括号包含的内容叫做hash_tag,可以提供不同的key可以具备相同slot的功能。例如,key:{user}:1 和 key:{user}:2 会映射到相同的slot。getHashTag方法在于提取大括号中的内容。
- 使用 "CRC16(key) & 16383" 获取到对应的slot。
(2)通过槽和节点的映射关系找到对应节点连接,执行命令。
private T runWithRetries(final int slot, int attempts, boolean tryRandomNode, JedisRedirectionException redirect) {
if (attempts <= 0) {
throw new JedisClusterMaxAttemptsException("No more cluster attempts left.");
}
Jedis connection = null;
try {
/**
* 服务端返回重定向信息,Redis有两种重定向:
* 1. MOVED重定向:集群已经完成故障转移及数据迁移,主从节点发生切换,客户端需要更新slots缓存。
* 2. ASK重定向:集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只是临时性的重定向,客户端不会更新slots缓存。
*/
if (redirect != null) {
//从重定向异常提取出目标节点信息
connection = this.connectionHandler.getConnectionFromNode(redirect.getTargetNode());
//如果是ASK重定向,需要发送asking命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返回不存在信息。
if (redirect instanceof JedisAskDataException) {
// TODO: Pipeline asking with the original command to make it faster....
connection.asking();
}
} else {
if (tryRandomNode) {
//随机获取某个节点的连接池,并使用"ping-pong"机制保证节点是存活态。
connection = connectionHandler.getConnection();
} else {
//从slots中获取slot对应的节点连接,如果不存在则需要刷新slots映射。
connection = connectionHandler.getConnectionFromSlot(slot);
}
}
//执行命令
return execute(connection);
} catch (JedisNoReachableClusterNodeException jnrcne) {
throw jnrcne;
} catch (JedisConnectionException jce) {
// release current connection before recursion
releaseConnection(connection);
connection = null;
//当发生连接异常时,会发生重试,最后一次尝试时会刷新slots数据。
//采用最后一次才刷新,是因为在刷新方法内部使用了写锁,导致所有请求都阻塞,对于并发量高的场景将极大地影响集群吞吐。
if (attempts <= 1) {
//We need this because if node is not reachable anymore - we need to finally initiate slots
//renewing, or we can stuck with cluster state without one node in opposite case.
//But now if maxAttempts = [1 or 2] we will do it too often.
//TODO make tracking of successful/unsuccessful operations for node - do renewing only
//if there were no successful responses from this node last few seconds
this.connectionHandler.renewSlotCache();
}
return runWithRetries(slot, attempts - 1, tryRandomNode, redirect);
} catch (JedisRedirectionException jre) {
// if MOVED redirection occurred,
//发生MOVED重定向,说明已经发生故障转移及数据迁移,即主从节点进行切换,因此需要刷新slots的映射信息。
if (jre instanceof JedisMovedDataException) {
// it rebuilds cluster's slot cache recommended by Redis cluster specification
this.connectionHandler.renewSlotCache(connection);
}
// release current connection before recursion
releaseConnection(connection);
connection = null;
return runWithRetries(slot, attempts - 1, false, jre);
} finally {
releaseConnection(connection);
}
}
具体流程以及注意点已经在注释中标出,下面重点分析下其他的几个重要方法。
- getConnection()
该方法主要是在nodes映射中随机选取某个节点的连接。
@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
//使用Collections.shuffle方法产生随机数据
List<JedisPool> pools = cache.getShuffledNodesPool();
for (JedisPool pool : pools) {
Jedis jedis = null;
try {
jedis = pool.getResource();
if (jedis == null) {
continue;
}
//Redis Cluster内部使用"ping-pong"机制来判断节点的存活状态
String result = jedis.ping();
if (result.equalsIgnoreCase("pong")) return jedis;
jedis.close();
} catch (JedisException ex) {
if (jedis != null) {
jedis.close();
}
}
}
throw new JedisNoReachableClusterNodeException("No reachable node in cluster");
}
- getConnectionFromSlot(int slot)
该方法从slots数据中获取slot对应的连接。
@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 {
renewSlotCache(); //It's abnormal situation for cluster mode, that we have just nothing for slot, try to rediscover state
connectionPool = cache.getSlotPool(slot);
if (connectionPool != null) {
return connectionPool.getResource();
} else {
//no choice, fallback to new connection to random node
return getConnection();
}
}
}
- renewSlotCache()
该方法调用JedisClusterInfoCache类的renewClusterSlots(Jedis jedis)方法来刷新slots映射。
public void renewSlotCache() {
cache.renewClusterSlots(null);
}
- renewClusterSlots(Jedis jedis)
该方法需要了解的有两点:
(1)通过volatile变量和写锁保证每次只有一个线程执行。
(2)当jedis参数不为空时,只刷新对应节点的slots和nodes映射数据;为空时则刷新所有节点的映射关系。
其核心逻辑是在discoverClusterSlots方法里。
private volatile boolean rediscovering;
public void renewClusterSlots(Jedis jedis) {
//If rediscovering is already in process - no need to start one more same rediscovering, just return
if (!rediscovering) {
try {
w.lock();
if (!rediscovering) {
rediscovering = true;
try {
if (jedis != null) {
try {
discoverClusterSlots(jedis);
return;
} catch (JedisException e) {
//try nodes from all pools
}
}
for (JedisPool jp : getShuffledNodesPool()) {
Jedis j = null;
try {
j = jp.getResource();
discoverClusterSlots(j);
return;
} catch (JedisConnectionException e) {
// try next nodes
} finally {
if (j != null) {
j.close();
}
}
}
} finally {
rediscovering = false;
}
}
} finally {
w.unlock();
}
}
}
- discoverClusterSlots(Jedis jedis)
discoverClusterSlots方法与《Cluster连接创建与关闭》中介绍的discoverClusterNodesAndSlots方法相似,大体都是通过"cluster slots"指令获取槽和节点的信息, 更新slots和nodes数据。
private void discoverClusterSlots(Jedis jedis) {
List<Object> slots = jedis.clusterSlots();
this.slots.clear();
for (Object slotInfoObj : slots) {
List<Object> slotInfo = (List<Object>) slotInfoObj;
if (slotInfo.size() <= MASTER_NODE_INDEX) {
continue;
}
List<Integer> slotNums = getAssignedSlotArray(slotInfo);
// hostInfos
//获取主节点IP端口信息
List<Object> hostInfos = (List<Object>) slotInfo.get(MASTER_NODE_INDEX);
if (hostInfos.isEmpty()) {
continue;
}
// at this time, we just use master, discard slave information
HostAndPort targetNode = generateHostAndPort(hostInfos);
assignSlotsToNode(slotNums, targetNode);
}
}
public void assignSlotsToNode(List<Integer> targetSlots, HostAndPort targetNode) {
w.lock();
try {
JedisPool targetPool = setupNodeIfNotExist(targetNode);
for (Integer slot : targetSlots) {
slots.put(slot, targetPool);
}
} finally {
w.unlock();
}
}
public JedisPool setupNodeIfNotExist(HostAndPort node) {
w.lock();
try {
//ip:port格式
String nodeKey = getNodeKey(node);
JedisPool existingPool = nodes.get(nodeKey);
if (existingPool != null) return existingPool;
JedisPool nodePool = new JedisPool(poolConfig, node.getHost(), node.getPort(),
connectionTimeout, soTimeout, password, 0, clientName,
ssl, sslSocketFactory, sslParameters, hostnameVerifier);
nodes.put(nodeKey, nodePool);
return nodePool;
} finally {
w.unlock();
}
}
2. 读取数据
JedisCluster读取string类型数据的get方法如下:
@Override
public String get(final String key) {
return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {
@Override
public String execute(Jedis connection) {
return connection.get(key);
}
}.run(key);
}
public T run(String key) {
return runWithRetries(JedisClusterCRC16.getSlot(key), this.maxAttempts, false, null);
}
从上面代码可知,读取数据的流程是与写入数据的流程是一致的,这里就不再赘述。
但有一点值得注意的是,runWithRetries方法中的tryRandomNode参数可以决定数据是否一定在主节点上读写,set和get方法都是使用false参数,因此会调用getConnectionFromSlot方法获取连接,由于slots中存储的都是主节点的连接,因此默认都是在主节点上进行操作。
当tryRandomNode参数设置为true时,会调用getConnection方法,该方法随机从nodes中获取连接,而nodes中存储了主从节点的连接,因此有可能获取到从节点的连接。