99面試常問:中高階開發面試必問的Redis,看這篇就夠了!

AI喬治發表於2020-11-21

 

一、概述

Redis 是速度非常快的非關係型(NoSQL)記憶體鍵值資料庫,可以儲存鍵和五種不同型別的值之間的對映。

鍵的型別只能為字串,值支援五種資料型別:字串、列表、集合、雜湊表、有序集合。

Redis 支援很多特性,例如將記憶體中的資料持久化到硬碟中,使用複製來擴充套件讀效能,使用分片來擴充套件寫效能。

二、資料型別

資料型別可以儲存的值操作
STRING字串、整數或者浮點數對整個字串或者字串的其中一部分執行操作
對整數和浮點數執行自增或者自減操作
LIST列表從兩端壓入或者彈出元素
對單個或者多個元素
進行修剪,只保留一個範圍內的元素
SET無序集合新增、獲取、移除單個元素
檢查一個元素是否存在於集合中
計算交集、並集、差集
從集合裡面隨機獲取元素
HASH包含鍵值對的無序雜湊表新增、獲取、移除單個鍵值對
獲取所有鍵值對
檢查某個鍵是否存在
ZSET有序集合新增、獲取、刪除元素
根據分值範圍或者成員來獲取元素
計算一個鍵的排名

What Redis data structures look like

STRING

 

> set hello world
OK
> get hello
"world"
> del hello
(integer) 1
> get hello
(nil)

LIST

 

> rpush list-key item
(integer) 1
> rpush list-key item2
(integer) 2
> rpush list-key item
(integer) 3

> lrange list-key 0 -1
1) "item"
2) "item2"
3) "item"

> lindex list-key 1
"item2"

> lpop list-key
"item"

> lrange list-key 0 -1
1) "item2"
2) "item"

SET

 

> sadd set-key item
(integer) 1
> sadd set-key item2
(integer) 1
> sadd set-key item3
(integer) 1
> sadd set-key item
(integer) 0

> smembers set-key
1) "item"
2) "item2"
3) "item3"

> sismember set-key item4
(integer) 0
> sismember set-key item
(integer) 1

> srem set-key item2
(integer) 1
> srem set-key item2
(integer) 0

> smembers set-key
1) "item"
2) "item3"

HASH

 

> hset hash-key sub-key1 value1
(integer) 1
> hset hash-key sub-key2 value2
(integer) 1
> hset hash-key sub-key1 value1
(integer) 0

> hgetall hash-key
1) "sub-key1"
2) "value1"
3) "sub-key2"
4) "value2"

> hdel hash-key sub-key2
(integer) 1
> hdel hash-key sub-key2
(integer) 0

> hget hash-key sub-key1
"value1"

> hgetall hash-key
1) "sub-key1"
2) "value1"

ZSET

 

> zadd zset-key 728 member1
(integer) 1
> zadd zset-key 982 member0
(integer) 1
> zadd zset-key 982 member0
(integer) 0

> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"

> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"

> zrem zset-key member1
(integer) 1
> zrem zset-key member1
(integer) 0

> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"

三、資料結構

字典

dictht 是一個雜湊表結構,使用拉鍊法儲存雜湊衝突。

/* 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;
    unsigned long used;
} dictht;
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

Redis 的字典 dict 中包含兩個雜湊表 dictht,這是為了方便進行 rehash 操作。在擴容時,將其中一個 dictht 上的鍵值對 rehash 到另一個 dictht 上面,完成之後釋放空間並交換兩個 dictht 的角色。

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

rehash 操作不是一次性完成,而是採用漸進方式,這是為了避免一次性執行過多的 rehash 操作給伺服器帶來過大的負擔。

漸進式 rehash 通過記錄 dict 的 rehashidx 完成,它從 0 開始,然後每執行一次 rehash 都會遞增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],這一次會把 dict[0] 上 table[rehashidx] 的鍵值對 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,並令 rehashidx++。

在 rehash 期間,每次對字典執行新增、刪除、查詢或者更新操作時,都會執行一次漸進式 rehash。

採用漸進式 rehash 會導致字典中的資料分散在兩個 dictht 上,因此對字典的查詢操作也需要到對應的 dictht 去執行。

/* Performs N steps of incremental rehashing. Returns 1 if there are still
 * keys to move from the old to the new hash table, otherwise 0 is returned.
 *
 * Note that a rehashing step consists in moving a bucket (that may have more
 * than one key as we use chaining) from the old to the new hash table, however
 * since part of the hash table may be composed of empty spaces, it is not
 * guaranteed that this function will rehash even a single bucket, since it
 * will visit at max N*10 empty buckets in total, otherwise the amount of
 * work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
    int empty_visits = n * 10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    while (n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(d->ht[0].size > (unsigned long) d->rehashidx);
        while (d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while (de) {
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}

跳躍表

是有序集合的底層實現之一。

跳躍表是基於多指標有序連結串列實現的,可以看成多個有序連結串列。

在查詢時,從上層指標開始查詢,找到對應的區間之後再到下一層去查詢。下圖演示了查詢 22 的過程。

 

與紅黑樹等平衡樹相比,跳躍表具有以下優點:

  • 插入速度非常快速,因為不需要進行旋轉等操作來維護平衡性;

  • 更容易實現;

  • 支援無鎖操作。

四、使用場景

計數器

可以對 String 進行自增自減運算,從而實現計數器功能。

Redis 這種記憶體型資料庫的讀寫效能非常高,很適合儲存頻繁讀寫的計數量。

快取

將熱點資料放到記憶體中,設定記憶體的最大使用量以及淘汰策略來保證快取的命中率。

查詢表

例如 DNS 記錄就很適合使用 Redis 進行儲存。

查詢表和快取類似,也是利用了 Redis 快速的查詢特性。但是查詢表的內容不能失效,而快取的內容可以失效,因為快取不作為可靠的資料來源。

訊息佇列

List 是一個雙向連結串列,可以通過 lpush 和 rpop 寫入和讀取訊息

不過最好使用 Kafka、RabbitMQ 等訊息中介軟體。

會話快取

可以使用 Redis 來統一儲存多臺應用伺服器的會話資訊。

當應用伺服器不再儲存使用者的會話資訊,也就不再具有狀態,一個使用者可以請求任意一個應用伺服器,從而更容易實現高可用性以及可伸縮性。

分散式鎖實現

在分散式場景下,無法使用單機環境下的鎖來對多個節點上的程式進行同步。

可以使用 Redis 自帶的 SETNX 命令實現分散式鎖,除此之外,還可以使用官方提供的 RedLock 分散式鎖實現。

其它

Set 可以實現交集、並集等操作,從而實現共同好友等功能。

ZSet 可以實現有序性操作,從而實現排行榜等功能。

五、Redis 與 Memcached

兩者都是非關係型記憶體鍵值資料庫,主要有以下不同:

資料型別

Memcached 僅支援字串型別,而 Redis 支援五種不同的資料型別,可以更靈活地解決問題。

資料持久化

Redis 支援兩種持久化策略:RDB 快照和 AOF 日誌,而 Memcached 不支援持久化。

分散式

Memcached 不支援分散式,只能通過在客戶端使用一致性雜湊來實現分散式儲存,這種方式在儲存和查詢時都需要先在客戶端計算一次資料所在的節點。

Redis Cluster 實現了分散式的支援。

記憶體管理機制

  • 在 Redis 中,並不是所有資料都一直儲存在記憶體中,可以將一些很久沒用的 value 交換到磁碟,而 Memcached 的資料則會一直在記憶體中。

  • Memcached 將記憶體分割成特定長度的塊來儲存資料,以完全解決記憶體碎片的問題。但是這種方式會使得記憶體的利用率不高,例如塊的大小為 128 bytes,只儲存 100 bytes 的資料,那麼剩下的 28 bytes 就浪費掉了。

六、鍵的過期時間

Redis 可以為每個鍵設定過期時間,當鍵過期時,會自動刪除該鍵。

對於雜湊表這種容器,只能為整個鍵設定過期時間(整個雜湊表),而不能為鍵裡面的單個元素設定過期時間。

七、資料淘汰策略

可以設定記憶體最大使用量,當記憶體使用量超出時,會施行資料淘汰策略。

Redis 具體有 6 種淘汰策略:

策略描述
volatile-lru從已設定過期時間的資料集中挑選最近最少使用的資料淘汰
volatile-ttl從已設定過期時間的資料集中挑選將要過期的資料淘汰
volatile-random從已設定過期時間的資料集中任意選擇資料淘汰
allkeys-lru從所有資料集中挑選最近最少使用的資料淘汰
allkeys-random從所有資料集中任意選擇資料進行淘汰
noeviction禁止驅逐資料

作為記憶體資料庫,出於對效能和記憶體消耗的考慮,Redis 的淘汰演算法實際實現上並非針對所有 key,而是抽樣一小部分並且從中選出被淘汰的 key。

使用 Redis 快取資料時,為了提高快取命中率,需要保證快取資料都是熱點資料。可以將記憶體最大使用量設定為熱點資料佔用的記憶體量,然後啟用 allkeys-lru 淘汰策略,將最近最少使用的資料淘汰。

Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通過統計訪問頻率,將訪問頻率最少的鍵值對淘汰。

八、持久化

Redis 是記憶體型資料庫,為了保證資料在斷電後不會丟失,需要將記憶體中的資料持久化到硬碟上。

RDB 持久化

將某個時間點的所有資料都存放到硬碟上。

可以將快照複製到其它伺服器從而建立具有相同資料的伺服器副本。

如果系統發生故障,將會丟失最後一次建立快照之後的資料。

如果資料量很大,儲存快照的時間會很長。

AOF 持久化

將寫命令新增到 AOF 檔案(Append Only File)的末尾。

使用 AOF 持久化需要設定同步選項,從而確保寫命令什麼時候會同步到磁碟檔案上。這是因為對檔案進行寫入並不會馬上將內容同步到磁碟上,而是先儲存到緩衝區,然後由作業系統決定什麼時候同步到磁碟。有以下同步選項:

選項同步頻率
always每個寫命令都同步
everysec每秒同步一次
no讓作業系統來決定何時同步
  • always 選項會嚴重減低伺服器的效能;

  • everysec 選項比較合適,可以保證系統崩潰時只會丟失一秒左右的資料,並且 Redis 每秒執行一次同步對伺服器效能幾乎沒有任何影響;

  • no 選項並不能給伺服器效能帶來多大的提升,而且也會增加系統崩潰時資料丟失的數量。

隨著伺服器寫請求的增多,AOF 檔案會越來越大。Redis 提供了一種將 AOF 重寫的特性,能夠去除 AOF 檔案中的冗餘寫命令。

九、事務

一個事務包含了多個命令,伺服器在執行事務期間,不會改去執行其它客戶端的命令請求。

事務中的多個命令被一次性傳送給伺服器,而不是一條一條傳送,這種方式被稱為流水線,它可以減少客戶端與伺服器之間的網路通訊次數從而提升效能。

Redis 最簡單的事務實現方式是使用 MULTI 和 EXEC 命令將事務操作包圍起來。

十、事件

Redis 伺服器是一個事件驅動程式。

檔案事件

伺服器通過套接字與客戶端或者其它伺服器進行通訊,檔案事件就是對套接字操作的抽象。

Redis 基於 Reactor 模式開發了自己的網路事件處理器,使用 I/O 多路複用程式來同時監聽多個套接字,並將到達的事件傳送給檔案事件分派器,分派器會根據套接字產生的事件型別呼叫相應的事件處理器。

 

時間事件

伺服器有一些操作需要在給定的時間點執行,時間事件是對這類定時操作的抽象。

時間事件又分為:

  • 定時事件:是讓一段程式在指定的時間之內執行一次;

  • 週期性事件:是讓一段程式每隔指定時間就執行一次。

Redis 將所有時間事件都放在一個無序連結串列中,通過遍歷整個連結串列查詢出已到達的時間事件,並呼叫相應的事件處理器。

事件的排程與執行

伺服器需要不斷監聽檔案事件的套接字才能得到待處理的檔案事件,但是不能一直監聽,否則時間事件無法在規定的時間內執行,因此監聽時間應該根據距離現在最近的時間事件來決定。

事件排程與執行由 aeProcessEvents 函式負責,虛擬碼如下:

def aeProcessEvents():
    # 獲取到達時間離當前時間最接近的時間事件
    time_event = aeSearchNearestTimer()
    # 計算最接近的時間事件距離到達還有多少毫秒
    remaind_ms = time_event.when - unix_ts_now()
    # 如果事件已到達,那麼 remaind_ms 的值可能為負數,將它設為 0
    if remaind_ms < 0:
        remaind_ms = 0
    # 根據 remaind_ms 的值,建立 timeval
    timeval = create_timeval_with_ms(remaind_ms)
    # 阻塞並等待檔案事件產生,最大阻塞時間由傳入的 timeval 決定
    aeApiPoll(timeval)
    # 處理所有已產生的檔案事件
    procesFileEvents()
    # 處理所有已到達的時間事件
    processTimeEvents()

將 aeProcessEvents 函式置於一個迴圈裡面,加上初始化和清理函式,就構成了 Redis 伺服器的主函式,虛擬碼如下:

def main():
    # 初始化伺服器
    init_server()
    # 一直處理事件,直到伺服器關閉為止
    while server_is_not_shutdown():
        aeProcessEvents()
    # 伺服器關閉,執行清理操作
    clean_server()

從事件處理的角度來看,伺服器執行流程如下:

 

十一、複製

通過使用 slaveof host port 命令來讓一個伺服器成為另一個伺服器的從伺服器。

一個從伺服器只能有一個主伺服器,並且不支援主主複製。

連線過程

  1. 主伺服器建立快照檔案,傳送給從伺服器,並在傳送期間使用緩衝區記錄執行的寫命令。快照檔案傳送完畢之後,開始向從伺服器傳送儲存在緩衝區中的寫命令;

  2. 從伺服器丟棄所有舊資料,載入主伺服器發來的快照檔案,之後從伺服器開始接受主伺服器發來的寫命令;

  3. 主伺服器每執行一次寫命令,就向從伺服器傳送相同的寫命令。

主從鏈

隨著負載不斷上升,主伺服器可能無法很快地更新所有從伺服器,或者重新連線和重新同步從伺服器將導致系統超載。為了解決這個問題,可以建立一箇中間層來分擔主伺服器的複製工作。中間層的伺服器是最上層伺服器的從伺服器,又是最下層伺服器的主伺服器。

 

 

十二、Sentinel

Sentinel(哨兵)可以監聽叢集中的伺服器,並在主伺服器進入下線狀態時,自動從從伺服器中選舉出新的主伺服器。

十三、分片

分片是將資料劃分為多個部分的方法,可以將資料儲存到多臺機器裡面,這種方法在解決某些問題時可以獲得線性級別的效能提升。

假設有 4 個 Redis 例項 R0,R1,R2,R3,還有很多表示使用者的鍵 user:1,user:2,... ,有不同的方式來選擇一個指定的鍵儲存在哪個例項中。

  • 最簡單的方式是範圍分片,例如使用者 id 從 0~1000 的儲存到例項 R0 中,使用者 id 從 1001~2000 的儲存到例項 R1 中,等等。但是這樣需要維護一張對映範圍表,維護操作代價很高。

  • 還有一種方式是雜湊分片,使用 CRC32 雜湊函式將鍵轉換為一個數字,再對例項數量求模就能知道應該儲存的例項。

根據執行分片的位置,可以分為三種分片方式:

  • 客戶端分片:客戶端使用一致性雜湊等演算法決定鍵應當分佈到哪個節點。

  • 代理分片:將客戶端請求傳送到代理上,由代理轉發請求到正確的節點上。

  • 伺服器分片:Redis Cluster。

十四、一個簡單的論壇系統分析

該論壇系統功能如下:

  • 可以釋出文章;

  • 可以對文章進行點贊;

  • 在首頁可以按文章的釋出時間或者文章的點贊數進行排序顯示。

文章資訊

文章包括標題、作者、贊數等資訊,在關係型資料庫中很容易構建一張表來儲存這些資訊,在 Redis 中可以使用 HASH 來儲存每種資訊以及其對應的值的對映。

Redis 沒有關係型資料庫中的表這一概念來將同種型別的資料存放在一起,而是使用名稱空間的方式來實現這一功能。鍵名的前面部分儲存名稱空間,後面部分的內容儲存 ID,通常使用 : 來進行分隔。例如下面的 HASH 的鍵名為 article:92617,其中 article 為名稱空間,ID 為 92617。

點贊功能

當有使用者為一篇文章點贊時,除了要對該文章的 votes 欄位進行加 1 操作,還必須記錄該使用者已經對該文章進行了點贊,防止使用者點贊次數超過 1。可以建立文章的已投票使用者集合來進行記錄。

為了節約記憶體,規定一篇文章釋出滿一週之後,就不能再對它進行投票,而文章的已投票集合也會被刪除,可以為文章的已投票集合設定一個一週的過期時間就能實現這個規定。

對文章進行排序

為了按釋出時間和點贊數進行排序,可以建立一個文章釋出時間的有序集合和一個文章點贊數的有序集合。(下圖中的 score 就是這裡所說的點贊數;下面所示的有序集合分值並不直接是時間和點贊數,而是根據時間和點贊數間接計算出來的)


最新2020整理收集的一些面試題(都整理成文件),有很多幹貨,包含mysql,netty,spring,執行緒,spring cloud、jvm、原始碼、演算法等詳細講解,也有詳細的學習規劃圖,面試題整理等,需要獲取這些內容的朋友掃描下方二維碼免費獲取:暗號:【CSDN】

 

å¨è¿éæå¥å¾çæè¿°

看完三件事❤️

如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。

  2. 關注公眾號 『 java爛豬皮 』,不定期分享原創知識。

  3. 同時可以期待後續文章ing?

 

相關文章