Jedis之Sharded源码分析

1.概述

    当业务的数据量非常庞大时,单实例Redis可能无法支撑,需要考虑将数据存储到多个缓存节点上,如何定位数据应该存储的节点?有人说使用Redis集群不就搞定了吗?但Redis集群的部署和维护成本比较高,对于一些业务来说并不适合,所以更好的方案可能是在客户端实现对数据的分区(Sharding)。Sharding本质上实现了大数据分布式存储以及体现了集群的高可用。Jedis中Sharding通常使用一致性哈希算法,Jedis在客户端的角度实现了一致性哈希算法,对数据进行分片,存储到对应不同的redis实例中。

2.使用示例
package sharded;

import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.*;

import java.util.ArrayList;
import java.util.List;

/**
 * ShardJedis的测试类
 */
public class ShardJedisTest {

    private ShardedJedisPool sharedPool;
    private ShardedJedis shardedJedis;

    @Before
    public void initJedis(){
        JedisPoolConfig config =new JedisPoolConfig();//Jedis池配置
        config.setTestOnBorrow(true);
        String hostA = "192.168.58.99";
        int portA = 6379;
        String hostB = "192.168.58.99";
        int portB = 6380;
        List jdsInfoList =new ArrayList(2);
        JedisShardInfo infoA = new JedisShardInfo(hostA, portA);
        JedisShardInfo infoB = new JedisShardInfo(hostB, portB);
        jdsInfoList.add(infoA);
        jdsInfoList.add(infoB);
        shardedJedis = new ShardedJedis(jdsInfoList);

        sharedPool =new ShardedJedisPool(config, jdsInfoList);
    }

    @Test
    public void testSetKV() throws InterruptedException {
        try {
            for (int i=0;i<50;i++){
                String key = "test"+i;
                ShardedJedis shardedJedis = sharedPool.getResource();
                System.out.println(key+":"+shardedJedis.getShard(key).getClient().getHost()+":"+shardedJedis.getShard(key).getClient().getPort());
                System.out.println(shardedJedis.set(key,Math.random()+""));
                shardedJedis.close();
            }
        }catch (Exception e){
            e.printStackTrace();
        }

    }

    @Test
    public void ShardedJedisTest() {
        try {
            for (int i=0;i<50;i++){
                String key = "test"+i;
                System.out.println(key+":"+shardedJedis.getShard(key).getClient().getHost()+":"+shardedJedis.getShard(key).getClient().getPort());
                System.out.println(shardedJedis.set(key,Math.random()+""));
            }
            shardedJedis.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}
test29:192.168.58.99:6380
OK
test30:192.168.58.99:6379
OK
test31:192.168.58.99:6380
OK
test32:192.168.58.99:6379
OK
test33:192.168.58.99:6379
OK
test34:192.168.58.99:6380
OK
test35:192.168.58.99:6380
OK
test36:192.168.58.99:6379
OK
test37:192.168.58.99:6379

      可以看到不同的kv存储到了不同的Redis实例,Shard使用起来很简单,下面我们来看看他的具体实现。

3.源码解析
3.1 继承体系
ShardedJedis--->BinaryShardedJedis--->Sharded 
3.2 构造函数
public ShardedJedis(List shards, Hashing algo, Pattern keyTagPattern) {
        super(shards, algo, keyTagPattern);
    }

构造函数说明:

  • shards是一个JedisShardInfo的列表,一个JedisShardedInfo类代表一个数据分片的主体
  • algo是用来进行数据分片的算法
  • keyTagPattern,自定义分片算法所依据的key的形式。例如,可以不针对整个key的字符串做哈希计算,而是类似对thisisa{key}中包含在大括号内的字符串进行哈希计算。这样通过合理命名key,可以将一组相关联的key放入同一个Redis节点,这在避免跨节点访问相关数据时很重要。
        可以看出JedisShardInfo是构成ShardedJedis的主要组成:
public class JedisShardInfo extends ShardInfo {
  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;
    }
  }
              .......
@Override
  public Jedis createResource() {
    return new Jedis(this);
  }
    /**
    *    省略setters和getters
    **/
}

    JedisShardInfo包含了一个redis节点ip地址,端口号和密码等信息,要构建一个ShardedJedis,需要提供一个或多个JedisShardInfo,最终的构造函数在其父类Sharded里面:

 public Sharded(List shards, Hashing algo, Pattern tagPattern) {
    this.algo = algo;
    this.tagPattern = tagPattern;
    initialize(shards);
  }
3.3哈希环的初始化

    Sharded类里面维护了一个TreeMap,基于红黑树实现,用来存放经过一致性哈希计算后的redis节点,另外还维护了一个linkedHashMap用来保存ShardInfo与Jedis实例的对应关系。

3.3.1 定位key

    先在TreeMap中找到对应key所对应的ShardInfo,然后通过ShardInfo在LinkedHashMap中找到对应的Jedis实例。Sharded类对这些变量的定义如下:

public static final int DEFAULT_WEIGHT = 1;
    private TreeMap nodes;
    private final Hashing algo;
    private final Map, R> resources = new LinkedHashMap, R>();
    private Pattern tagPattern = null;
    // the tag is anything between {}
    public static final Pattern DEFAULT_KEY_TAG_PATTERN = Pattern.compile("\\{(.+?)\\}");

    Sharded构造函数中的initialize方法:

private void initialize(List shards) {
        nodes = new TreeMap();

        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++) {                   // 存放shard的hash key 与shardInfo的对应关系
                    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);
                }
            // 存放shardInfo和redis实例的对应关系
            resources.put(shardInfo, shardInfo.createResource());
        }
    }

    可以看到,它对每一个ShardInfo通过一定规则计算其哈希值,然后保存到TreeMap中,这里它实现了一致性哈希算法中虚拟节点的概念,因为我们看到同一个ShardInfo不止一次被放到TreeMap中,数量是权重*160,权重默认为1,可以自定义设置。增加虚拟节点的好处在于能避免数据在redis节点间分布不均匀。然后在LinkedHashMap中存放ShardInfo以及对应的jedis实例,通过调用自身的createSource()来创建jedis实例。

3.3.2 数据定位

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

public String set(String key, String value) {
        Jedis j = getShard(key);
        return j.set(key, value);
    }
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 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实例了。

4 presharding

    Redis Sharding采用客户端Sharding方式,服务端Redis还是一个个相对独立的Redis实例节点,没有做任何变动。同时,我们也不需要增加额外的中间处理组件,这是一种非常轻量、灵活的Redis多实例集群方法。当然,Redis Sharding这种轻量灵活方式必然在集群其它能力方面做出妥协。比如扩容,当想要增加Redis节点时,尽管采用一致性哈希,毕竟还是会有key匹配不到而丢失,这时需要键值迁移。作为轻量级客户端sharding,处理Redis键值迁移是不现实的,这就要求应用层面允许Redis中数据丢失或从后端数据库重新加载数据。但有些时候,击穿缓存层,直接访问数据库层,会对系统访问造成很大压力。那是否有办法让几乎所有的缓存都不会发生迁移呢??
    redis的作者给出的一个“机智”的方案了,那就是pre-sharding。具体的方法思路就是:前期机器有限的时候,在一台机器上部署多个redis实例(先把坑占住)。redis实例占用的系统资源很小,小于1M,所以这种方案是可行的。在客户端集群中让所有的实例都参与hash,当需要扩容的时候,选择这台机器上的一个实例作为主节点,新机器上的redis作为从节点,复制主节点上的所有数据,数据备份完成之后,将配置文件中该节点替换成新机器上的redis,同时将该节点提升为主节点,原旧节点可不再使用。需注意的是得让新旧节点的hash结果一致,否则不是我们期望的结果。所有节点用域名代替IP,替换时更换IP映射,这种方式比较好,就不需要更改配置文件了。这就相当于是先给集群加入很多的节点,每个节点的存储空间都很小,后续需要扩容时就将某个小节点焕成空间大的节点,并继续保持key和缓存节点之间的映射关系。这样就完成了平滑扩容。

你可能感兴趣的:(Jedis之Sharded源码分析)