13

原文链接:《Jedis 源码分析》http://www.ytbean.com/posts/jedis-source-walk/

概述

Jedis是Redis官方推荐的Java客户端,更多Redis的客户端可以参考Redis官网客户端列表

JedisSentinelPool

简介

Redis-Sentinel作为官方推荐的HA解决方案,Jedis也在客户端角度实现了对Sentinel的支持,主要实现在JedisSentinelPool.java这个类中,下文会分析这个类的实现。

属性

JedisSentinelPool类里有以下的属性:

    //基于apache的commom-pool2的对象池配置
    protected GenericObjectPoolConfig poolConfig;
    
    //超时时间,默认是2000
    protected int timeout = Protocol.DEFAULT_TIMEOUT;
    
    //sentinel的密码
    protected String password;

    //redis数据库的数目
    protected int database = Protocol.DEFAULT_DATABASE;

    //master监听器,当master的地址发生改变时,会触发这些监听者
    protected Set<MasterListener> masterListeners = new HashSet<MasterListener>();

    protected Logger log = Logger.getLogger(getClass().getName());
    
    //Jedis实例创建工厂
    private volatile JedisFactory factory;
    
    //当前的master,HostAndPort是一个简单的包装了ip和port的模型类
    private volatile HostAndPort currentHostMaster;

构造器

构造器的代码如下:

public JedisSentinelPool(String masterName, Set<String> sentinels, final GenericObjectPoolConfig poolConfig, int timeout, final String password, final int database) {

        this.poolConfig = poolConfig;
        this.timeout = timeout;
        this.password = password;
        this.database = database;

        HostAndPort master = initSentinels(sentinels, masterName);
        initPool(master);
}

构造器一开始对实例变量进行赋值,参数sentinels是客户端所需要打交道的Redis-Sentinel,允许有多个,用一个集合来盛装。

然后通过initSentinels方法与sentinel沟通后,确定当前sentinel所监视的master是哪一个。然后通过master来创建好对象池,以便后续从对象池中取出一个Jedis实例,来对master进行操作。

initSentinels方法

initSentinels方法的代码如下所示,我加了一些注释:

    private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {

        HostAndPort master = null;
        boolean sentinelAvailable = false;

        log.info("Trying to find master from available Sentinels...");

        // 有多个sentinels,遍历这些个sentinels
        for (String sentinel : sentinels) {
            // host:port表示的sentinel地址转化为一个HostAndPort对象。
            final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));

            log.fine("Connecting to Sentinel " + hap);

            Jedis jedis = null;
            try {
                // 连接到sentinel
                jedis = new Jedis(hap.getHost(), hap.getPort());

                // 根据masterName得到master的地址,返回一个list,host= list[0], port =
                // list[1]
                List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);

                // connected to sentinel...
                sentinelAvailable = true;

                if (masterAddr == null || masterAddr.size() != 2) {
                    log.warning("Can not get master addr, master name: " + masterName
                            + ". Sentinel: " + hap + ".");
                    continue;
                }

                master = toHostAndPort(masterAddr);
                log.fine("Found Redis master at " + master);
                // 如果在任何一个sentinel中找到了master,不再遍历sentinels
                break;
            } catch (JedisConnectionException e) {
                log.warning("Cannot connect to sentinel running @ " + hap
                        + ". Trying next one.");
            } finally {
                // 关闭与sentinel的连接
                if (jedis != null) {
                    jedis.close();
                }
            }
        }

        // 到这里,如果master为null,则说明有两种情况,一种是所有的sentinels节点都down掉了,一种是master节点没有被存活的sentinels监控到
        if (master == null) {
            if (sentinelAvailable) {
                // can connect to sentinel, but master name seems to not
                // monitored
                throw new JedisException("Can connect to sentinel, but " + masterName
                        + " seems to be not monitored...");
            } else {
                throw new JedisConnectionException(
                        "All sentinels down, cannot determine where is " + masterName
                                + " master is running...");
            }
        }

        //如果走到这里,说明找到了master的地址
        log.info("Redis master running at " + master + ", starting Sentinel listeners...");

        //启动对每个sentinels的监听
        for (String sentinel : sentinels) {
            final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));
            MasterListener masterListener = new MasterListener(masterName, hap.getHost(),
                    hap.getPort());
            masterListeners.add(masterListener);
            masterListener.start();
        }

        return master;
    }

可以看到initSentinels方法的参数有一个masterName,就是我们所需要查找的master的名字。
一开始,遍历多个sentinels,一个一个连接到sentinel,去询问关于masterName的消息,可以看到是通过jedis.sentinelGetMasterAddrByName()方法去连接sentinel,并询问当前的master的地址。点进这个方法去看看,源代码是这样写的:

/**
   * <pre>
   * redis 127.0.0.1:26381> sentinel get-master-addr-by-name mymaster
   * 1) "127.0.0.1"
   * 2) "6379"
   * </pre>
   * @param masterName
   * @return two elements list of strings : host and port.
   */
  public List<String> sentinelGetMasterAddrByName(String masterName) {
    client.sentinel(Protocol.SENTINEL_GET_MASTER_ADDR_BY_NAME, masterName);
    final List<Object> reply = client.getObjectMultiBulkReply();
    return BuilderFactory.STRING_LIST.build(reply);
  }

调用的是与Jedis绑定的client去发送一个"get-master-addr-by-name"命令。

回到initSentinels方法中,如果没有询问到master的地址,那就询问下一个sentinel。如果询问到了master的地址,那么将不再遍历sentinel集合,直接break退出循环遍历。

如果循环结束后,master的值为null,那么有两种可能:

  • 一种是所有的sentinel实例都不可用了
  • 另外一种是,sentinel实例有可用的,但是没有监控名字为masterName的Redis。

如果master为null,程序会抛出异常,不再往下走了。如果master不为null呢,继续往下走。

可以从代码中看到,为每个sentinel都启动了一个监听者MasterListener。MasterListener本身是一个线程,它会去订阅sentinel上关于master节点地址改变的消息。

接下来先分析构造方法中的另外一个方法:initPool。之后再看MasterListener的实现。

initPool方法

initPool的实现源代码如下所示:

private void initPool(HostAndPort master) {
        if (!master.equals(currentHostMaster)) {
            currentHostMaster = master;
            if (factory == null) {
                factory = new JedisFactory(master.getHost(), master.getPort(), timeout,
                        password, database);
                initPool(poolConfig, factory);
            } else {
                factory.setHostAndPort(currentHostMaster);
                // although we clear the pool, we still have to check the
                // returned object
                // in getResource, this call only clears idle instances, not
                // borrowed instances
                internalPool.clear();
            }

            log.info("Created JedisPool to master at " + master);
        }
    }
    

可以看到,作为参数传进来的master会与实例变量currentHostMaster作比较,看看是否是相同的,为什么要作这个比较呢,因为前文中提到的MasterListener会在发现master地址改变以后,去调用initPool方法。
如果是第一次调用initPool方法(构造函数中调用),那么会初始化Jedis实例创建工厂,如果不是第一次调用(MasterListener中调用),那么只对已经初始化的工厂进行重新设置。
从以上也可以看出为什么currentHostMasterfactory这两个变量为什么要声明为volatile,它们会在多线程环境下被访问和修改,因此必须保证可见性
第一次调用时,会调用initPool(poolConfig, factory)方法。
看看这个方法的源代码:

public void initPool(final GenericObjectPoolConfig poolConfig,
            PooledObjectFactory<T> factory) {

        if (this.internalPool != null) {
            try {
                closeInternalPool();
            } catch (Exception e) {
            }
        }

        this.internalPool = new GenericObjectPool<T>(factory, poolConfig);
    }

基本上只干了一件事:初始化内部对象池。

MasterListener监听者线程

直接看它的run方法实现吧:



        public void run() {

            running.set(true);

            while (running.get()) {

                j = new Jedis(host, port);

                try {
                    //订阅sentinel上关于master地址改变的消息
                    j.subscribe(new JedisPubSub() {
                        @Override
                        public void onMessage(String channel, String message) {
                            log.fine("Sentinel " + host + ":" + port + " published: "
                                    + message + ".");

                            String[] switchMasterMsg = message.split(" ");

                            if (switchMasterMsg.length > 3) {

                                if (masterName.equals(switchMasterMsg[0])) {
                                    initPool(toHostAndPort(Arrays.asList(
                                            switchMasterMsg[3], switchMasterMsg[4])));
                                } else {
                                    log.fine("Ignoring message on +switch-master for master name "
                                            + switchMasterMsg[0]
                                            + ", our master name is " + masterName);
                                }

                            } else {
                                log.severe("Invalid message received on Sentinel " + host
                                        + ":" + port + " on channel +switch-master: "
                                        + message);
                            }
                        }
                    }, "+switch-master");

                } catch (JedisConnectionException e) {

                    if (running.get()) {
                        log.severe("Lost connection to Sentinel at " + host + ":" + port
                                + ". Sleeping 5000ms and retrying.");
                        try {
                            Thread.sleep(subscribeRetryWaitTimeMillis);
                        } catch (InterruptedException e1) {
                            e1.printStackTrace();
                        }
                    } else {
                        log.fine("Unsubscribing from Sentinel at " + host + ":" + port);
                    }
                }
            }
        }

可以看到它依然委托了Jedis去与sentinel打交道,订阅了关于master地址变换的消息,当master地址变换时,就会再调用一次initPool方法,重新设置对象池相关的设置。

尾声

Jedis的JedisSentinelPool的实现仅仅适用于单个master-slave。
现在有了更多的需求,既需要sentinel提供的自动主备切换机制,又需要客户端能够做数据分片(Sharding),类似于memcached用一致性哈希进行数据分片。
接下来可能会自己在现有Jedis上实现一个支持一致性哈希分片的ShardedJedisSentinelPool。

Sharded

概述

当业务的数据量非常庞大时,需要考虑将数据存储到多个缓存节点上,如何定位数据应该存储的节点,一般用的是一致性哈希算法。Jedis在客户端角度实现了一致性哈希算法,对数据进行分片,存储到对应的不同的redis实例中。
Jedis对Sharded的实现主要是在ShardedJedis.javaShardedJedisPool.java中。本文主要介绍ShardedJedis的实现,ShardedJedisPool是基于apache的common-pool2的对象池实现。

继承关系

ShardedJedis--->BinaryShardedJedis--->Sharded <Jedis, JedisShardInfo>

构造函数

查看其构造函数

public ShardedJedis(List<JedisShardInfo> shards, Hashing algo, Pattern keyTagPattern) {
        super(shards, algo, keyTagPattern);
    }

构造器参数解释:

  • shards是一个JedisShardInfo的列表,一个JedisShardedInfo类代表一个数据分片的主体。
  • algo是用来进行数据分片的算法
  • keyTagPattern,自定义分片算法所依据的key的形式。例如,可以不针对整个key的字符串做哈希计算,而是类似对thisisa{key}中包含在大括号内的字符串进行哈希计算。

JedisShardInfo是什么样的?

public class JedisShardInfo extends ShardInfo<Jedis> {

  public String toString() {
    return host + ":" + port + "*" + getWeight();
  }

  private int connectionTimeout;
  private int soTimeout;
  private String host;
  private int port;
  private String password = null;
  private String name = null;
  // Default Redis DB
  private int db = 0;

  public String getHost() {
    return host;
  }

  public int getPort() {
    return port;
  }

  public JedisShardInfo(String host) {
    super(Sharded.DEFAULT_WEIGHT);
    URI uri = URI.create(host);
    if (JedisURIHelper.isValid(uri)) {
      this.host = uri.getHost();
      this.port = uri.getPort();
      this.password = JedisURIHelper.getPassword(uri);
      this.db = JedisURIHelper.getDBIndex(uri);
    } else {
      this.host = host;
      this.port = Protocol.DEFAULT_PORT;
    }
  }

  public JedisShardInfo(String host, String name) {
    this(host, Protocol.DEFAULT_PORT, name);
  }

  public JedisShardInfo(String host, int port) {
    this(host, port, 2000);
  }

  public JedisShardInfo(String host, int port, String name) {
    this(host, port, 2000, name);
  }

  public JedisShardInfo(String host, int port, int timeout) {
    this(host, port, timeout, timeout, Sharded.DEFAULT_WEIGHT);
  }

  public JedisShardInfo(String host, int port, int timeout, String name) {
    this(host, port, timeout, timeout, Sharded.DEFAULT_WEIGHT);
    this.name = name;
  }

  public JedisShardInfo(String host, int port, int connectionTimeout, int soTimeout, int weight) {
    super(weight);
    this.host = host;
    this.port = port;
    this.connectionTimeout = connectionTimeout;
    this.soTimeout = soTimeout;
  }

  public JedisShardInfo(String host, String name, int port, int timeout, int weight) {
    super(weight);
    this.host = host;
    this.name = name;
    this.port = port;
    this.connectionTimeout = timeout;
    this.soTimeout = timeout;
  }

  public JedisShardInfo(URI uri) {
    super(Sharded.DEFAULT_WEIGHT);
    if (!JedisURIHelper.isValid(uri)) {
      throw new InvalidURIException(String.format(
        "Cannot open Redis connection due invalid URI. %s", uri.toString()));
    }

    this.host = uri.getHost();
    this.port = uri.getPort();
    this.password = JedisURIHelper.getPassword(uri);
    this.db = JedisURIHelper.getDBIndex(uri);
  }

@Override
  public Jedis createResource() {
    return new Jedis(this);
  }
    /**
    *    省略setters和getters
    **/
}

可见JedisShardInfo包含了一个redis节点ip地址,端口号,name,密码等等相关信息。要构造一个ShardedJedis,提供一个或多个JedisShardInfo。

最终构造函数的实现在其父类Sharded里面

public Sharded(List<S> shards, Hashing algo, Pattern tagPattern) {
        this.algo = algo;
        this.tagPattern = tagPattern;
        initialize(shards);
    }

哈希环的初始化

Sharded类里面维护了一个TreeMap,基于红黑树实现,用来盛放经过一致性哈希计算后的redis节点,另外维护了一个LinkedHashMap,用来保存ShardInfo与Jedis实例的对应关系。
定位的流程如下
先在TreeMap中找到对应key所对应的ShardInfo,然后通过ShardInfo在LinkedHashMap中找到对应的Jedis实例。

Sharded类对这些实例变量的定义如下所示:

public static final int DEFAULT_WEIGHT = 1;
    private TreeMap<Long, S> nodes;
    private final Hashing algo;
    private final Map<ShardInfo<R>, R> resources = new LinkedHashMap<ShardInfo<R>, R>();

    /**
     * The default pattern used for extracting a key tag. The pattern must have
     * a group (between parenthesis), which delimits the tag to be hashed. A
     * null pattern avoids applying the regular expression for each lookup,
     * improving performance a little bit is key tags aren't being used.
     */
    private Pattern tagPattern = null;
    // the tag is anything between {}
    public static final Pattern DEFAULT_KEY_TAG_PATTERN = Pattern.compile("\\{(.+?)\\}");

接下来看其构造函数中的initialize方法

private void initialize(List<S> shards) {
        nodes = new TreeMap<Long, S>();

        for (int i = 0; i != shards.size(); ++i) {
            final S shardInfo = shards.get(i);
            if (shardInfo.getName() == null)
                for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
                    nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);
                }
            else
                for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
                    nodes.put(
                            this.algo.hash(shardInfo.getName() + "*"
                                    + shardInfo.getWeight() + n), shardInfo);
                }
            resources.put(shardInfo, shardInfo.createResource());
        }
    }

可以看到,它对每一个ShardInfo通过一定规则计算其哈希值,然后存到TreeMap中,这里它实现了一致性哈希算法中虚拟节点的概念,因为我们可以看到同一个ShardInfo不止一次被放到TreeMap中,数量是,权重*160。
增加了虚拟节点的一致性哈希有很多好处,能避免数据在redis节点间分布不均匀。

然后,在LinkedHashMap中放入ShardInfo以及其对应的Jedis实例,通过调用其自身的createSource()来得到jedis实例。

数据定位

从ShardedJedis的代码中可以看到,无论进行什么操作,都要先根据key来找到对应的Redis,然后返回一个可供操作的Jedis实例。

例如其set方法:

public String set(String key, String value) {
        Jedis j = getShard(key);
        return j.set(key, value);
    }

而getShard方法则在Sharded.java中实现,其源代码如下所示:

public R getShard(byte[] key) {
        return resources.get(getShardInfo(key));
    }

    public R getShard(String key) {
        return resources.get(getShardInfo(key));
    }

    public S getShardInfo(byte[] key) {
        SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));
        if (tail.isEmpty()) {
            return nodes.get(nodes.firstKey());
        }
        return tail.get(tail.firstKey());
    }

    public S getShardInfo(String key) {
        return getShardInfo(SafeEncoder.encode(getKeyTag(key)));
    }

可以看到,先通过getShardInfo方法从TreeMap中获得对应的ShardInfo,然后根据这个ShardInfo就能够再LinkedHashMap中获得对应的Jedis实例了。

联系我


ytbean
3.1k 声望714 粉丝

十年学会编程