概述
對于高并發(fā)架構(gòu),毫無疑問緩存是最重要的一環(huán),對于大量的高并發(fā),可以采用三層緩存架構(gòu)來實現(xiàn),Nginx+redis+ehcache,下面對這每個環(huán)節(jié)做一下介紹。
nginx
對于中間件nginx常用來做流量的分發(fā),同時nginx本身也有自己的緩存(容量有限),我們可以用來緩存熱點數(shù)據(jù),讓用戶的請求直接走緩存并返回,減少流向服務(wù)器的流量
1、模板引擎
通常我們可以配合使用freemaker/velocity等模板引擎來抗住大量的請求
- 小型系統(tǒng)可能直接在服務(wù)器端渲染出所有的頁面并放入緩存,之后的相同頁面請求就可以直接返回,不用去查詢數(shù)據(jù)源或者做數(shù)據(jù)邏輯處理
- 對于頁面非常之多的系統(tǒng),當(dāng)模板有改變,上述方法就需要重新渲染所有的頁面模板,毫無疑問是不可取的。因此配合nginx+lua(OpenResty),將模板單獨保存在nginx緩存中,同時對于用來渲染的數(shù)據(jù)也存在nginx緩存中,但是需要設(shè)置一個緩存過期的時間,以盡可能保證模板的實時性
2、雙層nginx來提升緩存命中率
對于部署多個nginx而言,如果不加入一些數(shù)據(jù)的路由策略,那么可能導(dǎo)致每個nginx的緩存命中率很低。因此可以部署雙層nginx
- 分發(fā)層nginx負(fù)責(zé)流量分發(fā)的邏輯和策略,根據(jù)自己定義的一些規(guī)則,比如根據(jù)productId進(jìn)行hash,然后對后端nginx數(shù)量取模將某一個商品的訪問請求固定路由到一個nginx后端服務(wù)器上去
- 后端nginx用來緩存一些熱點數(shù)據(jù)到自己的緩存區(qū)
3、推薦架構(gòu)
nginx作為最前端的web cache系統(tǒng),通常的架構(gòu)如下

這個結(jié)構(gòu)的優(yōu)點:
- 可以使用nginx前端進(jìn)行諸多復(fù)雜的配置,這些配置從前在squid是沒法做或者做起來比較麻煩的,比如針對目錄的防盜鏈。
- nginx前端可以直接轉(zhuǎn)發(fā)部分不需要緩存的請求。
- 因為nginx效率高于squid,所以某些情況下可以利用nginx的緩存來減輕squid壓力。
- 可以實現(xiàn)url hash等分配策略
- 可以在最前端開啟gzip壓縮,這樣后面的squid緩存的純粹是無壓縮文檔,可以避免很多無謂的穿透。
- 因為nginx穩(wěn)定性比較高,所以lvs不需要經(jīng)常調(diào)整,通過nginx調(diào)整就可以。
- squid的文件打開數(shù)按默認(rèn)的1024就綽綽有余,不過處理的請求可一個都不會少。
- 可以啟用nginx的日志功能取代squid,這樣做實時點擊量統(tǒng)計時可以精確定位到url,不必要再用低效率的grep來過濾。
- 因為nginx的負(fù)載能力高于squid,所以在用lvs分流時可以不必分得特別均衡,出現(xiàn)單點故障的幾率比較低。
nginx和squid配合搭建的web服務(wù)器前端系統(tǒng)架構(gòu):

前端的lvs和squid,按照安裝方法,把epoll打開,配置文件照搬,基本上問題不多。
這個架構(gòu)和App_squid架構(gòu)的區(qū)別,也是關(guān)鍵點就是:加入了一級中層代理,中層代理的好處實在太多了:
- gzip壓縮:壓縮可以通過nginx做,這樣,后臺應(yīng)用服務(wù)器不管是Apache、resin、lighttpd甚至iis或其他古怪服務(wù)器,都不用考慮壓縮的功能問題。
- 負(fù)載均衡和故障屏蔽:nginx可以作為負(fù)載均衡代理使用,并有故障屏蔽功能,這樣,根據(jù)目錄甚至一個正則表達(dá)式來制定負(fù)載均衡策略變成了小case。
- 方便的運維管理,在各種情況下可以靈活制訂方案。
- 權(quán)限清晰:這臺機(jī)器就是不寫程序的維護(hù)人員負(fù)責(zé),程序員一般不需要管理這臺機(jī)器,這樣假如出現(xiàn)故障,很容易能找到正確的人。對于應(yīng)用服務(wù)器和數(shù)據(jù)庫服務(wù)器,最好是從維護(hù)人員的視線中消失,我的目標(biāo)是,這些服務(wù)只要能跑得起來就可以了,其它的事情全部可以在外部處理掉。
redis(看架構(gòu)也可以考慮memcache)
用戶的請求,在nginx沒有緩存相應(yīng)的數(shù)據(jù),那么會進(jìn)入到redis緩存中,redis可以做到全量數(shù)據(jù)的緩存,通過水平擴(kuò)展能夠提升并發(fā)、高可用的能力
1、持久化機(jī)制:將redis內(nèi)存中的數(shù)據(jù)持久化到磁盤中,然后可以定期將磁盤文件上傳至S3(AWS)或者ODPS(阿里云)等一些云存儲服務(wù)上去。
1)RDB
對redis中的數(shù)據(jù)執(zhí)行周期性的持久化,每一刻持久化的都是全量數(shù)據(jù)的一個快照。對redis性能影響較小,基于RDB能夠快速異常恢復(fù)
2)AOF
以append-only的模式寫入一個日志文件中,在redis重啟的時候可以通過回放AOF日志中的寫入指令來重新構(gòu)建整個數(shù)據(jù)集。(實際上每次寫的日志數(shù)據(jù)會先到linux os cache,然后redis每隔一秒調(diào)用操作系統(tǒng)fsync將os cache中的數(shù)據(jù)寫入磁盤)。對redis有一定的性能影響,能夠盡量保證數(shù)據(jù)的完整性。redis通過rewrite機(jī)制來保障AOF文件不會太龐大,基于當(dāng)前內(nèi)存數(shù)據(jù)并可以做適當(dāng)?shù)闹噶钪貥?gòu)。
如果同時使用RDB和AOF兩種持久化機(jī)制,那么在redis重啟的時候,會使用AOF來重新構(gòu)建數(shù)據(jù),因為AOF中的數(shù)據(jù)更加完整,建議將兩種持久化機(jī)制都開啟,用AO F來保證數(shù)據(jù)不丟失,作為數(shù)據(jù)恢復(fù)的第一選擇;用RDB來作不同程度的冷備,在AOF文件都丟失或損壞不可用的時候來快速進(jìn)行數(shù)據(jù)的恢復(fù)。
2、redis集群
1)replication

一主多從架構(gòu),主節(jié)點負(fù)責(zé)寫,并且將數(shù)據(jù)同步到其他salve節(jié)點(異步執(zhí)行),從節(jié)點負(fù)責(zé)讀,主要就是用來做讀寫分離的橫向擴(kuò)容架構(gòu)。這種架構(gòu)的master節(jié)點數(shù)據(jù)一定要做持久化,否則,當(dāng)master宕機(jī)重啟之后內(nèi)存數(shù)據(jù)清空,那么就會將空數(shù)據(jù)復(fù)制到slave,導(dǎo)致所有數(shù)據(jù)消失
2)sentinal哨兵

哨兵是redis集群架構(gòu)中很重要的一個組件,負(fù)責(zé)監(jiān)控redis master和slave進(jìn)程是否正常工作,當(dāng)某個redis實例故障時,能夠發(fā)送消息報警通知給管理員,當(dāng)master node宕機(jī)能夠自動轉(zhuǎn)移到slave node上,如果故障轉(zhuǎn)移發(fā)生來,會通知client客戶端新的master地址。sentinal至少需要3個實例來保證自己的健壯性,并且能夠更好地進(jìn)行quorum投票以達(dá)到majority來執(zhí)行故障轉(zhuǎn)移。
前兩種架構(gòu)方式最大的特點是,每個節(jié)點的數(shù)據(jù)是相同的,無法存取海量的數(shù)據(jù)。因此哨兵集群的方式使用與數(shù)據(jù)量不大的情況
3)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就是可以任意擴(kuò)展的,直接橫向擴(kuò)展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ī)模的宕機(jī),導(dǎo)致nginx大量流量直接涌入數(shù)據(jù)生產(chǎn)服務(wù),那么最后的tomcat堆內(nèi)存緩存也可以處理部分請求,避免所有請求都直接流向DB。
具有以下特性:
1、快速輕量
- 過去幾年,諸多測試表明Ehcache是最快的JAVA緩存之一。
- Ehcache的線程機(jī)制是為大型高并發(fā)系統(tǒng)設(shè)計的。
- 大量性能測試用例保證Ehcache在不同版本間性能表現(xiàn)得一致性。
- 很多用戶都不知道他們正在用Ehcache,因為不需要什么特別的配置。
- API易于使用,這就很容易部署上線和運行。
- 很小的jar包,Ehcache 2.2.3才668kb。
- 最小的依賴:唯一的依賴就是SLF4J了。
2、伸縮性
- 緩存在內(nèi)存和磁盤存儲可以伸縮到數(shù)G,Ehcache為大數(shù)據(jù)存儲做過優(yōu)化。
- 大內(nèi)存的情況下,所有進(jìn)程可以支持?jǐn)?shù)百G的吞吐。
- 為高并發(fā)和大型多CPU服務(wù)器做優(yōu)化。
- 線程安全和性能總是一對矛盾,Ehcache的線程機(jī)制設(shè)計采用了Doug Lea的想法來獲得較高的性能。
- 單臺虛擬機(jī)上支持多緩存管理器。
- 通過Terracotta服務(wù)器矩陣,可以伸縮到數(shù)百個節(jié)點。
3、靈活性
- Ehcache 1.2具備對象API接口和可序列化API接口。
- 不能序列化的對象可以使用除磁盤存儲外Ehcache的所有功能。
- 除了元素的返回方法以外,API都是統(tǒng)一的。只有這兩個方法不一致:getObjectValue和getKeyValue。這就使得緩存對象、序列化對象來獲取新的特性這個過程很簡單。
- 支持基于Cache和基于Element的過期策略,每個Cache的存活時間都是可以設(shè)置和控制的。
- 提供了LRU、LFU和FIFO緩存淘汰算法,Ehcache 1.2引入了最少使用和先進(jìn)先出緩存淘汰算法,構(gòu)成了完整的緩存淘汰算法。
- 提供內(nèi)存和磁盤存儲,Ehcache和大多數(shù)緩存解決方案一樣,提供高性能的內(nèi)存和磁盤存儲。
- 動態(tài)、運行時緩存配置,存活時間、空閑時間、內(nèi)存和磁盤存放緩存的最大數(shù)目都是可以在運行時修改的。
4、標(biāo)準(zhǔn)支持
- Ehcache提供了對JSR107 JCACHE API最完整的實現(xiàn)。因為JCACHE在發(fā)布以前,Ehcache的實現(xiàn)(如net.sf.jsr107cache)已經(jīng)發(fā)布了。
- 實現(xiàn)JCACHE API有利于到未來其他緩存解決方案的可移植性。
- Ehcache的維護(hù)者Greg Luck,正是JSR107的專家委員會委員。
5、可擴(kuò)展性
- 監(jiān)聽器可以插件化。Ehcache 1.2提供了CacheManagerEventListener和CacheEventListener接口,實現(xiàn)可以插件化,并且可以在ehcache.xml里配置。
- 節(jié)點發(fā)現(xiàn),冗余器和監(jiān)聽器都可以插件化。
- 分布式緩存,從Ehcache 1.2開始引入,包含了一些權(quán)衡的選項。Ehcache的團(tuán)隊相信沒有什么是萬能的配置。
- 實現(xiàn)者可以使用內(nèi)建的機(jī)制或者完全自己實現(xiàn),因為有完整的插件開發(fā)指南。
- 緩存的可擴(kuò)展性可以插件化。創(chuàng)建你自己的緩存擴(kuò)展,它可以持有一個緩存的引用,并且綁定在緩存的生命周期內(nèi)。
- 緩存加載器可以插件化。創(chuàng)建你自己的緩存加載器,可以使用一些異步方法來加載數(shù)據(jù)到緩存里面。
- 緩存異常處理器可以插件化。創(chuàng)建一個異常處理器,在異常發(fā)生的時候,可以執(zhí)行某些特定操作。
6、應(yīng)用持久化
在VM重啟后,持久化到磁盤的存儲可以復(fù)原數(shù)據(jù)。
Ehcache是第一個引入緩存數(shù)據(jù)持久化存儲的開源Java緩存框架。緩存的數(shù)據(jù)可以在機(jī)器重啟后從磁盤上重新獲得。
根據(jù)需要將緩存刷到磁盤。將緩存條目刷到磁盤的操作可以通過cache.flush()方法來執(zhí)行,這大大方便了Ehcache的使用。
7、分布式緩存
從Ehcache 1.2開始,支持高性能的分布式緩存,兼具靈活性和擴(kuò)展性。
分布式緩存的選項包括:
- 通過Terracotta的緩存集群:設(shè)定和使用Terracotta模式的Ehcache緩存。緩存發(fā)現(xiàn)是自動完成的,并且有很多選項可以用來調(diào)試緩存行為和性能。
- 使用RMI、JGroups或者JMS來冗余緩存數(shù)據(jù):節(jié)點可以通過多播或發(fā)現(xiàn)者手動配置。狀態(tài)更新可以通過RMI連接來異步或者同步完成。
- Custom:一個綜合的插件機(jī)制,支持發(fā)現(xiàn)和復(fù)制的能力。
- 可用的緩存復(fù)制選項。支持的通過RMI、JGroups或JMS進(jìn)行的異步或同步的緩存復(fù)制。
- 可靠的分發(fā):使用TCP的內(nèi)建分發(fā)機(jī)制。
- 節(jié)點發(fā)現(xiàn):節(jié)點可以手動配置或者使用多播自動發(fā)現(xiàn),并且可以自動添加和移除節(jié)點。對于多播阻塞的情況下,手動配置可以很好地控制。
- 分布式緩存可以任意時間加入或者離開集群。緩存可以配置在初始化的時候執(zhí)行引導(dǎo)程序員。
- BootstrapCacheLoaderFactory抽象工廠,實現(xiàn)了BootstrapCacheLoader接口(RMI實現(xiàn))。
- 緩存服務(wù)端。Ehcache提供了一個Cache Server,一個war包,為絕大多數(shù)web容器或者是獨立的服務(wù)器提供支持。
- 緩存服務(wù)端有兩組API:面向資源的RESTful,還有就是SOAP。客戶端沒有實現(xiàn)語言的限制。
- RESTful緩存服務(wù)器:Ehcached的實現(xiàn)嚴(yán)格遵循RESTful面向資源的架構(gòu)風(fēng)格。
- SOAP緩存服務(wù)端:Ehcache RESTFul Web Services API暴露了單例的CacheManager,他能在ehcache.xml或者IoC容器里面配置。
- 標(biāo)準(zhǔn)服務(wù)端包含了內(nèi)嵌的Glassfish web容器。它被打成了war包,可以任意部署到支持Servlet 2.5的web容器內(nèi)。Glassfish V2/3、Tomcat 6和Jetty 6都已經(jīng)經(jīng)過了測試。
8、Java EE和應(yīng)用緩存
為普通緩存場景和模式提供高質(zhì)量的實現(xiàn)。
阻塞緩存:它的機(jī)制避免了復(fù)制進(jìn)程并發(fā)操作的問題。
SelfPopulatingCache在緩存一些開銷昂貴操作時顯得特別有用,它是一種針對讀優(yōu)化的緩存。它不需要調(diào)用者知道緩存元素怎樣被返回,也支持在不阻塞讀的情況下刷新緩存條目。
CachingFilter:一個抽象、可擴(kuò)展的cache filter。
SimplePageCachingFilter:用于緩存基于request URI和Query String的頁面。它可以根據(jù)HTTP request header的值來選擇采用或者不采用gzip壓縮方式將頁面發(fā)到瀏覽器端。你可以用它來緩存整個Servlet頁面,無論你采用的是JSP、velocity,或者其他的頁面渲染技術(shù)。
SimplePageFragmentCachingFilter:緩存頁面片段,基于request URI和Query String。在JSP中使用jsp:include標(biāo)簽包含。
已經(jīng)使用Orion和Tomcat測試過,兼容Servlet 2.3、Servlet 2.4規(guī)范。
Cacheable命令:這是一種老的命令行模式,支持異步行為、容錯。
兼容Hibernate,兼容google App Engine。
基于JTA的事務(wù)支持,支持事務(wù)資源管理,二階段提交和回滾,以及本地事務(wù)。
經(jīng)典的緩存+數(shù)據(jù)庫讀寫的模式
1、讀的時候,先讀緩存,緩存沒有的話,那么就讀數(shù)據(jù)庫,然后取出數(shù)據(jù)后放入緩存,同時返回響應(yīng)
2、更新的時候,先刪除緩存,然后再更新數(shù)據(jù)庫
之所以更新的時候只是刪除緩存,因為對于一些復(fù)雜有邏輯的緩存數(shù)據(jù),每次數(shù)據(jù)變更都更新一次緩存會造成額外的負(fù)擔(dān),只是刪除緩存,讓該數(shù)據(jù)下一次被使用的時候再去執(zhí)行讀的操作來重新緩存,這里采用的是懶加載的策略。
舉個例子,一個緩存涉及的表的字段,在1分鐘內(nèi)就修改了20次,或者是100次,那么緩存跟新20次,100次;但是這個緩存在1分鐘內(nèi)就被讀取了1次,因此每次更新緩存就會有大量的冷數(shù)據(jù),對于緩存符合28黃金法則,20%的數(shù)據(jù),占用了80%的訪問量
數(shù)據(jù)庫和redis緩存雙寫不一致的問題
1、最初級的緩存不一致問題以及解決方案
問題:如果先修改數(shù)據(jù)庫再刪除緩存,那么當(dāng)緩存刪除失敗來,那么會導(dǎo)致數(shù)據(jù)庫中是最新數(shù)據(jù),緩存中依舊是舊數(shù)據(jù),造成數(shù)據(jù)不一致。
解決方案:可以先刪除緩存,再修改數(shù)據(jù)庫,如果刪除緩存成功但是數(shù)據(jù)庫修改失敗,那么數(shù)據(jù)庫中是舊數(shù)據(jù),緩存是空不會出現(xiàn)不一致
2、比較復(fù)雜的數(shù)據(jù)不一致問題分析
問題:對于數(shù)據(jù)發(fā)生來變更,先刪除緩存,然后去修改數(shù)據(jù)庫,此時數(shù)據(jù)庫中的數(shù)據(jù)還沒有修改成功,并發(fā)的讀請求到來去讀緩存發(fā)現(xiàn)是空,進(jìn)而去數(shù)據(jù)庫查詢到此時的舊數(shù)據(jù)放到緩存中,然后之前對數(shù)據(jù)庫數(shù)據(jù)的修改成功來,就會造成數(shù)據(jù)不一致
解決方案:將數(shù)據(jù)庫與緩存更新與讀取操作進(jìn)行異步串行化。當(dāng)更新數(shù)據(jù)的時候,根據(jù)數(shù)據(jù)的唯一標(biāo)識,將更新數(shù)據(jù)操作路由到一個jvm內(nèi)部的隊列中,一個隊列對應(yīng)一個工作線程,線程串行拿到隊列中的操作一條一條地執(zhí)行。當(dāng)執(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ù)庫中的舊值
對于這種處理方式需要注意一些問題:
- 讀請求長時阻塞:由于讀請求進(jìn)行來非常輕度的異步化,所以對超時的問題需要格外注意,超過超時時間會直接查詢DB,處理不好會對DB造成壓力,因此需要測試系統(tǒng)高峰期QPS來調(diào)整機(jī)器數(shù)以及對應(yīng)機(jī)器上的隊列數(shù)最終決定合理的請求等待超時時間
- 多實例部署的請求路由:可能這個服務(wù)會部署多個實例,那么必須保證對應(yīng)的請求都通過nginx服務(wù)器路由到相同的服務(wù)實例上
- 熱點數(shù)據(jù)的路由導(dǎo)師請求的傾斜:因為只有在商品數(shù)據(jù)更新的時候才會清空緩存,然后才會導(dǎo)致讀寫并發(fā),所以更新頻率不是太高的話,這個問題的影響并不是特別大,但是的確可能某些機(jī)器的負(fù)載會高一些