Redis內存模型原理
字符串
Redis 沒有直接使用 C 字符串(即以空字符’\0’結尾的字符數組)作爲默認的字符串表示,而是使用了SDS。SDS 是簡單動態字符串(Simple Dynamic String)的縮寫。
它是自己構建了一種名爲 簡單動態字符串(simple dynamic string,SDS)的抽象類型,並將 SDS 作爲
Redis的默認字符串表示。
SDS 定義:
struct sdshdr{
//記錄buf數組中已使用字節的數量
//等於 SDS 保存字符串的長度
int len;
//記錄 buf 數組中未使用字節的數量
int free;
//字節數組,用於保存字符串
char buf[];
}
- len 保存了SDS保存字符串的長度
- buf[] 數組用來保存字符串的每個元素
- free j記錄了 buf 數組中未使用的字節數量
SDS 在 C 字符串的基礎上加入了 free 和 len 字段,帶來了很多好處:
- 獲取字符串長度:SDS 是 O(1),C 字符串是 O(n)。
緩衝區溢出:使用 C 字符串的 API 時,如果字符串長度增加(如 strcat 操作)而忘記重新分配內存,很容易造成緩衝區的溢出。 - 而 SDS 由於記錄了長度,相應的 API 在可能造成緩衝區溢出時會自動重新分配內存,杜絕了緩衝區溢出。
- 修改字符串時內存的重分配:對於 C 字符串,如果要修改字符串,必須要重新分配內存(先釋放再申請),因爲如果沒有重新分配,字符串長度增大時會造成內存緩衝區溢出,字符串長度減小時會造成內存泄露。
而對於 SDS,由於可以記錄 len 和 free,因此解除了字符串長度和空間數組長度之間的關聯,可以在此基礎上進行優化。 - 空間預分配策略(即分配內存時比實際需要的多)使得字符串長度增大時重新分配內存的概率大大減小;惰性空間釋放策略使得字符串長度減小時重新分配內存的概率大大減小。
- 存取二進制數據:SDS 可以,C 字符串不可以。因爲 C 字符串以空字符作爲字符串結束的標識,而對於一些二進制文件(如圖片等)。
- 內容可能包括空字符串,因此 C 字符串無法正確存取;而 SDS 以字符串長度 len 來作爲字符串結束標識,因此沒有這個問題。
- 此外,由於 SDS 中的 buf 仍然使用了 C 字符串(即以’\0’結尾),因此 SDS 可以使用 C 字符串庫中的部分函數。
- 但是需要注意的是,只有當 SDS 用來存儲文本數據時纔可以這樣使用,在存儲二進制數據時則不行(’\0’不一定是結尾)。
鏈表
鏈表在Redis中的應用非常廣泛,列表(List)的底層實現之一就是雙向鏈表。此外發布與訂閱、慢查詢、
監視器等功能也用到了鏈表。
typedef struct listNode {
//前置節點
struct listNode *prev;
//後置節點
struct listNode *next;
//節點的值
void *value;
} listNode
typedef struct list {
//表頭節點
listNode.head;
//表尾節點
listNode.tail;
//鏈表所包含的節點數量
unsigned long len;
//節點值複製函數
void *(*dup)(void *ptr);
//節點值釋放函數
void *(*free)(void *ptr);
//節點值對比函數
int (*match)(void *ptr,void *key);
} list;
Redis鏈表優勢:
- 雙向:鏈表具有前置節點和後置節點的引用,獲取這兩個節點時間複雜度都爲O(1)。
與傳統鏈表(單鏈表)相比,Redis鏈表結構的優勢有:
普通鏈表(單鏈表):節點類保留下一節點的引用。鏈表類只保留頭節點的引用,只能從頭節點插入刪
除 - 無環:表頭節點的 prev 指針和表尾節點的 next 指針都指向 NULL,對鏈表的訪問都是以 NULL
結束。 - 帶鏈表長度計數器:通過 len 屬性獲取鏈表長度的時間複雜度爲 O(1)。
- 多態:鏈表節點使用 void* 指針來保存節點值,可以保存各種不同類型的值。
字典
字典又稱爲符號表或者關聯數組、或映射(map),是一種用於保存鍵值對的抽象數據結構。
字典中的每一個鍵 key 都是唯一的,通過 key 可以對值來進行查找或修改。
Redis 的字典使用哈希表作爲底層實現。
哈希(作爲一種數據結構),不僅是 Redis 對外提供的 5 種對象類型的一種(hash),也是 Redis 作
爲 Key-Value 數據庫所使用的數據結構。
typedef struct dictht{
//哈希表數組
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩碼,用於計算索引值
//總是等於 size-1
unsigned long sizemask;
//該哈希表已有節點的數量
unsigned long used;
} dictht
/*哈希表是由數組 table 組成,table 中每個元素都是指向 dict.h/dictEntry 結構,
dictEntry 結構定義如下:
*/
typedef struct dictEntry {
//鍵
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
} v;
//指向下一個哈希表節點,形成鏈表
struct dictEntry *next;
} dictEntry
跳錶(zset)
普通單鏈表查詢一個元素的時間複雜度爲O(n),即使該單鏈表是有序的。
查找46 : 55—21—55–37–55–46
typedef struct zskiplistNode {
//層
struct zskiplistLevel{
//前進指針
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
//後退指針
struct zskiplistNode *backward;
//分值
double score;
//成員對象
robj *obj;
} zskiplistNode
//鏈表
typedef struct zskiplist{
//表頭節點和表尾節點
structz skiplistNode *header, *tail;
//表中節點的數量
unsigned long length;
//表中層數最大的節點的層數
int level;
} zskiplist;
- 搜索:從最高層的鏈表節點開始,如果比當前節點要大和比當前層的下一個節點要小,那麼則往下
找,也就是和當前層的下一層的節點的下一個節點進行比較,以此類推,一直找到最底層的最後一個節
點,如果找到則返回,反之則返回空。 - 插入:首先確定插入的層數,有一種方法是假設拋一枚硬幣,如果是正面就累加,直到遇見反
面爲止,最後記錄正面的次數作爲插入的層數。當確定插入的層數k後,則需要將新元素插入到從底層
到k層。 - 刪除:在各個層中找到包含指定值的節點,然後將節點從鏈表中刪除即可,如果刪除以後只剩
下頭尾兩個節點,則刪除這一層。
緩存淘汰策略
最大緩存
- 在 redis 中,允許用戶設置最大使用內存大小maxmemory,默認爲0,沒有指定最大緩存,如果有新的數據添加,超過最大內存,則會使redis崩潰,所以一定要設置。
- redis 內存數據集大小上升到一定大小的時候,就會實行數據淘汰策略。
淘汰策略
- redis淘汰策略配置:maxmemory-policy voltile-lru,支持熱配置
redis 提供 6種數據淘汰策略:
- volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰
- volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰
- volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰
- allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰
- allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰
- no-enviction(驅逐):禁止驅逐數據
LRU原理
LRU(Least recently used,最近最少使用)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心
思想是“如果數據最近被訪問過,那麼將來被訪問的機率也更高”。
- 新數據插入到鏈表頭部;
- 每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部;
- 當鏈表滿的時候,將鏈表尾部的數據丟棄。
在Java中可以使用LinkHashMap去實現LRU
事務
事務中命令按順序執行,中途有命令出錯後續命令仍執行,如果是語法錯誤則事務無法提交。
-
Redis事務沒有隔離級別:
Redis事務執行命令會放入隊列中,事務未提交時不會被執行,也就不存在事務內的查詢要看到事務裏的更新,事務外查詢不能看到。 -
Redis不保證原子性:
Redis中單條命令是原子性執行的,但事務不保證原子性,且沒有回滾。事務中任意命令執行失敗,其餘的命令仍會被執行。
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> SET book-name "Hello World"
QUEUED
redis 127.0.0.1:6379> GET book-name
QUEUED
redis 127.0.0.1:6379> SADD tag "java" "go" "c"
QUEUED
redis 127.0.0.1:6379> SMEMBERS tag
QUEUED
redis 127.0.0.1:6379> EXEC
1) OK
2) "Hello World"
3) (integer) 3
4) 1) "c"
2) "go"
3) "java"
WATCH機制(樂觀鎖)
watch變量,並開啓事務,如果該變量被修改那麼事務無法執行,否則成功執行。
-
初始化信用卡可用餘額和欠額
-
用watch監控,進行數據監控,事務成功執行
-
監控過程中,他人纂改,事務無法執行