Jedis一致性hash与sharding

来源:互联网 发布:淘宝结算页面打不开 编辑:程序博客网 时间:2024/05/18 00:32

 Redis-server本身并没有sharding方法,不过我们可以借助客户端程序来实现此功能,Jedis中已经为我们提供了足够的API,接下来通过2种方式分别介绍3个API使用方法。不过首先介绍一下Jedis中sharding原理

 

一.Sharding与一致性Hash

    sharding的核心理念就是将数据按照一定的策略"分散"存储在集群中不同的物理server上,本质上实现了"大数据"分布式存储,以及体现了"集群"的高可用性.比如1亿数据,我们按照数据的hashcode散列存储在5个server上.

    Jedis中sharding基于“一致性hash”算法,其思路非常清晰,代码基本也是标准的“一致性hash”的实现,我们先来“欣赏”一下:

    1) hashcode取值:源码来自redis.clients.util.Hashing,Jedis中默认的hash值计算采取了MD5作为辅助,似乎此算法已经成为“标准”:

Java代码  收藏代码
  1. //少量优化性能  
  2. public ThreadLocal<MessageDigest> md5Holder = new ThreadLocal<MessageDigest>();  
  3. public static final Hashing MD5 = new Hashing() {  
  4. public long hash(String key) {  
  5.     return hash(SafeEncoder.encode(key));  
  6. }  
  7.   
  8. public long hash(byte[] key) {  
  9.     try {  
  10.         if (md5Holder.get() == null) {  
  11.             md5Holder.set(MessageDigest.getInstance("MD5"));  
  12.         }  
  13.     } catch (NoSuchAlgorithmException e) {  
  14.         throw new IllegalStateException("++++ no md5 algorythm found");  
  15.     }  
  16.     MessageDigest md5 = md5Holder.get();  
  17.   
  18.     md5.reset();  
  19.     md5.update(key);  
  20.     byte[] bKey = md5.digest();//获得MD5字节序列  
  21.     //前四个字节作为计算参数,最终获得一个32位int值.  
  22.     //此种计算方式,能够确保key的hash值更加“随即”/“离散”  
  23.     //如果hash值过于密集,不利于一致性hash的实现(特别是有“虚拟节点”设计时)  
  24.     long res = ((long) (bKey[3] & 0xFF) << 24)  
  25.             | ((long) (bKey[2] & 0xFF) << 16)  
  26.             | ((long) (bKey[1] & 0xFF) << 8) | (long) (bKey[0] & 0xFF);  
  27.     return res;  
  28. }  
  29. };  

   

    2) node构建过程(redis.clients.util.Sharded):

Java代码  收藏代码
  1. //shards列表为客户端提供了所有redis-server配置信息,包括:ip,port,weight,name  
  2. //其中weight为权重,将直接决定“虚拟节点”的“比例”(密度),权重越高,在存储是被hash命中的概率越高  
  3. //--其上存储的数据越多。  
  4. //其中name为“节点名称”,jedis使用name作为“节点hash值”的一个计算参数。  
  5. //---  
  6. //一致性hash算法,要求每个“虚拟节点”必须具备“hash值”,每个实际的server可以有多个“虚拟节点”(API级别)  
  7. //其中虚拟节点的个数= “逻辑区间长度” * weight,每个server的“虚拟节点”将会以“hash”的方式分布在全局区域中  
  8. //全局区域总长为2^32.每个“虚拟节点”以hash值的方式映射在全局区域中。  
  9. // 环形:0-->vnode1(:1230)-->vnode2(:2800)-->vnode3(400000)---2^32-->0  
  10. //所有的“虚拟节点”将按照其”节点hash“顺序排列(正序/反序均可),因此相邻两个“虚拟节点”之间必有hash值差,  
  11. //那么此差值,即为前一个(或者后一个,根据实现而定)“虚拟节点”所负载的数据hash值区间。  
  12. //比如hash值为“2000”的数据将会被vnode1所接受。  
  13. //---  
  14. private void initialize(List<S> shards) {  
  15.     nodes = new TreeMap<Long, S>();//虚拟节点,采取TreeMap存储:排序,二叉树  
  16.   
  17.     for (int i = 0; i != shards.size(); ++i) {  
  18.         final S shardInfo = shards.get(i);  
  19.         if (shardInfo.getName() == null)  
  20.                 //当没有设置“name”是,将“SHARD-NODE”作为“虚拟节点”hash值计算的参数  
  21.                 //"逻辑区间步长"为160,为什么呢??  
  22.                 //最终多个server的“虚拟节点”将会交错布局,不一定非常均匀。  
  23.             for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {  
  24.                 nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);  
  25.             }  
  26.         else  
  27.             for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {  
  28.                 nodes.put(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n), shardInfo);  
  29.             }  
  30.         resources.put(shardInfo, shardInfo.createResource());  
  31.     }  
  32. }  

    3) node选择方式:

Java代码  收藏代码
  1. public R getShard(String key) {  
  2.     return resources.get(getShardInfo(key));  
  3. }  
  4. //here:  
  5. public S getShardInfo(byte[] key) {  
  6.         //获取>=key的“虚拟节点”的列表  
  7.     SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));  
  8.         //如果不存在“虚拟节点”,则将返回首节点。  
  9.     if (tail.size() == 0) {  
  10.         return nodes.get(nodes.firstKey());  
  11.     }  
  12.         //如果存在,则返回符合(>=key)条件的“虚拟节点”的第一个节点  
  13.     return tail.get(tail.firstKey());  
  14. }  

    4) 补充:

    Jedis sharding模式下,如果某个server失效,客户端并不会删除此shard,所以如果访问此shard将会抛出异常。这是为了保持所有的客户端数据视图一致性。你可能希望动态的一致性hash拓扑结构(即如果某个shard失效,shard结构则重新调整,失效的shard上的数据则被hash到其他shard上),但是很遗憾,SharedJedis客户端无法支持,如果非要支持,则需要巨大的代码调整,而且还需要引入额外的拓扑自动发现机制。(参看:redis cluster架构,已提供此问题的完善解决方案)

    不过,在持久存储的情况下,我们可以使用"强hash"分片,则需要重写其Hash算法,参见"程序实例1中的InnnerHashing"实现.强hash算法下,如果某个虚拟节点所在的物理server故障,将导致数据无法访问(读取/存储);即不会从虚拟节点列表中删除那些失效的server。

 

    对于jedis如果重写了Hashing算法,你需要兼顾几个方面:1) 虚拟节点hash是否相对均匀 2) 数据的hash值分布是否均匀 3) 虚拟节点在“全局”是否散列均匀。。如果设计不良,很有可能导致数据在server上分布不均,而失去了sharding的本身意义。

 

二.ShardedJedis

    1) 程序实例

 

Java代码  收藏代码
  1. public static void main(String[] args){  
  2.     //ip,port,timeout,weight  
  3.     JedisShardInfo si1 = new JedisShardInfo("127.0.0.1"6379,15000,1);  
  4.     JedisShardInfo si2 = new JedisShardInfo("127.0.0.1"6479,15000,1);  
  5.     List<JedisShardInfo> shards = new ArrayList<JedisShardInfo>();  
  6.     shards.add(si1);  
  7.     shards.add(si2);  
  8.     //指定hash算法,默认为"cache-only"的一致性hash,不过此处InnerHashing为强hash分片  
  9.     ShardedJedis shardedJedis = new ShardedJedis(shards,new InnerHashing());  
  10.     //指定hash算法  
  11.     shardedJedis.set("k1""v1");  
  12.     Charset charset = Charset.forName("utf-8");  
  13.     //注意此处对key的字节转换时,一定要和Innerhashing.hash(String)保持一致  
  14.     System.out.println(shardedJedis.get("k1").getBytes(charset));  
  15. }  
  16. //不建议自己重新hash算法,jedis的默认算法已经足够良好,默认为"一致性hash"分片  
  17. //此hash算法,为"强Hash"分片  
  18. static class InnerHashing implements Hashing{  
  19.     static Charset charset = Charset.forName("utf-8");  
  20.     @Override  
  21.     public long hash(String key) {  
  22.         return hash(key.getBytes(charset));  
  23.     }  
  24.   
  25.     @Override  
  26.     public long hash(byte[] key) {  
  27.         int hashcode = new HashCodeBuilder().append(key).toHashCode();  
  28.         return hashcode & 0x7FFFFFFF;  
  29.     }  
  30.       
  31. }  

    2) spring环境下

 

Java代码  收藏代码
  1. <bean id="shardedJedis" class="redis.clients.jedis.ShardedJedis">  
  2.     <constructor-arg>  
  3.         <list>  
  4.             <bean class="redis.clients.jedis.JedisShardInfo">  
  5.                 <constructor-arg value="127.0.0.1"></constructor-arg>  
  6.                 <constructor-arg value="6379"></constructor-arg>  
  7.                 <property name="password" value="0123456"></property>  
  8.             </bean>  
  9.             <bean class="redis.clients.jedis.JedisShardInfo">  
  10.                 <constructor-arg value="127.0.0.1"></constructor-arg>  
  11.                 <constructor-arg value="6379"></constructor-arg>  
  12.                 <property name="password" value="0123456"></property>  
  13.             </bean>  
  14.         </list>  
  15.     </constructor-arg>  
  16. </bean>  
Java代码  收藏代码
  1. //resources/beans.xml  
  2. ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:beans.xml");  
  3. ShardedJedis shardedJedis = (ShardedJedis)context.getBean("shardedJedis");  
  4. try{  
  5.     shardedJedis.set("k1""v2");  
  6.     System.out.println(shardedJedis.get("k1"));  
  7. }catch(Exception e){  
  8.     e.printStackTrace();  
  9. }  

 

三.ShardedJedisPool

    基于连接池的sharding代码实例.

    1) 程序实例

Java代码  收藏代码
  1. JedisPoolConfig config = new JedisPoolConfig();  
  2. config.setMaxActive(32);  
  3. config.setMaxIdle(6);  
  4. config.setMinIdle(0);  
  5. config.setMaxWait(15000);  
  6.   
  7. JedisShardInfo si1 = new JedisShardInfo("127.0.0.1"6379,15000,1);  
  8. JedisShardInfo si2 = new JedisShardInfo("127.0.0.1"6479,15000,1);  
  9. List<JedisShardInfo> shards = new ArrayList<JedisShardInfo>();  
  10. shards.add(si1);  
  11. shards.add(si2);  
  12. ShardedJedisPool sjp = new ShardedJedisPool(config, shards, new InnerHashing());  
  13. ShardedJedis shardedJedis = sjp.getResource();  
  14. try{  
  15.     System.out.println(shardedJedis.get("k1"));  
  16. }catch(Exception e){  
  17.     e.printStackTrace();  
  18. }finally{  
  19.     sjp.returnResource(shardedJedis);  
  20. }  

    2) spring环境下

Java代码  收藏代码
  1. <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">  
  2.     <property name="maxActive" value="32"></property>  
  3.     <property name="maxIdle" value="6"></property>  
  4.     <property name="maxWait" value="15000"></property>  
  5.     <property name="minEvictableIdleTimeMillis" value="300000"></property>  
  6.     <property name="numTestsPerEvictionRun" value="3"></property>  
  7.     <property name="timeBetweenEvictionRunsMillis" value="60000"></property>  
  8.     <property name="whenExhaustedAction" value="1"></property>  
  9. </bean>  
  10. <bean id="shardedJedisPool" class="redis.clients.jedis.ShardedJedisPool" destroy-method="destroy">  
  11.     <constructor-arg ref="jedisPoolConfig"></constructor-arg>  
  12.     <constructor-arg>  
  13.         <list>  
  14.             <bean class="redis.clients.jedis.JedisShardInfo">  
  15.                 <constructor-arg value="127.0.0.1"></constructor-arg>  
  16.                 <constructor-arg value="6379"></constructor-arg>  
  17.                 <property name="password" value="0123456"></property>  
  18.             </bean>  
  19.             <bean class="redis.clients.jedis.JedisShardInfo">  
  20.                 <constructor-arg value="127.0.0.1"></constructor-arg>  
  21.                 <constructor-arg value="6379"></constructor-arg>  
  22.                 <property name="password" value="0123456"></property>  
  23.             </bean>  
  24.         </list>  
  25.     </constructor-arg>  
  26. </bean>  

    更多连接池的配置参数,请参考:jedis连接池

Java代码  收藏代码
  1. //resources/beans.xml  
  2. ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:beans.xml");  
  3. ShardedJedisPool shardedJedisPool = (ShardedJedisPool)context.getBean("shardedJedisPool");  
  4. ShardedJedis shardedJedis = shardedJedisPool.getResource();  
  5. try{  
  6.     shardedJedis.set("k1""v2");  
  7.     System.out.println(shardedJedis.get("k1"));  
  8. }catch(Exception e){  
  9.     e.printStackTrace();  
  10. }finally{  
  11.     shardedJedisPool.returnResource(shardedJedis);  
  12. }  

 

四.ShardedJedisPipeline    其他配置参见上文

Java代码  收藏代码
  1. ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:beans.xml");  
  2. ShardedJedisPool shardedJedisPool = (ShardedJedisPool)context.getBean("shardedJedisPool");  
  3. ShardedJedis shardedJedis = shardedJedisPool.getResource();  
  4. try{  
  5.     ShardedJedisPipeline shardedJedisPipeline = new ShardedJedisPipeline();  
  6.     shardedJedisPipeline.setShardedJedis(shardedJedis);  
  7.     shardedJedisPipeline.set("k1""v1");  
  8.     shardedJedisPipeline.set("k3""v3");  
  9.     shardedJedisPipeline.get("k3");  
  10.     List<Object> results = shardedJedisPipeline.syncAndReturnAll();  
  11.     for(Object result : results){  
  12.         System.out.println(result.toString());  
  13.     }  
  14. }catch(Exception e){  
  15.     e.printStackTrace();  
  16. }finally{  
  17.     shardedJedisPool.returnResource(shardedJedis);  
  18. }