緩存的基礎

緩存的基礎

該文檔編寫的目的主要是讓開發者明白緩存的相關概念,在使用緩存的時候清楚本身的在作什麼事,避免盲目使用形成項目的可維護性變差。本文將從幾個方面的來闡述緩存的相關基礎概念,包括緩存解決的問題、緩存的弊端、緩存的相關概念、緩存的使用誤區。數據庫

1、 緩存解決的問題

互聯網項目區分於傳統的企業軟件開發最大的不一樣點就是互聯網項目須要應對更多的用戶。這覺得的應用必須提供更高的併發量支持、同時還須要有更高的性能來提升用戶體驗。這兩個特色致使了互聯網項目對緩存的依賴特別重。由於緩存偏偏就是爲了解決這兩個問題而誕生的。編程

1. 高併發

高併發須要解決的問題從 Web 系統來說主要有兩個方面。Web 服務器的的 IO 數和數據庫的 IO 數。Web 服務器的 IO 數在設計良好的系統上能夠經過簡單的增長實例來解決,大多數狀況主要是數據庫的 IO 問題。一個請求發送到 Web 服務器上到數據庫上可能就會放大到幾倍,再加上數據庫在一些場景上也很能經過增長實例來解決問題。因此大多數狀況下只能經過減小對數據庫的請求來解決問題。這裏就是緩存的一個重要使用場景,經過直接讀寫緩存來減小對數據庫的請求,提升系統的負載能力。後端

2. 高性能

高性能能夠經過緩存解決能夠從兩個角度來思考。一個複雜計算的結果緩存。對於須要消耗不少的時間和資源計算,咱們能夠經過緩存結果來快速響應請求。另一個角度就是通信時間。不少時間,花費在計算上的時間並無多少,更多的是數據經過網絡傳輸太耗時。經過讀取更近的緩存來減小請求的響應時間也是一個很重要的用途。緩存

2、緩存的弊端

理想狀況下咱們確定是指望緩存是對系統的一個補充,更多的提升系統的上限。可是現實狀況是,緩存在系統設計之處就不得不認真考慮。由於互聯網項目的須要面對的問題決定了咱們的系統下限就已經容忍度很低了,在沒有新技術誕生的狀況下,現有的架構和中間件就難以達到下限。緩存成了系統中必不可少一部分,因此咱們也要認清緩存會帶來的問題,盡最大努力下降加緩存給開發帶來的難度。服務器

1. 緩存對業務邏輯的侵入

原來的單體應用,緩存都是存儲在本地、須要緩存的內容大多也就是數據庫的數據,這其實是比較好搞定的一種緩存使用場景。而到了更復雜的系統中,服務都要求是無狀態的、可分佈式部署的,這致使原有的緩存理念實際上不能知足如今的使用場景。原有的緩存框架也不適應如今的開發面臨的問題。不少時候咱們須要在業務邏輯上手動操做緩存,而不僅僅依靠框架來解決問題。同時爲了讓緩存應用起來更簡單,也要改變原來的一些開發理念,因此緩存也會影響咱們的業務邏輯。網絡

2. 緩存讓架構更復雜

前文也提到了互聯網應用的下限容忍度很低,不少時間一個系統若是沒有緩存可能根本就沒法提供服務。所以咱們的架構上更加複雜了,須要加上更多的中間件,對中間件的可靠性要求也更高了。同時根據咱們選擇的緩存的策略的不一樣,整個系統可能就並不是原來的頁面、應用、數據庫這三大件這麼簡單了。咱們須要使用 MQ、Redis 來作緩存,計算過程更加複雜,調用鏈難以追蹤,錯誤難以排查。數據結構

3. 犧牲了數據的一致性

使用緩存必然致使數據很難作到實時一致性,只能作到最終一致性。這也是不少單體架構進行服務拆分時碰到的問題。在使用緩存之後咱們不得不考慮到數據不一致對業務邏輯的影響,甚至爲此係統用戶的使用體驗。架構

3、緩存的相關概念

若是你只作業務邏輯開發,前面的內容可能對你無關緊要,有其餘人幫你考慮這些問題,可是對於緩存的分類是須要清楚的,這關係到具體的代碼應該怎麼寫。併發

1. 緩存的概念

前面說了緩存的利弊、說了使用緩存的緣由,可是並無對緩存作一個定義。一方面是緩存的概念沒那麼複雜,就是存儲計算結果從而讓下次計算能夠直接跳過計算步驟直接獲取結果。框架

事實上在更多領域對緩存的使用其可能有更復雜的定義。可是從咱們應用系統開發的層面來講,這種理解是足夠的。更多的是咱們須要瞭解緩存的一些具體的分類,這有助於咱們更清楚本身作的事,更清楚本身應該使用那種緩存。

2. 緩存的分類

從幾個不一樣的維度,咱們對緩存作了一些分類。清楚本身的業務場景,選擇合適的緩存方式能夠下降應用的難度。

  • 從存儲位置分類

    • 本地緩存

      存儲在本地的緩存。通常是存儲在內存中,僅供本進程讀取的數據。這種技術使用的很廣泛,傳統的緩存框架已經針對這種緩存作了很好的封裝。咱們更多的須要考慮緩存的邊界問題,緩存在本進程的共享範圍、緩存的生命週期可否和進程保持一致。

    • 分佈式緩存

      由於多實例部署的需求,只把數據緩存在本地已經很難知足咱們的業務場景了。分佈式緩存上咱們花費了更多的精力。簡單場景咱們能夠繼續使用原來的緩存套路,把分佈式緩存當作本地緩存來用。可是這麼作其實並無什麼意義,這樣的緩存應用模式很難被其餘實例共享。更多的只是爲了解決應用服務的內存佔用問題。

      在使用分佈式緩存時咱們須要清楚使用分佈式緩存的代價,將緩存內容放在 Redis 等中間件裏咱們須要更多的通信時間、讀取緩存的速度也降低了。放棄性能更好的本地緩存不用,確定是由於這些緩存使用分佈式存儲性價比更高。這些緩存須要多實例共享、生命週期沒法和進程保持一致、須要預加載等緣由都是咱們使用分佈式緩存的緣由。咱們能夠徹底用緩存服務器代替本地緩存,可是要清楚代價。

  • 從更新方式分類

    • Cache Aside 更新模式

      這是咱們接觸比較多的一種模式,邏輯也比較簡單。它的更新模式以下:

      失效:應用程序先從 cache 取數據,沒有獲得,則從數據庫中取數據,成功後,放到緩存中。 命中:應用程序從 cache 中取數據,取到後返回。 更新:先把數據存到數據庫中,成功後,再讓緩存失效。

      咱們在本身寫緩存操做邏輯的時候記住遵循這種模式就行了。具體爲何要這麼作就再也不贅述的,這種緩存更新模式的相關資料不少。

    • Read/Write Through 更新模式

      在上面的 Cache Aside 套路中,應用代碼須要維護兩個數據存儲,一個是緩存(cache),一個是數據庫(repository)。因此,應用程序比較囉嗦。而 Read/Write Through 套路是把更新數據庫(repository)的操做由緩存本身代理了,因此,對於應用層來講,就簡單不少了。能夠理解爲,應用認爲後端就是一個單一的存儲,而存儲本身維護本身的 Cache。 Read Through Read Through 套路就是在查詢操做中更新緩存,也就是說,當緩存失效的時候(過時或 LRU 換出),Cache Aside 是由調用方負責把數據加載入緩存,而 Read Through 則用緩存服務本身來加載,從而對應用方是透明的。 Write Through Write Through 套路和 Read Through 相仿,不過是在更新數據時發生。當有數據更新的時候,若是沒有命中緩存,直接更新數據庫,而後返回。若是命中了緩存,則更新緩存,而後由 Cache 本身更新數據庫(這是一個同步操做)。

      這種緩存使用場景咱們可能使用的比較少,更多的是中間件本身提供的功能。

    • Write Behind Caching 更新模式

      Write Back 套路就是,在更新數據的時候,只更新緩存,不更新數據庫,而咱們的緩存會異步地批量更新數據庫。這個設計的好處就是讓數據的 I/O 操做飛快無比(由於直接操做內存嘛)。由於異步,Write Back 還能夠合併對同一個數據的屢次操做,因此性能的提升是至關可觀的。

      這種緩存通常須要本身開發中間件來作,對性能和併發要求極高的場景能夠考慮投入必定的精力來搭建緩存中間件。

  • 從緩存內容分類

    • Action 緩存

      這類緩存使用的很普遍。從運維的角度咱們就有 CDN 緩存和反向代理緩存,咱們主要討論後端應用的緩存。Action 緩存是後端應用最先能夠加緩存的地方,若是場景足夠清晰徹底能夠在這一層就把緩存加上來。固然由於考慮到項目的分層問題,可能更多的只把相關邏輯挪到緊鄰 controller 的 service 處。這裏的緩存建議採用分佈式緩存。

    • 方法緩存

      方法緩存在傳統項目開發中使用的很廣泛,到了互聯網項目裏雖然應用場景減小了,可是依然使用的很普遍。這種緩存的特徵是緩存的 Key 是方法的入參,Value 是方法的結果。這是一個範圍比較廣的分類,和其餘分類多有重合。可是實際上有本質的不一樣,能夠自行體會一下。

    • 數據緩存

      這種緩存主要是緩存 repository 的數據。比較多的使用場景是爲了較少 DB 的壓力。在性能要求比較的高的場景能夠關閉 repository 的緩存功能,徹底用數據緩存來代替。這樣 DB 須要的內存更多,響應速度也更快了。

    • 對象緩存

      把對象緩存放在最後講不是由於對象緩存不重要。實際上對象緩存很是重要,一個系統若是有良好的對象緩存,那麼它緩存應用難度會下降不少,讀取和更新會很是簡單。可是使用對象緩存的前提是開發是以面向對象的方式來架構系統的,對於對象的建模能力要求很高,這纔是使用對象緩存最大的困難點。

4、緩存的使用誤區

這裏咱們援引了一篇博客(使用緩存的9大誤區)上總結的誤區,在這個基礎上我補充了本身對這些使用誤區的理解。

  • 過於依賴默認的緩存機制

    由於直接使用語言提供的序列化方式比較簡單,因此不少時候咱們直接使用這種序列化方式並無什麼問題。可是不少場景咱們能夠本身定製序列化的方式,或者是使用的數據量更少、又或者是這種結構讓咱們更新的時候比較方便。一個很典型的場景,咱們可使用 Redis 的 Hash 結構來緩存對象,這樣若是咱們想更新緩存的某一條屬性的時候是很是方便的。

  • 緩存大對象

    這個更可能是開發者沒有正確的認知到本身緩存的對象究竟是什麼樣的數據結構。好比緩存 Spring 管理的 Bean,世界上咱們在使用這些 Bean 時操做的更可能是本身對象的代理類,這裏面可能有不少框架附加上去的信息。咱們覺得本身的對象結構很簡單就直接緩存了,等實際序列化之後是很龐大的。緩存畢竟仍是很消耗資源的,對於本身到底緩存了什麼東西要內心有數。

  • 使用緩存機制在不一樣服務間共享數據

    這裏我對原做者的內容作了一些修改,加上了不一樣服務間的前提。由於微服務架構中一個服務部署多個實例是一件很正常的事情,同一個服務間共享同一份緩存並無什麼問題。可是不一樣服務間共享緩存就問題不少了,這讓原來辛辛苦苦拆分出來的服務一會兒又被耦合到一塊兒了,同時可能不一樣服務直接不瞭解對方的邏輯,還可能致使緩存的內容被修改爲錯誤的值。這是絕對須要避免的錯誤。可是這麼作又是極具誘惑力的,緩存的良好性能讓、方便的操做讓經過緩存共享數據很省事,可是這對於架構的破壞性很是大。

  • 認爲調用緩存 API 以後,數據會被馬上緩存起來

    這個誤區更多的是出如今使用了 MQ 作緩存的場景,或者是併發量極大的場景。並無什麼很好的方式來避免這種錯誤,更多的是在寫緩存邏輯的時候注意到這種狀況是有可能發生的。

  • 緩存大量的數據集合,而讀取其中一部分

    也是一個比較常見的誤區。不少時候是由於從數據庫獲取的數據就是一個集合,因此直接緩存了這個集合。等到要讀取數據的時候不得不反序列化整個集合的數據再從集合裏找本身想要的內容。這也致使本身的緩存更新很是麻煩,明明只是更新了集合中某一個緩存就不能不讓整個集合的緩存失效,緩存的命中率大大下降了。

    這種狀況下試試將集合中的緩存一條一條的存儲,也就是將大的緩存對象拆分紅多個緩存。

  • 緩存大量具備圖結構的對象致使內存浪費

    在面向對象的編程思惟中,咱們很容易就會設計出層級比較多的對象。ORM 框架通常會幫咱們作懶加載,這其實能夠很好的利用到數據庫的緩存機制。可是這卻不利於咱們作分佈式緩存。因此說原來的架構並無很好的貼合分佈式系統的使用場景。這裏 MyBatis 框架由於它的靈活性卻是提供了一些方法來作緩存。

  • 緩存應用程序的配置信息

    這也是一種極具誘惑力的使用方式。在緩存服務器中存儲配置,這樣當須要更新配置時只要修改緩存的值就能夠統一修改全部實例的配置。可是這樣作風險很大。緩存服務器雖然已經作了不少可靠性保障,可是其本意並非像數據庫這類中間件同樣必須100%可用,緩存服務器是容許掛掉(理論上)的。若是咱們把配置放在緩存服務器上,這致使咱們不得不把緩存服務器的可靠性也提升到100%。在分佈式架構中,配置的分發更推薦使用專業的中間件,例如 zookeeper、etcd 等。它們在設計上就是要作到100%可靠,同時也提供了推送機制,配置更新更及時。

  • 使用不少不一樣的鍵指向相同的緩存項

    這個是在使用方法緩存的時候比較常犯的錯誤,使用不一樣的參數進行計算可是結果實際上是同一個。參考數據庫裏的索引設計,實際上是同樣的道理。若是緩存的 Key 比較複雜,其實能夠經過維護一份 Key 的緩存,最後都指向緩存的惟一性標示,類型數據庫的主鍵的設計。這樣數據咱們只須要維護一份,只須要維護 Key 的緩存就行了。

  • 沒有及時的更新或者刪除再緩存中已通過期或者失效的數據

    這個是在使用緩存的時候不多注意到的問題。由於各類緩存框架、緩存服務器通常會幫咱們作一些緩存剔除操做。可是若是須要本身操做緩存的時候就須要特別注意這個問題。一旦緩存裏出現了非預期的髒數據,不但清理起來很麻煩,找到出現問題的地方有時候也很難。對於這點更多的是想清楚本身緩存的生命週期,在生命週期結束上記得加上清理邏輯。好比服務關閉的時候進行緩存清理。

相關文章
相關標籤/搜索