Redis 核心知識點深度剖析:原理、機制與應(yīng)用
在當今數(shù)字化浪潮洶涌澎湃的時代,數(shù)據(jù)如同企業(yè)的生命線,高效的數(shù)據(jù)存儲、處理與管理成為眾多應(yīng)用程序成功的關(guān)鍵。在這數(shù)據(jù)管理的競技場上,Redis作為一款開源的內(nèi)存數(shù)據(jù)結(jié)構(gòu)存儲系統(tǒng),憑借其卓越的性能、豐富的數(shù)據(jù)結(jié)構(gòu)和強大的功能,脫穎而出,成為開發(fā)者手中的得力利器。無論是高并發(fā)場景下的數(shù)據(jù)緩存,還是實時數(shù)據(jù)分析、消息隊列等應(yīng)用,Redis都展現(xiàn)出無可替代的價值。
本文將深入探討Redis的核心知識點,帶你領(lǐng)略其內(nèi)部的奧秘,助力你在數(shù)據(jù)處理的領(lǐng)域中如魚得水。
一、詳解Redis基礎(chǔ)知識點
1. 為什么Redis被設(shè)計成是單線程的
redis本質(zhì)上都是在內(nèi)存操作,性能瓶頸不在CPU,通過單線程處理客戶端指令可以避免線程上下文切換開銷。
此時如果使用多線程進行操作,勢必要保護臨界資源并發(fā)安全而采用較粗力度的鎖,由此導(dǎo)致的大量線程阻塞爭搶臨界資源而導(dǎo)致操作各種大耗時操作顯然是得不償失的,并且多線程操作一般會引入各種同步原語,對于我們這種動輒十幾萬的內(nèi)存數(shù)據(jù)庫問題的定位和排查的難度都會大大增加。
2. 為什么Redis單線程也能這么快
- 通過IO多路復(fù)用保證單線程處理多連接
- 數(shù)據(jù)結(jié)構(gòu)做了極致的優(yōu)化
- 活躍于內(nèi)存即純粹的內(nèi)存操作,性能表現(xiàn)出色
- 單線程處理所有指令,避免線程上下文切換和同步原語的使用的開銷
3. 說說Redis6.0中的多線程
Redis6.0的多線程是用多線程來處理數(shù)據(jù)的讀寫和協(xié)議解析,但是Redis執(zhí)行命令還是單線程的:
4. Redis管道pipeline的概念
特定場景下我們某些業(yè)務(wù)需要針對redis執(zhí)行多條指令,按照傳統(tǒng)做法我們需要逐條發(fā)送指令,這樣的做法使得一個業(yè)務(wù)針對redis的操作存在多次網(wǎng)絡(luò)往返即多次RTT:
于是就有了pipeline的概念,通過管道一次性將要執(zhí)行的多條命令發(fā)送給服務(wù)端,其作用是為了降低 RTT(Round Trip Time) 對性能的影響,redis收到這些指令之后會依次執(zhí)行并響應(yīng)給客戶端:
Redis 服務(wù)端接收到管道發(fā)送過來的多條命令后,會一直執(zhí)命令,并將命令的執(zhí)行結(jié)果進行緩存,直到最后一條命令執(zhí)行完成,再所有命令的執(zhí)行結(jié)果一次性返回給客戶端 。
在性能方面, pipeline 有下面兩個優(yōu)勢:
- 節(jié)省了RTT:將多條命令打包一次性發(fā)送給服務(wù)端,減少了客戶端與服務(wù)端之間的網(wǎng)絡(luò)調(diào)用次數(shù)。
- 減少了上下文切換:當客戶端/服務(wù)端需要從網(wǎng)絡(luò)中讀寫數(shù)據(jù)時,都會產(chǎn)生一次系統(tǒng)調(diào)用,系統(tǒng)調(diào)用是非常耗時的操作,其中設(shè)計到程序由用戶態(tài)切換到內(nèi)核態(tài),再從內(nèi)核態(tài)切換回用戶態(tài)的過程。當我們執(zhí)行 10 條 redis 命令的時候,就會發(fā)生 10 次用戶態(tài)到內(nèi)核態(tài)的上下文切換,但如果我們使用 pipeline將多條命令打包成一條一次性發(fā)送給服務(wù)端,就只會產(chǎn)生一次上下文切換。
唯一需要注意的是redis的pipeline不保證原子性,即使我們通過pipeline處理多條指令,它也是逐條執(zhí)行的,這一點我們還是需要注意一下。
5. Redis如何保證命令原子性
使用原子命令:
- Redis 提供了 INCR/DECR/SETNX 命令,把RMW三個操作轉(zhuǎn)變?yōu)橐粋€原子操作
- Redis 是使用單線程串行處理客戶端的請求來操作命令,所以當 Redis 執(zhí)行某個命令操作時,其他命令是無法執(zhí)行的,這相當于命令操作是互斥執(zhí)行的.
加鎖:
加鎖主要是考慮多個客戶端對相同業(yè)務(wù)方法進行修改操作,我們可以使用加鎖的方式保證原子性,大致的方式為:
- 使用setnx上鎖
- 上鎖成功后,執(zhí)行業(yè)務(wù)修改操作
- 使用del釋放鎖
這期間你可能會遇到兩個問題:
- 假如在操作期間出現(xiàn)了業(yè)務(wù)異常(或者服務(wù)器宕機了),就會導(dǎo)致key未能及時釋放,進而導(dǎo)致鎖無法釋放,我們必須對這個鎖設(shè)置時效,并且在操作期間定時監(jiān)測和續(xù)命。
SET key value [EX seconds | PX milliseconds] [NX]
誤刪除,比如用戶1持有鎖,用戶2拿不到鎖,用del命令把這個鎖刪除,對此我們可以使用setnx的value比對看看上鎖和用戶和解鎖的用戶是不是同一個進行進一步的操作。
SET lock_key unique_value NX PX 10000
使用lua腳本:多個操作寫到一個 Lua 腳本中(Redis 會把整個 Lua 腳本作為一個整體執(zhí)行,在執(zhí)行的過程中不會被其他命令打斷,從而保證了 Lua 腳本中操作的原子性)
local current current = redis.call("incr",KEYS[1])
if tonumber(current) == 1
then redis.call("expire",KEYS[1],60)
end
6. Redis 使用什么協(xié)議進行通信
是resp自己設(shè)計的RESP協(xié)議,該協(xié)議的特點為:
- 簡單
- 高效
- 易于解析
- 保證二進制安全
7. Redis 與 Memcached 有什么區(qū)別
- 數(shù)據(jù)結(jié)構(gòu)層面:redis支持多種數(shù)據(jù)結(jié)構(gòu)例如字符串、列表、集合、有序集合、哈希,Memcached 僅僅支持簡單的鍵值對存儲。
- 持久化層面:Redis 支持RDB或者AOF的方式進行持久化,后者不支持持久化。
- 數(shù)據(jù)分片層面:redis通過hash slot實現(xiàn)自動分片和負載均衡,而后者只能手動進行分片。
- 處理數(shù)據(jù)的方式:redis通過單線程處理所有的指令,并且支持事務(wù)、lua腳本等高級功能,而后者使用多線程處理請求,且僅僅支持get、set操作。
- 協(xié)議:redis使用自定義的resp協(xié)議、同時支持多個數(shù)據(jù)庫并且支持密碼認證,而后者僅僅支持文本協(xié)議且只有一個默認的數(shù)據(jù)庫。
- 內(nèi)存管理:redis內(nèi)存層面各種緩存置換、數(shù)據(jù)持久化等策略相比后者更加健壯和復(fù)雜。
8. Redis為什么這么快
- 操作數(shù)據(jù)活躍于內(nèi)存:通過內(nèi)存進行數(shù)據(jù)操作速度遠快于硬盤訪問速度。
- 單線程:通過單線程處理所有客戶端請求,避免線程上下文切換開銷,大大提高的redis的運行效率和響應(yīng)速度。
- IO多路復(fù)用:以Linux系統(tǒng)為例,redis通過epoll模型實現(xiàn)單線程處理大量客戶端并發(fā)請求,提升了redis的并發(fā)性能。
- 數(shù)據(jù)結(jié)構(gòu):redis提供了各種各樣的數(shù)據(jù)結(jié)構(gòu),并且針對這些數(shù)據(jù)類型都進行了各種極致的優(yōu)化,例如哈希對象,在數(shù)據(jù)大小較小的情況下使用壓縮列表,一旦數(shù)據(jù)大小達到閾值后就會轉(zhuǎn)為哈希集。
- 6.0引入多線程:在高并發(fā)場景下,性能的瓶頸往往處于網(wǎng)絡(luò)連接上,為了進一步提升IO性能,redis通過多線程來充分利用CPU核心處理盡可能多個客戶端連接。
9. Redis 支持哪幾種數(shù)據(jù)類型
比較常見的有:
- 字符串
- 列表
- 集合
- 有序集合
- 字典
需要補充的是redis還有一些高級的數(shù)據(jù)結(jié)構(gòu)例如:
- stream
- bitmap
- Geo
- HyperLogLog
10. Redis為什么要自己定義SDS
這里我們直接引用《redis設(shè)計與實現(xiàn)》一書中的說法:
- C語言的字符串用\0收尾,在redis的使用場景下,很可能因為這個結(jié)束符導(dǎo)致數(shù)據(jù)被截斷。
- C語言字符串獲取長度需要進行遍歷,即O(n)級別的時間復(fù)雜度。
- C語言進行字符串拼接總是需要預(yù)先做好分配,否則很容易出現(xiàn)緩沖區(qū)溢出的問題。對于不斷擴大的字符串還需要反復(fù)創(chuàng)建新的字符數(shù)組解決問題。
11. Redis中的Zset是怎么實現(xiàn)的
這個問題我們可以針對不同的版本進行回答:
- 在5.0之前:有序集合在數(shù)據(jù)體積不是很大的情況下,通過ziplist或dict+skiplist的方式實現(xiàn)有序集合。
- 7.0 之后:完全取消了壓縮列表,改為dict+listpack/skiplist。
以我們最常用的版本,即5.0左右的版本,本質(zhì)上redis的有序集合是通過dict保證O(1)級別的直接映射定位,通過跳表實現(xiàn)O(logN)級別的范圍有序查詢。
12. 什么是GEO,有什么用
用于表示地理坐標信息,從而實現(xiàn)經(jīng)緯度數(shù)據(jù)檢索,它主要支持的命令有: Redis 的 GEO 模塊提供了一系列用于處理地理位置數(shù)據(jù)的命令。以下是一些常見的 GEO 命令:
- GEOADD:將一個或多個地理空間元素(經(jīng)度、緯度和成員)添加到指定的鍵中。
GEOADD key longitude latitude member [longitude latitude member ...]
- GEODIST:返回兩個給定成員之間的距離。如果其中任何一個成員不存在,則返回空值。
GEODIST key member1 member2 [unit]
- GEOHASH:返回一個或多個成員的 Geohash 表示形式。
GEOHASH key member [member ...]
- GEOPOS:返回一個或多個成員的位置(經(jīng)度和緯度)。如果成員不存在,則返回空值。
GEOPOS key member [member ...]
- GEORADIUS:以給定的經(jīng)緯度為中心,返回指定半徑范圍內(nèi)的所有成員。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
- GEORADIUSBYMEMBER:以給定成員的位置為中心,返回指定半徑范圍內(nèi)的所有成員。
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
這些命令可以幫助您在 Redis 中高效地存儲和查詢地理位置信息。
13. 為什么Redis 6.0引入了多線程
redis處理能力即qps大約在8w-10w之間,對于某些高并發(fā)存在大量客戶端連接的請求,本質(zhì)上可以通過增加實例解決,但是這種做法在資源消耗和成本無疑是非常大的:
經(jīng)過分析,這些場景大概率性能瓶頸在連接處理上,雖說redis采用epoll等多路復(fù)用技術(shù),但epoll本質(zhì)還是一個同步阻塞IO模型,所以redis增加多個線程,充分利用CPU核心,從而減少網(wǎng)絡(luò)等待的影響,提升程序執(zhí)行性能。
14. 為什么Lua腳本可以保證原子性
redis針對lua腳本的處理上,會一次性將lua腳本封裝成一個單獨的事務(wù),從而保證操作的指令執(zhí)行的原子性但不保證發(fā)生錯誤后的回滾兜底。
15. Redis中的setnx命令為什么是原子性的
以下兩個原因保證了setnx的原子性:
- 該指令只有在key不存在時才會插入/
- 單機情況下redis是單線程執(zhí)行,所以保證執(zhí)行執(zhí)行的有序性,間接保證臨界資源操作的線程安全。
16. Redis 5.0中的 Stream是什么
5.0版本新增的數(shù)據(jù)結(jié)構(gòu),主要用于處理有序且可追朔的消息流,每個消息都有唯一的id,按照添加順序進行排序,并且開發(fā)人員可以從中添加、讀取和刪除消息,同時它還是支持讓多個消費者并發(fā)的處理消息流。 在5.0之前redis通過使用發(fā)布訂閱模型實現(xiàn)消息隊列,但缺點是不支持持久化,如果出現(xiàn)網(wǎng)絡(luò)斷開、redis宕機等情況,就會造成消息丟失。 而stream提供了消息持久化和主從復(fù)制功能保證消息不丟失,保證客戶端可以訪問任何時刻的數(shù)據(jù),并且還能記住訪問位置。
總的來說,stream有幾個幾個優(yōu)點:
- 有序性
- 多消費者支持
- 持久化
- 支持消息分組
17. Redis的虛擬內(nèi)存機制是什么
2.4 之前的版本,redis提供了一種虛擬內(nèi)存的機制,當內(nèi)存空間不足時,將部分數(shù)據(jù)持久化到磁盤上,避免redis進程占用過多的內(nèi)存。
18. Redis的持久化機制是怎樣的
- rdb:按照協(xié)議規(guī)范定期生成持久化二進制數(shù)據(jù),文件小,恢復(fù)速度快,適合做備份和災(zāi)難恢復(fù),當然缺點也很明顯,定期更新可能導(dǎo)致丟失某一部分的數(shù)據(jù)。
- aof:實時完成指令持久化,有著更高的數(shù)據(jù)可靠性和更細粒度的數(shù)據(jù)恢復(fù),缺點是文件可能占用空間更多,每次寫操作都需要寫磁盤導(dǎo)致負載過高。
19. Redis 的事務(wù)機制是怎樣的
《掌握 Redis 事務(wù),提升數(shù)據(jù)處理效率的必備秘籍》
20. Redis的定期內(nèi)存淘汰策略是怎么樣的
redis通過定期刪除和惰性刪除處理過期key:
- 定期刪除:redis的serverCron函數(shù)會每個100ms隨機抽檢一些key查看是否過期,如果過期則將這些key刪除,通過隨機抽檢保證單線程執(zhí)行不會阻塞。
- 惰性刪除:當用戶查詢某個key的時候,redis函數(shù)會檢查該key是否會過期,如果過期則將其刪除并返回nil。
Redis 的內(nèi)存淘汰策略用于在內(nèi)存不足時決定如何移除數(shù)據(jù),以確保 Redis 可以繼續(xù)正常運行。以下是 Redis 支持的主要內(nèi)存淘汰策略:
- noeviction:默認策略,當達到最大內(nèi)存限制時,任何寫入操作都會返回錯誤(讀取操作仍然可以進行)。
- allkeys-lru:從所有鍵中使用最近最少使用的算法來驅(qū)逐鍵。
- volatile-lru:僅從設(shè)置了過期時間的鍵中使用最近最少使用的算法來驅(qū)逐鍵。
- allkeys-random:從所有鍵中隨機選擇鍵來驅(qū)逐。
- volatile-random:僅從設(shè)置了過期時間的鍵中隨機選擇鍵來驅(qū)逐。
- volatile-ttl:優(yōu)先根據(jù)剩余生存時間(TTL)來驅(qū)逐鍵,即 TTL 較短的鍵會被優(yōu)先驅(qū)逐。
這些策略可以在 Redis 配置文件 redis.conf 中通過 maxmemory-policy 參數(shù)設(shè)置。選擇合適的淘汰策略取決于具體的應(yīng)用場景和需求。例如,如果希望盡可能保留熱點數(shù)據(jù),可以選擇 allkeys-lru 或 volatile-lru;如果希望更公平地處理所有數(shù)據(jù),則可以選擇 allkeys-random 或 volatile-random。
21. Redis如何實現(xiàn)發(fā)布/訂閱
redis發(fā)布訂閱是通過pub和sub指令實現(xiàn)的,如果客戶端對某個事件感興趣可以通過sub訂閱,這些客戶端就會存儲到主題的channel中的鏈表,一旦有發(fā)送者用pub消息,channel就會遍歷訂閱者通知消息。
當然隨著stream的出現(xiàn),可能更多的企業(yè)會考慮使用更可靠的stream實現(xiàn)發(fā)布訂閱。
22. 除了做緩存,Redis還能用來干什么
- 消息隊列
- 延遲隊列
- 排行版
- 分布式id
- 分布式鎖
- 地理位置運用
- 分布式限流
- 分布式session
- 布隆過濾器
- 狀態(tài)統(tǒng)計
- 共同關(guān)注
- 推薦關(guān)注
- 數(shù)據(jù)庫
23. 為什么ZSet 既能支持高效的范圍查詢,還能以 O(1) 復(fù)雜度獲取元素權(quán)重值?
底層數(shù)據(jù)結(jié)構(gòu)由字典和調(diào)表構(gòu)成,兩者共同維護持有元素指針,當進行鍵定位時通過字典的哈希算法完成O(1)級別的定位,當需要有序的范圍查詢時,又可以通過跳表完成O(logN)級別的范圍檢索定位。
24. 什么是Redis的漸進式rehash
redis底層字典本質(zhì)上是通過數(shù)組+哈希算法和拉鏈法解決沖突,隨著時間推移可能會重現(xiàn)大量的鏈表導(dǎo)致查詢性能下降,又因為redis是單線程,為避免哈希表擴容耗時長導(dǎo)致性能下降,redis采用漸進式哈希逐步遷移數(shù)據(jù)到新表。
對于源碼感興趣的讀者可以參考這篇文章:《聊聊redis中的字典設(shè)計與實現(xiàn)》
25. Redis中key過期了一定會立即刪除嗎
不一定,serverCron的定時函數(shù)會批量抽取一批key進行檢查然后刪除。
26. Redis中有一批key瞬間過期,為什么其它key的讀寫效率會降低
出現(xiàn)讀寫效率低,大體是因為主動過期即用戶手動提交一批刪除過期key的任務(wù),因為redis的單線程的原因,對于瞬時的過期key操作勢必出現(xiàn)大量指令需要處理,這時候就會對其他客戶端的讀寫請求造成一定的阻塞,對此我們的解決策略大體有:
- 設(shè)置時間為隨機過期
- 采用被動過期設(shè)置key,即通過redis ex指令完成
27. 什么是Redis的Pipeline,和事務(wù)有什么區(qū)別
redis的pipeline主要為了解決網(wǎng)絡(luò)延遲的技術(shù),客戶端可以一次性批量提交請求,且無需等待每個命令的響應(yīng),redis收到這些請求后會依次執(zhí)行并返回,需要注意的是該操作與事務(wù)不同的是它不保證操作處理的原子性,唯一與事務(wù)的相同點都是一條指令失敗后,后續(xù)的指令都還會執(zhí)行且不會回滾操作。
28. Redis的事務(wù)和Lua之間有哪些區(qū)別
事務(wù)和lua之間的相同點是兩者都可以保證操作的原子性,不同點是前者一條指令失敗不影響后續(xù)指令的執(zhí)行,而后者反之。
29. 為什么Redis不支持回滾
本質(zhì)來說redis支持組隊時事務(wù)異常回滾,但是不支持執(zhí)行時異常回滾,設(shè)計者針對這種情況也給出相應(yīng)的原因:
- redis的設(shè)計初衷就是為了簡單、高效,過于復(fù)雜的事務(wù)實現(xiàn)會讓系統(tǒng)復(fù)雜并影響性能
- 從使用場景來說,redis本質(zhì)上就是一個緩存工具,不需要復(fù)雜的事務(wù)支持
- redis中出錯的問題基本上都是指令不正確,這些問題一般都需要預(yù)先解決,而不是依靠事務(wù)
30. 關(guān)于redis中的布隆過濾器
布隆過濾器是一種概率性的數(shù)據(jù)結(jié)構(gòu),用戶快速判斷一個元素是否存在于某個集合中,它的特點是:
- 通過盡可能少的物理空間維護盡可能多的數(shù)據(jù)的存在情況
- 允許誤判(這一點后續(xù)會補充)
- 無法進行元素刪除
對于redis而言實現(xiàn)布隆過濾器的方式有兩種:
- 基于bitmap結(jié)合多個哈希函數(shù)模擬布隆過濾器
- 引入redis官方的redisBloom模塊,對應(yīng)的操作指令示例如下:
BF.ADD myfilter "user123" # 添加元素
BF.EXISTS myfilter "user123" # 檢查是否存在
我們來一個實際的場景,例如我們要統(tǒng)計系統(tǒng)中千萬用戶是否在線,我們就布隆過濾器進行記錄和維護,整體的流程比較簡單:
- 通過多次哈希運算定位當前用戶id對應(yīng)的布隆過濾器中的位置。
- 定位到bit array將索引i位置標記為1。
需要了解的是布隆過濾器在進行哈希的時候是可能存在碰撞的,例如id為1和id為13232的用戶可能因為哈希算法導(dǎo)致的bitmap索引位是一樣的,所以我們可以得出以下結(jié)論:
- 當布隆過濾器認為數(shù)據(jù)不存在的時候,它100%不存在。
- 當布隆過濾器認為數(shù)據(jù)存在的時候,它不一定存在。
二、詳解Redis持久化機制
1. Redis持久化方式有哪些?有什么區(qū)別?
持久化分為rdb和aof兩種。
RDB持久化是把當前進程數(shù)據(jù)生成快照保存到硬盤的過程,觸發(fā)RDB持久化過程分為手動觸發(fā)和自動觸發(fā)。分別使用命令save或者bgsave。 同時rdb是一個二進制的壓縮文件,
以下幾個場景會自動觸發(fā)rdb持久化:
- 使用save相關(guān)配置,如“save m n”。表示m秒內(nèi)數(shù)據(jù)集存在n次修改時,自動觸發(fā)bgsave。
- 如果從節(jié)點執(zhí)行全量復(fù)制操作,主節(jié)點自動執(zhí)行bgsave生成RDB文件并發(fā)送給從節(jié)點
- 執(zhí)行debug reload命令重新加載Redis時,也會自動觸發(fā)save操作
- 默認情況下執(zhí)行shutdown命令時,如果沒有開啟AOF持久化功能則自動執(zhí)行bgsave。
而AOF則是以獨立日志的方式記錄每次寫命令, 重啟時再重新執(zhí)行AOF文件中的命令達到恢復(fù)數(shù)據(jù)的目的,整體工作過程為:
- 所有的寫入命令會追加到aof_buf(緩沖區(qū))中。
- AOF緩沖區(qū)根據(jù)對應(yīng)的策略向硬盤做同步操作。
- 隨著AOF文件越來越大,需要定期對AOF文件進行重寫,達到壓縮 的目的。
- 當Redis服務(wù)器重啟時,可以加載AOF文件進行數(shù)據(jù)恢復(fù)。
2. rdb和aof各自有什么優(yōu)缺點?
rdb優(yōu)點:
- 只有一個緊湊的二進制文件 dump.rdb,非常適合備份、全量復(fù)制的場景。
- 容災(zāi)性好,可以把RDB文件拷貝道遠程機器或者文件系統(tǒng)張,用于容災(zāi)恢復(fù)。
- 恢復(fù)速度快,RDB恢復(fù)數(shù)據(jù)的速度遠遠快于AOF的方式
rdb的缺點:
- 實時性低,RDB 是間隔一段時間進行持久化,沒法做到實時持久化/秒級持久化。如果在這一間隔事件發(fā)生故障,數(shù)據(jù)會丟失。
- 存在兼容問題,Redis演進過程存在多個格式的RDB版本,存在老版本Redis無法兼容新版本RDB的問題。
aof優(yōu)點:
- 實時性好,aof 持久化可以配置 appendfsync 屬性,有 always,每進行一次命令操作就記錄到 aof 文件中一次。
- 通過 append 模式寫文件,即使中途服務(wù)器宕機,可以通過 redis-check-aof 工具解決數(shù)據(jù)一致性問題。
aof缺點:
- AOF文件比RDB 文件大,且 恢復(fù)速度慢。
- 數(shù)據(jù)集大的時候,比RDB 啟動效率低。
3. rdb和aof如何選擇
如果想達到足以媲美數(shù)據(jù)庫的 數(shù)據(jù)安全性,應(yīng)該 同時使用兩種持久化功能。在這種情況下,當 Redis 重啟的時候會優(yōu)先載入 AOF 文件來恢復(fù)原始的數(shù)據(jù),因為在通常情況下 AOF 文件保存的數(shù)據(jù)集要比 RDB 文件保存的數(shù)據(jù)集要完整。
如果 可以接受數(shù)分鐘以內(nèi)的數(shù)據(jù)丟失,那么可以 只使用 RDB 持久化。
有很多用戶都只使用 AOF 持久化,但并不推薦這種方式,因為定時生成 RDB 快照(snapshot)非常便于進行數(shù)據(jù)備份, 并且 RDB 恢復(fù)數(shù)據(jù)集的速度也要比 AOF 恢復(fù)的速度要快。
如果只需要數(shù)據(jù)在服務(wù)器運行的時候存在,也可以不使用任何持久化方式。
當然如果既要保證同步和故障恢復(fù)效率,又要盡可能減少數(shù)據(jù)丟失的概率,也可以考慮混合持久化機制。
4. Redis的數(shù)據(jù)恢復(fù)如何做到的?
- AOF持久化開啟且存在AOF文件時,優(yōu)先加載AOF文件。
- AOF關(guān)閉或者AOF文件不存在時,加載RDB文件。
- 加載AOF/RDB文件成功后,Redis啟動成功。
- AOF/RDB文件存在錯誤時,Redis啟動失敗并打印錯誤信息。
5. Redis4.0的混合持久化持久化
將 rdb 文件的內(nèi)容和增量的 AOF 日志文件存在一起。這里的 AOF 日志不再是全量的日志,而是 自持久化開始到持久化結(jié)束 的這段時間發(fā)生的增量 AOF 日志,通常這部分 AOF 日志很小。
三、Redis場景架構(gòu)設(shè)計
1. 緩存擊穿、緩存穿透、緩存雪崩問題以及應(yīng)對策略
緩存擊穿:要查詢的某一個緩存數(shù)據(jù)剛剛好過期,導(dǎo)致大量查詢的請求直接打到數(shù)據(jù)庫上,讓數(shù)據(jù)庫處于高負載狀態(tài)。
解決策略:
- 加個互斥鎖保證單位時間內(nèi)只有一個請求處理SQL查詢并緩存數(shù)據(jù)。
- 設(shè)置熱點數(shù)據(jù)永不過期。
緩存穿透:盡管我們將數(shù)據(jù)庫中某些數(shù)據(jù)換到到內(nèi)存中,但是若有些攻擊者使用一些數(shù)據(jù)庫中不存在的key進行惡意攻擊,這時候,所有的查詢請求就像穿透了緩存中間件一樣直接在數(shù)據(jù)庫中進行查詢操作,在高并發(fā)場景,這樣的攻擊就會使得數(shù)據(jù)壓力過大,從而導(dǎo)致數(shù)據(jù)庫被打死
針對緩存穿透問題,對此我們的應(yīng)對策略有:
- 使用過濾器,我們可以使用布隆過濾器來減少對數(shù)據(jù)庫的請求,布隆過濾器的原理是將數(shù)據(jù)庫的數(shù)據(jù)哈希到 bitmap 中(在initialBean階段將數(shù)據(jù)緩存到內(nèi)存中),每次查詢之前,借用布隆過濾器的特性(不能保證數(shù)據(jù)一定存在,但一定能保證數(shù)據(jù)不存在),過濾掉一定不存在的無效請求,從而避免了無效請求給數(shù)據(jù)庫帶來的查詢壓力。
- 緩存空結(jié)果,我們可以把每次從數(shù)據(jù)庫查詢的數(shù)據(jù)都保存到緩存中,為了提高前臺用戶的使用體驗 (解決長時間內(nèi)查詢不到任何信息的情況),我們可以將空結(jié)果的緩存時間設(shè)置得短一些,例如 3~5 分鐘,但是有可能導(dǎo)致數(shù)據(jù)一致性問題,所以我們建議查詢或者更新的時候要對這個類型的緩存上個鎖進行進一步的操作。
- 緩存雪崩:大量定時緩存失效或緩存服務(wù)器宕機,導(dǎo)致數(shù)據(jù)庫服務(wù)器被打死。
解決策略:
- 加鎖排隊,示例代碼如下所示,如果數(shù)據(jù)庫中沒有值的話直接上鎖到數(shù)據(jù)庫查在放到緩存中,有點類似于單例模式的雙重鎖校驗,但是并發(fā)場景性能表現(xiàn)會差一些:
// 緩存 key
String cacheKey = "userlist";
// 查詢緩存
String data = jedis.get(cacheKey);
if (StringUtils.isNotBlank(data)) {
// 查詢到數(shù)據(jù),直接返回結(jié)果
return data;
} else {
// 先排隊查詢數(shù)據(jù)庫,再放入緩存
synchronized (cacheKey) {
data = jedis.get(cacheKey);
if (!StringUtils.isNotBlank(data)) { // 雙重判斷
// 查詢數(shù)據(jù)庫
data = findUserInfo();
// 放入緩存
jedis.set(cacheKey, data);
}
return data;
}
}
- 設(shè)計緩存時,對緩存設(shè)置隨機時間:
// 緩存原本的失效時間
int exTime = 10 * 60;
// 隨機數(shù)生成類
Random random = new Random();
// 緩存設(shè)置
jedis.setex(cacheKey, exTime + random.nextInt(1000) , value);
2. 緩存污染(緩存空間全滿)
某些數(shù)據(jù)查詢一次就被緩存在數(shù)據(jù)庫中,隨著時間推移,緩存空間已經(jīng)滿了,這時候redis就要根據(jù)緩存策略進行緩存置換。這就造成沒意義的數(shù)據(jù)需要通過緩存置換策略來淘汰數(shù)據(jù),而且還可能出現(xiàn)淘汰熱點數(shù)據(jù)的情況。
解決方案:選定合適的緩存置換策略,而redis緩存策略主要分三類。
- noeviction (v4.0后默認的):不會淘汰任何過期鍵,滿了就報錯,對設(shè)置了過期時間的數(shù)據(jù)中進行淘汰
- volatile-random:隨機刪除過期key
- volatile-ttl:根據(jù)過期時間進行排序,越早過期的數(shù)據(jù)就優(yōu)先被淘汰。
- volatile-lru:即最近最少使用算法(推薦),redis的lru緩存置換算法相比傳統(tǒng)的算法做了一定優(yōu)化,根據(jù) maxmemory-samples從緩存中隨機取出幾個key值,然后進行比較在進行淘汰,這樣就避免了緩存置換時需要操作一個大鏈表進行key值淘汰了。
- volatile-lfu:lru只知曉用戶最近使用次數(shù),而不知道該數(shù)據(jù)使用頻率,所以lfu就是基于lru進一步的優(yōu)化,進行淘汰時隨機取出訪問次數(shù)最少的數(shù)據(jù),如果最少的數(shù)據(jù)有多個,按按照lru算法進行淘汰。但是redis只用8bit記錄訪問次數(shù),超過255就無法進行自增了,所以我們可以使用lfu-log-factor 和lfu-decay-time來用戶訪問次數(shù)增加的頻率。
- lfu-decay-time:控制訪問次數(shù)衰減。LFU 策略會計算當前時間和數(shù)據(jù)最近一次訪問時間的差值,并把這個差值換算成以分鐘為單位。然后,LFU 策略再把這個差值除以 lfu_decay_time 值,所得的結(jié)果就是數(shù)據(jù) counter 要衰減的值。若設(shè)置為0,則意味著每次掃描訪問次數(shù)都會扣減。
- lfu-log-factor:用計數(shù)器當前的值乘以配置項 lfu_log_factor 再加 1,再取其倒數(shù),得到一個 p 值;然后,把這個 p 值和一個取值范圍在(0,1)間的隨機數(shù) r 值比大小,只有 p 值大于 r 值時,計數(shù)器才加 1。 全部數(shù)據(jù)進行淘汰
- allkeys-random:從所有鍵值對中使用lru淘汰
- allkeys-lru:從所有鍵值對中隨機刪除
- allkeys-lfu:從所有鍵值對中使用lfu隨機淘汰
3. 基于Redis定位億級數(shù)據(jù)
假如Redis里面有1億個key,其中有10w個key是以某個固定的已知的前綴開頭的,如何將它們?nèi)空页鰜恚?/p>
我們可以使用 keys 指令可以掃出指定模式的 key 列表。但是要注意 keys 指令會導(dǎo)致線程阻塞一段時間,線上服務(wù)會停頓,直到指令執(zhí)行完畢,服務(wù)才能恢復(fù)。這個時候可以使用 scan 指令,scan 指令可以無阻塞的提取出指定模式的 key 列表,但是會有一定的重復(fù)概率,在客戶端做一次去重就可以了,但是整體所花費的時間會比直接用 keys 指令長。
4. 什么情況下會出現(xiàn)數(shù)據(jù)庫和緩存不一致的問題?
大體有以下兩種情況: 我們先來說說更新數(shù)據(jù)庫,然后更新緩存的情況,如下圖所示,線程1和線程2都是先更新數(shù)據(jù)再更新緩存,由于線程1因為網(wǎng)絡(luò)波動或者線程調(diào)度順序原因?qū)е潞蟾戮彺妫罱K導(dǎo)致數(shù)據(jù)庫和緩存不一致,而先更新緩存再更新數(shù)據(jù)庫同理這里就不多贅述:
還有一種情況是針對讀場景的,如下所示:
- 線程2查詢緩存發(fā)現(xiàn)沒有數(shù)據(jù),到數(shù)據(jù)庫讀取到值10。
- 此時,線程1更新緩存值為20,準備寫數(shù)據(jù)庫。
- 線程2將數(shù)據(jù)庫讀取到的10寫入緩存。
- 線程1將數(shù)據(jù)庫更新為20。
自此,緩存不一致問題又出現(xiàn):
5. 如何解決Redis和數(shù)據(jù)庫的一致性問題?
- 延時雙刪
- 先更新數(shù)據(jù)庫再刪除緩存
- 更新數(shù)據(jù)庫,并基于用binlog監(jiān)聽數(shù)據(jù)庫變化進行緩存刪除。
6. 為什么需要延遲雙刪,兩次刪除的原因是什么?
第一次刪除避免讀請求讀到臟數(shù)據(jù) 第二次刪除避免讀請求將臟數(shù)據(jù)寫入緩存.
7. Redis如何實現(xiàn)延遲消息
通過配置notify-keyspace-events Ex開啟過期key事件,再通過程序繼承KeyExpirationEventMessageListener監(jiān)聽過期的事件,這種做法的缺點也很明顯,即過期的key不一定會立即刪除,且該消息沒有持久化可能出現(xiàn)丟失。 關(guān)于過期key不一定會立即刪除的這一點。
通過zset將過期時間作為score,然后key作為member,程序通過計算過期時間差值進行休眠,到期后刪除這個key,當然我們需要保證的就是如果有時效更短的key進來注意更新時間。
通過redission內(nèi)存輪子提交一個任務(wù),原理和方法2差不多,只不過對于并發(fā)消費等問題有了較好的優(yōu)化,且使用更加簡單。
8. 如何基于Redis實現(xiàn)滑動窗口限流?
滑動窗口本質(zhì)上就是通過有序集合的方式保證單位時間內(nèi)保持一定流量數(shù)據(jù),避免突然流量突刺的問題,假設(shè)我們現(xiàn)在有個接口,希望每秒對應(yīng)請求控制在2000,對應(yīng)的落地方案為:
- 將請求接口作為key。
- 當某個請求到來時,生成唯一id作為member,時間戳作為value。
- 基于當前時間戳減去60s看看60s以內(nèi)的請求數(shù)。
- 查看當前有序集合中元素是否小于2000,如果是則允許新的請求到來。反之不允許。
當然我們也可以直接用redisson中的RRateLimiter,它底層本質(zhì)就是用一個令牌桶算法。
9. 怎么處理熱key
什么是熱Key? 所謂的熱key,就是訪問頻率比較的key。 比如,熱門新聞事件或商品,這類key通常有大流量的訪問,對存儲這類信息的 Redis來說,是不小的壓力。 假如Redis集群部署,熱key可能會造成整體流量的不均衡,個別節(jié)點出現(xiàn)OPS過大的情況,極端情況下熱點key甚至?xí)^ Redis本身能夠承受的OPS。
怎么處理熱key?
熱key處理 對熱key的處理,最關(guān)鍵的是對熱點key的監(jiān)控,可以從這些端來監(jiān)控?zé)狳ckey: 客戶端 客戶端其實是距離key“最近”的地方,因為Redis命令就是從客戶端發(fā)出的,例如在客戶端設(shè)置全局字典(key和調(diào)用次數(shù)),每次調(diào)用Redis命令時,使用這個字典進行記錄。 代理端 像Twemproxy、Codis這些基于代理的Redis分布式架構(gòu),所有客戶端的請求都是通過代理端完成的,可以在代理端進行收集統(tǒng)計。
Redis服務(wù)端 使用monitor命令統(tǒng)計熱點key是很多開發(fā)和運維人員首先想到,monitor命令可以監(jiān)控到Redis執(zhí)行的所有命令。
只要監(jiān)控到了熱key,對熱key的處理就簡單了: 把熱key打散到不同的服務(wù)器,降低壓? 加??級緩存,提前加載熱key數(shù)據(jù)到內(nèi)存中,如果redis宕機,?內(nèi)存查詢
10. 緩存預(yù)熱怎么做?
所謂緩存預(yù)熱,就是提前把數(shù)據(jù)庫里的數(shù)據(jù)刷到緩存里,通常有這些方法:
- 直接寫個緩存刷新頁面或者接口,上線時手動操作
- 數(shù)據(jù)量不大,可以在項目啟動的時候自動進行加載(我們目前就是執(zhí)行這種操作,通過繼承InitializingBean實現(xiàn))
- 定時任務(wù)刷新緩存.
11. 熱點key重建問題了解過?你是如何解決的呢?
開發(fā)的時候一般使用“緩存+過期時間”的策略,既可以加速數(shù)據(jù)讀寫,又保證數(shù)據(jù)的定期更新,這種模式基本能夠滿足絕大部分需求。
但是有兩個問題如果同時出現(xiàn),可能就會出現(xiàn)比較大的問題:
- 當前key是一個熱點key(例如一個熱門的娛樂新聞),并發(fā)量非常大。
- 重建緩存不能在短時間完成,可能是一個復(fù)雜計算,例如復(fù)雜的 SQL、多次IO、多個依賴等。 在緩存失效的瞬間,有大量線程來重建緩存,造成后端負載加大,甚至可能會讓應(yīng)用崩潰。
要解決這個問題也不是很復(fù)雜,解決問題的要點在于:
- 減少重建緩存的次數(shù)。
- 數(shù)據(jù)盡可能一致。
- 較少的潛在危險。 所以一般采用如下方式:
- 互斥鎖(mutex key) 這種方法只允許一個線程重建緩存,其他線程等待重建緩存的線程執(zhí)行完,重新從緩存獲取數(shù)據(jù)即可。
- 永遠不過期 “永遠不過期”包含兩層意思:
- 從緩存層面來看,確實沒有設(shè)置過期時間,所以不會出現(xiàn)熱點key過期后產(chǎn)生的問題,也就是“物理”不過期,注意數(shù)據(jù)更新后要實時加鎖更新。
從功能層面來看,為每個value設(shè)置一個邏輯過期時間,當發(fā)現(xiàn)超過邏輯過期時間后,會使用單獨的線程去構(gòu)建緩存。
四、詳解Redis日常運維
1. Redis阻塞問題如何解決
(1) API或數(shù)據(jù)結(jié)構(gòu)使用不合理:通常Redis執(zhí)行命令速度非常快,但是不合理地使用命令,可能會導(dǎo)致執(zhí)行速度很慢,導(dǎo)致阻塞,對于高并發(fā)的場景,應(yīng)該盡量避免在大對象上執(zhí)行算法復(fù)雜 度超過O(n)的命令。
對慢查詢的處理分為兩步:
- 發(fā)現(xiàn)慢查詢: slowlog get{n}命令可以獲取最近 的n條慢查詢命令;
- 發(fā)現(xiàn)慢查詢后,可以從兩個方向去優(yōu)化慢查詢: 1)修改為低算法復(fù)雜度的命令,如hgetall改為hmget等,禁用keys、sort等命 令 2)調(diào)整大對象:縮減大對象數(shù)據(jù)或把大對象拆分為多個小對象,防止一次命令操作過多的數(shù)據(jù)。
(2) CPU飽和的問題:單線程的Redis處理命令時只能使用一個CPU,而CPU飽和是指Redis單核CPU使用率跑到接近100%。
針對這種情況,處理步驟一般如下:
- 判斷當前Redis并發(fā)量是否已經(jīng)達到極限,可以使用統(tǒng)計命令`redis-cli-h{ip}-p{port}--stat`獲取當前 Redis使用情況
- 如果Redis的請求幾萬+,那么大概就是Redis的OPS已經(jīng)到了極限,應(yīng)該做集群化水品擴展來分攤OPS壓力
- 如果只有幾百幾千,那么就得排查命令和內(nèi)存的使用
(3) 持久化相關(guān)的阻塞:對于開啟了持久化功能的Redis節(jié)點,需要排查是否是持久化導(dǎo)致的阻塞。
fork阻塞 fork操作發(fā)生在RDB和AOF重寫時,Redis主線程調(diào)用fork操作產(chǎn)生共享 內(nèi)存的子進程,由子進程完成持久化文件重寫工作。如果fork操作本身耗時過長,必然會導(dǎo)致主線程的阻塞。
AOF刷盤阻塞 當我們開啟AOF持久化功能時,文件刷盤的方式一般采用每秒一次,后臺線程每秒對AOF文件做fsync操作。當硬盤壓力過大時,fsync操作需要等待,直到寫入完成。如果主線程發(fā)現(xiàn)距離上一次的fsync成功超過2秒,為了 數(shù)據(jù)安全性它會阻塞直到后臺線程執(zhí)行fsync操作完成。
HugePage寫操作阻塞 對于開啟Transparent HugePages的 操作系統(tǒng),每次寫命令引起的復(fù)制內(nèi)存頁單位由4K變?yōu)?MB,放大了512 倍,會拖慢寫操作的執(zhí)行時間,導(dǎo)致大量寫操作慢查詢。
2. Redis大key問題
Redis使用過程中,有時候會出現(xiàn)大key的情況, 比如:
(1) 單個簡單的key存儲的value很大,size超過10KBhash, set,zset,list 中存儲過多的元素(以萬為單位) 大key會造成什么問題呢?
- 客戶端耗時增加,甚至超時
- 對大key進行IO操作時,會嚴重占用帶寬和CPU
- 造成Redis集群中數(shù)據(jù)傾斜
- 主動刪除、被動刪等,可能會導(dǎo)致阻塞
(2) 如何找到大key?
- bigkeys命令:使用bigkeys命令以遍歷的方式分析Redis實例中的所有Key,并返回整體統(tǒng)計信息與每個數(shù)據(jù)類型中Top1的大Key
- redis-rdb-tools:redis-rdb-tools是由Python寫的用來分析Redis的rdb快照文件用的工具,它可以把rdb快照文件生成json文件或者生成報表用來分析Redis的使用詳情。
(3) 如何處理大key?
- 刪除大key:當Redis版本大于4.0時,可使用UNLINK命令安全地刪除大Key,該命令能夠以非阻塞的方式,逐步地清理傳入的Key。 當Redis版本小于4.0時,避免使用阻塞式命令KEYS,而是建議通過SCAN命令執(zhí)行增量迭代掃描key,然后判斷進行刪除。
- 壓縮和拆分key:當vaule是string時,比較難拆分,則使用序列化、壓縮算法將key的大小控制在合理范圍內(nèi),但是序列化和反序列化都會帶來更多時間上的消耗。 當value是string,壓縮之后仍然是大key,則需要進行拆分,一個大key分為不同的部分,記錄每個部分的key,使用multiget等操作實現(xiàn)事務(wù)讀取。 當value是list/set等集合類型時,根據(jù)預(yù)估的數(shù)據(jù)規(guī)模來進行分片,不同的元素計算后分到不同的片。
3. Redis常見的性能問題和解決方案了解嘛?
Master 最好不要做任何持久化工作,包括內(nèi)存快照和 AOF 日志文件,特別是不要啟用內(nèi)存快照做持久化。
如果數(shù)據(jù)比較關(guān)鍵,某個 Slave 開啟 AOF 備份數(shù)據(jù),策略為每秒同步一次。
為了主從復(fù)制的速度和連接的穩(wěn)定性,Slave 和 Master 最好在同一個局域網(wǎng)內(nèi)。 盡量避免在壓力較大的主庫上增加從庫。
Master 調(diào)用 BGREWRITEAOF 重寫 AOF 文件,AOF 在重寫的時候會占大量的 CPU 和內(nèi)存資源,導(dǎo)致服務(wù) load 過高,出現(xiàn)短暫服務(wù)暫停現(xiàn)象。
為了 Master 的穩(wěn)定性,主從復(fù)制不要用圖狀結(jié)構(gòu),用單向鏈表結(jié)構(gòu)更穩(wěn)定,即主從關(guān)為:Master<–Slave1<–Slave2<–Slave3…,這樣的結(jié)構(gòu)也方便解決單點故障問題,實現(xiàn) Slave 對 Master 的替換,也即,如果 Master 掛了,可以立馬啟用 Slave1 做 Master,其他不變。
4. 什么是熱Key問題,如何解決熱key問題
即同一個時間點上,redis中同一個key被大量訪問,導(dǎo)致流量過于集中,進而導(dǎo)致服務(wù)器資源無法支撐嚴重情況下可能導(dǎo)致服務(wù)癱瘓。 所以,對應(yīng)的處理策略要針對不同的情況,如果是事前,我們可以根據(jù)往年經(jīng)驗識別出對應(yīng)的熱點key,提前擴充實例,做預(yù)熱緩存。 如果是事中,需要考慮線上進行熱點key拆分,多級緩存、增加實例甚至通過限流等策略來解決問題。
5. 如何用Redis實現(xiàn)樂觀鎖、可重入鎖
大體可以通過以下幾個指令:
- watch監(jiān)視一個或者多個鍵
- get查詢數(shù)據(jù)
- multi開始事務(wù)
- set執(zhí)行命令
- exec提交事務(wù)
而可重入鎖通過setnx+incr和decr指令完成上鎖和解鎖邏輯。
6. Redis實現(xiàn)分布鎖的時候,哪些問題需要考慮
- 互斥
- 性能
- 誤解鎖
- 鎖超時
- 鎖續(xù)命
- 單點故障
- 鎖重入
- 網(wǎng)絡(luò)分區(qū)
- 時間漂移
7. Redis如何高效安全的遍歷所有key
加入redis中存在大量的key,使用常規(guī)的keys * 請求會導(dǎo)致其他的客戶端請求阻塞,所以針對遍歷key的需求,我們更建議使用scan,它會以游標的方式分批次迭代鍵集合,這個概念和游標查詢是優(yōu)點類似的:
這里筆者簡單制造了百萬級別的熱key進行演示:
StringRedisTemplate redisTemplate = SpringUtil.getBean(StringRedisTemplate.class);
IntStream.rangeClosed(0, 100_0000).parallel()
.forEach(n -> {
redisTemplate.opsForValue().set("key-" + n, "value-" + n);
});
這里筆者用了keys * 嘗試了一下遍歷,可以看到耗時約70s,這也就意味則在這70s之間其他的請求是阻塞的:
而使用scan就可以很好的解決問題,通過scan指令從0開始,每次基于上一次的游標進行數(shù)據(jù)檢索獲取,通過逐批次的檢索和遍歷很好的解決keys *的阻塞問題:
雖說scan很好的解決的遍歷阻塞問題,但它對于數(shù)據(jù)實時性的把控不是很好,從上面我可以知道scan指令本質(zhì)上就是漸進式的遍歷,這意味著在掃描過的區(qū)間上進行的任何修改操作我們都是無法感知的。