1. Redis為什么這么快?
完全基于內存,絕大部分請求是純粹的內存操作,執(zhí)行效率非常高。數(shù)據(jù)存在內存中,類似于HashMap,HashMap的優(yōu)勢就是查找和操作的時間復雜度都是O(1);
數(shù)據(jù)結構簡單,對數(shù)據(jù)操作也簡單,Redis中的數(shù)據(jù)結構是專門進行設計的;
采用單線程(該單線程指的是處理網絡請求的線程),避免了不必要的上下文切換和競爭條件,也不存在多進程或者多線程導致的切換而消耗 CPU,不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有因為可能出現(xiàn)死鎖而導致的性能消耗;
使用多路I/O復用模型,非阻塞IO;
2. Redis中常用數(shù)據(jù)類型
3. 從海量Key里查詢出某一固定前綴的Key
留意細節(jié):
- 摸清數(shù)據(jù)規(guī)模,即問清楚邊界;
使用keys指令來掃出指定模式的key列表,
使用keys對線上的業(yè)務的影響:
KEYS pattern:查找所有符合給定模式pattern的key
缺點:
KEYS指令一次性返回所有匹配的key;
鍵的數(shù)量過大會使得服務卡頓;
這時可以使用SCAN指令:
SCAN cursor [MATCH pattern] [COUNT count]
基于游標的迭代器,需要基于上一次的游標延續(xù)之前的迭代過程;
以0作為游標開始一次新的迭代,直到命令返回游標0完成一次遍歷;
不保證每次執(zhí)行都返回某個給定數(shù)量的元素,支持模糊查詢;
對于增量式迭代命令,一次返回的數(shù)量不可控,只能是大概率符合count參數(shù);
>scan 0 match k1* count 10
4. 如何通過Redis實現(xiàn)分布式鎖
分布式鎖需要解決的問題:
互斥性
安全性
死鎖:一個持有鎖的客戶端宕機而導致其他客戶端再也無法獲得鎖,從而導致的死鎖;
容錯
如何實現(xiàn):
SETNX(Set if not exsist) key value:如果key不存在,則創(chuàng)建并賦值。因為SETNX有上述功能,并且操作都是原子的,因此在初期的時候可以用來實現(xiàn)分布式鎖。
時間復雜度:O(1)
返回值:設置成功,返回1,表明此時沒有其他線程占用該資源;設置失敗,返回0,表示此時有別的線程正在占用該資源。
使用EXPIRE key seconds來解決SETNX長期有效的問題:
- 設置key的生存時間,當key過期時(生存時間為0),會被自動刪除;
RedisService redisService = SpringUtils.getBean(RedisService.class);
long status = redisService.setnx(key,"1");
if(status == 1){
redisService.expire(key,expire);
//執(zhí)行獨占資源邏輯
doOcuppiedWork();
}
- 以上方法的缺點:原子性無法得到滿足
從Redis 2.1.6 以后,原子操作set:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:設置鍵的過期時間為second(秒)
PX milliseconds:設置鍵的過期時間為millisecond(毫秒)
NX:只在鍵不存在的時候,才對鍵進行設置操作,效果等同于setnx
XX:只在鍵已存在的時候,才對鍵進行設置操作
SET操作成功完成時,返回OK,否則則返回nil
> set lock 123 ex 10 nx
OK
> set lock 122 ex 10 nx
(nil)
代碼實現(xiàn)例如:
RedisService redisService = SpringUtils.getBean(RedisService.class);
String result = redisService.set(lockKey,requestId,SET_IF_NOT_EXIST,
SET_WITH_WITH_EXPIRE_TIME,expireTime);
if("OK".equals(result)){
//執(zhí)行獨占資源邏輯
doOcuppiedWork();
}
大量key同時過期的注意事項:
集中過期,由于清楚大量key很耗時,會出現(xiàn)短暫的卡頓現(xiàn)象。
解決方法:在設置key的過期時間的時候,給每個key加上一個隨機值。
5. 如何使用Redis做異步隊列
使用List作為隊列,RPUSH生產消息,LPOP消費消息。
缺點:沒有等待隊列中有值就直接消費;
彌補:可以在應用層引入Sleep機制去調用LPOP重試
BLPOP key [key ...] timeout:阻塞直到隊列有消息就能夠返回或超時
- 缺點:只能供一個消費者消費
pub/sub:主題發(fā)布-訂閱模式
發(fā)送者(publish)發(fā)送消息,訂閱者(subscribe)接收消息;
訂閱者可以訂閱任意數(shù)量的頻道(Topic);
?

?
- 缺點:消息的發(fā)布是無狀態(tài)的,無法保證可達性,即發(fā)送完該消息無法保證該消息被接收到。若想解決該問題需要使用專業(yè)的消息隊列,例如Kafka等。
6. Redis如何做持久化
Redis有三種持久化的方式:
- RDB(快照)持久化:保存某個時間點的全量數(shù)據(jù)快照;
缺點:
內存數(shù)據(jù)的全量同步,數(shù)據(jù)量大會由于I/O而嚴重影響性能;
可能會因為Redis掛掉而丟失從當前至最近一次快照期間的數(shù)據(jù);
redis.conf文件中:
save 900 1 #900秒之內如果有1條寫入指令就觸發(fā)一次快照
save 300 10
save 60 10000
?
stop-writes-on-bgsave-error yes #表示備份進程出錯的時候,主進程就停止接收新的寫入操作,是為了保護持久化數(shù)據(jù)的一致性
?
rdbcompression no #RDB的壓縮設置為no,因為壓縮會占用更多的CPU資源
手動觸發(fā):
SAVE:阻塞Redis的服務器進程,知道RDB文件被創(chuàng)建完畢,很少被使用;
BGSAVE:Fork出一個子進程來創(chuàng)建RDB文件,不會阻塞服務器主進程。
自動觸發(fā):
根據(jù)redis.conf配置里的Save m n 定時觸發(fā)(用的是BGSAVE)
主從復制,主節(jié)點自動觸發(fā)。從節(jié)點全量復制時,主節(jié)點發(fā)送RDB文件給從節(jié)點完成復制操作,主節(jié)點這時候會觸發(fā)BGSAVE;
執(zhí)行Debug Reload;
執(zhí)行Shutdown且沒有開啟AOF持久化
BGSAVE原理:
?

?
檢查子進程的目的是:為了防止子進程之間的競爭;
系統(tǒng)(Linux)調用fork():創(chuàng)建進程,實現(xiàn)Copy-on-write(寫時復制)。傳統(tǒng)方式下,在fork進程時直接把所有資源全部復制給子進程,這種實現(xiàn)方式簡單但是效率低下。Linux為了降低創(chuàng)建子進程的成本,改進fork實現(xiàn),當父進程創(chuàng)建子進程時,內核只為子進程創(chuàng)建虛擬空間,父子兩個進程使用的是相同的物理空間,只有子進程發(fā)生更改的時候,才會為子進程分配獨立的物理空間。
Copy-on-write:
如果有多個調用者同時要求相同資源(如內存或磁盤上的數(shù)據(jù)存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統(tǒng)才會真正復制一份專用副本給該調用者,而其他調用者所見到的最初的資源仍然保持不變。
- AOF(Append-Only-File)持久化:保持寫狀態(tài)
記錄下除了查詢以外的所有變更數(shù)據(jù)庫狀態(tài)的指令,所有寫入AOF的指令都是以Redis協(xié)議格式來保存的;
以append的形式追加保存到AOF文件中(增量),就算遇到停電的情況也能盡最大全力去保證數(shù)據(jù)的無損;
日志重寫解決AOF文件大小不斷增大的問題,原理如下:
調用fork(),創(chuàng)建一個子進程;
子進程把新的AOF寫到一個臨時文件里面,不依賴于原來的AOF文件;
主進程持續(xù)將新的變動同時寫到內存buffer中和原來的AOF文件里;
主進程獲取子進程重寫AOF的完成信號,往新的AOF文件同步增量變動;
使用新的AOF文件替換舊的AOF文件
?

?
對于上圖有幾個關鍵點:
在重寫期間,由于主進程依然在響應命令,為了保證最終備份的完整性。因此它依然會寫入舊的AOF file中,如果重寫失敗,能夠保證數(shù)據(jù)不丟失。
為了把重寫期間響應的寫入信息也寫入到新的文件中,因此也會為子進程保留一個buf,防止新寫的file丟失數(shù)據(jù)。
重寫是直接把當前內存的數(shù)據(jù)生成對應命令,并不需要讀取老的AOF文件進行分析、命令合并。
AOF文件直接采用的文本協(xié)議,主要是兼容性好、追加方便、可讀性高。
RDB和AOF文件共存情況下的恢復流程
?

?
RDB和AOF的優(yōu)缺點:
| 類別 | 優(yōu)點 | 缺點 |
|---|---|---|
| RDB | 全量數(shù)據(jù)快照,文件小,恢復快 | 無法保存最近一次快照之后的數(shù)據(jù),會丟失這部分的數(shù)據(jù) |
| AOF | 可讀性高,適合保存增量數(shù)據(jù),數(shù)據(jù)不易丟失 | 文件體積大,恢復時間長 |
- RDB-AOF混合持久化方式
在Redis 4.0之后推出了混合持久化方式,而且作為默認的配置方式。先以RDB方式從管道寫全量數(shù)據(jù)再使用AOF方式從管道追加。AOF文件先半段是RDB形式的全量數(shù)據(jù),后半段是Redis命令形式的增量數(shù)據(jù)。
- BGSAVE做鏡像全量持久化,AOF做增量持久化。因為BGSAVE需要耗費大量的時間,不夠實時,在停機的時候會造成大量數(shù)據(jù)丟失,這時需要AOF配合使用。在Redis實例重啟的時候,會使用BGSAVE持久化文件重新構建內容,再使用AOF重放近期的操作指令,來實現(xiàn)完整恢復重啟之前的狀態(tài)。
7. 使用Pineline的好處
Pineline和Linux的管道類似;
Redis基于請求/響應模型,單個請求處理需要一一應答;
Pineline批量執(zhí)行指令,節(jié)省了多次I/O往返的時間;
有順序依賴的指令建議分批發(fā)送;
Redis的同步機制:
主從同步原理
全同步過程:
Salve發(fā)送sync命令到Master;
Master啟動一個后臺進程,將Redis中的數(shù)據(jù)快照保存到文件中(BGSAVE);
Master將保存數(shù)據(jù)快照期間接收到的寫命令(增量數(shù)據(jù))緩存起來;
Master完成寫文件操作后,將該文件發(fā)送給Salve;
Salve接收到文件之后,使用新的AOF文件替換掉舊的AOF文件;
Master將這期間收集的增量寫命令發(fā)送給Salve端,進行回放。
全同步完成后,后續(xù)所有寫操作都是在Master上進行,讀操作都是在Salve上進行。
增量同步過程:
Master接收到用戶的操作指令,判斷是否需要傳播到Slave;
首先將操作轉換成Redis內部協(xié)議格式并以字符串的形式存儲,然后將字符串存儲的操作紀錄追加到AOF文件;
將操作傳播到其他Slave:1. 對齊主從庫,確保從數(shù)據(jù)庫的該操作的數(shù)據(jù)庫;2. 將命令和參數(shù)按照Redis內部協(xié)議格式往響應緩存中寫入指令;
將緩存中的數(shù)據(jù)發(fā)送給Slave。
主從模式的弊端在于不具有高可用性,當Master掛掉以后,Redis將不能對外提供寫入操作。
Redis Sentinel(Redis哨兵)
解決主從同步Master宕機后的主從切換問題:
監(jiān)控:檢查主從服務器是否運行正常;
提醒:通過API向管理員或者其他應用程序發(fā)送故障通知;
自動故障遷移:主從切換;
留言協(xié)議Gossip
在雜亂無章中尋求一致
每個節(jié)點都隨機地與對方通信,最終所有節(jié)點的狀態(tài)都會達到一致;
種子節(jié)點定期(每秒)隨機向其他節(jié)點發(fā)送節(jié)點列表以及需要傳播的消息;
不保證信息一定會傳遞給所有節(jié)點,但是最終會趨于一致。
8. Redis的集群原理
如何從海量數(shù)據(jù)里快速找到所需?
分片:按照某種規(guī)則去劃分數(shù)據(jù),分散存儲在多個節(jié)點上;
常規(guī)的按照哈希劃分無法實現(xiàn)節(jié)點的動態(tài)增減,為了解決這個問題引入了一致性哈希算法。
一致性哈希算法:對2^32取模,將哈希值空間組織成虛擬的圓環(huán)
?

?
將數(shù)據(jù)key使用相同的函數(shù)Hash計算出哈希值,具體可以選擇服務器的ip或主機名作為關鍵字進行hash,這樣就能確定每個服務器在Hash環(huán)上的位置。接下來,對數(shù)據(jù)使用同樣的方法進行Hash去定位訪問到的服務器,沿環(huán)順時針行走第一臺遇到的服務器就是存儲的目標服務器。
綜上所述,一致性哈希算法對于節(jié)點的增減都只需要定位環(huán)空間中的一小部分數(shù)據(jù),具有較好的容錯性和擴展性。
缺點:服務節(jié)點較少的時候,Hash環(huán)的數(shù)據(jù)傾斜問題
?

?
解決方法:引入虛擬節(jié)點,即對每個服務器節(jié)點計算多個hash,計算結果的位置都放置一個節(jié)點稱為虛擬節(jié)點。具體做法,例如在服務器ip或主機名后添加后綴名。
?

?