Redis集群方案及实现

之前做了一个Redis的集群方案,跑了小半年,线上运行的很稳定
差不多可以跟大家分享下经验,前面写了一篇文章 数据在线服务的一些探索经验,可以做为背景阅读

应用

我们的Redis集群主要承担了以下服务:
1. 实时推荐
2. 用户画像
3. 诚信分值服务

集群状况

集群峰值QPS 1W左右,RW响应时间999线在1ms左右
整个集群:
1. Redis节点: 8台物理机;每台128G内存;每台机器上8个instance
2. Sentienl:3台虚拟机

集群方案


Redis Node由一组Redis Instance组成,一组Redis Instatnce可以有一个Master Instance,多个Slave Instance

Redis官方的cluster还在beta版本,参看Redis cluster tutorial
在做调研的时候,曾经特别关注过KeepAlived+VIP 和 Twemproxy
不过最后还是决定基于Redis Sentinel实现一套,整个项目大概在1人/1个半月

 

 

整体设计

1. 数据Hash分布在不同的Redis Instatnce上
2. M/S的切换采用Sentinel
3. 写:只会写master Instance,从sentinel获取当前的master Instane
4. 读:从Redis Node中基于权重选取一个Redis Instance读取,失败/超时则轮询其他Instance
5. 通过RPC服务访问,RPC server端封装了Redis客户端,客户端基于jedis开发
6. 批量写/删除:不保证事务

RedisKey

 

[java] view plaincopy
 
  1. public class RedisKey implements Serializable{  
  2.     private static final long serialVersionUID = 1L;  
  3.       
  4.     //每个业务不同的family  
  5.     private String family;  
  6.       
  7.     private String key;  
  8.           
  9.     ......    
  10.     //物理保存在Redis上的key为经过MurmurHash之后的值  
  11.     private String makeRedisHashKey(){  
  12.         return String.valueOf(MurmurHash.hash64(makeRedisKeyString()));  
  13.     }  
  14.       
  15.     //ReidsKey由family.key组成  
  16.     private String makeRedisKeyString(){  
  17.         return family +":"+ key;  
  18.     }  
  19.   
  20.     //返回用户的经过Hash之后RedisKey  
  21.     public String getRedisKey(){  
  22.         return makeRedisHashKey();  
  23.     }  
  24.     .....  
  25. }  


Family的存在时为了避免多个业务key冲突,给每个业务定义自己独立的Faimily
出于性能考虑,参考Redis存储设计,实际保存在Redis上的key为经过hash之后的值

接口

目前支持的接口包括:

[java] view plaincopy
 
  1. public interface RedisUseInterface{  
  2.     /** 
  3.      * 通过RedisKey获取value 
  4.      *  
  5.      * @param redisKey 
  6.      *           redis中的key 
  7.      * @return  
  8.      *           成功返回value,查询不到返回NULL 
  9.      */  
  10.     public String get(final RedisKey redisKey) throws Exception;  
  11.       
  12.     /** 
  13.      * 插入<k,v>数据到Redis 
  14.      *  
  15.      * @param redisKey 
  16.      *           the redis key 
  17.      * @param value 
  18.      *           the redis value 
  19.      * @return  
  20.      *           成功返回"OK",插入失败返回NULL 
  21.      */  
  22.     public String set(final RedisKey redisKey, final String value) throws Exception;  
  23.       
  24.     /** 
  25.      * 批量写入数据到Redis 
  26.      *  
  27.      * @param redisKeys 
  28.      *           the redis key list 
  29.      * @param values 
  30.      *           the redis value list 
  31.      * @return  
  32.      *           成功返回"OK",插入失败返回NULL 
  33.      */  
  34.     public String mset(final ArrayList<RedisKey> redisKeys, final ArrayList<String> values) throws Exception;  
  35.       
  36.       
  37.     /** 
  38.      * 从Redis中删除一条数据 
  39.      *  
  40.      * @param redisKey 
  41.      *           the redis key 
  42.      * @return  
  43.      *           an integer greater than 0 if one or more keys were removed 0 if none of the specified key existed 
  44.      */  
  45.     public Long del(RedisKey redisKey) throws Exception;  
  46.       
  47.     /** 
  48.      * 从Redis中批量删除数据 
  49.      *  
  50.      * @param redisKey 
  51.      *           the redis key 
  52.      * @return  
  53.      *           返回成功删除的数据条数 
  54.      */  
  55.     public Long del(ArrayList<RedisKey> redisKeys) throws Exception;  
  56.       
  57.     /** 
  58.      * 插入<k,v>数据到Redis 
  59.      *  
  60.      * @param redisKey 
  61.      *           the redis key 
  62.      * @param value 
  63.      *           the redis value 
  64.      * @return  
  65.      *           成功返回"OK",插入失败返回NULL 
  66.      */  
  67.     public String setByte(final RedisKey redisKey, final byte[] value) throws Exception;  
  68.       
  69.     /** 
  70.      * 插入<k,v>数据到Redis 
  71.      *  
  72.      * @param redisKey 
  73.      *           the redis key 
  74.      * @param value 
  75.      *           the redis value 
  76.      * @return  
  77.      *           成功返回"OK",插入失败返回NULL 
  78.      */  
  79.     public String setByte(final String redisKey, final byte[] value) throws Exception;  
  80.       
  81.     /** 
  82.      * 通过RedisKey获取value 
  83.      *  
  84.      * @param redisKey 
  85.      *           redis中的key 
  86.      * @return  
  87.      *           成功返回value,查询不到返回NULL 
  88.      */  
  89.     public byte[] getByte(final RedisKey redisKey) throws Exception;  
  90.       
  91.     /** 
  92.      * 在指定key上设置超时时间 
  93.      *  
  94.      * @param redisKey 
  95.      *           the redis key 
  96.      * @param seconds 
  97.      *           the expire seconds 
  98.      * @return  
  99.      *           1:success, 0:failed 
  100.      */  
  101.     public Long expire(RedisKey redisKey, int seconds) throws Exception;  
  102. }  

 

写Redis流程

1. 计算Redis Key Hash值
2. 根据Hash值获取Redis Node编号
3. 从sentinel获取Redis Node的Master
4.  写数据到Redis

[java] view plaincopy
 
  1. //获取写哪个Redis Node  
  2. int slot = getSlot(keyHash);  
  3. RedisDataNode redisNode =  rdList.get(slot);  
  4.   
  5. //写Master  
  6. JedisSentinelPool jp = redisNode.getSentinelPool();  
  7. Jedis je = null;  
  8. boolean success = true;  
  9. try {  
  10.     je = jp.getResource();  
  11.     return je.set(key, value);  
  12. catch (Exception e) {  
  13.     log.error("Maybe master is down", e);  
  14.     e.printStackTrace();  
  15.     success = false;  
  16.     if (je != null)  
  17.         jp.returnBrokenResource(je);  
  18.     throw e;  
  19. finally {  
  20.     if (success && je != null) {  
  21.         jp.returnResource(je);  
  22.     }  
  23. }  




读流程

1. 计算Redis Key Hash值
2. 根据Hash值获取Redis Node编号
3. 根据权重选取一个Redis Instatnce
4.  轮询读

[java] view plaincopy
 
  1. //获取读哪个Redis Node  
  2. int slot = getSlot(keyHash);  
  3. RedisDataNode redisNode =  rdList.get(slot);  
  4.   
  5. //根据权重选取一个工作Instatnce  
  6. int rn = redisNode.getWorkInstance();  
  7.   
  8. //轮询  
  9. int cursor = rn;  
  10. do {              
  11.     try {  
  12.         JedisPool jp = redisNode.getInstance(cursor).getJp();  
  13.         return getImpl(jp, key);  
  14.     } catch (Exception e) {  
  15.         log.error("Maybe a redis instance is down, slot : [" + slot + "]" + e);  
  16.         e.printStackTrace();  
  17.         cursor = (cursor + 1) % redisNode.getInstanceCount();  
  18.         if(cursor == rn){  
  19.             throw e;  
  20.         }  
  21.     }  
  22. while (cursor != rn);  




权重计算

初始化的时候,会给每个Redis Instatnce赋一个权重值weight
根据权重获取Redis Instance的代码:

[java] view plaincopy
 
    1. public int getWorkInstance() {  
    2.     //没有定义weight,则完全随机选取一个redis instance  
    3.     if(maxWeight == 0){  
    4.         return (int) (Math.random() * RANDOM_SIZE % redisInstanceList.size());  
    5.     }  
    6.       
    7.     //获取随机数  
    8.     int rand = (int) (Math.random() * RANDOM_SIZE % maxWeight);  
    9.     int sum = 0;  
    10.   
    11.     //选取Redis Instance  
    12.     for (int i = 0; i < redisInstanceList.size(); i++) {  
    13.         sum += redisInstanceList.get(i).getWeight();  
    14.         if (rand < sum) {  
    15.             return i;  
    16.         }  
    17.     }  
    18.       
    19.     return 0;  
    20. }  
posted @ 2015-08-04 17:01  郑文亮  阅读(856)  评论(0编辑  收藏  举报