1. 程式人生 > >Redis基礎知識點面試手冊

Redis基礎知識點面試手冊

本文精心整理了書籍、部落格以及本人面試中遇到的基礎知識點,方便大家快速回顧知識。

基礎

概述

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

  • 鍵的型別只能為字串

  • 值支援的五種型別資料型別為:字串、列表、集合、有序集合、散列表。

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

資料型別

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

STRING

image.png

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

LIST

image.png

> 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

image.png

> 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

image.png

> 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(SORTEDSET)

image.png

> 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"

zset是set的一個升級版本,他在set的基礎上增加了一個順序屬性,這一屬性在新增修改元素的時候可以指定,每次指定後,zset會自動重新按新的值調整順序。 可以對指定鍵的值進行排序權重的設定,它應用排名模組比較多。

跳躍表(shiplist)是實現sortset(有序集合)的底層資料結構之一

另外還可以用 Sorted Sets 來做帶權重的佇列,比如普通訊息的 score 為1,重要訊息的 score 為2,然後工作執行緒可以選擇按 score的倒序來獲取工作任務,讓重要的任務優先執行。

資料結構

字典

dictht 是一個散列表結構,使用拉鍊法儲存雜湊衝突的 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;
    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;
}

跳躍表

來看看跳躍表的複雜度分析:

  • 空間複雜度: O(n) (期望)
  • 跳躍表高度: O(logn) (期望)

相關操作的時間複雜度:

  • 查詢: O(logn) (期望)
  • 插入: O(logn) (期望)
  • 刪除: O(logn) (期望)

其效率可比擬於二叉查詢樹(對於大於數操作需要O(log n)平均時間),並且不需要像二叉樹一樣過段時間重新平衡。

它是按層建造的。底層是一個普通的有序連結串列。每個更高層都充當下面列表的“快速跑道”,這裡在層i中的元素按概率l/p出現在層i+1中。

平均起來,每個元素都在p/(p-1)個列表中出現,而最高層的元素(通常是在跳躍列表前段的一個特殊的頭元素)在O(logp n)個列表中出現。

調節p的大小可以在記憶體消耗和時間消耗上進行折中。

image.png

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

image.png

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

  • 插入速度非常快速,因為不需要進行旋轉等操作來維護平衡性;
  • 更容易實現
  • 支援無鎖操作

使用場景

會話快取

在分散式場景下具有多個應用伺服器,可以使用 Redis 來統一儲存這些應用伺服器的會話資訊。

當應用伺服器不再儲存使用者的會話資訊,也就不再具有狀態,一個使用者可以請求任意一個應用伺服器。

快取

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

計數器

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

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

查詢表

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

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

訊息佇列

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

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

分散式 Session

多個應用伺服器的 Session 都儲存到 Redis 中來保證 Session 的一致性。

分散式鎖

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

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

其它

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

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

Redis 與 Memcached 對比

image.png

資料型別

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

資料持久化

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

單執行緒

Redis快的主要原因是:

  • 完全基於記憶體

  • 資料結構簡單,對資料操作也簡單

  • 使用多路 I/O 複用模型

  • 單程序單執行緒好處

    • 程式碼更清晰,處理邏輯更簡單
    • 不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,沒有因為可能出現死鎖而導致的效能消耗
    • 不存在多程序或者多執行緒導致的切換而消耗CPU
  • 單程序單執行緒弊端

    • 無法發揮多核CPU效能,不過可以通過在單機開多個Redis例項來完善;
  • 其他一些優秀的開源軟體採用的模型

    • 多程序單執行緒模型:Nginx
    • 單程序多執行緒模型:Memcached

image.png

分散式

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

Redis Cluster 實現了分散式的支援。採用虛擬槽。(為何不需要計算了?不懂)

記憶體管理機制

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

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

鍵的過期時間

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)的末尾。

對硬碟的檔案進行寫入時,寫入的內容首先會被儲存到緩衝區,然後由作業系統決定什麼時候將該內容同步到硬碟,使用者可以呼叫 file.flush() 方法請求作業系統儘快將緩衝區儲存的資料同步到硬碟。可以看出寫入檔案的資料不會立即同步到硬碟上,在將寫命令新增到 AOF 檔案時,要根據需求來保證何時同步到硬碟上。

有以下同步選項:

選項 同步頻率
always 每個寫命令都同步
everysec 每秒同步一次
no 讓作業系統來決定何時同步
  • always 選項會嚴重減低伺服器的效能;
  • everysec 選項比較合適,可以保證系統奔潰時只會丟失一秒左右的資料,並且 Redis 每秒執行一次同步對伺服器效能幾乎沒有任何影響;
  • no 選項並不能給伺服器效能帶來多大的提升,而且也會增加系統奔潰時資料丟失的數量。

隨著伺服器寫請求的增多,AOF 檔案會越來越大。Redis 提供了一種將 AOF 重寫的特性,能夠去除 AOF 檔案中的冗餘寫命令,使得 AOF 檔案的體積不會超出儲存資料集狀態所需的實際大小。

如果 AOF 檔案出錯了,怎麼辦?

伺服器可能在程式正在對 AOF 檔案進行寫入時停機, 如果停機造成了 AOF 檔案出錯(corrupt), 那麼 Redis 在重啟時會拒絕載入這個 AOF 檔案, 從而確保資料的一致性不會被破壞。

釋出與訂閱

訂閱者訂閱了頻道之後,釋出者向頻道傳送字串訊息會被所有訂閱者接收到。

某個客戶端使用 SUBSCRIBE 訂閱一個頻道,其它客戶端可以使用 PUBLISH 向這個頻道傳送訊息。

釋出與訂閱模式和觀察者模式有以下不同:

  • 觀察者模式中,觀察者和主題都知道對方的存在;而在釋出與訂閱模式中,釋出者與訂閱者不知道對方的存在,它們之間通過頻道進行通訊。
  • 觀察者模式是同步的,當事件觸發時,主題會去呼叫觀察者的方法;而釋出與訂閱模式是非同步的;

事務

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

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

它先以 MULTI 開始一個事務, 然後將多個命令入隊到事務中, 最後由 EXEC 命令觸發事務, 一併執行事務中的所有命令

單個 Redis 命令的執行是原子性的,但 Redis 沒有在事務上增加任何維持原子性的機制,所以 Redis 事務的執行並不是原子性的。

事務可以理解為一個打包的批量執行指令碼,但批量指令並非原子化的操作,中間某條指令的失敗不會導致前面已做指令的回滾,也不會造成後續的指令不做。

事件

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

檔案事件

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

image.png

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()

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

image.png

複製(增強讀效能)

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

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

連線過程

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

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

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

主從鏈

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

image.png

Sentinel(哨兵)

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

分片(增強寫效能)

分片是將資料劃分為多個部分的方法,可以將資料儲存到多臺機器裡面.

主要有三種分片方式:

  • 客戶端分片:客戶端使用一致性雜湊等演算法決定鍵應當分佈到哪個節點。
  • 代理分片:將客戶端請求傳送到代理上,由代理轉發請求到正確的節點上。
  • 伺服器分片:Redis Cluster。

Redis-cluster (Redis分散式)

但從Redis 3.0開始,引入了Redis Cluster,從此Redis進入了真正的“分散式叢集“時代。

image.png

P2P架構

image.png

為什麼是16384?

很顯然,我們需要維護節點和槽之間的對映關係,每個節點需要知道自己有哪些槽,並且需要在結點之間傳遞這個訊息。

為了節省儲存空間,每個節點用一個Bitmap來存放其對應的槽: 2k = 2*1024*8 = 16384,也就是說,每個結點用2k的記憶體空間,總共16384個位元位,就可以儲存該結點對應了哪些槽。然後這2k的資訊,通過Gossip協議,在結點之間傳遞。

客戶端儲存路由資訊

對於客戶端來說,維護了一個路由表:每個槽在哪臺機器上。這樣儲存(key, value)時,根據key計算出槽,再根據槽找到機器。

無損擴容

雖然Hash環(Memcached)可以減少擴容時失效的key的數量,但畢竟有丟失。而在redis-cluster中,當新增機器之後,槽會在機器之間重新分配,同時被影響的資料會自動遷移,從而做到無損擴容。

這裡可以結合補充知識點-快取-一致性雜湊來一起理解。虛擬槽改變的是槽的分配,一致性雜湊則會使旁邊的節點key失效。

主從複製

redis-cluster也引入了master-slave機制,從而提供了fail-over機制,這很大程度上解決了“快取雪崩“的問題。關於這個,後面有機會再詳細闡述。

image.png

Redis叢集相對單機在功能上有一定限制。

key批量操作支援有限。如:MSET``MGET,目前只支援具有相同slot值的key執行批量操作

key事務操作支援有限。支援多key在同一節點上的事務操作,不支援分佈在多個節點的事務功能。

key作為資料分割槽的最小粒度,因此不能將一個大的鍵值物件對映到不同的節點。如:hash、list。

不支援多資料庫空間。單機下Redis支援16個數據庫,叢集模式下只能使用一個數據庫空間,即db 0。

複製結構只支援一層,不支援巢狀樹狀複製結構。

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

該論壇系統功能如下:

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

文章資訊

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

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

image.png

點贊功能

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

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

image.png

對文章進行排序

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

image.png

Redis經典面試題

Redis有哪些資料結構?

字串String、字典Hash、列表List、集合Set、有序集合SortedSet。

如果你是Redis中高階使用者,還需要加上下面幾種資料結構HyperLogLog、Geo、Pub/Sub。

如果你說還玩過Redis Module,像BloomFilter,RedisSearch,Redis-ML,面試官得眼睛就開始發亮了。

使用過Redis分散式鎖麼,它是什麼回事?

先拿setnx來爭搶鎖,搶到之後,再用expire給鎖加一個過期時間防止鎖忘記了釋放。

這時候對方會告訴你說你回答得不錯,然後接著問如果在setnx之後執行expire之前程序意外crash或者要重啟維護了,那會怎麼樣?

這時候你要給予驚訝的反饋:唉,是喔,這個鎖就永遠得不到釋放了。緊接著你需要抓一抓自己得腦袋,故作思考片刻,好像接下來的結果是你主動思考出來的,然後回答:我記得set指令有非常複雜的引數,這個應該是可以同時把setnx和expire合成一條指令來用的。對方這時會顯露笑容,心裡開始默唸:摁,這小子還不錯。

假如Redis裡面有1億個key,其中有10w個key是以某個固定的已知的字首開頭的,如果將它們全部找出來?

使用keys指令可以掃出指定模式的key列表。

對方接著追問:如果這個redis正在給線上的業務提供服務,那使用keys指令會有什麼問題?

這個時候你要回答redis關鍵的一個特性:redis的單執行緒的。keys指令會導致執行緒阻塞一段時間,線上服務會停頓,直到指令執行完畢,服務才能恢復。這個時候可以使用scan指令,scan指令可以無阻塞的提取出指定模式的key列表,但是會有一定的重複概率,在客戶端做一次去重就可以了,但是整體所花費的時間會比直接用keys指令長。

使用過Redis做非同步佇列麼,你是怎麼用的?

一般使用list結構作為佇列,rpush生產訊息,lpop消費訊息。當lpop沒有訊息的時候,要適當sleep一會再重試。

如果對方追問可不可以不用sleep呢?list還有個指令叫blpop,在沒有訊息的時候,它會阻塞住直到訊息到來。

如果對方追問能不能生產一次消費多次呢?使用pub/sub主題訂閱者模式,可以實現1:N的訊息佇列。

如果對方追問pub/sub有什麼缺點?

在消費者下線的情況下,生產的訊息會丟失,得使用專業的訊息佇列如rabbitmq等。

如果對方追問redis如何實現延時佇列?

使用sortedset,拿時間戳作為score,訊息內容作為key呼叫zadd來生產訊息,消費者用zrangebyscore指令獲取N秒之前的資料輪詢進行處理。

如果有大量的key需要設定同一時間過期,一般需要注意什麼?

如果大量的key過期時間設定的過於集中,到過期的那個時間點,redis可能會出現短暫的卡頓現象。一般需要在時間上加一個隨機值,使得過期時間分散一些。

Redis如何做持久化的?

bgsave做映象全量持久化,aof做增量持久化。

因為bgsave會耗費較長時間,不夠實時,在停機的時候會導致大量丟失資料,所以需要aof來配合使用。在redis例項重啟時,會使用bgsave持久化檔案重新構建記憶體,再使用aof重放近期的操作指令來實現完整恢復重啟之前的狀態。

對方追問那如果突然機器掉電會怎樣?取決於aof日誌sync屬性的配置,如果不要求效能,在每條寫指令時都sync一下磁碟,就不會丟失資料。但是在高效能的要求下每次都sync是不現實的,一般都使用定時sync,比如1s1次,這個時候最多就會丟失1s的資料。

對方追問bgsave的原理是什麼?

你給出兩個詞彙就可以了,fork和cow。fork是指redis通過建立子程序來進行bgsave操作,cow指的是copy on write,子程序建立後,父子程序共享資料段,父程序繼續提供讀寫服務,寫髒的頁面資料會逐漸和子程序分離開來。

Pipeline有什麼好處,為什麼要用pipeline?

可以將多次IO往返的時間縮減為一次,前提是pipeline執行的指令之間沒有因果相關性。使用redis-benchmark進行壓測的時候可以發現影響redis的QPS峰值的一個重要因素是pipeline批次指令的數目。

Redis的同步機制瞭解麼?

Redis可以使用主從同步,從從同步。第一次同步時,主節點做一次bgsave,並同時將後續修改操作記錄到記憶體buffer,待完成後將rdb檔案全量同步到複製節點,複製節點接受完成後將rdb映象載入到記憶體。載入完成後,再通知主節點將期間修改的操作記錄同步到複製節點進行重放就完成了同步過程。

是否使用過Redis叢集,叢集的原理是什麼?

Redis Sentinal著眼於高可用,在master宕機時會自動將slave提升為master,繼續提供服務。

Redis Cluster著眼於擴充套件性,在單個redis記憶體不足時,使用Cluster進行分片儲存。

參考與拓展閱讀