考慮到絕大部分寫業務的程序員,在實際開發中使用 Redis 的時候,只會 Set Value 和 Get Value 兩個操做,對 Redis 總體缺少一個認知。java
因此我斗膽以 Redis 爲題材,對 Redis 常見問題作一個總結,但願可以彌補你們的知識盲點。python
爲何使用Redis程序員
使用Redis 有什麼缺點面試
單線程的Redis 爲何這麼快redis
Redis 的數據類型,以及每種數據類型的使用場景數據庫
Redis 的過時策略以及內存淘汰機制緩存
Redis 和數據庫雙寫一致性問題數據結構
如何應對緩存穿透和緩存雪崩問題併發
如何解決Redis 的併發競爭 Key 問題app
我以爲在項目中使用 Redis,主要是從兩個角度去考慮:性能和併發。固然,Redis 還具有能夠作分佈式鎖等其餘功能,可是若是隻是爲了分佈式鎖這些其餘功能,徹底還有其餘中間件,如 ZooKpeer 等代替,並非非要使用Redis。所以,這個問題主要從性能和併發兩個角度去答。
性能
以下圖所示,咱們在碰到須要執行耗時特別久,且結果不頻繁變更的 SQL,就特別適合將運行結果放入緩存。這樣,後面的請求就去緩存中讀取,使得請求可以迅速響應。
題外話:突然想聊一下這個迅速響應的標準。根據交互效果的不一樣,這個響應時間沒有固定標準。
不過曾經有人這麼告訴我:"在理想狀態下,咱們的頁面跳轉須要在瞬間解決,對於頁內操做則須要在剎那間解決。
另外,超過一彈指的耗時操做要有進度提示,而且能夠隨時停止或取消,這樣才能給用戶最好的體驗。"
那麼瞬間、剎那、一彈指具體是多少時間呢?
根據《摩訶僧祗律》記載:
一剎那者爲一念,二十念爲一瞬,二十瞬爲一彈指,二十彈指爲一羅預,二十羅預爲一須臾,一日一晚上有三十須臾。
那麼,通過周密的計算,一瞬間爲 0.36 秒、一剎那有 0.018 秒、一彈指長達 7.2 秒。
併發
以下圖所示,在大併發的狀況下,全部的請求直接訪問數據庫,數據庫會出現鏈接異常。
這個時候,就須要使用Redis 作一個緩衝操做,讓請求先訪問到Redis,而不是直接訪問數據庫。
你們用 Redis 這麼久,這個問題是必需要了解的,基本上使用 Redis 都會碰到一些問題,常見的也就幾個。
回答主要是四個問題:
緩存和數據庫雙寫一致性問題
緩存雪崩問題
緩存擊穿問題
緩存的併發競爭問題
這四個問題,我我的以爲在項目中是常碰見的,具體解決方案,後文給出。
這個問題是對Redis內部機制的一個考察。根據個人面試經驗,不少人都不知道Redis是單線程工做模型。因此,這個問題仍是應該要複習一下的。
回答主要是如下三點:
純內存操做
單線程操做,避免了頻繁的上下文切換
採用了非阻塞 I/O 多路複用機制
題外話:咱們如今要仔細的說一說 I/O 多路複用機制,由於這個說法實在是太通俗了,通俗到通常人都不懂是什麼意思。
打一個比方:小曲在 S 城開了一家快遞店,負責同城快送服務。小曲由於資金限制,僱傭了一批快遞員,而後小曲發現資金不夠了,只夠買一輛車送快遞。
經營方式一
客戶每送來一份快遞,小曲就讓一個快遞員盯着,而後快遞員開車去送快遞。
慢慢的小曲就發現了這種經營方式存在下述問題:
幾十個快遞員基本上時間都花在了搶車上了,大部分快遞員都處在閒置狀態,誰搶到了車,誰就能去送快遞。
隨着快遞的增多,快遞員也愈來愈多,小曲發現快遞店裏愈來愈擠,沒辦法僱傭新的快遞員了。
快遞員之間的協調很花時間。
綜合上述缺點,小曲痛定思痛,提出了下面的經營方式。
經營方式二
小曲只僱傭一個快遞員。而後呢,客戶送來的快遞,小曲按送達地點標註好,而後依次放在一個地方。
最後,那個快遞員依次的去取快遞,一次拿一個,而後開着車去送快遞,送好了就回來拿下一個快遞。
上述兩種經營方式對比,是否是明顯以爲第二種,效率更高,更好呢?
在上述比喻中:
每一個快遞員→每一個線程
每一個快遞→每一個 Socket(I/O 流)
快遞的送達地點→Socket 的不一樣狀態
客戶送快遞請求→來自客戶端的請求
小曲的經營方式→服務端運行的代碼
一輛車→CPU 的核數
因而咱們有以下結論:
經營方式一就是傳統的併發模型,每一個 I/O 流(快遞)都有一個新的線程(快遞員)管理。
經營方式二就是 I/O 多路複用。只有單個線程(一個快遞員),經過跟蹤每一個 I/O 流的狀態(每一個快遞的送達地點),來管理多個 I/O 流。
下面類比到真實的Redis 線程模型,如圖所示:
簡單來講,就是咱們的 redis-client 在操做的時候,會產生具備不一樣事件類型的 Socket。
在服務端,有一段I/O 多路複用程序,將其置入隊列之中。而後,文件事件分派器,依次去隊列中取,轉發到不一樣的事件處理器中。
須要說明的是,這個 I/O 多路複用機制,Redis 還提供了 select、epoll、evport、kqueue 等多路複用函數庫,你們能夠自行去了解。
是否是以爲這個問題很基礎?我也這麼以爲。然而根據面試經驗發現,至少百分之八十的人答不上這個問題。
建議,在項目中用到後,再類比記憶,體會更深,不要硬記。基本上,一個合格的程序員,五種類型都會用到。
String
這個沒啥好說的,最常規的 set/get 操做,Value 能夠是 String 也能夠是數字。通常作一些複雜的計數功能的緩存。
Hash
這裏 Value 存放的是結構化的對象,比較方便的就是操做其中的某個字段。
我在作單點登陸的時候,就是用這種數據結構存儲用戶信息,以 CookieId 做爲 Key,設置 30 分鐘爲緩存過時時間,能很好的模擬出相似 Session 的效果。
List
使用 List 的數據結構,能夠作簡單的消息隊列的功能。另外還有一個就是,能夠利用 lrange 命令,作基於 Redis 的分頁功能,性能極佳,用戶體驗好。
Set
由於 Set 堆放的是一堆不重複值的集合。因此能夠作全局去重的功能。爲何不用 JVM 自帶的 Set 進行去重?
由於咱們的系統通常都是集羣部署,使用 JVM 自帶的 Set,比較麻煩,難道爲了一個作一個全局去重,再起一個公共服務,太麻煩了。
另外,就是利用交集、並集、差集等操做,能夠計算共同喜愛,所有的喜愛,本身獨有的喜愛等功能。
Sorted Set
Sorted Set多了一個權重參數 Score,集合中的元素可以按 Score 進行排列。
能夠作排行榜應用,取 TOP N 操做。Sorted Set 能夠用來作延時任務。最後一個應用就是能夠作範圍查找。
這個問題至關重要,到底 Redis 有沒用到家,這個問題就能夠看出來。
好比你Redis 只能存 5G 數據,但是你寫了 10G,那會刪 5G 的數據。怎麼刪的,這個問題思考過麼?
還有,你的數據已經設置了過時時間,可是時間到了,內存佔用率仍是比較高,有思考過緣由麼?
回答:Redis 採用的是按期刪除+惰性刪除策略。
爲何不用定時刪除策略
定時刪除,用一個定時器來負責監視 Key,過時則自動刪除。雖然內存及時釋放,可是十分消耗 CPU 資源。
在大併發請求下,CPU 要將時間應用在處理請求,而不是刪除 Key,所以沒有采用這一策略。
按期刪除+惰性刪除是如何工做
按期刪除,Redis 默認每一個 100ms 檢查,是否有過時的 Key,有過時 Key 則刪除。
須要說明的是,Redis 不是每一個 100ms 將全部的 Key 檢查一次,而是隨機抽取進行檢查(若是每隔 100ms,所有 Key 進行檢查,Redis 豈不是卡死)。
所以,若是隻採用按期刪除策略,會致使不少 Key 到時間沒有刪除。因而,惰性刪除派上用場。
也就是說在你獲取某個 Key 的時候,Redis 會檢查一下,這個 Key 若是設置了過時時間,那麼是否過時了?若是過時了此時就會刪除。
採用按期刪除+惰性刪除就沒其餘問題了麼?
不是的,若是按期刪除沒刪除 Key。而後你也沒即時去請求 Key,也就是說惰性刪除也沒生效。這樣,Redis的內存會愈來愈高。那麼就應該採用內存淘汰機制。
在 redis.conf 中有一行配置:
# maxmemory-policy volatile-lru
該配置就是配內存淘汰策略的(什麼,你沒配過?好好檢討一下本身):
noeviction:當內存不足以容納新寫入數據時,新寫入操做會報錯。應該沒人用吧。
allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的 Key。推薦使用,目前項目在用這種。
allkeys-random:當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個 Key。應該也沒人用吧,你不刪最少使用 Key,去隨機刪。
volatile-lru:當內存不足以容納新寫入數據時,在設置了過時時間的鍵空間中,移除最近最少使用的 Key。這種狀況通常是把Redis 既當緩存,又作持久化存儲的時候才用。不推薦。
volatile-random:當內存不足以容納新寫入數據時,在設置了過時時間的鍵空間中,隨機移除某個 Key。依然不推薦。
volatile-ttl:當內存不足以容納新寫入數據時,在設置了過時時間的鍵空間中,有更早過時時間的 Key 優先移除。不推薦。
PS:若是沒有設置 expire 的 Key,不知足先決條件(prerequisites);那麼 volatile-lru,volatile-random 和 volatile-ttl 策略的行爲,和 noeviction(不刪除) 基本上一致。
一致性問題是分佈式常見問題,還能夠再分爲最終一致性和強一致性。數據庫和緩存雙寫,就必然會存在不一致的問題。
答這個問題,先明白一個前提。就是若是對數據有強一致性要求,不能放緩存。咱們所作的一切,只能保證最終一致性。
另外,咱們所作的方案從根本上來講,只能說下降不一致發生的機率,沒法徹底避免。所以,有強一致性要求的數據,不能放緩存。
回答:首先,採起正確更新策略,先更新數據庫,再刪緩存。其次,由於可能存在刪除緩存失敗的問題,提供一個補償措施便可,例如利用消息隊列。
這兩個問題,說句實在話,通常中小型傳統軟件企業,很難碰到這個問題。若是有大併發的項目,流量有幾百萬左右。這兩個問題必定要深入考慮。
緩存穿透,即黑客故意去請求緩存中不存在的數據,致使全部的請求都懟到數據庫上,從而數據庫鏈接異常。
緩存穿透解決方案:
利用互斥鎖,緩存失效的時候,先去得到鎖,獲得鎖了,再去請求數據庫。沒獲得鎖,則休眠一段時間重試。
採用異步更新策略,不管 Key 是否取到值,都直接返回。Value 值中維護一個緩存失效時間,緩存若是過時,異步起一個線程去讀數據庫,更新緩存。須要作緩存預熱(項目啓動前,先加載緩存)操做。
提供一個能迅速判斷請求是否有效的攔截機制,好比,利用布隆過濾器,內部維護一系列合法有效的 Key。迅速判斷出,請求所攜帶的 Key 是否合法有效。若是不合法,則直接返回。
緩存雪崩,即緩存同一時間大面積的失效,這個時候又來了一波請求,結果請求都懟到數據庫上,從而致使數據庫鏈接異常。
緩存雪崩解決方案:
給緩存的失效時間,加上一個隨機值,避免集體失效。
使用互斥鎖,可是該方案吞吐量明顯降低了。
雙緩存。咱們有兩個緩存,緩存 A 和緩存 B。緩存 A 的失效時間爲 20 分鐘,緩存 B 不設失效時間。本身作緩存預熱操做。
而後細分如下幾個小點:從緩存 A 讀數據庫,有則直接返回;A 沒有數據,直接從 B 讀數據,直接返回,而且異步啓動一個更新線程,更新線程同時更新緩存 A 和緩存 B。
這個問題大體就是,同時有多個子系統去 Set 一個 Key。這個時候你們思考過要注意什麼呢?
須要說明一下,我提早百度了一下,發現答案基本都是推薦用Redis 事務機制。
我並不推薦使用Redis 的事務機制。由於咱們的生產環境,基本都是 Redis 集羣環境,作了數據分片操做。
你一個事務中有涉及到多個 Key 操做的時候,這多個 Key 不必定都存儲在同一個 redis-server 上。所以,Redis 的事務機制,十分雞肋。
若是對這個 Key 操做,不要求順序
這種狀況下,準備一個分佈式鎖,你們去搶鎖,搶到鎖就作 set 操做便可,比較簡單。
若是對這個 Key 操做,要求順序
假設有一個 key1,系統 A 須要將 key1 設置爲 valueA,系統 B 須要將 key1 設置爲 valueB,系統 C 須要將 key1 設置爲 valueC。
指望按照 key1 的 value 值按照 valueA > valueB> valueC 的順序變化。這種時候咱們在數據寫入數據庫的時候,須要保存一個時間戳。
假設時間戳以下:
系統A key 1 {valueA 3:00}
系統B key 1 {valueB 3:05}
系統C key 1 {valueC 3:10}
那麼,假設這會系統 B 先搶到鎖,將 key1 設置爲{valueB 3:05}。接下來系統 A 搶到鎖,發現本身的 valueA 的時間戳早於緩存中的時間戳,那就不作 set 操做了,以此類推。
其餘方法,好比利用隊列,將 set 方法變成串行訪問也能夠。總之,靈活變通。
本文對Redis 的常見問題作了一個總結。大部分是本身在工做中遇到,以及以前面試別人的時候,愛問的一些問題。
合理利用本身每一分每一秒的時間來學習提高本身,不要再用「 沒有時間 」來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代!
文章來源:
https://my.oschina.net/u/3967312/blog/2088188
做者:Java乾貨分享
-END-