如何使用 Redis 緩存
前言
對於 Redis 來講,作為緩存使用,是我們在業務中經常使用的,這里總結下,Redis 作為緩存在業務中的使用。
旁路緩存
Cache Aside(旁路緩存)策略以數據庫中的數據為准,緩存中的數據是按需加載的。它可以分為讀策略和寫策略。
只讀緩存
只讀緩存 從緩存中讀取數據;如果緩存命中,則直接返回數據;如果緩存不命中,則從數據庫中查詢數據;查詢到數據后,將數據寫入到緩存中,並且返回給用戶。
如果需要對數據進行修改的時候,直接修改數據庫中的數據,然后刪除緩存中的舊數據。
只讀緩存的優點:
所有最新的數據都在數據庫中,數據不存在丟失的風險。
缺點:
每次修改數據,都會刪除緩沖,之后的請求會發生一次緩存缺失。
讀寫緩存
除了進行讀操作外,數據的修改操作也會發送到緩存中,直接在緩存中對數據進行修改。此時,得益於Redis的高性能訪問特性,數據的增刪改操作可以在緩存中快速完成,處理結果也會快速返回給業務應用,這就可以提升業務應用的響應速度。
當然 Redis 是內存數據庫,一旦掉電或宕機,內存中的數據就有可能存在丟失。
針對這種情況,一般會有兩種回寫策略:
- 1、同步回寫;
寫請求發給緩存的同時,也會發給后端數據庫進行處理,等到緩存和數據庫都寫完數據,才給客戶端返回。這樣,即使緩存宕機或發生故障,最新的數據仍然保存在數據庫中,這就提供了數據可靠性保證。
不過,同步直寫會降低緩存的訪問性能。這是因為緩存中處理寫請求的速度是很快的,而數據庫處理寫請求的速度較慢。即使緩存很快地處理了寫請求,也需要等待數據庫處理完所有的寫請求,才能給應用返回結果,這就增加了緩存的響應延遲。
- 2、異步回寫。
所有寫請求都先在緩存中處理。可以定時將緩存寫入到內存中,然后等到這些增改的數據要被從緩存中淘汰出來時,再次將它們寫回后端數據庫。這樣一來,處理這些數據的操作是在緩存中進行的,很快就能完成。只不過,如果發生了掉電,而它們還沒有被寫回數據庫,就會有丟失的風險了。
優點:
被修改的數據永遠在緩存中,不會發生緩存缺失,下次可以直接訪問,不在需要向數據庫中進行一次查詢。
缺點:
數據可能存在丟失的風險。
設置多大的緩存合適
緩存能夠提高響應速度,但是緩存的數量也不是越多越好?
1、大容量緩存是能帶來性能加速的收益,但是成本也會更高;
2、在一些場景中,比如秒殺,少量的緩存承擔的就是絕大部分的流量訪問。
系統的設計選擇是一個權衡的過程:大容量緩存是能帶來性能加速的收益,但是成本也會更高,而小容量緩存不一定就起不到加速訪問的效果。一般來說,建議把緩存容量設置為總數據量的15%到30%,兼顧訪問性能和內存空間開銷。
內存被寫滿了如何處理
Redis 中的內存被寫滿了,就會觸發內存淘汰機制了
具體參加內存淘汰機制
緩存經常遇到的問題
Redis 作為緩存,經常遇到的幾種情況:緩存中的數據和數據庫中的不一致;緩存雪崩;緩存擊穿和緩存穿透。
下面一一來探討下
1、緩存中的數據和數據庫中的不一致
數據一致性,通俗的理解就是,數據庫中的數據和緩沖中的數據完全一致就滿足一致性。不過對於只讀緩存,如果緩沖中沒有就去數據庫中查詢,這樣如果緩存中沒有數據,但是數據庫中的數據是最新的,最終也能滿足數據一致性。
所以總結下,一致性大致分成下面的兩種情況:
1、緩存中有數據,緩存中的數據和數據庫中的數據一樣;
2、緩存中沒有數據,數據庫中記錄了最新的數據。
下面分析下只讀緩存和讀寫緩存中的數據不一致情況
讀寫緩存
讀寫緩存有同步寫回和異步寫回兩種策略
同步寫回:緩存在新增修改的時候,也會同步數據到數據庫中,這樣總能保持緩存中的數據和數據庫中的一致;
異步寫回:緩存新增修改時候,先不寫回到數據庫中,定時或者緩存中數據淘汰的時候,再寫回到數據庫中。這種,如果 Redis 故障宕機了,沒有及時寫回數據到數據庫中,就會造成數據的不一致。
對於讀寫緩存,使用同步寫回的策略,能保證數據數據的一致性。不過,需要在業務應用中使用事務機制,來保證緩存和數據庫的更新具有原子性,也就是說,兩者要不一起更新,要不都不更新,返回錯誤信息,進行重試。否則,我們就無法實現同步直寫。
如果系統沒宕機,redis 系統正常的情況下,因為讀寫緩存,緩存中的數據是一直存在的,所以當修改數據的時候先修改緩存中的數據,這樣就算並發很大的情況下,因為緩存中的數據都是最新的,並且一直存在,這樣數據總能讀取到最新的數據。
只讀緩存
只讀緩存,如果數據新增,直接寫入到數據庫中,如果有數據修改刪除,也是直接操作數據庫不過緩存中的數據不會更新,而是直接刪除緩存中的數據。
這樣數據的更新操作之后,數據庫中的數據總是最新的,緩存中就會發生緩存缺失,此時就會從數據庫中讀取數據,然后再加載到緩存中,這樣緩存中的數據總能和數據庫中的數據一致。
只讀緩存在數據新增的時候,緩存中是沒有數據的,所以肯定是要從數據庫中加載,這種情況不存在數據不一致的情況。
在只讀緩存中,數據不一致的情況,發生在數據的更新刪除操作中,下面來一一分析下
刪改操作既要修改數據庫,同時還要刪除對應的緩存,如果這兩個操作的原子性無法得到保證,(一起操作成功,或者一起操作失敗),那么數據的一致性就得不到保證了。
來個異常的栗子
1、先修改數據庫,然后刪除緩存,但是刪除緩存失敗了;
刪除緩存失敗了,那么緩存中存在的就是舊值,這時候用戶的請求過來了,首先去緩存中查詢,這時候拿到的就是老舊的數據。
2、先刪除緩存,在修改數據庫,修改數據庫失敗了;
緩存刪除成功,數據庫修改失敗了,那么數據庫中存在的就是舊值,因為緩存已經被刪除了,這時候去緩存中查詢,發生了緩存的缺失,數據就會從數據庫中加載到緩存中,這時候讀取到也是老舊的數據。
針對這種問題如何解決呢?
上面出現異常的兩種場景,歸根到底,就是兩者操作的原子性沒有得到保證,所以可以借助於消息隊列實現最終的一致性。
使用 mq 解決分布式事務可參見分布式事務
這里的操作場景相對簡單一點,只要借助於 mq 的重試機制,保證第二步的操成功就可以了。
栗如:
1、先修改數據庫;
2、發送刪除緩存的消息到 mq 中;
3、下游收到刪除的消息,操作刪除緩存,如果失敗,借助於 mq 的重試機制,就能進行重試操作,直到成功。當然如果,重試多次還是失敗,我們需要記錄錯誤原因,然后通知業務方。

那到底應該先刪除緩存還是先修改數據庫呢?這里我們再探討一下
1、先刪除緩存后修改數據庫
先刪除緩存,然后修改數據庫
如果數據庫的更新有延遲,那么這時候一個線程過來查詢該數據,因為緩存中已經刪除了,這時候發生了緩存的缺失,然后就回去數據庫中查詢,數據庫可能還沒有更新成功,就可能獲取到舊值。
如何解決呢
使用 延遲雙刪 策略
當數據庫被修改之后,線程 sleep 一段時間,然后再次刪除緩存,然緩存發生一次缺失,這樣下次的請求,就能把數據庫中最新的數據加載到緩存中。

比如上面的這種情況,因為數據庫的更新可能存在延遲,所以時候線程2讀取到了數據庫的舊值,然后加載到了緩存中,這樣接下來的所有的查詢就都會讀取舊值
所以 線程1,通過延遲雙刪來處理這種情況
線程1,在 sleep 一段時間之后,刪除緩存,這樣就能使后續的緩存缺失,后續的查詢就能加載數據庫中最新的數據到緩存中。
不過 sleep 的時間需要大於,線程2,讀數據並且寫入數據到內存的時間,如果 sleep 時間過小,這時候線程2,的舊值還沒有寫入到緩存中,線程1,已經再次刪除了緩存,然后這時候線程2把舊值寫入,導致緩存中依然是舊數據。
redis.delKey(X)
db.update(X)
Thread.sleep(N)
redis.delKey(X)
當然,這在 sleep 的時間內,還是有一部分請求會讀取到舊值
2、先修改數據庫然后刪除緩存
先修改數據庫,然后刪除緩存
如果緩存刪除有延遲,那么這時候過來的請求,就會讀取到緩存中老舊的數據,不過緩存會馬上被刪除,只會有少部分的數據讀取到老舊的數據,對業務影響比較小。
經過對比,發現先修改數據庫然后在刪除緩存,對我們業務的影響比較小,同時也跟容易處理。
只讀緩存和讀寫緩存如何選擇
讀寫緩存對比只讀緩存
優點:緩存中一直會有數據,如果更新操作后會立即再次訪問,可以直接命中緩存,能夠降低讀請求對於數據庫的壓力。
缺點:如果更新后的數據,之后很少再被訪問到,會導致緩存中保留的不是最熱的數據,緩存利用率不高(只讀緩存中保留的都是熱數據)。
所以讀寫緩存比較適合用於讀寫相當的業務場景。
2、緩存雪崩
什么是緩存雪崩
緩存雪崩是指大量的應用請求無法在Redis緩存中進行處理,緊接着,應用將大量請求發送到數據庫層,導致數據庫層的壓力激增。
緩存雪崩有兩種場景
1、大量緩存同時過期
如果有大量的緩存 key 設置了同樣的過期時間,如果這些緩存 key 過期了,同時有大量的請求,進來了,這些請求就會直接打到數據庫中,數據庫可能因為這些請求,導致數據庫壓力增大,嚴重的時候數據庫宕機。
如何解決呢?
1、避免給大量的過期鍵設置相同的過期時間,設計過期時間的時候,可以考慮加入一個業務上允許的過期隨機值;
2、服務降級,只有部分核心業務的請求,才會流轉到數據庫中,數據庫的壓力就會被大大減輕了;
-
當業務應用訪問的是非核心數據(例如電商商品屬性)時,暫時停止從緩存中查詢這些數據,而是直接返回預定義信息、空值或是錯誤信息;
-
當業務應用訪問的是核心數據(例如電商商品庫存)時,仍然允許查詢緩存,如果緩存缺失,也可以繼續通過數據庫讀取。
2、Redis 實例發生宕機
Redis 實例的宕機,緩存層就不能處理數據,最總流量都會流入到數據庫中
如何解決呢?
1、業務中實現服務熔斷或者請求限流機制;
-
服務熔斷:如果監聽到發生了緩存雪崩,直接暫停對緩存服務的請求,但是這種對業務的影響比較大;
-
服務限流:可以在入口做限流,不要讓所有的請求都流入到后端的服務中;
2、提前預防,搭建 Redis 的高可用集群;
- 嘗試構建 Redis 的高可用集群,比如當某主節點掛掉了,集群能夠馬上重新選出新的主節點。例如哨兵機制
3、緩存擊穿
其實跟緩存雪崩有點類似,緩存雪崩是大規模的key失效,而緩存擊穿是一個熱點的Key,有大並發集中對其進行訪問,突然間這個Key失效了,導致大並發全部打在數據庫上,導致數據庫壓力劇增。這種現象就叫做緩存擊穿。
如何解決?
對於熱點 key 可以不設置過期時間,或者設置一個超過使用周期的過期時間,保證這個 key 在業務使用期間永遠存在。
4、緩存穿透
如果業務請求的緩存,既不在緩存中,也不再數據庫中,那么緩存將沒有用,所有的請求都會流入到數據庫中。
那么,緩存穿透會發生在什么時候呢?一般來說,有兩種情況。
1、業務層誤操作:緩存中的數據和數據庫中的數據被誤刪除了,所以緩存和數據庫中都沒有數據;
2、惡意攻擊:專門訪問數據庫中沒有的數據。
如何解決?
1、緩存空值或缺省值;
一旦發生緩存穿透,在緩存中寫入一個業務中允許的空值,這樣緩存中有數據了,就避免了緩存穿透。
2、使用布隆過濾器;
使用布隆過濾器判斷下數據是否存在,數據如果不存在,就不向數據庫發起請求了。
3、在請求入口的前端進行請求檢測;
緩存穿透的一個原因是有大量的惡意請求訪問不存在的數據,所以,一個有效的應對方案是在請求入口前端,對業務系統接收到的請求進行合法性檢測,把惡意的請求(例如請求參數不合理、請求參數是非法值、請求字段不存在)直接過濾掉,不讓它們訪問后端緩存和數據庫。這樣一來,也就不會出現緩存穿透問題了。
緩存中的 hot key 和 big key
這兩種的處理方式可參見
總結
對於緩存的使用,我們經常用到的有兩種1、只讀緩存;2、讀寫緩存;
只讀緩存,對比讀寫緩存
優點:緩存中一直會有數據,如果更新操作后會立即再次訪問,可以直接命中緩存,能夠降低讀請求對於數據庫的壓力。
缺點:如果更新后的數據,之后很少再被訪問到,會導致緩存中保留的不是最熱的數據,緩存利用率不高(只讀緩存中保留的都是熱數據)。
所以讀寫緩存比較適合用於讀寫相當的業務場景。
緩存在使用的過程中,會面臨緩存中的數據和數據庫中的不一致;緩存雪崩;緩存擊穿和緩存穿透,這些我們需要弄明白這些情況發生的額場景,然后再業務中一一去避免。
參考
【Redis核心技術與實戰】https://time.geekbang.org/column/intro/100056701
【Redis設計與實現】https://book.douban.com/subject/25900156/
【什么是緩存雪崩、緩存擊穿、緩存穿透】https://zhuanlan.zhihu.com/p/346651831
【詳解布隆過濾器的原理,使用場景和注意事項】https://zhuanlan.zhihu.com/p/43263751
【Redis 學習筆記】https://github.com/boilingfrog/Go-POINT/tree/master/redis
【如何使用Redis緩存】https://boilingfrog.github.io/2022/04/20/Redis中緩存如何使用/