Jedis客户端分片的实现

对于单实例的Redis的使用,我们可以用Jedis,并发环境下我们可以用JedisPool。但是这两种方法否是针对于单实例的Redis的情况下使用的,但是有时候我们的业务可能不是单实例Redis能支撑的,那么我们这时候需要引入多个实例进行“数据分区”。其实好多人都说,用Redis集群不就搞定了吗?但是Redis集群无论部署还是维护成本都比较高,对于一些业务来说,使用起来还是成本很高。所以,对我们来说更好的方案可能是在客户端实现对数据的手动分区.

对于分区的方案,我感觉大多数人都会想到Hash,的确Hash是最简单最有效的方式。但是Hash的问题是:“单节点挂掉不可用,数据量大了不好扩容”。对于如果业务的可靠性要求不高同时数据可控的情况下可以考虑数据分区的方式。

其实数据分区就是Shard,其实Redis已经对Shard有很好的支持了,接下来简单的搞一下数据分片:

package redis.clients.jedis.tests;

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;

    @Before
    public void initJedis(){
        JedisPoolConfig config =new JedisPoolConfig();//Jedis池配置
        config.setTestOnBorrow(true);
        String hostA = "127.0.0.1";
        int portA = 6381;
        String hostB = "127.0.0.1";
        int portB = 6382;
        List<JedisShardInfo> jdsInfoList =new ArrayList<JedisShardInfo>(2);
        JedisShardInfo infoA = new JedisShardInfo(hostA, portA);
        JedisShardInfo infoB = new JedisShardInfo(hostB, portB);
        jdsInfoList.add(infoA);
        jdsInfoList.add(infoB);
        sharedPool =new ShardedJedisPool(config, jdsInfoList);
    }

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

    }

}

这里我是用JUnit做的测试,我在本机开了两个Redis实例:

Paste_Image.png

端口号分别是6381和6382。然后用ShardedJedisPool实现了一个Shard,主要是生成了50个Key,分别存到Redis中。运行结果如下:

test0:127.0.0.1:6382
OK
test1:127.0.0.1:6382
OK
test2:127.0.0.1:6381
OK
test3:127.0.0.1:6382
OK
test4:127.0.0.1:6382
OK
test5:127.0.0.1:6382
OK
test6:127.0.0.1:6382
OK
test7:127.0.0.1:6382
OK
test8:127.0.0.1:6381
OK
test9:127.0.0.1:6381

可以看到,KV分别分发到了不同的Redis实例,这种Shard的方式需要我们提前计算好数据量的大小,便于决定实例的个数。同时这种shard的可靠性不是很好,如果单个Redis实例挂掉了,那么这个实例便不可用了。

其实Shard使用起来很简单,接下来我们看看ShardedJedisPool的具体的实现:

首先在初始化ShardedJedisPool的时候我们需要创建一个JedisShardInfo实例,JedisShardInfo主要是对单个连接的相关配置:

public class JedisShardInfo extends ShardInfo<Jedis> {

  private static final String REDISS = "rediss";
  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;
  private boolean ssl;
  private SSLSocketFactory sslSocketFactory;
  private SSLParameters sslParameters;
  private HostnameVerifier hostnameVerifier;

像连接超时时间、发送超时时间、Host和port等。这些都是之前我们实例化Jedis用到的。
同时还需要进行JedisPoolConfig的设置,可以猜到ShardedJedisPool也是基于JedisPool来实现的。
看看ShardedJedisPool的构造:

  public ShardedJedisPool(final GenericObjectPoolConfig poolConfig, List<JedisShardInfo> shards) {
    this(poolConfig, shards, Hashing.MURMUR_HASH);
  }
    public ShardedJedisPool(final GenericObjectPoolConfig poolConfig, List<JedisShardInfo> shards,
      Hashing algo) {
    this(poolConfig, shards, algo, null);
  }
    public ShardedJedisPool(final GenericObjectPoolConfig poolConfig, List<JedisShardInfo> shards,
      Hashing algo, Pattern keyTagPattern) {
    super(poolConfig, new ShardedJedisFactory(shards, algo, keyTagPattern));
  }
    public Pool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {
    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);
  }

构造方法很长,但是很清晰,关键点在ShardedJedisFactory的构建,因为这是使用commons-pool的必要工厂类。同时我们可以看到,这里分分片策略使用的确实是Hash,而且还是冲突率很低的MURMUR_HASH。这里不了解commons-pool的可以看一下之前的Commons-pool源码分析[http://www.jianshu.com/p/b49452fb3a67]

那么我们直接看ShardedJedisFactory类就好了,因为commons-pool就是基于这个工厂类来管理相关的对象的,这里缓存的对象是ShardedJedis
我们先看一下ShardedJedisFactory:

    public ShardedJedisFactory(List<JedisShardInfo> shards, Hashing algo, Pattern keyTagPattern) {
      this.shards = shards;
      this.algo = algo;
      this.keyTagPattern = keyTagPattern;
    }

    @Override
    public PooledObject<ShardedJedis> makeObject() throws Exception {
      ShardedJedis jedis = new ShardedJedis(shards, algo, keyTagPattern);
      return new DefaultPooledObject<ShardedJedis>(jedis);
    }

    @Override
    public void destroyObject(PooledObject<ShardedJedis> pooledShardedJedis) throws Exception {
      final ShardedJedis shardedJedis = pooledShardedJedis.getObject();
      for (Jedis jedis : shardedJedis.getAllShards()) {
        try {
          try {
            jedis.quit();
          } catch (Exception e) {

          }
          jedis.disconnect();
        } catch (Exception e) {

        }
      }
    }
    
        @Override
    public boolean validateObject(PooledObject<ShardedJedis> pooledShardedJedis) {
      try {
        ShardedJedis jedis = pooledShardedJedis.getObject();
        for (Jedis shard : jedis.getAllShards()) {
          if (!shard.ping().equals("PONG")) {
            return false;
          }
        }
        return true;
      } catch (Exception ex) {
        return false;
      }
    }

其实这里makeObject是创建一个ShardedJedis,同时ShardedJedis也是连接池里保存的对象。
可以看到destroyObject和validateObject都是将ShardedJedis里的redis实例当做了一个整体去对待,一个失败,全部失败。
接下来看下ShardedJedis的实现,这个里面主要做了Hash的处理和各个Shard的Client的缓存。

public class ShardedJedis extends BinaryShardedJedis implements JedisCommands, Closeable {

  protected ShardedJedisPool dataSource = null;

  public ShardedJedis(List<JedisShardInfo> shards) {
    super(shards);
  }

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

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

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

这里的dataSource是对连接池的引用,用于在Close的时候资源返还。和JedisPool的思想差不多。
由于ShardedJedis是BinaryShardedJedis的子类,所以构造函数会一直向上调用,在Shard中:

  public Sharded(List<S> shards, Hashing algo, Pattern tagPattern) {
    this.algo = algo;
    this.tagPattern = tagPattern;
    initialize(shards);
  }
  
  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());
    }
  }

这里主要做整个ShardedJedis中Jedis缓存池的初始化和分片的实现,可以看到首先获取shardInfo就是之前的JedisShardInfo,根据shardInfo生成多个槽位,将这些槽位存到TreeMap中,同时将shardInfo和Jedis的映射存到resources中。当我们做Client的获取的时候:
首先调用ShardedJedisPool的getResource方法,从对象池中获取一个ShardedJedis:

ShardedJedis jedisClient = sharedPool.getResource();

调用ShardedJedis的getShard方法获取一个Jedis实例——一个shard。

  public R getShard(String key) {
    return resources.get(getShardInfo(key));
  }
    public S getShardInfo(String key) {
    return getShardInfo(SafeEncoder.encode(getKeyTag(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());
  }

这里主要是对key做hash,然后去TreeMap中判断,当前的key落在哪个区间上,再通过这个区间上的ShardInfo从resources的Map中获取对应的Jedis实例。

这也就是说,每一个ShardedJedis都维护了所有的分片,将多个实例当成一个整体去使用,这也就导致,只要集群中一个实例不可用,整个ShardedJedis就不可用了。同时对于hash的分片方式,是不可扩容的,扩容之后原本应该存储在一起的数据就分离了。

其实这种是Jedis默认提供的分片方式,其实针对我们自己的场景我们也可以尝试自己做一个路由机制,例如根据不同年份、月份的数据落到一个实例上。

上面就是所有的数据分片的jedis实现的分析,我们线上的业务也是基于ShardedJedis来实现的,由于线上业务的QPS不高,量也不是很大,所以运行还算平稳。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容

  • 转载:Redis 宝典 | 基础、高级特性与性能调优 本文由 DevOpsDays 本文由简书作者kelgon供稿...
    meng_philip123阅读 3,105评论 1 34
  • 1.1 资料 ,最好的入门小册子,可以先于一切文档之前看,免费。 作者Antirez的博客,Antirez维护的R...
    JefferyLcm阅读 17,028评论 1 51
  • 对于日常开发,Redis由于单线程的并发模型、丰富的数据结构和简单的API,深受广大程序员的喜爱。Redis提供了...
    一只小哈阅读 9,023评论 10 26
  • 科技是第一生产力,世界历史已经证明了这样一个原理。人类世界在我们现在所认知的是经历了三次科技革命。 这三次科技革命...
    林沐日记阅读 1,445评论 0 2
  • 雨天,繁忙的市中心,一个垂垂老矣,拄着拐杖的老人,正在拦车, 来来往往的出租车很多,但是司机或许是怕他在车上有意外...
    燚月仁心阅读 229评论 0 0