redis常見問題的總結

這篇文章希望對redis整體做一個總結。

一、使用redis的原因
1、redis 的響應速度快,將一些不頻換變動的查詢數據存入redis能夠提供更快的響應速度。
2、關係型數據庫的數據存儲在硬盤,在高併發環境下I/O較高,併發能力弱,一般我們使用redis 做緩衝代替直接訪問數據庫,能夠提高redis的併發量。
3、Redis爲單進程單線程模式,採用隊列模式將併發訪問變成串行訪問,且多客戶端對Redis的連接並不存在競爭關係,適合用於分佈式鎖。
4、redis的數據格式靈活,能夠在設置值的時候添加有效期。

二、redis響應快的原因
1、純內存訪問,Redis 將所有數據放在內存中,非數據同步正常工作時,是不需要從磁盤讀取數據的。(存儲在內存也決定了數據相對於存儲硬盤更易丟失 ,所以有了 RDB 持久化 和 AOF 持久化。內存相對硬盤造價更高,也決定了redis不能無限擴容,故有了過期策略和內存淘汰機制)

2、redis是單線程的,避免了頻繁的上下文切換。(因爲Redis是基於內存的操作,CPU不是Redis的瓶頸,Redis的瓶頸最有可能是機器內存的大小或者網絡帶寬。既然單線程容易實現,而且CPU不會成爲瓶頸,那就順理成章地採用單線程的方案了)

3、採用了非阻塞 I/O 多路複用機制。(Redis-client 在操作的時候,會產生具有不同事件類型的 Socket。在服務端,有一段 I/O 多路複用程序,將其置入隊列之中。然後,文件事件分派器,依次去隊列中取,轉發到不同的事件處理器中。 指的其實是在單個線程通過記錄跟蹤每一個Sock(I/O流)的狀態,來同時管理多個I/O流,提高了的提高服務器的吞吐能力。)

4、數據結構簡單,操作節省時間。(redis支持這 String 整數,浮點數或者字符串、Hash 散列表、Set 集合、Zset 有序集合、List 列表 這五種數據類型,同時也決定了沒辦法想關係性數據庫一樣直接在數據庫對數據進行復雜的操作)

三、redis五種常用場景介紹

String 整數,浮點數或者字符串:
最常規的 set/get 操作,Value 可以是 String 也可以是數字。一般做一些複雜的計數功能的緩存。

Hash 散列表
這裏 Value 存放的是結構化的對象,比較方便的就是操作其中的某個字段。能夠通過這種數據結構存儲用戶信息,以 CookieId 作爲 Key,設置 30 分鐘爲緩存過期時間,能很好的模擬出類似 Session 的效果。

List 列表
使用 List 的數據結構,可以做簡單的消息隊列的功能。另外,可以利用 lrange 命令,做基於 Redis 的分頁功能,性能極佳,用戶體驗好。

Set 集合
因爲 Set 堆放的是一堆不重複值的集合。所以可以做全局去重的功能。我們的系統一般都是集羣部署,使用 JVM 自帶的 Set 比較麻煩。另外,就是利用交集、並集、差集等操作,可以計算共同喜好,全部的喜好,自己獨有的喜好等功能。

Zset 有序集合
Sorted Set 多了一個權重參數 Score,集合中的元素能夠按 Score 進行排列。可以做排行榜應用,取 TOP N 操作。Sorted Set 可以用來做延時任務。

四、Redis 的過期刪除策略

1、定時刪除策略
定時刪除,用一個定時器來負責監視 Key,過期則自動刪除。雖然內存及時釋放,但是十分消耗 CPU 資源。在大併發請求下,CPU 要將時間應用在處理請求,而不是刪除 Key,因此沒有采用這一策略。

2、惰性刪除策略

放任鍵過期不管,但是每次從鍵空間中獲取鍵是,都檢查取得的鍵的過期時間,如果過期的話,刪除即可;惰性操作對於CPU來說是友好的,過期鍵只有在程序讀取時判斷是否過期才刪除掉,而且也只會刪除這一個過期鍵,但是對於內存來說是不友好的,如果多個鍵都已經過期了,而這些鍵又恰好沒有被訪問,那麼這部分的內存就都不會被釋放出來;

3、定期刪除策略:
每隔一段時間,程序就對數據庫進行一次檢查,刪除掉過期鍵;定期刪除是上面兩種方案的折中方案,每隔一段時間來刪除過期鍵,並通過限制刪除操作執行的時長和頻率來減少刪除操作對CPU時間的影響,除此之外,還有效的減少內存的浪費;但是該策略的難點在於間隔時長,這個需要根據自身業務情況來進行設置;

3.1 如何配置定期刪除執行時間間隔

redis的定時任務默認是每秒執行10次,如果要修改這個值,可以在redis.conf中修改hz的值。
redis.conf中,hz默認設爲10,提高它的值將會佔用更多的cpu,當然相應的redis將會更快的處理同時到期的許多key,以及更精確的去處理超時。 hz的取值範圍是1~500,通常不建議超過100,只有在請求延時非常低的情況下可以將值提升到100。

# The range is between 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is required.
hz 10

3.2 單線程的redis,如何知道要運行定時任務

redis是單線程的,線程不但要處理定時任務,還要處理客戶端請求,線程不能阻塞在定時任務或處理客戶端請求上,那麼,redis是如何知道何時該運行定時任務的呢?

Redis 的定時任務會記錄在一個稱爲最小堆的數據結構中。這個堆中,最快要執行的任務排在堆的最上方。在每個循環週期,Redis 都會將最小堆裏面已經到點的任務立即進行處理。處理完畢後,將最快要執行的任務還需要的時間記錄下來,這個時間就是接下來處理客戶端請求的最大時長,若達到了該時長,則暫時不處理客戶端請求而去運行定時任務

定期刪除+惰性刪除
定期刪除,Redis 默認每個 100ms 檢查,有過期 Key 則刪除。需要說明的是,Redis 不是每個 100ms 將所有的 Key 檢查一次,而是隨機抽取進行檢查。如果只採用定期刪除策略,會導致很多 Key 到時間沒有刪除。於是,惰性刪除派上用場。

五、內存淘汰機制

1、內存淘汰策略:

5.1.1、noeviction
當內存不足以容納新寫入數據時,新寫入操作會報錯。
5.1.2、allkeys-lru
當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的 Key。(redis 4.0 以前推薦使用)
5.1.3、allkeys-random
當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個 Key。(不推薦)
5.1.4、volatile-lru
當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的 Key。這種情況一般是把 Redis 既當緩存,又做持久化存儲的時候才用。(不推薦)
5.1.5、volatile-random
當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個 Key。(依然不推薦)
5.1.6、volatile-ttl
當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的 Key 優先移除。(不推薦)
5.1.7、volatile-lfu
在設置了過期時間的key中使用LFU算法淘汰key。在設置了過期時間的鍵空間中,key的最近被訪問的頻率進行淘汰,很少被訪問的優先被淘汰,被訪問的多的則被留下來。(不推薦)

5.1.8、allkeys-lfu
在所有的key中使用LFU算法淘汰數據,在鍵空間中,ey的最近被訪問的頻率進行淘汰,很少被訪問的優先被淘汰,被訪問的多的則被留下來。(Redis4.0 出現, 推薦)

2、更改內存淘汰策略
5.2.1、通過更改redis.conf更改內存淘汰策略

# maxmemory-policy noeviction

# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated
# algorithms (in order to save memory), so you can tune it for speed or
# accuracy. For default Redis will check five keys and pick the one that was
# used less recently, you can change the sample size using the following
# configuration directive.
#
# The default of 5 produces good enough results. 10 Approximates very closely
# true LRU but costs more CPU. 3 is faster but not very accurate.
#
# maxmemory-samples 5

5.2.2、通過命令行更改redis淘汰策略

[root@localhost src]# ./redis-cli -p 7000 -c 
127.0.0.1:7000> config get maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"
127.0.0.1:7000> config set maxmemory-policy allkeys-lfu
OK
127.0.0.1:7000> config get maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lfu"
127.0.0.1:7000> 

3、採用近視算法的原因
之所以採用近似算法,可以提高性能,不用每一次都去排序計算概率。

六、redis的持久化方式

RDB持久化
原理是將Reids在內存中的數據庫記錄定時dump到磁盤上的RDB持久化
優點:
1、保存了 Redis 在各個時間點上的數據集,能恢復到各個時間節點上。 這種文件非常適合用於進行備份: 比如說,你可以在最近的 24 小時內,每小時備份一次 RDB 文件,並且在每個月的每一天,也備份一個 RDB 文件。 這樣的話,即使遇上問題,也可以隨時將數據集還原到不同的版本。

2、數據丟失後更容易恢復。因爲我們可以非常輕鬆的將一個單獨的文件壓縮後再轉移到其它存儲介質上。

3、性能最大化。對於Redis的服務進程而言,在開始持久化時,它唯一需要做的只是fork出子進程,之後再由子進程完成這些持久化的工作,這樣就可以極大的避免服務進程執行IO操作了。

4、RDB 在恢復大數據集時的速度比 AOF 的恢復速度要快。

缺點:
1、RDB可能會造成數據丟失的可能性大於AOF,一旦發生故障停機, 可能會丟失好幾分鐘的數據。
2、可能造成redis停止處理客戶端數據。fork() 可能會非常耗時,造成服務器在某某毫秒內停止處理客戶端; 如果數據集非常巨大,並且 CPU 時間非常緊張的話,那麼這種停止時間甚至可能會長達整整一秒。

save 900 1    # 900 秒內有至少有 1 個鍵被改動 ,觸發保存
save 300 10   # 300 秒內有至少有 10 個鍵被改動,觸發保存
save 60 10000 # 60 秒內有至少有 1000 個鍵被改動,觸發保存

AOF持久化
原理是將Reids的操作日誌以追加的方式寫入文件
優點:
1、能夠同過採用配置宕機之後丟失更少的數據。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。每秒同步也是異步完成的,其效率也是非常高的,所差的是一旦系統出現宕機現象,那麼這一秒鐘之內修改的數據將會丟失。每修同步是是同步完成的,效率比較低。

2、 AOF 文件裏面,即使重寫過程中發生停機,現有的 AOF 文件也不會丟失。
3、AOF 文件有序地保存了對數據庫執行的所有寫入操作, 這些寫入操作以 Redis 協議的格式保存, 因此 AOF 文件的內容非常容易被人讀懂, 對文件進行分析(parse)也很輕鬆。 導出(export) AOF 文件也非常簡單: 舉個例子, 如果你不小心執行了 FLUSHALL 命令, 但只要 AOF 文件未被重寫, 那麼只要停止服務器, 移除 AOF 文件末尾的 FLUSHALL 命令, 並重啓 Redis , 就可以將數據集恢復到 FLUSHALL 執行之前的狀態。

缺點:
1、對於相同數量的數據集而言,AOF文件通常要大於RDB文件。RDB 在恢復大數據集時的速度比 AOF 的恢復速度要快。

開啓:
開啓AOF持久化

修改redis.conf配置文件,redis 默認開啓的.

appendonly yes
# appendfsync always(每修改同步)
appendfsync everysec(每秒同步)
# appendfsync no(每修改同步)

命令

config set appendonly yes

RDB與AOF如何選擇:
1、小孩才做選擇,大人全都要。應該同時使用兩種持久化方式。
2、如果可以忍受幾分鐘數據缺失可以採用RDB

六、緩存和數據庫雙寫一致性問題
一致性問題還可以再分爲最終一致性和強一致性。數據庫和緩存雙寫,就必然會存在不一致的問題。前提是如果對數據有強一致性要求,不能放緩存。我們所做的一切,只能保證最終一致性。

解決方案:
1、首先需要按照比較好的一個方案,對數據進行更新。先更新數據庫,再刪緩存。這種情況下也不是沒有可能造成數據不一致了,如果下面這種情況:
(1)緩存剛好失效
(2)請求A查詢數據庫,得一箇舊值
(3)請求B將新值寫入數據庫
(4)請求B刪除緩存
(5)請求A將查到的舊值寫入緩存
造成這種情況只有 必須步驟(3)的寫數據庫操作比步驟(2)的讀數據庫操作耗時更短,纔有可能使得步驟(4)先於步驟(5),這種情況發生概率太小,也是目前比較合適的一種處理方案。如果出現刪除緩存失敗的情況,可以添加一個異步隊列對緩存進行更新。

七、緩存雪崩問題

1、使用互斥鎖,但是該方案吞吐量明顯下降了。
2、 給緩存的失效時間,加上一個隨機值,避免集體失效,這個方案是工作更爲常用方式。

3、雙緩存。我們有兩個緩存,緩存 A 和緩存 B。緩存 A 的失效時間爲 20 分鐘,緩存 B 不設失效時間。自己做緩存預熱操作。

然後細分以下幾個小點:從緩存 A 讀數據庫,有則直接返回;A 沒有數據,直接從 B 讀數據,直接返回,並且異步啓動一個更新線程,更新線程同時更新緩存 A 和緩存 B。

八、緩存穿透問題

指查詢一個不存在的數據時候,由於緩存是不命中時,出於容錯考慮,如果從存儲層查不到數據則不寫入緩存,這將導致這個不存在的數據每次。請求都要到存儲層去查詢,失去了緩存的意義。在流量大時,可能DB就掛掉了,要是有人利用不存在的key頻繁攻擊我們的應用,這就是漏洞。
  
解決方案:
1、利用互斥鎖,緩存失效的時候,先去獲得鎖,得到鎖了,再去請求數據庫。沒得到鎖,則休眠一段時間重試。這個會大大降低併發量,一般不會採用。
2、採用異步更新策略,無論 Key 是否取到值,都直接返回。Value 值中維護一個緩存失效時間,緩存如果過期,異步起一個線程去讀數據庫,更新緩存。需要做緩存預熱(項目啓動前,先加載緩存)操作。

3、提供一個能迅速判斷請求是否有效的攔截機制,比如,利用布隆過濾器,內部維護一系列合法有效的 Key。迅速判斷出,請求所攜帶的 Key 是否合法有效。如果不合法,則直接返回。

九、緩存的併發競爭問題

多客戶端同時併發寫一個key,可能本來應該先到的數據後到了,導致數據緩存錯誤。如:客戶端A一個key的值修改爲valueA,客戶端B修改爲改爲valueB。原本先改爲valueA再改爲valueB,但valueB卻先到了,然後被改成了valueA。

解決方案:
1、首先使用分佈式鎖,確保同一時間,只能有一個系統實例在操作某個key。
2、key的值時,將value值攜帶時間(如valueB 02292317)。 要先判斷這值的時間戳是否比緩存裏的值的時間戳更靠後,如果是舊數據就不要更新了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章