天天看點

Redis連環11問

Redis作為如今最火的NoSQL資料庫,在大廠面試中出現的頻率越來越高,掌握Redis已成為後端開發工程師的必備技能。

庫森校招時,Redis就是殺手锏,好多次和面試官聊Redis都超過半小時,為面試超加分!

庫森将連載幾期Redis面試文章,這次帶來的是Redis基礎的面試題,先點贊和收藏再看吧~

老規矩,先上目錄~

Redis連環11問

Redis本質上是一個Key-Value類型的記憶體資料庫,很像Memcached,整個資料庫加載在記憶體當中操作,定期通過異步操作把資料庫中的資料flush到硬碟上進行儲存。

因為是純記憶體操作,Redis的性能非常出色,每秒可以處理超過 10萬次讀寫操作,是已知性能最快的Key-Value 資料庫。

優點:

讀寫性能極高, Redis能讀的速度是110000次/s,寫的速度是81000次/s。

支援資料持久化,支援AOF和RDB兩種持久化方式。

支援事務, Redis的所有操作都是原子性的,意思就是要麼成功執行要麼失敗完全不執行。單個操作是原子性的。多個操作也支援事務,即原子性,通過MULTI和EXEC指令包起來。

資料結構豐富,除了支援string類型的value外,還支援hash、set、zset、list等資料結構。

支援主從複制,主機會自動将資料同步到從機,可以進行讀寫分離。

豐富的特性 – Redis還支援 publish/subscribe, 通知, key 過期等特性。

缺點:

資料庫容量受到實體記憶體的限制,不能用作海量資料的高性能讀寫,是以Redis适合的場景主要局限在較小資料量的高性能操作和運算上。

記憶體存儲:Redis是使用記憶體(in-memeroy)存儲,沒有磁盤IO上的開銷。資料存在記憶體中,類似于 HashMap,HashMap 的優勢就是查找和操作的時間複雜度都是O(1)。

單線程實作( Redis 6.0以前):Redis使用單個線程處理請求,避免了多個線程之間線程切換和鎖資源争用的開銷。注意:單線程是指的是在核心網絡模型中,網絡請求子產品使用一個線程來處理,即一個線程處理所有網絡請求。

非阻塞IO:Redis使用多路複用IO技術,将epoll作為I/O多路複用技術的實作,再加上Redis自身的事件處理模型将epoll中的連接配接、讀寫、關閉都轉換為事件,不在網絡I/O上浪費過多的時間。

優化的資料結構:Redis有諸多可以直接應用的優化資料結構的實作,應用層可以直接使用原生的資料結構提升性能。

使用底層模型不同:Redis直接自己建構了 VM (虛拟記憶體)機制 ,因為一般的系統調用系統函數的話,會浪費一定的時間去移動和請求。

Redis的VM(虛拟記憶體)機制就是暫時把不經常通路的資料(冷資料)從記憶體交換到磁盤中,進而騰出寶貴的記憶體空間用于其它需要通路的資料(熱資料)。通過VM功能可以實作冷熱資料分離,使熱資料仍在記憶體中、冷資料儲存到磁盤。這樣就可以避免因為記憶體不足而造成通路速度下降的問題。 Redis提高資料庫容量的辦法有兩種:一種是可以将資料分割到多個RedisServer上;另一種是使用虛拟記憶體把那些不經常通路的資料交換到磁盤上。需要特别注意的是Redis并沒有使用OS提供的Swap,而是自己實作。

資料類型:Memcached所有的值均是簡單的字元串,Redis支援更為豐富的資料類型,支援string(字元串),list(清單),Set(集合)、Sorted Set(有序集合)、Hash(哈希)等。

持久化:Redis支援資料落地持久化存儲,可以将記憶體中的資料保持在磁盤中,重新開機的時候可以再次加載進行使用。Memcached不支援資料持久存儲 。

叢集模式:Redis提供主從同步機制,以及 Cluster叢集部署能力,能夠提供高可用服務。Memcached沒有原生的叢集模式,需要依靠用戶端來實作往叢集中分片寫入資料

性能對比:Redis的速度比Memcached快很多。

網絡IO模型:Redis使用單線程的多路 IO 複用模型,Memcached使用多線程的非阻塞IO模式。

Redis支援伺服器端的資料操作:Redis相比Memcached來說,擁有更多的資料結構和并支援更豐富的資料操作,通常在Memcached裡,你需要将資料拿到用戶端來進行類似的修改再set回去。

這大大增加了網絡IO的次數和資料體積。在Redis中,這些複雜的操作通常和一般的GET/SET一樣高效。是以,如果需要緩存能夠支援更複雜的結構和操作,那麼Redis會是不錯的選擇。

1、緩存

緩存現在幾乎是所有中大型網站都在用的必殺技,合理的利用緩存不僅能夠提升網站通路速度,還能大大降低資料庫的壓力。Redis提供了鍵過期功能,也提供了靈活的鍵淘汰政策,是以,現在Redis用在緩存的場合非常多。

2、排行榜

很多網站都有排行榜應用的,如京東的月度銷量榜單、商品按時間的上新排行榜等。Redis提供的有序集合資料類構能實作各種複雜的排行榜應用。

3、計數器

什麼是計數器,如電商網站商品的浏覽量、視訊網站視訊的播放數等。為了保證資料實時效,每次浏覽都得給+1,并發量高時如果每次都請求資料庫操作無疑是種挑戰和壓力。Redis提供的incr指令來實作計數器功能,記憶體操作,性能非常好,非常适用于這些計數場景。

4、分布式會話

叢集模式下,在應用不多的情況下一般使用容器自帶的session複制功能就能滿足,當應用增多相對複雜的系統中,一般都會搭建以Redis等記憶體資料庫為中心的session服務,session不再由容器管理,而是由session服務及記憶體資料庫管理。

5、分布式鎖

在很多網際網路公司中都使用了分布式技術,分布式技術帶來的技術挑戰是對同一個資源的并發通路,如全局ID、減庫存、秒殺等場景,并發量不大的場景可以使用資料庫的悲觀鎖、樂觀鎖來實作,但在并發量高的場合中,利用資料庫鎖來控制資源的并發通路是不太理想的,大大影響了資料庫的性能。可以利用Redis的setnx功能來編寫分布式的鎖,如果設定傳回1說明擷取鎖成功,否則擷取鎖失敗,實際應用中要考慮的細節要更多。

6、 社交網絡

點贊、踩、關注/被關注、共同好友等是社交網站的基本功能,社交網站的通路量通常來說比較大,而且傳統的關系資料庫類型不适合存儲這種類型的資料,Redis提供的哈希、集合等資料結構能很友善的的實作這些功能。如在微網誌中的共同好友,通過Redis的set能夠很友善得出。

7、最新清單

Redis清單結構,LPUSH可以在清單頭部插入一個内容ID作為關鍵字,LTRIM可用來限制清單的數量,這樣清單永遠為N個ID,無需查詢最新的清單,直接根據ID去到對應的内容頁即可。

8、消息系統

消息隊列是大型網站必用中間件,如ActiveMQ、RabbitMQ、Kafka等流行的消息隊列中間件,主要用于業務解耦、流量削峰及異步處理實時性低的業務。Redis提供了釋出/訂閱及阻塞隊列功能,能實作一個簡單的消息隊列系統。另外,這個不能和專業的消息中間件相比。

有五種常用資料類型:String、Hash、Set、List、SortedSet。以及三種特殊的資料類型:Bitmap、HyperLogLog、Geospatial ,其中HyperLogLog、Bitmap的底層都是 String 資料類型,Geospatial 的底層是 Sorted Set 資料類型。

五種常用的資料類型:

1、String:String是最常用的一種資料類型,普通的key- value 存儲都可以歸為此類。其中Value既可以是數字也可以是字元串。使用場景:正常key-value緩存應用。正常計數: 微網誌數, 粉絲數。

2、Hash:Hash 是一個鍵值(key => value)對集合。Redishash 是一個 string 類型的 field 和 value 的映射表,hash 特别适合用于存儲對象,并且可以像資料庫中update一個屬性一樣隻修改某一項屬性值。

3、Set:Set是一個無序的天然去重的集合,即Key-Set。此外還提供了交集、并集等一系列直接操作集合的方法,對于求共同好友、共同關注什麼的功能實作特别友善。

4、List:List是一個有序可重複的集合,其遵循FIFO的原則,底層是依賴雙向連結清單實作的,是以支援正向、反向雙重查找。通過List,我們可以很方面的獲得類似于最新回複這類的功能實作。

5、SortedSet:類似于java中的TreeSet,是Set的可排序版。此外還支援優先級排序,維護了一個score的參數來實作。适用于排行榜和帶權重的消息隊列等場景。

三種特殊的資料類型:

1、Bitmap:位圖,Bitmap想象成一個以位為機關數組,數組中的每個單元隻能存0或者1,數組的下标在Bitmap中叫做偏移量。使用Bitmap實作統計功能,更省空間。如果隻需要統計資料的二值狀态,例如商品有沒有、使用者在不在等,就可以使用 Bitmap,因為它隻用一個 bit 位就能表示 0 或 1。

2、Hyperloglog。HyperLogLog 是一種用于統計基數的資料集合類型,HyperLogLog 的優點是,在輸入元素的數量或者體積非常非常大

時,計算基數所需的空間總是固定 的、并且是很小的。每個 HyperLogLog 鍵隻需要花費 12 KB 記憶體,就可以計算接近 2^64 個不同元素的基 數。

場景:統計網頁的UV(即Unique Visitor,不重複訪客,一個人通路某個網站多次,但是還是隻計算為一次)。

要注意,HyperLogLog 的統計規則是基于機率完成的,是以它給出的統計結果是有一定誤差的,标準誤算率是 0.81%。

3、Geospatial :主要用于存儲地理位置資訊,并對存儲的資訊進行操作,适用場景如朋友的定位、附近的人、打車距離計算等。

在Redisv6.0以前,Redis的核心網絡模型選擇用單線程來實作。先來看下官方的回答:

It's not very frequent that CPU becomes your bottleneck with Redis, as usually Redisis either memory or network bound. For instance, using pipelining Redisrunning on an average Linux system can deliver even 1 million requests per second, so if your application mainly uses O(N) or O(log(N)) commands, it is hardly going to use too much CPU.

核心意思就是,對于一個 DB 來說,CPU 通常不會是瓶頸,因為大多數請求不會是 CPU 密集型的,而是 I/O 密集型。

具體到 Redis的話,如果不考慮 RDB/AOF 等持久化方案,Redis是完全的純記憶體操作,執行速度是非常快的,是以這部分操作通常不會是性能瓶頸,Redis真正的性能瓶頸在于網絡 I/O,也就是用戶端和服務端之間的網絡傳輸延遲,是以 Redis選擇了單線程的 I/O 多路複用來實作它的核心網絡模型。

實際上更加具體的選擇單線程的原因如下:

避免過多的上下文切換開銷:如果是單線程則可以規避程序内頻繁的線程切換開銷,因為程式始終運作在程序中單個線程内,沒有多線程切換的場景。

避免同步機制的開銷:如果 Redis選擇多線程模型,又因為 Redis是一個資料庫,那麼勢必涉及到底層資料同步的問題,則必然會引入某些同步機制,比如鎖,而我們知道 Redis不僅僅提供了簡單的 key-value 資料結構,還有 list、set 和 hash 等等其他豐富的資料結構,而不同的資料結構對同步通路的加鎖粒度又不盡相同,可能會導緻在操作資料過程中帶來很多加鎖解鎖的開銷,增加程式複雜度的同時還會降低性能。

簡單可維護:如果 Redis使用多線程模式,那麼所有的底層資料結構都必須實作成線程安全的,這無疑又使得 Redis的實作變得更加複雜。

總而言之,Redis選擇單線程可以說是多方博弈之後的一種權衡:在保證足夠的性能表現之下,使用單線程保持代碼的簡單和可維護性。

讨論 這個問題前,先看下 Redis的版本中兩個重要的節點:

Redis 4.0(引入多線程處理異步任務)

Redis 6.0(正式在網絡模型中實作 I/O 多線程)

是以,網絡上說的Redis是單線程,通常是指在Redis 6.0之前,其核心網絡模型使用的是單線程;而Redis的異步任務使用的仍是多線程。

Redis在 4.0 版本的時候就已經引入了的多線程來做一些異步操作,此舉主要針對的是那些非常耗時的指令,通過将這些指令的執行進行異步化,避免阻塞單線程的事件循環。 在 Redis 4.0 之後增加了一些的非阻塞指令如 <code>UNLINK</code>、<code>FLUSHALL ASYNC</code>、<code>FLUSHDB ASYNC</code>。

很簡單,就是 Redis的網絡 I/O 瓶頸已經越來越明顯了。

随着網際網路的飛速發展,網際網路業務系統所要處理的線上流量越來越大,Redis的單線程模式會導緻系統消耗很多 CPU 時間在網絡 I/O 上進而降低吞吐量,要提升 Redis的性能有兩個方向:

優化網絡 I/O 子產品

提高機器記憶體讀寫的速度

後者依賴于硬體的發展,暫時無解。是以隻能從前者下手,網絡 I/O 的優化又可以分為兩個方向:

零拷貝技術或者 DPDK 技術

利用多核優勢

零拷貝技術有其局限性,無法完全适配 Redis這一類複雜的網絡 I/O 場景,更多網絡 I/O 對 CPU 時間的消耗和 Linux 零拷貝技術。而 DPDK 技術通過旁路網卡 I/O 繞過核心協定棧的方式又太過于複雜以及需要核心甚至是硬體的支援。

是以,利用多核優勢成為了優化網絡 I/O 成本效益最高的方案。

Redis的過期删除政策就是:惰性删除和定期删除兩種政策配合使用。

惰性删除:Redis的惰性删除政策由<code>db.c/expireIfNeeded</code>函數實作,所有鍵讀寫指令執行之前都會調用 <code>expireIfNeeded</code>函數對其進行檢查,如果過期,則删除該鍵,然後執行鍵不存在的操作;未過期則不作操作,繼續執行原有的指令。

定期删除:由<code>Redis.c/activeExpireCycle</code> 函數實作,函數以一定的頻率運作,每次運作時,都從一定數量的資料庫中取出一定數量的随機鍵進行檢查,并删除其中的過期鍵。

附:删除key常見的三種處理方式。

1、定時删除

在設定某個key 的過期時間同時,我們建立一個定時器,讓定時器在該過期時間到來時,立即執行對其進行删除的操作。

優點:定時删除對記憶體是最友好的,能夠儲存記憶體的key一旦過期就能立即從記憶體中删除。

缺點:對CPU最不友好,在過期鍵比較多的時候,删除過期鍵會占用一部分 CPU 時間,對伺服器的響應時間和吞吐量造成影響。

2、惰性删除

設定該key 過期時間後,我們不去管它,當需要該key時,我們在檢查其是否過期,如果過期,我們就删掉它,反之傳回該key。

優點:對 CPU友好,我們隻會在使用該鍵時才會進行過期檢查,對于很多用不到的key不用浪費時間進行過期檢查。

缺點:對記憶體不友好,如果一個鍵已經過期,但是一直沒有使用,那麼該鍵就會一直存在記憶體中,如果資料庫中有很多這種使用不到的過期鍵,這些鍵便永遠不會被删除,記憶體永遠不會釋放。進而造成記憶體洩漏。

3、定期删除

每隔一段時間,我們就對一些key進行檢查,删除裡面過期的key。

優點:可以通過限制删除操作執行的時長和頻率來減少删除操作對 CPU 的影響。另外定期删除,也能有效釋放過期鍵占用的記憶體。

缺點:難以确定删除操作執行的時長和頻率。如果執行的太頻繁,定期删除政策變得和定時删除政策一樣,對CPU不友好。如果買QQ号平台地圖執行的太少,那又和惰性删除一樣了,過期鍵占用的記憶體不會及時得到釋放。另外最重要的是,在擷取某個鍵時,如果某個鍵的過期時間已經到了,但是還沒執行定期删除,那麼就會傳回這個鍵的值,這是業務不能忍受的錯誤。

當現有記憶體大于 maxmemory 時,便會觸發Redis主動淘汰記憶體方式,有如下幾種淘汰方式:

Redis 4.0前提供 6種資料淘汰政策:

volatile-lru:利用LRU算法移除設定過過期時間的key (LRU:最近使用 Least Recently Used )

allkeys-lru:當記憶體不足以容納新寫入資料時,在鍵空間中,移除最近最少使用的key(這個是最常用的)

volatile-ttl:從已設定過期時間的資料集(server.db[i].expires)中挑選将要過期的資料淘汰

volatile-random:從已設定過期時間的資料集(server.db[i].expires)中任意選擇資料淘汰

allkeys-random:從資料集(server.db[i].dict)中任意選擇資料淘汰

no-eviction:禁止驅逐資料,也就是說當記憶體不足以容納新寫入資料時,新寫入操作會報錯。這個應該沒人使用吧!

Redis 4.0後增加以下兩種:

volatile-lfu:從已設定過期時間的資料集(server.db[i].expires)中挑選最不經常使用的資料淘汰(LFU(Least Frequently Used)算法,也就是最頻繁被通路的資料将來最有可能被通路到)

allkeys-lfu:當記憶體不足以容納新寫入資料時,在鍵空間中,移除最不經常使用的key。

為了能夠重用Redis資料,或者防止系統故障,我們需要将Redis中的資料寫入到磁盤空間中,即持久化。Redis提供了兩種不同的持久化方法可以将資料存儲在磁盤中,一種叫快照<code>RDB</code>,另一種叫隻追加檔案<code>AOF</code>。

RDB

在指定的時間間隔内将記憶體中的資料集快照寫入磁盤(<code>Snapshot</code>),它恢複時是将快照檔案直接讀到記憶體裡。

優勢:适合大規模的資料恢複;對資料完整性和一緻性要求不高

劣勢:在一定間隔時間做一次備份,是以如果Redis意外<code>down</code>掉的話,就會丢失最後一次快照後的所有修改。

AOF

以日志的形式來記錄每個寫操作,将Redis執行過的所有寫指令記錄下來(讀操作不記錄),隻許追加檔案但不可以改寫檔案,Redis啟動之初會讀取該檔案重新建構資料,換言之,Redis重新開機的話就根據日志檔案的内容将寫指令從前到後執行一次以完成資料的恢複工作。

AOF采用檔案追加方式,檔案會越來越大,為避免出現此種情況,新增了重寫機制,當AOF檔案的大小超過所設定的門檻值時, Redis就會啟動AOF檔案的内容壓縮,隻保留可以恢複資料的最小指令集。

優勢

每修改同步:<code>appendfsync always</code> 同步持久化,每次發生資料變更會被立即記錄到磁盤,性能較差但資料完整性比較好。

每秒同步:<code>appendfsync everysec</code> 異步操作,每秒記錄,如果一秒内當機,有資料丢失。

不同步:<code>appendfsync no</code>   從不同步。

劣勢

相同資料集的資料而言<code>aof</code>檔案要遠大于<code>rdb</code>檔案,恢複速度慢于<code>rdb。</code>

<code>aof</code>運作效率要慢于<code>rdb</code>,每秒同步政策效率較好,不同步效率和<code>rdb</code>相同。

如何選擇RDB和AOF

如果是資料不那麼敏感,且可以從其他地方重新生成補回的,那麼可以關閉持久化。

如果是資料比較重要,不想再從其他地方擷取,且可以承受數分鐘的資料丢失,比如緩存等,那麼可以隻使用RDB。

如果是用做記憶體資料庫,要使用Redis的持久化,建議是RDB和AOF都開啟,或者定期執行bgsave做快照備份,RDB方式更适合做資料的備份,AOF可以保證資料的不丢失。

Redis4.0 對于持久化機制的優化

Redis4.0相對與3.X版本其中一個比較大的變化是4.0添加了新的混合持久化方式。

簡單的說:新的AOF檔案前半段是RDB格式的全量資料後半段是AOF格式的增量資料,如下圖:

Redis連環11問

優勢:混合持久化結合了RDB持久化 和 AOF 持久化的優點, 由于絕大部分都是RDB格式,加載速度快,同時結合AOF,增量的資料以AOF方式儲存了,資料更少的丢失。

劣勢:相容性差,一旦開啟了混合持久化,在4.0之前版本都不識别該aof檔案,同時由于前部分是RDB格式,閱讀性較差