1.緩存
緩存,數據交換的緩沖區(qū),針對緩沖對象的不同(不同的硬件)都可以構建緩存。
目的是,把讀寫速度慢的介質數據保存在讀寫速度快的介質中,從而提高讀寫的速度,減少時間消耗。例如:
- CPU高速緩存:高速緩存的讀寫速度遠高于內存。
- CPU讀數據時,如在高速緩存中找到所需數據,就不需要讀內存
- CPU寫數據是,先寫入到高速緩存中,在回寫到內存
- 磁盤緩存:磁盤緩存其實就把常用的磁盤數據保存在內存中,內存的讀寫數據也遠高于磁盤的
- 讀數據時從內存中去取
- 寫數據時,可以先寫到內存中,定時或者定量回寫到磁盤,或者同步回寫。
目的:使用緩存的目的就是為了提升讀寫性能。在實際的業(yè)務場景中就是為了提升讀性能,帶來更好的性能,更高的并發(fā)量
2.緩存算法
- LRU(least recentlty used,最近最少使用)算法
- LFU(least Frequently used,最不經常使用)
- FIFO(first in first out,先進先出)
3.緩存穿透
存儲穿透:是指查詢一個一定不存在的數據,由于緩存是不命中時被動寫,并且處于容錯考慮,如果從DB查不到數據就不寫入緩存,這將導致這個不存在的數據一直去請求DB,是去緩存的意義。
被動寫:當從緩存中查詢不到數據的時候,從DB查到數據,然后將數據寫入到緩存。

如何解決?
-
方案一,緩存空對象。
當從DB查詢數據為空的時候,我們仍然將這個結果進行緩存,具體的值需要使用特殊的標識,能和真正的緩存數據區(qū)分開。另外需要設置較短的過期時間,不建議超過5分鐘。緩存時間太久沒意義,浪費緩存內存。
-
方案二,BloomFilter布隆過濾器
在緩存服務的基礎上,構建BloomFilter數據結構。在BloomFilter中存儲對應的key是否存在,如果存在,再說明該key的值不為空,邏輯如下:
- 根據key查詢BloomFilter,如果不存在對應的值,直接返回。如果存在,繼續(xù)向下執(zhí)行。
- 根據key的值,查詢緩存的值,如果存在,直接返回,不存在,向下執(zhí)行。
- 查詢db的值,如果存在,更新到緩存,直接返回。
為什么BloomFilter不存儲Key是不存在的情況?
- BloomFilter存在誤判。簡單來說,存在的不一定存在,不存在的不一定不存在。一個誤判的key就會被誤判為不存在。
- BloomFilter不允許刪除。如果一個key開始不存在,后面又有數據了,這時候會被判斷為一致不存在。
緩存空對象 BloomFilter 使用場景 1.數據命中率不高;2.保證一致性 1.數據命中不高。2數據相對固定,實時性低 維護成本 1.代碼維護簡單;2.需要過多的緩存空間;3.數據不一致 1.代碼維護復雜;2.緩存空間占用少
4.緩存雪崩
概念
緩存雪崩,是指緩存由于某些原因無法提供服務(比如緩存掛了),所有的請求到達DB,導致DB負荷增大,最終掛掉的情況。
如何解決
-
緩存高可用
通過搭建緩存的高可用,避免緩存掛掉導致無法提供服務的情況,從而降低出現緩存雪崩的情況。Redis可以通過搭建Redis Sentinel或者Redis Cluster來做緩存高可用
-
本地緩存
使用本地緩存,即使分布式緩存掛了,也可以將DB查詢到的數據緩存到本地,避免后續(xù)請求全部到達DB。
-
請求DB限流
通過限制DB的每秒請求數,避免DB掛掉,這樣做的好處:
- 有一部分用戶任然可以使用該系統(tǒng)。
- 緩存服務恢復后,立即可以使用。不用再去管理DB服務
被限流的請求,我們可以通過服務降級,提供一些默認的值,比如空白頁面、友情提示等到。我們可以通過Sentinel、Hystrix等來實現。
引入本地緩存的問題
- 本地緩存實時性如何保證
- 引入消息隊列。在數據更新時,發(fā)布數據更新的消息;而進程中有相應的消費者消費該消息,從而更新本地緩存;
- 設置較短的過期時長,請求從DB拉取數據;
- 通過手動過期;
- 每個進程可能會在本地緩存相同的數據,導致資源浪費?
- 需要配置本地的緩存過期策略和緩存數量上限。
5.緩存擊穿
概念
緩存擊穿,是指在某個熱點數據在某個時間點過期的時候,恰好這個時間對這個key有大量的的并發(fā)請求過來,這些請求發(fā)現緩存過期,都會去請求到DB加載數據并回寫到緩存,這個時候過大的并發(fā)就有可能瞬間把DB壓垮。
如何解決
-
方案一,使用互斥鎖
請求發(fā)現緩存不存在后,去查詢DB前,使用分布式鎖,保證只有一個線程拿到鎖,然后取請求DB,然后更新寫入緩存
- 1獲取分布式鎖,直到成功或者超時。如果超時則拋出異常,成功,繼續(xù)向下執(zhí)行
- 2、獲取緩存,如果存在值,直接返回,如果不存在,繼續(xù)。
- 3、查詢DB,更新到緩存,返回值。
-
方案二,手動過期
緩存上不設置過期時間,功能上將過期時間設置在Value中。流程如下
- 1、獲取緩存,通過value中的時間來比較是否過期。如果未過期,直接返回,如果過期,繼續(xù)向下執(zhí)行。
- 2、通過一個后臺的異步線程進行緩存的構建,也就是“手動過期”。通過后臺的異步線程,保證只有一個線程查詢DB。
- 3、同時,雖然Value還是過期,但還是直接返回。通過這樣的方式保證了服務的可用性,但是損失了一定的實效性。
6.緩存與DB一致性保持
產生不一致的原因
-
并發(fā)場景下,導致老的DB數據寫入到緩存中。
這里指的是,更新DB之前,先刪除Cache中的數據。在低并發(fā)的情況下,不會出問題,但是在高并發(fā)的情況下就會出問題。在刪除Cache和更新DB之間,這里恰好有請求進來,這時候使用被動讀,因為在DB中的數據還是老數據,這時候又會將老數據寫入到緩存中。
緩存和DB的操作不在同一個事物中,可能DB操作成功,二cache操作失敗,這樣會導致不一致
解決方案
-
將緩存可能出現的并行寫,實現串行寫
這里指的緩存并行寫。在被動讀中,如果緩存不存在,也存在寫。
-
在寫請求前,先淘汰緩存之前,先獲取分布式鎖。
寫-->獲取鎖-->delete cache-->update db ---> write cache
-
在讀請求時,如果緩存不存在,先去獲取分布式鎖
讀-->讀cache,如果null-->獲取鎖-->查詢cache,如果null-->查db-->write cache
-
-
實現緩存的最終一致性。
-
先淘汰緩存,在寫數據庫。
因為先淘汰緩存,所以數據的最終一致性是可以保證的。為什么尼?先淘汰緩存,即使寫數據庫發(fā)生異常,在下次緩存讀取時候,多讀取一次數據庫。
-
先寫數據庫,在更新緩存
需要保證寫數據庫和更新緩存的操作,能夠在一個”事務“中,從而實現最終一致性。
基于定時任務來實現
- 首先,寫入數據庫
- 然后在寫入數據庫所在的事物中,插入一個記錄的任務表,該記錄表存儲需要存入cache中的key和value
- 【異步】遍歷任務表,寫入cache
基于消息隊列來實現
- 首先,寫入數據庫
- 然后發(fā)送帶我緩存key和value的事物消息。
- 【異步】消費者消費該消息,寫入緩存
基于數據庫的binlog日志
image- 應用直接將數據寫入數據庫
- 數據庫會更新binlog日志
- 利用Cannal中間件讀取binlog日志
- Cannal借助于限流主鍵按頻率將數據發(fā)送到MQ中
- 應用監(jiān)控MQ渠道,將MQ的數據緩存到cache中
-
7.緩存預熱
啟動時,先將熱點數據緩存到緩存中
如何實現
- 數據量不大的時候,項目啟動,自動進行初始化
- 通過修復腳本,執(zhí)行腳本
- 寫管理頁面,手動操作
7.緩存淘汰策略
- 定時清理
- 當用戶請求過來的時候,去判斷是否過期,過期的話就去底層系統(tǒng)獲取數據,進行更新
