1. Redis支持的數據類型
Redis詳解(五)------ redis的五大數據類型實現(xiàn)原理
(1). 對象類型和編碼
? Redis中每次創(chuàng)建一個鍵值對是,至少會創(chuàng)建兩個對象,鍵對象和值對象,
? Redis中每一個對象都是由redisObject來表示的:
typedef struct redisObject{
//類型
unsigned type:4;
//編碼
unsigned encoding:4;
//指向底層數據結構的指針
void *ptr;
//引用計數
int refcount;
//記錄最后一次被程序訪問的時間
unsigned lru:22;
}robj
? ==type==屬性就是我們所講的五大數據類型(鍵一般就是字符串,值可以是字符串,列表,集合等等)
? ==encoding==指的是每種數據結構存儲的不同數據(例如字符串可以存儲字符類型,也可以存儲數值類型).也用來數據類型的不同實現(xiàn)方式(list的壓縮列表實現(xiàn)和雙端鏈表實現(xiàn)).
? ==ptr==指向的是底層數據結構的物理存儲地址.
(1). string
1). 定義
? ==字符串,能保存任何類型的數據==,包括二進制數據,最大512M(單個的key-value)
? 所有的key都是string類型,另外其他數據結構的構成元素也是字符串
? 格式: set key value
2). 編碼
? 編碼可以是int編碼(long類型的整數值),embstr編碼(長度大于44字節(jié)的字符串),raw編碼(大于44字節(jié)的字符串)
3). 存儲
? raw和embstr編碼使用==sdshdr==保存數據,其==內部維護一個字符數組,并存儲已用容量和未使用容量==.(這與C語言中的字符串實現(xiàn)不同,C語言中的字符串的數組是不可變的,但是共同點是都==以'\0'結尾==,目的是為了使用c的部分str庫函數)
struct sdshdr{
//記錄buf數組中已使用字節(jié)的數量
//等于 SDS 保存字符串的長度
int len;
//記錄 buf 數組中未使用字節(jié)的數量
int free;
//字節(jié)數組,用于保存字符串
char buf[];
}
? 使用sds而不是c格式的字符串的好處:方便獲取字符串長度,杜絕溢出(會先檢查空閑空間大小),減少內存重新分配(重用),二進制安全(二進制表示的數據中可能會出現(xiàn)'\0',sds雖然以'\0'結尾但是并不以'\0'為結束符,而是根據長度判斷是否結束)
? raw分配空間時,redisObject和sdshdr不在一起,使用指針連接.embstr分配空間則是連續(xù)的.
4). 轉碼
? int編碼保存的值超過long大小范圍后,會轉化為raw.對于embstr編碼的數據在修改時一定會轉化為raw編碼.
(2). list
1). 定義
? ==list:列表,簡單的字符串列表==,按照插入順序排序
? 可以頭添加和尾添加,==底層使用鏈表實現(xiàn)==
? 格式: lpush name value1 value2......
2). 編碼
? 編碼可以是ziplist(壓縮鏈表,將數據按照一定規(guī)則編碼在一塊連續(xù)的內存區(qū)域)和linkedlist(雙端鏈表)
1>. 壓縮鏈表
壓縮列表的每個節(jié)點構成如下:
①锉走、previous_entry_ength:記錄壓縮列表前一個字節(jié)的長度藕届。previous_entry_ength的長度可能是1個字節(jié)或者是5個字節(jié)休偶,如果上一個節(jié)點的長度小于254踏兜,則該節(jié)點只需要一個字節(jié)就可以表示前一個節(jié)點的長度了八秃,如果前一個節(jié)點的長度大于等于254,則previous length的第一個字節(jié)為254昔驱,后面用四個字節(jié)表示當前節(jié)點前一個節(jié)點的長度。利用此原理即當前節(jié)點位置減去上一個節(jié)點的長度即得到上一個節(jié)點的起始位置纳本,壓縮列表可以從尾部向頭部遍歷繁成。這么做很有效地減少了內存的浪費朴艰。
②祠墅、encoding:節(jié)點的encoding保存的是節(jié)點的content的內容類型(前兩位)以及長度(后面的所有位)encoding區(qū)域長度為1字節(jié)、2字節(jié)或者5字節(jié)長瓜挽。
③、content:content區(qū)域用于保存節(jié)點的內容腔长,節(jié)點內容類型和長度由encoding決定捞附。內部數據如果是數值類型,那么轉換為2進制存儲,如果是字符串類型,那么將每個字符的ACSII碼找出,然后用兩位16進制數存儲(一共16位的空間).
2>. 雙端鏈表
typedef struct listNode{
//前置節(jié)點
struct listNode *prev;
//后置節(jié)點
struct listNode *next;
//節(jié)點的值
void *value;
}listNode;
typedef struct list{
//表頭節(jié)點
listNode *head;
//表尾節(jié)點
listNode *tail;
//鏈表所包含的節(jié)點數量
unsigned long len;
//節(jié)點值復制函數
void (*free) (void *ptr);
//節(jié)點值釋放函數
void (*free) (void *ptr);
//節(jié)點值對比函數
int (*match) (void *ptr,void *key);
}list;
? 就是雙向鏈表么.
(3). hash
1). 定義
? 哈希類型,是一個string類型的field和value的映射表(參考Map,name指的是數據類型的名稱,下同)
? 格式: hmset name key1 value1 key2 value2........
2). 編碼
? ziplist(相鄰節(jié)點存儲key和value)和hashtable(下面講解)
1>. hashtable
? 類比HashMap
typedef struct dictht{
//哈希表數組
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩碼胆绊,用于計算索引值 總是等于 size-1
unsigned long sizemask;
//該哈希表已有節(jié)點的數量
unsigned long used;
}dictht;
typedef struct dictEntry{
//鍵
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
//指向下一個哈希表節(jié)點欧募,形成鏈表,鏈地址法解決哈希沖突
struct dictEntry *next;
}dictEntry;
2>. 哈希算法
// 使用字典設置的哈希函數跟继,計算鍵 key 的哈希值
int hash = dict->type->hashFunction(key);
// 有三種hash函數,分別對整型提供一種算法,字符串提供兩種算法
// 使用哈希表的sizemask屬性和第一步得到的哈希值舔糖,計算索引值
int index = hash & dict->ht[x].sizemask;
3>. 收縮和擴容
? 二倍擴容/收縮.對每一個元素重新哈希后放入新的內存空間,然后將原內存空間釋放.
? 負載因子 = 哈希表大小/數組長度
? 執(zhí)行磁盤訪問時(BGSAVE和BGREWRITEAOF),負載因子大于5才會擴容,否則大于1就會擴容.
4>. 漸進式擴容
? 擴容并不是一次性完成的,數據量過大的情況下阻塞會非常明顯.
? 所以依次擴容行為分為多次進行,在這期間產生了兩個hash,當對數據的操作在其中一張表中沒有找到時,就會查找另一張表.
3). 轉碼
? 保存元素小于512,每個元素長度小于64字節(jié)時,使用ziplist,否則使用hashtable
(4). set
1). 定義
? set:集合,無序,成員唯一
? 格式: sadd name value1 value2......
2). 編碼
? 有intset和hashtable兩種.
? intset只能存儲整數類型.
? hashtable底層使用hash實現(xiàn),可以理解為Java中的HashSet.
3). 轉碼
? 當集合中所有元素都是整數,并且總量不超過512時,使用intset,其他所有情況使用hashtable.
(5). zset
1). 定義
? 有序集和,每一個value都對應一個score(double類型)用以排序
? 格式: zadd name score1 value1 score2 value2......
? zset的成員是唯一的,但分數(score)卻可以重復
2). 編碼
? 可以是ziplist(之前提到過,使用兩個相鄰的節(jié)點存儲元素和分值,內部存儲時就已經按分值排序了)和skiplist(跳躍表,下面講解)
1>. skiplist
typedef struct zset{
//跳躍表
zskiplist *zsl;
//字典
dict *dice; //字典的鍵存放元素的分值,字典的值存放元素本身
} zset;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 鏈式存儲,這里是有序鏈表
unsigned long length;
int level;
} zskiplist;
typedef struct zskiplistNode {
robj *obj; // 存儲元素,這里和最外層的字典共享指針,保證數據的不重復
double score; // 存儲分值
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
3). 轉碼
? 當滿足元素數量小于128并且所有元素長度小于64字節(jié)時,使用ziplist,否則使用skiplist.
-
list:列表,簡單的字符串列表,按照插入順序排序
格式: lpush name value1 value2......
-
set:集合,無序,成員唯一
格式: sadd name value1 value2......
通過哈希表實現(xiàn)
zset:有序集和,每一個value都對應一個score(double類型)用以排序
格式: zadd name score1 value1 score2 value2......
zset的成員是唯一的,但分數(score)卻可以重復
2. 什么是Redis的持久化
(1). 概念
? 持久化就是將Redis中用內存存儲的數據寫入磁盤,下次啟動Redis服務可以恢復到內存中
(2). 方式
- RDB:即Redis DataBase.就是將Redis中的數據寫入磁盤中
- 核心功能在于rdbSave(寫入RDB文件)和rdbLoad(從文件加載到內存)兩個函數
- AOF:即Append-Only File.字面意思是只可追加的文件,也就是以重做日志的方式去存儲Redis中數據的變化
- 每次執(zhí)行服務器(定時)任務時,flushAppendOnlyFile函數都會被調用執(zhí)行兩個操作
- WRITE:將緩存寫入文件
- SAVE:將文件保存入磁盤
- 會對過時的更改日志進行刪除
- 內容是redis通訊協(xié)議(==RESP==)格式的命令文本存儲慨蛙。即存儲命令
- 每次執(zhí)行服務器(定時)任務時,flushAppendOnlyFile函數都會被調用執(zhí)行兩個操作
(3). 兩種持久化比較
RDB特點:
- 方便備份(直接將文件解壓,復制)
- 性能最大化,只占用子進程進行持久化
- 數據集很大的情況下啟動快速
- 安全性不高,宕機只能恢復上一次持久化的數據
- 數據集較大的情況下子進程的持久化可能會使服務有較大時間的阻塞
AOF特點:
- 高數據安全性,宕機不會丟失數據
- 提供了每秒同步,每修改同步和不同步,每秒同步會丟失一秒內的數據,每修改同步效率低下
- 如果日志過大,啟動初始化時間過長,會用新的文件存儲這個時間內的操作,一旦準備好第二個文件期贫,Redis會切換這兩個文件并開始追加到新的那一個
- 文件大小大于RDB
- 日志改寫:會刪除之前失效的日志
(4). RDB細節(jié)
1). 工作原理
? Redis調用fork會產生一個子進程,主進程將數據寫入一個臨時的RDB文件,寫入結束后替換掉舊的文件
2). SAVE和BGSAVE
? 有兩個命令可以生成RDB文件:SAVE(會阻塞主線程)和BGSAVE(在子線程中完成).實際創(chuàng)建RDB的工作由rdbSave完成,這兩個命令內部的調用細節(jié)不同.BGSAVE內部會創(chuàng)建子進程,子進程處理,父進程中會輪詢的等待子進程信號
? BGSAVE命令在子線程中生成RDB文件的過程中,主線程如果再次調用了SAVE和BGSAVE命令,會被拒絕.
? BGSAVE和BGREWRITEAOF命令不能同時執(zhí)行,會相互延遲執(zhí)行.(這里實際上不會出現(xiàn)什么問題,但是處于性能上的考慮,禁止同時執(zhí)行)
3). 文件載入
? 文件的載入工作在服務器啟動的時候自動執(zhí)行(檢測到RDB文件就會進行載入),并沒有專門用于載入RDB文件的命令.
? 如果服務器開啟了AOF持久化功能,那么會優(yōu)先使用AOF文件還原數據庫狀態(tài).
? RDB文件載入時,服務處于阻塞狀態(tài)
4). 自動間隔性保存
? Redis的默認設置:
save 900 1 //900秒內進行一次同步
save 300 10 //300秒內進行10次同步
save 60 10000 //60秒內進行10000次同步
? 當滿足以上條件時,會==執(zhí)行BGSAVE命令==.
? 服務器會根據配置文件中的該配置設置saveParams屬性數組:
struct saveParams{
// 秒數
time_t seconds;
// 修改數
int changes;
}
? Redis還會維持一個dirty計數器(上一次SAVE后產生的臟數據數),和一個lastsave屬性(距離上一次SAVE的時間).Redis會周期性(100毫秒)的執(zhí)行serverCron,來檢查是否達到上面的條件,如果滿足就調用BGSAVE
(5). AOF細節(jié)
1). AOF文件的存儲
? Redis調用flushAppendOnlyFile函數執(zhí)行WRITE(將緩存寫入內存中的AOF文件中)和SAVE(將AOF文件從內存持久化到磁盤)兩個工作.
? 支持三種工作方式:
- 每秒同步:==原則上==每秒進行一次同步,SAVE由子線程執(zhí)行,不會引起主線程恩阻塞
- 當進行同步時,子線程正在進行同步,如果子線程同步未超過2秒,那么跳過本次同步,如果超過,本次不進行SAVE(原因是本次的WRITE延遲,要避免影響到下一次同步)
- 子線程沒在進行同步,如果距離上一次同步不超過一秒,不進行SAVE
- 性能與安全性兼顧
- 每命令同步:每執(zhí)行一次任務同步一次,==SAVE是由主線程執(zhí)行的,會阻塞主線程==
- 安全性最高,但是效率被同步拉低
- 不同步:==Redis被關閉,AOF功能被關閉==,或者==系統(tǒng)緩存被刷新==時會阻塞主線程進行SAVE
- 宕機會丟失數據,但是不用進行同步所以效率最高
2). 文件讀取和數據還原
? ==AOF文件采用RESP通訊協(xié)議保存命令==.
? 只要根據AOF文件中的協(xié)議,重新執(zhí)行一遍AOF文件中的所有命令就可以還原Redis的數據了.
步驟:
- 創(chuàng)建一個不連接網絡的偽客戶端
- 讀取AOF文件,還原出命令以及參數
- 使用偽客戶端執(zhí)行這些命令
使用偽客戶端的原因是恢復數據不需要網絡,效果完全一樣.
3). AOF重寫
? BGREWRITEAOF命令
? Redis會在AOF文件中進行命令的重寫,==相當于合并命令到另一個文件==,這個過程在子線程中進行,主線程可以繼續(xù)處理命令請求.
? ==重寫期間的命令會寫入重寫緩沖區(qū)==,在重寫完成之后==追加在新AOF文件末尾==.
? 這個過程完成之后使用新的AOF文件代替原來的舊文件.
3. Redis通訊協(xié)議RESP
? RESP是Redis客戶端和服務端的一種通訊協(xié)議,請求格式都相同,使用數組搭配多行字符串.而返回有很多種
? 每一行消息是以\r\n結尾的,也就是分行
- 簡單字符串回復: " + "開頭
- 錯誤消息: " - "開頭
- 整型數字: " : "開頭
- 復雜字符串回復: " $ "開頭
- 數組格式回復: " * "開頭
(1). 請求格式
*3 // 這里星號指數組,后面數字代表數組長度,也就是命令的分段數,后面會緊跟3個多行字符串
$3 // 美元符號指多行字符串,后面數字代表字符串長度
SET // 這是多行字符串的內容
$3
KEY
$5
VALUE
(2). 響應格式
? 1234四種格式的消息或者5復合前面4中基礎格式的消息.
4. Redis的架構模式
(1). 單機版
? 多個client連接==一個Redis服務端==
? 容量有限,處理能力有限
(2). 主從復制
? 根據==一個主服務器復制出多個從服務器==,從服務器負責查詢,主服務器進行數據的添加刪除和修改.每當主服務器上的數據有變動時,會同步到從服務器上.
? 降低了master的讀壓力,但是沒有緩解寫壓力.
(3). 哨兵
? 在主從復制的基礎上==添加了哨兵機制,主服務器下線時進行故障轉移(將另一臺從服務器切換為主服務器來預防單點故障)==.
- 監(jiān)控:哨兵會不斷檢查主服務器和從服務器是否運作正常
- 提醒:當一臺服務器出現(xiàn)問題時,會通過API向管理員或者應用程序發(fā)送通知
- 自動故障遷移:當主服務器不能正常工作時,哨兵會進行故障轉移
? 優(yōu)點是自動故障遷移,保證穩(wěn)定性,缺點還是沒有緩解主服務器的寫壓力
(4). 集群(proxy)
? 使用代理進行服務的分發(fā)(通過hash).減緩各服務器的壓力.
? Twemproxy是Twitter開源的一個Redis和memcache輕量級代理服務器.
? 通過代理對象將寫請求分發(fā)到多個主服務器上,將讀請求分發(fā)到多個從服務器上.各個服務器之間進行同步.
? 優(yōu)點在于增加了各種算法,合理的分配服務,還支持故障節(jié)點的自動刪除.缺點是增加了新的proxy,需要維護.
(5). 集群(直接連接)
? Redis集群由對臺Redis服務器組成,這種直連方式對服務器部分主從.每個節(jié)點要處理部分寫請求和讀請求.通過同步進行統(tǒng)一.
? 優(yōu)點是可大量擴展,高可用(部分節(jié)點不可用時,整個集群還是工作的),自動故障處理.
? 缺點是資源隔離性較差,數據通過異步復制,不保證強一致性.
5. Redis分布式鎖
- setnx(key, value)
- “set if not exits”
- 若該key-value不存在迹冤,則成功加入緩存并且返回1泡徙,否則返回0堪藐。
- 相當于獲取鎖,如果key已經存在了,返回0
- expire(key, seconds)
- 設置key-value的有效期為seconds秒挑围。
- getset(key, value)
- 先進行get獲取原值,再設置新的值(用于解決死鎖)
setnx和expire中間出現(xiàn)故障的解決辦法:
-
放棄使用expire命令.將當前時間戳作為value存入此鎖中杉辙,通過當前時間戳和Redis中的時間戳進行對比,如果超過一定差值枫绅,認為鎖已經時效并淋,防止鎖無限期的鎖下去.如果兩個線程同時發(fā)現(xiàn)鎖超時,可能會同時獲取到鎖.這個問題通過getset()解決,通過getset原子操作保證只能有
while(jedis.setnx(lock, now+超時時間)==0){ if(now>jedis.get(lock) && now>jedis.getset(lock, now+超時時間)){ // 這里先判斷鎖是否過期 // 然后如果鎖過期了,嘗試競爭鎖,只有一個線程能成功正確的返回之前的過期時間 // 這時多個線程中的其他線程都會返回新的超時時間 // 這個超時時間被更改并不重要,主要就是用于防止永久鎖,問題不大 break; }else{ Thread.sleep(300); } } // 執(zhí)行業(yè)務代碼; jedis.del(lock);
-
合并命令
// redis6.2后可將上述兩步合并起來 set key value seconds milliseconds nx|xx // seconds:秒 // milliseconds:毫秒 // nx:只有鍵不存在時县耽,才對鍵進行設置操作 // xx:只有鍵存在時兔毙,才對鍵進行設置操作 // set操作成功完成時,返回ok锡溯,否則返回nil
6. 一致性哈希算法