开发中常见的十种对缓存的错误使用
簡介
緩存那些頻繁使用的很耗費資源的對象,就可以通過更加快速地加載使應用程序獲得更快的響應。在并發請求時,緩存能夠更好地擴展應用程序。但一些難以覺察的錯誤,可能讓應用程序處于高負荷下,更不用說想讓緩存有更好的表現了,特別是當你正在使用分布式緩存并且將緩存項存儲在不同的緩存服務器或緩存應用程序中時。另外,當緩存在進程外被構建時使用進程內緩存工作地很好的代碼可能會失敗。這里我將向你展示一些通常的分布式緩存錯誤,它將幫助你做更好的決定——是否使用緩存。
這里列出了我見過的前十種錯誤:
1、 依賴.net默認的序列化器
2、 在一個單獨的緩存項中存儲大對象
3、 在線程間使用緩存共享對象
4、 假設存儲那些項之后,它們就會立即被緩存
5、 使用嵌套對象存儲整個集合
6、 將父-子對象存儲在一起或者分開
7、 緩存配置項
8、 緩存已打開的流、文件、注冊表或者網絡句柄的活動對象
9、 使用多個鍵存儲相同的值
10、在更新或者刪除緩存項到持久存儲介質之后,沒有同步更新或刪除緩存
讓我們看看這些錯誤是怎么回事,并且看看如何避免它們。
我假設你已經使用asp.net緩存或者企業庫的緩存模塊一段時間了,你很滿意,現在你需要更好的可擴展性并且想將緩存移動到進程外的實現或者像Velocity、Memcache這樣的分布式緩存上去。在這以后,一切都開始土崩瓦解,因此下面列出的錯誤可能很適合你。
依賴.net默認的序列化器
當你使用一個像Velocity、Memcached這樣的進程外緩存的解決方案時,那些緩存項被存儲的地方在一個單獨的進程上,而不是在你正在運行的應用程序上。每次你向緩存中增加一項,該項都會被序列化到一個字節數組然后將該字節數組發送到緩存服務器并存儲它。簡單地說,當你從緩存中獲得一項時,緩存服務器將這些字節數組發送回你的應用程序,然后客戶端庫反序列化該字節數組得到目標對象。現在,.net的默認序列化不是最佳的選擇,因為它依賴于反射,而反射是一種CPU密集型操作。結果是,在緩存中存儲項以及從緩存中獲取項,增加了序列化和反序列化的開銷,進而導致了CPU的開銷,特別是當你緩存復雜類型時。這種高CPU的消耗發生在你的應用程序中,而不是在緩存服務器上。所以你總是應該使用一個更好的解決方案來讓CPU在序列化以及反序列化時的開銷最小。我個人比較喜歡的方式是自己去序列化和反序列化所有的屬性,通過實現Iserializable接口,并實現反序列化構造器。
這可以防止反射格式化器。當你在存儲大對象時,使用這種方案,你獲得的性能提升可能是默認序列化的100倍。所以,我強烈建議你至少為了那些被緩存的對象,你應該總是實現你自己的序列化和反序列化代碼,而不是讓.net使用反射去決定應該序列化什么。
在一個單獨的緩存項中存儲大對象
有時我們覺得大對象應該被緩存起來,因為得到它們要花費很大的代價。例如,也許你覺得緩存一個1MB的圖像對象,可以比從文件系統或者數據庫加載圖片對象給你帶來更好的性能。你可能會奇怪為什么這不具有可擴展性。當你一次只有一個請求時這確實會比從數據庫加載相同的東西更快。但在并發加載的時候,頻繁地訪問大的圖片對象將降低服務器的CPU效率。這是因為總得來說,緩存時的序列化和反序列化開銷很大。每次你將嘗試從一個外部進程緩存中獲取一個1MB的圖片對象,在內存中構建這樣一個圖片對象對CPU來說是一個很明顯的耗時操作。解決方案是不在緩存中使用一個單獨的鍵來緩存大的圖片對象為一個單獨的項。取而代之的是,你應該將這個大的圖片對象拆分為一些更小的項,然后個別地緩存那些更小的項。你應該只從緩存中檢索那些你需要的最小的項。
這種想法是,看看從大對象中拆出來的那些項中,哪些是你最需要頻繁訪問的(比如說從配置中獲取的圖片對象的連接字符串),并且在緩存中單獨存儲那些項。總是記住那些你從緩存中檢索的項應該盡可能地小,比如最大為8KB。
在線程間使用緩存共享對象
既然你能夠從多個線程中訪問緩存對象,那么有時你就可能在多個線程之間共享數據。但是緩存,就像靜態變量一樣,可能導致競爭條件。當緩存是分布式,并且一旦存儲和讀取一項需要線程外的通信,這種情況就更為常見,并且你的線程彼此之間將獲得更多的機會重疊。接下來的示例展示了進程內緩存很少產生競爭條件但進程外緩存總是出現這種情況:
上面的代碼大部分時間都在演示絕大部分會出現的正確的行為,當你正在使用一個進程內緩存。但,當你走到進程外或者分布式時,它將一直不會成功地演示大部分情況下的正確行為。你需要在這里實現某種形式的鎖,某些緩存提供程序允許你鎖住一項。例如,Velocity就具有鎖這一特性,但是memcache就沒有。在Velocity,你可以鎖住一項:
你可以使用鎖來可靠地將那些被多線程改變的項從緩存中讀取和寫入。
假設存儲那些項之后,它們就會立即被緩存
有時你在點擊一個提交按鈕并且假設頁面被提交之后,你認為緩存中就存儲了一項,并且該項能夠被從緩存中讀取,因為它剛剛被存儲了。你錯了!
你永遠都不能假設你確信一項被存儲在緩存中。甚至你在第一行存儲了一項,并且在第三行讀取了該項。當你的應用程序處在很大的壓力之下并且缺乏物理內存,那些不是很頻繁被訪問的緩存項將被清除。所以,代碼到達第三行的時候,緩存有可能被清除了。永遠都不要假設你總是能夠從緩存中獲得某一項。你總是應該使用一個“非空”檢測,并且從持久存儲器檢索。
當從緩存中讀取一項時,你應該總是使用這種格式。
使用嵌套對象存儲整個集合
有時你會在一個單獨的緩存項中存儲一個完整的集合,因為你需要頻繁地訪問集合中的項。因此每一次你嘗試讀取集合中的某一項,你不得不首先加載整個集合,然后像通常地那樣讀取。有點像這樣的做法:
這種做法是低效的。你沒有必要加載整個集合而僅僅是讀取其中的一項。當緩存早進程內的時候,這絕對沒任何問題,因為在緩存中僅僅存儲著該集合的一個引用。但是,在一個分布式的緩存中,任何時候你訪問它,整個集合都是分離存儲的,這將導致很差的性能。代替緩存整個集合,你應該緩存分離開來的單個的項。
這種想法很簡單,你使用一個鍵來獨立地存儲集合中的每一項。可以想象這種做法很簡單,例如使用索引來區分。
將父-子對象存儲在一起或者分開
有時,你在緩存中存儲的一項有一個子對象,而該子對象也被你單獨地存儲在另一個緩存項中。例如,你有一個customer對象,它有一個order集合。所以,當你緩存customer,order集合也被緩存了。但是,然后你又單獨地存儲了order集合。所以,當一個單獨的order在緩存中被更新時,在customer內部包含相同order項的order集合沒有被更新,并且因此給你造成了不一致的結果。又一次,當你使用進程內緩存的方式,它工作地很好;但是當你的緩存被構建在進程外或分布式架構上時,它將會失敗。
這是一個很難解決的問題。它要求清晰的設計,以至于你永遠都不會在緩存中存儲一個對象兩次。一個通常的解決方案是不在緩存中存儲子對象,而是存儲子對象的Key,來讓它們可以被獨立地檢索。所以在上面的場景中,你將不在緩存中存儲customer的order集合。取而代之的是,你將隨著Customer存儲OrderID集合,然后當你需要讀取customer的訂單集合時,你可以使用OrderID來加載單獨的oder對象。
這種方案能夠確保一個實體的實例在緩存中只會被存儲一次,無論它多少次出現在集合或者父對象中。
緩存配置項
有時你緩存配置項。你使用某些緩存過期策略來確保配置被及時刷新,或者當配置文件、數據庫表改變的時候被刷新。你認為既然配置項會被頻繁地訪問,從緩存中讀取可以很明顯地減小CPU的壓力。但其實,取而代之的是,你應該使用靜態變量來存儲配置。
你不應該采用這樣的方案。從緩存中獲得一項并不“廉價”。它可能沒有比從文件或者直接讀取開銷大。但是,它也有一定的消耗,特別是如果該項是一個自定義的類,并且加入了某些序列化的操作。所以,應該用存儲靜態變量來存儲它。但你也許會為問,當我們將配置項存儲在靜態變量中,我們如何刷新它而不重啟應用程序?你可以使用某些失效邏輯,當配置文件改變時,例如采用文件監聽器來重新加載配置。或者使用某些數據庫輪詢來檢查數據庫的更新。
緩存已打開的流、文件、注冊表或者網絡句柄的活動對象
我看到過一些開發者緩存某些類的實例,這些實例持有打開的文件,注冊表或者外部網絡連接。這種做法很危險。當這些項從緩存中移除的時候,它們無法自動銷毀。除非你手動銷毀這些對象,否則你就會泄露系統資源。
你永遠都不應該僅僅為了在你需要打開的流、文件句柄、注冊表句柄或者網絡連接的時候,保存那些打開的資源,而緩存持它們。取而代之的是,你應該使用某些靜態變量或者某些基于內存的緩存,這些緩存保證給你一個在失效時的回調,能夠讓你正確地釋放它們。進程外的緩存或者用Session存儲,不能給你失效時的回調。所以永遠都不要用它們存儲活動對象。
使用多個鍵存儲相同的值
有時你使用Key并且也使用index來在緩存中存儲對象,因為你不僅需要基于key的檢索,同時也需要通過索引來枚舉它們。例如,
如果你正在使用線程內緩存,接下來的代碼將工作地很好
上面的這段代碼在進程內緩存時,緩存中的兩項都指向了相同的對象實例。所以,不管你如何從緩存中獲得某項,它總是返回相同的對象實例。但是在一個進程外緩存中,特別是在一個分布式緩存中,那些對象都是被序列化后存儲的。而且存儲并不是基于對象引用的,你存儲的是緩存項的一份拷貝,你永遠都無法存儲對象本身。所以,如果你是基于一個Key來檢索一項,當一項被反序列化后或者剛剛被創建后,你從緩存中獲取它,也只是獲取了那一項的最新副本。結果,該對象的任何改變將無法反映給緩存,除非你在對象狀態發生改變之后,覆寫這些緩存中的項。所以,在一個分布式的緩存中,你將不得不像下面這么做:
一旦你使用更改過的項來更新緩存實體,它看起來就像緩存中的項接受了一個該項的新拷貝一樣。
在更新或者刪除緩存項到持久存儲介質之后,沒有同步更新或刪除緩存
它仍然能在進程內緩存中工作地很好,但是當你采用進程外緩存或者分布式緩存時同樣將會失敗。下面是一個例子:
其原因就是你改變了對象,但是卻沒有將最新的對象更新到緩存內。緩存中的項被作為一份拷貝而存儲,不是原本的對象。
另一個錯誤是當該項已經從數據庫中刪除了,卻沒有在緩存中被刪除。
當你從數據庫、文件或者一些持久化存儲中刪除一項時,不要忘記從緩存中刪除該項,刪除所有訪問它的可能性。
總結
緩存要求謹慎的計劃和對緩存數據的清晰理解。否則,當你的緩存構建在分布式上時,它不僅會表現糟糕,甚至能夠產生異常。將這些常見的錯誤記住吧!
原文發布時間為:2011-10-24
本文來自云棲社區合作伙伴CSDN博客,了解相關信息可以關注CSDN博客。
總結
以上是生活随笔為你收集整理的开发中常见的十种对缓存的错误使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 天水在线市长留言板 用文本文件制作留言板
- 下一篇: 征信报告哪里可以打 去哪里可以打印征信报