對于高并發(fā)架構(gòu),毫無疑問緩存是最重要的一環(huán),對于大量的高并發(fā),可以采用三層緩存架構(gòu)來實(shí)現(xiàn),nginx+redis+ehcache
對于中間件nginx常用來做流量的分發(fā),同時(shí)nginx本身也有自己的緩存(容量有限),我們可以用來緩存熱點(diǎn)數(shù)據(jù),讓用戶的請求直接走緩存并返回,減少流向服務(wù)器的流量
模板引擎
通常我們可以配合使用freemaker/velocity等模板引擎來抗住大量的請求
雙層nginx來提升緩存命中率
對于部署多個(gè)nginx而言,如果不加入一些數(shù)據(jù)的路由策略,那么可能導(dǎo)致每個(gè)nginx的緩存命中率很低。因此可以部署雙層nginx
用戶的請求,在nginx沒有緩存相應(yīng)的數(shù)據(jù),那么會(huì)進(jìn)入到redis緩存中,redis可以做到全量數(shù)據(jù)的緩存,通過水平擴(kuò)展能夠提升并發(fā)、高可用的能力
持久化機(jī)制:將redis內(nèi)存中的數(shù)據(jù)持久化到磁盤中,然后可以定期將磁盤文件上傳至S3(AWS)或者ODPS(阿里云)等一些云存儲(chǔ)服務(wù)上去。
如果同時(shí)使用RDB和AOF兩種持久化機(jī)制,那么在redis重啟的時(shí)候,會(huì)使用AOF來重新構(gòu)建數(shù)據(jù),因?yàn)锳OF中的數(shù)據(jù)更加完整,建議將兩種持久化機(jī)制都開啟,用AO F來保證數(shù)據(jù)不丟失,作為數(shù)據(jù)恢復(fù)的第一選擇;用RDB來作不同程度的冷備,在AOF文件都丟失或損壞不可用的時(shí)候來快速進(jìn)行數(shù)據(jù)的恢復(fù)。
實(shí)戰(zhàn)踩坑:對于想從RDB恢復(fù)數(shù)據(jù),同時(shí)AOF開關(guān)也是打開的,一直無法正?;謴?fù),因?yàn)槊看味紩?huì)優(yōu)先從AOF獲取數(shù)據(jù)(如果臨時(shí)關(guān)閉AOF,就可以正?;謴?fù))。此時(shí)首先停止redis,然后關(guān)閉AOF,拷貝RDB到相應(yīng)目錄,啟動(dòng)redis之后熱修改配置參數(shù)redis config set appendonly yes,此時(shí)會(huì)自動(dòng)生成一個(gè)當(dāng)前內(nèi)存數(shù)據(jù)的AOF文件,然后再次停止redis,打開AOF配置,再次啟動(dòng)數(shù)據(jù)就正常啟動(dòng)
redis集群
tomcat jvm堆內(nèi)存緩存,主要是抗redis出現(xiàn)大規(guī)模災(zāi)難。如果redis出現(xiàn)了大規(guī)模的宕機(jī),導(dǎo)致nginx大量流量直接涌入數(shù)據(jù)生產(chǎn)服務(wù),那么最后的tomcat堆內(nèi)存緩存也可以處理部分請求,避免所有請求都直接流向DB
最初級(jí)的緩存不一致問題以及解決方案
問題:如果先修改數(shù)據(jù)庫再刪除緩存,那么當(dāng)緩存刪除失敗來,那么會(huì)導(dǎo)致數(shù)據(jù)庫中是最新數(shù)據(jù),緩存中依舊是舊數(shù)據(jù),造成數(shù)據(jù)不一致。
解決方案:可以先刪除緩存,再修改數(shù)據(jù)庫,如果刪除緩存成功但是數(shù)據(jù)庫修改失敗,那么數(shù)據(jù)庫中是舊數(shù)據(jù),緩存是空不會(huì)出現(xiàn)不一致
比較復(fù)雜的數(shù)據(jù)不一致問題分析
問題:對于數(shù)據(jù)發(fā)生來變更,先刪除緩存,然后去修改數(shù)據(jù)庫,此時(shí)數(shù)據(jù)庫中的數(shù)據(jù)還沒有修改成功,并發(fā)的讀請求到來去讀緩存發(fā)現(xiàn)是空,進(jìn)而去數(shù)據(jù)庫查詢到此時(shí)的舊數(shù)據(jù)放到緩存中,然后之前對數(shù)據(jù)庫數(shù)據(jù)的修改成功來,就會(huì)造成數(shù)據(jù)不一致
解決方案:將數(shù)據(jù)庫與緩存更新與讀取操作進(jìn)行異步串行化。當(dāng)更新數(shù)據(jù)的時(shí)候,根據(jù)數(shù)據(jù)的唯一標(biāo)識(shí),將更新數(shù)據(jù)操作路由到一個(gè)jvm內(nèi)部的隊(duì)列中,一個(gè)隊(duì)列對應(yīng)一個(gè)工作線程,線程串行拿到隊(duì)列中的操作一條一條地執(zhí)行。當(dāng)執(zhí)行隊(duì)列中的更新數(shù)據(jù)操作,刪除緩存,然后去更新數(shù)據(jù)庫,此時(shí)還沒有完成更新的時(shí)候過來一個(gè)讀請求,讀到了空的緩存那么可以先將緩存更新的請求發(fā)送至路由之后的隊(duì)列中,此時(shí)會(huì)在隊(duì)列積壓,然后同步等待緩存更新完成,一個(gè)隊(duì)列中多個(gè)相同數(shù)據(jù)緩存更新請求串在一起是沒有意義的,因此可以做過濾處理。等待前面的更新數(shù)據(jù)操作完成數(shù)據(jù)庫操作之后,才會(huì)去執(zhí)行下一個(gè)緩存更新的操作,此時(shí)會(huì)從數(shù)據(jù)庫中讀取最新的數(shù)據(jù),然后寫入緩存中,如果請求還在等待時(shí)間范圍內(nèi),不斷輪詢發(fā)現(xiàn)可以取到緩存中值就可以直接返回(此時(shí)可能會(huì)有對這個(gè)緩存數(shù)據(jù)的多個(gè)請求正在這樣處理);如果請求等待事件超過一定時(shí)長,那么這一次的請求直接讀取數(shù)據(jù)庫中的舊值
對于這種處理方式需要注意一些問題:
對于緩存生產(chǎn)服務(wù),可能部署在多臺(tái)機(jī)器,當(dāng)redis和ehcache對應(yīng)的緩存數(shù)據(jù)都過期不存在時(shí),此時(shí)可能nginx過來的請求和kafka監(jiān)聽的請求同時(shí)到達(dá),導(dǎo)致兩者最終都去拉取數(shù)據(jù)并且存入redis中,因此可能產(chǎn)生并發(fā)沖突的問題,可以采用redis或者zookeeper類似的分布式鎖來解決,讓請求的被動(dòng)緩存重建與監(jiān)聽主動(dòng)的緩存重建操作避免并發(fā)的沖突,當(dāng)存入緩存的時(shí)候通過對比時(shí)間字段廢棄掉舊的數(shù)據(jù),保存最新的數(shù)據(jù)到緩存
當(dāng)系統(tǒng)第一次啟動(dòng),大量請求涌入,此時(shí)的緩存為空,可能會(huì)導(dǎo)致DB崩潰,進(jìn)而讓系統(tǒng)不可用,同樣當(dāng)redis所有緩存數(shù)據(jù)異常丟失,也會(huì)導(dǎo)致該問題。因此,可以提前放入數(shù)據(jù)到redis避免上述冷啟動(dòng)的問題,當(dāng)然也不可能是全量數(shù)據(jù),可以根據(jù)類似于當(dāng)天的具體訪問情況,實(shí)時(shí)統(tǒng)計(jì)出訪問頻率較高的熱數(shù)據(jù),這里熱數(shù)據(jù)也比較多,需要多個(gè)服務(wù)并行的分布式去讀寫到redis中(所以要基于zk分布式鎖)
通過nginx+lua將訪問流量上報(bào)至kafka中,storm從kafka中消費(fèi)數(shù)據(jù),實(shí)時(shí)統(tǒng)計(jì)處每個(gè)商品的訪問次數(shù),訪問次數(shù)基于LRU(apache commons collections LRUMap)內(nèi)存數(shù)據(jù)結(jié)構(gòu)的存儲(chǔ)方案,使用LRUMap去存放是因?yàn)閮?nèi)存中的性能高,沒有外部依賴,每個(gè)storm task啟動(dòng)的時(shí)候基于zk分布式鎖將自己的id寫入zk同一個(gè)節(jié)點(diǎn)中,每個(gè)storm task負(fù)責(zé)完成自己這里的熱數(shù)據(jù)的統(tǒng)計(jì),每隔一段時(shí)間就遍歷一下這個(gè)map,然后維護(hù)一個(gè)前1000的數(shù)據(jù)list,然后去更新這個(gè)list,最后開啟一個(gè)后臺(tái)線程,每隔一段時(shí)間比如一分鐘都將排名的前1000的熱數(shù)據(jù)list同步到zk中去,存儲(chǔ)到這個(gè)storm task對應(yīng)的一個(gè)znode中去
部署多個(gè)實(shí)例的服務(wù),每次啟動(dòng)的時(shí)候就會(huì)去拿到上述維護(hù)的storm task id列表的節(jié)點(diǎn)數(shù)據(jù),然后根據(jù)taskid,一個(gè)一個(gè)去嘗試獲取taskid對應(yīng)的znode的zk分布式鎖,如果能夠獲取到分布式鎖,再去獲取taskid status的鎖進(jìn)而查詢預(yù)熱狀態(tài),如果沒有被預(yù)熱過,那么就將這個(gè)taskid對應(yīng)的熱數(shù)據(jù)list取出來,從而從DB中查詢出來寫入緩存中,如果taskid分布式鎖獲取失敗,快速拋錯(cuò)進(jìn)行下一次循環(huán)獲取下一個(gè)taskid的分布式鎖即可,此時(shí)就是多個(gè)服務(wù)實(shí)例基于zk分布式鎖做協(xié)調(diào)并行的進(jìn)行緩存的預(yù)熱
對于瞬間大量的相同數(shù)據(jù)的請求涌入,可能導(dǎo)致該數(shù)據(jù)經(jīng)過hash策略之后對應(yīng)的應(yīng)用層nginx被壓垮,如果請求繼續(xù)就會(huì)影響至其他的nginx,最終導(dǎo)致所有nginx出現(xiàn)異常整個(gè)系統(tǒng)變得不可用。
基于nginx+lua+storm的熱點(diǎn)緩存的流量分發(fā)策略自動(dòng)降級(jí)來解決上述問題的出現(xiàn),可以設(shè)定訪問次數(shù)大于后95%平均值n倍的數(shù)據(jù)為熱點(diǎn),在storm中直接發(fā)送http請求到流量分發(fā)的nginx上去,使其存入本地緩存,然后storm還會(huì)將熱點(diǎn)對應(yīng)的完整緩存數(shù)據(jù)沒發(fā)送到所有的應(yīng)用nginx服務(wù)器上去,并直接存放到本地緩存。對于流量分發(fā)nginx,訪問對應(yīng)的數(shù)據(jù),如果發(fā)現(xiàn)是熱點(diǎn)標(biāo)識(shí)就立即做流量分發(fā)策略的降級(jí),對同一個(gè)數(shù)據(jù)的訪問從hash到一臺(tái)應(yīng)用層nginx降級(jí)成為分發(fā)至所有的應(yīng)用層nginx。storm需要保存上一次識(shí)別出來的熱點(diǎn)List,并同當(dāng)前計(jì)算出來的熱點(diǎn)list做對比,如果已經(jīng)不是熱點(diǎn)數(shù)據(jù),則發(fā)送對應(yīng)的http請求至流量分發(fā)nginx中來取消對應(yīng)數(shù)據(jù)的熱點(diǎn)標(biāo)識(shí)
redis集群徹底崩潰,緩存服務(wù)大量對redis的請求等待,占用資源,隨后緩存服務(wù)大量的請求進(jìn)入源頭服務(wù)去查詢DB,使DB壓力過大崩潰,此時(shí)對源頭服務(wù)的請求也大量等待占用資源,緩存服務(wù)大量的資源全部耗費(fèi)在訪問redis和源服務(wù)無果,最后使自身無法提供服務(wù),最終會(huì)導(dǎo)致整個(gè)網(wǎng)站崩潰。
事前的解決方案,搭建一套高可用架構(gòu)的redis cluster集群,主從架構(gòu)、一主多從,一旦主節(jié)點(diǎn)宕機(jī),從節(jié)點(diǎn)自動(dòng)跟上,并且最好使用雙機(jī)房部署集群。
事中的解決方案,部署一層ehcache緩存,在redis全部實(shí)現(xiàn)情況下能夠抗住部分壓力;對redis cluster的訪問做資源隔離,避免所有資源都等待,對redis cluster的訪問失敗時(shí)的情況去部署對應(yīng)的熔斷策略,部署redis cluster的降級(jí)策略;對源服務(wù)訪問的限流以及資源隔離
事后的解決方案:redis數(shù)據(jù)做了備份可以直接恢復(fù),重啟redis即可;redis數(shù)據(jù)徹底失敗來或者數(shù)據(jù)過舊,可以快速緩存預(yù)熱,然后讓redis重新啟動(dòng)。然后由于資源隔離的half-open策略發(fā)現(xiàn)redis已經(jīng)能夠正常訪問,那么所有的請求將自動(dòng)恢復(fù)
對于在多級(jí)緩存中都沒有對應(yīng)的數(shù)據(jù),并且DB也沒有查詢到數(shù)據(jù),此時(shí)大量的請求都會(huì)直接到達(dá)DB,導(dǎo)致DB承載高并發(fā)的問題。解決緩存穿透的問題可以對DB也沒有的數(shù)據(jù)返回一個(gè)空標(biāo)識(shí)的數(shù)據(jù),進(jìn)而保存到各級(jí)緩存中,因?yàn)橛袑?shù)據(jù)修改的異步監(jiān)聽,所以當(dāng)數(shù)據(jù)有更新,新的數(shù)據(jù)會(huì)被更新到緩存匯中。
可以在nginx本地,設(shè)置緩存數(shù)據(jù)的時(shí)候隨機(jī)緩存的有效期,避免同一時(shí)刻緩存都失效而大量請求直接進(jìn)入redis
聯(lián)系客服