深入解析Jedis底层源码

第1章 访问缓存服务器过程(hget)
架构在jedis外层封装了一个客户端ClusterNativeClient,在这个类中,提供了很多访问redis的方法,包括:hget、hset、hdel等,即成为前台业务代码和jedis进行交互的桥梁。
我们以查询操作为例,详细描述一下jedis的实现过程。
1.1 源码分析
1.1.1 ClusterNativeClient中hget方法:
public String hget(String key, String field)
{
return pool.hget(key, field);
}

说明:
Pool是缓存初始化时建立的,我们后面详细讲。

public static void init(Properties props)
{
String[] servers = props.getProperty(“host”).split(",");
HashSet nodes = new HashSet();
for (String s : servers) {
String[] ipAndPort = s.split("😊;
String ip = ipAndPort[0];
int port = Integer.valueOf(ipAndPort[1]).intValue();
nodes.add(new HostAndPort(ip, port));
}

JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(Integer.parseInt(props.getProperty(“maxIdle”)));
config.setMaxTotal(Integer.parseInt(props.getProperty(“maxTotal”)));

pool = new JedisCluster(nodes, config);
}
说明:
Pool的建立即调用了JedisCluster中hget方法。
1.1.2 JedisCluster中hget方法
public String hget(String key, String field)
{
return (String)new JedisClusterCommand(this.connectionHandler, this.maxRedirections, key, field)
{
public String execute(Jedis connection) {
return connection.hget(this.val k e y , t h i s . v a l key, this.val key,this.valfield);
}
}
.run(key);
}
说明:
该hget方法先new了一个JedisClusterCommand对象,该对象中的run方法为新建jedis连接的过程,即通过key获取slot,再获取该slot所在的节点进行连接。Execute方法则通过该连接向目标节点发送命令,以获取结果。通过该方法可以调用BinaryJedis中的hget()。
1.1.3 BinaryJedis中hget方法
public byte[] hget(byte[] key, byte[] field)
{
checkIsInMulti();
this.client.hget(key, field);
return this.client.getBinaryBulkReply();
}
说明:
BinaryJedis是jedis的二进制实现,即参数都进行二进制转化。在这一层调用了client的hegt方法,即向底层发送hget命令,getBinaryBulkReply()为获取命令执行后的结果再返回给上层对象。
1.1.4 BinaryClient中hget方法
public void hget(byte[] key, byte[] field) {
sendCommand(Protocol.Command.HGET, new byte[][] { key, field });
}
说明:
BinaryClient为Client的二进制实现。在这一层调用了父类Connection的sendCommand方法,通过字面可以猜出是向底层客户端发送命令的意思。
1.1.5 Connection中sendCommand方法
protected Connection sendCommand(ProtocolCommand cmd, byte[][] args) {
try {
connect();
Protocol.sendCommand(this.outputStream, cmd, args);
this.pipelinedCommands += 1;
return this;
}
catch (JedisConnectionException ex) {
this.broken = true;
}throw ex;
}
说明:
在Connection的sendCommand方法中先对目标节点进行了socket连接,connect()方法如下。然后调用Protocol的sendCommand方法,注意参数有outputStream,可以看出是要以输出流的形式给底层发送命令。
public void connect() {
if (!isConnected())
try {
this.socket = new Socket();

   this.socket.setReuseAddress(true);
   this.socket.setKeepAlive(true);

   this.socket.setTcpNoDelay(true);

   this.socket.setSoLinger(true, 0);

   this.socket.connect(new InetSocketAddress(this.host, this.port), this.connectionTimeout);
   this.socket.setSoTimeout(this.soTimeout);
   this.outputStream = new RedisOutputStream(this.socket.getOutputStream());
   this.inputStream = new RedisInputStream(this.socket.getInputStream());
 } catch (IOException ex) {
   this.broken = true;
   throw new JedisConnectionException(ex);
 }

}
1.1.6 Protocol中sendCommand方法
private static void sendCommand(RedisOutputStream os, byte[] command, byte[][] args)
{
try {
os.write(42);
os.writeIntCrLf(args.length + 1);
os.write(36);
os.writeIntCrLf(command.length);
os.write(command);
os.writeCrLf();

 for (byte[] arg : args) {
   os.write(36);
   os.writeIntCrLf(arg.length);
   os.write(arg);
   os.writeCrLf();
 }

} catch (IOException e) {
throw new JedisConnectionException(e);
}
}
说明:
Protocol的sendCommand方法中可以更直观的看出,这是利用输出流向指定的连接执行写操作。

以上,即为jedis实现的hget方法,其他实现方法类似。
1.2 总结

和Redis Server通信的协议规则都在redis.clients.jedis.Protocol这个类中,主要是通过对RedisInputStream和RedisOutputStream对读写操作来完成。命令的发送都是通过redis.clients.jedis.Protocol的sendCommand来完成的,就是对RedisOutputStream写入字节流,返回的数据是通过读取RedisInputStream 进行解析处理后得到的。
和Redis Sever的Socket通信是由 redis.clients.jedis.Connection 实现的。Connection 中维护了一个底层Socket连接和自己的I/O Stream 还有Protocol,I/O Stream是在Connection中Socket建立连接后获取并在使用时传给Protocol的。

第2章 Jedis定位redis集群节点过程
前面提到JedisClusterCommand类的run方法为新建jedis连接的过程,即访问节点定位过程,下面我们就来研究一下当一个客户端请求过来的时候,jedis是如何定位其访问的服务器节点的。
2.1 源码分析
2.1.1 JedisClusterCommand类的run方法
public T run(String key) {
if (key == null) {
throw new JedisClusterException(“No way to dispatch this command to Redis Cluster.”);
}

return runWithRetries(key, this.redirections, false, false);
}
说明:
Run方法很简单就是调用了一下runWithRetries方法。
2.1.2 runWithRetries方法
private T runWithRetries(String key, int redirections, boolean tryRandomNode, boolean asking) {
if (redirections <= 0) {
throw new JedisClusterMaxRedirectionsException(“Too many Cluster redirections?”);
}

Jedis connection = null;
try
{
if (asking)
{
connection = (Jedis)this.askConnection.get();
connection.asking();

   asking = false;
 }
 else if (tryRandomNode) {
   connection = this.connectionHandler.getConnection();
 } else {
  Connection= = this.connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
 }

 Object localObject1 = execute(connection);
 return localObject1;

}
catch (JedisConnectionException jce)
{
if (tryRandomNode)
{
throw jce;
}

 releaseConnection(connection, true);
 connection = null;

localObject2 = runWithRetries(key, redirections - 1, true, asking);
 return localObject2;

}
catch (JedisRedirectionException jre)
{
if ((jre instanceof JedisAskDataException)) {
asking = true;
this.askConnection.set(this.connectionHandler.getConnectionFromNode(jre.getTargetNode()));
} else if ((jre instanceof JedisMovedDataException))
{
this.connectionHandler.renewSlotCache();
} else {
throw new JedisClusterException(jre);
}

 releaseConnection(connection, false);
 connection = null;

Object localObject2 = runWithRetries(key, redirections - 1, false, asking);
 return localObject2; } finally { releaseConnection(connection, false); } throw localObject3;

}
说明:
runWithRetries的实现过程主要分为三个部分:正常执行、抛JedisConnectionException异常后的执行和抛JedisRedirectionException异常后的执行。
1) 首先两个参数asking和tryRandomNode在最初正常执行runWithRetries方法时都默认为false,即首次会执行:
Connection=this.connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
该方法首先通过key计算相应的slot。再通过slot定位到node。
2) 当试图连接指定的节点发生连接异常JedisConnectionException时,会执行:
localObject2 = runWithRetries(key, redirections - 1, true, asking); 该方法中参数tryRandomNode为true,即执行
connection = this.connectionHandler.getConnection();
进行随机访问。
3) 当发生重定向异常JedisRedirectionException时(随机访问的节点,key值可能不在该节点,则需要重定向到key所在的节点,一般重定向发生异常都是指定的slot正在发生迁移所致),说明指定查询key值所在的slot正在发生迁移,此时的slot是不接受任何命令的,但是若加上asking命令,则正常执行,因此会执行:
Object localObject2 = runWithRetries(key, redirections - 1, false, asking);
该方法中参数asking为true,即执行:
connection = (Jedis)this.askConnection.get();
connection.asking();
该方法会先发送一个asking命令给服务器端,然后再执行其他的命令。
2.1.3 根据slot获取对应node过程
下面我们说一下获取slot的过程,JedisClusterCRC16的getSlot方法过程:取CRC16校验码除以16384取模,16384为slot的个数。
获取到slot后,要确定slot被分配到哪台节点上了,然后让客户端连接指定的目标节点。该过程是通过JedisSlotBasedConnectionHandler的getConnectionFromSlot方法实现的。
2.1.3.1 getConnectionFromSlot方法
public Jedis getConnectionFromSlot(int slot)
{
JedisPool connectionPool = this.cache.getSlotPool(slot);
if (connectionPool != null)
{
return connectionPool.getResource();
}
return getConnection();
}
说明:
通过上面的方法可以看出当取出的节点为空时,要调用getConnection()进行随机选取节点进行连接,否则就连接所得到的目标节点。

2.1.3.2 getConnection()方法
public Jedis getConnection()
{
List 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);
   }
 }

}
说明:
随机选取目标节点主要实现方法为getShuffledNodesPool(),即将当前获取到的节点进行洗牌,然后循环洗牌后的所有节点,知道取出可以ping通的节点为止。
2.1.3.3 JedisClusterInfoCache中的getSlotPool方法
public JedisPool getSlotPool(int slot)
{
this.r.lock();
try {
JedisPool localJedisPool = (JedisPool)this.slots.get(Integer.valueOf(slot));
return localJedisPool; } finally { this.r.unlock(); } throw localObject;
}
说明:
该方法从全局变量slots中取出指定slot对应的node,slots里存储了slot和node的对应关系,slots变量在缓存初始化时对其进行赋值。
2.2 总结
2.2.1 正常情况

正常情况下,jedis会根据客户端传的key定位到slot,再跟据slot从slots中查到其分配的节点,然后向该节点发送请求并处理返回结果。

2.2.2 异常情况1:连接异常

当指定的节点发生连接异常,例如突然当机,jedis会调用一个随机选择的方法getConnection(),然后向随机选取后的节点发送请求,并处理其返回结果。重新选择的机会只有5次(jedis底层配置),否则会报Too many Cluster redirections错误。
2.2.3 异常情况2:slots为空

当slots为空,例如初始化时发生错误,那么根据slot是得不到指定的节点,则jedis会随机选取一个节点,然后向其发送请求,并处理返回结果。
第3章 缓存初始化过程
每次web服务启动时都会执行缓存初始化过程。前面讲过存储slot和node对应关系的slots全局变量要在缓存初始化的时候被赋值,其实还有一个全局变量需要在初始化时赋值,即nodes,该变量存储了集群所有节点的信息。在web服务器启动时,会调用RedisCacheStoreInitializationImpl的initCache()方法初始化缓存集群
3.1 源码分析
3.1.1 RedisCacheStoreInitializationImpl的initCache()
public boolean initCache()
{
if (“TRUE”.equalsIgnoreCase(this.cacheStoreConfig.getEnabled())) {
Properties props = loadProperty(“jedis.properties”);
props.setProperty(“host”, this.cacheStoreConfig.getHost());

 this.isClusterEnabled = Boolean.parseBoolean(props.getProperty("enableCluster"));
 if (this.isClusterEnabled)
    ClusterNativeClient.init(props);
 else {
   SingleNativeClient.init(props);
 }

 this.cacheStoreConfig.setState(1);
 this.log.info("Redis cache: cache started");
 return true;

}
说明:
如果是集群配置,则调用ClusterNativeClient的init方法。
3.1.2 ClusterNativeClient的init方法
public static void init(Properties props)
{
String[] servers = props.getProperty(“host”).split(",");
HashSet nodes = new HashSet();
for (String s : servers) {
String[] ipAndPort = s.split("😊;
String ip = ipAndPort[0];
int port = Integer.valueOf(ipAndPort[1]).intValue();
nodes.add(new HostAndPort(ip, port));
}

JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(Integer.parseInt(props.getProperty(“maxIdle”)));
config.setMaxTotal(Integer.parseInt(props.getProperty(“maxTotal”)));
pool = new JedisCluster(nodes, config);
}
说明:
在init方法中会new一个JedisCluster对象。
public JedisCluster(Set nodes, GenericObjectPoolConfig poolConfig) {
this(nodes, 2000, 5, poolConfig);
}
public JedisCluster(Set jedisClusterNode, int timeout, int maxRedirections, GenericObjectPoolConfig poolConfig)
{
this.connectionHandler = new JedisSlotBasedConnectionHandler(jedisClusterNode, poolConfig, timeout);

this.maxRedirections = maxRedirections;
}
这里jedis会给两个参数赋默认值:timeout=2000 ,maxRedirections=5。Timeout为连接超时时间,maxRedirections为最大重定向次数。

3.1.3 JedisSlotBasedConnectionHandler构造方法
public JedisSlotBasedConnectionHandler(Set nodes, GenericObjectPoolConfig poolConfig, int timeout)
{
super(nodes, poolConfig, timeout);
}
说明:
JedisSlotBasedConnectionHandler的构造方法中会继续调用其父类JedisClusterConnectionHandler的构造方法。
3.1.4 JedisClusterConnectionHandler构造方法
public JedisClusterConnectionHandler(Set nodes, GenericObjectPoolConfig poolConfig, int timeout)
{
this.cache = new JedisClusterInfoCache(poolConfig, timeout);
initializeSlotsCache(nodes, poolConfig);
}
说明:
在JedisClusterConnectionHandler构造方法中会执行初始化方法initializeSlotsCache。
3.1.5 初始化方法initializeSlotsCache
private void initializeSlotsCache(Set startNodes, GenericObjectPoolConfig poolConfig) {
Iterator i$ = startNodes.iterator();
Jedis jedis;
if (iKaTeX parse error: Expected '}', got 'EOF' at end of input: … (HostAndPort)i.next();
jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort()); } Iterator i$;
try { this.cache.discoverClusterNodesAndSlots(jedis);

 if (jedis != null)
   jedis.close();

}
catch (JedisConnectionException e)
{
if (jedis != null)
jedis.close();
}
finally
{
if (jedis != null) {
jedis.close();
}

}

while (iKaTeX parse error: Expected '}', got 'EOF' at end of input: … (HostAndPort)i.next();
this.cache.setNodeIfNotExist(node);
}
}
说明:
initializeSlotsCache方法告诉我们两个信息:1、初始化和服务器端进行交互时,是选取第一个节点并试图和其连接,这个节点即是cache-applicationContext.xml文件中我们配置的第一个节点。



2、当发生连接异常时,例如这个节点当掉,不会抛异常,而是会直接关闭连接,并执行setNodeIfNotExist,将配置文件中的节点全部添加到nodes当中。discoverClusterNodesAndSlots方法是给slots进行初始化。其中,会调用clusterNodes()方法,该方法是给服务器端发送了一个cluster nodes命令,发送该命令可以获取当前集群的基本信息:

192.168.174.24:7001> cluster nodes
eb52be41cd51117460254a0c6a7badf6e82a3b49 192.168.174.24:7002 master - 0 1476165816509 7 connected 5461-10922
9e62f8dd7e1dc8b021dc01d9b37b58fce175022b 192.168.174.26:7001 master - 0 1476165819511 10 connected 0-5460
0b0c0bae6e73389466c178a23f6b9cba560824cb 192.168.174.26:7000 slave eb52be41cd51117460254a0c6a7badf6e82a3b49 0 1476165818511 7 connected
6df2966a46d8ee812b3757094731290604a917c1 192.168.174.24:7000 slave 9e62f8dd7e1dc8b021dc01d9b37b58fce175022b 0 1476165817509 10 connected
595bd6dd6f5e89d6a936bf9d4d8be7fef5018b80 192.168.174.26:7002 slave 2e171799e798b6f72b1d0b1396a17f6584e13b30 0 1476165820511 12 connected
2e171799e798b6f72b1d0b1396a17f6584e13b30 192.168.174.24:7001 myself,master - 0 0 12 connected 10923-16383

Jedis就是靠这个方法获取及时的节点和slot的分配信息然后给slots初始化。

3.1.6 JedisClusterInfoCache的discoverClusterNodesAndSlots方法:
public void discoverClusterNodesAndSlots(Jedis jedis) {
this.w.lock();
try
{
this.nodes.clear();
this.slots.clear();

String localNodes = jedis.clusterNodes();
 for (String nodeInfo : localNodes.split("\n")) {
   ClusterNodeInformation clusterNodeInfo = nodeInfoParser.parse(nodeInfo, new HostAndPort(jedis.getClient().getHost(), jedis.getClient().getPort()));

   HostAndPort targetNode = clusterNodeInfo.getNode();
   setNodeIfNotExist(targetNode);
   assignSlotsToNode(clusterNodeInfo.getAvailableSlots(), targetNode);
 }

} finally {
this.w.unlock();
}
}

3.1.7 assignSlotsToNode方法
public void assignSlotsToNode(List targetSlots, HostAndPort targetNode) {
this.w.lock();
try {
targetPool = (JedisPool)this.nodes.get(getNodeKey(targetNode));

 if (targetPool == null) {
   setNodeIfNotExist(targetNode);
   targetPool = (JedisPool)this.nodes.get(getNodeKey(targetNode));
 }

 for (Integer slot : targetSlots)
  this.slots.put(slot, targetPool);

}
finally
{
JedisPool targetPool;
this.w.unlock();
}
}

说明:
由以上可以看出,初始化过程中,当连接的节点当机时,是不能通过cluster nodes命令获取集群信息的,那么slots将不能被初始化,即slots为空,而nodes则记录了配置文件中手动输入的集群ip信息。但是web服务并不会因此报错,服务会正常启动,不过当客户端发起缓存查询或写入命令时,例如hget,从slots中获取slot对应的node会取空,因此会执行getConnection()随机选取节点进行连接。
public Jedis getConnectionFromSlot(int slot)
{
JedisPool connectionPool = this.cache.getSlotPool(slot);
if (connectionPool != null)
{
return connectionPool.getResource();
}
return getConnection();
}
3.2 总结
3.2.1 正常情况

每次web服务启动时,都会执行缓存的初始化(initCache()),初始化时会对两个全局Map ——slots和nodes进行初始化,首先jedis会加载缓存配置文件,获取配置文件中事先写好的第一个节点地址进行连接,并向其发送cluster nodes 命令,该命令会返回集群节点和slot的分配信息,然后根据返回的结果初始化slots和nodes,该过程结束后,还会再次取配置文件中的节点地址给nodes赋值,若nodes中已经存在该值则跳过,即取集群中实际节点和配置节点的并集。这么做的目的是由于jedis在执行随机选取节点(getConnection())时,会从nodes中选择一个节点,若cluster nodes发送失败,获取不到实际的集群信息,还可以通过配置文件获取相应信息,以保证缓存的可用性。
3.2.2 异常情况:连接异常

当发送cluster nodes 发生连接异常时,jedis得不到实际的集群配置信息,就不会对slots进行初始化,此时slots为null,但是会将配置文件中的节点信息放到nodes中。以保证随机访问的可用性。

第4章 总结:
1、 初始化时当所连接节点当机,slots不会被初始化,即为空,nodes会记录配置文件中的节点信息。那么此次服务启动后所有访问缓存的操作将不能定向访问,会变成随机访问再重定向模式。
2、 当服务启动后,访问缓存前,有节点当机,slots中的槽分配信息就不准确了,此时会取到已经当机的节点进行连接,因此会发生连接异常,这时也会转换成随机访问的模式。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值