Redis 基礎



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 說明沒有發生哈希沖突。

image-20211008192638266

  • 為什么要定義兩個哈希表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

image-20211008220129870

  • 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 - 應用場景
- 排行榜(直播間在線⽤戶列表,各種禮物排⾏榜,彈幕消息)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM