Redis 缓存穿透、雪崩、缓存数据库不一致、持久化方式、分布式锁、过期策略
1. Redis 緩存穿透
1.1 Redis 緩存穿透概念
訪問了不存在的 key,緩存未命中,請求會穿透到 DB,量大時可能會對 DB 造成壓力導(dǎo)致服務(wù)異常。
由于不恰當(dāng)?shù)臉I(yè)務(wù)功能實現(xiàn),或者外部惡意攻擊不斷地請求某些不存在的數(shù)據(jù)內(nèi)存,由于緩存中沒有保存該數(shù)據(jù),導(dǎo)致所有的請求都會落到數(shù)據(jù)庫上,對數(shù)據(jù)庫可能帶來一定的壓力,甚至崩潰。
1.2 Redis 緩存穿透解決方案
- 針對這些不存在的 key,可以在 Redis 中保存對應(yīng)的 key 和空數(shù)據(jù),并設(shè)置較短的過期時間;
- 或者使用 Redis Bitmap 來判斷數(shù)據(jù)是否存在以過濾無效請求;
2. Redis 緩存雪崩
2.1 Redis 緩存雪崩概念
緩存同時失效或緩存服務(wù)異常,瞬時大量請求直接到達 DB,對 DB 造成壓力導(dǎo)致服務(wù)異常,
2.2 Redis 緩存雪崩解決方案:
- 保證緩存服務(wù)的高可用,采用集群,多主多從模式部署,并開啟 RDB 和 AOF 備份,在 Redis 服務(wù)出問題時能快速根據(jù)備份文件恢復(fù)緩存數(shù)據(jù);
- 緩存過期時間的設(shè)置隨機化;
- 調(diào)用緩存服務(wù)時增加熔斷模塊,類似 Hystrix;
總的來說,緩存穿透和雪崩帶來的影響都是緩存失效或未命中導(dǎo)致 DB 壓力大,從而可能拖垮服務(wù)。這些情況都是可能導(dǎo)致 DB 異常從而影響服務(wù)的可用性。
其實關(guān)鍵就是過濾無效的數(shù)據(jù),增加緩存命中率,并添加對應(yīng)的隔離措施以增加整個服務(wù)的可用性。
3. Redis 緩存和數(shù)據(jù)庫不一致問題
使用了緩存的話,就有雙寫問題。通常,涉及到雙寫問題,就會有數(shù)據(jù)不一致的情況。需要保存數(shù)據(jù)一致需要犧牲性能,不過實際場景中一般也不要求這么高的一致性。要求嚴(yán)格一致的話,可以將讀請求和寫請求串行化,串到一個內(nèi)存隊列里去,這樣就可以保證一定不會出現(xiàn)不一致的情況。
緩存模式:Cache Aside Pattern(先淘汰緩存,再寫數(shù)據(jù)庫):
- 讀的時候,先讀緩存,緩存沒有的話,那么就讀數(shù)據(jù)庫,然后取出數(shù)據(jù)后放入緩存,同時返回響應(yīng)讀的時候;
- 寫的時候,先更新數(shù)據(jù)庫,然后再刪除緩存。
先看一下數(shù)據(jù)的讀取過程,規(guī)則是“先讀 cache,再讀 db”,詳細(xì)步驟如下:
-
每次讀取數(shù)據(jù),都從 cache 里讀;
-
如果讀到了,則直接返回,稱作 cache hit;
-
如果讀不到 cache 的數(shù)據(jù),則從 db 里面撈一份,稱作 cache miss;
-
將讀取到的數(shù)據(jù)塞入到緩存中,下次讀取時,就可以直接命中。
再來看一下寫請求,規(guī)則是“先更新 db,再刪除緩存”,詳細(xì)步驟如下:
-
將變更寫入到數(shù)據(jù)庫中;
-
刪除緩存里對應(yīng)的數(shù)據(jù)。
3.1 并發(fā)情況下出現(xiàn)了數(shù)據(jù)不一致問題怎么辦?
eg. 有個寫請求,此時刪除了緩存中的數(shù)據(jù),但是還沒來得及寫數(shù)據(jù)庫;這個時候來了一個讀請求,又把數(shù)據(jù)庫中的舊數(shù)據(jù) load 到緩存中去了,然后上一個請求的寫數(shù)據(jù)庫操作完成了。這個時候,數(shù)據(jù)庫中的是最新的數(shù)據(jù),緩存中的卻是舊數(shù)據(jù)了.
解決思路:部分請求串行化或采取補償操作。
- 串行化:將數(shù)據(jù)庫與緩存更新與讀取操作進行異步串行化;將相同 id 的讀寫請求 hash 到同一臺服務(wù)處理,服務(wù)中使用隊列一個一個地執(zhí)行。如果發(fā)現(xiàn)隊列中有對應(yīng)資源的寫請求,那么就等待其執(zhí)行結(jié)束后再去取值返回(這里需要考慮等待時間過長的問題,還有該方案影響較大,較復(fù)雜)。
- 補償操作:既然是延遲導(dǎo)致的數(shù)據(jù)不一致,那么根據(jù)數(shù)據(jù)庫實際的延遲時間,我們使用定時任務(wù)或者消息觸發(fā),如果有寫請求結(jié)束后,我們在指定的時間之后再刪除一次該緩存值,這樣即使有不一致的臟數(shù)據(jù),那也只會出現(xiàn)在延遲的這一段時間中。
4. 基于 Redis 的分布式鎖是怎么實現(xiàn)的?
分布式鎖實現(xiàn)要保證幾個基本點。
互斥性:任意時刻,只有一個資源能夠獲取到鎖。
容災(zāi)性:能夠在未成功釋放鎖的的情況下,一定時限內(nèi)能夠恢復(fù)鎖的正常功能。
統(tǒng)一性:加鎖和解鎖保證同一資源來進行操作。
示例:我們通過 SETNX 命令來實現(xiàn)的分布式鎖,設(shè)置了過期時間,不至于會出現(xiàn)線程異常導(dǎo)致鎖無法釋放的情況。
5. 為啥 Redis 單線程模型也能效率這么高?
Redis 基于 Reactor 模式開發(fā)了網(wǎng)絡(luò)事件處理器,這個處理器叫做文件事件處理器(File Event Handler)。這個文件事件處理器是單線程的,Redis 才叫做單線程的模型,采用 IO 多路復(fù)用機制同時監(jiān)聽多個 Socket,根據(jù) Socket 上的事件來選擇對應(yīng)的事件處理器來處理這個事件,為啥快呢:
- 純內(nèi)存操作
- 核心是基于非阻塞的 IO 多路復(fù)用機制
- 單線程反而避免了多線程的頻繁上下文切換問題)
- 內(nèi)部數(shù)據(jù)結(jié)構(gòu)設(shè)計,整個的結(jié)構(gòu)都類似于一個 map,查找效率賊高
Redis是單線程處理網(wǎng)絡(luò)指令請求,所以不需要考慮并發(fā)安全問題。所有的網(wǎng)絡(luò)請求都是一個線程處理。但不代表所有模塊都是單線程。
6. Redis 持久化方式及其區(qū)別
Redis 有持久化機制的,它支持 AOF 和 RDB 兩種持久化方式。
RDB:通過 fork 一個子進程保存當(dāng)前內(nèi)存的一個快照實現(xiàn)備份. 適合大規(guī)模的數(shù)據(jù)恢復(fù),但是數(shù)據(jù)的完整性和一致性不高,因為 RDB 可能在最后一次備份時宕機了。另外備份時會占用內(nèi)存,因為 Redis 在備份時會獨立創(chuàng)建一個子進程,將數(shù)據(jù)寫入到一個臨時文件(此時內(nèi)存中的數(shù)據(jù)是原來的兩倍哦),最后再將臨時文件替換之前的備份文件。配置方法如下:
# save <指定時間間隔> <執(zhí)行指定次數(shù)更新操作>,滿足條件就將內(nèi)存中的數(shù)據(jù)同步到硬盤中save <seconds> <changes> # 指定本地數(shù)據(jù)庫文件名,一般采用默認(rèn)的 dump.rdbdbfilename dump.rdb # 默認(rèn)開啟數(shù)據(jù)壓縮rdbcompression yes
AOF: 通過對每條寫入命令以 append-only 的模式寫入一個日志文件中,在 Redis 重啟的時候,可以通過回放 AOF 日志中的寫入指令來重新構(gòu)建整個數(shù)據(jù)集。 AOF 對數(shù)據(jù)數(shù)據(jù)的完整性和一致性支持更好,但是其備份日志文件一般會比 RDB 方式的備份文件更大,恢復(fù)也更慢,同時由于 fsync 的頻率方式,會影響 Redis 的性能。
# 打開aofappendonly yes # 日志文件appendfilename "appendonly.aof" # 更新條件# appendfsync alwaysappendfsync everysec# appendfsync no# 觸發(fā)重寫的配置# 時間長了日志會特別大,此時需要觸發(fā)重寫# 重寫的原理:Redis 會fork出一條新進程,讀取內(nèi)存中的數(shù)據(jù),并重新寫到一個臨時文件中。# 最后替換舊的aof文件。auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb
后續(xù):Redis 4.0 之后,持久化增加了混合 RDB-AOF 持久化格式。具體可以參考
http://blog.huangz.me/diary/2016/redis-4-outline.html
7. Redis 如何實現(xiàn)分布式和高可用?
Redis 主從模式原理:
Redis 2.8 之前主從模式,主從之間的數(shù)據(jù)復(fù)制只有全復(fù)制機制,通過執(zhí)行命令 SLAVEOF ip port,使得其成為從服務(wù)器。全數(shù)據(jù)復(fù)制流程如下:
從服務(wù)器向主服務(wù)器發(fā)送 SYNC 命令。
主服務(wù)器收到 SYNC 請求后,執(zhí)行 BGSAVE 命令在后臺生成 RDB 文件,并使用一個緩沖區(qū)記錄從現(xiàn)在開始執(zhí)行的寫命令。
主服務(wù)器將生成的 RDB 文件發(fā)送給從服務(wù)器,從服務(wù)器根據(jù)RDB文件更新狀態(tài)。
主服務(wù)器將緩沖區(qū)中的寫命令發(fā)送給從服務(wù)器,從服務(wù)器執(zhí)行這些命令,最終和主服務(wù)器達到一致的狀態(tài)。
命令傳播
數(shù)據(jù)復(fù)制完成后,為了保持一致的狀態(tài),主服務(wù)的寫命令都需要傳播給其從服務(wù)器。
不足:上面的方式存在不足,即如果從服務(wù)器是和主服務(wù)器斷開后,又立馬重新連接上后。那么此時如果還執(zhí)行全同步的話,就十分浪費。因為全同步非常占用 CPU、IO 和帶寬等。
Redis 2.8 之后,支持了 PSYNC,即部分重同步機制。部分重同步機制主要依靠:
主從服務(wù)器的復(fù)制偏移量: 用于記錄主從服務(wù)器的狀態(tài)是否一致,以及數(shù)據(jù)復(fù)制時的偏移量
主服務(wù)器的復(fù)制積壓緩沖區(qū):固定大小的(默認(rèn) 1M)FIFO 隊列,用于記錄主服務(wù)器的寫命令
主服務(wù)器的 ID:用于判斷,從服務(wù)器斷開連接后重新連接到的主服務(wù)器還是不是原來那個
部分重同步流程:
主從服務(wù)器都有一個復(fù)制偏移量,當(dāng)執(zhí)行了寫命令時,復(fù)制偏移量對應(yīng)會增加。
從服務(wù)器請求同步,帶上當(dāng)前的 offset 和之前的主服務(wù)器 ID;
如果請求中的主服務(wù) ID 和當(dāng)前的主服務(wù)器ID不一致,或者其 offset 的值已經(jīng)不再復(fù)制積壓緩沖區(qū)內(nèi),那么需要執(zhí)行全同步,流程同上;
否則,執(zhí)行部分同步,主服務(wù)器將復(fù)制積壓緩沖區(qū)中 offset 之后的命令發(fā)送給從服務(wù)器;
從服務(wù)器執(zhí)行對應(yīng)寫命令,并修改 offset,達到和主服務(wù)器狀態(tài)一致。
8. Redis 的過期策略都有哪些?
8.1 定時刪除策略
在設(shè)置 key 的過期時間的同時,為該 key 創(chuàng)建一個定時器,讓定時器在 key 的過期時間來臨時,對 key 進行刪除。
- 優(yōu)點:保證內(nèi)存盡快釋放。
- 缺點:若 key 過多,刪除這些 key 會占用很多 CPU 時間, 而且每個 key 創(chuàng)建一個定時器,性能影響嚴(yán)重。
8.2 惰性刪除策略
key 過期的時候不刪除,每次從數(shù)據(jù)庫獲取 key 的時候去檢查是否過期,若過期,則刪除,返回 null。
- 優(yōu)點:CPU 時間占用比較少。
- 缺點:若 key 很長時間沒有被獲取, 將不會被刪除,可能造成內(nèi)存泄露
8.3 定期刪除策略
每隔一段時間執(zhí)行一次刪除(在 redis.conf 配置文件設(shè)置 hz,1s 刷新的頻率)過期 key 操作。
-
優(yōu)點:可以控制刪除操作的時長和頻率,來減少 CPU 時間占用,可以避免惰性刪除時候內(nèi)存泄漏的問題。
-
缺點:
對內(nèi)存友好方面,不如定時策略
對 CPU 友好方面,不如惰性策略
那如果執(zhí)行了上述的刪除操作后,Redis 的內(nèi)存空間還是不足怎么辦呢?
設(shè)置了過期時間的,到期后會被上述操作刪除掉;如果此時內(nèi)存還是不夠的話,Redis 會根據(jù)配置的策略來執(zhí)行對應(yīng)的操作,主要有 noeviction、allkeys-lru、allkeys-random、volatile-lru、volatile-random、volatile-ttl 這幾種,balabalabala…
- noeviction:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,新寫入操作會報錯,這個一般沒人用吧,實在是太惡心了。
- allkeys-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在鍵空間中,移除最近最少使用的 key(這個是最常用的)。
- allkeys-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在鍵空間中,隨機移除某個 key,這個一般沒人用吧,為啥要隨機,肯定是把最近最少使用的 key 給干掉啊。
- volatile-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在設(shè)置了過期時間的鍵空間中,移除最近最少使用的 key(這個一般不太合適)。
- volatile-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在設(shè)置了過期時間的鍵空間中,隨機移除某個 key。
- volatile-ttl:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在設(shè)置了過期時間的鍵空間中,有更早過期時間的 key 優(yōu)先移除。
部分內(nèi)容參考以下鏈接
https://gitbook.cn/books/5d07228c2df51311ff3a6498/index.html
https://gitbook.cn/books/5c97654679ca930d56250d14/index.html
總結(jié)
以上是生活随笔為你收集整理的Redis 缓存穿透、雪崩、缓存数据库不一致、持久化方式、分布式锁、过期策略的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Docker | Docker技术基础梳
- 下一篇: golang etcd 报错 undef