String 字符串
存儲類型
可以用來存儲字符串兰英、整數(shù)、浮點數(shù)供鸠。
操作命令
設(shè)置多個值(批量操作畦贸,原子性)
mset weishao 1 lbj 2
設(shè)置值,如果 key 存在楞捂,則不成功
setnx weishao
基于此可實現(xiàn)分布式鎖薄坏。用 del key 釋放鎖。 但如果釋放鎖的操作失敗了寨闹,導致其他節(jié)點永遠獲取不到鎖胶坠,怎么辦? 加過期時間繁堡。單獨用 expire 加過期沈善,也失敗了,無法保證原子性椭蹄,怎么辦闻牡?多參數(shù)
set key value [expiration EX seconds|PX milliseconds][NX|XX]
使用參數(shù)的方式
set lock1 1 EX 10 NX
(整數(shù))值遞增
incr weishao
incrby weishao 100
(整數(shù))值遞減
decr weishao
decrby weishao 100
浮點數(shù)增量
set f 2.6
incrbyfloat f 7.3
獲取多個值
mget weishao lbj
獲取值長度
strlen weishao
字符串追加內(nèi)容
append weishao 123
獲取指定范圍的字符
getrange weishao 0 5
存儲(實現(xiàn))原理
數(shù)據(jù)模型
set hello word 為例,因為Redis 是KV 的數(shù)據(jù)庫绳矩,它是通過hashtable 實現(xiàn)的(我們把這個叫做外層的哈希)罩润。所以每個鍵值對都會有一個dictEntry(源碼位置:dict.h),里面指向了key 和value 的指針翼馆。next 指向下一個dictEntry割以。
typedef struct dictEntry {
void *key; /* key 關(guān)鍵字定義*/
union {
void *val; uint64_t u64; /* value 定義*/
int64_t s64; double d;
} v;
struct dictEntry *next; /* 指向下一個鍵值對節(jié)點*/
} dictEntry;
key 是字符串,但是Redis 沒有直接使用C 的字符數(shù)組写妥,而是存儲在自定義的SDS中拳球。
value 既不是直接作為字符串存儲,也不是直接存儲在SDS 中珍特,而是存儲在redisObject 中祝峻。實際上五種常用的數(shù)據(jù)類型的任何一種,都是通過redisObject 來存儲的。
redisObject
redisObject 定義在src/server.h 文件中莱找。
typedef struct redisObject {
unsigned type:4; /* 對象的類型酬姆,包括:OBJ_STRING、OBJ_LIST奥溺、OBJ_HASH辞色、OBJ_SET、OBJ_ZSET */
unsigned encoding:4; /* 具體的數(shù)據(jù)結(jié)構(gòu)*/
unsigned lru:LRU_BITS; /* 24 位浮定,對象最后一次被命令程序訪問的時間相满,與內(nèi)存回收有關(guān)*/
int refcount; /* 引用計數(shù)。當refcount 為0 的時候桦卒,表示該對象已經(jīng)不被任何對象引用立美,則可以進行垃圾回收了*/
void *ptr; /* 指向?qū)ο髮嶋H的數(shù)據(jù)結(jié)構(gòu)*/
} robj;
可以使用type 命令來查看對外的類型。
127.0.0.1:6379> type weishao
string
內(nèi)部編碼
127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> set weishao "hello world hello world hello world hello world hello world hello world "
OK
127.0.0.1:6379> set lbj mvp
OK
127.0.0.1:6379> object encoding number
"int"
127.0.0.1:6379> object encoding lbj
"embstr"
127.0.0.1:6379> object encoding weishao
"raw"
字符串類型的內(nèi)部編碼有三種:
1方灾、int建蹄,存儲8 個字節(jié)的長整型(long,2^63-1)裕偿。
2洞慎、embstr, 代表embstr 格式的SDS(Simple Dynamic String 簡單動態(tài)字符串),存儲小于44 個字節(jié)的字符串嘿棘。
3劲腿、raw,存儲大于44 個字節(jié)的字符串(3.2 版本之前是39 字節(jié))鸟妙。
/* object.c */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
幾個疑問
問題1谆棱、什么是SDS?
Redis 中字符串的實現(xiàn)圆仔。
在3.2 以后的版本中,SDS 又有多種結(jié)構(gòu)(sds.h):sdshdr5蔫劣、sdshdr8坪郭、sdshdr16、sdshdr32脉幢、sdshdr64歪沃,用于存儲不同的長度的字符串,分別代表
2^5=32byte嫌松,
2^8=256byte沪曙,
2^16=65536byte=64KB,
2^32byte=4GB萎羔。
/* sds.h */
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 當前字符數(shù)組的長度*/
uint8_t alloc; /*當前字符數(shù)組總共分配的內(nèi)存大小*/
unsigned char flags; /* 當前字符數(shù)組的屬性液走、用來標識到底是sdshdr8 還是sdshdr16 等*/
char buf[]; /* 字符串真正的值*/
};
問題2、為什么Redis 要用SDS 實現(xiàn)字符串?
C 語言本身沒有字符串類型(只能用字符數(shù)組char[]實現(xiàn))缘眶。
1嘱根、使用字符數(shù)組必須先給目標變量分配足夠的空間,否則可能會溢出巷懈。
2该抒、如果要獲取字符長度,必須遍歷字符數(shù)組顶燕,時間復(fù)雜度是O(n)凑保。
3、C 字符串長度的變更會對字符數(shù)組做內(nèi)存重分配涌攻。
4欧引、通過從字符串開始到結(jié)尾碰到的第一個'\0'來標記字符串的結(jié)束,因此不能保存圖片癣漆、音頻维咸、視頻、壓縮文件等二進制(bytes)保存的內(nèi)容惠爽,二進制不安全癌蓖。
SDS 的特點:
1、不用擔心內(nèi)存溢出問題婚肆,如果需要會對SDS 進行擴容租副。
2、獲取字符串長度時間復(fù)雜度為O(1)较性,因為定義了len 屬性用僧。
3、通過“空間預(yù)分配”( sdsMakeRoomFor)和“惰性空間釋放”赞咙,防止多次重分配內(nèi)存责循。
4、判斷是否結(jié)束的標志是len 屬性(它同樣以'\0'結(jié)尾是因為這樣就可以使用C語言中函數(shù)庫操作字符串的函數(shù)了)攀操,可以包含'\0'院仿。
問題3、embstr 和raw 的區(qū)別速和?
embstr 的使用只分配一次內(nèi)存空間(因為RedisObject 和SDS 是連續(xù)的)歹垫,而raw需要分配兩次內(nèi)存空間(分別為RedisObject 和SDS 分配空間)。
因此與raw 相比颠放,embstr 的好處在于創(chuàng)建時少分配一次空間排惨,刪除時少釋放一次空間,以及對象的所有數(shù)據(jù)連在一起碰凶,尋找方便暮芭。
而embstr 的壞處也很明顯鹿驼,如果字符串的長度增加需要重新分配內(nèi)存時,整個RedisObject 和SDS 都需要重新分配空間谴麦,因此Redis 中的embstr 實現(xiàn)為只讀蠢沿。
問題4:int 和embstr 什么時候轉(zhuǎn)化為raw?
當int 數(shù)據(jù)不再是整數(shù)匾效, 或大小超過了long 的范圍(2^63-1=9223372036854775807)時舷蟀,自動轉(zhuǎn)化為embstr。
問題5:明明沒有超過閾值面哼,為什么變成raw 了野宜?
127.0.0.1:6379> set k2 a
OK
127.0.0.1:6379> object encoding k2
"embstr"
127.0.0.1:6379> append k2 b
(integer) 2
127.0.0.1:6379> object encoding k2
"raw"
對于embstr,由于其實現(xiàn)是只讀的魔策,因此在對embstr 對象進行修改時匈子,都會先轉(zhuǎn)化為raw 再進行修改。
因此闯袒,只要是修改embstr 對象虎敦,修改后的對象一定是raw 的,無論是否達到了44個字節(jié)政敢。
問題6:當長度小于閾值時其徙,會還原嗎?
關(guān)于Redis 內(nèi)部編碼的轉(zhuǎn)換喷户,都符合以下規(guī)律:編碼轉(zhuǎn)換在Redis 寫入數(shù)據(jù)時完成唾那,且轉(zhuǎn)換過程不可逆,只能從小內(nèi)存編碼向大內(nèi)存編碼轉(zhuǎn)換(但是不包括重新set)
問題7:為什么要對底層的數(shù)據(jù)結(jié)構(gòu)進行一層包裝呢褪尝?
通過封裝闹获,可以根據(jù)對象的類型動態(tài)地選擇存儲結(jié)構(gòu)和可以使用的命令,實現(xiàn)節(jié)省空間和優(yōu)化查詢速度河哑。
應(yīng)用場景
緩存
String 類型
例如:熱點數(shù)據(jù)緩存(例如報表避诽,明星出軌),對象緩存璃谨,全頁緩存茎用。
可以提升熱點數(shù)據(jù)的訪問速度。
數(shù)據(jù)共享分布式
STRING 類型睬罗,因為Redis 是分布式的獨立服務(wù),可以在多個應(yīng)用之間共享
例如:分布式Session
分布式鎖
STRING 類型setnx 方法旭斥,只有不存在時才能添加成功容达,返回true。
全局ID
INT 類型垂券,INCRBY花盐,利用原子性
計數(shù)器
INT 類型羡滑,INCR 方法
例如:文章的閱讀量,微博點贊數(shù)算芯,允許一定的延遲柒昏,先寫入Redis 再定時同步到數(shù)據(jù)庫。
限流
INT 類型熙揍,INCR 方法
以訪問者的IP 和其他信息作為key职祷,訪問一次增加一次計數(shù),超過次數(shù)則返回false届囚。
位統(tǒng)計
String 類型的BITCOUNT(1.6.6 的bitmap 數(shù)據(jù)結(jié)構(gòu)介紹)有梆。
字符是以8 位二進制存儲的。
set k1 a
setbit k1 6 1
setbit k1 7 0
get k1
a 對應(yīng)的ASCII 碼是97意系,轉(zhuǎn)換為二進制數(shù)據(jù)是01100001
b 對應(yīng)的ASCII 碼是98泥耀,轉(zhuǎn)換為二進制數(shù)據(jù)是01100010
因為bit 非常節(jié)省空間(1 MB=8388608 bit),可以用來做大數(shù)據(jù)量的統(tǒng)計蛔添。
例如:在線用戶統(tǒng)計痰催,留存用戶統(tǒng)計
setbit onlineusers 0 1
setbit onlineusers 1 1
setbit onlineusers 2 0
支持按位與、按位或等等操作迎瞧。
BITOP AND destkey key [key ...] 夸溶,對一個或多個key 求邏輯并,并將結(jié)果保存到destkey 夹攒。
BITOP OR destkey key [key ...] 蜘醋,對一個或多個key 求邏輯或,并將結(jié)果保存到destkey 咏尝。
BITOP XOR destkey key [key ...] 压语,對一個或多個key 求邏輯異或,并將結(jié)果保存到destkey 编检。
BITOP NOT destkey key 胎食,對給定key 求邏輯非,并將結(jié)果保存到destkey 允懂。
計算出7 天都在線的用戶
BITOP "AND" "7_days_both_online_users" "day_1_online_users" "day_2_online_users" ... "day_7_online_users"
Hash 哈希
存儲類型
包含鍵值對的無序散列表厕怜。value 只能是字符串,不能嵌套其他類型蕾总。
同樣是存儲字符串粥航,Hash 與String 的主要區(qū)別?
1生百、把所有相關(guān)的值聚集到一個key 中递雀,節(jié)省內(nèi)存空間
2、只使用一個key蚀浆,減少key 沖突
3缀程、當需要批量獲取值的時候搜吧,只需要使用一個命令,減少內(nèi)存/IO/CPU 的消耗
Hash 不適合的場景:
1杨凑、Field 不能單獨設(shè)置過期時間
2滤奈、沒有bit 操作
3、需要考慮數(shù)據(jù)量分布的問題(value 值非常大的時候撩满,無法分布到多個節(jié)點)
操作命令
hset h1 f 6
hset h1 e 5
hmset h1 a 1 b 2 c 3 d 4
hget h1 a
hmget h1 a b c d
hkeys h1
hvals h1
hgetall h1
key 操作
hget exists h1
hdel h1
hlen h1
存儲(實現(xiàn))原理
Redis 的Hash 本身也是一個KV 的結(jié)構(gòu)蜒程,類似于Java 中的HashMap。
外層的哈希(Redis KV 的實現(xiàn))只用到了hashtable鹦牛。當存儲hash 數(shù)據(jù)類型時搞糕,叫做內(nèi)層的哈希。內(nèi)層的哈希底層可以使用兩種數(shù)據(jù)結(jié)構(gòu)實現(xiàn):
ziplist:OBJ_ENCODING_ZIPLIST(壓縮列表)
hashtable:OBJ_ENCODING_HT(哈希表)
127.0.0.1:6379> hset h2 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
127.0.0.1:6379> hset h3 f aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
127.0.0.1:6379> object encoding h2
"ziplist"
127.0.0.1:6379> object encoding h3
"hashtable"
ziplist 壓縮列表
ziplist 壓縮列表是什么曼追?
/* ziplist.c 源碼頭部注釋*/
The ziplist is a specially encoded dually linked list that is designed to be very memory efficient. It stores both strings and
integer values, where integers are encoded as actual integers instead of a series of characters. It allows push and pop
operations on either side of the list in O(1) time. However, because every operation requires a reallocation of the memory
used by the ziplist, the actual complexity is related to the amount of memory used by the ziplist.
ziplist 是一個經(jīng)過特殊編碼的雙向鏈表窍仰,它不存儲指向上一個鏈表節(jié)點和指向下一個鏈表節(jié)點的指針,而是存儲上一個節(jié)點長度和當前節(jié)點長度礼殊,通過犧牲部分讀寫性能驹吮,來換取高效的內(nèi)存空間利用率,是一種時間換空間的思想晶伦。只用在字段個數(shù)少碟狞,字段值小的場景里面。
ziplist 的內(nèi)部結(jié)構(gòu)
ziplist.c 源碼第16 行的注釋:
* <zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
typedef struct zlentry {
unsigned int prevrawlensize; /* 上一個鏈表節(jié)點占用的長度*/
unsigned int prevrawlen; /* 存儲上一個鏈表節(jié)點的長度數(shù)值所需要的字節(jié)數(shù)*/
unsigned int lensize; /* 存儲當前鏈表節(jié)點長度數(shù)值所需要的字節(jié)數(shù)*/
unsigned int len; /* 當前鏈表節(jié)點占用的長度*/
unsigned int headersize; /* 當前鏈表節(jié)點的頭部大谢榕恪(prevrawlensize + lensize)族沃,即非數(shù)據(jù)域的大小*/
unsigned char encoding; /* 編碼方式*/
unsigned char *p; /* 壓縮鏈表以字符串的形式保存,該指針指向當前節(jié)點起始位置*/
} zlentry;
編碼encoding(ziplist.c 源碼第204 行)
#define ZIP_STR_06B (0 << 6) //長度小于等于63 字節(jié)
#define ZIP_STR_14B (1 << 6) //長度小于等于16383 字節(jié)
#define ZIP_STR_32B (2 << 6) //長度小于等于4294967295 字節(jié)
問題:什么時候使用ziplist 存儲泌参?
當hash 對象同時滿足以下兩個條件的時候脆淹,使用ziplist 編碼:
1)所有的鍵值對的健和值的字符串長度都小于等于64byte(一個英文字母一個字節(jié));
2)哈希對象保存的鍵值對數(shù)量小于512 個沽一。
/* src/redis.conf 配置*/
hash-max-ziplist-value 64 // ziplist 中最大能存放的值長度
hash-max-ziplist-entries 512 // ziplist 中最多能存放的entry 節(jié)點數(shù)量
/* 源碼位置:t_hash.c 盖溺,當達字段個數(shù)超過閾值,使用HT 作為編碼*/
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
/*源碼位置: t_hash.c铣缠,當字段值長度過大烘嘱,轉(zhuǎn)為HT */
for (i = start; i <= end; i++) {
if (sdsEncodedObject(argv[i]) && sdslen(argv[i]->ptr) > server.hash_max_ziplist_value){
hashTypeConvert(o, OBJ_ENCODING_HT);
break;
}
}
一個哈希對象超過配置的閾值(鍵和值的長度有>64byte,鍵值對個數(shù)>512 個)時蝗蛙,會轉(zhuǎn)換成哈希表(hashtable)
hashtable(dict)
在Redis 中蝇庭,hashtable 被稱為字典(dictionary),它是一個數(shù)組+鏈表的結(jié)構(gòu)捡硅。
源碼位置:dict.h
前面我們知道了遗契,Redis 的KV 結(jié)構(gòu)是通過一個dictEntry 來實現(xiàn)的。
Redis 又對dictEntry 進行了多層的封裝病曾。
typedef struct dictEntry {
void *key; /* key 關(guān)鍵字定義*/
union {
void *val; uint64_t u64; /* value 定義*/
int64_t s64; double d;
} v;
struct dictEntry *next; /* 指向下一個鍵值對節(jié)點*/
} dictEntry;
dictEntry 放到了dictht(hashtable 里面):
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table; /* 哈希表數(shù)組*/
unsigned long size; /* 哈希表大小*/
unsigned long sizemask; /* 掩碼大小牍蜂,用于計算索引值√┩浚總是等于size-1 */
unsigned long used; /* 已有節(jié)點數(shù)*/
} dictht;
ht 放到了dict 里面:
typedef struct dict {
dictType *type; /* 字典類型*/
void *privdata; /* 私有數(shù)據(jù)*/
dictht ht[2]; /* 一個字典有兩個哈希表*/
long rehashidx; /* rehash 索引*/
unsigned long iterators; /* 當前正在使用的迭代器數(shù)量*/
} dict;
從最底層到最高層dictEntry——dictht——dict——OBJ_ENCODING_HT
總結(jié):哈希的存儲結(jié)構(gòu)
注意:dictht 后面是NULL 說明第二個ht 還沒用到鲫竞。dictEntry*后面是NULL 說明沒有hash 到這個地址。dictEntry 后面是NULL 說明沒有發(fā)生哈希沖突逼蒙。
問題:為什么要定義兩個哈希表呢从绘?ht[2]
redis 的hash 默認使用的是ht[0],ht[1]不會初始化和分配空間是牢。
哈希表dictht 是用鏈地址法來解決碰撞問題的僵井。在這種情況下,哈希表的性能取決于它的大胁道狻(size 屬性)和它所保存的節(jié)點的數(shù)量(used 屬性)之間的比率:
- 比率在1:1 時(一個哈希表ht 只存儲一個節(jié)點entry)批什,哈希表的性能最好;
- 如果節(jié)點數(shù)量比哈希表的大小要大很多的話(這個比例用ratio 表示社搅,5 表示平均一個ht 存儲5 個entry)驻债,那么哈希表就會退化成多個鏈表,哈希表本身的性能優(yōu)勢就不再存在形葬。
在這種情況下需要擴容合呐。Redis 里面的這種操作叫做rehash。
rehash 的步驟:
1笙以、為字符ht[1]哈希表分配空間淌实,這個哈希表的空間大小取決于要執(zhí)行的操作,以及ht[0]當前包含的鍵值對的數(shù)量猖腕。
擴展:ht[1]的大小為第一個大于等于ht[0].used*2拆祈。
2、將所有的ht[0]上的節(jié)點rehash 到ht[1]上谈息,重新計算hash 值和索引缘屹,然后放入指定的位置。
3侠仇、當ht[0]全部遷移到了ht[1]之后轻姿,釋放ht[0]的空間,將ht[1]設(shè)置為ht[0]表逻炊,并創(chuàng)建新的ht[1]互亮,為下次rehash 做準備。
問題:什么時候觸發(fā)擴容余素?
負載因子(源碼位置:dict.c):
static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;
ratio = used / size豹休,已使用節(jié)點與字典大小的比例
dict_can_resize 為1 并且dict_force_resize_ratio 已使用節(jié)點數(shù)和字典大小之間的比率超過1:5,觸發(fā)擴容
應(yīng)用場景
String 可以做的事情桨吊,Hash 都可以做威根。
存儲對象類型的數(shù)據(jù)
比如對象或者一張表的數(shù)據(jù)凤巨,比String 節(jié)省了更多key 的空間,也更加便于集中管理洛搀。
購物車
key:用戶id敢茁;field:商品id;value:商品數(shù)量留美。
+1:hincr彰檬。-1:hdecr。刪除:hdel谎砾。全選:hgetall逢倍。商品數(shù):hlen。
List 列表
操作命令
元素增減:
lpush queue a
lpush queue b c
rpush queue d e
lpop queue
rpop queue
blpop queue
brpop queue
取值
lindex queue 0
lrange queue 0 -1
存儲(實現(xiàn))原理
在早期的版本中景图,數(shù)據(jù)量較小時用ziplist 存儲较雕,達到臨界值時轉(zhuǎn)換為linkedlist 進行存儲,分別對應(yīng)OBJ_ENCODING_ZIPLIST 和OBJ_ENCODING_LINKEDLIST 症歇。
3.2 版本之后郎笆,統(tǒng)一用quicklist 來存儲。quicklist 存儲了一個雙向鏈表忘晤,每個節(jié)點都是一個ziplist宛蚓。
127.0.0.1:6379> object encoding queue
"quicklist"
quicklist
quicklist(快速列表)是ziplist 和linkedlist 的結(jié)合體。
quicklist.h设塔,head 和tail 指向雙向鏈表的表頭和表尾
typedef struct quicklist {
quicklistNode *head; /* 指向雙向鏈表的表頭*/
quicklistNode *tail; /* 指向雙向鏈表的表尾*/
unsigned long count; /* 所有的ziplist 中一共存了多少個元素*/
unsigned long len; /* 雙向鏈表的長度凄吏,node 的數(shù)量*/
int fill : 16; /* fill factor for individual nodes */
unsigned int compress : 16; /* 壓縮深度,0:不壓縮闰蛔; */
} quicklist;
quicklistNode 中的*zl 指向一個ziplist痕钢,一個ziplist 可以存放多個元素。
typedef struct quicklistNode {
struct quicklistNode *prev; /* 前一個節(jié)點*/
struct quicklistNode *next; /* 后一個節(jié)點*/
unsigned char *zl; /* 指向?qū)嶋H的ziplist */
unsigned int sz; /* 當前ziplist 占用多少字節(jié)*/
unsigned int count : 16; /* 當前ziplist 中存儲了多少個元素序六,占16bit(下同)任连,最大65536 個*/
unsigned int encoding : 2; /* 是否采用了LZF 壓縮算法壓縮節(jié)點,1:RAW 2:LZF */
unsigned int container : 2; /* 2:ziplist例诀,未來可能支持其他結(jié)構(gòu)存儲*/
unsigned int recompress : 1; /* 當前ziplist 是不是已經(jīng)被解壓出來作臨時使用*/
unsigned int attempted_compress : 1; /* 測試用*/
unsigned int extra : 10; /* 預(yù)留給未來使用*/
} quicklistNode;
ziplist 的結(jié)構(gòu)前面已經(jīng)說過了随抠,不再重復(fù)。
應(yīng)用場景
用戶消息時間線timeline
因為List 是有序的繁涂,可以用來做用戶時間線
消息隊列
List 提供了兩個阻塞的彈出操作:BLPOP/BRPOP拱她,可以設(shè)置超時時間。
BLPOP:BLPOP key1 timeout 移出并獲取列表的第一個元素扔罪, 如果列表沒有元素會阻塞列表直到等待超時或發(fā)現(xiàn)可彈出元素為止秉沼。
BRPOP:BRPOP key1 timeout 移出并獲取列表的最后一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發(fā)現(xiàn)可彈出元素為止。
隊列:先進先出:rpush blpop唬复,左頭右尾矗积,右邊進入隊列,左邊出隊列敞咧。
棧:先進后出:rpush brpop
Set 集合
存儲類型
String 類型的無序集合漠魏,最大存儲數(shù)量2^32-1(40 億左右)。
操作命令
添加一個或者多個元素
sadd myset a b c d e f g
獲取所有元素
smembers myset
統(tǒng)計元素個數(shù)
scard myset
隨機獲取一個元素
srandmember key
隨機彈出一個元素
spop myset
移除一個或者多個元素
srem myset d e f
查看元素是否存在
sismember myset a
存儲(實現(xiàn))原理
Redis 用intset 或hashtable 存儲set妄均。如果元素都是整數(shù)類型,就用inset 存儲哪自。
如果不是整數(shù)類型丰包,就用hashtable(數(shù)組+鏈表的存來儲結(jié)構(gòu))。
問題:KV 怎么存儲set 的元素壤巷?key 就是元素的值邑彪,value 為null。
如果元素個數(shù)超過512 個胧华,也會用hashtable 存儲寄症。
配置文件redis.conf
set-max-intset-entries 512
127.0.0.1:6379> sadd iset 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379> object encoding iset
"intset"
127.0.0.1:6379> sadd myset a b c d e f
(integer) 6
127.0.0.1:6379> object encoding myset
"hashtable"
應(yīng)用場景
抽獎
隨機獲取元素
spop myset
點贊、簽到矩动、打卡
這條微博的ID 是t1001有巧,用戶ID 是u3001。
用like:t1001 來維護t1001 這條微博的所有點贊用戶悲没。
點贊了這條微博:sadd like:t1001 u3001
取消點贊:srem like:t1001 u3001
是否點贊:sismember like:t1001 u3001
點贊的所有用戶:smembers like:t1001
點贊數(shù):scard like:t1001
比關(guān)系型數(shù)據(jù)庫簡單許多篮迎。
商品標簽
sadd tags:i5001 畫面清晰細膩
sadd tags:i5001 真彩清晰顯示屏
sadd tags:i5001 流暢至極
商品篩選
獲取差集
獲取交集
獲取并集
ZSet 有序集合
存儲類型
sorted set,有序的set示姿,每個元素有個score甜橱。
score 相同時,按照key 的ASCII 碼排序栈戳。
數(shù)據(jù)結(jié)構(gòu)對比:
操作命令
添加元素
zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
獲取全部元素
zrange myzset 0 -1 withscores
zrevrange myzset 0 -1 withscores
根據(jù)分值區(qū)間獲取元素
zrangebyscore myzset 20 30
移除元素
也可以根據(jù)score rank 刪除
zrem myzset php cpp
統(tǒng)計元素個數(shù)
zcard myzset
分值遞增
zincrby myzset 5 python
根據(jù)分值統(tǒng)計個數(shù)
zcount myzset 20 60
獲取元素rank
zrank myzset java
獲取元素score
zsocre myzset java
也有倒序的rev 操作(reverse)
存儲(實現(xiàn))原理
同時滿足以下條件時使用ziplist 編碼:
- 元素數(shù)量小于128 個
- 所有member 的長度都小于64 字節(jié)
在ziplist 的內(nèi)部岂傲,按照score 排序遞增來存儲。插入的時候要移動之后的數(shù)據(jù)子檀。
對應(yīng)redis.conf 參數(shù):
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
超過閾值之后镊掖,使用skiplist+dict 存儲。
問題:什么是skiplist命锄?
我們先來看一下有序鏈表:
在這樣一個鏈表中堰乔,如果我們要查找某個數(shù)據(jù),那么需要從頭開始逐個進行比較脐恩,直到找到包含數(shù)據(jù)的那個節(jié)點镐侯,或者找到第一個比給定數(shù)據(jù)大的節(jié)點為止(沒找到)。
也就是說,時間復(fù)雜度為O(n)苟翻。同樣韵卤,當我們要插入新數(shù)據(jù)的時候,也要經(jīng)歷同樣的查找過程崇猫,從而確定插入位置沈条。
而二分查找法只適用于有序數(shù)組,不適用于鏈表诅炉。
假如我們每相鄰兩個節(jié)點增加一個指針(或者理解為有三個元素進入了第二層)蜡歹,讓指針指向下下個節(jié)點。
這樣所有新增加的指針連成了一個新的鏈表涕烧,但它包含的節(jié)點個數(shù)只有原來的一半(上圖中是7, 19, 26)月而。在插入一個數(shù)據(jù)的時候,決定要放到那一層议纯,取決于一個算法(在redis 中t_zset.c 有一個zslRandomLevel 這個方法)驼卖。
現(xiàn)在當我們想查找數(shù)據(jù)的時候步责,可以先沿著這個新鏈表進行查找络凿。當碰到比待查數(shù)據(jù)大的節(jié)點時觉既,再回到原來的鏈表中的下一層進行查找。比如阀参,我們想查找23肝集,查找的路徑是沿著下圖中標紅的指針所指向的方向進行的:
- 23 首先和7 比較,再和19 比較结笨,比它們都大包晰,繼續(xù)向后比較。
- 但23 和26 比較的時候炕吸,比26 要小伐憾,因此回到下面的鏈表(原鏈表),與22比較赫模。
- 23 比22 要大树肃,沿下面的指針繼續(xù)向后和26 比較。23 比26 小瀑罗,說明待查數(shù)據(jù)23 在原鏈表中不存在
在這個查找過程中胸嘴,由于新增加的指針,我們不再需要與鏈表中每個節(jié)點逐個進行比較了斩祭。需要比較的節(jié)點數(shù)大概只有原來的一半劣像。這就是跳躍表。
為什么不用AVL 樹或者紅黑樹摧玫?因為skiplist 更加簡潔耳奕。
應(yīng)用場景
排行榜
id 為6001 的新聞點擊數(shù)加1:zincrby hotNews:20190926 1 n6001
獲取今天點擊最多的15 條:zrevrange hotNews:20190926 0 15 withscores
——學自咕泡學院