摘要: 對于高并發(fā)架構(gòu),,毫無疑問緩存是最重要的一環(huán),對于大量的高并發(fā),,可以采用三層緩存架構(gòu)來實現(xiàn),,nginx+redis+ehcache Nginx 對于中間件nginx常用來做流量的分發(fā),同時nginx本身也有自己的緩存(容量有限),,我們可以用來緩存熱點數(shù)據(jù),,讓用戶的請求直接走緩存并返回,減少流向服務(wù)器的流量 一,、模板引擎 通常我們可以配合使用freemaker/velocity等模板引擎來抗住大量的請求 小型系統(tǒng)可能直接在服務(wù)器端渲染出所有的頁面并放入緩存,,之后的相同頁面請求就可以直接返回,不用去查詢數(shù)據(jù)源或者做數(shù)據(jù)邏輯處理 對于頁面非常之多的系統(tǒng),,當模板有改變,,上述方法就需要重新渲染所有的頁面模板,毫無疑問是不可取的,。因此配合nginx+lua(OpenResty),,將模板單獨保存在nginx緩存中,同時對于用來渲染的數(shù)據(jù)也存在nginx緩存中,,但是需要設(shè)置一個緩存過期的時間,,以盡可能保證模板的實時性 二、雙層nginx來提升緩存命中率 對于部署多個nginx而言,,如果不加入一些數(shù)據(jù)的路由策略,,那么可能導致每個nginx的緩存命中率很低,。因此可以部署雙層nginx 分發(fā)層nginx負責流量分發(fā)的邏輯和策略,根據(jù)自己定義的一些規(guī)則,,比如根據(jù)productId進行hash,,然后對后端nginx數(shù)量取模將某一個商品的訪問請求固定路由到一個nginx后端服務(wù)器上去 后端nginx用來緩存一些熱點數(shù)據(jù)到自己的緩存區(qū)(分發(fā)層只能配置1個嗎) Redis 用戶的請求,在nginx沒有緩存相應(yīng)的數(shù)據(jù),,那么會進入到redis緩存中,,redis可以做到全量數(shù)據(jù)的緩存,通過水平擴展能夠提升并發(fā),、高可用的能力 一,、持久化機制 持久化機制:將redis內(nèi)存中的數(shù)據(jù)持久化到磁盤中,然后可以定期將磁盤文件上傳至S3(AWS)或者ODPS(阿里云)等一些云存儲服務(wù)上去,。 如果同時使用RDB和AOF兩種持久化機制,,那么在redis重啟的時候,會使用AOF來重新構(gòu)建數(shù)據(jù),,因為AOF中的數(shù)據(jù)更加完整,,建議將兩種持久化機制都開啟,用AO F來保證數(shù)據(jù)不丟失,,作為數(shù)據(jù)恢復(fù)的第一選擇,;用RDB來作不同程度的冷備,在AOF文件都丟失或損壞不可用的時候來快速進行數(shù)據(jù)的恢復(fù),。 實戰(zhàn)踩坑:對于想從RDB恢復(fù)數(shù)據(jù),,同時AOF開關(guān)也是打開的,一直無法正?;謴?fù),,因為每次都會優(yōu)先從AOF獲取數(shù)據(jù)(如果臨時關(guān)閉AOF,就可以正?;謴?fù)),。此時首先停止redis,然后關(guān)閉AOF,,拷貝RDB到相應(yīng)目錄,,啟動redis之后熱修改配置參數(shù)redis config set appendonly yes,此時會自動生成一個當前內(nèi)存數(shù)據(jù)的AOF文件,,然后再次停止redis,,打開AOF配置,再次啟動數(shù)據(jù)就正常啟動 二,、redis集群 replication 一主多從架構(gòu),主節(jié)點負責寫,,并且將數(shù)據(jù)同步到其他salve節(jié)點(異步執(zhí)行),,從節(jié)點負責讀,,主要就是用來做讀寫分離的橫向擴容架構(gòu)。這種架構(gòu)的master節(jié)點數(shù)據(jù)一定要做持久化,,否則,,當master宕機重啟之后內(nèi)存數(shù)據(jù)清空,那么就會將空數(shù)據(jù)復(fù)制到slave,,導致所有數(shù)據(jù)消失 sentinal哨兵 哨兵是redis集群架構(gòu)中很重要的一個組件,,負責監(jiān)控redis master和slave進程是否正常工作,當某個redis實例故障時,,能夠發(fā)送消息報警通知給管理員,,當master node宕機能夠自動轉(zhuǎn)移到slave node上,如果故障轉(zhuǎn)移發(fā)生來,,會通知client客戶端新的master地址,。sentinal至少需要3個實例來保證自己的健壯性,并且能夠更好地進行quorum投票以達到majority來執(zhí)行故障轉(zhuǎn)移,。 前兩種架構(gòu)方式最大的特點是,,每個節(jié)點的數(shù)據(jù)是相同的,無法存取海量的數(shù)據(jù),。因此哨兵集群的方式使用與數(shù)據(jù)量不大的情況 redis cluster redis cluster支撐多master node,,每個master node可以掛載多個slave node,如果mastre掛掉會自動將對應(yīng)的某個slave切換成master,。需要注意的是redis cluster架構(gòu)下slave節(jié)點主要是用來做高可用,、故障主備切換的,如果一定需要slave能夠提供讀的能力,,修改配置也可以實現(xiàn)(同時也需要修改jedis源碼來支持該情況下的讀寫分離操作),。redis cluster架構(gòu)下,master就是可以任意擴展的,,直接橫向擴展master即可提高讀寫吞吐量,。slave節(jié)點能夠自動遷移(讓master節(jié)點盡量平均擁有slave節(jié)點),對整個架構(gòu)過載冗余的slave就可以保障系統(tǒng)更高的可用性,。 ehcache tomcat jvm堆內(nèi)存緩存,,主要是抗redis出現(xiàn)大規(guī)模災(zāi)難。如果redis出現(xiàn)了大規(guī)模的宕機,,導致nginx大量流量直接涌入數(shù)據(jù)生產(chǎn)服務(wù),,那么最后的tomcat堆內(nèi)存緩存也可以處理部分請求,避免所有請求都直接流向DB 針對上面的技術(shù)我特意整理了一下,,有很多技術(shù)不是靠幾句話能講清楚,,所以干脆找朋友錄制了一些視頻,很多問題其實答案很簡單,,但是背后的思考和邏輯不簡單,,要做到知其然還要知其所以然,。如果想學習Java工程化、高性能及分布式,、深入淺出,。微服務(wù)、Spring,,MyBatis,,Netty源碼分析的朋友可以加我的Java進階群:694549689,群里有阿里大牛直播講解技術(shù),,以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費分享給大家,。 緩存數(shù)據(jù)更新策略 對時效性要求高的緩存數(shù)據(jù),當發(fā)生變更的時候,,直接采取數(shù)據(jù)庫和redis緩存雙寫的方案,,讓緩存時效性最高。 對時效性不高的數(shù)據(jù),,當發(fā)生變更之后,,采取MQ異步通知的方式,通過數(shù)據(jù)生產(chǎn)服務(wù)來監(jiān)聽MQ消息,,然后異步去拉取服務(wù)的數(shù)據(jù)更新tomcat jvm緩存和redis緩存,,對于nginx本地緩存過期之后就可以從redis中拉取新的數(shù)據(jù)并更新到nginx本地。 經(jīng)典的緩存+數(shù)據(jù)庫讀寫的模式,,cache aside pattern 讀的時候,,先讀緩存,緩存沒有的話,,那么就讀數(shù)據(jù)庫,,然后取出數(shù)據(jù)后放入緩存,同時返回響應(yīng) 更新的時候,,先刪除緩存,,然后再更新數(shù)據(jù)庫 之所以更新的時候只是刪除緩存,因為對于一些復(fù)雜有邏輯的緩存數(shù)據(jù),,每次數(shù)據(jù)變更都更新一次緩存會造成額外的負擔,,只是刪除緩存,讓該數(shù)據(jù)下一次被使用的時候再去執(zhí)行讀的操作來重新緩存,,這里采用的是懶加載的策略,。舉個例子,一個緩存涉及的表的字段,,在1分鐘內(nèi)就修改了20次,,或者是100次,那么緩存跟新20次,100次,;但是這個緩存在1分鐘內(nèi)就被讀取了1次,,因此每次更新緩存就會有大量的冷數(shù)據(jù),對于緩存符合28黃金法則,,20%的數(shù)據(jù),占用了80%的訪問量 數(shù)據(jù)庫和redis緩存雙寫不一致的問題 最初級的緩存不一致問題以及解決方案 問題:如果先修改數(shù)據(jù)庫再刪除緩存,,那么當緩存刪除失敗來,,那么會導致數(shù)據(jù)庫中是最新數(shù)據(jù),緩存中依舊是舊數(shù)據(jù),,造成數(shù)據(jù)不一致,。 解決方案:可以先刪除緩存,再修改數(shù)據(jù)庫,,如果刪除緩存成功但是數(shù)據(jù)庫修改失敗,,那么數(shù)據(jù)庫中是舊數(shù)據(jù),緩存是空不會出現(xiàn)不一致 比較復(fù)雜的數(shù)據(jù)不一致問題分析 問題:對于數(shù)據(jù)發(fā)生來變更,,先刪除緩存,,然后去修改數(shù)據(jù)庫,此時數(shù)據(jù)庫中的數(shù)據(jù)還沒有修改成功,,并發(fā)的讀請求到來去讀緩存發(fā)現(xiàn)是空,,進而去數(shù)據(jù)庫查詢到此時的舊數(shù)據(jù)放到緩存中,然后之前對數(shù)據(jù)庫數(shù)據(jù)的修改成功來,,就會造成數(shù)據(jù)不一致 解決方案:將數(shù)據(jù)庫與緩存更新與讀取操作進行異步串行化,。當更新數(shù)據(jù)的時候,根據(jù)數(shù)據(jù)的唯一標識,,將更新數(shù)據(jù)操作路由到一個jvm內(nèi)部的隊列中,,一個隊列對應(yīng)一個工作線程,線程串行拿到隊列中的操作一條一條地執(zhí)行,。當執(zhí)行隊列中的更新數(shù)據(jù)操作,,刪除緩存,然后去更新數(shù)據(jù)庫,,此時還沒有完成更新的時候過來一個讀請求,,讀到了空的緩存那么可以先將緩存更新的請求發(fā)送至路由之后的隊列中,此時會在隊列積壓,,然后同步等待緩存更新完成,,一個隊列中多個相同數(shù)據(jù)緩存更新請求串在一起是沒有意義的,因此可以做過濾處理,。等待前面的更新數(shù)據(jù)操作完成數(shù)據(jù)庫操作之后,,才會去執(zhí)行下一個緩存更新的操作,此時會從數(shù)據(jù)庫中讀取最新的數(shù)據(jù),然后寫入緩存中,,如果請求還在等待時間范圍內(nèi),,不斷輪詢發(fā)現(xiàn)可以取到緩存中值就可以直接返回(此時可能會有對這個緩存數(shù)據(jù)的多個請求正在這樣處理);如果請求等待事件超過一定時長,,那么這一次的請求直接讀取數(shù)據(jù)庫中的舊值 對于這種處理方式需要注意一些問題: 讀請求長時阻塞:由于讀請求進行來非常輕度的異步化,,所以對超時的問題需要格外注意,超過超時時間會直接查詢DB,,處理不好會對DB造成壓力,,因此需要測試系統(tǒng)高峰期QPS來調(diào)整機器數(shù)以及對應(yīng)機器上的隊列數(shù)最終決定合理的請求等待超時時間 多實例部署的請求路由:可能這個服務(wù)會部署多個實例,那么必須保證對應(yīng)的請求都通過nginx服務(wù)器路由到相同的服務(wù)實例上 熱點數(shù)據(jù)的路由導師請求的傾斜:因為只有在商品數(shù)據(jù)更新的時候才會清空緩存,,然后才會導致讀寫并發(fā),,所以更新頻率不是太高的話,這個問題的影響并不是特別大,,但是的確可能某些機器的負載會高一些 分布式緩存重建并發(fā)沖突解決方案 對于緩存生產(chǎn)服務(wù),,可能部署在多臺機器,當redis和ehcache對應(yīng)的緩存數(shù)據(jù)都過期不存在時,,此時可能nginx過來的請求和kafka監(jiān)聽的請求同時到達,,導致兩者最終都去拉取數(shù)據(jù)并且存入redis中,因此可能產(chǎn)生并發(fā)沖突的問題,,可以采用redis或者zookeeper類似的分布式鎖來解決,,讓請求的被動緩存重建與監(jiān)聽主動的緩存重建操作避免并發(fā)的沖突,當存入緩存的時候通過對比時間字段廢棄掉舊的數(shù)據(jù),,保存最新的數(shù)據(jù)到緩存 緩存冷啟動以及緩存預(yù)熱解決方案 當系統(tǒng)第一次啟動,,大量請求涌入,此時的緩存為空,,可能會導致DB崩潰,,進而讓系統(tǒng)不可用,同樣當redis所有緩存數(shù)據(jù)異常丟失,,也會導致該問題,。因此,可以提前放入數(shù)據(jù)到redis避免上述冷啟動的問題,,當然也不可能是全量數(shù)據(jù),,可以根據(jù)類似于當天的具體訪問情況,實時統(tǒng)計出訪問頻率較高的熱數(shù)據(jù),,這里熱數(shù)據(jù)也比較多,,需要多個服務(wù)并行的分布式去讀寫到redis中(所以要基于zk分布式鎖) 通過nginx+lua將訪問流量上報至kafka中,storm從kafka中消費數(shù)據(jù),,實時統(tǒng)計處每個商品的訪問次數(shù),,訪問次數(shù)基于LRU(apache commons collections LRUMap)內(nèi)存數(shù)據(jù)結(jié)構(gòu)的存儲方案,,使用LRUMap去存放是因為內(nèi)存中的性能高,沒有外部依賴,,每個storm task啟動的時候基于zk分布式鎖將自己的id寫入zk同一個節(jié)點中,,每個storm task負責完成自己這里的熱數(shù)據(jù)的統(tǒng)計,每隔一段時間就遍歷一下這個map,,然后維護一個前1000的數(shù)據(jù)list,,然后去更新這個list,最后開啟一個后臺線程,,每隔一段時間比如一分鐘都將排名的前1000的熱數(shù)據(jù)list同步到zk中去,,存儲到這個storm task對應(yīng)的一個znode中去 部署多個實例的服務(wù),每次啟動的時候就會去拿到上述維護的storm task id列表的節(jié)點數(shù)據(jù),,然后根據(jù)taskid,一個一個去嘗試獲取taskid對應(yīng)的znode的zk分布式鎖,,如果能夠獲取到分布式鎖,,再去獲取taskid status的鎖進而查詢預(yù)熱狀態(tài),如果沒有被預(yù)熱過,,那么就將這個taskid對應(yīng)的熱數(shù)據(jù)list取出來,,從而從DB中查詢出來寫入緩存中,如果taskid分布式鎖獲取失敗,,快速拋錯進行下一次循環(huán)獲取下一個taskid的分布式鎖即可,,此時就是多個服務(wù)實例基于zk分布式鎖做協(xié)調(diào)并行的進行緩存的預(yù)熱 緩存熱點導致系統(tǒng)不可用解決方案 對于瞬間大量的相同數(shù)據(jù)的請求涌入,可能導致該數(shù)據(jù)經(jīng)過hash策略之后對應(yīng)的應(yīng)用層nginx被壓垮,,如果請求繼續(xù)就會影響至其他的nginx,,最終導致所有nginx出現(xiàn)異常整個系統(tǒng)變得不可用。 基于nginx+lua+storm的熱點緩存的流量分發(fā)策略自動降級來解決上述問題的出現(xiàn),,可以設(shè)定訪問次數(shù)大于后95%平均值n倍的數(shù)據(jù)為熱點,,在storm中直接發(fā)送http請求到流量分發(fā)的nginx上去,使其存入本地緩存,,然后storm還會將熱點對應(yīng)的完整緩存數(shù)據(jù)沒發(fā)送到所有的應(yīng)用nginx服務(wù)器上去,,并直接存放到本地緩存。 對于流量分發(fā)nginx,,訪問對應(yīng)的數(shù)據(jù),,如果發(fā)現(xiàn)是熱點標識就立即做流量分發(fā)策略的降級,對同一個數(shù)據(jù)的訪問從hash到一臺應(yīng)用層nginx降級成為分發(fā)至所有的應(yīng)用層nginx,。storm需要保存上一次識別出來的熱點List,,并同當前計算出來的熱點list做對比,如果已經(jīng)不是熱點數(shù)據(jù),,則發(fā)送對應(yīng)的http請求至流量分發(fā)nginx中來取消對應(yīng)數(shù)據(jù)的熱點標識 緩存雪崩解決方案 redis集群徹底崩潰,,緩存服務(wù)大量對redis的請求等待,,占用資源,隨后緩存服務(wù)大量的請求進入源頭服務(wù)去查詢DB,,使DB壓力過大崩潰,,此時對源頭服務(wù)的請求也大量等待占用資源,緩存服務(wù)大量的資源全部耗費在訪問redis和源服務(wù)無果,,最后使自身無法提供服務(wù),,最終會導致整個網(wǎng)站崩潰。 事前的解決方案,,搭建一套高可用架構(gòu)的redis cluster集群,,主從架構(gòu)、一主多從,,一旦主節(jié)點宕機,,從節(jié)點自動跟上,并且最好使用雙機房部署集群,。 事中的解決方案,,部署一層ehcache緩存,在redis全部實現(xiàn)情況下能夠抗住部分壓力,;對redis cluster的訪問做資源隔離,,避免所有資源都等待,對redis cluster的訪問失敗時的情況去部署對應(yīng)的熔斷策略,,部署redis cluster的降級策略,;對源服務(wù)訪問的限流以及資源隔離 事后的解決方案:redis數(shù)據(jù)做了備份可以直接恢復(fù),重啟redis即可,;redis數(shù)據(jù)徹底失敗來或者數(shù)據(jù)過舊,,可以快速緩存預(yù)熱,然后讓redis重新啟動,。然后由于資源隔離的half-open策略發(fā)現(xiàn)redis已經(jīng)能夠正常訪問,,那么所有的請求將自動恢復(fù) 緩存穿透解決方案 對于在多級緩存中都沒有對應(yīng)的數(shù)據(jù),并且DB也沒有查詢到數(shù)據(jù),,此時大量的請求都會直接到達DB,,導致DB承載高并發(fā)的問題。解決緩存穿透的問題可以對DB也沒有的數(shù)據(jù)返回一個空標識的數(shù)據(jù),,進而保存到各級緩存中,,因為有對數(shù)據(jù)修改的異步監(jiān)聽,所以當數(shù)據(jù)有更新,,新的數(shù)據(jù)會被更新到緩存匯中,。 Nginx緩存失效導致redis壓力倍增 可以在nginx本地,設(shè)置緩存數(shù)據(jù)的時候隨機緩存的有效期,,避免同一時刻緩存都失效而大量請求直接進入redis 這個過程值得我們?nèi)ド钊雽W習和思考,。 |
|