Redis——基礎篇

Redis特性

將數據存儲在緩存,可大大提高數據的IO性能,於是有了緩存的使用,隨着對緩存的利用越來越多樣化越來越充分,就有了各種緩存框架,Redis是其中較爲優秀的,其特性如下幾點:

  1. 更豐富的數據類型
  2. 進程內與跨進程
  3. 單機與分佈式
  4. 功能豐富:持久化機制、過期策略
  5. 支持多種編程語言
  6. 高可用,集羣化

Redis安裝

我這裏用centOS7的虛擬機來裝,爲什麼,別問,問就是在玩docker。

獲取安裝包

wget http://download.redis.io/releases/redis-5.0.5.tar.gz

什麼?你告訴我你沒有wget?   那就用yum -y install wget命令下載wget

解壓

tar -zxvf redis-5.0.5.tar.gz

安裝gcc依賴

yum install gcc

編譯安裝redis

cd redis-5.0.5
make MALLOC=libc

 將編譯後的src目錄下的二進制文件安裝到/usr/local/bin

cd src
make install

 修改配置文件

默認配置文件是/home/soft/redis-5.0.5/redis.conf

後臺啓動

daemonize no

改成

daemonize yes

下面一行必須改成 bind 0.0.0.0 或註釋,否則只能在本機訪問

bind 127.0.0.1

如果需要密碼訪問,取消requirepass的註釋

requirepass yourpassword

使用指定配置文件啓動Redis(這個命令建議配置alias)

/home/soft/redis-5.0.5/src/redis-server /home/soft/redis-5.0.5/redis.conf

進入客戶端(這個命令建議配置alias)

/home/soft/redis-5.0.5/src/redis-cli

停止redis(在客戶端中)

127.0.0.1:6379> shutdown

 完事兒啦!!!接下來就好好研究研究redis的基本數據類型吧。上手才能學的最快,嗯嗯。

基本操作

默認有16個庫,可以在配置文件中修改,默認使用第一個db0

databases 16

因爲沒有完全隔離,不像數據庫的database,不適合把不同的庫分配給不同的業務使用。

切換數據庫

select 0

清空當前數據庫

flushdb

清空所有數據庫

flushall

Redis是字典結構的存儲方式,採用key-value存儲。key和value的最大長度限制是512M。

設值

set king 123

取值

get king

查看所有鍵

keys *

獲取鍵總數

dbsize

查看鍵是否存在

exists king

刪除鍵

del king

重命名鍵

rename king chenxiansheng

查看類型

type king

Redis基本數據類型

最基本也是最常用的數據類型就是String。set和get命令就是String的操作命令。

String字符串

存儲類型

可以用來存儲字符串、整數、浮點數。

操作命令

設置多個值

mset king 123 letme 321

設置值,如果key存在,則設值不成功

setnx king 123

基於此特性,可用來實現分佈式鎖。用del key釋放鎖。

但如果釋放鎖的操作失敗,導致其他節點永遠獲取不了鎖,那麼就引出了下面一個話題,給key加過期時間。

加過期時間可用expire命令

expire letme 5

但如此一來,設值與加時間就不是一個原子操作了。可能導致,加鎖後設置過期時間失敗。所以可以使用多參數設值的方式

具體命令

set lock 1 EX 5 NX

設值一個lock鎖存在則失敗,不存在則5秒後過期。

(整數)值遞增、遞減

incr king
incrby king 100

decr king
decrby king 100

(浮點型)增量

set f 2.6
incrbyfloat f 7.4

獲取多個值

mget king f

獲取值長度

strlen king

字符串追加內容

append king test

獲取指定範圍的字符

getrange king 0 3

存儲原理

以set king test爲例,因爲Redis是KV的數據庫,它是通過hashtable實現的。所以每個鍵值對都會有一個dictEntry(源碼位置:dict.h),裏面指向了key和value的指針。next指向下一個dictEntry。

typedef struct dictEntry {
    void *key; /* key 關鍵字定義 */
    union {
        void *val; 
        uint64_t u64; /* value 定義 */
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; /* 指向下一個鍵值對節點 */
} dictEntry;

key是字符串,但是Redis沒有直接使用C的字符數組,而是存儲在自定義的SDS中。

value既不是直接作爲字符串存儲,也不是直接存儲在SDS中,而是存儲在redisObject中。實際上五種常用的數據類型的任何一種,都是通過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; /* 具體的數據結構 */
    unsigned lru:LRU_BITS; /* 24 位,對象最後一次被命令程序訪問的時間,與內存回收有關 */
    int refcount; /* 引用計數。當 refcount 爲 0 的時候,表示該對象已經不被任何對象引用,則可以進行垃圾回收了*/
    void *ptr; /* 指向對象實際的數據結構 */
} robj;

可以使用type命令來查看對外的類型。

內部編碼

可以看到,字符串類型的內部編碼有三種:

  1. int,存儲9個字節的長整型(long , 2^63-1)
  2. embstr,代表embstr格式的SDS(Simple Dynamic String 簡單動態字符串),存儲小於44個字節的字符串
  3. raw,存儲大於44個字節的字符串
/* object.c */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44

1、什麼是SDS?

在3.2以後的版本中,SDS又有多種結構(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; /* 當前字符數組的長度 */
    uint8_t alloc; /*當前字符數組總共分配的內存大小 */
    unsigned char flags; /* 當前字符數組的屬性、用來標識到底是 sdshdr8 還是 sdshdr16 等 */
    char buf[]; /* 字符串真正的值 */
};

2、爲什麼Redis要用SDS實現字符串呢?

我們知道,C語言本身沒有字符串類型(只能用字符串數組char[]實現)。

  1. 使用字符數組必須先給目標變量分配足夠的空間,否則可能OutOfBound
  2. 如果要獲取字符長度,必須遍歷字符數組,時間複雜度O(n)。
  3. C字符串長度的變更會對字符數組做內存重分配。
  4. 通過從字符串開始到結尾碰到的第一個'\0'來標記字符串的結束,因此不能保存圖片、音頻、視頻、壓縮文件等二進制(bytes)保存的內容,二進制不安全。

SDS的特點:

  • 不用擔心內存溢出問題,如果需要會對SDS進行擴容。
  • 獲取字符串長度時間複雜度爲O(1),因爲定義了len屬性。
  • 通過“空間預分配”(sdsMakeRoomFor)和“惰性空間釋放”,防止多次重分配內存。
  • 判斷是否結束的標誌是len屬性(它同樣以'\0'結尾是因爲這樣就可以使用C語言中的函數庫操作字符串的函數了),可以包含'\0'。

3、embstr和raw的區別?

embstr的使用只分配一次內存空間(因爲RedisObject和SDS是連續的),而raw需要分配兩次內存空間(分別爲RedisObject和SDS分配空間)。

因此與raw相比,embstr的好處在於創建時少分配一次空間,刪除時少釋放一次空間,以及對象的所有數據連在一起,尋找方便。

而embstr的壞處也很明顯,如果字符串的長度增加需要重新分配內存時,整個RedisObject和SDS都需要重新分配空間,因此Redis中的embstr實現爲只讀。

4、int和embstr什麼時候轉化爲raw?

當int數據不再是整數,或大小超過了long的範圍,自動轉化爲embstr。

5、未超過閾值情況下,爲什麼會轉換成raw?

這是爲什麼呢?對於embstr來說,由於是隻讀的,因此在對embstr對象進行修改時,都會先轉化爲raw再進行修改。

因此,只要是修改embstr對象,修改後的對象一定時raw的,無論是否達到了44個字節。

6、當長度小於閾值時,會還原嗎?

關於Redis內部編碼的轉換,都符合以下規律:編碼轉換在Redis寫入數據時完成,且轉換過程不可逆,只能從小內存編碼向大內存塊編碼轉換(但是不包括重新set)。

7、爲什麼要對底層的數據結構進行一層包裝呢?

通過封裝,可以根據對象的類型動態地選擇存儲結構和可以使用的命令,實現節省空間和優化查詢速度。

應用場景

緩存

String類型

例如:熱點數據緩存(例如報表,明星出軌),對象緩存,全頁緩存。

可以提升熱點數據的訪問速度。

數據共享分佈式

String類型,因爲Redis是分佈式的獨立服務,可以在多個應用之間共享

例如:分佈式Session

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

分佈式鎖

String類型setnx方法,只有不存在時才能添加成功,返回true。

SET key value [EX seconds] [PX milliseconds] [NX|XX]

public Boolean getLock(Object lockObject){
    jedisUtil = getJedisConnetion();
    boolean flag = jedisUtil.setNX(lockObj, 1);
    if(flag){
        expire(locakObj,10);
    }
    return flag;
}
​
public void releaseLock(Object lockObject){
    del(lockObj);
}

全局ID

int類型,incrby,利用原子性

incrby userid 1000

(分庫分表的場景,一次性拿一段)

計數器

int類型,incr方法

例如:文章的閱讀量,微博點贊數,允許一定的延遲,先寫入Redis再定時同步到數據庫。

發散:這讓我想到了我們項目的一個實際場景,多線程分頁查詢問題,如何保證數據不重複取。可以用計數器

限流

int類型,incr方法

以訪問者的IP和其他信息作爲key,訪問一次增加一次計數,超過次數則返回false。

位統計

String類型的bitcount。

字符是以8位二進制存儲的。

set k1 a
setbit k1 6 1
get k1
setbit k1 7 0
get k1

哎喲 有點東西哦。

a對應的ASCII碼是97,轉換爲二進制數據是01100001;

b對應的ASCII碼是98,轉換爲二進制數據是01100010;

因爲bit非常節省空間(1MB=8388608bit),可以用來做大量數據量的統計。

例如:在線用戶統計,留存用戶統計

setbit onlineusers 0 1
setbit onlineusers 1 1
setbit onlineusers 2 0

支持按位與、按位或等操作。

BITOP AND destkey key [key ...] ,對一個或多個 key 求邏輯並,並將結果保存到 destkey 。
BITOP OR destkey key [key ...] ,對一個或多個 key 求邏輯或,並將結果保存到 destkey 。
BITOP XOR destkey key [key ...] ,對一個或多個 key 求邏輯異或,並將結果保存到 destkey 。
BITOP NOT destkey key ,對給定 key 求邏輯非,並將結果保存到 destkey 。

如果一個對象的value有多個值的時候應該怎麼存儲?

例如用一個key存儲一張表的數據。

利用redis String數據類型,可以通過key分層的方式來實現,例如:

mset test:1:name king test:1:dbid 2

在取值時,可以一次獲取多個值:

mget test:1:name test:1:dbid

當然啦,這纔看到String,這種方式費勁哦,key那麼長 寫起來都費勁,更別說佔用空間問題咯。

Hash哈希

來吧,類型一出,結構先行。

存儲類型

包含鍵值對的無序散列表。value只能是字符串,不能嵌套其他類型。

同樣是存儲字符串,Hash與String的主要區別?

  1. 把所有相關的值聚集到一個key中,節省內存空間;
  2. 只使用一個key,減少key衝突;
  3. 當需要批量獲取值的時候,只需要使用一個命令,減少內存/IO/CPU的消耗。

Hash不適合的場景:

  • Field不能單獨設置過期時間
  • 沒有bit操作
  • 需要考慮數據量分佈問題

操作命令

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操作

hexists h1 a
hdel h1 a
hlen h1

存儲(實現)原理

Redis的hash本身也是一個kv的結構,類似於Java中的HashMap。

外層的哈希只用到了hashtable。當存儲hash數據類型時,我們把它叫做內層的哈希。內層的哈希底層可以使用兩種數據結構實現:

ziplist:OBJ_ENCODING_ZIPLIST(壓縮列表)

hashtable:OBJ_ENCODING_HT(哈希表)

ziplist壓縮列表

ziplist壓縮列表是什麼?

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.c的註釋)

ziplist是經過特殊編碼的雙向鏈接,旨在提高內存效率。 它存儲字符串和整數值,其中整數被編碼爲實際整數,而不是一系列字符。 它允許在O(1)時間在列表的任一側進行推和彈出操作。 但是,由於每個操作都需要重新分配zip列表使用的內存,因此實際的複雜性與zip列表使用的內存量有關。

ziplist的內部結構?

ziplist.c源碼第16行的註釋:

<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. */
    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;

編碼 encoding(ziplist.c 源碼第 204 行)
#define ZIP_STR_06B (0 << 6) //長度小於等於 63 字節
#define ZIP_STR_14B (1 << 6) //長度小於等於 16383 字節
#define ZIP_STR_32B (2 << 6) //長度小於等於 4294967295 字節

詳細吧,這可不是我畫滴。

什麼時候使用ziplist存儲?

當hash對象同時滿足以下兩個條件的時候,使用ziplist編碼:

  1. 所有的鍵值對的鍵和值的字符串長度都小於等於64bytes;
  2. 哈希對象保存的鍵值對數量小於512個。

/* 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);

/* Check the length of a number of objects to see if we need to convert a
 * ziplist to a real hash. Note that we only check string encoded objects
 * as their string length can be queried in constant time. */
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
    int i;

    if (o->encoding != OBJ_ENCODING_ZIPLIST) return;

    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;
        }
    }
}

一個哈希對象超過配置的閾值(鍵和值的長度>64bytes,鍵值對個數>512)時,會轉換成哈希表(hashtable)。

hashtable(dict)

在redis中,hashtable被稱爲字典(dictionary),它是一個數組+鏈表的結構。前面我們知道了,Redis的KV結構是通過一個dictEntry來實現的。Redis又對dictEntry進行了多層的封裝。

/* 源碼位置:dict.h */
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} 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; /* 哈希表數組 */
    unsigned long size; /* 哈希表大小 */
    unsigned long sizemask; /* 掩碼大小,用於計算索引值。總是等於 size-1 */
    unsigned long used; /* 已有節點數 */
} dictht;

ht放到了dict裏面:

typedef struct dict {
    dictType *type; /* 字典類型 */
    void *privdata; /* 私有數據 */
    dictht ht[2]; /* 一個字典有兩個哈希表 */
    long rehashidx; /* rehash 索引 */
    unsigned long iterators; /* 當前正在使用的迭代器數量 */
} dict;

從最底層到最頂層:dictEntry——dictht——dict——OBJ_ENCODING_HT

總結:哈希的存儲結構

注意:dictht後面是NULL說明第二個ht還沒用到。dictEntry*後面是NULL說明沒有hash到這個地址。dictEntry後面是NULL說明沒有發生哈希衝突。

爲什麼要定義兩個哈希表呢?

redis的hash默認使用的是ht[0],ht[1]不會初始化和分配空間。

哈希表dictht是用鏈地址法來解決碰撞問題。在這種情況下,哈希表的性能取決於它的大小(size屬性)和它所保存的節點的數量(userd屬性)之間的比率:

  • 比率在1:1時(一個哈希表ht只存儲一個節點entry),哈希表的性能最好;
  • 如果節點數量比哈希表的大小要大很多的話(這個比例用ratio表示,5表示平均一個ht存儲5個entry),那麼哈希表就會退化成多個鏈表,哈希表本身的性能優勢就不再存在。

在這種情況下需要擴容。Redis裏面的這種操作叫做rehash。

rehash的步驟:

  1. 爲字符ht[1]哈希表分配空間,這個哈希表的空間大小取決於要執行的操作,以及ht[0]當前包含的鍵值對的數量。擴展:ht[1]的大小爲第一個大於等於ht[0].used*2。
  2. 將所有的ht[0]上的節點rehash到ht[1]上,重新計算hash值和索引,然後放入指定的位置。
  3. 當ht[0]全部遷移到了ht[1]之後,釋放ht[0]的空間,將ht[1]設置爲ht[0]表,並創建新的ht[1],爲下次rehash做準備。

什麼時候觸發擴容?

/* 源碼位置:dict.c */
/* Using dictEnableResize() / dictDisableResize() we make possible to
 * enable/disable resizing of the hash table as needed. This is very important
 * for Redis, as we use copy-on-write and don't want to move too much memory
 * around when there is a child performing saving operations.
 *
 * Note that even when dict_can_resize is set to 0, not all resizes are
 * prevented: a hash table is still allowed to grow if the ratio between
 * the number of elements and the buckets > dict_force_resize_ratio. */
static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;

ratio = used / size,已使用節點與字典大小的比例

dict_can_resize爲 1 並且 dict_force_resize_ratio 已使用節點數和字典大小之間的比率超過 1:5,觸發擴容

/* 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))
    {
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

擴容方法dictExpand

/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
    /* 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);

    /* 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;
    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));
}

應用場景

String

String可以做的事情,除了位運算Hash都可以做。

存儲對象類型的數據

比如對象或者一張表的數據,比String節省了更多key的空間,也更加便於集中管理。

購物車

key:用戶id;field:商品id;value:商品數量。

+1:hincr

-1:hdecr

刪除:hdel

全選:hgetall

商品數:hlen

List列表

存儲類型

存儲有序的字符串(從左到右),元素可以重複。可以充當隊列和棧的角色。

操作命令

元素增減:

lpush queue a b c d

lpush queue x y z
rpush queue d e
lpop queue

blpop queue
brpop queue

取值:

lindex queue 0
lrange queue 0 -1

lpush與rpush簡單理解就是,都是壓棧操作,先入後出,lpush是正向壓棧的話,rpush就是反向壓棧:

存儲(實現)原理

統一使用quicklist來存儲。quicklist存儲了一個雙向鏈表,每個節點都是一個ziplist。

quicklist

quicklist(快速列表)是ziplist和linkedlist的結合體。

quicklist.h,head和tail指向雙向列表的表頭和表尾。

typedef struct quicklist {
    quicklistNode *head; /* 指向雙向列表的表頭 */
    quicklistNode *tail; /* 指向雙向列表的表尾 */
    unsigned long count; /* 所有的 ziplist 中一共存了多少個元素 */
    unsigned long len; /* 雙向鏈表的長度,node 的數量 */
    int fill : 16; /* fill factor for individual nodes */
    unsigned int compress : 16; /* 壓縮深度,0:不壓縮; */
} quicklist;

redis-conf相關參數:

參數 含義
list-max-ziplist-size(fill)

正數表示單個 ziplist 最多所包含的 entry 個數。
負數代表單個 ziplist 的大小,默認 8k。

-1:4KB;-2:8KB;-3:16KB;-4:32KB;-5:64KB

list-compress-depth(compress) 壓縮深度,默認是 0。
1:首尾的 ziplist 不壓縮;2:首尾第一第二個 ziplist 不壓縮,以此類推

 

 

 

 

 

 

quicklistNode中的*zl指向一個ziplist,一個ziplist可以存放多個元素。

typedef struct quicklistNode {
    struct quicklistNode *prev; /* 前一個節點 */
    struct quicklistNode *next; /* 後一個節點 */
    unsigned char *zl; /* 指向實際的 ziplist */
    unsigned int sz; /* 當前 ziplist 佔用多少字節 */
    unsigned int count : 16; /* 當前 ziplist 中存儲了多少個元素,佔 16bit(下同),最大 65536 個 */
    unsigned int encoding : 2; /* 是否採用了 LZF 壓縮算法壓縮節點,1:RAW 2:LZF */
    unsigned int container : 2; /* 2:ziplist,未來可能支持其他結構存儲 */
    unsigned int recompress : 1; /* 當前 ziplist 是不是已經被解壓出來作臨時使用 */
    unsigned int attempted_compress : 1; /* 測試用 */
    unsigned int extra : 10; /* 預留給未來使用 */
} quicklistNode;

應用場景

用戶消息時間線timeline

因爲List是有序的,可以用來做用戶時間線

消息隊列

List提供了兩個阻塞的彈出操作:BLPOP/BRPOP,可以設置超時時間。

BLPOP:BLPOP key1 timeout 彈出並獲取列表的第一個元素,如果列表沒有元素會阻塞列表知道等待超時或發現可彈出元素爲止。

BRPOP:BRPOP key1 timeout 彈出並獲取列表的最後一個元素,如果列表沒有元素會阻塞列表知道等待超時或發現可彈出元素爲止。

隊列:FIFO:rpush blpop,左頭右尾,右邊進入隊列,左邊出隊列。

:FILO:rpush brpop

Set集合

存儲類型

String類型的無序集合,最大存儲數量2^32-1(40億左右)。

操作命令

#添加一個或多個元素
sadd myset a b c d e f g

#獲取所有元素
smembers myset

#統計元素個數
scard myset

#隨機獲取一個元素
srandmember key

#隨機彈出一個元素
spop myset

#移除一個或者多個元素
srem myset d e f g

#查看元素是否存在
sismember myset g

存儲(實現)原理

Redis用intset或hashtable存儲set。如果元素都是整數類型,就用intset;如果不是整數類型,就用hashtable(數組+鏈表的存儲結構)。

KV是怎麼存儲set的元素的呢?key就是元素的值,value爲null。

如果元素超過512個,也會用hashtable存儲。

應用場景

隨機獲取元素 spop myset

點贊、簽到、打卡

商品標籤

用tags:i5021來維護商品所有的標籤

sadd tags:i5021 畫面清晰

sadd tags:i5021 流暢

sadd tags:i5021 美觀

商品篩選

#獲取差集
sdiff set1 set2

#獲取交集
sinter set1 set2

#獲取並集
sunion set1 set2

ZSet有序集合

存儲類型

sorted set,有序的 set,每個元素有個 score。
score 相同時,按照 key 的 ASCII 碼排序。
數據結構對比:

數據結構 是否允許重複元素 是否有序 有序實現方式
列表list 索引下標
集合set
有序集合zset 分值score

 

 

 

 

 

操作命令

#添加元素
zadd myzset 10 java 20 php 30 ruby 40 python

#獲取全部元素
zrange myzset 0 -1 withscores
zrevrange myzset 0 -1 withscores

#根據分值區間獲取元素
zrangebyscore myzset 20 30

#移除元素,也可以根據 score rank刪除
zrem myzset php ruby

#統計元素格式
zcard myzset

#分值遞增
zincrby myzset 5 java

#根據分值統計個數
zcount myzset 20 60

#獲取元素rank
zrank myzset java

#獲取元素score
zscore myzset java

倒序reverse

 存儲(實現)原理

同時滿足以下條件時使用ziplist編碼:

  • 元素數量小於128個
  • 所有member的長度都小於64字節

在ziplist的內部,按照score排序遞增來存儲。插入的時候要移動之後的數據。

超過閾值之後,使用skiplist+dict存儲。

什麼是skiplist?

有序鏈表如下:

爲提高查找表的效率,提出了跳錶的概念如下結構

 

假設每相鄰兩個節點增加一個指針,讓指針指向下下個節點。如此連成新的鏈表,相較於原鏈表跨度更大,以此提高查找效率。

 redis中t_zset.c源碼中有一個zslRandomLevel的方法。

應用場景

排行榜

id爲6001的新聞點擊數加1:zincrby hotNews:20190926 1 n6001

獲取今天點擊最多的15條:zrevrange hotNews:20190926 0 15 withscores

 

應用場景總結

  • 緩存——提升熱點數據的訪問速度
  • 共享數據——數據的存儲和共享的問題
  • 全局 ID —— 分佈式全局 ID 的生成方案(分庫分表)
  • 分佈式鎖——進程間共享數據的原子操作保證在線用戶統計和計數
  • 隊列、棧——跨進程的隊列/棧
  • 消息隊列——異步解耦的消息機制
  • 服務註冊與發現 —— RPC 通信機制的服務協調中心(Dubbo 支持 Redis)
  • 購物車
  • 新浪/Twitter 用戶消息時間線
  • 抽獎邏輯(禮物、轉發)
  • 點贊、簽到、打卡
  • 商品標籤
  • 用戶(商品)關注(推薦)模型
  • 電商產品篩選
  • 排行榜
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章