簡介
緩衝那些頻繁使用的很耗費資源的對象,就可以通過更加快速地載入使應用程式獲得更快的響應。在並發請求時,緩衝能夠更好地擴充應用程式。但一些難以覺察的錯誤,可能讓應用程式處於高負荷下,更不用說想讓緩衝有更好的表現了,特別是當你正在使用分布式緩衝並且將快取項目儲存在不同的快取服務器或緩衝應用程式中時。另外,當緩衝在進程外被構建時使用進程內緩衝工作地很好的代碼可能會失敗。這裡我將向你展示一些通常的分布式緩衝錯誤,它將協助你做更好的決定——是否使用緩衝。
這裡列出了我見過的前十種錯誤:
1、 依賴.net預設的序列化器
2、 在一個單獨的快取項目中儲存大對象
3、 線上程間使用緩衝共用對象
4、 假設儲存那些項之後,它們就會立即被緩衝
5、 使用嵌套Object Storage Service整個集合
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,你可以鎖住一項:
你可以使用鎖來可靠地將那些被多線程改變的項從緩衝中讀取和寫入。
假設儲存那些項之後,它們就會立即被緩衝
有時你在點擊一個提交按鈕並且假設頁面被提交之後,你認為緩衝中就儲存了一項,並且該項能夠被從緩衝中讀取,因為它剛剛被儲存了。你錯了!
你永遠都不能假設你確信一項被儲存在緩衝中。甚至你在第一行儲存了一項,並且在第三行讀取了該項。當你的應用程式處在很大的壓力之下並且缺乏實體記憶體,那些不是很頻繁被訪問的快取項目將被清除。所以,代碼到達第三行的時候,緩衝有可能被清除了。永遠都不要假設你總是能夠從緩衝中獲得某一項。你總是應該使用一個“非空”檢測,並且從持久儲存空間檢索。
當從緩衝中讀取一項時,你應該總是使用這種格式。
使用嵌套Object Storage Service整個集合
有時你會在一個單獨的快取項目中儲存一個完整的集合,因為你需要頻繁地訪問集合中的項。因此每一次你嘗試讀取集合中的某一項,你不得不首先載入整個集合,然後像通常地那樣讀取。有點像這樣的做法:
這種做法是低效的。你沒有必要載入整個集合而僅僅是讀取其中的一項。當緩衝早進程內的時候,這絕對沒任何問題,因為在緩衝中僅僅儲存著該集合的一個引用。但是,在一個分布式的緩衝中,任何時候你訪問它,整個集合都是分離儲存的,這將導致很差的效能。代替緩衝整個集合,你應該緩衝分離開來的單個的項。
這種想法很簡單,你使用一個鍵來獨立地儲存集合中的每一項。可以想象這種做法很簡單,例如使用索引來區分。
將父-子物件儲存在一起或者分開
有時,你在緩衝中儲存的一項有一個子物件,而該子物件也被你單獨地儲存在另一個快取項目中。例如,你有一個customer對象,它有一個order集合。所以,當你緩衝customer,order集合也被緩衝了。但是,然後你又單獨地儲存了order集合。所以,當一個單獨的order在緩衝中被更新時,在customer內部包含相同order項的order集合沒有被更新,並且因此給你造成了不一致的結果。又一次,當你使用進程內緩衝的方式,它工作地很好;但是當你的緩衝被構建在進程外或分布式架構上時,它將會失敗。
這是一個很難解決的問題。它要求清晰的設計,以至於你永遠都不會在緩衝中儲存一個對象兩次。一個通常的解決方案是不在緩衝中儲存子物件,而是儲存子物件的Key,來讓它們可以被獨立地檢索。所以在上面的情境中,你將不在緩衝中儲存customer的order集合。取而代之的是,你將隨著Customer儲存OrderID集合,然後當你需要讀取customer的訂單集合時,你可以使用OrderID來載入單獨的oder對象。
這種方案能夠確保一個實體的執行個體在緩衝中只會被儲存一次,無論它多少次出現在集合或者父物件中。
緩衝配置項
有時你緩衝配置項。你使用某些緩衝到期策略來確保配置被及時重新整理,或者當設定檔、資料庫表改變的時候被重新整理。你認為既然配置項會被頻繁地訪問,從緩衝中讀取可以很明顯地減小CPU的壓力。但其實,取而代之的是,你應該使用靜態變數來儲存配置。
你不應該採用這樣的方案。從緩衝中獲得一項並不“廉價”。它可能沒有比從檔案或者直接讀取開銷大。但是,它也有一定的消耗,特別是如果該項是一個自訂的類,並且加入了某些序列化的操作。所以,應該用儲存靜態變數來儲存它。但你也許會為問,當我們將配置項儲存在靜態變數中,我們如何重新整理它而不重啟應用程式?你可以使用某些失效邏輯,當設定檔改變時,例如採用檔案監聽器來重新載入配置。或者使用某些資料庫輪詢來檢查資料庫的更新。
緩衝已開啟的流、檔案、註冊表或者網路控制代碼的使用中的物件
我看到過一些開發人員緩衝某些類的執行個體,這些執行個體持有開啟的檔案,註冊表或者外部網路連接。這種做法很危險。當這些項從緩衝中移除的時候,它們無法自動銷毀。除非你手動銷毀這些對象,否則你就會泄露系統資源。
你永遠都不應該僅僅為了在你需要開啟的流、檔案控制代碼、註冊表控制代碼或者網路連接的時候,儲存那些開啟的資源,而緩衝持它們。取而代之的是,你應該使用某些靜態變數或者某些基於記憶體的緩衝,這些緩衝保證給你一個在失效時的回調,能夠讓你正確地釋放它們。進程外的緩衝或者用Session儲存,不能給你失效時的回調。所以永遠都不要用它們儲存使用中的物件。
使用多個鍵儲存相同的值
有時你使用Key並且也使用index來在緩衝中儲存物件,因為你不僅需要基於key的檢索,同時也需要通過索引來枚舉它們。例如,
如果你正在使用線程內緩衝,接下來的代碼將工作地很好
上面的這段代碼在進程內緩衝時,緩衝中的兩項都指向了相同的對象執行個體。所以,不管你如何從緩衝中獲得某項,它總是返回相同的對象執行個體。但是在一個進程外緩衝中,特別是在一個分布式緩衝中,那些對象都是被序列化後儲存的。而且儲存並不是基於對象引用的,你儲存的是快取項目的一份拷貝,你永遠都無法儲存物件本身。所以,如果你是基於一個Key來檢索一項,當一項被還原序列化後或者剛剛被建立後,你從緩衝中擷取它,也只是擷取了那一項的最新副本。結果,該對象的任何改變將無法反映給緩衝,除非你在對象狀態發生改變之後,覆寫這些緩衝中的項。所以,在一個分布式的緩衝中,你將不得不像下面這麼做:
一旦你使用更改過的項來更新緩衝實體,它看起來就像緩衝中的項接受了一個該項的新拷貝一樣。
在更新或者刪除快取項目到持久儲存介質之後,沒有同步更新或刪除緩衝
它仍然能在進程內緩衝中工作地很好,但是當你採用進程外緩衝或者分布式緩衝時同樣將會失敗。下面是一個例子:
其原因就是你改變了對象,但是卻沒有將最新的對象更新到緩衝內。緩衝中的項被作為一份拷貝而儲存,不是原本的對象。
另一個錯誤是當該項已經從資料庫中刪除了,卻沒有在緩衝中被刪除。
當你從資料庫、檔案或者一些持久化儲存中刪除一項時,不要忘記從緩衝中刪除該項,刪除所有訪問它的可能性。
總結
緩衝要求謹慎的計劃和對快取資料的清晰理解。否則,當你的緩衝構建在分布式上時,它不僅會表現糟糕,甚至能夠產生異常。將這些常見的錯誤記住吧!