Redisobject 對象
對象分為鍵對象和值對象
鍵對象一般是string類型
值對象可以是string,list,set,zset,hash
q:redisobj的結構
typedef struct redisObject { //類型 unsigned type:4; //編碼 unsigned encoding:4; //指向底層實現數據結構的指針 void *ptr; //引用計數,垃圾回收的時候使用int refcount;//最近被使用的時間,內存淘汰的時候用unsigned lru;
} robj;
q:數據類型,編碼和數據結構之間的對應 關系?
Redis對象和數據結構的關系
鍵總是一個字符串對象
而值可以是五種中的一種
type 命令 得到的結果就是值的類型
可以用object encoding命令查看編碼
list數據類型的編碼由linkedlist和ziplist編碼合并成了quicklist編碼
q: 通用數據類型命令
keys * //查看當前庫所有key (匹配:keys *1)exists key //判斷某個key是否存在,如果鍵存在則返回1,不存在則返回0:type key //查看你的key是什么類型del key //刪除指定的key數據,del是一個通用命令,無論值是什么數據結構類型,del命令都可以將其 刪除,返回結果為成功刪除鍵的個數,假設刪除一個不存在的鍵,就會返回 0unlink key //根據value選擇非阻塞刪除,僅將keys從keyspace元數據中刪除,真正的刪除會在后續異步操作。expire key 10 //10秒鐘:為給定的key設置過期時間ttl key //查看還有多少秒過期,-1表示永不過期,-2表示已過期select number //命令切換數據庫dbsize //查看當前數據庫的key的數量flushdb //清空當前庫flushall //通殺全部庫
命令在執行前都會判斷 參數是否是自己可以接收,否則會返回錯誤
數據類型
string 字符串
q: 特點
其value是字符串,不過根據字符串的格式不同,又可以分為3類:
-
string:普通字符串
-
int:整數類型,可以做自增、自減操作
-
float:浮點類型,可以做自增、自減操作
q: 適用場景?
String 的常見應用場景如下:
- 常規數據(比如 session、token、序列化后的對象、圖片的路徑)的緩存;
- 計數比如用戶單位時間的請求數(簡單限流可以用到)、頁面單位時間的訪問數;
- 分布式鎖(利用
SETNX key value
命令可以實現一個最簡易的分布式鎖);
q:常用命令?
set <key> <value> //添加鍵值對*NX:當數據庫中key不存在時,可以將key-value添加數據庫*XX:當數據庫中key存在時,可以將key-value添加數據庫,與NX參數互斥*EX:key的超時秒數*PX:key的超時毫秒數,與EX互斥get <key> //查詢對應鍵值append <key> <value> //將給定的<value> 追加到原值的末尾,返回長度strlen <key> //獲得值的長度setnx <key> <value> //只有在 key 不存在時 設置 key 的值incr <key> //將 key 中儲存的數字值增1 只能對數字值操作,如果為空,新增值為1decr <key> //將 key 中儲存的數字值減1 只能對數字值操作,如果為空,新增值為-1incrby / decrby <key> <步長> //將 key 中儲存的數字值增減。自定義步長。mset <key1><value1><key2><value2> ..... //同時設置一個或多個 key-value對 mget <key1><key2><key3> ..... //同時獲取一個或多個 value msetnx <key1><value1><key2><value2> ..... //同時設置一個或多個 key-value 對,當且僅當所有給定 key 都不存在。 原子性,有一個失敗則都失敗getrange <key><起始位置><結束位置> //得值的范圍,類似java中的substring,前包,后包setrange <key><起始位置><value> //用 <value> 覆寫<key>所儲存的字符串值,從<起始位置>開始(索引從0開始)。setex <key><過期時間><value> //設置鍵值的同時,設置過期時間,單位秒。getset <key><value> //以新換舊,設置了新值同時獲得舊值。
q:底層編碼方式和數據結構?
- 當存儲的是long 數字的時候,使用int編碼,prt直接存儲數字
不包括浮點數
- 當存儲的字符串小于44個字節的時候,使用embstr編碼,字符串和redisobject存儲在一起
- 當存儲的字符串大于44個字節的時候,使用raw編碼,prt存儲的是sds的地址指針
set 集合
q: 特點
-
無序
-
元素不可重復
-
查找快
-
支持交集、并集、差集等功能
q: 適用場景?
應用場景:
- 需要存放的數據不能重復的場景
- 不可重復下單
- 點贊
- 需要獲取多個數據源交集、并集和差集的場景
- 共同好友(交集)、共同粉絲(交集)、共同關注(交集)、好友推薦(差集)、音樂推薦(差集)、訂閱號推薦(差集+交集) 等場景。
- 需要隨機獲取數據源中的元素的場景
- 抽獎系統、隨機點名等場景。
- 相關命令:
SPOP
(隨機獲取集合中的元素并移除,適合不允許重復中獎的場景)、SRANDMEMBER
(隨機獲取集合中的元素,適合允許重復中獎的場景)。
q:常用命令?
sadd <key><value1><value2> ..... //將一個或多個 member 元素加入到集合 key 中,已經存在的 member 元素將被忽略smembers <key> //取出該集合的所有值。sismember <key><value> //判斷集合<key>是否為含有該<value>值,有1,沒有0scard<key> //返回該集合的元素個數。srem <key><value1><value2> .... //刪除集合中的某個元素。spop <key> //隨機從該集合中吐出一個值。srandmember <key><n> //隨機從該集合中取出n個值。不會從集合中刪除 。smove <source><destination>value //把集合中一個值從一個集合移動到另一個集合sinter <key1><key2> //返回兩個集合的交集元素。sunion <key1><key2> //返回兩個集合的并集元素。sdiff <key1><key2> //返回兩個集合的差集元素(key1中的,不包含key2中的)
q:底層編碼方式和數據結構?
- 當存儲的所有數據都是整數,元素數量不超過set-max-intset-entries時,Set會采用IntSet編碼,以節省內存,底層數據結構是intset
- 當存儲的所有數據不都是整數,或元素數量超過set-max-intset-entries時,set采用hashtable編碼,底層是Dict中的key用來存儲元素,value統一為null。
hash 哈希
q: 特點
hash也叫散列, 是一個鍵值對集合。
q: 適用場景?
hash特別適合用于存儲對象。
q:常用命令?
hset <key><field><value> <field2><value2> //給<key>集合中的 <field>鍵賦值<value>hget <key1><field> //從<key1>集合<field>取出 valuehmset <key1><field1><value1><field2><value2>... //批量設置hash的值,hmset被棄用,可以用hset做到hexists<key1><field> //查看哈希表 key 中,給定域 field 是否存在。hkeys <key> //列出該hash集合的所有fieldhvals <key> //列出該hash集合的所有valuehincrby <key><field><increment> //為哈希表 key 中的域 field 的值加上增量 1 -1hsetnx <key><field><value> //將哈希表 key 中的域 field 的值設置為 value ,當且僅當域 field 不存在 .
q:底層編碼方式和數據結構?
-
Hash結構默認采用ZipList編碼,用以節省內存。 ZipList中相鄰的兩個entry 分別保存field和value;底層數據結構式ziplist
-
當數據量較大時,Hash結構會轉為hashtable編碼,底層數據結構是Dict,觸發條件有兩個:
- ZipList中的元素數量超過了hash-max-ziplist-entries(默認512)
- ZipList中的任意entry大小超過了hash-max-ziplist-value(默認64字節)
節點過多,或單個節點過大
zset/sorted set 有序集合
q: 特點
- 無重復
- 有序
每個成員都關聯了一個評分(score),這個評分(score)被用來按照從最低分到最高分的方式排序集合中的成員。
集合的成員是唯一的,但是評分可以是重復了 。
q: 適用場景?
適合范圍或者排序的應用場景:
- 排行榜
q:常用命令?
zadd <key><score1><value1><score2><value2>… //將一個或多個 member 元素及其 score 值加入到有序集 key 當中。zrange <key><start><stop> [WITHSCORES] //返回有序集 key 中,下標在<start><stop>之間的元素,帶WITHSCORES,可以讓分數一起和值返回到結果集。zrangebyscore key minmax [withscores] [limit offset count] //返回有序集 key 中,所有 score 值介于 min 和 max 之間(包括等于 min 或 max )的成員。有序集成員按 score 值遞增(從小到大)次序排列。 zrevrangebyscore key maxmin [withscores] [limit offset count] //同上,改為從大到小排列。 zincrby <key><increment><value> // 為元素的score加上增量zrem <key><value> //刪除該集合下,指定值的元素 zcount <key><min><max> //統計該集合,分數區間內的元素個數 zrank <key><value> //返回該值在集合中的排名,從0開始。
q:底層編碼方式和數據結構?
- 當滿足下面條件時,采用ziplist編碼,底層數據結構是ziplist
- 元素數量小于zset_max_ziplist_entries,默認值128
- 每個元素都小于zset_max_ziplist_value字節,默認值64
ziplist本身沒有排序功能,而且沒有鍵值對的概念,因此需要有zset通過編碼實現:
- ZipList是連續內存,因此score和element是緊挨在一起的兩個entry, element在前,score在后
- score越小越接近隊首,score越大越接近隊尾,按照score值升序排列
- 否則采用zset編碼,底層是zset數據結構,zset的數據結構又指向skiplist和dict
- SkipList:可以排序,并且可以同時存儲score和ele值(member)
- Dict:可以鍵值存儲,并且可以根據key找value,實現O(1)的查找
二者實際上共用對象,不會造成內存的浪費
list 列表
q: 特點
雙向鏈表結構。既可以支持正向檢索和也可以支持反向檢索。
特征也與LinkedList類似:
- 單鍵多值
- 有序
- 元素可以重復
- 插入和刪除快
- 查詢速度一般
q: 適用場景?
應用場景:
- 常用來存儲一個有序數據,例如:朋友圈點贊列表,評論列表等。
- 可以用來做消息隊列,只是功能過于簡單且存在很多缺陷,不建議這樣做。
q:常用命令?
lpush/rpush <key><value1><value2><value3> //.... 從左邊/右邊插入一個或多個值。lpush是頭插法lpop/rpop <key> //從左邊/右邊吐出一個值。值在鍵在,值光鍵亡。rpoplpush <key1><key2>從<key1> //列表右邊吐出一個值,插到<key2>列表左邊。lrange <key><start><stop> //按照索引下標獲得元素(從左到右)lrange <key> 0 -1 //0左邊第一個,-1右邊第一個,(0 -1表示獲取所有)lindex <key><index> //按照索引下標獲得元素(從左到右)llen <key> //獲得列表長度 linsert <key> before/after <value><newvalue> //在<value>的前面、后面插入<newvalue>插入值lrem <key><n><value> //從左邊刪除n個value(從左到右)lset<key><index><value> //將列表key下標為index的值替換成value
q:底層編碼方式和數據結構?
- List的編碼方式是quicklist,底層數據結構為快速鏈表quickList,quicklist的節點又指向了ziplist
Redis 3.2 之前,List 底層實現是 LinkedList 或者 ZipList。 Redis 3.2 之后,引入了 LinkedList 和 ZipList 的結合 QuickList,List 的底層實現變為 QuickList。
首先在列表元素較少的情況下會使用一塊連續的內存存儲,這個結構是ziplist,也即是壓縮列表。(它將所有的元素緊挨著一起存儲,分配的是一塊連續的內存。)
當數據量比較多的時候才會改成quicklist。
因為普通的鏈表需要的附加指針空間太大,會比較浪費空間。比如這個列表里存的只是int類型的數據,結構上還需要兩個額外的指針prev和next。
Redis將鏈表和ziplist結合起來組成了quicklist。也就是將多個ziplist使用雙向指針串起來使用。這樣既滿足了快速的插入刪除性能,又不會出現太大的空間冗余。
數據結構
sds 簡單動態字符串
q:sds的結構
sds的結構
struct sdshdr {
//記錄buf數組中已使用字節的數量
//等于SDS所保存字符串的長度
int len;
//記錄buf數組中未使用字節的數量
int free;
//字節數組,用于保存字符串
char buf[];
};
q: sds和c字符串的區別
相同點:
- 都是用char數組來記錄字符,最后都有一個
\0
來代表字符串結束
sds最后也用
\0
代表結束,是為了重用c語言字符串的一些函數,例如printf打印,而不用重寫所有的函數
不同點
肯定是c語言字符串存在一定的缺陷,redis才會重寫,那么這些既是redis和c語言字符串的不同點,也是redis sds的優點
- 常數級別獲取字符串長度,sds獲取字符串長度的時間復雜度是O(1),c語言是O(n),通過len的冗余存儲來實現
- 杜絕緩沖區溢出,字符串在拼接之前可以做內存檢查,確保空間充足,否則進行擴充;不會像c語言一樣造成內存溢出
- 減少修改字符串時帶來的內存重分配次數,預分配空間free,來減少內存重分配的次數,可以提升性能
- 二進制安全,c語言字符串通過
\0
空字符來標志字符串結束,因此不能包含空字符;而sds通過len來表示字符串結束,可以包含空字符,可以存儲圖片等二進制信息,因此是二進制安全的
q: 容量擴充機制?
如圖中所示,內部為當前字符串實際分配的空間capacity一般要高于實際字符串長度len。
- 當字符串長度小于1M時,擴容都是加倍現有的空間
- 如果超過1M,擴容時一次只會多擴1M的空間。需要注意的是字符串最大長度為512M。
intset 正數集合
q: intset的結構
typedef struct intset { //編碼方式 uint32_t encoding; //集合包含的元素數量 uint32_t length; //保存元素的數組 int8_t contents[];
} intset;
- 數組升序排列
- 沒有重復
q: 如何升級?
當新元素很大的時候,集合要升級成更大的編碼方式
升級整數集合并添加新元素共分為三步進行:
1)根據新元素的類型,擴展整數集合底層數組的空間大小,并為新元素分配空間。
2)將底層數組現有的所有元素都轉換成與新元素相同的類型,并將類型轉換后的元素放置到正確的位上,而且在放置元素的過程中,需要繼續維持底層數組的有序性質不變。
3)將新元素添加到底層數組里面。
?升級操作為整數集合帶來了操作上的靈活性,并且盡可能地節約了內存。
?整數集合只支持升級操作,不支持降級操作。
dictht 哈希表
typedef struct dictht { //哈希表數組 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩碼,用于計算索引值 //總是等于size-1 unsigned long sizemask; //該哈希表已有節點的數量 unsigned long used;
} dictht;
哈希表結點
typedef struct dictEntry { //鍵 void *key; //值 union{ void *val; uint64_tu64; int64_ts64; } v; //指向下個哈希表節點,形成鏈表 struct dictEntry *next;
} dictEntry;
q:如何解決哈希沖突?
用拉鏈法的頭插法解決哈希沖突
dict 字典
typedef struct dict { //類型特定函數 dictType *type; //私有數據 void *privdata; //哈希表 dictht ht[2]; // rehash索引 //當rehash不在進行時,值為-1 in trehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
ht有兩個,一般只使用第一個,第二個哈希表只在rehash的時候用
插入數據的時候,先計算哈希值,再計算索引值,再插入到指定位置
q : 如何rehash?
隨著操作的不斷執行,哈希表保存的鍵值對會逐漸地增多或者減少,為了讓哈希表的負載因子(load factor)維持在一個合理的范圍之內,當哈希表保存的鍵值對數量太多或者太少時,程序需要對哈希表的大小進行相應的擴展或者收縮。
擴展和收縮哈希表的工作可以通過執行rehash(重新散列)操作來完成,Redis對字典的哈希表執行rehash的步驟如下:
1)為字典的ht[1]哈希表分配空間,這個哈希表的空間大小取決于要執行的操作,以及ht[0]當前包含的鍵值對數量(也即是ht[0].used屬性的值):
?如果執行的是擴展操作,那么ht[1]的大小為第一個大于等于ht[0].used*2的2 n(2的n次方冪);
?如果執行的是收縮操作,那么ht[1]的大小為第一個大于等于ht[0].used的2 n。
2)將保存在ht[0]中的所有鍵值對rehash到ht[1]上面:rehash指的是重新計算鍵的哈希值和索引值,然后將鍵值對放置到ht[1]哈希表的指定位置上。
3)當ht[0]包含的所有鍵值對都遷移到了ht[1]之后(ht[0]變為空表),釋放ht[0],將ht[1]設置為ht[0],并在ht[1]新創建一個空白哈希表,為下一次rehash做準備。
為ht[1]分配空間,復制ht[0]的數據到ht[1],釋放ht[0],把ht[1]設為ht[0],ht[1]創建一個空哈希表
q:什么時候rehash
哈希表的擴展與收縮
哈希表的擴展與收縮當以下條件中的任意一個被滿足時,程序會自動開始對哈希表執行擴展操作:
1)服務器目前沒有在執行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的負載因子大于等于1。
2)服務器目前正在執行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的負載因子大于等于5。
為什么rehash是漸進式?
如果ht[0]的數據非常多,那么把數據全部轉移到ht[1]將會非常耗費時間,因此這個過程是分多次,漸進式完成的
rehashidx記錄了正在轉移的索引下標,當轉移完成,會置為-1
因為在進行漸進式rehash的過程中,字典會同時使用ht[0]和ht[1]兩個哈希表,所以在漸進式rehash進行期間,字典的刪除(delete)、查找(find)、更新(update)等操作會在兩個哈希表上進行。例如,要在字典里面查找一個鍵的話,程序會先在ht[0]里面進行查找,如果沒找到的話,就會繼續到ht[1]里面進行查找,諸如此類。
另外,在漸進式rehash執行期間,新添加到字典的鍵值對一律會被保存到ht[1]里面,而ht[0]則不再進行任何添加操作,這一措施保證了ht[0]包含的鍵值對數量會只減不增,并隨著rehash操作的執行而最終變成空表。
ziplist 壓縮列表
q: ziplist的結構?
每個壓縮列表節點可以保存一個字節數組或者一個整數值
q: ziplist過大的時候有什么缺點?
當ziplist變得很?的時候,它有如下幾個缺點:
- 每次插?或修改引發的realloc操作會有更?的概率造成內存拷貝,從而降低性能。
- ?旦發生內存拷貝,內存拷貝的成本也相應增加,因為要拷貝更?的?塊數據。
- 當ziplist數據項過多的時候,在它上?查找指定的數據項就會性能變得很低,因為ziplist上的查找需要進行遍歷。
skiplist 跳表
skiplist是多層級不同跨度的鏈表
q: skiplist的結構?
zsikpList
typedef struct zskiplist { //表頭節點和表尾節點 structz skiplistNode *header, *tail; //表中節點的數量 unsigned long length; //表中層數最大的節點的層數 int level;
} zskiplist;
zskipListNode
typedef struct zskiplistNode { //層 struct zskiplistLevel { //前進指針 struct zskiplistNode *forward; //跨度 unsigned int span; } level[]; //后退指針 struct zskiplistNode *backward; //分值 double score; //成員對象 robj *obj;
} zskiplistNode;
每個結點的成員對象是唯一的,但是分值可以相同,分值相同就按照成員對象由小到大排序,整個鏈表都是按照分值由小到大排序
q: skiplist如何遍歷?
遍歷:
首先遍歷高層級跨度大的指針,如果過大,就遍歷下一層級
zset
q:zset的結構?
typedef struct zset { zskiplist *zsl; dict *dict;
} zset;
- SkipList:可以排序,并且可以同時存儲score和ele值(member)
- Dict:可以鍵值存儲,并且可以根據key找value,實現O(1)的查找
二者實際上共用對象,不會造成內存的浪費
quicklist 快表
q:quicklist的結構
quicklist 實際上是 zipList 和 linkedList 的混合體,它將 linkedList 按段切分,每一段使用 zipList 來緊湊存儲,多個 zipList 之間使用雙向指針串接起來。
typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; /* total count of all entries in all ziplists */ unsigned long len;
} quicklist;
typedef struct quicklistNode { struct quicklistNode *prev; //上一個node節點 struct quicklistNode *next; //下一個node unsigned char *zl; //保存的數據 壓縮前ziplist 壓縮后壓縮的數據 unsigned int sz; /* ziplist size in bytes */ unsigned int count : 16; /* count of items in ziplist */ } quicklistNode;