1. 程式人生 > >美團在Redis上踩過的一些坑

美團在Redis上踩過的一些坑

上上週和同事(龍哥)參加了360組織的網際網路技術訓練營第三期,美團網的DBA負責人侯軍偉給大家介紹了美團網在redis上踩得一些坑,講的都是乾貨和坑。

    分為5個部分:

1. 背景:

      大部分網際網路公司都會有Mysql或者Oracle的DBA,但是在Nosql方面一般不會設定專門的DBA。不過對於一些知名的網際網路公司來說,Nosql的使用量是巨大的,所以通常讓Mysql的DBA或者單獨聘請工程師來維護一些Nosql資料庫,比如:

      Redis, Hbase, Memcache(其實嚴格講不是nosql), Mongodb, Cassandra。從講座看美團網應該是有專職的Redis DBA。所以作為業務開發人員不需要自己安裝、配置、運維Redis,只需要找Redis DBA來申請就可以了。

      這裡為了簡化說明:Redis DBA提供的服務叫做Redis雲,業務開發人員叫做業務端(redis的使用者)

     

   2. 現象:

       業務端在使用redis雲提供的redis服務後,經常出現connect timeout:

Java程式碼  收藏程式碼

  1. redis.clients.jedis.exceptions.JedisConnectionException  
  2. java.net.SocketException  
  3. java.net.SocketTimeoutException:connect time out  

   3. 分析和懷疑:

   業務端一般認為redis出現問題,就是redis雲有問題,人的“正常”思維:看別人錯誤容易,發現自己難,扯多了, 出現這個有很多原因:

   (1). 網路原因:比如是否存在跨機房、網路割接等等。

   (2). 慢查詢,因為redis是單執行緒,如果有慢查詢的話,會阻塞住之後的操作。 

   (3). value值過大?比如value幾十兆,當然這種情況比較少,其實也可以看做是慢查詢的一種

   (4). aof重寫/rdb fork發生?瞬間會堵一下Redis伺服器。

   (5). 其他..................

   4. 查詢原因

   演講者一開始懷疑是網路問題,但是並未發現問題,觀察各種對比圖表,tcp listenOverFlow和timeout經常週期出現。(贊一下這個監控,我們監控現在還沒有這個層面的)

   有關listenOverFlow:

檢視現有的連線數是否大於設定的backlog,如果大於就丟棄,並相應的引數值加1。其中backlog是由程式和系統引數net.core.somaxconn共同設定,當backlog的值大於系統設定的net.core.somaxconn時則取net.core.somaxconn的值,否則取程式設定的backlog值。這種出錯的方式也被記錄在TcpListenOverflows中(其只記錄了連線個數不足而產生溢位錯誤的次數!)。

   覺得可能和TCP相關,於是分析了Tcp三次握手:最後一次握手客戶端的請求會進入伺服器端的一個佇列(可以認為是下三圖)中,如果這個佇列滿了,就會發生上面的異常。(accept)

  (1) TCP三次握手: 

  

  (2) redis客戶端與redis伺服器互動的過程(本質就是TCP請求)

  (3) I/O 多路複用程式通過佇列向檔案事件分派器傳送套接字的過程

   

   (4) 和redis有什麼關係呢?

        由於Redis的單執行緒模型(對命令的處理和連線的處理都是在一個執行緒中),如果存在慢查詢的話,會出現上面的這種情況,造成新的accept的連線進不了佇列。

   

    如果上面的圖沒法理解的話,看看這張圖:

     

   5. 解決方法:

    (1) 對慢查詢進行持久化,比如定時存放到mysql之類。(redis的慢查詢只是一個list,超過list設定的最大值,會清除掉之前的資料,也就是看不到歷史)

    (2) 對慢查詢進行報警(頻率、數量、時間)等等因素

    (3) 打屁股,哈哈:

    

     (4) 其實應該做的是:對業務端進行培訓,告訴他們一下redis開發的坑,redis不是萬金油,這個和Mysql DBA要培訓Mysql使用者一樣,否則防不勝防。

      比如他執行了 monitor, keys *, flushall, drop table, update table set a=1; 這種也是防不勝防的(當然也可以做限制,利用rename-command一個隨機數),但是提高工程師的水平才是關鍵。

     

一、背景

1. AOF:

    Redis的AOF機制有點類似於Mysql binlog,是Redis的提供的一種持久化方式(另一種是RDB),它會將所有的寫命令按照一定頻率(no, always, every seconds)寫入到日誌檔案中,當Redis停機重啟後恢復資料庫。

     

2. AOF重寫:

     (1) 隨著AOF檔案越來越大,裡面會有大部分是重複命令或者可以合併的命令(100次incr = set key 100)

     (2) 重寫的好處:減少AOF日誌尺寸,減少記憶體佔用,加快資料庫恢復時間。

    

二、單機多例項可能存在Swap和OOM的隱患:

    由於Redis的單執行緒模型,理論上每個redis例項只會用到一個CPU, 也就是說可以在一臺多核的伺服器上部署多個例項(實際就是這麼做的)。但是Redis的AOF重寫是通過fork出一個Redis程序來實現的,所以有經驗的Redis開發和運維人員會告訴你,在一臺伺服器上要預留一半的記憶體(防止出現AOF重寫集中發生,出現swap和OOM)。

    

三、最佳實踐

1. meta資訊:作為一個redis雲系統,需要記錄各個維度的資料,比如:業務組、機器、例項、應用、負責人多個維度的資料,相信每個Redis的運維人員都應該有這樣的持久化資料(例如Mysql),一般來說還有一些運維介面,為自動化和運維提供依據

    例如如下:

2. AOF的管理方式:

 (1) 自動:讓每個redis決定是否做AOF重寫操作(根據auto-aof-rewrite-percentage和auto-aof-rewrite-min-size兩個引數):

  

 (2) crontab: 定時任務,可能仍然會出現多個redis例項,屬於一種折中方案。

 (3) remote集中式:

       最終目標是一臺機器一個時刻,只有一個redis例項進行AOF重寫。

       具體做法其實很簡單,以機器為單位,輪詢每個機器的例項,如果滿足條件就執行(比如currentSize和baseSize滿足什麼關係)bgrewriteaof命令。

       期間可以監控發生時間、耗時、頻率、尺寸的前後變化            

策略 優點 缺點
自動 無需開發

1. 有可能出現(無法預知)上面提到的Swap和OOM

2. 出了問題,處理起來其實更費時間。

AOF控制中心(remote集中式)

1. 防止上面提到Swap和OOM。

2. 能夠收集更多的資料(aof重寫的發生時間、耗時、頻率、尺寸的前後變化),更加有利於運維和定位問題(是否有些機器的例項需要拆分)。

控制中心需要開發。

一臺機器輪詢執行bgRewriteAof程式碼示例:

Java程式碼  收藏程式碼

  1. package com.sohu.cache.inspect.impl;  
  2. import com.sohu.cache.alert.impl.BaseAlertService;  
  3. import com.sohu.cache.entity.InstanceInfo;  
  4. import com.sohu.cache.inspect.InspectParamEnum;  
  5. import com.sohu.cache.inspect.Inspector;  
  6. import com.sohu.cache.util.IdempotentConfirmer;  
  7. import com.sohu.cache.util.TypeUtil;  
  8. import org.apache.commons.collections.MapUtils;  
  9. import org.apache.commons.lang.StringUtils;  
  10. import redis.clients.jedis.Jedis;  
  11. import java.util.Collections;  
  12. import java.util.LinkedHashMap;  
  13. import java.util.List;  
  14. import java.util.Map;  
  15. import java.util.concurrent.TimeUnit;  
  16. public class RedisIsolationPersistenceInspector extends BaseAlertService implements Inspector {  
  17.     public static final int REDIS_DEFAULT_TIME = 5000;  
  18.     @Override  
  19.     public boolean inspect(Map<InspectParamEnum, Object> paramMap) {  
  20.         // 某臺機器和機器下所有redis例項  
  21.         final String host = MapUtils.getString(paramMap, InspectParamEnum.SPLIT_KEY);  
  22.         List<InstanceInfo> list = (List<InstanceInfo>) paramMap.get(InspectParamEnum.INSTANCE_LIST);  
  23.         // 遍歷所有的redis例項  
  24.         for (InstanceInfo info : list) {  
  25.             final int port = info.getPort();  
  26.             final int type = info.getType();  
  27.             int status = info.getStatus();  
  28.             // 非正常節點  
  29.             if (status != 1) {  
  30.                 continue;  
  31.             }  
  32.             if (TypeUtil.isRedisDataType(type)) {  
  33.                 Jedis jedis = new Jedis(host, port, REDIS_DEFAULT_TIME);  
  34.                 try {  
  35.                     // 從redis info中索取持久化資訊  
  36.                     Map<String, String> persistenceMap = parseMap(jedis);  
  37.                     if (persistenceMap.isEmpty()) {  
  38.                         logger.error("{}:{} get persistenceMap failed", host, port);  
  39.                         continue;  
  40.                     }  
  41.                     // 如果正在進行aof就不做任何操作,理論上要等待它完畢,否則  
  42.                     if (!isAofEnabled(persistenceMap)) {  
  43.                         continue;  
  44.                     }  
  45.                     // 上一次aof重寫後的尺寸和當前aof的尺寸  
  46.                     long aofCurrentSize = MapUtils.getLongValue(persistenceMap, "aof_current_size");  
  47.                     long aofBaseSize = MapUtils.getLongValue(persistenceMap, "aof_base_size");  
  48.                     // 閥值大於60%  
  49.                     long aofThresholdSize = (long) (aofBaseSize * 1.6);  
  50.                     double percentage = getPercentage(aofCurrentSize, aofBaseSize);  
  51.                     // 大於60%且超過60M  
  52.                     if (aofCurrentSize >= aofThresholdSize && aofCurrentSize > (64 * 1024 * 1024)) {  
  53.                         // bgRewriteAof 非同步操作。  
  54.                         boolean isInvoke = invokeBgRewriteAof(jedis);  
  55.                         if (!isInvoke) {  
  56.                             logger.error("{}:{} invokeBgRewriteAof failed", host, port);  
  57.                             continue;  
  58.                         } else {  
  59.                             logger.warn("{}:{} invokeBgRewriteAof started percentage={}", host, port, percentage);  
  60.                         }  
  61.                         // 等待Aof重寫成功(bgRewriteAof是非同步操作)  
  62.                         while (true) {  
  63.                             try {  
  64.                                 // before wait 1s  
  65.                                 TimeUnit.SECONDS.sleep(1);  
  66.                                 Map<String, String> loopMap = parseMap(jedis);  
  67.                                 Integer aofRewriteInProgress = MapUtils.getInteger(loopMap, "aof_rewrite_in_progress", null);  
  68.                                 if (aofRewriteInProgress == null) {  
  69.                                     logger.error("loop watch:{}:{} return failed", host, port);  
  70.                                     break;  
  71.                                 } else if (aofRewriteInProgress <= 0) {  
  72.                                     // bgrewriteaof Done  
  73.                                     logger.warn("{}:{} bgrewriteaof Done lastSize:{}Mb,currentSize:{}Mb", host, port,  
  74.                                             getMb(aofCurrentSize),  
  75.                                             getMb(MapUtils.getLongValue(loopMap, "aof_current_size")));  
  76.                                     break;  
  77.                                 } else {  
  78.                                     // wait 1s  
  79.                                     TimeUnit.SECONDS.sleep(1);  
  80.                                 }  
  81.                             } catch (Exception e) {  
  82.                                 logger.error(e.getMessage(), e);  
  83.                             }  
  84.                         }  
  85.                     } else {  
  86.                         if (percentage > 50D) {  
  87.                             long currentSize = getMb(aofCurrentSize);  
  88.                             logger.info("checked {}:{} aof increase percentage:{}% currentSize:{}Mb", host, port,  
  89.                                     percentage, currentSize > 0 ? currentSize : "<1");  
  90.                         }  
  91.                     }  
  92.                 } finally {  
  93.                     jedis.close();  
  94.                 }  
  95.             }  
  96.         }  
  97.         return true;  
  98.     }  
  99.     private long getMb(long bytes) {  
  100.         return (long) (bytes / 1024 / 1024);  
  101.     }  
  102.     private boolean isAofEnabled(Map<String, String> infoMap) {  
  103.         Integer aofEnabled = MapUtils.getInteger(infoMap, "aof_enabled", null);  
  104.         return aofEnabled != null && aofEnabled == 1;  
  105.     }  
  106.     private double getPercentage(long aofCurrentSize, long aofBaseSize) {  
  107.         if (aofBaseSize == 0) {  
  108.             return 0.0D;  
  109.         }  
  110.         String format = String.format("%.2f", (Double.valueOf(aofCurrentSize - aofBaseSize) * 100 / aofBaseSize));  
  111.         return Double.parseDouble(format);  
  112.     }  
  113.     private Map<String, String> parseMap(final Jedis jedis) {  
  114.         final StringBuilder builder = new StringBuilder();  
  115.         boolean isInfo = new IdempotentConfirmer() {  
  116.             @Override  
  117.             public boolean execute() {  
  118.                 String persistenceInfo = null;  
  119.                 try {  
  120.                     persistenceInfo = jedis.info("Persistence");  
  121.                 } catch (Exception e) {  
  122.                     logger.warn(e.getMessage() + "-{}:{}", jedis.getClient().getHost(), jedis.getClient().getPort(),  
  123.                             e.getMessage());  
  124.                 }  
  125.                 boolean isOk = StringUtils.isNotBlank(persistenceInfo);  
  126.                 if (isOk) {  
  127.                     builder.append(persistenceInfo);  
  128.                 }  
  129.                 return isOk;  
  130.             }  
  131.         }.run();  
  132.         if (!isInfo) {  
  133.             logger.error("{}:{} info Persistence failed", jedis.getClient().getHost(), jedis.getClient().getPort());  
  134.             return Collections.emptyMap();  
  135.         }  
  136.         String persistenceInfo = builder.toString();  
  137.         if (StringUtils.isBlank(persistenceInfo)) {  
  138.             return Collections.emptyMap();  
  139.         }  
  140.         Map<String, String> map = new LinkedHashMap<String, String>();  
  141.         String[] array = persistenceInfo.split("\r\n");  
  142.         for (String line : array) {  
  143.             String[] cells = line.split(":");  
  144.             if (cells.length > 1) {  
  145.                 map.put(cells[0], cells[1]);  
  146.             }  
  147.         }  
  148.         return map;  
  149.     }  
  150.     public boolean invokeBgRewriteAof(final Jedis jedis) {  
  151.         return new IdempotentConfirmer() {  
  152.             @Override  
  153.             public boolean execute() {  
  154.                 try {  
  155.                     String response = jedis.bgrewriteaof();  
  156.                     if (response != null && response.contains("rewriting started")) {  
  157.                         return true;  
  158.                     }  
  159.                 } catch (Exception e) {  
  160.                     String message = e.getMessage();  
  161.                     if (message.contains("rewriting already")) {  
  162.                         return true;  
  163.                     }  
  164.                     logger.error(message, e);  
  165.                 }  
  166.                 return false;  
  167.             }  
  168.         }.run();  
  169.     }  
  170. }  

附圖一張:

 

 一、現象:

    redis-cluster某個分片記憶體飆升,明顯比其他分片高很多,而且持續增長。並且主從的記憶體使用量並不一致。

 

二、分析可能原因:

 1.  redis-cluster的bug (這個應該不存在)

 2. 客戶端的hash(key)有問題,造成分配不均。(redis使用的是crc16, 不會出現這麼不均的情況)

 3. 存在個別大的key-value: 例如一個包含了幾百萬資料set資料結構(這個有可能)

 4. 主從複製出現了問題。

 5. 其他原因

三、調查原因:

 1. 經查詢,上述1-4都不存在

 2. 觀察info資訊,有一點引起了懷疑: client_longes_output_list有些異常。

3. 於是理解想到服務端和客戶端互動時,分別為每個客戶端設定了輸入緩衝區和輸出緩衝區,這部分如果很大的話也會佔用Redis伺服器的記憶體。

 

從上面的client_longest_output_list看,應該是輸出緩衝區佔用記憶體較大,也就是有大量的資料從Redis伺服器向某些客戶端輸出。

於是使用client list命令(類似於mysql processlist) redis-cli -h host -p port client list | grep -v "omem=0",來查詢輸出緩衝區不為0的客戶端連線,於是查詢到禍首monitor,於是豁然開朗.

 

monitor的模型是這樣的,它會將所有在Redis伺服器執行的命令進行輸出,通常來講Redis伺服器的QPS是很高的,也就是如果執行了monitor命令,Redis伺服器在Monitor這個客戶端的輸出緩衝區又會有大量“存貨”,也就佔用了大量Redis記憶體。

 

四、緊急處理和解決方法

進行主從切換(主從記憶體使用量不一致),也就是redis-cluster的fail-over操作,繼續觀察新的Master是否有異常,通過觀察未出現異常。

查詢到真正的原因後,也就是monitor,關閉掉monitor命令的程序後,記憶體很快就降下來了。

五、 預防辦法:

1. 為什麼會有monitor這個命令發生,我想原因有兩個:

(1). 工程師想看看究竟有哪些命令在執行,就用了monitor

(2). 工程師對於redis學習的目的,因為進行了redis的託管,工程師只要會用redis就可以了,但是作為技術人員都有學習的好奇心和慾望。

2. 預防方法:

(1) 對工程師培訓,講一講redis使用過程中的坑和禁忌

(2) 對redis雲進行介紹,甚至可以讓有興趣的同學參與進來

(3) 針對client做限制,但是官方也不建議這麼做,官方的預設配置中對於輸出緩衝區沒有限制。

Java程式碼  收藏程式碼

  1. client-output-buffer-limit normal 0 0 0  

(4) 密碼:redis的密碼功能較弱,同時多了一次IO

(5) 修改客戶端原始碼,禁止掉一些危險的命令(shutdown, flushall, monitor, keys *),當然還是可以通過redis-cli來完成

(6) 新增command-rename配置,將一些危險的命令(flushall, monitor, keys * , flushdb)做rename,如果有需要的話,找到redis的運維人員處理

Java程式碼  收藏程式碼

  1. rename-command FLUSHALL "隨機數"  
  2. rename-command FLUSHDB "隨機數"  
  3. rename-command KEYS "隨機數"  

六、模擬實驗:

1.  開啟一個空的Redis(最簡,直接redis-server)

Java程式碼  收藏程式碼

  1. redis-server  

    初始化記憶體使用量如下:

Java程式碼  收藏程式碼

  1. # Memory  
  2. used_memory:815072  
  3. used_memory_human:795.97K  
  4. used_memory_rss:7946240  
  5. used_memory_peak:815912  
  6. used_memory_peak_human:796.79K  
  7. used_memory_lua:36864  
  8. mem_fragmentation_ratio:9.75  
  9. mem_allocator:jemalloc-3.6.0  

    client緩衝區:

Java程式碼  收藏程式碼

  1. # Clients  
  2. connected_clients:1  
  3. client_longest_output_list:0  
  4. client_biggest_input_buf:0  
  5. blocked_clients:0  

2. 開啟一個monitor:

Java程式碼  收藏程式碼

  1. redis-cli -h 127.0.0.1 -p 6379 monitor  

3. 使用redis-benchmark:

Java程式碼  收藏程式碼

  1. redis-benchmark -h 127.0.0.1 -p 6379 -c 500 -n 200000  

4. 觀察

(1) info memory:記憶體一直增加,直到benchmark結束,monitor輸出完畢,但是used_memory_peak_human(歷史峰值)依然很高--觀察附件中日誌

(2)info clients: client_longest_output_list: 一直在增加,直到benchmark結束,monitor輸出完畢,才變為0--觀察附件中日誌

(3)redis-cli -h host -p port client list | grep "monitor" omem一直很高,直到benchmark結束,monitor輸出完畢,才變為0--觀察附件中日誌

監控指令碼:

Java程式碼  收藏程式碼

  1. while [ 1 == 1 ]  
  2. do  
  3. now=$(date "+%Y-%m-%d_%H:%M:%S")  
  4. echo "=========================${now}==============================="  
  5. echo " #Client-Monitor"  
  6. redis-cli -h 127.0.0.1 -p 6379 client list | grep monitor  
  7. redis-cli -h 127.0.0.1 -p 6379 info clients  
  8. redis-cli -h 127.0.0.1 -p 6379 info memory  
  9. #休息100毫秒  
  10. usleep 100000  
  11. done  

 完整的日誌檔案:

一、背景: 選擇合適的使用場景

   很多時候Redis被誤解並亂用了,造成的Redis印象:耗記憶體、價格成本很高:

   1. 為了“趕時髦”或者對於Mysql的“誤解”在一個併發量很低的系統使用Redis,將原來放在Mysql資料全部放在Redis中。

     ----(Redis比較適用於高併發系統,如果是一些複雜Mis系統,用Redis反而麻煩,因為單從功能講Mysql要更為強大,而且Mysql的效能其實已經足夠了。)

   2. 覺得Redis就是個KV快取

     -----(Redis支援多資料結構,並且具有很多其他豐富的功能)

   3. 喜歡做各種對比,比如Mysql, Hbase, Redis等等

    -----(每種資料庫都有自己的使用場景,比如Hbase吧,我們系統的個性化資料有1T,此時放在Redis根本就不合適,而是將一些熱點資料放在Redis)

    總之就是在合適的場景,選擇合適的資料庫產品。

  附贈兩個名言:

Evan Weaver, Twitter, March 2009 寫道

Everything runs from memory in Web 2.0!

Tim Gray 寫道

Tape is Dead, Disk is Tape, Flash is Disk, RAM Locality is king. (磁帶已死,磁碟是新磁帶,快閃記憶體是新磁碟,隨機儲存器區域性性是為王道)

二、一次string轉化為hash的優化

1. 場景:

    使用者id: userId,

    使用者微博數量:weiboCount    

userId(使用者id) weiboCount(微博數)
1 2000
2

10

3

288

.... ...
1000000 1000

2. 實現方法:

(1) 使用Redis字串資料結構, userId為key, weiboCount作為Value

(2) 使用Redis雜湊結構,hashkey只有一個, key="allUserWeiboCount",field=userId,fieldValue= weiboCount

(3) 使用Redis雜湊結構,  hashkey為多個, key=userId/100, field=userId%100, fieldValue= weiboCount

前兩種比較容易理解,第三種方案解釋一下:每個hashKey存放100個hash-kv,field=userId%100,也就是

userId hashKey field
1 0 1
2 0

2

3 0

3

... .... ...
99 0 99
100 1 0
101 1 1
.... ... ...
9999 99 99
100000 1000 0

3. 獲取方法:

Java程式碼  收藏程式碼

  1. #獲取userId=5003使用者的微博數  
  2. (1) get 5003  
  3. (2) hget allUserWeiboCount 5003  
  4. (3) hget 50 3  

4. 記憶體佔用量對比(100萬用戶 userId:1~1000000) 

Java程式碼  收藏程式碼

  1. #方法一 Memory  
  2. used_memory:85999592  
  3. used_memory_human:82.02M  
  4. used_memory_rss:96043008  
  5. used_memory_peak:85999592  
  6. used_memory_peak_human:82.02M  
  7. used_memory_lua:36864  
  8. mem_fragmentation_ratio:1.12  
  9. mem_allocator:jemalloc-3.6.0  
  10. #方法二 Memory  
  11. used_memory:101665632  
  12. used_memory_human:96.96M  
  13. used_memory_rss:110702592  
  14. used_memory_peak:101665632  
  15. used_memory_peak_human:96.96M  
  16. used_memory_lua:36864  
  17. mem_fragmentation_ratio:1.09  
  18. mem_allocator:jemalloc-3.6.0  
  19. #方法三 Memory  
  20. used_memory:9574136  
  21. used_memory_human:9.13M  
  22. used_memory_rss:17285120  
  23. used_memory_peak:101665632  
  24. used_memory_peak_human:96.96M  
  25. used_memory_lua:36864  
  26. mem_fragmentation_ratio:1.81  
  27. mem_allocator:jemalloc-3.6.0  

  記憶體使用量:

  

5. 匯入資料程式碼(不考慮程式碼優雅性,單純為了測試,勿噴)

Java程式碼  收藏程式碼

  1. package com.carlosfu.redis;  
  2. import java.util.ArrayList;  
  3. import java.util.HashMap;  
  4. import java.util.List;  
  5. import java.util.Map;  
  6. import java.util.Random;  
  7. import org.junit.Test;  
  8. import redis.clients.jedis.Jedis;  
  9. /** 
  10.  * 一次string-hash優化 
  11.  * @author carlosfu 
  12.  * @Date 2015-11-8 
  13.  * @Time 下午7:27:45 
  14.  */  
  15. public class TestRedisMemoryOptimize {  
  16.     private final static int TOTAL_USER_COUNT = 1000000;  
  17.     /** 
  18.      * 純字串 
  19.      */  
  20.     @Test  
  21.     public void testString() {  
  22.         Jedis jedis = null;  
  23.         try {  
  24.             jedis = new Jedis("127.0.0.1", 6379);  
  25.             List<String> kvsList = new ArrayList<String>(200);  
  26.             for (int i = 1; i <= TOTAL_USER_COUNT; i++) {  
  27.                 String userId = String.valueOf(i);  
  28.                 kvsList.add(userId);  
  29.                 String weiboCount = String.valueOf(new Random().nextInt(100000));  
  30.                 kvsList.add(weiboCount);  
  31.                 if (i % 2000 == 0) {  
  32.                     System.out.println(i);  
  33.                     jedis.mset(kvsList.toArray(new String[kvsList.size()]));  
  34.                     kvsList = new ArrayList<String>(200);  
  35.                 }  
  36.             }  
  37.         } catch (Exception e) {  
  38.             e.printStackTrace();  
  39.         } finally {  
  40.             if (jedis != null) {  
  41.                 jedis.close();  
  42.             }  
  43.         }  
  44.     }  
  45.     /** 
  46.      * 純hash 
  47.      */  
  48.     @Test  
  49.     public void testHash() {  
  50.         String hashKey = "allUserWeiboCount";  
  51.         Jedis jedis = null;  
  52.         try {  
  53.             jedis = new Jedis("127.0.0.1", 6379);  
  54.             Map<String,String> kvMap = new HashMap<String, String>();  
  55.             for (int i = 1; i <= TOTAL_USER_COUNT; i++) {  
  56.                 String userId = String.valueOf(i);  
  57.                 String weiboCount = String.valueOf(new Random().nextInt(100000));  
  58.                 kvMap.put(userId, weiboCount);  
  59.                 if (i % 2000 == 0) {  
  60.                     System.out.println(i);  
  61.                     jedis.hmset(hashKey, kvMap);  
  62.                     kvMap = new HashMap<String, String>();  
  63.                 }  
  64.             }  
  65.         } catch (Exception e) {  
  66.             e.printStackTrace();  
  67.         } finally {  
  68.             if (jedis != null) {  
  69.                 jedis.close();  
  70.             }  
  71.         }  
  72.     }  
  73.     /** 
  74.      * segment hash 
  75.      */  
  76.     @Test  
  77.     public void testSegmentHash() {  
  78.         int segment = 100;  
  79.         Jedis jedis = null;  
  80.         try {  
  81.             jedis = new Jedis("127.0.0.1", 6379);  
  82.             Map<String,String> kvMap = new HashMap<String, String>();  
  83.             for (int i = 1; i <= TOTAL_USER_COUNT; i++) {  
  84.                 String userId = String.valueOf(i % segment);  
  85.                 String weiboCount = String.valueOf(new Random().nextInt(100000));  
  86.                 kvMap.put(userId, weiboCount);  
  87.                 if (i % segment == 0) {  
  88.                     System.out.println(i);  
  89.                     int hash = (i-1) / segment;  
  90.                     jedis.hmset(String.valueOf(hash), kvMap);  
  91.                     kvMap = new HashMap<String, String>();  
  92.                 }  
  93.             }  
  94.         } catch (Exception e) {  
  95.             e.printStackTrace();  
  96.         } finally {  
  97.             if (jedis != null) {  
  98.                 jedis.close();  
  99.             }  
  100.         }  
  101.     }  
  102. }  

三、結果對比

 redis核心物件 資料型別 + 編碼方式 + ptr  分段hash也不會造成drift

方案 優點 缺點
string

直觀、容易理解

  1. 記憶體佔用較大
  2. key值分散、不變於計算整體
hash

直觀、容易理解、整合整體

  1. 記憶體佔用大
  2. 一個key佔用過大記憶體,如果是redis-cluster會出 現data drift
segment-hash

記憶體佔用量小,雖然理解不夠直觀,但是總體上是最優的。

理解不夠直觀。

四、結論:

   在使用Redis時,要選擇合理的資料結構解決實際問題,那樣既可以提高效率又可以節省記憶體。所以此次優化方案三為最佳。

附圖一張:redis其實是一把瑞士軍刀:

由於演講時間有限,有關Redis-Cluster,演講者沒做太多介紹,簡單的介紹了一些Redis-Cluster概念作用和遇到的兩個問題,我們在Redis-Cluster也有很多運維經驗,將來的文章會介紹。

但是講演者反覆強調,不要聽信網上對於Redis-Cluster的毀謗(實踐出真知),對於這一點我很贊同,我們從Redis-Cluster beta版 RC1~4 到現在的3.0-release均沒有遇到什麼大問題(線上維護600個例項)。

一、Redis-Cluster

有關Redis-Cluster的詳細介紹有很多這裡就不多說了,可以參考:

4. Redis設計與實現那本書(作者:黃建巨集):非常的推薦看這本書。

總之Redis-Cluster是一個無中心的分散式Redis儲存架構,解決了Redis高可用、可擴充套件等問題。

 

二、兩個問題:

1. Redis-Cluster主從節點不要在同一個機器部署

   (1) 以我們的經驗看redis例項本身基本不會掛掉,通常是機器出了問題(斷電、機器故障)、甚至是機架、機櫃出了問題,造成Redis掛掉。

   (2) 如果Redis-Cluster的主從都在一個機器上,那麼如果這臺機器掛了,主從全部掛掉,高可用就無法實現。(如果full converage=true,也就意味著整個叢集掛掉)

   (3) 通常來講一對主從所在機器:不跨機房、要跨機架、可以在一個機櫃。

2. Redis-Cluster誤判節點fail進行切換

   (1) Redis-Cluster是無中心的架構,判斷節點失敗是通過仲裁的方式來進行(gossip和raft),也就是大部分節點認為一個節點掛掉了,就會做fail判定。

   (2) 如果某個節點在執行比較重的操作(flushall, slaveof等等)(可能短時間redis客戶端連線會阻塞(redis單執行緒))或者由於網路原因,造成其他節點認為它掛掉了,會做fail判定。

   (3) Redis-Cluster提供了cluster-node-timeout這個引數(預設15秒),作為fail依據(如果超過15秒還是沒反應,就認為是掛掉了),具體可以參考這篇文章:Redis-Cluster的FailOver失敗案例分析

        以我們的經驗看15秒完全夠用。

   

三、未來要介紹的問題:

1. Redis-Cluster客戶端實現Mget操作。

3. Redis-Cluster無底洞問題解析。

4. 兩個Redis-Cluster叢集,meet操作問題後的惡果。

5. Redis-Cluster配置之full converage問題。

7. Redis-Cluster常用運維技巧。

8. Redis-Cluster一鍵開通。

9. Redis-Cluster客戶端jedis詳解。

四、附贈一些不錯的資料: