您的位置:首页 > 数据库 > Redis

Redis集群方案及实现

2016-06-02 13:09 465 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。

目录(?)[+]

之前做了一个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
plain copy

public class RedisKey implements Serializable{  

    private static final long serialVersionUID = 1L;  

      

    //每个业务不同的family  

    private String family;  

      

    private String key;  

          

    ......    

    //物理保存在Redis上的key为经过MurmurHash之后的值  

    private String makeRedisHashKey(){  

        return String.valueOf(MurmurHash.hash64(makeRedisKeyString()));  

    }  

      

    //ReidsKey由family.key组成  

    private String makeRedisKeyString(){  

        return family +":"+ key;  

    }  

  

    //返回用户的经过Hash之后RedisKey  

    public String getRedisKey(){  

        return makeRedisHashKey();  

    }  

    .....  

}  

Family的存在时为了避免多个业务key冲突,给每个业务定义自己独立的Faimily

出于性能考虑,参考Redis存储设计,实际保存在Redis上的key为经过hash之后的值

接口

目前支持的接口包括:

[java] view
plain copy

public interface RedisUseInterface{  

    /** 

     * 通过RedisKey获取value 

     *  

     * @param redisKey 

     *           redis中的key 

     * @return  

     *           成功返回value,查询不到返回NULL 

     */  

    public String get(final RedisKey redisKey) throws Exception;  

      

    /** 

     * 插入<k,v>数据到Redis 

     *  

     * @param redisKey 

     *           the redis key 

     * @param value 

     *           the redis value 

     * @return  

     *           成功返回"OK",插入失败返回NULL 

     */  

    public String set(final RedisKey redisKey, final String value) throws Exception;  

      

    /** 

     * 批量写入数据到Redis 

     *  

     * @param redisKeys 

     *           the redis key list 

     * @param values 

     *           the redis value list 

     * @return  

     *           成功返回"OK",插入失败返回NULL 

     */  

    public String mset(final ArrayList<RedisKey> redisKeys, final ArrayList<String> values) throws Exception;  

      

      

    /** 

     * 从Redis中删除一条数据 

     *  

     * @param redisKey 

     *           the redis key 

     * @return  

     *           an integer greater than 0 if one or more keys were removed 0 if none of the specified key existed 

     */  

    public Long del(RedisKey redisKey) throws Exception;  

      

    /** 

     * 从Redis中批量删除数据 

     *  

     * @param redisKey 

     *           the redis key 

     * @return  

     *           返回成功删除的数据条数 

     */  

    public Long del(ArrayList<RedisKey> redisKeys) throws Exception;  

      

    /** 

     * 插入<k,v>数据到Redis 

     *  

     * @param redisKey 

     *           the redis key 

     * @param value 

     *           the redis value 

     * @return  

     *           成功返回"OK",插入失败返回NULL 

     */  

    public String setByte(final RedisKey redisKey, final byte[] value) throws Exception;  

      

    /** 

     * 插入<k,v>数据到Redis 

     *  

     * @param redisKey 

     *           the redis key 

     * @param value 

     *           the redis value 

     * @return  

     *           成功返回"OK",插入失败返回NULL 

     */  

    public String setByte(final String redisKey, final byte[] value) throws Exception;  

      

    /** 

     * 通过RedisKey获取value 

     *  

     * @param redisKey 

     *           redis中的key 

     * @return  

     *           成功返回value,查询不到返回NULL 

     */  

    public byte[] getByte(final RedisKey redisKey) throws Exception;  

      

    /** 

     * 在指定key上设置超时时间 

     *  

     * @param redisKey 

     *           the redis key 

     * @param seconds 

     *           the expire seconds 

     * @return  

     *           1:success, 0:failed 

     */  

    public Long expire(RedisKey redisKey, int seconds) throws Exception;  

}  

写Redis流程

1. 计算Redis Key Hash值

2. 根据Hash值获取Redis Node编号

3. 从sentinel获取Redis Node的Master

4.  写数据到Redis

[java] view
plain copy

//获取写哪个Redis Node  

int slot = getSlot(keyHash);  

RedisDataNode redisNode =  rdList.get(slot);  

  

//写Master  

JedisSentinelPool jp = redisNode.getSentinelPool();  

Jedis je = null;  

boolean success = true;  

try {  

    je = jp.getResource();  

    return je.set(key, value);  

} catch (Exception e) {  

    log.error("Maybe master is down", e);  

    e.printStackTrace();  

    success = false;  

    if (je != null)  

        jp.returnBrokenResource(je);  

    throw e;  

} finally {  

    if (success && je != null) {  

        jp.returnResource(je);  

    }  

}  

读流程

1. 计算Redis Key Hash值

2. 根据Hash值获取Redis Node编号

3. 根据权重选取一个Redis Instatnce

4.  轮询读

[java] view
plain copy

//获取读哪个Redis Node  

int slot = getSlot(keyHash);  

RedisDataNode redisNode =  rdList.get(slot);  

  

//根据权重选取一个工作Instatnce  

int rn = redisNode.getWorkInstance();  

  

//轮询  

int cursor = rn;  

do {              

    try {  

        JedisPool jp = redisNode.getInstance(cursor).getJp();  

        return getImpl(jp, key);  

    } catch (Exception e) {  

        log.error("Maybe a redis instance is down, slot : [" + slot + "]" + e);  

        e.printStackTrace();  

        cursor = (cursor + 1) % redisNode.getInstanceCount();  

        if(cursor == rn){  

            throw e;  

        }  

    }  

} while (cursor != rn);  

权重计算

初始化的时候,会给每个Redis Instatnce赋一个权重值weight

根据权重获取Redis Instance的代码:

[java] view
plain copy

public int getWorkInstance() {  

    //没有定义weight,则完全随机选取一个redis instance  

    if(maxWeight == 0){  

        return (int) (Math.random() * RANDOM_SIZE % redisInstanceList.size());  

    }  

      

    //获取随机数  

    int rand = (int) (Math.random() * RANDOM_SIZE % maxWeight);  

    int sum = 0;  

  

    //选取Redis Instance  

    for (int i = 0; i < redisInstanceList.size(); i++) {  

        sum += redisInstanceList.get(i).getWeight();  

        if (rand < sum) {  

            return i;  

        }  

    }  

      

    return 0;  

}  




13

0
 
 

上一篇HBase原子性保证

下一篇Vector
Clock理解

我的同类文章

Redis(12)

•Redis
Cluster(Redis 3.X)设计要点2014-10-13阅读15735
•Redis
Sentinel的信息同步2014-04-14阅读2451
•Redis
Sentinel源码分析(一)2014-03-31阅读5040
Redis源码分析:主从复制2012-02-06阅读6001
Redis源码分析:AOF2012-01-11阅读3906
•Redis
存储结构设计2014-05-04阅读8757
•Redis
Sentinel源码分析(二)2014-03-31阅读2880
•Redis
repl-disable-tcp-nodelay配置2014-03-20阅读3301
Redis源码分析:snapshot2012-01-13阅读2769
Redis源码分析:内存管理2012-01-06阅读2823
更多文章

猜你在找

高并发之Redis初级
Linux运维高薪入门及进阶全新经典视频-老男孩Linux第一部(下)
高并发之Redis高级
Linux运维高薪入门及进阶全新经典视频-老男孩Linux第一部(上)
Java之路

ElasticSearch+LogStash+Kibana+Redis日志服务的高可用方案
Redis配置详解
Redis配置大全
分布式统一框架的设计与实现数据库
MemcacheEhcahe和Redis 三种缓存技术的对比

查看评论

8楼 qq_25073449 2016-03-24 19:32发表 [回复]


请问楼主:int slot = getSlot(keyHash); 

RedisDataNode redisNode = rdList.get(slot); 这两行是如何获取的?

rdList是什么列表?RedisDataNode 这个又是什么对象里面的

Re: yfk 2016-03-25 11:33发表 [回复]


回复qq_25073449:int slot = getSlot(keyHash) ; //获取key在哪个slot里

RedisDataNode redisNode = rdList.get(slot) //根据slot获取对应的RedisDataNode

RedisDataNode是封装的一个Node,里面有多台Redis Instance

rdList是RedisDataNode的List,包含了所有的RedisNode
7楼 liubaiwu 2015-10-30 18:02发表 [回复]


请问如果新增一个节点来分担其中一个节点 的数据,这时是否需要重新修改代码来调整。里面的RedisKey 对应的famliy是不是固定了。新增的时候需要重新指定新redis 存放的业务famly ?
6楼 liubaiwu 2015-10-30 18:00发表 [回复]


有一个问题,按你说的数据分为8份,每个master 有 1/8的数据。从上面描述中我的理解是这8份数据相当于对应8个业务板块,也就是说在RedisKey的family是8个不同的family。我们暂且把master 编号定义为1到8号。

假设现在1号master 由于访问量增加,需要在1号master 节点增加一台master 9号, 来为1号master分担压力。请问这时候要怎么操作。因为1号master 对应有一个family。那么现在增加了节点。就需要将所有属于这个family的数据拆分一部分出来,存放到9号master。这个才分过程是否需要去修改代码。qq:286067198 可以交流交流。正在研究这块的东西。

Re: yfk 2015-11-02 11:09发表 [回复]


回复liubaiwu:Hi,你好

Key存放Redis经过了MurmurHash做了Hash,所以不同的业务会打散分散到不同的Redis Node上

扩容是通过提前规划集群大小,通过迁移节点来完成的:)
5楼 liubaiwu 2015-10-30 17:59发表 [回复]


有一个问题,按你说的数据分为8份,每个master 有 1/8的数据。从上面描述中我的理解是这8份数据相当于对应8个业务板块,也就是说在RedisKey的family是8个不同的family。我们暂且把master 编号定义为1到8号。

假设现在1号master 由于访问量增加,需要在1号master 节点增加一台master 9号, 来为1号master分担压力。请问这时候要怎么操作。因为1号master 对应有一个family。那么现在增加了节点。就需要将所有属于这个family的数据拆分一部分出来,存放到9号master。这个才分过程是否需要去修改代码。qq:286067198 可以交流交流。正在研究这块的东西。
4楼 cheney0728 2015-06-11 18:04发表 [回复]


你所有的master之间的数据是怎么同步的?

Re: yfk 2015-06-12 13:55发表 [回复]


回复cheney0728:hi,“数据Hash分布在不同的Redis Instatnce上”

例如数据分成8份,则每个master有1/8的数据,彼此之间不同步数据~

Re: dodolzg 2015-08-09 16:39发表 [回复]


回复yfkiss:这样的话,当增加或减少节点有个手动的数据迁移过程,当节点出现故障服务就有概率性不能用

Re: yfk 2015-08-10 10:31发表 [回复]


回复dodolzg:迁移步骤是先在新机器上新增slave节点,该slave和master同步完后node状况是1主2从

如果待迁移节点是Master节点,则先切master,再下线相应节点

如果待迁移节点是Slave节点,则直接下线待迁移节点、

所以,如果不是迁移过程中,MS都挂掉,集群是可用的
Re: yfk 2015-08-10 10:32发表 [回复]


回复dodolzg:迁移步骤是先在新机器上新增slave节点,该slave和master同步完后node状况是1主2从

如果待迁移节点是Master节点,则先切master,再下线相应节点

如果待迁移节点是Slave节点,则直接下线待迁移节点、

所以,如果不是迁移过程中,MS都挂掉,集群是可用的
3楼 dev_test 2015-05-04 20:20发表 [回复]


不错!
2楼 gu566320 2015-03-16 10:48发表 [回复]


麻烦问一下那边''写Redis流程"和"读流程" 那边的keyhash怎么来的,还有getSlot 方法实现怎么搞的哈,万分感谢

我邮箱752396003@qq.com

Re: yfk 2015-03-16 11:35发表 [回复]


回复gu566320:用的murmur hash

getslot方法获取数据落在哪个桶里面
1楼 迷_失 2014-12-19 20:57发表 [回复] [引用] [举报]


您好!看了您的博客,有些感触,也有些疑问,希望能得到您的回复,有发私信给你,非常期待你的解答:84226733@qq.com
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: