版权声明:本文为博主原创文章,未经博主允许不得转载。
之前做了一个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
- 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之后的值
接口
目前支持的接口包括:- 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
- //获取写哪个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. 轮询读
- //获取读哪个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的代码:
- 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
-
猜你在找
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