Redis內存模型原理
字符串
Redis 沒有直接使用 C 字符串(即以空字符’\0’結尾的字符數組)做爲默認的字符串表示,而是使用了SDS。SDS 是簡單動態字符串(Simple Dynamic String)的縮寫。java
它是本身構建了一種名爲 簡單動態字符串(simple dynamic string,SDS)的抽象類型,並將 SDS 做爲
Redis的默認字符串表示。
redis
SDS 定義:算法
struct sdshdr{ //記錄buf數組中已使用字節的數量 //等於 SDS 保存字符串的長度 int len; //記錄 buf 數組中未使用字節的數量 int free; //字節數組,用於保存字符串 char buf[]; }
- len 保存了SDS保存字符串的長度
- buf[] 數組用來保存字符串的每一個元素
- free j記錄了 buf 數組中未使用的字節數量
SDS 在 C 字符串的基礎上加入了 free 和 len 字段,帶來了不少好處:
shell
- 獲取字符串長度: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
dom
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監控,進行數據監控,事務成功執行
-
監控過程當中,他人纂改,事務沒法執行