JedisCluster源码阅读

1,592 阅读5分钟

JedisCluster类的定义

public class JedisCluster extends BinaryJedisCluster implements JedisCommands, MultiKeyJedisClusterCommands, JedisClusterScriptingCommands {.....}
BinaryJedisCluster 用于进行字节数组的key,value
JedisCommands 用于操作字符串类型的key,value
MultiKeyJedisClusterCommands 用于操作多个key类型的
JedisClusterScriptingCommands 用于lua脚本操作

JedisClusterCommand

在JedisCluster客户端中,JedisClusterCommand是一个非常重要的类,采用模板方法设计,高度封装了操作Redis集群的操作。对于集群中各种存储操作,提供了一个抽象execute方法。
JedisCluster各种具体操作Redis集群方法,只需要通过匿名内部类的方式,灵活扩展Execute方法。
内部通过JedisClusterConnectionHandler封装了Jedis的实例。

该类主要由两个重点:
  1. 采用模板方法设计,具体存取操作有子类实现
  2. 在集群操作中,为了保证高可用方式,采用递归算法进行尝试发生的MOVED,ASK,数据迁移操作等。
BinaryJedisCluster和JedisCommands通过实现创建JedisClusterCommand实例实现execute抽象方法之后调用run方法进行redis的操作。核心类在JedisClusterCommand


public abstract class JedisClusterCommand<T> {
    // JedisCluster的连接真正持有类
    private JedisClusterConnectionHandler connectionHandler;
    // 重试次数默认为5
    private int maxAttempts;
    // 线程本地变量
    private ThreadLocal<Jedis> askConnection = new ThreadLocal();

    public JedisClusterCommand(JedisClusterConnectionHandler connectionHandler, int maxAttempts) {
        this.connectionHandler = connectionHandler;
        this.maxAttempts = maxAttempts;
    }

    public abstract T execute(Jedis var1);

    public T run(String key) {
        if (key == null) {
            throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
        } else {
            return this.runWithRetries(SafeEncoder.encode(key), this.maxAttempts, false, false);
        }
    }

    public T run(int keyCount, String... keys) {
        if (keys != null && keys.length != 0) {
            if (keys.length > 1) {
                int slot = JedisClusterCRC16.getSlot(keys[0]);

                for(int i = 1; i < keyCount; ++i) {
                    int nextSlot = JedisClusterCRC16.getSlot(keys[i]);
                    if (slot != nextSlot) {
                        throw new JedisClusterException("No way to dispatch this command to Redis Cluster because keys have different slots.");
                    }
                }
            }

            return this.runWithRetries(SafeEncoder.encode(keys[0]), this.maxAttempts, false, false);
        } else {
            throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
        }
    }

    public T runBinary(byte[] key) {
        if (key == null) {
            throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
        } else {
            return this.runWithRetries(key, this.maxAttempts, false, false);
        }
    }

    public T runBinary(int keyCount, byte[]... keys) {
        if (keys != null && keys.length != 0) {
            if (keys.length > 1) {
                int slot = JedisClusterCRC16.getSlot(keys[0]);

                for(int i = 1; i < keyCount; ++i) {
                    int nextSlot = JedisClusterCRC16.getSlot(keys[i]);
                    if (slot != nextSlot) {
                        throw new JedisClusterException("No way to dispatch this command to Redis Cluster because keys have different slots.");
                    }
                }
            }

            return this.runWithRetries(keys[0], this.maxAttempts, false, false);
        } else {
            throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
        }
    }

    public T runWithAnyNode() {
        Jedis connection = null;

        Object var2;
        try {
            connection = this.connectionHandler.getConnection();
            var2 = this.execute(connection);
        } catch (JedisConnectionException var6) {
            throw var6;
        } finally {
            this.releaseConnection(connection);
        }

        return var2;
    }

    /**
    * 这个方法是核心操作,整个类都是在调用这个方法
    * 该方法采用递归方式,保证在往集群中存取数据时,发生MOVED,ASKing,数据迁移过程中遇到问题,也是一种实现高可用的方式。
    * 该方法中调用execute方法,该方法由子类具体实现
    */
    private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {
        if (attempts <= 0) {
            throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");
        } else {
            Jedis connection = null;

            Object var7;
            try {
                // 如果是在slot的迁移过程中访问了slot中已经被迁移的key,那么先发送asking命令,然后再去目标节点操作
                if (asking) {
                    connection = (Jedis)this.askConnection.get();
                    connection.asking();
                    asking = false;
                } else if (tryRandomNode) {
                    //随机获取一个节点,第一次执行时,tryRandomNode为false。
                    connection = this.connectionHandler.getConnection();
                } else {
                    //根据key的slot去JedisClusterInfoCache 中获取Jedis的实例
                    connection = this.connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
                }
                // 执行命令
                Object var6 = this.execute(connection);
                return var6;
            } catch (JedisNoReachableClusterNodeException var13) {
                throw var13;
            } catch (JedisConnectionException var14) {
                //发生了连接异常
                this.releaseConnection(connection);
                connection = null;
                if (attempts <= 1) {
                    //重新更新redis slot缓存
                    this.connectionHandler.renewSlotCache();
                    throw var14;
                }
                //递归调用一次
                var7 = this.runWithRetries(key, attempts - 1, tryRandomNode, asking);
                return var7;
            } catch (JedisRedirectionException var15) {
                // 重定向异常,key不再当前连接的节点上,需要定位到其他节点,并且更新slot缓存,方便下次直接定位过去
                if (var15 instanceof JedisMovedDataException) {
                    this.connectionHandler.renewSlotCache(connection);
                }
                this.releaseConnection(connection);
                connection = null;
                //slot迁移的时候得ask响应,需要把当前请求重定向到目标机器,不用缓存slot
                if (var15 instanceof JedisAskDataException) {
                    asking = true;
                    this.askConnection.set(this.connectionHandler.getConnectionFromNode(var15.getTargetNode()));
                } else if (!(var15 instanceof JedisMovedDataException)) {
                    throw new JedisClusterException(var15);
                }
                //执行请求
                var7 = this.runWithRetries(key, attempts - 1, false, asking);
            } finally {
                this.releaseConnection(connection);
            }

            return var7;
        }
    }

    private void releaseConnection(Jedis connection) {
        if (connection != null) {
            connection.close();
        }

    }
}

JedisClusterInfoCache

用于封装连接信息的类,这个类中可以获取每一个slot对应的Master节点的连接。这里面有两个缓存Map<String, JedisPool> nodes,Map<Integer, JedisPool> slots

public class JedisClusterInfoCache {
    //节点缓存,key=ip:port,value=JedisPool
    private final Map<String, JedisPool> nodes;
    //slot对应的Jedis,key是0-16383个slot
    private final Map<Integer, JedisPool> slots;

    public JedisClusterInfoCache(GenericObjectPoolConfig poolConfig, int timeout) {
        this(poolConfig, timeout, timeout, (String)null);
    }
    
    /**
    * 这个方法在JedisClusterConnectionHandler(连接持有者)的初始化方法中调用
    * 作用是使用当前的jedis中获取到的集群信息去初始化缓存
    */
    public void discoverClusterNodesAndSlots(Jedis jedis) {
        //加写锁
        this.w.lock();
        try {
            this.reset();
            //获取集群信息,slot,master,slave,有多少个master,list就有多少个元素
            List<Object> slots = jedis.clusterSlots();
            Iterator var3 = slots.iterator();

            //循环生成每个Master的jedis对象pool
            while(true) {
                List slotInfo;
                do {
                    if (!var3.hasNext()) {
                        return;
                    }

                    Object slotInfoObj = var3.next();
                    slotInfo = (List)slotInfoObj;
                } while(slotInfo.size() <= 2);

                List<Integer> slotNums = this.getAssignedSlotArray(slotInfo);
                int size = slotInfo.size();

                for(int i = 2; i < size; ++i) {
                    List<Object> hostInfos = (List)slotInfo.get(i);
                    if (hostInfos.size() > 0) {
                        HostAndPort targetNode = this.generateHostAndPort(hostInfos);
                        //设置nodes缓存,使用ip:port作为key,然后创建JedisPool作为value缓存起来。
                        this.setupNodeIfNotExist(targetNode);
                        if (i == 2) {
                            //设置slot缓存,这里会将每一个slot对应的JedisPool缓存起来
                            this.assignSlotsToNode(slotNums, targetNode);
                        }
                    }
                }
            }
        } finally {
            this.w.unlock();
        }  
}

总结:集群操作主要是在客户端缓存了每个slot对应的master的连接池,由客户端进行key的分发,如果客户端分发的key和集群不一致,那么redis server会返回重定向异常,客户端根据重定向异常重新去缓存即可。