目錄
Redis 基礎
Redis 定位 - 特性
關系型數據庫 特性
# 特點
- 它以表格的形式,基於行存儲數據,是一個二維的模式;
- 它存儲的是結構化的數據,數據存儲有固定的模式(schema),數據需要適應表結構;
- 表與表之間存在關聯(Relationship);
- 大部分關系型數據庫都支持 SQL(結構化查詢語言)的操作,支持復雜的關聯查詢;
- 通過支持事務ACID(酸)來提供嚴格或者實時的數據一致性。
# 缺點
- 實現擴容技術復雜,分為Scale-up(縱向擴展)和Scale-out(橫向擴展);
- 表結構修改困難,因此存儲的數據格式也受到限制;
- 在高並發和高數據量的情況下,關系型數據庫通常會把數據持久化到磁盤,基於磁盤的讀寫壓力比較大。
非關系型數據庫 特性
# 特點
- 存儲非結構化的數據,比如文本、圖片、音頻、視頻;
- 表與表之間沒有關聯,可擴展性強;
- 保證數據的最終一致性。遵循 BASE(鹼)理論。Basically Available(基本可用),Soft-state(軟狀態),Eventually Consistent(最終一致性);
- 支持海量數據的存儲和高並發的高效讀寫;
- 支持分布式,能夠對數據進行分片存儲,擴縮容簡單。
Redis 特性
- Redis 是使⽤ C 語⾔開發的數據庫,與傳統數據庫不同的是 Redis 的數據是存在內存中的,Redis是內存數據庫,所以讀寫速度⾮常快,因此 Redis 被⼴泛應⽤於緩存⽅向;
- 豐富的數據類型;
- 功能豐富,如分布式鎖、持久化、過期策略;
Redis 安裝 - 啟動 - 使用
Redis 安裝
Redis 啟動
Redis 使用
- 切換數據庫
select 0
- 清空當前數據庫
flushdb
- 清空所有數據庫
flushall
- 查看key對外類型
type key
- 查看key內部類型
object encoding key
- 查看生成快照時間
lastsave
Redis 數據類型
對象 | 對象type屬性值 | type命令輸出 | 底層的存儲結構 | object encoding |
---|---|---|---|---|
字符串對象 (String) | OBJ_STRING | string | OBJ_ENCODING_INT OBJ_ENCODING_EMBSTR OBJ_ENCODING_RAW |
int embstr raw |
列表對象 (list) | OBJ_LIST | list | OBJ_ENCODING_QUICKLIST | quicklist |
哈希對象 (Hash) | OBJ_HASH | hash | OBJ_ENCODING_ZIPLIST OBJ_ENCODING_HT |
ziplist hashtable |
集合對象 (Sets) | OBJ_SET | set | OBJ_ENCODING_INTSET OBJ_ENCODING_HT |
intset hashtable |
有序集合對象 (Sorted Sets) | OBJ_ZSET | zset | OBJ_ENCODING_ZIPLIST OBJ_ENCODING_SKIPLIST |
ziplist skiplist(包含ht) |
字符串 (String)
String - 使用
- 字符串是一種最基本的Redis值類型。Redis字符串是二進制安全的,這意味着一個Redis字符串能包含任意類型的數據,例如:一張JPEG格式的圖片或者一個序列化的Ruby對象。
# 可使用命令
- SET: ;
- SETNX:
- SETEX
- PSETEX
- GET
- GETSET
- STRLEN
- APPEND
- SETRANGE
- GETRANGE
- INCR
- INCRBY
- INCRBYFLOAT
- DECR
- DECRBY
- MSET
- MSETNX
- MGET
- set - get
將字符串值 value 關聯到 key,如果 key 已經持有其他值,SET 就覆寫舊值,無視類型;
# 可選參數
- EX seconds: 將鍵的過期時間設置為 seconds 秒。執行 SET key value EX seconds 的效果等同於執行 SETEX key seconds value;
- PX milliseconds : 將鍵的過期時間設置為 milliseconds 毫秒。執行 SET key value PX milliseconds 的效果等同於執行 PSETEX key milliseconds value;
- NX : 只在鍵不存在時,才對鍵進行設置操作。執行 SET key value NX 的效果等同於執行 SETNX key value ;
- XX : 只在鍵已經存在時,才對鍵進行設置操作。
# 設置key value
> set k1 v2
OK
> get k1
"v1"
# 設置 EX(過期時間 expire time 單位:S)
> SET key-with-expire-time "hello" EX 10086
OK
> GET key-with-expire-time
"hello"
> TTL key-with-expire-time
(integer) 10069
# 使用 NX選項
> SET not-exists-key "value" NX
OK # 鍵不存在,設置成功
> GET not-exists-key
"value"
> SET not-exists-key "new-value" NX
(nil) # 鍵已經存在,設置失敗
> GEt not-exists-key
"value" # 維持原值不變
# 使用 XX選項
> EXISTS exists-key
(integer) 0
> SET exists-key "value" XX
(nil) # 因為鍵不存在,設置失敗
> SET exists-key "value"
OK # 先給鍵設置一個值
> SET exists-key "new-value" XX
OK # 設置新值成功
> GET exists-key
"new-value"
- incr - decr
# 設置數字 incr(為鍵 key 儲存的數字值加上1)
> SET page_view 20
OK
> INCR page_view
(integer) 21
> GET page_view # 數字值在 Redis 中以字符串的形式保存
"21"
# 設置數字 decr(為鍵 key 儲存的數字值減去1)
redis> SET failure_times 10
OK
redis> DECR failure_times
(integer) 9
- mset - mget
> mset key1 value1 key2 value2
OK
> mget key1 key2
1) "value1"
2) "value2"
- 其它
# STRLEN key(返回鍵 key 儲存的字符串值的長度)
> SET mykey "Hello world"
OK
> STRLEN mykey
(integer) 11
# APPEND key value(如果鍵 key 已經存在並且它的值是一個字符串,APPEND 命令將把 value 追加到鍵 key 現有值的末尾)
> EXISTS myphone # 確保 myphone 不存在
(integer) 0
> APPEND myphone "nokia" # 對不存在的 key 進行 APPEND ,等同於 SET myphone "nokia"
(integer) 5
> APPEND myphone " - 1110" # 對已存在的字符串進行 APPEN 長度從 5 個字符增加到 12 個字符
(integer) 12
> GET myphone
"nokia - 1110"
String - 原理
dict.h
typedef struct dictEntry {
void *key; /* key 關鍵字定義 */
union {
void *val; /* value 定義 */
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; /* 指向下一個鍵值對節點 */
} dictEntry;
server.h
typedef struct redisObject {
unsigned type:4; /* 對象的類型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET */
unsigned encoding:4; /* 具體的數據結構 */
unsigned lru:LRU_BITS; /* 24 位,對象最后一次被命令程序訪問的時間,與內存回收有關 */
/*
* LRU time (relative to global lru_clock)
* or LFU data (least significant 8 bits frequency
* and most significant 16 bits access time).
*/
int refcount;/* 引用計數。當 refcount 為 0 的時候,表示該對象已經不被任何對象引用,則可以進行垃圾回收了 */
void *ptr;/* 指向對象實際的數據結構 */
} robj;
sds.h
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
...
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used 當前字符數組使用長度 */
uint32_t alloc; /* excluding the header and null terminator 當前字符數組總共分配的內存大小*/
unsigned char flags; /* 3 lsb of type, 5 unused bits 當前字符數組的屬性、用來標識到底是 sdshdr8、sdshdr32等*/
char buf[]; /* 字符串存儲的值 */
};
...
- 字符串類型內部編碼
# 字符串類型內部編碼
- int: 存儲8個字節的長整型;
- embstr: 代表embstr格式的sds(Simple Dynamic String 簡單動態字符串),存儲小於44個字節的字符串;
- raw: 存儲大於44個字節的字符串(3.2版本之前39字節)。#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
# String 類型內部編碼查看
> set number 1
OK
> set str "this strtype is embstr "
OK
> 127.0.0.1:6379> set rawstr "this strtype is embstr aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
OK
> type number
string
> type str
string
> type rawstr
string
> OBJECT encoding number
"int"
> OBJECT encoding str
"embstr"
> OBJECT encoding rawstr
"raw"
String - 問題
- SDS是什么?
SDS: 全稱Simple Dynamic String(簡單動態字符串),Redis 中字符串的實現;
- Redis 為什么使用SD實現字符串
# C語言字符串(使用字符數組char[]實現)
- 字符數組必須先給目標變量分配足夠的空間,否則可能會溢出;
- 要獲取字符串使用長度,必須遍歷字符數組,時間復雜度是 O(n);
- 字符串長度的變更會對字符數組做內存重分配;
- 通過從字符串開始到結尾碰到的第一個'\0'來標記字符串的結束,因此不能保存圖片、音頻、視頻、壓縮文件等二進制(bytes)保存的內容,二進制不安全。
# Redis字符串(內部實現為SDS)
- 不用擔心內存溢出問題,如果需要會對 SDS 進行擴容;
- 獲取字符串使用長度時間復雜度為 O(1),因為結構體sdshdr32定義了 len 屬性;
- 通過“空間預分配” (sdsMakeRoomFor)和“惰性空間釋放”,防止多次重分配內存;
- 判斷是否結束的標志是 len 屬型(同樣以'\0'結尾是因為這樣就可以使用C語言中函數庫操作字符串的函數);
C 字符串 | Redis 字符串 |
---|---|
獲取字符串長度的復雜度為 O(N) | 獲取字符串長度的復雜度為 O(1) |
API 是不安全的,可能會造成緩沖區溢出 API 是安全的 | API安全,不會造成個緩沖區溢出 |
修改字符串長度 N 次必然需要執行 N 次內存重分配 | 修改字符串長度 N 次最多需要執行 N 次內存重分配 |
只能保存文本數據 | 可以保存文本或者二進制數據 |
可以使用所有<string.h>庫中的函數 | 可以使用一部分<string.h>庫中的函數 |
- embstr和raw區別
- embstr 使用只分配一次內存空間(因為 RedisObject 和 SDS 是連續的),而 raw需要分配兩次內存空間(分別為 RedisObject 和 SDS 分配空間);
- embstr優點: 創建時少分配一次空間,刪除時少釋放一次空間,以及對象的所有數據連在一起,尋找方便;
- embstr缺點: 如果字符串的長度增加需要重新分配內存時,整個RedisObject 和 SDS 都需要重新分配空間,因此 Redis 中的 embstr實現為只讀。
- int和embstr如何轉為raw
# 轉換條件
int -> raw : 當int數據不再是整數,或大小超過了long的范圍(2^63 - 1)時,自動轉為raw;
embstr -> raw : 當empstr執行append操作時,自動轉為raw。
# 演示過程
> set number 1
OK
> set embstr "this strtype is embstr"
OK
> object encoding number
"int"
> object encoding embstr
"embstr"
> append number a
(integer) 2
> append embstr append
(integer) 28
> object encoding number
"raw"
> object encoding embstr
"raw"
- 存儲小於44個字節的字符串,為什么變成raw
# 演示過程
> set k2 a
OK
> OBJECT encoding k2
"embstr"
> append k2 b
(integer) 2
> OBJECT encoding k2
"raw"
# 結論
- 對於 embstr,由於其實現是只讀的,因此在對 embstr 對象進行修改時,都會先轉化為 raw 再進行修改。
- 當字符串長度小於44時,為什么不會恢復成embstr
關於 Redis 內部編碼的轉換,都符合以下規律:編碼轉換在 Redis 寫入數據時完成,且轉換過程不可逆,只能從小內存編碼向大內存編碼轉換(但是不包括重新 set)。
- 為什么對底層的數據結構進行一層包裝
通過封裝,可以根據對象的類型動態地選擇存儲結構和可以使用的命令,實現節省空間和優化查詢速度。
String - 應用場景
- 把字符串當作原子計數器使用,如用戶訪問次數、熱點文章點贊、轉發等;
- 熱點數據緩存;
- 數據共享分布式;
- 分布式鎖;
- 全局 ID;
- 限流;
- 位統計;
- 在小空間里編碼大量數據;
哈希表 (Hash)
- Redis Hashes是字符串字段和字符串值之間的映射,所以它們是完美的表示對象(eg:一個有名,姓,年齡等屬性的用戶)的數據類型。
# 可使用命令
- HSET
- HSETNX
- HGET
- HEXISTS
- HDEL
- HLEN
- HSTRLEN
- HINCRBY
- HINCRBYFLOAT
- HMSET
- HMGET
- HKEYS
- HVALS
- HGETALL
- HSCAN
Hash - 使用
- hset - hget
> HSET website google "www.g.cn"
(integer) 1
> HGET website google
"www.g.cn"
- hsetnx - hget
> HSETNX database key-value-store Redis
(integer) 1
> HGET database key-value-store
"Redis"
- hmset - hmget
> hmset user:1000 username antirez birthyear 1977 verified 1
OK
> hget user:1000 username
"antirez"
> hget user:1000 birthyear
"1977"
> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"
- 其它
# hkeys 返回哈希表 key 中的所有域
> HMSET website google www.google.com yahoo www.yahoo.com
OK
> HKEYS website
1) "google"
2) "yahoo"
# hexists 返回哈希表 key 中是否存在的Key
> hexists website google
(integer) 1
Hash - 原理
redis.conf
# Hashes are encoded using a memory efficient data structure when they have a
# small number of entries, and the biggest entry does not exceed a given
# threshold. These thresholds can be configured using the following directives.
hash-max-ziplist-entries 512 /* ziplist 中最多能存放的 entry 節點數 */
hash-max-ziplist-value 64 /* ziplist 中最大能存放的值長度 */
t_hash.c
/* Check if the ziplist needs to be converted to a hash table */
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
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.
*/
/*
* The general layout of the ziplist is as follows
* <zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
*/
typedef struct zlentry {
/* 上一個鏈表節點占用的長度 */
unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
/* 存儲上一個鏈表節點的長度數值所需要的字節數 */
unsigned int prevrawlen; /* Previous entry len. */
/* 存儲上一個鏈表節點的長度數值所需要的字節數 */
unsigned int lensize; /* Bytes used to encode this entry type/len.
For example strings have a 1, 2 or 5 bytes
header. Integers always use a single byte.*/
/* 當前鏈表節點占用的長度 */
unsigned int len; /* Bytes used to represent the actual entry.
For strings this is just the string length
while for integers it is 1, 2, 3, 4, 8 or
0 (for 4 bit immediate) depending on the
number range. */
/* 當前鏈表節點的頭部大小(prevrawlensize + lensize),即非數據域的大小 */
unsigned int headersize; /* prevrawlensize + lensize. */
/* 編碼方式 */
unsigned char encoding; /* Set to ZIP_STR_* or ZIP_INT_* depending on
the entry encoding. However for 4 bits
immediate integers this can assume a range
of values and must be range-checked. */
/* 壓縮鏈表以字符串的形式保存,該指針指向當前節點起始位置 */
unsigned char *p; /* Pointer to the very start of the entry, that
is, this points to prev-entry-len field. */
} zlentry;
/* Different encoding/length possibilities */
#define ZIP_STR_MASK 0xc0
#define ZIP_INT_MASK 0x30
#define ZIP_STR_06B (0 << 6) /* 長度小於等於 63 字節 */
#define ZIP_STR_14B (1 << 6) /* 長度小於等於 16383 字節 */
#define ZIP_STR_32B (2 << 6) /* 長度小於等於 4294967295 字 */
#define ZIP_INT_16B (0xc0 | 0<<4)
#define ZIP_INT_32B (0xc0 | 1<<4)
#define ZIP_INT_64B (0xc0 | 2<<4)
#define ZIP_INT_24B (0xc0 | 3<<4)
#define ZIP_INT_8B 0xfe
dict.h
typedef struct dictEntry {
void *key; /* key 關鍵字定義 */
union {
void *val; /* value 定義 */
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; /* 指向下一個鍵值對節點 */
} dictEntry;
/* 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; /* 哈希表數組 */
unsigned long size; /* 哈希表大小 */
unsigned long sizemask; /* 掩碼大小 用於計算索引值;總是等於size-1 */
unsigned long used; /* 已有節點數 */
} dictht;
typedef struct dict {
dictType *type; /* 字典類型 */
void *privdata; /* 私有數據 */
dictht ht[2]; /* 一個字典有兩個哈希表 */
/* rehash索引 */
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
/* 當前正在使用的迭代器數量 */
int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;
dict.c
/* 擴容判斷 _dictExpandIfNeeded */
/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
/* Incremental rehashing already in progress. Return. */
if (dictIsRehashing(d)) return DICT_OK;
/* If the hash table is empty expand it to the initial size. */
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);
/* If we reached the 1:1 ratio, and we are allowed to resize the hash
* table (global setting) or we should avoid it but the ratio between
* elements/buckets is over the "safe" threshold, we resize doubling
* the number of buckets. */
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio) &&
dictTypeExpandAllowed(d))
{
return dictExpand(d, d->ht[0].used + 1);
}
return DICT_OK;
}
/* 擴容方法 */
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
if (malloc_failed) *malloc_failed = 0;
/* the size is invalid if it is smaller than the number of
* elements already inside the hash table */
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
dictht n; /* the new hash table */
unsigned long realsize = _dictNextPower(size);
/* Detect overflows */
if (realsize < size || realsize * sizeof(dictEntry*) < realsize)
return DICT_ERR;
/* Rehashing to the same table size is not useful. */
if (realsize == d->ht[0].size) return DICT_ERR;
/* Allocate the new hash table and initialize all pointers to NULL */
n.size = realsize;
n.sizemask = realsize-1;
if (malloc_failed) {
n.table = ztrycalloc(realsize*sizeof(dictEntry*));
*malloc_failed = n.table == NULL;
if (*malloc_failed)
return DICT_ERR;
} else
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = 0;
/* Is this the first initialization? If so it's not really a rehashing
* we just set the first hash table so that it can accept keys. */
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}
server.c
/* 縮容 */
int htNeedsResize(dict *dict) {
long long size, used;
size = dictSlots(dict);
used = dictSize(dict);
return (size > DICT_HT_INITIAL_SIZE &&
(used*100/size < HASHTABLE_MIN_FILL));
}
- 哈希表類型內部編碼
# 哈希表類型內部編碼
- ziplist: 壓縮列表 OBJ_ENCODING_ZIPLIST,一個經過特殊編碼的雙向鏈表;
- hashtable(dict): 哈希表 OBJ_ENCODING_HT,稱為字典(dictionary),它是一個數組+鏈表的結構。
# hash類型內部編碼查看
> hset h1 k1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
> hset h2 k2 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 0
> OBJECT encoding h1
"ziplist"
> OBJECT encoding h2
"hashtable"
Hash - 問題
- 什么時候使用ziplist存儲
# 滿足以下條件時候使用ziplist存儲
- 所有的鍵值對的健和值的字符串長度都小於等於 64byte(一個英文字母一個字節);
- 哈希對象保存的鍵值對數量小於 512。
# 滿足以下條件時侯使用hashtable存儲
- 一個哈希對象超過配置的閾值(鍵和值的長度有>64byte,鍵值對個數>512 個)時,會轉換成哈希表(hashtable)。
# hashtable數據
- dictEntry -> dictht -> dict -> OBJ_ENCODING_HT
- dictht 后面是 NULL 說明第二個 ht 還沒用到;dictEntry后面是 NULL 說明沒有 hash 到這個地址;dictEntry 后面是NULL 說明沒有發生哈希沖突。
- 為什么要定義兩個哈希表ht[2]
# dictht
- redis 的 hash 默認使用的是 ht[0],ht[1]不會初始化和分配空間;
- 哈希表 dictht 是用鏈地址法來解決碰撞問題的;在這種情況下,哈希表的性能取決於它的大小(size 屬性)和它所保存的節點的數量(used 屬性)之間的比率:
- 比率在 1:1 時(一個哈希表 ht 只存儲一個節點 entry),哈希表性能最好;
- 如果節點數量比哈希表的大小要大很多(比例用ratio表示,5表示平均一個ht存儲5個entry),哈希表就會退化成多個鏈表,哈希表本身的性能優勢就不再存在,這種情況下就需要擴容,Redis的這種操作叫做rehash。
# rehash步驟:
- 為字符 ht[1]哈希表分配空間,這個哈希表的空間大小取決於要執行的操作,以及 ht[0]當前包含的鍵值對的數量;ht[1]的大小為第一個大於等於 ht[0].used*2;
- 將所有的 ht[0]上的節點 rehash 到 ht[1]上,重新計算 hash 值和索引,然后放入指定的位置;
- 當 ht[0]全部遷移到了 ht[1]之后,釋放 ht[0]的空間,將 ht[1]設置為 ht[0]表,並創建新的 ht[1],為下次 rehash 做准備。
- 什么時候觸發擴容
# 負載因子 dict.c
- static int dict_can_resize = 1;
- static unsigned int dict_force_resize_ratio = 5;
# 觸發擴容條件
- radio = used/size,已使用節點與字典大小的比例;
- dict_can_resize 為 1 並且 dict_force_resize_ratio 已使用節點數和字典大小之間的比率超過 1:5,觸發擴容。
Hash - 應用場景
- String可做的,Hash都可以;
- 存儲對象類型的數據;
- 購物車;
- 可以在一個小型的 Redis實例中存儲上百萬的對象。
列表 (Lists)
Lists - 使用
- Redis列表是簡單的字符串列表,按照插入順序排序。 你可以添加一個元素到列表的頭部(左邊)或者尾部(右邊)。
- LPUSH 命令插入一個新元素到列表頭部,而RPUSH命令 插入一個新元素到列表的尾部。當 對一個空key執行其中某個命令時,將會創建一個新表。 類似的,如果一個操作要清空列表,那么key會從對應的key空間刪除。這是個非常便利的語義, 因為如果使用一個不存在的key作為參數,所有的列表命令都會像在對一個空表操作一樣。
# 可使用命令
- LPUSH
- LPUSHX
- RPUSH
- RPUSHX
- LPOP
- RPOP
- RPOPLPUSH
- LREM
- LLEN
- LINDEX
- LINSERT
- LSET
- LRANGE
- LTRIM
- BLPOP
- BRPOP
- BRPOPLPUSH
- push - pop
# 將一個或多個值 value 插入到列表 key 的表頭
# 如果 key 不存在,一個空列表會被創建並執行 LPUSH 操作。
# 當 key 存在但不是列表類型時,返回一個錯誤。
> LPUSH languages python # 加入單個元素
(integer) 1
> LPUSH languages python # 加入重復元素
(integer) 2
> LRANGE languages 0 -1 # 列表允許重復元素
1) "python"
2) "python"
> LPUSH mylist a b c # 加入多個元素
(integer) 3
> LRANGE mylist 0 -1
1) "c"
2) "b"
3) "a"
Lists - 原理
- 鏈表類型內部編碼
# 鏈表類型內部編碼
- 3.2 版本前:
- ziplist:
- linkedlist:
- 3.2 版本后:
- quicklist: 存儲了一個雙向鏈表,ziplist 和 linkedlist 的結合體;
# 鏈表類型內部編碼查看
> LPUSH hello hello
(integer) 1
> LRANGE hello 0 -1
1) "hello"
> OBJECT encoding hello
"quicklist"
quicklist.h
typedef struct quicklist {
quicklistNode *head; /* 指向雙向列表的表頭 */
quicklistNode *tail; /* 指向雙向列表的表尾 */
/* 所有的 ziplist 中一共存了多少個元素 */
unsigned long count; /* total count of all entries in all ziplists */
/* 雙向鏈表的長度,node 的數量 */
unsigned long len; /* number of quicklistNodes */
/* 填充因子 */
int fill : QL_FILL_BITS; /* fill factor for individual nodes */
/* 壓縮深度,0: 不壓縮; */
unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; /* 前一個節點 */
struct quicklistNode *next; /* 后一個節點 */
/* 指向實際的 ziplis */
unsigned char *zl;
/* 當前 ziplist 占用多少字節 */
unsigned int sz; /* ziplist size in bytes */
/* 當前 ziplist 中存儲了多少個元素,占 16bit(下同),最大65536個 */
unsigned int count : 16; /* count of items in ziplist */
/* 是否采用了 LZF 壓縮算法壓縮節點;1:RAW 2:LZF */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
/* 2:ziplist,未來可能支持其他結構存儲 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
/* 當前 ziplist 是不是已經被解壓出來作臨時使用 */
unsigned int recompress : 1; /* was this node previous compressed? */
/* 測試用 */
unsigned int attempted_compress : 1; /* node can't compress; too small */
/* 預留給未來使用 */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
redis.conf
# Lists are also encoded in a special way to save a lot of space.
# The number of entries allowed per internal list node can be specified
# as a fixed maximum size or a maximum number of elements.
# For a fixed maximum size, use -5 through -1, meaning:
# -5: max size: 64 Kb <-- not recommended for normal workloads
# -4: max size: 32 Kb <-- not recommended
# -3: max size: 16 Kb <-- probably not recommended
# -2: max size: 8 Kb <-- good
# -1: max size: 4 Kb <-- good
# Positive numbers mean store up to _exactly_ that number of elements
# per list node.
# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
# but if your use case is unique, adjust the settings as necessary.
list-max-ziplist-size -2
# Lists may also be compressed.
# Compress depth is the number of quicklist ziplist nodes from *each* side of
# the list to *exclude* from compression. The head and tail of the list
# are always uncompressed for fast push/pop operations. Settings are:
# 0: disable all list compression
# 1: depth 1 means "don't start compressing until after 1 node into the list,
# going from either the head or tail"
# So: [head]->node->node->...->node->[tail]
# [head], [tail] will always be uncompressed; inner nodes will compress.
# 2: [head]->[next]->node->node->...->node->[prev]->[tail]
# 2 here means: don't compress head or head->next or tail->prev or tail,
# but compress all nodes between them.
# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
# etc.
list-compress-depth 0
Lists - 問題
Lists - 應用場景
- 發布訂閱;
- 消息隊列;
- 慢查詢。
集合 (Sets)
Sets - 使用
- Redis集合是一個無序的字符串合集;你可以以O(1) 的時間復雜度(無論集合中有多少元素時間復雜度都為常量)完成 添加,刪除以及測試元素是否存在的操作;
# 可使用命令
- SADD
- SISMEMBER
- SPOP
- SRANDMEMBER
- SREM
- SMOVE
- SCARD
- SMEMBERS
- SSCAN
- SINTER
- SINTERSTORE
- SUNION
- SUNIONSTORE
- SDIFF
- SDIFFSTORE
- SADD - SMEMBERS
> SADD bbs "discuz.net" # 添加單個元素
(integer) 1
> SADD bbs "discuz.net" # 添加重復元素
(integer) 0
> SADD bbs "tianya.cn" "groups.google.com" # 添加多個元素
(integer) 2
> SMEMBERS bbs # 獲取所有元素
1) "discuz.net"
2) "groups.google.com"
3) "tianya.cn"
- scard - sismember
> sadd myset a b c d e f g
(integer) 7
> smembers myset # 獲取所有元素
1) "e"
2) "a"
3) "d"
4) "f"
5) "c"
6) "g"
7) "b"
> SISMEMBER myset a # 統計元素是否存在
(integer) 1
> SCARD myset # 統計元素個數
(integer) 7
- spop - srem
> sadd myset a b c d e f g
(integer) 7
> spop myset # 隨機彈出一個元素
"g"
> srem myset d e f # 移除一個或者多個元素
(integer) 3
> SMEMBERS myset # 查看元素是否存在
1) "a"
2) "c"
3) "b"
- sdiff - sinter - sunion
# SDIFF
> sadd sa1 1 2 3 4 789
(integer) 5
> sadd sa2 3 4 5 789
(integer) 3
> SDIFF sa1 sa2
1) "1"
2) "2"
# SINTER
> SINTER sa1 sa2
1) "3"
2) "4"
3) "789"
# SUNION
> sunion sa1 sa2
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "789"
Sets - 原理
- 集合類型內部編碼
# 集合類型內部編碼
- intset: 若元素都是整數類型,就用 inset 存儲;
- hashtable: 如果不是整數類型,就用 hashtable(數組+鏈表的存來儲結構)。
# 集合類型內部編碼查看
> sadd iset 1 2 3 4 5 6
(integer) 6
> object encoding iset
"intset"
> sadd myset a b c d e f
(integer) 3
> OBJECT encoding myset
"hashtable"
redis.conf
# Sets have a special encoding in just one case: when a set is composed
# of just strings that happen to be integers in radix 10 in the range
# of 64 bit signed integers.
# The following configuration setting sets the limit in the size of the
# set in order to use this special memory saving encoding.
set-max-intset-entries 512
Sets - 問題
Sets - 應用場景
- 用集合跟蹤一個獨特的事;想要知道所有訪問某個博客文章的獨立IP?只要每次都用SADD來處理一個頁面訪問;那么你可以肯定重復的IP是不會插入的;
- 抽獎;
- 點贊、簽到、打卡;
- 商品標簽;
- 商品篩選;
有序集合 (Sorted sets)
Sorted Sets - 使用
- Redis有序集合和Redis集合類似,是不包含 相同字符串的合集。它們的差別是,每個有序集合 的成員都關聯着一個評分,這個評分用於把有序集 合中的成員按最低分到最高分排列。
# 可使用命令
- ZADD
- ZSCORE
- ZINCRBY
- ZCARD
- ZCOUNT
- ZRANGE
- ZREVRANGE
- ZRANGEBYSCORE
- ZREVRANGEBYSCORE
- ZRANK
- ZREVRANK
- ZREM
- ZREMRANGEBYRANK
- ZREMRANGEBYSCORE
- ZRANGEBYLEX
- ZLEXCOUNT
- ZREMRANGEBYLEX
- ZSCAN
- ZUNIONSTORE
- ZINTERSTORE
- zadd - zrange - zrevrange - zrangebyscore
> zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
(integer) 5
> ZRANGE myzset 0 -1 withscores # 升序排序
1) "java"
2) "10"
3) "php"
4) "20"
5) "ruby"
6) "30"
7) "cpp"
8) "40"
9) "python"
10) "50"
> ZREVRANGE myzset 0 -1 withscores # 降序排序
1) "python"
2) "50"
3) "cpp"
4) "40"
5) "ruby"
6) "30"
7) "php"
8) "20"
9) "java"
10) "10"
> zrangebyscore myzset 20 30 # 根據分值區間獲取元素
1) "php"
2) "ruby"
- zrem
> zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
(integer) 5
> ZRANGE myzset 0 -1 withscores
1) "java"
2) "10"
3) "php"
4) "20"
5) "ruby"
6) "30"
7) "cpp"
8) "40"
9) "python"
10) "50"
> ZREM myzset php cpp # 刪除php cpp
(integer) 2
> ZRANGE myzset 0 -1 withscores
1) "java"
2) "10"
3) "ruby"
4) "30"
5) "python"
6) "50"
- 其它
# zcard
> zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
(integer) 2
> zcard myzset # 統計元素個數
(integer) 5
# 分值遞增
> ZINCRBY myzset 5 python
"55"
# 根據分值統計個數
> ZCOUNT myzset 20 60
(integer) 4
# 獲取元素 zrank 位置
> zrank myzset java
(integer) 0
> zrank myzset python
(integer) 4
# 獲取元素 zscore
> ZSCORE myzset java
"10"
Sorted Sets - 原理
# 有序集合類型內部編碼
- ziplist: 元素數量小於128且所有member的長度都小於64字節時使用ziplist;在 ziplist 的內部,按照 score 排序遞增來存儲;插入的時候要移動之后的數據;
- skiplist+dict: 若不滿足ziplist條件,則使用skiplist+dict存儲。
- 跳躍表: https://baike.baidu.com/item/%E8%B7%B3%E8%A1%A8/22819833?fr=aladdin
redis.conf
# Similarly to hashes and lists, sorted sets are also specially encoded in
# order to save a lot of space. This encoding is only used when the length and
# elements of a sorted set are below the following limits:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
t_zset.c
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
server.h
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele; /*zset 的元素*/
double score; /* 分值 */
struct zskiplistNode *backward; /* 后退指針 */
struct zskiplistLevel {
struct zskiplistNode *forward; /* 前進指針 對應level的下一個節點 */
unsigned long span; /* 從當前節點到下一個節點的跨度(跨越的節點數) */
} level[]; /* 層 */
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; /* 指向跳躍表的頭結點和尾節點 */
unsigned long length; /* 跳躍表的節點數 */
int level; /* 最大的層數 */
} zskiplist;
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
Sorted Sets - 問題
- 為什么不使用AVL樹或紅黑樹?
skiplist更加簡潔
Sorted Sets - 應用場景
- 排行榜(直播間在線⽤戶列表,各種禮物排⾏榜,彈幕消息)。