Redis知識點總結

什麼是Redis?
Redis(Remote Dictionary Server)是使用C語言編寫,基于記憶體的K-V鍵值對資料庫,它支援資料的持久化以及多種資料類型(字元串、清單、哈希、集合、有序集合)單線程處理請求,QPS可達10W+,也支援簡單的事務、持久化、LUA腳本以及多種叢集方案。
五大資料類型
String(字元串)
Redis沒有使用C語言傳統的字元串表示(以空字元串結尾的字元數組,C字元串),而是自己建構了一種簡單動态字元串(Simple Dynamic String,SDS字元串)的抽象類型。
存儲結構
參數說明
- free:記錄buf數組中未使用的位元組的數量(0表示這個SDS沒有配置設定任何未使用空間)
- len:記錄buf數組中已使用位元組的長度(5表示這個SDS儲存了一個5位元組長的字元串)
- buf:位元組數組,用于儲存字元串(該屬性是一個char類型的數組,該數組分别儲存了’R’、‘e’、‘d’、‘i’、‘s’五個字元,最後一個位元組則儲存了空字元’\0’)
使用場景
做簡單的鍵值對緩存
C字元串與SDS之間的差別
C字元串 | SDS字元串 |
---|---|
擷取字元串長度的複雜度為O(N) | 擷取字元串長度的複雜度為O(1) |
API是不安全的,可能會造成緩沖區溢出 | API是安全的,不會造成緩沖區溢出 |
修改字元串長度N次必然要執行N次記憶體重配置設定 | 修改字元串長度N次最多需要N次記憶體重配置設定 |
隻能儲存文本資料 | 可以儲存文本或者二進制資料 |
可以使用所有<string.h>庫中的函數 | 隻能使用部分<string.h>庫中的函數 |
①常數時間擷取字元串的長度
SDS中len字段儲存着字元串的長度,是以總能在常數時間内擷取字元串長度
②避免緩沖區溢出
假設在記憶體中有兩個緊挨着的兩個字元串,s1=“1234”,s2=“abcd”。
由于記憶體上緊緊相連,當我們對s1進行擴充時,将s1="12345678"後,由于沒有進行相應的記憶體重配置設定,導緻s1把s2覆寫掉,導緻s2被莫名的修改。
但SDS的API對字元串修改時首先會檢查空間是否足夠,若不充足則會配置設定新空間,避免了緩沖區溢出問題。
③減少字元串修改時記憶體重配置設定次數
在C中當我們頻繁對一個字元串進行修改(append或trim)操作的時候,需要頻繁的進行記憶體重配置設定操作,十分影響性能。
對于Redis來說,本身就會頻繁的修改字元串,是以使用C字元串并不合适。而SDS實作了空間預配置設定和惰性空間釋放兩種優化政策。
-
空間預配置設定:當SDS的API對一個SDS修改後,并且對SDS空間擴充時,程式不僅會為SDS配置設定 所需要的必須空間,還會配置設定額外的未使用空間。
配置設定規則如下:如果對 SDS 修改後,len 的長度小于 1M,那麼程式将配置設定和 len 相同長度的未使用空間。舉個例子,如果 len=10,重新配置設定後,buf 的實際長度會變為 10(已使用空間)+10(額外空間)+1(空字元)=21。如果對 SDS 修改後 len 長度大于 1M,那麼程式将配置設定 1M 的未使用空間。
- 惰性空間釋放:當對SDS進行縮短操作時,程式并不會回收多餘的記憶體空間,而是使用free字段将這些位元組數量記錄下來,後面如果需要append操作,則直接使用free中未使用空間。
④二進制安全
在Redis中不僅可以存儲String類型的資料,而且也能存儲一些二進制資料。
二進制資料并不是規則的字元串格式,其中會包含一些特殊的字元如’\0’,在C中遇到’\0’則表示字元串結束,但在SDS中,标志字元串結束是len屬性。
List(連結清單)
連結清單提供了高效的節點重排能力,以及順序性的節點通路方式,并且可以通過增删節點來靈活地調整連結清單地長度。
由于Redis使用的C語言沒有内置這種資料結構,是以Redis建構了自己的連結清單實作。
存儲結構
節點的定義
連結清單的定義
示意圖
使用場景
連結清單被廣泛的用于實作Redis的各種功能,比如清單鍵、釋出與訂閱、慢查詢、螢幕等
Hash(哈希/字典)
在哈希表中,一個鍵(Key)可以和一個值(Value)進行關聯,這些關聯的鍵和值就稱為鍵值對。
字典中的每個鍵都是獨一無二的,可以在字典中根據鍵查找與之關聯的值,或者通過鍵更新值。
由于Redis所使用的C語言并沒有内置這種資料結構,是以Redis建構了自己的字典實作。
存儲結構
字典的定義
參數說明
- table :是一個數組,數組中的每個元素都指向dictEntry結構,每個dictEntry結構儲存着一個鍵值對
- size:記錄了哈希表的大小
- used:記錄哈希表目前已有節點(鍵值對)的數量
示意圖
使用場景
Redis持久化
Redis是将資料存儲在記憶體中的,要是服務當機,所有資料将會丢失,為了防止資料丢失,Redis支援兩種政策将記憶體中的資料寫到磁盤中來防止資料丢失。
Redis提供兩種持久化方式:RDB(Redis DataBase)和AOF(Append Only File)
RDB
該方式伺服器程序會fork一個子程序,由子程序去做持久化操作,子程序會先将Redis所有非空資料庫的資料進行拷貝到自己的記憶體空間,然後将這些資料寫到一個臨時檔案,寫入完畢,會使用臨時檔案替換掉dump.rdb檔案,線程銷毀
示意圖
觸發時機
使用相關指令儲存
- SAVE指令:該指令執行時,Redis伺服器會被阻塞,是以當SAVE指令正在執行時,用戶端發送的所有指令請求都會被拒絕。
- BGSAVE指令:該指令的儲存工作是由子程序執行的,是以在子程序建立RDB檔案的過程中,Redis伺服器任然可以繼續處理用戶端的指令請求。
自動間隔性儲存
在redis.conf檔案中已經預設配置了3種:
配置 | 描述 |
---|---|
save 900 1 | 伺服器在900s(15min)内,對資料庫進行了至少1次修改 |
save 300 10 | 伺服器在300s(5min)内,對資料庫進行了至少10次修改 |
save 60 10000 | 伺服器在60s(1min)内,對資料庫進行了至少有10000修改 |
AOF
該方式是以日志的形式記錄Redis每個寫操作,将Redis執行過的所有寫指令記錄下來(讀操作不記錄),隻許追加檔案不可以改寫檔案,Redis啟動之後會讀取appendonly.aof檔案來實作重新恢複資料。預設不開啟,需要将redis.conf種的appendonly no改為yes來啟動。
要開啟Redis的AOF持久化模式必須将
appendonly
配置項改為yes
appendfsync選項的值 | 對持久化行為的影響 |
---|---|
always | 将aof_buf緩沖區中的所有内容寫入并同步到AOF檔案 |
everysec | 将aof_buf緩沖區中的所有内容寫入到AOF檔案,如果上次同步AOF檔案的時間距離現在超過一秒鐘,那麼再次對AOF檔案進行同步,并且這個同步操作是由一個線程專門負責執行的 |
no | 将aof_buf緩沖區中的所有内容寫入到AOF檔案,但并不對AOF檔案進行同步,何時同步由作業系統來決定 |
AOF檔案的載入與資料還原
因為AOF檔案包含了重建資料庫狀态的所有寫指令,是以隻要伺服器載入AOF檔案重新執行一遍儲存的寫指令,就可以還原資料庫關閉之前的狀态。(由于Redis指令隻能在用戶端上下文中執行,是以這塊建立了一個僞用戶端)
重寫機制
上面介紹了AOF方式是通過記錄對Redis每次的寫操作,這樣的話就會存在一個問題,一直添加最終會導緻檔案過于龐大。是以,為了避免這種狀況,Redis提供了重寫機制,當AOF檔案得大小超過指定的門檻值時,Redis會自動啟用AOF檔案的内容壓縮,隻保留可以恢複資料的最小指令集,可以使用指令
bgrewriteaof
。
重寫原理:AOF檔案持續增長過大時,會fork出一條新程序來将檔案重寫(也是生成臨時檔案再rename),周遊服務程序的記憶體中的資料,每條記錄會對應生成一條set語句寫入到臨時檔案。重寫aof檔案的操作,并沒有讀取舊的aof檔案,而是将整個記憶體中的資料庫内容用指令的方式重寫了到一個新的aof檔案,類似快照。
觸發時機:Redis會記錄上一次重寫時的aof大小,預設配置是當AOF檔案大小是上一次的一倍并且大于64M時,會觸發重寫機制。
資料不一緻問題:Redis是通過建立一個子程序去做重寫操作的,是以就有可能出現下面這種資料不一緻情況
當子程序開始重寫時,資料庫中隻有K1這一個鍵,但是當子程序完成AOF檔案重寫之後,伺服器程序的資料庫已經新增了K2、K3、K4三個鍵,是以,重寫AOF檔案和伺服器目前的資料庫狀态并不一緻。
解決方案:為了解決上面的資料不一緻問題,Redis伺服器引入了一個AOF重寫緩沖區
如上圖,這個緩沖區在伺服器建立子程序的時候開始使用,當Redis執行完一個寫指令之後,它會同時将這個指令發送給AOF緩沖區和AOF重寫緩沖區。
RDB與AOF對比
RDB的優點:
- 如果要進行大規模資料的恢複,RDB方式要比AOF方式恢複速度要快
- RDB可以最大化Redis性能,父程序做的就是fork子程序,然後繼續接受用戶端請求,讓子程序負責持久化操作,父程序無需程序IO操作。
- RDB儲存了某一時刻的資料集,非常适合用作備份,同時也非常适合災難性恢複。
RDB的缺點:
- RDB不太适用對資料完整性要求嚴格的情況,盡管我們可以通過修改快照持久化的頻率,但是要持久化的資料時一段時間内的整個資料集的狀态,如果在還沒有觸發快照時,本機就當機了,那麼對資料庫所做的寫操作就随之消失。
- 每次進行RDB時,父程序會fork一個子程序,由子程序來進行實際的持久化操作,如果資料集龐大,那麼fork子程序這個過程将非常耗時,就會出現伺服器暫停用戶端請求,将記憶體中的資料複制一份給子程序,讓子程序程序持久化操作。
AOF的優點:
- AOF支援多種持久化政策
AOF的缺點:
- 對于相同的資料集來說,AOF檔案要比RDB檔案大。
- 根據持久化政策來說,AOF的速度要慢于RDB
RDB與AOF如何選擇
- 要想做到足夠高的資料安全性,應該同時使用兩種持久化方式。
- 如果可以接受分鐘級别内的資料丢失,可以隻使用RDB持久化(比如:隻用做緩存)。
資料恢複機制
上面介紹了資料的持久化,而持久化的最終目錄就是為了盡可能的避免資料丢失,當重新開機Redis服務的時候能夠重新恢複資料到記憶體。
重新開機Redis時,如果dump.rdb與appendfsync.aof同時存在時,Redis會優先讀取appendfsync.aof檔案進行資料恢複。
key過期機制
設定鍵的生存時間
EXPIRE <key> <ttl> # 将鍵key的生存時間設定為ttl秒
PEXPIRE <key> <ttl> # 将鍵key的生存時間設定為ttl毫秒
EXPIREAT <key> <timestamp> # 将鍵key的過期時間設定為timestamp所指定的秒數時間戳
PEXPIREAT <key> <timestamp> # 将鍵key的過期時間設定為timestamp所指定的毫秒數時間戳
為給定key設定生存時間,當key過期時,它會被自動删除。在Redis中帶有生存時間的key被稱為『易失的』(volatile)
檢視剩餘生存時間
TTL <key> #以秒為機關傳回鍵的剩餘生存時間
PTTL <key> #以毫秒為機關傳回鍵的剩餘生存時間
儲存過期鍵的結構
如下圖redisDb(代表Redis的資料庫對象)結構的expires字段儲存了資料庫中所有鍵的過期時間,我們稱為過期字典:
- 過期字典的鍵是一個指針,這個指針指向鍵空間中的某個鍵對象
- 過期字典的值是一個long long類型的整數,這個整數儲存了鍵所指向的資料庫鍵的過期時間——一個毫秒精度的UNIX時間戳
過期鍵删除政策
實際上Redis使用懶惰删除+定期删除相結合的方式處理過期的key。
懶惰删除
所謂懶惰删除就是在用戶端通路該key的時候,Redis會對key的過期時間進行檢查,如果過期了就立即删除。
這種方式看似很完美,在通路的時候檢查key的過期時間,不會占用太多的額外CPU資源。但是如果一個key已經過期了,如果長時間沒有被通路,那麼這個key就會一直存留在記憶體中,嚴重消耗記憶體資源。
定期删除
Redis會将所有設定了過期時間的key放入到一個字典中,然後每隔一段時間從字典中随機找出一些key檢查過期時間并删除已過期的key。
Redis預設每秒進行10次過期掃描:
- 從過期字典中随機20個key
- 删除這20個key中已過期的
- 如果超過25%的key過期,則重複第一步
同時,為了保證不出現循環過度的情況,Redis還設定了掃描的時間上限,預設不會超過25ms。
淘汰政策
一般緩存都是通過記憶體實作的,而記憶體的空間又非常的珍貴,可能我們一些資料偶爾讀取一次就被放入緩存,有的資料經常被通路。但是一般我們會設定緩存的上限,那麼如何保證熱點資料最終會保留下來呢?抛開Redis,常見的緩存淘汰機制有:随機、最近最少使用(lru)、先進先出等。那我們看看Redis都提供了哪些淘汰機制:
- volatile-lru:從已設定過期時間的資料集中挑選最近最少使用的資料淘汰
- volatile-ttl:從已設定過期時間的資料集中挑選将要過期的資料淘汰
- volatile-random:從已設定過期時間的資料集中選擇任意資料淘汰
- allkeys-lru:從資料集中選擇最近最少使用的資料淘汰
- allkeys-random:從資料集中任意選擇資料淘汰
- no-enviction:禁止驅逐資料
Redis多機實作
主從模式(master-slave)
可以通過執行
SLAVEOF
指令或者設定slaveof選項,讓一個伺服器去複制(replicate)另一台伺服器,這種工作模式我們稱為主從模式。其中被複制的伺服器為主伺服器(master),對主伺服器進行複制的伺服器稱為從伺服器(slave)。
示意圖
相關指令
# 設定主伺服器 host:主伺服器ip port:主伺服器端口
SLAVEOF host port
實作原理
舊版複制功能的實作
Reids的複制功能分為同步(sync)和指令傳播(command propagate)兩個操作。
- 同步操作用于将從伺服器的狀态更新至主伺服器目前所處的資料庫狀态
- 指令傳播操作當主伺服器的狀态被修改時,導緻主從伺服器的資料庫狀态不一緻時,讓主從伺服器的資料庫重新回到一緻狀态
同步操作是從伺服器向主伺服器發送
SYNC
指令來完成的,示意圖如下:
- 從伺服器向主伺服器發送
指令SYNC
- 主伺服器收到指令後執行
指令,在背景生成RDB檔案,同時使用一個緩沖區記錄從現在開始執行的所有寫操作。BGSAVE
- 主伺服器指令執行完畢,将生成的RDB檔案發送給從伺服器,從伺服器接受并載入,然後将更新資料庫狀态更新至主伺服器執行
指令時的資料庫狀态BGSAVE
指令傳播上面同步操作解決了初次的主從伺服器資料的一緻性,如果後續對主伺服器進行的寫操作,又會導緻主從伺服器不一緻問題,為了保證再次回到一緻狀态,主伺服器會将自己執行的寫指令發送給從伺服器執行,從伺服器執行發來的指令後會再次回到一緻狀态。
新版複制功能的實作
Redis從2.8版本開始,使用
PSYNC
指令代替
SYNC
指令來執行複制時的同步操作。
PSYNC
指令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)兩種模式
完整重同步用于處理初次複制的情況,完整重同步執行步驟和
SYNC
指令的執行步驟基本一樣,他們都是通過讓主伺服器建立并發送RDB檔案,以及向伺服器發送儲存在緩沖區裡面的寫指令來進行同步
部分重同步
PSYNC指令的實作
哨兵模式(Sentinel)
哨兵模式是Redis的高可用性解決方案:由一個或多個Sentinel執行個體組成的Sentinel系統可以同時監控任意多個主伺服器,以及每個主伺服器下的所有從伺服器。在監視到主伺服器進入下線狀态時,自動将下線主伺服器屬下的某個從伺服器更新為新的主伺服器,然後由新的主伺服器代替已下線的主伺服器繼續處理指令請求。
示意圖
- 雙環代表主伺服器server1
- 單環代表三個從伺服器server2、server3、server4
- server2、server3、server4三個從伺服器正在複制主伺服器server1,而sentinel系統正在監聽所有四個伺服器
可以看出哨兵模式就是在主從複制模式之上添加了哨兵系統,進而實作故障的自動轉移
故障轉移
如上圖16-3所示,主服務server1挂掉了,處于下線狀态,那麼server2、server3、server4對主伺服器的複制操作将被終止,并且隔一段時間sentinel系統也會察覺到server1的下線,下面先說一下故障轉移的流程:
- Sentinel系統會挑選server1屬下的其中一個從伺服器,并選中的從伺服器更新為新的主伺服器
- Sentinel系統會向server1屬下的所有從伺服器發送新的複制指令,讓他們成為新的主伺服器的從伺服器,當所有從伺服器都開始複制新的主伺服器時,故障轉移操作執行完畢
- Sentinel系統還會繼續監聽已下線的server1,如果它重新上線時,會将它設定為新的主伺服器的從伺服器
Sentinel系統與各個節點的通訊
Sentinel如何判斷主伺服器下線
主觀下線
在預設情況下,Sentinel系統會以每秒一次的頻率向所有與它建立了指令連接配接的執行個體發送
PING
指令,并通過執行個體傳回的結果來判斷執行個體是否線上。
如果一個執行個體在
down-after-milliseconds
毫秒内(預設30s),連續向Sentinel傳回無效回複,那該Sentinel就會将其标記為主觀下線狀态。
Sentinal配置檔案中的
down-after-milliseconds
選項指定了Sentinel判斷執行個體進入主觀下線所需的時間長度。
客觀下線
當Sentinel将一個主伺服器判斷為主觀下線之後,為了确認這個主伺服器是否真的下線了,它會向同樣監視這一主伺服器的其他Sentinel進行詢問,看它們是否也認為主伺服器已經是下線狀态(可以是主觀下線或客觀下線),當Sentinel從其他Sentinel那裡接受到足夠數量的已下線判斷之後,Sentinel就會将從伺服器判定為客觀下線,并對主伺服器開始執行故障轉移操作。
客觀下線狀态的判斷條件:當認為主伺服器已經進入下線狀态的Sentinel的數量,超過Sentinel配置中設定的quorum參數的值,那麼該Sentinel就會認為主伺服器已經進入客觀下線狀态。
上面配置的含義:包括目前Sentinel在内,隻要總共有兩個Sentinel認為主伺服器已經下線,那麼目前Sentinel就将主伺服器判斷為客觀下線。
當Sentinel将一個主伺服器判斷為主觀下線後,它會向同樣監視該主伺服器的其他Sentinel進行詢問,看它們是否同意這個主伺服器已經進入主觀下線狀态。
故障轉移
選舉領頭Sentinel
當一個主伺服器被判斷為客觀下線時,監視這個下線主伺服器的各個Sentinel會進行協商,選舉出一個領頭Sentinel,并由領頭Sentinel對下線主伺服器執行故障轉移操作。
選舉領頭Sentinel的規則:
- 所有線上的Sentinel都有被選為領頭Sentinel的資格
- 每次進行領頭Sentinel選舉之後,不論選舉是否成功,所有Sentinel的配置紀元的值都會自增一次。
- 在一個配置紀元裡面,所有Sentinel都有一次将某個Sentinel設定為局部領頭Sentinel的機會,并且局部領頭一旦設定,在這個配置紀元裡就不能再更改
- 每個發現主伺服器進入客觀下線的Sentinel都會要求其他Sentinel将自己設定為局部領頭Sentinel
- 如果某個Sentinel被半數以上的Sentinel設定成了局部領頭Sentinel,那麼這個Sentinel成為領頭Sentinel
- 如果在給定時限内,沒有一個Sentinel被選舉為領頭Sentinel,那麼各個Sentinel将在一段時間之後再次進行選舉
新主伺服器的選舉&故障轉移
選舉好領頭Sentinel之後,領頭Sentinel将對已下線的伺服器執行故障轉移操作。
第一步:首先會從已下線主伺服器(server1)屬下所有的從伺服器中挑選一個狀态良好、資料完整的從伺服器,并發送
SLAVE no one
指令。
第二步:此時領頭Sentinel會以每秒一次的頻率(平時十秒一次)向被更新的從伺服器(server2)發送INFO指令并觀察傳回的role是否已經變成了master,變成master說明更新成功。
第三步:領頭Sentinel向已下線主伺服器(server1)的兩個從伺服器(server3、server4)發送
SLAVEOF
指令,讓他們複制新的主伺服器(server2)。
所有從伺服器改變主伺服器後如下圖
第四步:當server1重新上線時,Sentinel就會向它發送
SLAVEOF
指令,讓它成為新主伺服器(server2)的從伺服器。
至此,整個故障轉移就完成了。
Redis高可用叢集
緩存穿透、擊穿、雪崩現象及解決方案
持續更新,未完待續。。。。
參考資料:Redis設計與實作[黃健宏]