緩存用不好,Bug改到老
前言
日常工作中,緩存的使用隨處可見。緩存使用得當(dāng),對提升系統(tǒng)的性能,提高用戶體驗(yàn)感有著至關(guān)重要的作用。但是如果使用不當(dāng),就會出現(xiàn)一些令人費(fèi)解或者數(shù)據(jù)混亂的問題。本文將給大家普及常見的一些緩存使用與緩存使用過程中的踩坑點(diǎn),希望能幫助大家更好的理解與使用緩存,文中如有寫的不對的地方,歡迎大家留言指正。
java緩存形式/介質(zhì)
眾所周知,緩存之所以訪問速度快,是因?yàn)榘丫彺娴慕换ソ橘|(zhì)是內(nèi)存。而常規(guī)的例如mysql數(shù)據(jù)交互介質(zhì)是磁盤。那么常見的java中或者中間件供我們可以用來做緩存的開發(fā)的工具有幾種呢?
jvm本地內(nèi)存
jvm本地內(nèi)存常見使用為定義一個全局靜態(tài)變量,保證后端服務(wù)在運(yùn)行過程中,對應(yīng)的對象空間直接保持被引用,不會被GC給回收。
guava緩存工具類
存儲數(shù)據(jù)的本質(zhì)與jvm內(nèi)存類似,內(nèi)部依靠維護(hù)java集合的子類來存儲數(shù)據(jù),但是提供了緩存數(shù)據(jù)的過期時間,過期策略等設(shè)置,像一個小型的中間件。
redis
Redis 是完全開源的,遵守 BSD 協(xié)議,是一個高性能的 key-value 數(shù)據(jù)庫。常用作數(shù)據(jù)庫、緩存和消息代理。Redis 提供了諸如字符串、散列、列表、集合、帶范圍查詢的排序集合、位圖、hyperloglogs、地理空間索引和流streams等數(shù)據(jù)結(jié)構(gòu)。Redis 內(nèi)建復(fù)制、支持 Lua 腳本、支持 LRU 緩存淘汰策略、事務(wù)和不同級別的磁盤持久化,并通過 Redis Sentinel 和 Redis Cluster 自動分區(qū)提供高可用性。同類型中間件中,Redis是最火的,沒有之一。
幾種常用緩存的對比
jvm緩存 | guava緩存 | redis緩存 | |
---|---|---|---|
速度 | 第一 | 第二 | 第三 |
緩存數(shù)據(jù)是否占用jvm內(nèi)存 | 是 | 是 | 否 |
提供過期時間等策略 | 否 | 是 | 是 |
能否緩存大量數(shù)據(jù) | 否 | 否 | 是 |
應(yīng)用重啟緩存是否丟失 | 是 | 是 | 否 |
使用場景 | 字典類型數(shù)據(jù),加載后修改頻率低 | 支持jvm緩存所有功能,并且適合與緩存token類型具有時效性的數(shù)據(jù) | 支持guava緩存所有功能,支持日常工作所有緩存場景,應(yīng)用系統(tǒng)數(shù)據(jù)重啟與否不影響緩存數(shù)據(jù)的加載與使用 |
緩存常見的坑
在分析緩存的坑之前我們先來看一下緩存的增刪改查如何保證數(shù)據(jù)庫與緩存的數(shù)據(jù)一致性。
查詢
查詢時先查詢緩存,如果緩存存在直接返回,不存在則查詢數(shù)據(jù)庫,將數(shù)據(jù)庫查詢結(jié)果寫入緩存【此處默認(rèn)數(shù)據(jù)庫存在數(shù)據(jù),不存在的情況后面分析】,然后將緩存的結(jié)果數(shù)據(jù)返回。
增刪改
增刪改時先增刪改數(shù)據(jù)庫,保證數(shù)據(jù)庫數(shù)據(jù)先被修改,然后同步緩存內(nèi)數(shù)據(jù),如果中間發(fā)生異常,則調(diào)用數(shù)據(jù)庫事務(wù)回滾數(shù)據(jù)。
緩存穿透
概念
正常情況下,查詢的數(shù)據(jù)都存在,如果請求一個不存在的數(shù)據(jù),也就是緩存和數(shù)據(jù)庫都查不到這個數(shù)據(jù),每次都會去數(shù)據(jù)庫查詢,這種查詢不存在數(shù)據(jù)的現(xiàn)象我們稱為緩存穿透。如上圖,用戶一直請求接口查詢不存在的id數(shù)據(jù)【數(shù)據(jù)庫中只存在id>0的數(shù)據(jù)】,對應(yīng)的數(shù)據(jù)永遠(yuǎn)不可能在緩存中。在高并發(fā)場景下,大量請求打到了數(shù)據(jù)庫,數(shù)據(jù)庫可能被突發(fā)的流量給打掛。
3.1.2.解決方式
1.過濾垃圾數(shù)據(jù)
在知道查詢的id數(shù)據(jù)大于0或者基于id是某種規(guī)則【例如雪花id】生成的情況下。過濾掉數(shù)據(jù)庫中不可能的存在的請求。方法入口直接增加一個參數(shù)校驗(yàn)。
2.緩存空值
發(fā)生穿透的原因是數(shù)據(jù)在數(shù)據(jù)庫中不存在,那我們把null值給緩存下來,當(dāng)請求到達(dá)時直接返回null。當(dāng)然這里對緩存是必須加上過期時間的,以免后續(xù)真的存在此id的數(shù)據(jù)。過期時間不宜過長,根據(jù)實(shí)際業(yè)務(wù)場景并發(fā)量來進(jìn)行設(shè)置。
3.IP攔截
對于惡意的攻擊請求,一直請求無效的數(shù)據(jù),可以設(shè)置ip請求策略。如果對應(yīng)的ip短時間內(nèi)發(fā)起了大量請求,且請求參數(shù)均為不存在的數(shù)據(jù)。則將ip進(jìn)行封禁一段時間,不允許再次請求系統(tǒng)。
**4.布隆過濾器
布隆過濾器(BloomFilter)用來判斷某個元素(key)是否存在于某個集合中我們把有數(shù)據(jù)的key都放到BloomFilter中,每次查詢的時候都先去BloomFilter判斷,如果沒有就直接返回null 。
注意BloomFilter沒有刪除操作,對于刪除的key,查詢就會經(jīng)過BloomFilter然后查詢緩存再查詢數(shù)據(jù)庫,所以BloomFilter可以結(jié)合緩存空值用,對于刪除的key,可以在緩存中緩存null
緩存擊穿
嚴(yán)格意義上說緩存穿透是緩存擊穿的一種。只不過緩存擊穿是查詢的有效數(shù)據(jù)。在高并發(fā)情況下,查詢緩存時,緩存中的數(shù)據(jù)不存在或者已經(jīng)失效了。那么會導(dǎo)致大量的請求打到了數(shù)據(jù)庫,打掛數(shù)據(jù)庫
解決方式
先來分析一下場景,大量請求同時到了緩存,緩存不存在,再請求數(shù)據(jù)庫。然后再將請求結(jié)果寫入到緩存。問題就是多個線程幾乎同時讀取緩存,又幾乎同時重寫緩存。多線程并發(fā)下解決問題,當(dāng)然是用鎖。單體應(yīng)用可以使用synchronized關(guān)鍵字或者ReentrantLock進(jìn)行加鎖,分布式服務(wù)則可使用分布式鎖的方式來實(shí)現(xiàn)加鎖。
大致的偽代碼如下:
- public Object query(){
- //查詢緩存,存在則直接返回
- Object value = queryCache;
- //這里為了防止緩存穿透,可以緩存下空對象,而不是null,保證null值是必須查詢緩存的
- if(Objects.nonNull(value)){
- return value;
- }
- //加鎖訪問數(shù)據(jù)庫
- lock{
- //二次查詢緩存,避免在高并發(fā)的情況下,多個線程都到了爭搶鎖的這個環(huán)節(jié),在此之前
- //已經(jīng)有線程拿到鎖寫入緩存了,無需再次查詢數(shù)據(jù)庫,直接返回即可
- value = queryCache;
- if(Objects.nonNull(value)){
- return value;
- }
- //查詢數(shù)據(jù)庫
- value = queryDb;
- //設(shè)置緩存數(shù)據(jù)
- setCache;
- //返回結(jié)果
- return value;
- }
- }
- 復(fù)制代碼
緩存雪崩
概念
緩存雪崩也是緩存擊穿的一種,緩存設(shè)置了過期時間/淘汰策略的情況下,在某個時間點(diǎn),大量的緩存失效。高并發(fā)情況下大量請求打到了數(shù)據(jù)庫。
解決方式
緩存雪崩時,請求方式與緩存擊穿一致,主要如何防護(hù)緩存雪崩,基本指導(dǎo)思想為:
熱點(diǎn)數(shù)據(jù)設(shè)置永不過期,緩存淘汰策略為淘汰最早過期數(shù)據(jù)
數(shù)據(jù)緩存過期時間設(shè)置高離散度隨機(jī)值,避免某個時間點(diǎn),大量緩存同時過期。
性能問題
概念
使用了緩存,但是性能還是上不去的場景。例如雙十一場景下,訂單數(shù)據(jù)量比較大。如果新增修改刪除所有操作都要先操作一遍數(shù)據(jù)庫,再回寫緩存的話效率是很低的。
解決方式
把緩存當(dāng)做數(shù)據(jù)庫來使用,當(dāng)然這里需要使用redis這種高可用的持久化緩存中間件。數(shù)據(jù)存在redis中,數(shù)據(jù)交互都直接交互redis??高^流量高峰之后,啟用定時任務(wù),將redis的數(shù)據(jù)刷入至數(shù)據(jù)庫或者ES。當(dāng)然這里也可以使用消息隊(duì)列,這里不具體展開。
總結(jié)
本文著重講述了緩存的增刪改查策略與日??狱c(diǎn)。
數(shù)據(jù)一致性
查詢操作,先走緩存再走數(shù)據(jù)庫,再更新緩存。
增刪改操作,先走數(shù)據(jù)庫再更新緩存。
坑點(diǎn)
從緩存雪崩去理解緩存穿透與緩存擊穿。
高性能
讀寫持久化緩存數(shù)據(jù),異步刷盤mysql。
本文轉(zhuǎn)載自微信公眾號「碼農(nóng)小胖哥」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系碼農(nóng)小胖哥公眾號。