Java面試題(六)--Redis

語言: CN / TW / HK

1 Redis基礎篇

1、簡單介紹一下Redis優點和缺點?

優點:

1、本質上是一個 Key-Value 型別的記憶體資料庫,很像memcached

2、整個資料庫統統載入在記憶體當中進行操作,定期通過非同步操作把資料庫資料 flush 到硬碟上進行儲存

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

4、Redis最大的魅力是支援儲存多種資料結構(string,list,set,hash,sortedset),此外單個 value 的最大限制是 1GB,不像memcached只能儲存 1MB 的資料

5、Redis也可以對存入的 Key-Value 設定 expire 時間,因此也可以被當作一個功能加強版的memcached 來用

缺點:

1、Redis 的主要缺點是資料庫容量受到實體記憶體的限制,不能用作海量資料的高效能讀寫,因此 Redis 適合的場景主要侷限在較小資料量的高效能操作和運算上。

2、沒有豐富的搜尋功能

2、系統中為什麼要使用快取?

主要從“高效能”和“高併發”這兩點來看待這個問題。

高效能:

假如使用者第一次訪問資料庫中的某些資料。這個過程會比較慢,因為是從硬碟上讀取的。將該使用者訪問的資料存在數快取中,這樣下一次再訪問這些資料的時候就可以直接從快取中獲取了。操作快取就是直接操作記憶體,所以速度相當快。如果資料庫中的對應資料改變的之後,同步改變快取中相應的資料即可!

高併發:

直接操作快取能夠承受的請求是遠遠大於直接訪問資料庫的,所以我們可以考慮把資料庫中的部分資料轉移到快取中去,這樣使用者的一部分請求會直接到快取這裡而不用經過資料庫。

3、常見的快取同步方案都有哪些?(高頻)

同步方案:更改程式碼業務程式碼,加入同步操作快取邏輯的程式碼(資料庫操作完畢以後,同步操作快取)

非同步方案:

1、使用訊息佇列進行快取同步:更改程式碼加入非同步操作快取的邏輯程式碼(資料庫操作完畢以後,將要同步的資料傳送到MQ中,MQ的消費者從MQ中獲取資料,然後更新快取)

2、使用阿里巴巴旗下的canal元件實現資料同步:不需要更改業務程式碼,部署一個canal服務。canal服務把自己偽裝成mysql的一個從節點,當mysql資料更新以後,canal會讀取binlog資料,然後在通過canal的客戶端獲取到資料,更新快取即可。

4、Redis常見資料結構以及使用場景有哪些?(高頻)

1、 string

常見命令:set、get、decr、incr、mget等。

基本特點:string資料結構是簡單的key-value型別,value其實不僅可以是String,也可以是數字。

應用場景:常規計數:微博數,粉絲數等。

2、hash

常用命令: hget、hset、hgetall等。

基本特點:hash 是一個 string 型別的 field 和 value 的對映表,hash 特別適合用於儲存物件,後續操作的時候,你可以直接僅僅修改這個物件中的某個欄位的值。

應用場景:儲存使用者資訊,商品資訊等。

3、list

常用命令: lpush、rpush、lpop、rpop、lrange等。

基本特點:類似於Java中的list可以儲存多個數據,並且資料可以重複,而且資料是有序的。

應用場景:儲存微博的關注列表,粉絲列表等。

4、set

常用命令: sadd、spop、smembers、sunion 等

基本特點:類似於Java中的set集合可以儲存多個數據,資料不可以重複,使用set集合不可以保證資料的有序性。

應用場景:可以利用Redis的集合計算功能,實現微博系統中的共同粉絲、公告關注的使用者列表計算。

5、sorted set

常用命令: zadd、zrange、zrem、zcard 等。

基本特點:和set相比,sorted set增加了一個權重引數score,使得集合中的元素能夠按score進行有序排列。

應用場景:在直播系統中,實時排行資訊包含直播間線上使用者列表,各種禮物排行榜等。

5、Redis有哪些資料刪除策略?(高頻)

資料刪除策略:Redis中可以對資料設定資料的有效時間,資料的有效時間到了以後,就需要將資料從記憶體中刪除掉。而刪除的時候就需要按照指定的規則進行刪除,這種刪除規則就被稱之為資料的刪除策略。

Redis中資料的刪除策略:

① 定時刪除

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

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

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

② 惰性刪除

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

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

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

③ 定期刪除

  • 概述:每隔一段時間,我們就對一些key進行檢查,刪除裡面過期的key(從一定數量的資料庫中取出一定數量的隨機鍵進行檢查,並刪除其中的過期鍵)。

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

  • 缺點:難以確定刪除操作執行的時長和頻率。

    如果執行的太頻繁,定期刪除策略變得和定時刪除策略一樣,對CPU不友好。如果執行的太少,那又和惰性刪除一樣了,過期鍵佔用的記憶體不會及時得到釋放。

另外最重要的是,在獲取某個鍵時,如果某個鍵的過期時間已經到了,但是還沒執行定期刪除,那麼就會返回這個鍵的值,這是業務不能忍受的錯誤。

Redis的過期刪除策略: 惰性刪除 + 定期刪除 兩種策略進行配合使用定期刪除函式的執行頻率,在Redis2.6版本中,規定每秒執行10次,大概100ms執行一次。在Redis2.8版本後,可以通過修改配置檔案redis.conf 的 hz 選項來調整這個次數。

6、Redis中有哪些資料淘汰策略?(高頻)

資料的淘汰策略:當Redis中的記憶體不夠用時,此時在向Redis中新增新的key,那麼Redis就會按照某一種規則將記憶體中的資料刪除掉,這種資料的刪除規則被稱之為記憶體的淘汰策略。

常見的資料淘汰策略:

noeviction # 不刪除任何資料,記憶體不足直接報錯(預設策略)

volatile-lru # 挑選最近最久使用的資料淘汰(舉例:key1是在3s之前訪問的, key2是在9s之前訪問的,刪除的就是key2)

volatile-lfu # 挑選最近最少使用資料淘汰 (舉例:key1最近5s訪問了4次, key2最近5s訪問了9次, 刪除的就是key1)

volatile-ttl # 挑選將要過期的資料淘汰

volatile-random # 任意選擇資料淘汰

allkeys-lru # 挑選最近最少使用的資料淘汰

allkeys-lfu # 挑選最近使用次數最少的資料淘汰

allkeys-random # 任意選擇資料淘汰,相當於隨機

注意:

1、不帶allkeys字樣的淘汰策略是隨機從Redis中選擇指定的數量的key然後按照對應的淘汰策略進行刪除,帶allkeys是對所有的key按照對應的淘汰策略進行刪除。

2、快取淘汰策略常見配置項

maxmemory-policy noeviction # 配置淘汰策略

maxmemory ?mb # 最大可使用記憶體,即佔用實體記憶體的比例,預設值為0,表示不限制。生產環境中根據需求設定,通常設定在50%以上。

maxmemory-samples count # 設定redis需要檢查key的個數

7、Redis中資料庫預設是多少個db即作用?

Redis預設支援16個數據庫,可以通過配置databases來修改這一數字。客戶端與Redis建立連線後會自動選擇0號資料庫,不過可以隨時使用select命令更換資料庫。

Redis支援多個數據庫,並且每個資料庫是隔離的不能共享,並且基於單機才有,如果是叢集就沒有資料庫的概念。

8、快取穿透、快取擊穿、快取雪崩解決方案?(高頻)

加入快取以後的資料查詢流程:

快取穿透:

概述:指查詢一個一定 不存在 的資料,如果從儲存層查不到資料則不寫入快取,這將導致這個不存在的資料每次請求都要到 DB 去查詢,可能導致 DB 掛掉。

解決方案:

1、查詢返回的資料為空,仍把這個空結果進行快取,但過期時間會比較短

2、布隆過濾器:將所有可能存在的資料雜湊到一個足夠大的 bitmap 中,一個一定不存在的資料會被這個 bitmap 攔截掉,從而避免了對DB的查詢

快取擊穿:

概述:對於設定了過期時間的key,快取在某個時間點過期的時候,恰好這時間點對這個Key有大量的併發請求過來,這些請求發現快取過期一般都會從後端 DB 載入資料並回設到快取 ,這個時候大併發的請求可能會瞬間把 DB 壓垮。

解決方案:

1、使用互斥鎖:當快取失效時,不立即去load db,先使用如 Redis 的 setnx 去設定一個互斥鎖,當操作成功返回時再進行 load db的操作並回設快取,否則重試get快取的方法

2、永遠不過期:不要對這個key設定過期時間

快取雪崩:

概述:設定快取時採用了相同的過期時間,導致快取在某一時刻 同時失效 ,請求全部轉發到DB,DB 瞬時壓力過重雪崩。與快取擊穿的區別:雪崩是很多key,擊穿是某一個key快取。

解決方案:

將快取失效時間分散開,比如可以在原有的失效時間基礎上增加一個 隨機值 ,比如1-5分鐘隨機,這樣每一個快取的過期時間的重複率就會降低,就很難引發集體失效的事件。

9、什麼是布隆過濾器?(高頻)

概述:布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實際上由一個很長的二進位制向量(二進位制陣列)和一系列隨機對映函式(hash函式)。

作用:布隆過濾器可以用於檢索一個元素是否在一個集合中。

新增元素:將商品的id(id1)儲存到布隆過濾器

假設當前的布隆過濾器中提供了三個hash函式,此時就使用三個hash函式對id1進行雜湊運算,運算結果分別為:1、4、9那麼就會陣列中對應的位置資料更改為1。

判斷資料是否存在:使用相同的hash函式對資料進行雜湊運算,得到雜湊值。然後判斷該雜湊值所對應的陣列位置是否都為1,如果不都是則說明該資料

肯定不存在。如果是說明該資料 可能 存在,因為雜湊運算可能就會存在重複的情況。如下圖所示:

假設新增完id1和id2資料以後,布隆過濾器中資料的儲存方式如上圖所示,那麼此時要判斷id3對應的資料在布隆過濾器中是否存在,按照上述的判斷規則應該是存在,但是id3這個資料在布隆過濾器中壓根就不存在,這種情況就屬於誤判。

誤判率:陣列越小誤判率就越大,陣列越大誤判率就越小,但是同時帶來了更多的記憶體消耗。

刪除元素:布隆布隆器不支援資料的刪除操作,因為如果支援刪除那麼此時就會影響判斷不存在的結果。

使用布隆過濾器:在谷歌的guava快取工具中提供了布隆過濾器的實現,使用方式如下所示:

pom.xml檔案


com.google.guava
guava
20.0

測試程式碼:

// 建立一個BloomFilter物件

// 第一個引數:布隆過濾器判斷的元素的型別

// 第二個引數:布隆過濾器儲存的元素個數

// 第三個引數:誤判率,預設值為0.03

int size = 100_000 ;

BloomFilter

bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), size, 0.03);

for(int x = 0 ; x < size ; x++) {

bloomFilter.put("add" + x) ;

}

// 在向其中新增100000個數據測試誤判率

int count = 0 ;     // 記錄誤判的資料條數

for(int x = size ; x < size * 2 ; x++) {

if(bloomFilter.mightContain("add" + x)) {

count++ ;

System.out.println(count + "誤判了");

}

}

// 輸出

System.out.println("總的誤判條數為:" + count);

Redis中使用布隆過濾器防止快取穿透流程圖如下所示:

10、Redis資料持久化有哪些方式?各自有什麼優缺點?(高頻)

在Redis中提供了兩種資料持久化的方式:1、RDB 2、AOF

RDB:定期更新,定期將Redis中的資料生成的快照同步到磁碟等介質上,磁碟上儲存的就是Redis的記憶體快照

優點:資料檔案的大小相比於aof較小,使用rdb進行資料恢復速度較快

缺點:比較耗時,存在丟失資料的風險

AOF:將Redis所執行過的所有指令都記錄下來,在下次Redis重啟時,只需要執行指令就可以了

優點:資料丟失的風險大大降低了

缺點:資料檔案的大小相比於rdb較大,使用aof檔案進行資料恢復的時候速度較慢

11、Redis都存在哪些叢集方案?

在Redis中提供的叢集方案總共有三種:

1、主從複製

  • 保證高可用性

  • 實現故障轉移需要手動實現

  • 無法實現海量資料儲存

2、哨兵模式

  • 保證高可用性

  • 可以實現自動化的故障轉移

  • 無法實現海量資料儲存

  • 監控

  • 故障轉移

  • 通知客戶端

3、Redis分片叢集

  • 保證高可用性

  • 可以實現自動化的故障轉移

  • 可以實現海量資料儲存

12、說說Redis雜湊槽的概念?

Redis 叢集沒有使用一致性 hash,而是引入了雜湊槽的概念,Redis 叢集有 16384 個雜湊槽,每個 key通過 CRC16 校驗後對 16384 取模來決定放置哪個槽,叢集的每個節點負責一部分 hash 槽。

13、Redis中的管道有什麼用?

一次請求/響應伺服器能實現處理新的請求即使舊的請求還未被響應,這樣就可以將多個命令傳送到服務 器,而不用等待回覆,最後在一個步驟中讀取該答覆。

14、談談你對Redis中事務的理解?(高頻)

事務是一個原子操作:事務中的命令要麼全部被執行,要麼全部都不執行。

Redis中的事務:Redis事務的本質是一組命令的集合。事務支援一次執行多個命令,一個事務中所有命令都會被序列化。在事務執行過程,會按照順序序列化執行佇列中的命令,其他客戶端提交的命令請求不會插入到事務執行命令序列中。

總結說:Redis事務就是一次性、順序性、排他性的執行一個佇列中的一系列命令。

15、Redis事務相關的命令有哪幾個?(高頻)

事務相關的命令:

1、MULTI:用來組裝一個事務

2、EXEC:執行一個事務

3、DISCARD:取消一個事務

4、WATCH:用來監視一些key,一旦這些key在事務執行之前被改變,則取消事務的執行

5、UNWATCH:取消 WATCH 命令對所有key的監視

如下所示:

16、Redis如何做記憶體優化?

儘可能使用散列表(hash),散列表(是說散列表裡面儲存的數少)使用的記憶體非常小,所以你應該儘可能的將你的資料模型抽象到一個散列表裡面。

比如你的 web 系統中有一個使用者物件,不要為這個使用者的名稱,姓氏,郵箱,密碼設定單獨的key,而是應該把這個使用者的所有資訊儲存到一張散列表裡面。

17、Redis是單線的,但是為什麼還那麼快?(高頻)

Redis總體快的原因:

1、完全基於記憶體的

2、採用單執行緒,避免不必要的上下文切換可競爭條件

3、使用多路I/O複用模型,非阻塞IO

2 分散式鎖篇

18、什麼是分散式鎖?

概述:在分散式系統中,多個執行緒訪問共享資料就會出現資料安全性的問題。而由於jdk中的鎖要求多個執行緒在同一個jvm中,因此在分散式系統中無法使用jdk中的鎖保證資料的安全性,那麼此時就需要使用分散式鎖。

作用:可以保證在分散式系統中多個執行緒訪問共享資料時資料的安全性

舉例:

在電商系統中,使用者在進行下單操作的時候需要扣減庫存。為了提高下單操作的執行效率,此時需要將庫存的資料儲存到Redis中。訂單服務每一次生成訂單之前需要查詢一下庫存資料,如果存在則生成訂單同時扣減庫存。在高併發場景下會存在多個訂單服務操作Redis,此時就會出現執行緒安全問題。

分散式鎖的工作原理:

分散式鎖應該具備哪些條件:

1、在分散式系統環境下,一個方法在同一時間只能被一個機器的一個執行緒執行

2、高可用的獲取鎖與釋放鎖

3、高效能的獲取鎖與釋放鎖

4、具備可重入特性

5、具備鎖失效機制,防止死鎖

可重入特性:獲取到鎖的執行緒再次呼叫需要鎖的方法的時候,不需要再次獲取鎖物件。

使用場景:遍歷樹形選單的時候的遞迴呼叫。

注意:鎖具備可重入性的主要目的是為了防止死鎖。

19、分散式鎖的實現方案都有哪些?(高頻)

分散式鎖的實現方案:

1、資料庫

2、zookeeper

3、redis

20、Redis怎麼實現分散式鎖思路?(高頻)

Redis實現分散式鎖主要利用Redis的 setnx 命令。setnx是SET if not exists(如果不存在,則 SET)的簡寫。

127.0.0.1:6379> setnx lock value1 #在鍵lock不存在的情況下,將鍵key的值設定為value1

(integer) 1

127.0.0.1:6379> setnx lock value2 #試圖覆蓋lock的值,返回0表示失敗

(integer) 0

127.0.0.1:6379> get lock #獲取lock的值,驗證沒有被覆蓋

"value1"

127.0.0.1:6379> del lock #刪除lock的值,刪除成功

(integer) 1

127.0.0.1:6379> setnx lock value2 #再使用setnx命令設定,返回0表示成功

(integer) 1

127.0.0.1:6379> get lock #獲取lock的值,驗證設定成功

"value2"

上面這幾個命令就是最基本的用來完成分散式鎖的命令。

加鎖:使用 setnx key value 命令,如果key不存在,設定value(加鎖成功)。如果已經存在lock(也就是有客戶端持有鎖了),則設定失敗(加鎖失敗)。

解鎖:使用 del 命令,通過刪除鍵值釋放鎖。釋放鎖之後,其他客戶端可以通過 setnx 命令進行加鎖。

21、Redis實現分散式鎖如何防止死鎖現象?(高頻)

產生死鎖的原因:如果一個客戶端持有鎖的期間突然崩潰了,就會導致無法解鎖,最後導致出現死鎖的現象。

所以要有個 超時的機制 ,在設定key的值時,需要加上有效時間,如果有效時間過期了,就會自動失效,就不會出現死鎖。然後加鎖的程式碼就會變成這樣。

22、Redis實現分散式鎖如何合理的控制鎖的有效時長?(高頻)

有效時間設定多長,假如我的業務操作比有效時間長?我的業務程式碼還沒執行完就自動給我解鎖了,不就完蛋了嗎。

解決方案:

1、第一種:程式設計師自己去把握,預估一下業務程式碼需要執行的時間,然後設定有效期時間比執行時間長一些,保證不會因為自動解鎖影響到客戶端業務程式碼的執行。

2、第二種:給鎖續期。

鎖續期實現思路:當加鎖成功後,同時開啟守護執行緒,預設有效期是使用者所設定的,然後每隔10秒就會給鎖續期到使用者所設定的有效期,只要持有鎖的客戶端沒有宕機,就能保證一直持有鎖,直到業務程式碼執行完畢由客戶端自己解鎖,如果宕機了自然就在有效期失效後自動解鎖。

上述的第二種解決方案可以使用redis官方所提供的Redisson進行實現。

Redisson是Redis官方推薦的Java版的Redis客戶端。它提供的功能非常多,也非常強大分散式服務,使用Redisson可以輕鬆的實現分散式鎖。Redisson中進行鎖續期的這種機制被稱為" 看門狗 "機制。

redission支援4種連線redis方式,分別為 單機 、主從、Sentinel、Cluster 叢集。

23、Redis實現分散式鎖如何保證鎖服務的高可用?(高頻)

解決方案:

1、使用Redis的哨兵模式構建一個主從架構的Redis叢集

2、使用Redis Cluster叢集

24、當同步鎖資料到從節點之前,主節點宕機了導致鎖失效,那麼此時其他執行緒就可以再次獲取到鎖,這個問題怎麼解決?(高頻)

使用Redission框架中的 RedLock 進行處理。

RedLock的方案基於2個前提:

1、不再需要部署從庫和哨兵例項,只部署主庫

2、但主庫要部署多個,官方推薦至少5個例項

也就是說,想使用RedLock,你至少要部署5個Redis例項,而且都是主庫,它們之間沒有任何關係,都是一個個孤立的例項。

工作流程如下所示:

1、客戶端先獲取【當前時間戳T1】

2、客戶端依次向這個5個Redis例項發起加鎖請求,且每個請求會設定超時時間(毫秒級,要遠小於鎖的有效時間),如果某一個例項加鎖失敗(包括網路超時,鎖被其他的人持有等各種異常情況),就立即向下一個Redis例項申請加鎖

3、如果客戶端從 >=3 個(大多數)以上Redis例項加鎖成功,則再次獲取【當前時間戳T2】, 如果 T2 - T1 < 鎖的過期時間,此時,認為客戶端加鎖成功,否則加鎖失敗

4、加鎖成功,去操作共享資源

5、加鎖失敗,向【全部節點】發起釋放鎖請求

總結4個重點:

1、客戶端在多個Redis例項上申請加鎖

2、必須保證大多數節點加鎖成功

3、大多數節點加鎖的總耗時,要小於鎖設定的過期時間

4、鎖釋放,要向全部節點發起釋放鎖請求

24.1 為什麼要在多個例項上加鎖?

本質上是為了【容錯】, 部分例項異常宕機,剩餘的例項加鎖成功,整個鎖服務依舊可用。

24.2 為什麼步驟3加鎖成功後,還要計算加鎖的累計耗時?

因為操作的是多個節點,所以耗時肯定會比操作單個例項耗時更久,而且,因為是網路請求,網路情況是複雜的,有可能存在延遲、丟包、超時等情況發生,網路請求越多,異常發生的概率就越大。所以,即使大多數節點加鎖成功,如果加鎖的累計耗時已經超過了鎖的過期時間,那此時有些例項上的鎖可能已經失效了,這個鎖就沒有意義了。

程式碼大致如下所示:

Config config1 = new Config();

config1.useSingleServer().setAddress("redis://192.168.0.1:5378").setPassword("a123456").setDatabase(0);

RedissonClient redissonClient1 = Redisson.create(config1);

Config config2 = new Config();

config2.useSingleServer().setAddress("redis://192.168.0.1:5379").setPassword("a123456").setDatabase(0);

RedissonClient redissonClient2 = Redisson.create(config2);

Config config3 = new Config();

config3.useSingleServer().setAddress("redis://192.168.0.1:5380").setPassword("a123456").setDatabase(0);

RedissonClient redissonClient3 = Redisson.create(config3);

String resourceName = "REDLOCK_KEY";

RLock lock1 = redissonClient1.getLock(resourceName);

RLock lock2 = redissonClient2.getLock(resourceName);

RLock lock3 = redissonClient3.getLock(resourceName);

// 向3個redis例項嘗試加鎖

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

boolean isLock;

try {

// isLock = redLock.tryLock();

// 500ms拿不到鎖, 就認為獲取鎖失敗。10000ms即10s是鎖失效時間。

isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);

System.out.println("isLock = "+isLock);

if (isLock) {

//TODO if get lock success, do something;

}

} catch (Exception e) {

} finally {

// 無論如何, 最後都要解鎖

redLock.unlock();

}