今天我們來(lái)聊一聊在大型分布式系統(tǒng)中,緩存應(yīng)該怎么玩,從畢業(yè)到現(xiàn)在也有三年多了,大大小小的系統(tǒng)也經(jīng)歷了幾十個(gè),今天就從各個(gè)角度來(lái)討論一下,我們的不同的緩存應(yīng)該怎么玩,才能用的高效。
我們團(tuán)隊(duì)現(xiàn)在做的是直播類的產(chǎn)品,就拿抖音來(lái)說(shuō),比如說(shuō)要開發(fā)一個(gè)榜一大哥、榜二大哥等各類大哥的排行榜單,要怎么開發(fā),對(duì)于抖音這個(gè)dau十億級(jí)別的產(chǎn)品,緩存的設(shè)計(jì)肯定是家常便飯。
對(duì)于一個(gè)百萬(wàn)、千萬(wàn)級(jí)別的接口調(diào)用,若是沒(méi)有緩存的設(shè)計(jì),直接打到數(shù)據(jù)持久層,那將是毀滅性的災(zāi)難。之前我就經(jīng)歷過(guò),一個(gè)接口一天幾百萬(wàn)次的調(diào)用,因?yàn)榫彺娴脑O(shè)計(jì)不嚴(yán)謹(jǐn),緩存失效后,瞬間直接打在數(shù)據(jù)庫(kù)層,幸好有告警,及時(shí)修復(fù),差點(diǎn)就領(lǐng)了p0的故障。
大體來(lái)說(shuō)緩存分為客戶端緩存 和服務(wù)端緩存 ,客戶端緩存我們比較常見的就是瀏覽器緩存,也就是通過(guò)http進(jìn)行控制的緩存。
客戶端緩存
基于請(qǐng)求-應(yīng)答模式下,在大多數(shù)場(chǎng)景下客戶端都是通過(guò)https協(xié)議,請(qǐng)求后臺(tái)獲取數(shù)據(jù),若是高頻的接口一天幾百萬(wàn)次的調(diào)用,即使短時(shí)間的客戶端緩存也會(huì)帶來(lái)高效的收益。
因?yàn)榭蛻舳说椒?wù)端要經(jīng)過(guò)漫長(zhǎng)的網(wǎng)絡(luò)鏈路,多變的網(wǎng)絡(luò)環(huán)境,數(shù)據(jù)包可能小的幾十K到大的數(shù)據(jù)包幾十M,這樣就能夠省去復(fù)雜多變的網(wǎng)絡(luò)請(qǐng)求的時(shí)間。
客戶端緩存減少了客戶端到服務(wù)端之間的通信次數(shù)以及成本,只要緩存可用,就能夠及時(shí)響應(yīng)數(shù)據(jù)。
客戶端緩存常見的也就是瀏覽器緩存,簡(jiǎn)而言之也就是http緩存,不知道大家在實(shí)際開發(fā)過(guò)程中有沒(méi)有用過(guò)這段代碼:
ResponseEntity.ok().cacheControl(CacheControl.maxAge(3,TimeUnit.SECONDS)).body()
看他的包,他就是屬于springframework 框架下http包下的一個(gè)工具類。
importorg.springframework.http.CacheControl; importorg.springframework.http.ResponseEntity;
在瀏覽器緩存中,http協(xié)議header有這么個(gè)key-value字段進(jìn)行控制,叫做Cache-Control:max-age=30 ,max-age標(biāo)志該資源在客戶端緩存多少秒。
假如max-age=0,表示不緩存數(shù)據(jù),除了max-age可以控制數(shù)據(jù)的緩存狀態(tài),還有以下三個(gè)屬性來(lái)控制緩存狀態(tài)no_store、no_cache、must-revalidate 。
no_store表示不緩存數(shù)據(jù),每次都去服務(wù)器獲取 。
no_cache看起來(lái)也是不緩存的意思,但是它表示的意思是可以緩存的,只不過(guò)在使用緩存之前,都要去服務(wù)器驗(yàn)證數(shù)據(jù)是否有效,是否過(guò)期,是否是最新版本 。
must-revalidate和no_cache有點(diǎn)類似,就是緩存不過(guò)期的話可以繼續(xù)使用,過(guò)期了就需要去服務(wù)器驗(yàn)證一下 。
除了Cache-Control可以使用客戶端緩存,在http里面還有一個(gè)條件請(qǐng)求的header更加智能的使用客戶端緩存。
條件請(qǐng)求是基于響應(yīng)報(bào)文返回的“Last-modified ”和“ETag ”實(shí)現(xiàn)的。Last-modified資源最后的一次修改時(shí)間,ETag則表示資源的唯一標(biāo)識(shí),你可以理解為只要資源修改后都不一樣了 。
再次請(qǐng)求的時(shí)候在請(qǐng)求頭里面就會(huì)帶上"If-None-Match:ETag返回的值 ",去驗(yàn)證資源是否有效。
假如有效的話,就會(huì)返回"304 Not Modified ",表示緩存資源有效還可以繼續(xù)使用。
但是這種方式我較少使用,基本上使用Cache-Control就夠了,控制好實(shí)效的時(shí)間,一般的場(chǎng)景都是允許短暫的不一致。
除了客戶端能夠發(fā)送Cache-Control之外,客戶端也能夠發(fā)送Cache-Control兩者進(jìn)行協(xié)商使用客戶端緩存的方案。
像我在瀏覽器訪問(wèn)一個(gè)連接,在輸入框敲一下回車,Request的Headers里面就有Cache-Control:max-age = 0,表示不使用緩存,直接去后臺(tái)獲取數(shù)據(jù)。
所以Cache-Control來(lái)控制客戶端緩存也不太好控制,要兩者協(xié)商好,但是Cache-Control有一個(gè)好處就是可以控制CDN緩存。
服務(wù)端緩存
CDN緩存
上面聊到Cache-Control來(lái)控制客戶端緩存,它也同時(shí)影響CDN緩存,告訴CDN客戶緩存這個(gè)接口的數(shù)據(jù)。
CDN服務(wù)一般是由第三方提供的內(nèi)容分發(fā)網(wǎng)絡(luò)服務(wù),主要是用于緩存靜態(tài)的數(shù)據(jù),比如:圖片、音頻、視頻,這些數(shù)據(jù),都是不不變的,那么命中率就很高。
不用回源獲取數(shù)據(jù),效率高,畢竟使用CDN的費(fèi)用高,一般小公司也不會(huì)用,可能大公司采用。
CDN廠商花費(fèi)大價(jià)錢在全國(guó)各地建立CDN的服務(wù)站點(diǎn),用于用戶的就近訪問(wèn),減少響應(yīng)時(shí)間。
所以這個(gè)對(duì)于應(yīng)用層的來(lái)說(shuō)是0開發(fā)的,一般只要在你的服務(wù)治理平臺(tái)針對(duì)某一個(gè)接口配置一下就好了。
除了緩存靜態(tài)數(shù)據(jù),想一些動(dòng)態(tài)數(shù)據(jù),但是不會(huì)經(jīng)常變的數(shù)據(jù)CDN也是可以緩存的,只不過(guò)可能緩存的時(shí)間設(shè)置的比較短,那么在高并發(fā)場(chǎng)井下取得的效益也是比較大的。
好了,關(guān)于CDN的也沒(méi)啥好說(shuō)的,我還沒(méi)接觸過(guò)CDN開發(fā),但是項(xiàng)目中使用到了,就是簡(jiǎn)單而配置一下而已,等我接觸到開發(fā),再和你們?cè)斄?,CDN其實(shí)就是代理源站服務(wù)器緩存數(shù)據(jù)而已。
Redis緩存
在服務(wù)端緩存中Redis緩存可能是我們最常見的緩存,可以說(shuō)Redis已經(jīng)是各大公司常用的緩存中間件選型之首,也不為過(guò)。
我們項(xiàng)目中也是在使用Redis,只不過(guò)在Redis的層面上進(jìn)行封裝,包括Redis哨兵、Redis分片 ,都是基于自己的業(yè)務(wù)情況下進(jìn)行二次開發(fā),然后供自己的業(yè)務(wù)使用。
在Redis的基礎(chǔ)數(shù)據(jù)類型中,有五大類型供大家選擇,包括String、List、Set、SortedSet、Hash 。這五種數(shù)據(jù)類型在百分之九十五的場(chǎng)景下都能夠解決,并且在這五種基本數(shù)據(jù)類型的底層運(yùn)用了高效與省空間的數(shù)據(jù)結(jié)構(gòu),所以Redis的高性能之一也是因?yàn)橛羞@些數(shù)據(jù)結(jié)構(gòu)作為支撐。
圖片來(lái)源于Redis核心技術(shù)與實(shí)戰(zhàn)
比如:要實(shí)現(xiàn)一個(gè)排行榜單,凸顯直播間榜一大哥以及上榜大哥財(cái)大氣粗的實(shí)力。
那么這個(gè)明顯是按照某個(gè)字段進(jìn)行排序,比如刷的抖音幣進(jìn)行排序,那個(gè)在redis中List和Sorted Set都是可以實(shí)現(xiàn)有序的緩存。
List是按照寫入List順序進(jìn)行存儲(chǔ),而Sorted Set是按照某一個(gè)字段的權(quán)重來(lái)排序,并且可以查詢權(quán)重范圍內(nèi)的數(shù)據(jù)。
對(duì)于我們的場(chǎng)景List可能不太適合,因?yàn)槭菍?duì)數(shù)據(jù)每次都是新產(chǎn)生的,并且按照時(shí)間來(lái)順序來(lái)寫入,List集合就比較適合。
我們的場(chǎng)景是某個(gè)在榜大哥不斷的刷禮物,就需要重新對(duì)他進(jìn)行排序,并不是按照每次新增寫入緩存的順序取數(shù),那么按照大哥刷的抖音幣的多少就可以當(dāng)做權(quán)重來(lái)排序,很好的服務(wù)我們的場(chǎng)景,按照時(shí)間來(lái)排序的場(chǎng)景Sorted Set都可以來(lái)做。
還有一些聚合統(tǒng)計(jì)的場(chǎng)景,比如要統(tǒng)計(jì)兩個(gè)key數(shù)據(jù)集的交集、并集、差集可以使用set集合來(lái)做。
假如某一天你的老板讓你開發(fā)一個(gè)統(tǒng)計(jì)每天新增的用戶數(shù)據(jù)功能,其實(shí)那么也就兩個(gè)集合差集,一個(gè)set集合用戶保存所有用戶的id,一個(gè)set用戶保存當(dāng)天用戶的id集合,然后當(dāng)天用戶集合與所有用戶集合的差集就是新增的用戶集合??梢允褂胹et集合中的SDIFFSTORE 命令進(jìn)行實(shí)現(xiàn)。
還有一些二值統(tǒng)計(jì)的場(chǎng)景,也就是基于redis的Bitmap來(lái)統(tǒng)計(jì),他并不記錄數(shù)據(jù)的本身,只能判斷是否存在,有沒(méi)有,Bitmap保存的是bit 位,所以億級(jí)別數(shù)量的存儲(chǔ)只要M級(jí)別的存儲(chǔ)單位就可以了,所以Bitmap非常的節(jié)省空間。
Bitmap中提供SETBIT設(shè)置bit位,以及GETBIT獲取某個(gè)bit位的值,還可以使用BITCOUNT統(tǒng)計(jì)bit位位1的值,比如可以統(tǒng)計(jì)某個(gè)月簽到場(chǎng)景。
Redis高性能的緩存給我們系統(tǒng)帶來(lái)了極大的性能提升,但是同時(shí)也會(huì)有一些類的問(wèn)題,比如數(shù)據(jù)一致性的問(wèn)題、緩存的三大問(wèn)題(擊穿、穿透、雪崩)、與Redis網(wǎng)絡(luò)通信接口超時(shí)、Redis里面緩存的數(shù)據(jù)變多,操作時(shí)間復(fù)雜度大的導(dǎo)致Redis變慢 。
數(shù)據(jù)的一致性
數(shù)據(jù)一致性問(wèn)題指的是緩存與數(shù)據(jù)庫(kù)的一致性問(wèn)題,只要使用緩存就會(huì)有一致性問(wèn)題,現(xiàn)在市場(chǎng)上都不會(huì)要求強(qiáng)一致性,都是追求最終一致性 。
緩存按照是否可寫分為讀寫緩存與只讀緩存 ,大部分是只讀緩存,現(xiàn)在我們來(lái)討論一下只讀緩存一致性問(wèn)題。
只讀緩存的一致性問(wèn)題包括以下以下兩種場(chǎng)景:
先更新數(shù)據(jù)庫(kù),然后刪除緩存。
先刪除緩存,然后更新數(shù)據(jù)庫(kù)。
但是這兩種場(chǎng)景在高并發(fā)場(chǎng)景下都會(huì)有問(wèn)題,先來(lái)看看第一種場(chǎng)景:先更新數(shù)據(jù)庫(kù),然后刪除緩存。
這種場(chǎng)景也會(huì)有一致性問(wèn)題,當(dāng)我們更新了數(shù)據(jù)庫(kù)后,然后刪除緩存,刪除緩存失敗了,此時(shí)請(qǐng)求讀取的緩存還是舊的值。
這種情況下的解決方案就是重試 ,可以在應(yīng)用層重試,也可以放入消息隊(duì)列里面重試,當(dāng)重試次數(shù)達(dá)到最大的限制,就需要發(fā)送告警進(jìn)行人工排查了。
或者設(shè)置比較短的緩存失效時(shí)間,短暫的不一致性,也是可以接受的。
第二種場(chǎng)景:先刪除緩存,然后再更新數(shù)據(jù)庫(kù)。在高并發(fā)場(chǎng)景下也有可能數(shù)據(jù)不一致。
假如線程A刪除了緩存,但是還沒(méi)有更新數(shù)據(jù)庫(kù),然后線程B讀取緩存發(fā)現(xiàn)緩存缺失,然后從數(shù)據(jù)庫(kù)里面讀取舊值,并且緩存到Redis中,后面的請(qǐng)求就會(huì)從Redis中讀取舊值。
這種場(chǎng)景市面上推薦使用延遲雙刪的方案進(jìn)行解決,就是在請(qǐng)求A刪除緩存后,更新數(shù)據(jù)庫(kù),然后等一段時(shí)間刪除緩存,請(qǐng)求A的sleep的時(shí)間大于請(qǐng)求B的讀取數(shù)據(jù)寫入緩存的時(shí)間。
但是這種一般等的時(shí)間不調(diào)好估計(jì),而且在高并發(fā)場(chǎng)景下,讓線程去等無(wú)疑是降低性能,這個(gè)通常是不允許的。
所以一般建議采用第一種方案,先更新數(shù)據(jù)庫(kù),然后刪除緩存的方式。
我們項(xiàng)目中也會(huì)用到讀寫緩存,之前遇到一個(gè)需求就是,在直播過(guò)程中,主播的公屏的流水,要顯示用戶中獎(jiǎng)的橫幅,也就是“恭喜某某在某某直播間抽中了XXX禮物”。
禮物的抽獎(jiǎng)流水之前就已經(jīng)發(fā)送消息隊(duì)列了,所以只要監(jiān)聽對(duì)應(yīng)抽獎(jiǎng)流水topic就行了,然后將中獎(jiǎng)的流水按照排序規(guī)則放入Redis中。然后客戶端從Redis中讀取,其實(shí)很簡(jiǎn)單,流水也不需要存庫(kù),只要展示就行了。
所以Redis的應(yīng)用場(chǎng)景還是很多的,幾乎可以覆蓋開發(fā)中的95%以上的需求
緩存擊穿、穿透、雪崩
使用分布式緩存還會(huì)涉及到緩存的三大問(wèn)題,也就是緩存擊穿、緩存穿透、緩存雪崩 。
緩存穿透 的解決方案有兩種:
緩存空對(duì)象:代碼維護(hù)較簡(jiǎn)單,但是效果不好。
布隆過(guò)濾器:代碼維護(hù)復(fù)雜,效果很好。
緩存空對(duì)象是指當(dāng)一個(gè)請(qǐng)求過(guò)來(lái)緩存中和數(shù)據(jù)庫(kù)中都不存在該請(qǐng)求的數(shù)據(jù),第一次請(qǐng)求就會(huì)跳過(guò)緩存進(jìn)行數(shù)據(jù)庫(kù)的訪問(wèn),并且訪問(wèn)數(shù)據(jù)庫(kù)后返回為空,此時(shí)也將該空對(duì)象進(jìn)行緩存。
若是再次進(jìn)行訪問(wèn)該空對(duì)象的時(shí)候,就會(huì)直接擊中緩存,而不是再次數(shù)據(jù)庫(kù),緩存空對(duì)象實(shí)現(xiàn)的原理圖如下:
緩存空對(duì)象的實(shí)現(xiàn)代碼如下:
publicclassUserServiceImpl{ @Autowired UserDAOuserDAO; @Autowired RedisCacheredisCache; publicUserfindUser(Integerid){ Objectobject=redisCache.get(Integer.toString(id)); //緩存中存在,直接返回 if(object!=null){ //檢驗(yàn)該對(duì)象是否為緩存空對(duì)象,是則直接返回null if(objectinstanceofNullValueResultDO){ returnnull; } return(User)object; }else{ //緩存中不存在,查詢數(shù)據(jù)庫(kù) Useruser=userDAO.getUser(id); //存入緩存 if(user!=null){ redisCache.put(Integer.toString(id),user); }else{ //將空對(duì)象存進(jìn)緩存 redisCache.put(Integer.toString(id),newNullValueResultDO()); } returnuser; } } }
布隆過(guò)濾器是一種基于概率的數(shù)據(jù)結(jié)構(gòu),主要用來(lái)判斷某個(gè)元素是否在集合內(nèi),它具有運(yùn)行速度快(時(shí)間效率),占用內(nèi)存小的優(yōu)點(diǎn)(空間效率),但是有一定的誤識(shí)別率和刪除困難的問(wèn)題。它只能告訴你某個(gè)元素一定不在集合內(nèi)或可能在集合內(nèi)。
在計(jì)算機(jī)科學(xué)中有一種思想:空間換時(shí)間,時(shí)間換空間。一般兩者是不可兼得,而布隆過(guò)濾器運(yùn)行效率和空間大小都兼得,它是怎么做到的呢?
在布隆過(guò)濾器中引用了一個(gè)誤判率的概念,即它可能會(huì)把不屬于這個(gè)集合的元素認(rèn)為可能屬于這個(gè)集合,但是不會(huì)把屬于這個(gè)集合的認(rèn)為不屬于這個(gè)集合,布隆過(guò)濾器的特點(diǎn)如下:
一個(gè)非常大的二進(jìn)制位數(shù)組 (數(shù)組里只有0和1)
若干個(gè)哈希函數(shù)
空間效率和查詢效率高
不存在漏報(bào)(False Negative):某個(gè)元素在某個(gè)集合中,肯定能報(bào)出來(lái)。
可能存在誤報(bào)(False Positive):某個(gè)元素不在某個(gè)集合中,可能也被爆出來(lái)。
不提供刪除方法,代碼維護(hù)困難。
位數(shù)組初始化都為0,它不存元素的具體值,當(dāng)元素經(jīng)過(guò)哈希函數(shù)哈希后的值(也就是數(shù)組下標(biāo))對(duì)應(yīng)的數(shù)組位置值改為1。
實(shí)際布隆過(guò)濾器存儲(chǔ)數(shù)據(jù)和查詢數(shù)據(jù)的原理圖如下:
緩存擊穿是指一個(gè)key非常熱點(diǎn),在不停的扛著大并發(fā),大并發(fā)集中對(duì)這一個(gè)點(diǎn)進(jìn)行訪問(wèn),當(dāng)這個(gè)key在失效的瞬間,持續(xù)的大并發(fā)就穿破緩存,直接請(qǐng)求數(shù)據(jù)庫(kù),瞬間對(duì)數(shù)據(jù)庫(kù)的訪問(wèn)壓力增大。
緩存擊穿這里強(qiáng)調(diào)的是并發(fā),造成緩存擊穿的原因有以下兩個(gè):
該數(shù)據(jù)沒(méi)有人查詢過(guò) ,第一次就大并發(fā)的訪問(wèn)。(冷門數(shù)據(jù))
添加到了緩存,reids有設(shè)置數(shù)據(jù)失效的時(shí)間 ,這條數(shù)據(jù)剛好失效,大并發(fā)訪問(wèn)(熱點(diǎn)數(shù)據(jù))
對(duì)于緩存擊穿的解決方案:
加鎖,用戶出現(xiàn)大并發(fā)訪問(wèn)的時(shí)候,在查詢緩存的時(shí)候和查詢數(shù)據(jù)庫(kù)的過(guò)程加鎖,只能第一個(gè)進(jìn)來(lái)的請(qǐng)求進(jìn)行執(zhí)行,當(dāng)?shù)谝粋€(gè)請(qǐng)求把該數(shù)據(jù)放進(jìn)緩存中,接下來(lái)的訪問(wèn)就會(huì)直接集中緩存,防止了緩存擊穿。
不設(shè)置熱點(diǎn)key的失效時(shí)間
緩存雪崩 是指在某一個(gè)時(shí)間段,緩存集中過(guò)期失效。此刻無(wú)數(shù)的請(qǐng)求直接繞開緩存,直接請(qǐng)求數(shù)據(jù)庫(kù)。
造成緩存雪崩的可能原因有:
reids宕機(jī)
大部分?jǐn)?shù)據(jù)失效
對(duì)于緩存雪崩的解決方案有以下兩種:
搭建高可用的集群,防止單機(jī)的redis宕機(jī)。
設(shè)置不同的過(guò)期時(shí)間,防止同一時(shí)間內(nèi)大量的key失效。
接口超時(shí)&操作時(shí)間復(fù)雜度高
Redis數(shù)據(jù)第三方緩存中間件,要與Redis通信,必須經(jīng)過(guò)網(wǎng)絡(luò),那么經(jīng)過(guò)網(wǎng)絡(luò)就有可能出現(xiàn)網(wǎng)絡(luò)超時(shí)的現(xiàn)象。
之前我們也出現(xiàn)過(guò),某個(gè)機(jī)房因?yàn)榫W(wǎng)絡(luò)波動(dòng),出現(xiàn)了一系列的Redis查詢網(wǎng)絡(luò)超時(shí)的告警。
所以為了解決一時(shí)的網(wǎng)絡(luò)超時(shí),我們有可能還要做好接口重試的機(jī)制,提高接口的可用性。
并且對(duì)Redis五種基本數(shù)據(jù)類型的底層數(shù)據(jù)結(jié)構(gòu)熟悉的,Redis中對(duì)集合類型的操作HGETALL、SMEMBERS,以及對(duì)集合進(jìn)行聚合統(tǒng)計(jì) 等,時(shí)間復(fù)雜度都是O(N)
那么Redis中存儲(chǔ)的數(shù)據(jù)越多,這個(gè)N就越大,操作的復(fù)雜度就越高,這就是所謂的bidkey現(xiàn)象,已經(jīng)出現(xiàn)查詢阻塞了。
當(dāng)然出現(xiàn)這種問(wèn)題時(shí),可以將bigkey按照一定規(guī)律進(jìn)行拆分,這樣分成多個(gè)key進(jìn)行存儲(chǔ),查詢的效率就會(huì)變高。
當(dāng)然Redis的數(shù)據(jù)分片解決方案也可以,將原來(lái)一個(gè)實(shí)例中存儲(chǔ)全量數(shù)據(jù),按照16384進(jìn)行crc16(key) % 16384 決定數(shù)據(jù)存儲(chǔ)于哪個(gè)槽中。
這樣擴(kuò)展性也比較好,不過(guò)一般優(yōu)先推薦拆分key的方案,這樣實(shí)現(xiàn)成本低,實(shí)現(xiàn)簡(jiǎn)單。
緩存消息隊(duì)列玩法
有一些場(chǎng)景還可以使用消息隊(duì)列進(jìn)行更新緩存,用戶更新數(shù)據(jù),異步的發(fā)送消息隊(duì)列,消費(fèi)者就可以監(jiān)聽消息隊(duì)列的消息,消費(fèi)消息后更新緩存。
因?yàn)橛行?shù)據(jù)的更新是需要發(fā)送消息隊(duì)列的,被其他消費(fèi)者監(jiān)聽使用,所以你只要監(jiān)聽消息隊(duì)列就行了。
并且消息的隊(duì)列的消息由消息隊(duì)列的方式來(lái)保證,包括生產(chǎn)者可靠的發(fā)送消息隊(duì)列,通過(guò)ack以及重試保證,消息隊(duì)列本身通過(guò)持久化機(jī)制來(lái)保證,而消費(fèi)者也是通過(guò)消費(fèi)后手動(dòng)ack來(lái)確認(rèn)消息消費(fèi)。
消息對(duì)壘更新緩存
定時(shí)任務(wù)
定時(shí)任務(wù)其實(shí)就是本地緩存了,在分布式系統(tǒng)中,定時(shí)任務(wù)就是每個(gè)服務(wù)中都會(huì)緩存一份,這樣數(shù)據(jù)不一致性也會(huì)加大。
但是在某些場(chǎng)景下,他帶來(lái)的收益也是非常可觀的,比如說(shuō)某個(gè)場(chǎng)景下你要查詢一些安全中臺(tái)的白名單/黑名單列表,而且這些列表不會(huì)經(jīng)常變,可能需求上線后只要配置一下就ok了,后面的更改頻率也是非常的低。
但是你的接口可能是高流量接口,每次用戶進(jìn)來(lái)都會(huì)請(qǐng)求一次,進(jìn)行判斷,而且用戶是千萬(wàn)級(jí)別的,那有可能一天的請(qǐng)求就是上百萬(wàn)次的請(qǐng)求。
那你有兩種選擇來(lái)請(qǐng)求安全中臺(tái)的白名單/黑名單列表,要么就是實(shí)時(shí)請(qǐng)求,要么就是定時(shí)任務(wù)請(qǐng)求本地緩存一份,然后查詢只要從本地獲取就行了。
在這種情況下肯定是定時(shí)任務(wù)請(qǐng)求,帶來(lái)的效益更大,在SprngBoot項(xiàng)目中開啟定時(shí)任務(wù)很簡(jiǎn)單,只需要在你的啟動(dòng)主類上加上這個(gè)注解:**@EnableScheduling**
然后在需要定時(shí)任務(wù)的執(zhí)行類的方法上加上這個(gè)注解:**@Scheduled(cron = "0 0 2 * * ?")** , 其實(shí)就是cron表達(dá)式,執(zhí)行的規(guī)律隔多久執(zhí)行一次。
只要你的時(shí)間配置的足夠短,這樣數(shù)據(jù)也是近實(shí)時(shí)的,不會(huì)差太遠(yuǎn),你可以配置成30秒或者幾十秒執(zhí)行一次,或者幾分鐘執(zhí)行一次都可以,這個(gè)可以和產(chǎn)品進(jìn)行協(xié)商,看產(chǎn)品可以接受多久的延遲。
然后,查詢的中臺(tái)的列表數(shù)據(jù)緩存在本地的一個(gè)map里面,用戶的uid作為map的key,然后后面需要查詢的時(shí)候,直接從map里面獲取。
這樣就不用每次請(qǐng)求過(guò)來(lái)都會(huì)實(shí)時(shí)的調(diào)用中臺(tái)的http/rpc接口查詢數(shù)據(jù),直接從本地獲取提高效率,這也是空間換時(shí)間的思想。
接口超時(shí)
這里需要注意的是,就是要提高你的接口調(diào)用的可用性 ,畢竟中臺(tái)屬于另一個(gè)服務(wù),那么服務(wù)之間涉及遠(yuǎn)程調(diào)用,就有可能存在超時(shí)的現(xiàn)象。
那么你就要確保你的接口99.9%可用,對(duì)于接口超時(shí),你可以就要設(shè)置接口重試 。
因?yàn)橛袝r(shí)候可能是網(wǎng)絡(luò)的原因?qū)е碌囊粫r(shí)超時(shí),設(shè)置被調(diào)用方一時(shí)因?yàn)榫W(wǎng)絡(luò)抖動(dòng)導(dǎo)致超時(shí),那么重試成功的概率就可能比較高。
一般重試的次數(shù)會(huì)設(shè)置為2-3 次比較合理,除非網(wǎng)絡(luò)故障了或者接口一直調(diào)不通,這樣的話就需要及時(shí)告警,通知到開發(fā)人員,及時(shí)檢查到底是哪里的問(wèn)題,確保好接口的兜底方案。
并且還要設(shè)置每次的超時(shí)時(shí)間,設(shè)置超時(shí)時(shí)間也是非常的重要,假如超時(shí)時(shí)間設(shè)置的太短,還沒(méi)有查出來(lái)就已經(jīng)超時(shí)了,這樣就會(huì)導(dǎo)致頻繁超時(shí),浪費(fèi)資源。
要是設(shè)置的超時(shí)間太長(zhǎng),那么線程就會(huì)一直阻塞在那里等待調(diào)用的結(jié)果返回,這樣在高并發(fā)場(chǎng)景下,就會(huì)資源耗盡,系統(tǒng)崩潰。
所以我給你的建議就是可以結(jié)合線上服務(wù)所在服務(wù)器的配置以及qps進(jìn)行配置,配置一個(gè)合理的超時(shí)時(shí)間,合理的時(shí)間內(nèi)能夠超時(shí)返回并且不會(huì)導(dǎo)致資源耗盡。
重試這種機(jī)制,在很多中間件的思想中都會(huì)涉及到,比如:在分布式事務(wù)中2PC和3PC 。
2PC在第二階段提交失敗,那么只能不斷重試,直到所有參與者都成功(回滾或者提交成功)。
因?yàn)槌酥卦嚕瑳](méi)有更好的辦法,只能不斷重試直到都成功,而且多數(shù)情況可能都是一時(shí)的網(wǎng)絡(luò)抖動(dòng)的原因?qū)е碌?,這樣重試成功的概率就非常高。
批量查詢數(shù)據(jù)
定時(shí)任務(wù)緩存其實(shí)也是一種集中式緩存 ,假如緩存的數(shù)據(jù)量也比較大,那么在接口調(diào)用時(shí)就需要批量獲取,但是一次性又不能查詢太多,一般嚴(yán)謹(jǐn)?shù)闹信_(tái)設(shè)計(jì),都會(huì)都傳參進(jìn)行參數(shù)校驗(yàn)。
因?yàn)閷?duì)于調(diào)用方完全是透明的,不可信任的 ,什么參數(shù)都有可能傳過(guò)來(lái),假如調(diào)用方一下子查幾萬(wàn)個(gè)或者是幾萬(wàn)個(gè)數(shù)據(jù)集,那不是接口都爆了。
所以,必須要做好分批調(diào)用,調(diào)用方分批、分頁(yè)調(diào)用 ,中臺(tái)對(duì)參數(shù)做校驗(yàn)一次只能查詢幾百個(gè),這樣子去規(guī)定,保證接口的可用性。
調(diào)用方的偽代碼如下:
booleanend=true; intpage=1; intpageSize=500; while(end){ //設(shè)置好超時(shí),失敗重試 Datadata=getData(page,pageSize); Mapmap=data.getDataMap(); //data里面的字段hashMore表示查詢下一個(gè)分頁(yè)是否還有數(shù)據(jù) end=Objects.equals(data.getHasMore(),1); page++; }
本地緩存@Cacheable
@Cacheable是springframework下提供的緩存注解類,在spring中定義了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口來(lái)統(tǒng)一實(shí)現(xiàn)cache。
除了@Cacheable用戶緩存數(shù)據(jù),也可以使用@CachePut用于緩存更新,這兩個(gè)是比較常用的。
他們緩存的數(shù)據(jù)也是緩存在本地和定時(shí)任務(wù)一樣,除了使用@Cacheable還可使用谷歌研發(fā)的cache工具類LoadingCache ,他也是本地緩存的一種,并且可以設(shè)置緩存的大小,重新刷新的時(shí)間。
相對(duì)比Cacheable會(huì)更加方便,因?yàn)槟惆l(fā)現(xiàn)Cacheable還缺少緩存時(shí)間和緩存更新的屬性配置實(shí)現(xiàn),可能還需要自己再二級(jí)開發(fā),比如加入緩存失效時(shí)間、多少秒后自動(dòng)更新更新緩存,這樣Cacheable才能更加完善。
privatefinalLoadingCache>tagCache=CacheBuilder.newBuilder() .concurrencyLevel(4) .expireAfterWrite(5,TimeUnit.SECONDS) .initialCapacity(8) .maximumSize(10000) .build(newCacheLoader >(){ @Override publicList load(StringcacheKey){ returnget(cacheKey); } @Override publicListenableFuture >reload(Stringkey,List
oldValue){ ListenableFutureTask >task=ListenableFutureTask.create(()->get(key)); executorService.execute(task); returntask; } } );
相比@Cacheable就是代碼比較冗余,注解方式會(huì)更加直觀簡(jiǎn)潔,不過(guò)LoadingCache的靈活性更高。
我們自己對(duì)Cacheable進(jìn)行了擴(kuò)展,加入了實(shí)效時(shí)間以及自動(dòng)更新的方案,這樣的Cacheable更加適用于我們的業(yè)務(wù)。
總結(jié)
在項(xiàng)目中可能多種緩存并行使用,使用不同的緩存都要基于業(yè)務(wù)進(jìn)行考量,包括成本,數(shù)據(jù)一致性,性能問(wèn)題等,不同的緩存方式有不同的特點(diǎn)。
redis緩存分布式系統(tǒng)中共享數(shù)據(jù),性能高效,擴(kuò)展性強(qiáng),redis可以基于數(shù)據(jù)分片、哨兵模式進(jìn)行擴(kuò)展,但是要額外的費(fèi)用進(jìn)行運(yùn)維,并且引入第三方中間件,系統(tǒng)的復(fù)雜度也高,排查困難,而且每次都要經(jīng)過(guò)網(wǎng)絡(luò)調(diào)用,有可能存在網(wǎng)絡(luò)超時(shí)的現(xiàn)象,數(shù)據(jù)丟失,所以要做好數(shù)據(jù)兼容,兜底方案。
本地緩存使用簡(jiǎn)單方便,低成本,每個(gè)服務(wù)實(shí)例都會(huì)冗余一份數(shù)據(jù),一致性問(wèn)題加大,但是效率非常高效,不用通過(guò)網(wǎng)絡(luò)傳輸獲取數(shù)據(jù)。
一般我們的項(xiàng)目中都會(huì)分配6-8G的內(nèi)存,所以一般本地緩存都?jí)蚴褂玫?,所以一般能用本地緩存的話,都可以?yōu)先使用本地緩存。
一些場(chǎng)景不得不使用分布式緩存的,就是用Redis緩存來(lái)共享數(shù)據(jù),綜合使用不同的緩存來(lái)解決項(xiàng)目中的問(wèn)題。
從上面的幾種緩存方案中可以看到重試方案,重試是解決很多問(wèn)題的重要手段之一,但是重試次數(shù),重試的超時(shí)時(shí)間也要控制,防止資源耗盡,在大多數(shù)場(chǎng)景下,重試都可以解決,要是重試次數(shù)達(dá)到限制都不成功,就有可能是網(wǎng)絡(luò)故障或者接口問(wèn)題,此時(shí)就需要應(yīng)用發(fā)送告警通知開發(fā)人員進(jìn)行排查,這是兜底方案。
客戶端緩存和CDN緩存這兩個(gè)對(duì)于服務(wù)端來(lái)說(shuō),比較少使用,一般公司都是用不到,大家可以把關(guān)注點(diǎn)放在服務(wù)端緩存中。
審核編輯:劉清
-
過(guò)濾器
+關(guān)注
關(guān)注
1文章
433瀏覽量
19760 -
Redis
+關(guān)注
關(guān)注
0文章
379瀏覽量
10967
原文標(biāo)題:大型分布式系統(tǒng)中,緩存就該這么玩
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
分布式軟件系統(tǒng)
基于Java的分布式緩存優(yōu)化在網(wǎng)絡(luò)管理系統(tǒng)中的應(yīng)用
淺談分布式緩存技術(shù)
分布式系統(tǒng)的優(yōu)勢(shì)是什么?
來(lái)聊一聊Altium中Fill,Polygon Pour,Plane的區(qū)別和用法
聊一聊stm32的低功耗調(diào)試
分布式KVM系統(tǒng)解決方案,坐席協(xié)作管理有什么用怎么用哪里用
分布式系統(tǒng)中的數(shù)據(jù)庫(kù)和緩存操作順序
![<b class='flag-5'>分布式</b><b class='flag-5'>系統(tǒng)</b><b class='flag-5'>中</b>的數(shù)據(jù)庫(kù)和<b class='flag-5'>緩存</b>操作順序](https://file.elecfans.com/web1/M00/BA/81/pIYBAF6WquSAZFrLAAGHDrJQjbY369.png)
聊一聊分布式系統(tǒng)的CAP理論
![<b class='flag-5'>聊</b><b class='flag-5'>一</b><b class='flag-5'>聊</b><b class='flag-5'>分布式</b><b class='flag-5'>系統(tǒng)</b>的CAP理論](https://file.elecfans.com/web1/M00/BA/89/pIYBAF6W2qaAaeSQAAHJ1jTI_kU653.png)
為什么需要分布式鎖 基于Zookeeper鎖安全嗎
分布式系統(tǒng)架構(gòu)設(shè)計(jì)中異地多活是什么
![<b class='flag-5'>分布式</b><b class='flag-5'>系統(tǒng)</b>架構(gòu)設(shè)計(jì)<b class='flag-5'>中</b>異地多活是什么](https://file.elecfans.com/web2/M00/1C/CF/poYBAGGNyDCAJAk0AAAtznymWpE660.png)
【職場(chǎng)雜談】與嵌入式物聯(lián)網(wǎng)架構(gòu)師聊一聊幾個(gè)話題
![【職場(chǎng)雜談】與嵌入<b class='flag-5'>式</b>物聯(lián)網(wǎng)架構(gòu)師<b class='flag-5'>聊</b><b class='flag-5'>一</b><b class='flag-5'>聊</b>幾個(gè)話題](https://file.elecfans.com//web2/M00/63/70/poYBAGMDHVuAEDY0AADB5DUW7Js794.jpg)
評(píng)論