Redis集群方案及實(shí)現(xiàn)
來源:程序員人生 發(fā)布時(shí)間:2014-09-02 23:37:00 閱讀次數(shù):3685次
之前做了一個(gè)Redis的集群方案,跑了小半年,線上運(yùn)行的很穩(wěn)定
差不多可以跟大家分享下經(jīng)驗(yàn),前面寫了一篇文章 數(shù)據(jù)在線服務(wù)的一些探索經(jīng)驗(yàn),可以做為背景閱讀
應(yīng)用
我們的Redis集群主要承擔(dān)了以下服務(wù):
1. 實(shí)時(shí)推薦
2. 用戶畫像
3. 誠(chéng)信分值服務(wù)
集群狀況
集群峰值QPS 1W左右,RW響應(yīng)時(shí)間999線在1ms左右
整個(gè)集群:
1. Redis節(jié)點(diǎn): 8臺(tái)物理機(jī);每臺(tái)128G內(nèi)存;每臺(tái)機(jī)器上8個(gè)instance
2. Sentienl:3臺(tái)虛擬機(jī)
集群方案

Redis Node由一組Redis Instance組成,一組Redis Instatnce可以有一個(gè)Master Instance,多個(gè)Slave Instance
Redis官方的cluster還在beta版本,參看Redis cluster tutorial
在做調(diào)研的時(shí)候,曾經(jīng)特別關(guān)注過KeepAlived+VIP 和 Twemproxy
不過最后還是決定基于Redis Sentinel實(shí)現(xiàn)一套,整個(gè)項(xiàng)目大概在1人/1個(gè)半月
整體設(shè)計(jì)
1. 數(shù)據(jù)Hash分布在不同的Redis Instatnce上
1. M/S的切換采用Sentinel
2. 寫:只會(huì)寫master Instance,從sentinel獲取當(dāng)前的master Instane
3. 讀:從Redis Node中基于權(quán)重選取一個(gè)Redis Instance讀取,失敗/超時(shí)則輪詢其他Instance
4. 通過RPC服務(wù)訪問,RPC server端封裝了Redis客戶端,客戶端基于jedis開發(fā)
5. 批量寫/刪除:不保證事務(wù)
RedisKey
public class RedisKey implements Serializable{
private static final long serialVersionUID = 1L;
//每個(gè)業(yè)務(wù)不同的family
private String family;
private String key;
......
//物理保存在Redis上的key為經(jīng)過MurmurHash之后的值
private String makeRedisHashKey(){
return String.valueOf(MurmurHash.hash64(makeRedisKeyString()));
}
//ReidsKey由family.key組成
private String makeRedisKeyString(){
return family +":"+ key;
}
//返回用戶的經(jīng)過Hash之后RedisKey
public String getRedisKey(){
return makeRedisHashKey();
}
.....
}
Family的存在時(shí)為了避免多個(gè)業(yè)務(wù)key沖突,給每個(gè)業(yè)務(wù)定義自己獨(dú)立的Faimily
出于性能考慮,參考Redis存儲(chǔ)設(shè)計(jì),實(shí)際保存在Redis上的key為經(jīng)過hash之后的值
接口
目前支持的接口包括:
public interface RedisUseInterface{
/**
* 通過RedisKey獲取value
*
* @param redisKey
* redis中的key
* @return
* 成功返回value,查詢不到返回NULL
*/
public String get(final RedisKey redisKey) throws Exception;
/**
* 插入<k,v>數(shù)據(jù)到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;
/**
* 批量寫入數(shù)據(jù)到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中刪除一條數(shù)據(jù)
*
* @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中批量刪除數(shù)據(jù)
*
* @param redisKey
* the redis key
* @return
* 返回成功刪除的數(shù)據(jù)條數(shù)
*/
public Long del(ArrayList<RedisKey> redisKeys) throws Exception;
/**
* 插入<k,v>數(shù)據(jù)到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>數(shù)據(jù)到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上設(shè)置超時(shí)時(shí)間
*
* @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. 計(jì)算Redis Key Hash值
2. 根據(jù)Hash值獲取Redis Node編號(hào)
3. 從sentinel獲取Redis Node的Master
4. 寫數(shù)據(jù)到Redis
//獲取寫哪個(gè)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. 計(jì)算Redis Key Hash值
2. 根據(jù)Hash值獲取Redis Node編號(hào)
3. 根據(jù)權(quán)重選取一個(gè)Redis Instatnce
4. 輪詢讀
//獲取讀哪個(gè)Redis Node
int slot = getSlot(keyHash);
RedisDataNode redisNode = rdList.get(slot);
//根據(jù)權(quán)重選取一個(gè)工作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);
權(quán)重計(jì)算
初始化的時(shí)候,會(huì)給每個(gè)Redis Instatnce賦一個(gè)權(quán)重值weight
根據(jù)權(quán)重獲取Redis Instance的代碼:
public int getWorkInstance() {
//沒有定義weight,則完全隨機(jī)選取一個(gè)redis instance
if(maxWeight == 0){
return (int) (Math.random() * RANDOM_SIZE % redisInstanceList.size());
}
//獲取隨機(jī)數(shù)
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;
}
生活不易,碼農(nóng)辛苦
如果您覺得本網(wǎng)站對(duì)您的學(xué)習(xí)有所幫助,可以手機(jī)掃描二維碼進(jìn)行捐贈(zèng)