Redis高頻面試題總結(jié)
通過面試多家大型互聯(lián)網(wǎng)企業(yè),總結(jié)了如下的高頻面試題目:
1、redis 過期鍵的刪除策略?
- 定時刪除:在設(shè)置鍵的過期時間的同時,創(chuàng)建一個定時器 timer). 讓定時器在鍵 的過期時間來臨時,立即執(zhí)行對鍵的刪除操作。
- 惰性刪除:放任鍵過期不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是 否過期,如果過期的話,就刪除該鍵;如果沒有過期,就返回該鍵。
- 定期刪除:每隔一段時間程序就對數(shù)據(jù)庫進行一次檢查,刪除里面的過期鍵
2、Redus的淘汰策略
redis 內(nèi)存數(shù)據(jù)集大小上升到一定大小的時候,就會施行 數(shù)據(jù)淘汰策略 。redis 提供6種數(shù)據(jù)淘汰策略:
通過淘汰策略也能保證Redis中緩存的都是熱點數(shù)據(jù)。
一個客戶端運行了新的命令,添加了新的數(shù)據(jù)。Redi 檢查內(nèi)存使用情況,如果大于 maxmemory 的限制, 則根據(jù)設(shè)定好的策略進行回收。
注意這里的 6 種機制,volatile 和 allkeys 規(guī)定了是對已設(shè)置過期時間的數(shù)據(jù)集淘汰數(shù)據(jù)還是從全部數(shù)據(jù)集淘汰數(shù)據(jù),后面的 lru、ttl 以及 random 是三種不同的淘汰策略,再加上一種 no-enviction 永不回收的策略。
使用策略規(guī)則:如果數(shù)據(jù)呈現(xiàn)冪律分布,也就是一部分?jǐn)?shù)據(jù)訪問頻率高,一部分?jǐn)?shù)據(jù)訪問頻率低,則使用 allkeys-lru;如果數(shù)據(jù)呈現(xiàn)平等分布,也就是所有的數(shù)據(jù)訪問頻率都相同,則使用
- allkeys-random
3、Redis分布式鎖的實現(xiàn)
Redis的分布式緩存特性使其成為了分布式鎖的一種基礎(chǔ)實現(xiàn)。通過Redis中是否存在某個鎖ID,則可以判斷是否上鎖。為了保證判斷鎖是否存在的原子性,保證只有一個線程獲取同一把鎖,Redis有 SETNX (即SET if Note Exists)和 GETSET (先寫新值,返回舊值,原子性操作,可以用于分辨是不是首次操作)操作。
(1)關(guān)于setnx:
- 將 key 的值設(shè)為 value ,當(dāng)且僅當(dāng) key 不存在,返回值為1。
- 若給定的 key 已經(jīng)存在,則setnx不做任何動作,返回值為0。
(2)關(guān)于set:一般操作
- ex seconds - seconds:設(shè)置失效時長,單位秒
- px - milliseconds:設(shè)置失效時長,單位毫秒
- nx - key不存在時設(shè)置value,成功返回OK,失敗返回(nil)
- xx - key存在時設(shè)置value,成功返回OK,失敗返回(nil)
為了防止主機宕機或網(wǎng)絡(luò)斷開之后的死鎖,Redis沒有ZK那種天然的實現(xiàn)方式,只能依賴設(shè)置超時時間來規(guī)避。所以如果使用setnx來實現(xiàn)分布式鎖,則實現(xiàn)步驟如下:
- 先拿 setnx 來爭搶鎖,搶到之后,再用 expire 給鎖加一個過期時間防止鎖忘記了釋放。
- 如果在 setnx 之后,執(zhí)行 expire 之前進程意外 crash 或重啟維護, 那么就需要把 setnx 和 expire 合成一條指令來用。
Redis 的 setnx 命令是當(dāng) key 不存在時設(shè)置 key ,但 setnx 不能同時完成 expire 設(shè)置失效時長,不能保證 setnx 和 expire 的原子性。我們可以使用 set 命令完成 setnx 和 expire 的操作,并且這種操作是原子操作。舉個例子如下:
- 案例:設(shè)置name=p7+,失效時長100s,不存在時設(shè)置
- 1.1.1.1:6379> set name p7+ ex 100 nx
- OK
- 1.1.1.1:6379> get name
- "p7+"
- 1.1.1.1:6379> ttl name
- (integer) 94
從上面可以看出,多個命令放在同一個 redis 連接中并且 redis 是單線程的,因此上面的操作可以看成 setnx 和 expire 的結(jié)合體,是原子性的。
4、Redis的Reactor模式
Redis基于Reactor模式開發(fā)了網(wǎng)絡(luò)事件處理器,這個處理器被稱為文件事件處理器。它的組成結(jié)構(gòu)為4部分:多個套接字、IO多路復(fù)用程序、文件事件分派器、事件處理器。
因為文件事件分派器隊列的消費是單線程的,所以Redis才叫單線程模型。
5、redis支持事務(wù)回滾嗎?
“ 不支持回滾動作,redis是支持簡單事務(wù)模式,只能discard,不能rollback ”
Redis在執(zhí)行事務(wù)命令的時候,在命令入隊的時候, Redis 就會檢測事務(wù)的命令是否正確,如果不正確則會產(chǎn)生錯誤。無論之前和之后的命令都會被事務(wù)所回滾,就變?yōu)槭裁炊紱]有執(zhí)行。
當(dāng)命令格式正確,而因為操作數(shù)據(jù)結(jié)構(gòu)引起的錯誤 ,則該命令執(zhí)行出現(xiàn)錯誤,而其之前和之后的命令都會被正常執(zhí)行。這點和數(shù)據(jù)庫很不一樣,這是需注意的地方。
對于一些重要的操作,我們必須通過程序去檢測數(shù)據(jù)的正確性,以保證 Redis 事務(wù)的正確執(zhí)行,避免出現(xiàn)數(shù)據(jù)不一致的情況。 Redis 之所以保持這樣簡易的事務(wù),完全是為了保證移動互聯(lián)網(wǎng)的核心問題一性能。
6、Redis的事務(wù)機制及CAS
watch指令在redis事物中提供了CAS的行為。為了檢測被watch的keys在是否有多個clients同時改變引起沖突,這些keys將會被監(jiān)控。如果至少有一個被監(jiān)控的key在執(zhí)行exec命令前被修改,整個事物將會回滾,不執(zhí)行任何動作,從而保證原子性操作,并且執(zhí)行exec會得到null的回復(fù)。
7、Redis和Memcached的區(qū)別
Redis的特性:
- 速度快,因為數(shù)據(jù)存在內(nèi)存中,類似于HashMap,HashMap的優(yōu)勢就是查找和操作的時間復(fù)雜度都是O(1)
- 支持豐富數(shù)據(jù)類型,支持字符串、鏈表、哈希、集合和有序集合
- 支持事務(wù),操作都是原子性,所謂的原子性就是對數(shù)據(jù)的更改要么全部執(zhí)行,要么全部不執(zhí)行
- 豐富的特性:可用于緩存,消息,按key設(shè)置過期時間,過期后將會自動刪除
與Memcached的區(qū)別在于:
- 存儲方式 Memecache把數(shù)據(jù)全部存在內(nèi)存之中,斷電后會掛掉,數(shù)據(jù)不能超過內(nèi)存大小。Redis有部分存在硬盤上,這樣能保證數(shù)據(jù)的持久性。
- 數(shù)據(jù)支持類型 Memcache對數(shù)據(jù)類型支持相對簡單。Redis有復(fù)雜的數(shù)據(jù)類型。
- 使用底層模型不同 它們之間底層實現(xiàn)方式 以及與客戶端之間通信的應(yīng)用協(xié)議不一樣。
Redis直接自己構(gòu)建了VM(Virtual Memory)機制 ,因為一般的系統(tǒng)調(diào)用系統(tǒng)函數(shù)的話(例如java調(diào)用自己的API),會浪費一定的時間去移動和請求。
8、緩存穿透、緩存擊穿和緩存雪崩
(1)緩存穿透
查詢不存在的數(shù)據(jù),緩存中沒有數(shù)據(jù),數(shù)據(jù)庫也沒有數(shù)據(jù)。因此所有的請求都訪問到了數(shù)據(jù)庫,給數(shù)據(jù)庫造成了壓力。解決方法如下:
采用布隆過濾器,將所有可能存在的數(shù)據(jù),哈希到一個很大的 bitmap 中,一個一定不存在的數(shù)據(jù)會被 bitmap 攔截調(diào),從而避免了對數(shù)據(jù)庫的查詢壓力。
如果查詢的數(shù)據(jù)為空,那么直接將空數(shù)據(jù)也緩存起來并設(shè)置較短的過期時間。這樣下次訪問的時候,就直接返回空值。
(2)緩存擊穿
緩存擊穿是指緩存過期之后,瞬時間并發(fā)客戶端特別多查詢同一條數(shù)據(jù)的情況下,導(dǎo)致數(shù)據(jù)庫壓力過大。業(yè)界比較常用的做法,是使用mutex。簡單地來說,就是在緩存失效的時候(判斷拿出來的值為空),不是立即去load db,
而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當(dāng)操作返回成功時,
再進行l(wèi)oad db的操作并回設(shè)緩存;否則,就重試整個get緩存的方法。類似下面的代碼:
- public String get(String key) {
- String value = redis.get(key);
- if (value == null) { // 代表緩存值過期
- // 設(shè)置3min的超時,防止del操作失敗的時候,下次緩存過期一直不能load db
- if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { // 代表設(shè)置成功
- value = db.get(key);
- redis.set(key, value, expire_secs);
- redis.del(key_mutex);
- }
- // 這個時候代表同時候的其他線程已經(jīng)load db并回設(shè)到緩存了,
- // 這時候重試獲取緩存值即可
- else {
- sleep(50);
- get(key); // 重試
- }
- } else {
- return value;
- }
- }
(3)緩存雪崩
雪崩就是指緩存中大批量熱點數(shù)據(jù)過期后系統(tǒng)涌入大量查詢請求,因為大部分?jǐn)?shù)據(jù)在Redis層已經(jīng)失效,請求滲透到數(shù)據(jù)庫層,大批量請求猶如洪水一般涌入,引起數(shù)據(jù)庫壓力造成查詢堵塞甚至宕機。
解決辦法:
將緩存失效時間分散開,比如每個key的過期時間是隨機,防止同一時間大量數(shù)據(jù)過期現(xiàn)象發(fā)生,這樣不會出現(xiàn)同一時間全部請求都落在數(shù)據(jù)庫層,如果緩存數(shù)據(jù)庫是分布式部署,將熱點數(shù)據(jù)均勻分布在不同Redis和數(shù)據(jù)庫中,有效分擔(dān)壓力,別一個人扛。
讓Redis數(shù)據(jù)永不過期(如果業(yè)務(wù)準(zhǔn)許)。
9、Redis數(shù)據(jù)傾斜
1、存在bigkey:業(yè)務(wù)層避免創(chuàng)建bigkey,把集合類型的bigkey拆分成多個小集合,分散保存bigkey 保存了大量集合元素(集合類型),會導(dǎo)致這個實例的數(shù)據(jù)量增加,內(nèi)存資源消耗也相應(yīng)增加。bigkey 的操作一般都會造成實例 IO 線程阻塞,如果 bigkey 的訪問量比較大,就會影響到這個實例上的其它請求被處理的速度。
2、slot手工分配不均勻:避免把較多的slot分配到一個實例上,進行槽的遷移
3、存在熱點數(shù)據(jù):采用帶有不同key前綴的多副本方法。 我們把熱點數(shù)據(jù)復(fù)制多份,在每一個數(shù)據(jù)副本的 key 中增加一個隨機前綴,讓它和其它副本數(shù)據(jù)不會被映射到同一個 Slot 中。這樣一來, 熱點數(shù)據(jù)既有多個副本可以同時服務(wù)請求,同時,這些副本數(shù)據(jù)的 key 又不一樣,會被映射到不同的 Slot 中。在給這些 Slot 分配實例時, 我們也要注意把它們分配到不同的實例上,那么,熱點數(shù)據(jù)的訪問壓力就被分散到不同的實例上了。 熱點數(shù)據(jù)多副本方法只能針對只讀的熱點數(shù)據(jù)。如果熱點數(shù)據(jù)是有讀有寫的話,就不適合采用多副本方法了,因為要保證多副本間的數(shù)據(jù)一致性,會帶來額外的開銷。
10、為什么Redis單線程模型也能效率這么高?
- 純內(nèi)存操作;
- 核心是基于非阻塞的IO多路復(fù)用機制;
- 底層使用C語言實現(xiàn),一般來說,C 語言實現(xiàn)的程序"距離"操作系統(tǒng)更近,執(zhí)行速度相對會更快;
- 單線程同時也避免了多線程的上下文頻繁切換問題,預(yù)防了多線程可能產(chǎn)生的競爭問題。
11、Redis做異步和延時隊列?
一般使用 list 結(jié)構(gòu)作為隊列,rpush 生產(chǎn)消息,lpop 消費消息。當(dāng) lpop 沒有消息的時候,要適當(dāng) sleep 一會再重試。如果對方追問可不可以不用 sleep 呢?list 還有個指令叫 blpop,在沒有消息的時候,它會阻塞住直到消息到來。如果對 方追問能不能生產(chǎn)一次消費多次呢?使用 pub/sub 主題訂閱者模式,可以實現(xiàn) 1:N 的消息隊列。
使用 zset(有序集合),拿時間戳作為score,消息內(nèi)容作為 key 調(diào)用 zadd 來生產(chǎn)消息,消費者用 zrangebyscore 指令獲取 N 秒之前的數(shù)據(jù)輪詢進行處理。
12、Redis的集群策略
(1)Redis主從同步Redis的主從結(jié)構(gòu)一主一從,一主多從或級聯(lián)結(jié)構(gòu),復(fù)制類型可以根據(jù)是否是全量而分為全量同步和增量同步。
(2)Redis哨兵 在主從復(fù)制實現(xiàn)之后,如果想對master進行監(jiān)控,Redis提供了一種哨兵機制,哨兵的含義就是監(jiān)控Redis系統(tǒng)的運行狀態(tài),并做相應(yīng)的響應(yīng)。Redis Sentinal 著眼于高可用,在 master 宕機時會自動將 slave 提升為master,繼續(xù)提供服務(wù)。
(3)Redis Cluster 著眼于擴展性,在單個 redis 內(nèi)存不足時,使用 Cluster 進行分片存儲。在redis-cluster架構(gòu)中,redis-master節(jié)點一般用于接收讀寫,而redis-slave節(jié)點則一般只用于備份,其與對應(yīng)的master擁有相同的slot集合,若某個redis-master意外失效,則再將其對應(yīng)的slave進行升級為臨時redis-master。
13、 Redis 的同步機制
Redis 可以使用主從同步,從從同步。第一次同步時,主節(jié)點做一次 bgsave,并同時將后續(xù)修改操作記錄到內(nèi)存 buffer,待完成后將 rdb 文件全量同步到復(fù)制節(jié)點,復(fù)制節(jié)點接受完成后將 rdb 鏡像加載到內(nèi)存。加載完成后,再通知主節(jié)點
將期間修改的操作記錄同步到復(fù)制節(jié)點進行重放就完成了同步過程。