1. 程式人生 > >Redis叢集方案及實現

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

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;
	}