標籤:blog http io java ar for 資料 2014 art
之前做了一個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上
1. M/S的切換採用Sentinel
2. 寫:只會寫master Instance,從sentinel擷取當前的master Instane
3. 讀:從Redis Node中基於權重選取一個Redis Instance讀取,失敗/逾時則輪詢其他Instance
4. 通過RPC服務訪問,RPC server端封裝了Redis用戶端,用戶端基於jedis開發
5. 批量寫/刪除:不保證事務
RedisKey
public class RedisKey implements Serializable{private static final long serialVersionUID = 1L;//每個業務不同的familyprivate 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之後RedisKeypublic 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 Nodeint slot = getSlot(keyHash);RedisDataNode redisNode = rdList.get(slot);//寫MasterJedisSentinelPool 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 Nodeint slot = getSlot(keyHash);RedisDataNode redisNode = rdList.get(slot);//根據權重選取一個工作Instatnceint 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 instanceif(maxWeight == 0){return (int) (Math.random() * RANDOM_SIZE % redisInstanceList.size());}//擷取隨機數int rand = (int) (Math.random() * RANDOM_SIZE % maxWeight);int sum = 0;//選取Redis Instancefor (int i = 0; i < redisInstanceList.size(); i++) {sum += redisInstanceList.get(i).getWeight();if (rand < sum) {return i;}}return 0;}
Redis叢集方案及實現