Jedis分片策略-一致性Hash


Jedis分片策略-一致性Hash
1. Spring配置文件:配置redis的参数

     <bean id="redisUtils" class="com.jd.data.spring.RedisClientFactoryBean">

        <!--zookeeper 配置优先文本配置,如果两个都有配置,只从zookeeper中取redis config -->

        <!-- 文本配置 开始 -->

        <!-- 单个应用中的链接池最大链接数-->

        <property name="maxActive" value="${redis.maxActive}"/>

        <!-- 单个应用中的链接池最大空闲数-->

        <property name="maxIdle" value="${redis.maxIdle}"/>

        <!-- 单个应用中的链接池取链接时最大等待时间,单位:ms-->

        <property name="maxWait" value="${redis.maxWait}"/>

        <!-- 设置在每一次取对象时测试ping-->

        <property name="testOnBorrow" value="${testOnBorrow}"/>

        <!-- 设置redis connect request response timeout 单位:ms-->

        <property name="timeout" value="${redis.timeout}"/>

        <!-- master redis server 设置 -->

        <!-- host:port:password[可选,password中不要有":"],redis server顺序信息一定不要乱,请按照分配顺序填写,乱了就可能会出现一致性hash不同,造成不命中cache情况-->

        <property name="masterConfString" value="${redis.master.hosts}"/>

        <!-- slave redis server 设置[可选]-->

        <!-- host:port:password[可选,password中不要有":"],redis server顺序信息一定不要乱,请按照分配顺序填写,乱了就可能会出现一致性hash不同,造成不命中cache情况-->

        <property name="slaveConfString" value="${redis.slave.hosts}"/>

        <!-- 文本配置 结束 -->

 

        <!--zookeeper 配置优先文本配置,如果两个都有,只从zookeeper中取redis config -->

        <!-- zookeeper server 地址-->

        <property name="zooKeeperServers" value="${redis.zkServers}"/>

        <!-- zookeeper中 redis config node path-->

        <property name="zooKeeperConfigRedisNodeName" value="${zooKeeperConfigRedisNodeName}"/>

        <!-- zookeeper client timeout -->

        <property name="zooKeeperTimeout" value="${redis.zkSessionTimeout}"/>

        <!--zookeeper 配置结束 -->

    </bean>

 

2. FactoryBean初始化RedisUtil对象

public class RedisClientFactoryBean implements FactoryBean{     

private ConnectionFactoryBuilder connectionFactoryBuilder = new ConnectionFactoryBuilder();

    private List<String> masterConfList = null;

    private List<String> slaveConfList = null;

 

    public Object getObject() throws Exception {

        //优先zookeeper配置,先检查,由于是分布式环境,我们在上线前手动调用zk,往一个目录中初始化redis参数,这样之后的线上环境就会读取zk里的redis信息了

        if(connectionFactoryBuilder.getZookeeperServers()!=null && connectionFactoryBuilder.getZookeeperServers().trim().length()>0

                && connectionFactoryBuilder.getZookeeperConfigRedisNodeName()!=null && connectionFactoryBuilder.getZookeeperConfigRedisNodeName().trim().length()>0){

            return new RedisUtils(connectionFactoryBuilder);

        }

        //检查spring redis server配置

        else if (slaveConfList==null || slaveConfList.size()==0){

            return newRedisUtils(connectionFactoryBuilder,masterConfList);

        }else if (masterConfList!=null && masterConfList.size()>0 && slaveConfList!=null && slaveConfList.size()>0){

            return new RedisUtils(connectionFactoryBuilder,masterConfList,slaveConfList);

        }else{

            throw new ExceptionInInitializerError("redisUtils all init parameter is empty,please check spring config file!");

        }

}

 

…

}

 

3. RedisUtil执行init方法

     public RedisUtils(ConnectionFactoryBuilder connectionFactoryBuilder, List<String> masterConfList, List<String> slaveConfList) {

        this.connectionFactoryBuilder = connectionFactoryBuilder;

        this.masterConfList = masterConfList;

        this.slaveConfList = slaveConfList;

        init();

}

 

   private void init() {

        log.info("init start~");

        List<JedisShardInfo> wShards = null;

        List<JedisShardInfo> rShards = null;

        //检查masterConfString 是否设置

        if (StringUtils.hasLength(connectionFactoryBuilder.getMasterConfString())) {

            //log.info("MasterConfString:" + connectionFactoryBuilder.getMasterConfString());

            masterConfList = Arrays.asList(connectionFactoryBuilder.getMasterConfString().split("(?:\\s|,)+"));

        }

        if (CollectionUtils.isEmpty(this.masterConfList)) {

            throw new ExceptionInInitializerError("masterConfString is empty!");

        }

        wShards = new ArrayList<JedisShardInfo>();

        for (String wAddr : this.masterConfList) {

            if (wAddr != null) {

                String[] wAddrArray = wAddr.split(":");

                if (wAddrArray.length == 1) {

                    throw new ExceptionInInitializerError(wAddr + " is not include host:port or host:port:passwd after split \":\"");

                }

                String host = wAddrArray[0];

                int port = Integer.valueOf(wAddrArray[1]);

                JedisShardInfo jedisShardInfo = new JedisShardInfo(host, port, connectionFactoryBuilder.getTimeout());

                log.info("masterConfList:" + jedisShardInfo.toString());

                //检查密码是否需要设置

                if (wAddrArray.length == 3 && StringUtils.hasLength(wAddrArray[2])) {

                    jedisShardInfo.setPassword(wAddrArray[2]);

                }

                wShards.add(jedisShardInfo);

            }

        }

//这里我们控制了读写分离,生成了两个连接池,一个wrtiePool,一个readPool。保存了我们的JedisShardInfo集合。

        this.writePool = new ShardedJedisPool(connectionFactoryBuilder.getJedisPoolConfig(), wShards);

 

        //检查slaveConfString 是否设置,并且检查主串与从串是否一致

        if (StringUtils.hasLength(connectionFactoryBuilder.getSlaveConfString()) &&

                !connectionFactoryBuilder.getSlaveConfString().equals(connectionFactoryBuilder.getMasterConfString())) {

            //log.info("SlaveConfString:" + connectionFactoryBuilder.getSlaveConfString());

            slaveConfList = Arrays.asList(connectionFactoryBuilder.getSlaveConfString().split("(?:\\s|,)+"));

            //检查是否有slave配置

            if (!CollectionUtils.isEmpty(this.slaveConfList)) {

                rShards = new ArrayList<JedisShardInfo>();

                for (String rAddr : this.slaveConfList) {

                    if (rAddr != null) {

                        String[] rAddrArray = rAddr.split(":");

                        if (rAddrArray.length == 1) {

                            throw new ExceptionInInitializerError(rAddr + " is not include host:port or host:port:passwd after split \":\"");

                        }

                        String host = rAddrArray[0];

                        int port = Integer.valueOf(rAddrArray[1]);

                        JedisShardInfo jedisShardInfo = new JedisShardInfo(host, port, connectionFactoryBuilder.getTimeout());

                        //检查密码是否需要设置

                        if (rAddrArray.length == 3 && StringUtils.hasLength(rAddrArray[2])) {

                            jedisShardInfo.setPassword(rAddrArray[2]);

                        }

                        log.info("slaveConfList:" + jedisShardInfo.toString());

                        rShards.add(jedisShardInfo);

                    }

                }

                this.readPool = new ShardedJedisPool(connectionFactoryBuilder.getJedisPoolConfig(), rShards);

                //在开启从机时,错误次数默认为1

                this.errorRetryTimes = 1;

            }

        }

 

        //出错后的重试次数

        if (connectionFactoryBuilder.getErrorRetryTimes() > 0) {

            this.errorRetryTimes = connectionFactoryBuilder.getErrorRetryTimes();

            log.error("after error occured redis api retry times is " + this.errorRetryTimes);

        }

 

        //是否有错误重试检查

        if (connectionFactoryBuilder.getErrorRetryTimes() > 0 && readPool == null) {

            //将主的连接池与从连接池设置为相同,为重试做准备

            this.readPool = this.writePool;

            log.error("readPool is null and errorRetryTimes >0,readPool is set to writePool");

        }

 

        //Object转码类定义

        transcoder = connectionFactoryBuilder.getDefaultTranscoder();

        log.info("init end~");

    }

遍历配置文件中的主从配置信息,构造一个JedisShardInfo对象,我们看下JedisShardInfo对象信息:

public class JedisShardInfo extends ShardInfo<Jedis> {

   //包含服务器的配置信息

 private int timeout;

    private String host;

    private int port;

    private String password = null;

private String name = null;

//重写createResource方法,用来生成该类对应的Jedis对象

@Override

 public Jedis createResource() {

        return new Jedis(this);

    }

}

 

public abstract class ShardInfo<T> {

    private int weight;//父类中包含了一个重要属性:权重,作为本jedis服务器的权值。

 

 

4. 构造ShardedJedis

构造ShardedJedis时,需要传入一个JedisShardInfo列表。然后ShardedJedis的父类的父类即Sharded就会对这个list进行初始化。

public class Sharded<R, S extends ShardInfo<R>> {

 

    public static final int DEFAULT_WEIGHT = 1; //默认权重1

    private TreeMap<Long, S> nodes; //一个treeMap,保存虚拟节点,模拟一致性hash

    private final Hashing algo;//hash算法,默认是murmurhash,这个算法的随机分布比较好

private final Map<ShardInfo<R>, R> resources = new LinkedHashMap<ShardInfo<R>, R>(); //保存shardInfo和jedis的映射关系,相当于一个主机对应的一个jedis,然后这个jedis来保存我们的缓存信息。

 

    public Sharded(List<S> shards, Hashing algo, Pattern tagPattern) {

        this.algo = algo;

        this.tagPattern = tagPattern;

        initialize(shards); //通过构造方法初始化虚拟节点和主机与jedis的映射关系

    }

 

    private void initialize(List<S> shards) {

        nodes = new TreeMap<Long, S>();

 

        for (int i = 0; i != shards.size(); ++i) { //遍历分片信息,即我们RedisUtil中初始化的List<JedisShardInfo>

            final S shardInfo = shards.get(i);

            if (shardInfo.getName() == null)

//一致性hash的核心:每个主机散列成160*权重个虚拟节点,分散在一个treeMap中

                   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与jedis的映射关系

        }

    }

这里的initialize方法是一致性Hash的核心。把每个主机对应成160*权重个虚拟节点,分散在环上(这里用的是TreeMap),比如有4台主机,权重为1,这样每个主机hash过来的key就可以分散成,160份,而非简单的1份,加大了散列的均匀性,更利于hash的性能。

       如果不用虚拟节点,比如只有4个主机,那么只有4个节点,假设node1对应key为0-1000的信息;node2对应1001-2000的信息… 这样,如果来了10个key全都小于1000,那么这些key全都分布在了node1上,node2,node3,node4根本没有key,分布很不均匀。

现在用了虚拟节点,一共有640个node,这样相当于node1-node160 对应key为0-1000的信息,node161-node320对应的key为1001-2000的信息,这样我们来了10个key全都小于1000,我们就会在node1-node160中找到10个node, 由于虚拟节点的treeMap是一颗红黑树,所以节点分布的比较均匀,而这10个node可能覆盖了所有的主机,这样我们的分布就非常均匀了。Weight越大,相当于一致性Hash的环路分布越密集,key对应的主机分散的概率就越大。

5. Set方法

RedisUtil中的方法:

            ShardedJedis j = null;

            String result = null;

            j = writePool.getResource(); //1.从写连接池中获取一个JedisShardInfo,然后获取对应的Jedis对象

            result = j.set(key, value); //调用ShardedJedis中的set方法

 

ShardedJedis的set方法:

    public String set(String key, String value) {

       Jedis j = getShard(key); //根据key,获取虚拟节点,

       return j.set(key, value); //

}

 

    public R getShard(String key) {

        return resources.get(getShardInfo(key));//根据key对应的JedisShardInfo信息从上文生成的resouces Map中拿到Jedis对象

    }

 

    public S getShardInfo(byte[] key) {

//获取key的hash值key1,然后在虚拟节点的map中找到key大于key1的节点,返回此映射的部分视图,利用map的tailMap方法。

        SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));

        if (tail.size() == 0) {

            return nodes.get(nodes.firstKey());//获取第一个大于key1的虚拟节点

        }

        return tail.get(tail.firstKey());

    }

 

大体思路:根据key进行murmurhash获取value,然后根据这个value,去nodes中查找key大于这个value的第一个键值对,返回对应的sharedInfo,(即一致性hash中,查找key对应的后续最近的服务器节点保存。),然后根据返回的sharedInfo从resource中获取对应的Jedis对象,然后进行set。

 

6. Get方法

RedisUtils  :  

public String get(String key) throws RedisAccessException {

        return get(errorRetryTimes, key); //这里的errorRetryTimes=0

    }  

 

private String get(int toTryCount, String key) throws RedisAccessException {

        String result = null;

        ShardedJedis j = null;

        boolean flag = true;

        try {

            if (toTryCount > 0) {//如果大于0,则读从库

                j = readPool.getResource();

            } else { //如果不大于0,则读主库

                j = writePool.getResource();

            }

            result = j.get(key); //拿到ShardedJedis对象

        …

}

 

ShardedJedis: get和set的逻辑基本一致了,先获取shard,然后获取jedis,然后获取value

    public String get(String key) {

       Jedis j = getShard(key);

       return j.get(key);

    }


展开阅读全文

没有更多推荐了,返回首页