大家好,今天為大家帶來Java項目中,幾乎必不可少的組件之一-Redis的一些常見面試題,幫忙近期需要面試的朋友們來一個理論基礎突擊!
一、數據類型
1.Redis的常用數據類型有哪些 ?
難易程度:☆☆☆
出現頻率:☆☆☆☆☆
Redis是典型的“鍵值型”數據庫,不同數據類型其key結構一致,value有所差異。常見的類型有:string、hash、list、set、SortedSet等。
而基于以上5種基本數據類型,Redis又拓展了幾種拓展類型,例如:BitMap、
HyperLogLog、Geo等。
String類型是Redis中最常見的數據類型,value與key一樣都是Redis自定義的字符串結構,稱為SDS。不過在保存數字、小字符串時因為采用INT和 EMBSTR編碼,內存結構緊湊,只需要申請一次內存分配,效率更高,更節省內存。
而超過44字節的大字符串時則需要采用RAW編碼,申請額外的SDS空間,需要兩次內存分配,效率較低,內存占用也較高,但最大不超過512mb,因此建議單個value盡量不要超過44字節。
String類型常用來做計數器、簡單數據存儲等。復雜數據建議采用其他數據結構。
Hash結構,其value與Java中的HashMap類似,是一個key-value結構。如果有一個對象需要被Redis緩存,而且將來可能有部分修改。建議用hash結構來存儲這個對象的每一個字段和字段值。而不是作為一個JSON字符串存儲到
String類型中。因為Hash結構的每一個字段都可以單獨做修改,而String的 JSON串必須整體覆蓋。
與Java中的hashMap不同的是,Redis中的Hash底層采用了漸進式rehash的算法,在做rehash時會創建一個新的hashtable,每次操作元素時移動一部分數據,直到所有數據遷移完成,再用新的HashTable來代替舊的,避免了因為 rehash導致的阻塞,因此性能更高。
List結構的value類型可以看作是一個雙端鏈表,提供了一些命令便于我們從首尾操作元素。為了節省內存空間,底層采用了ZipList(壓縮列表)來做基礎存儲。當壓縮列表數據達到閾值(512)則會創建新的壓縮列表。每個壓縮列表作為一個雙端鏈表的一個節點,最終形成一個QuickList結構。而且QuickList結構與一般的雙端鏈表不同,他可以對中間不常用的ZipList節點做壓縮以節省內存。
List結構常用來模擬隊列,實現任務排隊這樣的功能。
Set結構的value與Java的Set類似,元素不可重復。Redis提供了求交集、并集等命令,可以幫助我們實現例如:好友列表、共同好友等功能。
當存儲元素是整數時,其底層默認采用IntSet結構,可以看作是一個有序的數組,結構緊湊,效率較高。而元素如果不是整數,或者元素量超過512這個閾值時則會轉為hash表結構,內存占用會有大的增加。因此我們在使用Set結構時盡量采用數組存儲,例如數值類型的id。而且元素數量盡量不要超過512,避免出現BigKey。
SortedSet,也叫ZSet。其value就是一個有序的Set集合,元素唯一,并且會按照一個指定的score值排序。因此常用來做排行榜功能。
SortedSet底層的利用Hash表保證元素的唯一性。利用跳表(SkipList)來保證元素的有序性,因此數據會有重復存儲,內存占用較高,是一種典型的以空間換時間的設計。不建議在SortedSet中放入過多數據。
2.跳表你了解嗎?
難易程度:☆☆☆☆
出現頻率:☆☆
跳表(SkipList)首先是鏈表,但與傳統的鏈表相比有幾點差異:
- ·跳表結合了鏈表和二分查找的思想元素按照升序排列存儲
- ·節點可能包含多個指針,指針跨度不同
- ·查找時從頂層向下,不斷縮小搜索范圍
- ·整個查詢的復雜度為 O ( log?n )
Redis數據類型Sorted?Set使用了跳表作為其中一種數據結構
二、持久化
Redis的數據持久化策略有哪些 ?
難易程度:☆☆☆
出現頻率:☆☆☆☆
在Redis中提供了兩種數據持久化的方式:1、RDB?2、AOF
RDB:
定期更新,定期將Redis中的數據生成的快照同步到磁盤等介質上,磁盤上保存的就是Redis的內存快照
優點:數據文件的大小相比于aop較小,使用rdb進行數據恢復速度較快缺點:比較耗時,存在丟失數據的風險
AOF:
將Redis所執行過的所有指令都記錄下來,在下次Redis重啟時,只需要執行指令就可以了
優點:數據丟失的風險大大降低了
缺點:數據文件的大小相比于rdb較大,使用aop文件進行數據恢復的時候速度較慢
你們的項目中的持久化是如何配置選擇的?
RDB+AOF
三、主從和集群
3.1?Redis集群有哪些方案,?知道嗎??
難易程度:☆☆☆
出現頻率:☆☆☆
在Redis中提供的集群方案總共有三種:
1、主從復制
保證高可用性
實現故障轉移需要手動
實現無法實現海量數據存儲
2、哨兵模式
?保證高可用性
?可以實現自動化的故障轉移
?無法實現海量數據存儲
3、Redis分片集群
保證高可用性
可以實現自動化的故障轉移
可以實現海量數據存儲
3.2什么是 Redis?主從同步?
難易程度:☆☆☆☆
出現頻率:☆☆☆☆
主從第一次同步是全量同步
第一階段,全量同步流程
- 從節點執行replicaof命令,發送自己的replid和offset給主節點
- 主節點判斷從節點的replid與自己的是否一致,
- 如果不一致說明是第一次來,需要做全量同步,主節點返回自己的replid給從節點
- 主節點開始執行bgsave,生成rdb文件
- 主節點發送rdb文件給從節點,再發送的過程中
- 從節點接收rdb文件,清空本地數據,加載rdb文件中的數據
- 同步過程中,主節點接收到的新命令寫入從節點的寫緩沖區(repl_buffer)
- 從節點接收到緩沖區數據后寫入本地,并記錄最新數據對應的offset
- 后期采用增量同步
后期數據變化后,則執行增量同步
- 主節點會不斷把自己接收到的命令記錄在repl_baklog中,并修改offset
- 從節點向主節點發送psync命令,發送自己的offset和replid
- 主節點判斷replid和offset與從節點是否一致
- 如果replid一致,說明是增量同步。然后判斷offset是否一致
- 如果從節點offset小于主節點offset,并且在repl_baklog中能找到對應數據則將offset之間相差的數據發送給從節點
- 從節點接收到數據后寫入本地,修改自己的offset與主節點一致
增量同步的風險
repl_baklog大小有上限,寫滿后會覆蓋最早的數據。如果slave斷開時間過?久,導致尚未備份的數據被覆蓋,則無法基于log做增量同步,只能再次全量同步。
repl_baklog可以在配置文件中進行修改存儲大小
3.3你們使用Redis是單點還是集群 ??哪種集群 ?(說說你們生產環境redis部署情況?)
難易程度:☆☆☆
出現頻率:☆☆☆
一般部分服務做緩存用的Redis直接做主從(1主1從)加哨兵就可以了。單節點不超過10G內存,如果Redis內存不足則可以給不同服務分配獨立的Redis主從節點。盡量不做分片集群。
原因:
維護起來比較麻煩
集群之間的心跳檢測和數據通信會消耗大量的網絡帶寬
集群插槽分配不均和key的分批容易導致數據傾斜
客戶端的route會有性能損耗
集群模式下無法使用lua腳本、事務
3.4Redis分片集群中數據是怎么存儲和讀取的 ?
難易程度:☆☆☆
出現頻率:☆☆☆
Redis?集群引入了哈希槽的概念,Redis?集群有 16384?個哈希槽,每個 key通過 CRC16 校驗后對 16384 取模來決定放置哪個槽,集群的每個節點負責一部分 hash 槽。
上圖是存值的流程,取值的流程類似
set {aaa}name zhangsan?計算hash是根據aaa計算的
3.5redis集群腦裂?
難易程度:☆☆☆☆
出現頻率:☆☆☆
關于reids集群會由于網絡等原因出現腦裂的情況,所謂的集群腦裂就是,由于 redis master節點和redis salve節點和sentinel處于不同的網絡分區,使得sentinel沒有能夠心跳感知到master,所以通過選舉的方式提升了一個salve為master,這樣就存在了兩個master,就像大腦分裂了一樣,這樣會導致客戶端還在old
master那里寫入數據,新節點無法同步數據,當網絡恢復后,sentinel會將old master降為salve,這時再從新master同步數據,這會導致大量數據丟失。
正常情況:
腦裂情況:
當哨兵與主節點由于網絡抖動原因斷開了連接,哨兵監控到之后,則會從剩余的從節點中選出一個作為主節點
redis的客戶端這個時候并沒有是可以正常連接之前的maser(主節點),并且可以正常寫入數據
假如現在網絡恢復了,哨兵發現主從中有兩個主節點,則會強制一個主節點變為從節點,看下圖
由于原來的主節點變成了從節點,則需要執行主從同步流程,清理數據(之前的主節點),同步新主節點中的數據,在之前腦裂過程中,客戶端寫入的數據丟失
解決方案:
redis中有兩個配置參數:
?min-replicas-to-write 1?表示最少的salve節點為1個
?min-replicas-max-lag 5?表示數據復制和同步的延遲不能超過5秒
配置了這兩個參數:如果發生腦裂:原master會在客戶端寫入操作的時候拒絕請求。這樣可以避免大量數據丟失。
3.6怎么保證redis的高并發高可用
難易程度:☆☆☆
出現頻率:☆☆☆
主從+哨兵
集群
四、使用場景
4.1項目中哪塊使用了緩存?
難易程度:☆☆☆
出現頻率:☆☆☆☆☆
結合自己簡歷上寫的項目模塊說明這個問題,要陳述出當時的場景
?數據字典
?用戶Token
?熱點數據
4.2什么是緩存穿透 ??怎么解決 ?
難易程度:☆☆☆☆
出現頻率:☆☆☆☆☆
加入緩存以后的數據查詢流程:
緩存穿透:
概述:指查詢一個一定不存在的數據,如果從存儲層查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到 DB 去查詢,可能導致 DB 掛掉。
解決方案:
1、查詢返回的數據為空,仍把這個空結果進行緩存,但過期時間會比較短
2、布隆過濾器:將所有可能存在的數據哈希到一個足夠大的 bitmap?中,一個一定不存在的數據會被這個 bitmap 攔截掉,從而避免了對DB的查詢
4.3什么是緩存擊穿 ??怎么解決 ?
難易程度:☆☆☆☆
出現頻率:☆☆☆☆☆
概述:對于設置了過期時間的key,緩存在某個時間點過期的時候,恰好這時間點對這個Key有大量的并發請求過來,這些請求發現緩存過期一般都會從后端 DB加載數據并回設到緩存,這個時候大并發的請求可能會瞬間把 DB 壓垮。
解決方案:
1、使用互斥鎖:當緩存失效時,不立即去load?db,先使用如 Redis?的 setnx?去設置一個互斥鎖,當操作成功返回時再進行 load db的操作并回設緩存,否則重試get緩存的方法
2、可以設置當前key邏輯過期,大概思路如下:
①:在設置key的時候,設置一個過期時間字段一塊存入緩存中,不給當前key
設置過期時間
②:當查詢的時候,從redis取出數據后判斷時間是否過期
③:如果過期則開通另外一個線程進行數據同步,當前線程正常返回數據,這個數據不是最新
兩種方案對比:
解決方案 | 優點 | 缺點 |
互斥鎖 | 沒有額外的內存消耗保證一致性 實現簡單 | 線程需要等待,性能受影響可能有死鎖風險 |
邏輯過期 | 線程無需等待,性能較好 | 不保證一致性 有額外內存消耗實現復雜 |
4.4什么是緩存雪崩 ??怎么解決 ?
難易程度:☆☆☆☆
出現頻率:☆☆☆☆☆
概述:設置緩存時采用了相同的過期時間,導致緩存在某一時刻同時失效,請求全部轉發到DB,DB 瞬時壓力過重雪崩。與緩存擊穿的區別:雪崩是很多key,擊穿是某一個key緩存。
解決方案:
將緩存失效時間分散開,比如可以在原有的失效時間基礎上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重復率就會降低,就很難引發集體失效的事件。
4.5什么是布隆過濾器?
難易程度:☆☆☆☆
出現頻率:☆☆☆☆☆
概述:布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實際上由一個很長的二進制向量(二進制數組)和一系列隨機映射函數(hash函數)。
作用:布隆過濾器可以用于檢索一個元素是否在一個集合中。添加元素:將商品的id(id1)存儲到布隆過濾器
假設當前的布隆過濾器中提供了三個hash函數,此時就使用三個hash函數對id1進行哈希運算,運算結果分別為:1、4、9那么就會數組中對應的位置數據更改為1。
判斷數據是否存在:使用相同的hash函數對數據進行哈希運算,得到哈希值。然后判斷該哈希值所對應的數組位置是否都為1,如果不都是則說明該數據肯定不存在。如果是說明該數據可能存在,因為哈希運算可能就會存在重復的情況。如下圖所示:
假設添加完id1和id2數據以后,布隆過濾器中數據的存儲方式如上圖所示,那么此時要判斷id3對應的數據在布隆過濾器中是否存在,按照上述的判斷規則應該是存在,但是id3這個數據在布隆過濾器中壓根就不存在,這種情況就屬于誤?判。
誤判率:數組越小誤判率就越大,數組越大誤判率就越小,但是同時帶來了更多的內存消耗。
刪除元素:布隆過濾器不支持數據的刪除操作,因為如果支持刪除那么此時就會影響判斷不存在的結果。
使用布隆過濾器:在redis的框架redisson中提供了布隆過濾器的實現,使用方式如下所示:
pom.xml文件
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
測試代碼:
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter; import org.redisson.api.RedissonClient; import org.redisson.config.Config;public class Application {public static void main(String[] args) {
//鏈接redis,
Config config = new Config();config.useSingleServer().setAddress("redis://192.168.200.130:6379")
.setPassword("leadnews");
//創建redisson客戶端
RedissonClient redissonClient = Redisson.create(config);
//創建布隆過濾器 RBloomFilter<String> bloomFilter =
redissonClient.getBloomFilter("bloom-filter"); int size = 10000;
//初始化數據
// initData(bloomFilter, size);
//測試誤判率
int count = getData(bloomFilter, size); System.out.println("總的誤判條數為:" + count);}/**
*測試誤判率
*@param bloomFilter
*@param size
*@return
*/
private static int getData(RBloomFilter<String> bloomFilter, int size) {
int count = 0 ; // 記錄誤判的數據條數
for(int x = size; x < size * 2 ; x++) { if(bloomFilter.contains("add" + x)) {
count++ ;
}
}
return count;
}/**
*初始化數據
*@param bloomFilter
*@param size
*/
private static void initData(RBloomFilter<String> bloomFilter, int size) {
//第一個參數:布隆過濾器存儲的元素個數
//第一個參數:誤判率
bloomFilter.tryInit(size,0.01);
//在布隆過濾器初始化數據
for(int x = 0; x < size; x++) { bloomFilter.add("add" + x) ;
}
System.out.println("初始化完成...");
}
}
Redis中使用布隆過濾器防止緩存穿透流程圖如下所示:
4.6redis雙寫問題?
難易程度:☆☆☆
出現頻率:☆☆☆☆☆
同步方案:
普通緩存,一般采用更新時刪除緩存,查詢時建立緩存的延遲更新方案。異步方案:
1、使用消息隊列進行緩存同步:更改代碼加入異步操作緩存的邏輯代碼(數據庫操作完畢以后,將要同步的數據發送到MQ中,MQ的消費者從MQ中獲取數據,然后更新緩存)
2、使用阿里巴巴旗下的canal組件實現數據同步:不需要更改業務代碼,部署一個canal服務。canal服務把自己偽裝成mysql的一個從節點,當mysql數據更新以后,canal會讀取binlog數據,然后再通過canal的客戶端獲取到數據,更新緩存即可。