<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【99期】中高級開發(fā)面試必問的Redis,看這篇就夠了!

          共 10322字,需瀏覽 21分鐘

           ·

          2020-12-07 06:03

          程序員的成長之路
          互聯(lián)網(wǎng)/程序員/技術/資料共享?
          關注


          閱讀本文大概需要 12?分鐘。

          來自:https://github.com/CyC2018/CS-Notes


          • 一、概述

          • 二、數(shù)據(jù)類型

            • STRING

            • LIST

            • SET

            • HASH

            • ZSET

          • 三、數(shù)據(jù)結(jié)構

            • 字典

            • 跳躍表

          • 四、使用場景

            • 計數(shù)器

            • 緩存

            • 查找表

            • 消息隊列

            • 會話緩存

            • 分布式鎖實現(xiàn)

            • 其它

          • 五、Redis 與 Memcached

            • 數(shù)據(jù)類型

            • 數(shù)據(jù)持久化

            • 分布式

            • 內(nèi)存管理機制

          • 六、鍵的過期時間

          • 七、數(shù)據(jù)淘汰策略

          • 八、持久化

            • RDB 持久化

            • AOF 持久化

          • 九、事務

          • 十、事件

            • 文件事件

            • 時間事件

            • 事件的調(diào)度與執(zhí)行

          • 十一、復制

            • 連接過程

            • 主從鏈

          • 十二、Sentinel

          • 十三、分片

          • 十四、一個簡單的論壇系統(tǒng)分析

            • 文章信息

            • 點贊功能

            • 對文章進行排序

          • 參考資料

          一、概述

          Redis 是速度非常快的非關系型(NoSQL)內(nèi)存鍵值數(shù)據(jù)庫,可以存儲鍵和五種不同類型的值之間的映射。
          鍵的類型只能為字符串,值支持五種數(shù)據(jù)類型:字符串、列表、集合、散列表、有序集合。
          Redis 支持很多特性,例如將內(nèi)存中的數(shù)據(jù)持久化到硬盤中,使用復制來擴展讀性能,使用分片來擴展寫性能。

          二、數(shù)據(jù)類型

          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"

          三、數(shù)據(jù)結(jié)構

          字典

          dictht 是一個散列表結(jié)構,使用拉鏈法保存哈希沖突。
          /* 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 操作不是一次性完成,而是采用漸進方式,這是為了避免一次性執(zhí)行過多的 rehash 操作給服務器帶來過大的負擔。
          漸進式 rehash 通過記錄 dict 的 rehashidx 完成,它從 0 開始,然后每執(zhí)行一次 rehash 都會遞增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],這一次會把 dict[0] 上 table[rehashidx] 的鍵值對 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,并令 rehashidx++。
          在 rehash 期間,每次對字典執(zhí)行添加、刪除、查找或者更新操作時,都會執(zhí)行一次漸進式 rehash。
          采用漸進式 rehash 會導致字典中的數(shù)據(jù)分散在兩個 dictht 上,因此對字典的查找操作也需要到對應的 dictht 去執(zhí)行。
          /* 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;
          }

          跳躍表

          是有序集合的底層實現(xiàn)之一。
          跳躍表是基于多指針有序鏈表實現(xiàn)的,可以看成多個有序鏈表。
          在查找時,從上層指針開始查找,找到對應的區(qū)間之后再到下一層去查找。下圖演示了查找 22 的過程。

          與紅黑樹等平衡樹相比,跳躍表具有以下優(yōu)點:
          • 插入速度非常快速,因為不需要進行旋轉(zhuǎn)等操作來維護平衡性;

          • 更容易實現(xiàn);

          • 支持無鎖操作。

          四、使用場景

          計數(shù)器

          可以對 String 進行自增自減運算,從而實現(xiàn)計數(shù)器功能。
          Redis 這種內(nèi)存型數(shù)據(jù)庫的讀寫性能非常高,很適合存儲頻繁讀寫的計數(shù)量。

          緩存

          將熱點數(shù)據(jù)放到內(nèi)存中,設置內(nèi)存的最大使用量以及淘汰策略來保證緩存的命中率。

          查找表

          例如 DNS 記錄就很適合使用 Redis 進行存儲。
          查找表和緩存類似,也是利用了 Redis 快速的查找特性。但是查找表的內(nèi)容不能失效,而緩存的內(nèi)容可以失效,因為緩存不作為可靠的數(shù)據(jù)來源。

          消息隊列

          List 是一個雙向鏈表,可以通過 lpush 和 rpop 寫入和讀取消息
          不過最好使用 Kafka、RabbitMQ 等消息中間件。

          會話緩存

          可以使用 Redis 來統(tǒng)一存儲多臺應用服務器的會話信息。
          當應用服務器不再存儲用戶的會話信息,也就不再具有狀態(tài),一個用戶可以請求任意一個應用服務器,從而更容易實現(xiàn)高可用性以及可伸縮性。

          分布式鎖實現(xiàn)

          在分布式場景下,無法使用單機環(huán)境下的鎖來對多個節(jié)點上的進程進行同步。
          可以使用 Redis 自帶的 SETNX 命令實現(xiàn)分布式鎖,除此之外,還可以使用官方提供的 RedLock 分布式鎖實現(xiàn)。

          其它

          Set 可以實現(xiàn)交集、并集等操作,從而實現(xiàn)共同好友等功能。
          ZSet 可以實現(xiàn)有序性操作,從而實現(xiàn)排行榜等功能。

          五、Redis 與 Memcached

          兩者都是非關系型內(nèi)存鍵值數(shù)據(jù)庫,主要有以下不同:

          數(shù)據(jù)類型

          Memcached 僅支持字符串類型,而 Redis 支持五種不同的數(shù)據(jù)類型,可以更靈活地解決問題。

          數(shù)據(jù)持久化

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

          分布式

          Memcached 不支持分布式,只能通過在客戶端使用一致性哈希來實現(xiàn)分布式存儲,這種方式在存儲和查詢時都需要先在客戶端計算一次數(shù)據(jù)所在的節(jié)點。
          Redis Cluster 實現(xiàn)了分布式的支持。

          內(nèi)存管理機制

          • 在 Redis 中,并不是所有數(shù)據(jù)都一直存儲在內(nèi)存中,可以將一些很久沒用的 value 交換到磁盤,而 Memcached 的數(shù)據(jù)則會一直在內(nèi)存中。

          • Memcached 將內(nèi)存分割成特定長度的塊來存儲數(shù)據(jù),以完全解決內(nèi)存碎片的問題。但是這種方式會使得內(nèi)存的利用率不高,例如塊的大小為 128 bytes,只存儲 100 bytes 的數(shù)據(jù),那么剩下的 28 bytes 就浪費掉了。

          六、鍵的過期時間

          Redis 可以為每個鍵設置過期時間,當鍵過期時,會自動刪除該鍵。
          對于散列表這種容器,只能為整個鍵設置過期時間(整個散列表),而不能為鍵里面的單個元素設置過期時間。

          七、數(shù)據(jù)淘汰策略

          可以設置內(nèi)存最大使用量,當內(nèi)存使用量超出時,會施行數(shù)據(jù)淘汰策略。
          Redis 具體有 6 種淘汰策略:
          作為內(nèi)存數(shù)據(jù)庫,出于對性能和內(nèi)存消耗的考慮,Redis 的淘汰算法實際實現(xiàn)上并非針對所有 key,而是抽樣一小部分并且從中選出被淘汰的 key。
          使用 Redis 緩存數(shù)據(jù)時,為了提高緩存命中率,需要保證緩存數(shù)據(jù)都是熱點數(shù)據(jù)。可以將內(nèi)存最大使用量設置為熱點數(shù)據(jù)占用的內(nèi)存量,然后啟用 allkeys-lru 淘汰策略,將最近最少使用的數(shù)據(jù)淘汰。
          Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通過統(tǒng)計訪問頻率,將訪問頻率最少的鍵值對淘汰。

          八、持久化

          Redis 是內(nèi)存型數(shù)據(jù)庫,為了保證數(shù)據(jù)在斷電后不會丟失,需要將內(nèi)存中的數(shù)據(jù)持久化到硬盤上。

          RDB 持久化

          將某個時間點的所有數(shù)據(jù)都存放到硬盤上。
          可以將快照復制到其它服務器從而創(chuàng)建具有相同數(shù)據(jù)的服務器副本。
          如果系統(tǒng)發(fā)生故障,將會丟失最后一次創(chuàng)建快照之后的數(shù)據(jù)。
          如果數(shù)據(jù)量很大,保存快照的時間會很長。

          AOF 持久化

          將寫命令添加到 AOF 文件(Append Only File)的末尾。
          使用 AOF 持久化需要設置同步選項,從而確保寫命令什么時候會同步到磁盤文件上。這是因為對文件進行寫入并不會馬上將內(nèi)容同步到磁盤上,而是先存儲到緩沖區(qū),然后由操作系統(tǒng)決定什么時候同步到磁盤。有以下同步選項:
          • always 選項會嚴重減低服務器的性能;

          • everysec 選項比較合適,可以保證系統(tǒng)崩潰時只會丟失一秒左右的數(shù)據(jù),并且 Redis 每秒執(zhí)行一次同步對服務器性能幾乎沒有任何影響;

          • no 選項并不能給服務器性能帶來多大的提升,而且也會增加系統(tǒng)崩潰時數(shù)據(jù)丟失的數(shù)量。

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

          九、事務

          一個事務包含了多個命令,服務器在執(zhí)行事務期間,不會改去執(zhí)行其它客戶端的命令請求。
          事務中的多個命令被一次性發(fā)送給服務器,而不是一條一條發(fā)送,這種方式被稱為流水線,它可以減少客戶端與服務器之間的網(wǎng)絡通信次數(shù)從而提升性能。
          Redis 最簡單的事務實現(xiàn)方式是使用 MULTI 和 EXEC 命令將事務操作包圍起來。

          十、事件

          Redis 服務器是一個事件驅(qū)動程序。

          文件事件

          服務器通過套接字與客戶端或者其它服務器進行通信,文件事件就是對套接字操作的抽象。
          Redis 基于 Reactor 模式開發(fā)了自己的網(wǎng)絡事件處理器,使用 I/O 多路復用程序來同時監(jiān)聽多個套接字,并將到達的事件傳送給文件事件分派器,分派器會根據(jù)套接字產(chǎn)生的事件類型調(diào)用相應的事件處理器。

          時間事件

          服務器有一些操作需要在給定的時間點執(zhí)行,時間事件是對這類定時操作的抽象。
          時間事件又分為:
          • 定時事件:是讓一段程序在指定的時間之內(nèi)執(zhí)行一次;

          • 周期性事件:是讓一段程序每隔指定時間就執(zhí)行一次。

          Redis 將所有時間事件都放在一個無序鏈表中,通過遍歷整個鏈表查找出已到達的時間事件,并調(diào)用相應的事件處理器。

          事件的調(diào)度與執(zhí)行

          服務器需要不斷監(jiān)聽文件事件的套接字才能得到待處理的文件事件,但是不能一直監(jiān)聽,否則時間事件無法在規(guī)定的時間內(nèi)執(zhí)行,因此監(jiān)聽時間應該根據(jù)距離現(xiàn)在最近的時間事件來決定。
          事件調(diào)度與執(zhí)行由 aeProcessEvents 函數(shù)負責,偽代碼如下:
          def aeProcessEvents():
          # 獲取到達時間離當前時間最接近的時間事件
          time_event = aeSearchNearestTimer()
          # 計算最接近的時間事件距離到達還有多少毫秒
          remaind_ms = time_event.when - unix_ts_now()
          # 如果事件已到達,那么 remaind_ms 的值可能為負數(shù),將它設為 0
          if remaind_ms < 0:
          remaind_ms = 0
          # 根據(jù) remaind_ms 的值,創(chuàng)建 timeval
          timeval = create_timeval_with_ms(remaind_ms)
          # 阻塞并等待文件事件產(chǎn)生,最大阻塞時間由傳入的 timeval 決定
          aeApiPoll(timeval)
          # 處理所有已產(chǎn)生的文件事件
          procesFileEvents()
          # 處理所有已到達的時間事件
          processTimeEvents()
          將 aeProcessEvents 函數(shù)置于一個循環(huán)里面,加上初始化和清理函數(shù),就構成了 Redis 服務器的主函數(shù),偽代碼如下:
          def main():
          # 初始化服務器
          init_server()
          # 一直處理事件,直到服務器關閉為止
          while server_is_not_shutdown():
          aeProcessEvents()
          # 服務器關閉,執(zhí)行清理操作
          clean_server()
          從事件處理的角度來看,服務器運行流程如下:

          十一、復制

          通過使用 slaveof host port 命令來讓一個服務器成為另一個服務器的從服務器。
          一個從服務器只能有一個主服務器,并且不支持主主復制。

          連接過程

          1. 主服務器創(chuàng)建快照文件,發(fā)送給從服務器,并在發(fā)送期間使用緩沖區(qū)記錄執(zhí)行的寫命令。快照文件發(fā)送完畢之后,開始向從服務器發(fā)送存儲在緩沖區(qū)中的寫命令;

          2. 從服務器丟棄所有舊數(shù)據(jù),載入主服務器發(fā)來的快照文件,之后從服務器開始接受主服務器發(fā)來的寫命令;

          3. 主服務器每執(zhí)行一次寫命令,就向從服務器發(fā)送相同的寫命令。

          主從鏈

          隨著負載不斷上升,主服務器可能無法很快地更新所有從服務器,或者重新連接和重新同步從服務器將導致系統(tǒng)超載。為了解決這個問題,可以創(chuàng)建一個中間層來分擔主服務器的復制工作。中間層的服務器是最上層服務器的從服務器,又是最下層服務器的主服務器。


          十二、Sentinel

          Sentinel(哨兵)可以監(jiān)聽集群中的服務器,并在主服務器進入下線狀態(tài)時,自動從從服務器中選舉出新的主服務器。

          十三、分片

          分片是將數(shù)據(jù)劃分為多個部分的方法,可以將數(shù)據(jù)存儲到多臺機器里面,這種方法在解決某些問題時可以獲得線性級別的性能提升。
          假設有 4 個 Redis 實例 R0,R1,R2,R3,還有很多表示用戶的鍵 user:1,user:2,... ,有不同的方式來選擇一個指定的鍵存儲在哪個實例中。
          • 最簡單的方式是范圍分片,例如用戶 id 從 0~1000 的存儲到實例 R0 中,用戶 id 從 1001~2000 的存儲到實例 R1 中,等等。但是這樣需要維護一張映射范圍表,維護操作代價很高。

          • 還有一種方式是哈希分片,使用 CRC32 哈希函數(shù)將鍵轉(zhuǎn)換為一個數(shù)字,再對實例數(shù)量求模就能知道應該存儲的實例。

          根據(jù)執(zhí)行分片的位置,可以分為三種分片方式:
          • 客戶端分片:客戶端使用一致性哈希等算法決定鍵應當分布到哪個節(jié)點。

          • 代理分片:將客戶端請求發(fā)送到代理上,由代理轉(zhuǎn)發(fā)請求到正確的節(jié)點上。

          • 服務器分片:Redis Cluster。

          十四、一個簡單的論壇系統(tǒng)分析

          該論壇系統(tǒng)功能如下:
          • 可以發(fā)布文章;

          • 可以對文章進行點贊;

          • 在首頁可以按文章的發(fā)布時間或者文章的點贊數(shù)進行排序顯示。

          文章信息

          文章包括標題、作者、贊數(shù)等信息,在關系型數(shù)據(jù)庫中很容易構建一張表來存儲這些信息,在 Redis 中可以使用 HASH 來存儲每種信息以及其對應的值的映射。
          Redis 沒有關系型數(shù)據(jù)庫中的表這一概念來將同種類型的數(shù)據(jù)存放在一起,而是使用命名空間的方式來實現(xiàn)這一功能。鍵名的前面部分存儲命名空間,后面部分的內(nèi)容存儲 ID,通常使用 : 來進行分隔。例如下面的 HASH 的鍵名為 article:92617,其中 article 為命名空間,ID 為 92617。

          點贊功能

          當有用戶為一篇文章點贊時,除了要對該文章的 votes 字段進行加 1 操作,還必須記錄該用戶已經(jīng)對該文章進行了點贊,防止用戶點贊次數(shù)超過 1。可以建立文章的已投票用戶集合來進行記錄。
          為了節(jié)約內(nèi)存,規(guī)定一篇文章發(fā)布滿一周之后,就不能再對它進行投票,而文章的已投票集合也會被刪除,可以為文章的已投票集合設置一個一周的過期時間就能實現(xiàn)這個規(guī)定。

          對文章進行排序

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

          參考資料

          • Carlson J L. Redis in Action[J]. Media.johnwiley.com.au, 2013.

          • 黃健宏. Redis 設計與實現(xiàn) [M]. 機械工業(yè)出版社, 2014.

          • REDIS IN ACTION

          • Skip Lists: Done Right

          • 論述 Redis 和 Memcached 的差異

          • Redis 3.0 中文版- 分片

          • Redis 應用場景

          • Using Redis as an LRU cache

          推薦閱讀:

          【98期】面試官:給我說說你對Java GC機制的理解?

          【97期】一網(wǎng)打盡面試中常被問及的8種數(shù)據(jù)結(jié)構

          【96期】盤點那些關于Nginx的常考面試題

          5T技術資源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,單片機,樹莓派,等等。在公眾號內(nèi)回復「2048」,即可免費獲取!!

          微信掃描二維碼,關注我的公眾號

          朕已閱?

          瀏覽 34
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大香五月婷婷 | 国产婷婷999 | JIZZ国产丝袜19学生 | 成人一级aa黄色电影 | 北条麻妃中文字幕黄色片子 |