以前作了一個Redis的集羣方案,跑了小半年,線上運行的很穩定
差很少能夠跟你們分享下經驗,前面寫了一篇文章 數據在線服務的一些探索經驗,能夠作爲背景閱讀
java
應用
咱們的Redis集羣主要承擔瞭如下服務:
1. 實時推薦
2. 用戶畫像
3. 誠信分值服務redis
集羣情況
集羣峯值QPS 1W左右,RW響應時間999線在1ms左右
整個集羣:
1. Redis節點: 8臺物理機;每臺128G內存;每臺機器上8個instance
2. Sentienl:3臺虛擬機dom
集羣方案
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. 批量寫/刪除:不保證事務spa
RedisKey
[java] view plain copy.net
- 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 copyserver
- 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 copyblog
- //獲取寫哪一個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;
- }