目錄
七、Redis的瑞士軍刀
1 . 慢查詢
首先上一張redis查詢時的生命週期圖
在這裏我們可以看到總共有4個流程,每一個流程都可能造成客戶端超時,而慢查詢是在執行命令時(第三階段)造成的。
現在有兩個問題: 1. 什麼命令屬於慢查詢呢? 2. 判定爲慢查詢的命令redis怎麼處理?
對於第一個問題,redis中有個配置叫做 slowlog-log-slower-than ,它的單位是微秒,默認制定了10000微秒,意思是超過10ms的查詢會被當做慢查詢處理,當指定爲0時所有的都記錄,<0 時什麼都不記錄。
第二個問題,redis處理慢查詢時會把判定爲慢查詢的命令扔進一個先進先出的隊列,這個隊列是有固定長度的,還有直接保存在內存中的,我們可以指定隊列的長度,當隊列滿的時候會把隊列第一個彈出,這個隊列默認長度爲128。通過slowlog-max-len 來指定隊列長度。
查看慢查詢的API:
1 . slowlog get N : 得到慢查詢隊列中的N條命令,因爲慢查詢是用先進先出的隊列保存的,所以在get的時候得到的是最後進入隊列的N條慢查詢。
2 . slowlog len : 得到慢查詢隊列的當前長度。
3 . slowlog reset : 重置慢查詢隊列,相當於清空隊列,此時 slowlog len 爲0
下面有兩種配置這個兩個配置量的方法:
1 . 修改啓動的配置文件 slow-max-len 1000;
2 . 因爲這兩個配置支持熱配置,所以可以用config set slow-max-len 1000 的命令來指定最大隊列長度爲1000。
下面記錄了幾條建議:
2 . pipeline(管道)
根據redis的生命週期,一個命令從客戶端發出到收到結果需要一次網絡時間(發送命令和得到結果)和一次執行時間。
現在我們想象一個使用情景,一個客戶端發送了10K個 get 命令,那現在所需要的時間就是 10K 次網絡時間+10K次執行時間,我們知道一個get命令在不阻塞的情況下執行時間是微秒級別的,而客戶端請求服務器的網絡時間因爲地域,網絡質量等原因算作毫秒級別,這樣我們這個10K個命令都花在了網絡時間上,這時就在考慮能不能把多個命令組合起來一起發送,結果一起返回,看起來就像一條命令一樣呢?
所以這個pipeline的作用就是這樣。他可以一次性把多個命令一次性發送到redis-server執行,然後按命令順序返回結果。對的,這個和redis 提供的mset ,mget有點相似,都是操作多個key(對mset,mget操作不熟悉的,可以看我的redis學習日記(一))那他們有什麼區別呢?
mset,mget和pipeline的區別:1.mget,mset都是原子操作,屬於redis原生命令,不可拆分,如圖左;pipeline相當於把多個命令組合發送到服務器,到達服務器執行時這些命令會拆分成原來的子命令順序執行,但是這個順序執行中可以會插入其他來源的命令,如圖右。
pipeline使用注意:①:pipeline使用時要注意攜帶的數據量,當太大時也會造成網絡堵塞,所以要攜帶的數據量太大時要注意對數據量的拆分。②:pipeline每次只能作用於一個redis節點(不支持集羣)。
下面給出pipeline的使用方法和mset,set,pipeline處理10000條命令的效率:
@Test
public void test4 () {
Jedis jedis = JedisUtil.getJedis();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
jedis.hset("myhash1", "field" + i, "value" + i);
}
System.out.println("hash set 10000個字段花費時間 :" + (System.currentTimeMillis() - start));
jedis.close();
}
@Test
public void test5 () {
Jedis jedis = JedisUtil.getJedis();
HashMap<String, String> map = new HashMap<>();
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
map.put("field" + i, "value" + i);
}
jedis.hmset("myhash2", map);
System.out.println("hash hmset 10000個字段花費時間 :" + (System.currentTimeMillis() - start));
jedis.close();
}
@Test
public void test6 () {
Jedis jedis = JedisUtil.getJedis();
long start = System.currentTimeMillis();
Pipeline pipeline = jedis.pipelined();
for (int j =0; j < 10000; j++) {
pipeline.hset("myhash3", "field" + j, "value" + j);
}
pipeline.syncAndReturnAll();
System.out.println("hash pipeline hset 10000個字段花費時間 :" + (System.currentTimeMillis() - start));
jedis.close();
}
通過結果可以明顯看出在處理10000條數據時,hset比hmset和pipeline慢了非常多,我這裏是本地連接的阿里雲的redis,所以在處理成片的命令時網絡時間會佔的特別多,在這種情況下使用pipeline會顯著的提高效率。在可以使用m操作的命令時也可以使用mset,不能使用時可以使用pipeline,至於這兩者的區別和pipeline注意事項在上文都已經提到了。
3 . 發佈訂閱
a、角色
① : 訂閱者 =====> 訂閱科技頻道的人,如自己
② : 頻道 ======> 我收看的頻道,如科技頻道,電影頻道,音樂頻道等
③ : 發佈者 =====> 發佈信息到頻道的人,如搜狐科技發佈科技信息到科技頻道,電影發佈網發佈電影信息到電影頻道
b、模型
我們可以看到,訂閱者和發佈者都是客戶端,服務端相當於頻道,這裏server可以有多個頻道,訂閱者也可以訂閱多個頻道,一個新的訂閱者不能收到訂閱之前發佈者發佈在這個頻道上的消息。
c、API
publish channel message : 向channel 中發佈message信息 ,例如 publish douban loveMovie1
subscribe channel :訂閱channel
unsubcribe channel :取消訂閱channel
psubscribe [pattern] :訂閱匹配到pattern的頻道,如psubscribe * ,則訂閱所有頻道;
pubsub channel : 列出至少有一個訂閱者的頻道。
pubsub numsub channel :列出給定頻道的訂閱者數量。
d、訂閱模式和消息隊列
發佈訂閱模式是所有訂閱者都能收到頻道中的內容(例如訂閱科技新聞)。
消息隊列是訂閱者中搶頻道中的內容,並且只能一個訂閱者能得到(例如搶紅包),消息隊列可用list,lpush,rpop實現。
4 . bitmap
a、這是什麼?
位圖:是按位存取的數據結構。(如下圖所示就是big 按照位圖的存儲方式)
因爲ASCII碼用一個字節表示,所以這裏用8位來表示一個字符。因爲bitmap不是真正的數據類型,定義成的還是字符串,而一個字符串所能存儲的最大是512M,換算成位,就是 2^32bit,也就是最多有2^32個單元格。
這裏要注意一點,在初始的時候位圖是沒有賦值的,如當前索引在10,這時要給1000W處的索引出賦值,這時redis會把10~1000W之間都用0賦上,這個是非常耗時的,追其原因是因爲偏移量一次性移動長度太大,所以這時就得避免在一個小的位圖上突然給一個超級大的偏移量,這樣一次偏移量大的操作可以分成多次操作完成。
b、怎麼用?
1. setbit key offset value : 設置key的offset處爲value。如setbit mybit 10 1,即把mybit 第10個位置設置爲1,這個偏移量是從0開始的。
2. getbit keyoffset : 獲得指定偏移量的值
3. bitcount key [start, end] : start ,end 可選擇添加,不填則默認key的全部長度,獲取長度範圍內的1的數量
4. bitop [and, or, not, xor] destkey key1 key2 ... : and是交集,or是並集,not是非集,xor是異或,這裏有4種操作,將key1,key2 的操作結果保存到destkey中。
5. bitpos key targetBit [start, end] : 結果是在範圍內第一個值等於targetBit的索引值。
c、用哪裏?
1. 使用bitmap做獨立用戶統計,每個用戶id就是bitmap的索引值,可以用1表示訪問,0代表不訪問。最適合百萬級別的用戶訪問統計。
2. 給用戶做留存率,記錄註冊後留存情況。一個key是一個用戶,如索引1 代表第一天的留存情況,所以60代表註冊後第60天是否還留存。
5 . hyperloglog
這個數據結構使用極小的內存來保存獨立用戶的數量,本質數據結構還是string,其實使用了特別的算法來實現。
它的三個命令:
1. pfadd key value1 value2 value3... : 添加value值
2. pfcount key : 這個key 中擁有的key的數量
3. pfmerge destkey key1 key2 ... :將這些key合併成到一個destkey中
使用hyperloglog保存這些數據的時候內存極小,但是也有他的侷限性。
1. 在得到總數時會有錯誤(錯誤率0.81%),要權衡是否能容忍這個錯誤
2. 不能得到單條數據,因爲這個數據結構的內存極小,所以得到單條數據也是非常難的。
八、Redis的持久化
1 、持久化
a、什麼是持久化
持久化是把數據保存到硬盤的過程。因爲redis是保存在內存中的,電腦重啓內存就空了,不做持久化的redis數據也就清空了。
b、持久化的方式
2 、RDB模式
a、什麼是RDB
快照的實現方式,將redis此時此刻的狀態和數據等信息保存成一個rdb文件(二進制),這個rdb文件保存在硬盤中,就是一個持久化文件,當想恢復數據時,加載這個rdb文件即可。這個rdb文件可以當做複製的媒介,在主從複製時就可以通過這個rdb文件實現。
b、觸發機制-主要三種
① : save 命令 (同步):使用save命令即可,redis會將數據同步阻塞式地將數據保存爲一個rdb文件,它的阻塞是阻塞客戶端的命令。
② : bgsave (異步):使用bgsave命令即可,redis會將數據異步阻塞式地將數據保存爲一個rdb文件,這裏的fork方法調用的是操作系統的方法,複製一個新的進程完成這個save操作,這裏的fork函數會導致進程阻塞,但是相對於save的阻塞,這裏的fork會快的多。
③ : 自動方式:save 900 1 指的是900秒鐘內有一條更新操作就做bgsave保存rdb文件。
dbfilename 指的是保存rdb文件的名稱
dir 指的是保存的路徑
stop-writes-on-bgsave-error 指的是是否在bgsave失敗時停止寫入redis的操作
rdbcompression 是否對rdb文件進行壓縮
上述這裏三種的文件策略都是更新覆蓋,rdb文件只保存一個,當存在rdb文件時覆蓋原來的文件。
c、觸發機制-不容忽略方式(以下方式會自動生成rdb文件)
1 . 全量複製 (主從全量複製時會生成rdb文件)
2 . debug reload (debug級別的重啓,不清空數據的重啓)
3 . shutdown 操作
4 . flushall 命令
d、RDB總結
1 . RDB是以快照的方式把redis的數據持久化到硬盤的。
2 . save通常會阻塞redis。
3 . bgsave持久化時不會阻塞redis,但是在fork的時候會阻塞,但這個fork時間相對來說快的多。
4 . 自動化持久化會在滿足任一save參數時觸發。
3 、AOF模式
a、RDB模式的缺陷
① RDB模式耗時,耗性能,每次要全量備份非常耗時,而且這樣的寫操作消耗cpu,並且加劇I/O損耗。
② 第二RDB模式會造成數據的丟失,因爲RDB的模式決定了它不能經常持久化,這樣會造成大量的阻塞時間,在這個條件下的RDB模式如果在兩次備份期間redis服務器因爲外部原因宕機的話就就會造成數據的丟失,持久化的數據就是上次備份的數據,還沒來的及持久化的數據丟失,因爲兩次持久化的間隔可能時間較長,因此這個數據的丟失影響較大。
b、什麼是AOF
AOF就是持久化的另一種方式---日誌化管理。將redis執行的命令都以日誌的方式寫入文件,在恢復時就相當於重新執行一遍這些命令。
c、AOF的三種策略模式
上面講到AOF是將命令以日誌的形式寫入文件,這裏寫入時並不是絕對的一句一句的寫入,這裏就有3種寫入的策略。
redis在執行寫命令時並不是馬上寫入日誌中,而是先寫入緩衝區,再根據緩衝區的策略寫入到日誌中。這裏說的三種策略就是從緩衝區到硬盤的寫入。
① ererysec : 每秒把緩衝區的內容寫入AOF文件中,也就是說最多丟失1秒的數據(默認使用此策略)。
② always : always的策略是每條命令都寫入到AOF文件中,redis的寫入不會丟失,但是 I/O 開銷很大。
③ no : 讓操作系統來決定寫入AOF文件的時機,這樣我們就不用去管理這個時機,但也因爲這樣我們對AOF寫入變的不可控,不推薦。
d、AOF重寫
AOF存在的問題 : 當時間久了,AOF的文件會不斷變大,恢復redis的時間也就會變的更長。
那現在AOF重寫就是來解決這個問題的,他是怎麼解決的呢?簡單來說就是優化AOF中存儲的命令。舉例來說,自增1 的操作有100W次,那AOF重寫就可以變成自增100W,而不是執行100W此操作。
AOF重寫的作用是讓硬盤的佔有量變的更小,並且加速redis恢復時的速度。
實現重寫的兩種方式:
① bgrewriteaof : 和bgsave的方式類似,也是複製一個新的進程來做重寫任務,(注意!)這裏的AOF重寫並不是和上面的舉例一樣把幾句命令合併成一句或者抽象成一些命令,而是回溯redis的命令,分別用不同的命令來寫入key來達到和現在redis相同的redis,具體來說用set寫入字符串鍵值,用hmset 寫入hash鍵值,用lpush 寫入list結構的鍵值,用sadd 寫入set結構的鍵值,zadd等命令。這樣就不用管redis之前做的刪除或者修改操作,只要用新增命令來達到和現在redis一樣結構的就行了。
② 重寫自動配置: auto-aof-rewrite-min-size -----> AOF 文件重寫需要的尺寸
auto-aof-rewrite-percentage -----> AOF增長率,下一次重寫的尺寸是 增長率 * 現在的尺寸
也就是同時滿足這兩個條件的時候纔會自動重寫:現在的AOF大小(A),上一次重寫時的大小(B), AOF重寫所需要的最小的尺寸(C),AOF 增長率爲(D),那麼 A > C && (A - B)/ B > D 時自動重寫。
問題: 既然AOF備份是fork子進程來完成生成新的AOF文件,那麼子線程備份時主線程進行寫入,這時redis 怎麼處理這部分命令數據的呢?
這裏就牽扯到寫時複製技術了(Copy-On-Write):寫時複製是指當一個文件(A)在被其他多個進程讀取時,A有新的內容寫入,這時並不直接寫入A,而是複製一個A的備份B,將新的內容寫入B,當這些寫操作處理完的時候再講之前對A的引用指向B,完成。
應用在redis AOF流程就是
1 . AOF開始備份(redis狀態1,進程A);
2 . redis fork出一個新的進程(B)開始AOF備份;
3 . B生成新的AOF文件覆蓋原先的AOF文件。
注意: 如果在步驟2 的時候進程A又處理了新的數據(狀態2),這時狀態2和狀態1的數據就不同了,進程A將這部分新增的數據存入內存,我們可以稱爲新增數據緩存區,當進程B備份數據到狀態1的時候結束,在結束前再把這個緩衝區的數據寫入AOF文件中達到和現在的狀態一致的效果(寫時複製)。自己畫了一個流程圖,有不對的歡迎指正。
這裏還有個自動化AOF備份所需的配置: no-appendfsync-on-rewrite yes : 這個配置的意思是 是否在AOF重寫的時候將新的寫入命令寫入AOF文件中,正常備份的時候不存在問題,但是如果AOF備份失敗了就會導致這部分的數據丟失,因爲AOF重寫比較耗性能,所以在性能和數據準確性方面一般傾向於性能,選擇在AOF重寫的時候不備份。配置可以參考下面。
4 、RDB和AOF的抉擇
a、RDB和AOF的區別
b、最佳實踐
RDB+AOF
RDB每週定時全量備份 + AOF的自動增量備份。
關閉RDB的自動備份,定時集中化管理備份。開啓AOF,設置爲everysec策略。redis單點多臺時小分片,限制每個redis的最大存儲量。
5 、持久化中的問題
在持久化中無論是 bgsave 還是 bgrewriteaof 都會進行fork操作來創造子進程來完成任務,這時如果單機多部署的形式存在多個redis同時進行AOF重寫,我們這時就得考慮這些子進程的開銷和優化方式了。
6、數據恢復
情景1 : RDB 方式和 AOF 方式同時開啓,這時默認恢復是按照AOF文件來恢復的,如果這時 AOF 文件內容爲空,存在全部內容的RDB 備份文件,這樣恢復的時候redis也是沒數據的,開啓AOF就不會再用 RDB 文件恢復。
情景2 : 僅存在RDB方式時使用RDB文件來恢復。
注意:開啓 AOF 日誌時把配置文件中的 appendonly 設置爲 yes, 如果想把用 RDB 文件恢復先把 appendonly 項設置爲no,這時正常的恢復就會使用 RDB 文件。
那麼如何恢復數據呢?
第一步:查看appendonly 是否爲yes,yes則會使用 AOF 文件恢復,no使用 RDB 文件恢復。
第二部:查看 RDB 文件或者 AOF 文件存儲的路徑 ----》 進入客戶端界面使用 config get dir 命令查看 dir 路徑,這個路徑就是redis保存 RDB 文件和 AOF 文件的路徑。
第三部 : 進入上述的路徑,啓動redis服務,如 redis-server config/6380.conf 。 這裏就會自動按照 AOF 文件或者 RDB 文件恢復。