凌晨三點(diǎn)半了,太困了,還差一些,明天補(bǔ)上…
因?yàn)樽约鹤罱龅捻?xiàng)目涉及到了緩存,所以水一篇緩存相關(guān)的文章,供大家作為參考,若發(fā)現(xiàn)文章有紕漏,希望大家多指正。
緩存涉及到的范圍頗廣,從CPU緩存,到進(jìn)程內(nèi)緩存,到進(jìn)程外緩存。再加上已經(jīng)凌晨一點(diǎn)了,我得保住我的幾絲殘發(fā),本文不會(huì)將每一處的細(xì)枝末節(jié)都寫到,見諒。
關(guān)于CPU緩存
這里提一句CPU緩存,因?yàn)榫彺娴暮诵乃枷攵际悄屈c(diǎn)事,命中、淘汰、一致性等。
以前著重寫過CPU的一些東西,這里只附一張圖。
ps:聽說最近有哪個(gè)廠商的CPU把三級(jí)緩存架構(gòu)和總線鎖改了,有相關(guān)資源的小伙伴快發(fā)給我,我觀摩一下,hhh~
關(guān)于多級(jí)緩存
本文重點(diǎn)不在多級(jí)緩存,因?yàn)橐郧拔乙矊iT寫過一篇關(guān)于多級(jí)緩存的詳細(xì)設(shè)計(jì)。
簡要步驟:
- 瀏覽器緩存
- Nginx反向代理,負(fù)載OpenResty集群
- OpenResty基于Nginx和Lua,可實(shí)現(xiàn)Lua業(yè)務(wù)編碼,緩存性能很好,京東技術(shù)做過壓測(cè)對(duì)比。
- 如果OpenResty緩存未命中,則查詢Redis
- 若Redis緩存未命中,則查詢進(jìn)程緩存
- 為了保證緩存和DB的數(shù)據(jù)一致性,還可以用Canal和DTS做數(shù)據(jù)同步(基于Mysql的Binlog,和主從一個(gè)原理,偽裝成slave)
關(guān)于二級(jí)緩存
二級(jí)緩存最佳實(shí)踐:Caffeine + Redis
- 先走Caffeine,如果未命中,走Redis
- 為了保證數(shù)據(jù)一致性,可以用Canal / DTS做數(shù)據(jù)同步
- 進(jìn)程緩存Caffeine的話,設(shè)置個(gè)定時(shí)同步就可以了
性能優(yōu)化:
- 進(jìn)程緩存應(yīng)用Caffeine是因?yàn)槠涞讓覥oncurrentHashMap的結(jié)構(gòu),支持并發(fā)(后面會(huì)出各個(gè)進(jìn)程緩存性能對(duì)比報(bào)告)
- 進(jìn)程外緩存,我通常會(huì)無腦選Redis,基于其容錯(cuò)性,多數(shù)據(jù)結(jié)構(gòu)等。(后面會(huì)出和memcache等對(duì)比分析)
市面上也有二級(jí)緩存框架,比如J2Cache,該框架本身并沒有做額外工作,主要是集成了常見的進(jìn)程內(nèi)緩存和進(jìn)程外緩存。
如果基于Spring開發(fā),基于AOP設(shè)計(jì)的Spring Cache框架適配常用的緩存,自身的注解和策略天然和業(yè)務(wù)解耦,很不錯(cuò),但是,如何集成Redis,這里需要特別注意?。?!
因?yàn)榧蒖edis時(shí),Spring Cache的清除策略,在從Redis中刪除緩存時(shí)使用的是 keys指令,keys指令時(shí)間復(fù)雜度是O(N),如果緩存數(shù)量較大會(huì)產(chǎn)生明顯的阻,因此在生產(chǎn)環(huán)境中Redis會(huì)禁用這個(gè)指令,導(dǎo)致報(bào)錯(cuò)。
//keys 指令
byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet())
.toArray(new byte[0][]);
if (keys.length > 0) {
statistics.incDeletesBy(name, keys.length);
connection.del(keys);
}
所以,我們可以重寫DefaultRedisCacheWriter(spring cache提供的默認(rèn)的Redis緩存寫出器,其內(nèi)部封裝了緩存增刪改查等邏輯)
使用scan命令代替keys命令
//使用scan命令代替keys命令
Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match(new String(pattern)).count(1000).build());
Set<byte[]> byteSet = new HashSet<>();
while (cursor.hasNext()) {
byteSet.add(cursor.next());
}
byte[][] keys = byteSet.toArray(new byte[0][]);
講真的,多級(jí)緩存和二級(jí)緩存這東西,不要為了炫技亂用,可能會(huì)增加沒必要的開發(fā)成本和未知問題,而且還要做好數(shù)據(jù)量的評(píng)估,別搞了緩存,造成雪崩,那就真的血本無歸了。
至理名言:不結(jié)合業(yè)務(wù)的技術(shù)都是耍流氓。
進(jìn)程內(nèi)緩存
進(jìn)程內(nèi)緩存有什么好處?
與沒有緩存相比,進(jìn)程內(nèi)緩存的好處是,數(shù)據(jù)讀取不再需要訪問后端,例如數(shù)據(jù)庫。
與進(jìn)程外緩存相比(例如redis/memcache),進(jìn)程內(nèi)緩存省去了網(wǎng)絡(luò)開銷,所以一來節(jié)省了內(nèi)網(wǎng)帶寬,二來響應(yīng)時(shí)延會(huì)更低。
進(jìn)程內(nèi)緩存有什么缺點(diǎn)?
如果數(shù)據(jù)緩存在站點(diǎn)和服務(wù)的多個(gè)節(jié)點(diǎn)內(nèi),數(shù)據(jù)存了多份,一致性比較難保障。
如何保證進(jìn)程內(nèi)緩存的數(shù)據(jù)一致性?
- 可以通過單節(jié)點(diǎn)通知其他節(jié)點(diǎn)。
- 可以通過MQ通知其他節(jié)點(diǎn)。
- 為了避免耦合,降低復(fù)雜性,干脆放棄了“實(shí)時(shí)一致性”,每個(gè)節(jié)點(diǎn)啟動(dòng)一個(gè)timer,定時(shí)從后端拉取最新的數(shù)據(jù),更新內(nèi)存緩存。在有節(jié)點(diǎn)更新后端數(shù)據(jù),而其他節(jié)點(diǎn)通過timer更新數(shù)據(jù)之間,會(huì)讀到臟數(shù)據(jù)。
為什么不能頻繁使用進(jìn)程內(nèi)緩存?
站點(diǎn)與服務(wù)的進(jìn)程內(nèi)緩存,實(shí)際上違背了分層架構(gòu)設(shè)計(jì)的無狀態(tài)準(zhǔn)則
什么時(shí)候可以使用進(jìn)程內(nèi)緩存?
- 只讀數(shù)據(jù),可以考慮在進(jìn)程啟動(dòng)時(shí)加載到內(nèi)存。(實(shí)現(xiàn)InitializingBean)
- 極其高并發(fā)的,如果透?jìng)骱蠖藟毫O大的場(chǎng)景,可以考慮使用進(jìn)程內(nèi)緩存。(秒殺)
- 一定程度上允許數(shù)據(jù)不一致業(yè)務(wù)。
服務(wù)之間通過緩存?zhèn)鬟f數(shù)據(jù)的錯(cuò)誤性
- 數(shù)據(jù)管道場(chǎng)景,MQ比cache更合適;
- 多個(gè)服務(wù)不應(yīng)該公用一個(gè)cache實(shí)例,應(yīng)該垂直拆分解耦;
- 服務(wù)化架構(gòu),不應(yīng)該繞過service讀取其后端的cache/db,而應(yīng)該通過RPC接口訪問。
使用緩存未考慮雪崩的錯(cuò)誤性
如果緩存掛掉,所有的請(qǐng)求會(huì)壓到數(shù)據(jù)庫,如果未提前做容量預(yù)估,可能會(huì)把數(shù)據(jù)庫壓垮(在緩存恢復(fù)之前,數(shù)據(jù)庫可能一直都起不來),導(dǎo)致系統(tǒng)整體不可服務(wù)。
應(yīng)提前做容量預(yù)估,如果緩存掛掉,數(shù)據(jù)庫仍能扛住,才能執(zhí)行上述方案。
否則,就要進(jìn)一步設(shè)計(jì):
使用高可用緩存集群(例如主備),一個(gè)緩存實(shí)例掛掉后,能夠自動(dòng)做故障轉(zhuǎn)移。
使用緩存水平切分,一個(gè)緩存實(shí)例掛掉后,不至于所有的流量都?jí)旱綌?shù)據(jù)庫上。
多服務(wù)共用緩存實(shí)例的錯(cuò)誤性
- 可能導(dǎo)致key沖突,彼此沖掉對(duì)方的數(shù)據(jù);(可做namespace:key的方式來做key,隔離)
- 不同服務(wù)對(duì)應(yīng)的數(shù)據(jù)量,吞吐量不一樣,共用一個(gè)實(shí)例容易導(dǎo)致一個(gè)服務(wù)把另一個(gè)服務(wù)的熱數(shù)據(jù)擠出去;
- 共用一個(gè)實(shí)例,會(huì)導(dǎo)致服務(wù)之間的耦合,與微服務(wù)架構(gòu)的“數(shù)據(jù)庫,緩存私有”的設(shè)計(jì)原則是相悖的;
例如,我做過的一個(gè)單體架構(gòu)項(xiàng)目,緩存用Caffeine,每個(gè)業(yè)務(wù)都會(huì)有一個(gè)Caffeine實(shí)例。
緩存與數(shù)據(jù)庫不一致的解決方案
- 主從同步;
- 通過工具(DTS/cannal)訂閱從庫的binlog,這里能夠最準(zhǔn)確的知道,從庫數(shù)據(jù)同步完成的時(shí)間;
- 從庫執(zhí)行完寫操作,向緩存再次發(fā)起刪除,淘汰這段時(shí)間內(nèi)可能寫入緩存的舊數(shù)據(jù);
先操作緩存,還是數(shù)據(jù)庫
- 讀請(qǐng)求,先讀緩存,如果沒有命中,讀數(shù)據(jù)庫,再set回緩存
- 寫請(qǐng)求
- 先緩存,再數(shù)據(jù)庫
- 緩存,使用delete,而不是set
Cache Aside Pattern方案
對(duì)于讀請(qǐng)求:
(1)先讀cache,再讀db;
(2)如果,cache hit,則直接返回?cái)?shù)據(jù);
(3)如果,cache miss,則訪問db,并將數(shù)據(jù)set回緩存;
對(duì)于寫請(qǐng)求:
(1)淘汰緩存,而不是更新緩存;
(2)先操作數(shù)據(jù)庫,再淘汰緩存;
緩存為什么總是淘汰,不是修改
修改成本太大了,無腦選淘汰,問題不大
緩存相關(guān)的清除策略
FIFO(first in first out)
先進(jìn)先出策略,最先進(jìn)入緩存的數(shù)據(jù)在緩存空間不夠的情況下(超出最大元素限制)會(huì)被優(yōu)先被清除掉,以騰出新的空間接受新的數(shù)據(jù)。策略算法主要比較緩存元素的創(chuàng)建時(shí)間。在數(shù)據(jù)實(shí)效性要求場(chǎng)景下可選擇該類策略,優(yōu)先保障最新數(shù)據(jù)可用。
LFU(less frequently used)
最少使用策略,無論是否過期,根據(jù)元素的被使用次數(shù)判斷,清除使用次數(shù)較少的元素釋放空間。策略算法主要比較元素的hitCount(命中次數(shù))。在保證高頻數(shù)據(jù)有效性場(chǎng)景下,可選擇這類策略。
LRU(least recently used)
最近最少使用策略,無論是否過期,根據(jù)元素最后一次被使用的時(shí)間戳,清除最遠(yuǎn)使用時(shí)間戳的元素釋放空間。策略算法主要比較元素最近一次被get使用時(shí)間。在熱點(diǎn)數(shù)據(jù)場(chǎng)景下較適用,優(yōu)先保證熱點(diǎn)數(shù)據(jù)的有效性。
除此之外,還有一些簡單策略比如:
根據(jù)過期時(shí)間判斷,清理過期時(shí)間最長的元素;
根據(jù)過期時(shí)間判斷,清理最近要過期的元素;
隨機(jī)清理;
根據(jù)關(guān)鍵字(或元素內(nèi)容)長短清理等。
為什么選Caffeine
底層數(shù)據(jù)結(jié)構(gòu),W-TinyLFU算法,當(dāng)然還有權(quán)威給出個(gè)各個(gè)組件性能對(duì)比圖,誰不愿意用好的呢,對(duì)吧。(關(guān)于Caffeine源碼,改天單寫一篇)文章來源:http://www.zghlxwxcb.cn/news/detail-465388.html
為什么選Redis
沒有為什么,無腦選就完了,下周我寫一篇Redis7的源碼文章,你就懂了。文章來源地址http://www.zghlxwxcb.cn/news/detail-465388.html
Redis最佳應(yīng)用實(shí)踐
- 在主頁中顯示最新的項(xiàng)目列表:Redis使用的是常駐內(nèi)存的緩存,速度非???。LPUSH用來插入一個(gè)內(nèi)容ID,作為關(guān)鍵字存儲(chǔ)在列表頭部。LTRIM用來限制列表中的項(xiàng)目數(shù)最多為5000。如果用戶需要的檢索的數(shù)據(jù)量超越這個(gè)緩存容量,這時(shí)才需要把請(qǐng)求發(fā)送到數(shù)據(jù)庫。
- 刪除和過濾:如果一篇文章被刪除,可以使用LREM從緩存中徹底清除掉。
- 排行榜及相關(guān)問題:排行榜(leader board)按照得分進(jìn)行排序。ZADD命令可以直接實(shí)現(xiàn)這個(gè)功能,而ZREVRANGE命令可以用來按照得分來獲取前100名的用戶,ZRANK可以用來獲取用戶排名,非常直接而且操作容易。
- 按照用戶投票和時(shí)間排序:排行榜,得分會(huì)隨著時(shí)間變化。LPUSH和LTRIM命令結(jié)合運(yùn)用,把文章添加到一個(gè)列表中。一項(xiàng)后臺(tái)任務(wù)用來獲取列表,并重新計(jì)算列表的排序,ZADD命令用來按照新的順序填充生成列表。列表可以實(shí)現(xiàn)非常快速的檢索,即使是負(fù)載很重的站點(diǎn)。
- 過期項(xiàng)目處理:使用Unix時(shí)間作為關(guān)鍵字,用來保持列表能夠按時(shí)間排序。對(duì)current_time和time_to_live進(jìn)行檢索,完成查找過期項(xiàng)目的艱巨任務(wù)。另一項(xiàng)后臺(tái)任務(wù)使用ZRANGE…WITHSCORES進(jìn)行查詢,刪除過期的條目。
- 計(jì)數(shù):進(jìn)行各種數(shù)據(jù)統(tǒng)計(jì)的用途是非常廣泛的,比如想知道什么時(shí)候封鎖一個(gè)IP地址。INCRBY命令讓這些變得很容易,通過原子遞增保持計(jì)數(shù);GETSET用來重置計(jì)數(shù)器;過期屬性用來確認(rèn)一個(gè)關(guān)鍵字什么時(shí)候應(yīng)該刪除。
- 特定時(shí)間內(nèi)的特定項(xiàng)目:這是特定訪問者的問題,可以通過給每次頁面瀏覽使用SADD命令來解決。SADD不會(huì)將已經(jīng)存在的成員添加到一個(gè)集合。
- Pub/Sub:在更新中保持用戶對(duì)數(shù)據(jù)的映射是系統(tǒng)中的一個(gè)普遍任務(wù)。Redis的pub/sub功能使用了SUBSCRIBE、UNSUBSCRIBE和PUBLISH命令,讓這個(gè)變得更加容易。
- 隊(duì)列:在當(dāng)前的編程中隊(duì)列隨處可見。除了push和pop類型的命令之外,Redis還有阻塞隊(duì)列的命令,能夠讓一個(gè)程序在執(zhí)行時(shí)被另一個(gè)程序添加到隊(duì)列。
到了這里,關(guān)于【技術(shù)解決方案】(多級(jí))緩存架構(gòu)最佳實(shí)踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!