我們搞程式的都瞭解點演算法。總體來講,演算法是什嗎?
演算法就是“時間”和“空間”的互換策略。我們常常考究一個演算法的時間複雜度或空間複雜度,如果我們有絕對足夠的時間或空間,那麼演算法就不需要了,可惜這種條件是不存在的,只是在某些情況下相對來說我們不用去考慮其中一個。今天我們討論的“緩衝”,自然就是“用空間換時間”的演算法。 緩衝就是把一些資料暫時存放於某些地方,可能是記憶體,也有可能硬碟。總之,目的就是為了避免某些耗時的操作。我們常見的耗時的操作,比如資料庫的查詢、一些資料的計算結果,或者是為了減輕伺服器的壓力。其實減輕壓力也是因查詢或計算,雖然短耗時,但操作很頻繁,累加起來也很長,造成嚴重排隊等情況,伺服器抗不住)
概念性的東西暫就不說了,說多了都是故事。現在我們來談談各種緩衝。
初學.NET的朋友開始就會接觸到DataSet類,雲裡霧裡的看著DataSet的例子程式,也不管是咋回事,用就是了。其實DataSet就是緩衝,當我們去讀取一段資料集合的時候,如果每讀取一條資料就處理一條的話,那麼我們的程式和資料庫會一直串連著。假如處理一條資料的耗時可以忽略不計,或者只有你一個人使用這個資料庫的話,那麼資料庫一直連著也無所謂,我們寫代碼完全可以不用DataSet類。但是事實上不耗時不可能的,如果耗時嚴重的話,就會一直佔用這資料庫連接,直到我們處理完畢。如果這種查詢過多,串連數就會佔用過多,而且資料庫在某些操作時會鎖住表,這就會造成其他的請求等待,會出現查詢逾時,程式異常等現象。所以,我們必須先把資料拿出來,再對這些資料進行相關的處理,儘早的關閉資料庫連接,好讓資料庫處理其他的請求。 所以,適時地選用DataSet或DataReader是比較重要的(說明:DataReader就是hold住串連的讀取方式)。
如何使用緩衝,或者說何時使用緩衝?
你可能會迷惑,不知不覺中使用了緩衝(DataSet),這都是.net幫你完成的事。可是,你可能還是不太清楚該如何使用緩衝,或者說何時使用緩衝。不用著急,我們一一來看。
上面說過,我們緩衝的資料無非就是一些資料庫的查詢、計算結果和頻繁查詢。那麼,我們在實際開發中會碰到哪些這種資料呢? 其實仔細想想這是非常常見的,比如使用者登入後的設定檔,當他每次點擊串連後造成頁面重新整理,我們總不能都要去重新查詢資料庫吧?我們常常用Session來儲存這個人的資訊,當他退出系統後我們把Session清理掉,所以Session也是緩衝,只不過他也是.NET給我們提供好的類,sorry,我又舉了一個你不想看到的例子,哈哈。其實Session是私人化的資料,Session的資料訪問必須通過SessionID(詳情我就不多言了,大家google下),還不足以說明緩衝的意義。如果把這個問題延伸下去,假如我們開發的是一個多使用者的Blog系統, 每當我們訪問其中一個部落格時都要去查詢這個博主的資料,假如A和B同時訪問一個部落格時,最理想的狀態就是只查詢一次,而不是兩個人都去訪問資料庫!是不是呢?其實。。。是也不是!(故事裡的事,說是就是,不是也是;說不是就不是,是也不是。 :)。之所以說不是,是因為假如我們的部落格網站每天就幾個人訪問,而且一直發展不起來,我們就沒必要用緩衝,因為使用緩衝帶來了更多的開發複雜度,因為每當我們去更新博主的資料的時候不單單要更新資料庫的資訊,我們還要去處理緩衝。但是如果我們的部落格訪問量非常大,就像部落格園似的,如果再不緩衝,那資料庫伺服器早就Gameover了:),那麼現在就來看怎麼用緩衝的吧。
.Net Framework提供了現成的緩衝類供我們使用,常見的是 System.Web.HttpRuntime.Cache。每當我們去執行 BlogDataProvier.GetBlogInfo()方法時(假定這個方法是我們擷取博主資訊的方法,顧名思義嘛),需要在查詢之前先從緩衝擷取資料,假如資料不存在的話,再去資料庫擷取,並且把得到的結果存入緩衝,並且返回該結果既可。下面我把這個方法的虛擬碼寫出來,好讓從來沒用過緩衝的朋友大致瞭解一下。
public class SqlDataProvider |
public static object GetBlogInfo(string username) |
public class BlogDataProvider |
public static object GetBlogInfo(string username) |
var cacheKey = "Blog_" + username; |
var blog = CacheHelper.Get(cacheKey); |
blog = SqlDataProvider.GetBlogInfo(username); |
CacheHelper.Set(cacheKey, blog); |
public static object Get(string key) |
return System.Web.HttpRuntime.Cache.Get(key); |
public static void Set(string key, object value) |
System.Web.HttpRuntime.Cache.Insert(key, value); |
緩衝,兩個字道出了其實際意義,一個是“存”,我們剛剛存了;另一個是“緩”,暫緩,緩衝一般只是用來暫時儲存,其命運都會被刪除或替換掉,所以緩衝有個時效問題。如果你說你的資料永遠都不會到期,那麼說真的,我建議你直接寫在代碼裡就可以了。
上面的例子讓我們瞭解到了HttpCache類。看來我們可以用它來解決絕大部分的緩衝問題,主要是公用資料的緩衝(所謂公用資料就是你我都可以訪問的同一資料)。希望新手朋友捧著MSDN仔細學習該類的用法,真的很重要哦,不是嗎?
開始我們說了“拿空間換時間”,目前只提到了緩衝一些頻繁查詢的情況,犧牲空間緩衝時間的明顯些的例子有嗎?沒問題,你看好咯!
說之前先插一句,我們公司現在在招人,其中一道筆試題是介紹一下List<T>和Dictionary<TKey,TValue>的區別和用途。很遺憾,面試了很多人,只有一個同學回答的到位,其他的說什麼的都有。你想好怎麼回答了嗎?:)如果你看了下面發現和你現在想的一致而且你還需要找一份有挑戰的工作的話,給我訊息哦。
其實用List<T>,Dictionary<TKey,TValue>泛型就是用來迷魂人的,哈哈,就會有些同學往泛型上面扯,結果上當咯,我完全可以用ArrayList和Hashtable來問。
List是什麼資料結構?數組!而且是動態數組,之所以動態就是可以視情況動態申請空間。Dictionary是什麼結構?有的同學回答是字典。字典是什麼資料結構? 散列表!散列,一聽這名字就知道是散開分布的資料表。怎麼個“散”法? 自然是按照Key來散,每個Key對應一個Value,所以我們常叫做“索引值對”,Key和Value是成對的。我們把Dictionary看作是一個數組,那麼每個Key的hash值(什麼是hash值?在.net裡任何類型都有GetHashCode方法,返回int值,有木有),便是數組的下標,而該數組的元素值就是Value!所以我們在擷取Dictionary的某個Key的Value時,速度是非常快的,可以直接通過已知的下標拿到值,這個時間複雜度是O(1)。快不快啊?好快好快。但是,你有木有想到,所有的Key的hash值是按順序來的嗎?顯然不是,鬼知道你用的什麼key,所以,Dictionary的這個數組很長很長,浪費了很多空位置,所以,那就是 空間 換 時間。當然GetHashCode的演算法不同,Key對應的值的分布也有區別,有的比較緊密有的比較鬆散,常見的演算法比如一致性hash演算法。
dictionary的實際記憶體分布
如所示,dict的分布是不緊湊的,犧牲了很多空間,但可以最快速的找到資料,所以dict或hash或map等,不管什麼叫什麼類,總之都是hashtable,它們的用途主要就是查詢。所以,如果我們把部落格按使用者名稱作key緩衝起來的話,使用者訪問部落格時都是使用的username,所以我們甚至不需要blogId,就可以拿到博主的資訊,根本沒走資料庫。
而list這種排列緊湊的資料集合一般用於批處理。當然還有兼顧空間和速度的資料結構,那就是樹結構,在尋找時不需要所有資料都進行遍曆,時間複雜度一般是O(logn),而且空間是緊湊的,採用的是鏈表結構,而不是緊湊的數組。所以在時間和空間上都不比前兩者,但用途卻十分廣大,我們所用的資料庫的索引基本上都是用的樹。這樣既保證了佔用空間小,查詢的速度也不慢。
上面這一段我們介紹了hash表的基本原理,現在我們明白了緩衝的優勢,在實際的項目使用中,我們除了使用系統提供的Cache類以外,完全可以自己嘗試寫緩衝類,為什麼不呢?呵呵。我們把一個變數斯static,然後再public,就等於是全域變數了,我們可以到處訪問到他,而且我們還要用dict,因為他足夠快!還不快動手去寫一個,回來再接著看!
剛才提到了“緩”字,緩也有不同的策略,比如最常見的按時間緩衝,在單位時間內該資料有效,每當訪問時都要判斷緩衝的資料是否到期,再決定Get還是Remove。除了時間策略,還有使用熱度策略,由於記憶體有限,所以我們的緩衝也不是無限申請的,是時候限制長度了。限制了長度就意味著有人能進來就得有人要出去。這就是Remove策略。我們可以對所有的緩衝打上標記,來標記他的熱度,每次添加緩衝的時候把熱度最低的緩衝剔除掉(假如已經達到限制的話)。每次擷取緩衝的時候給該緩衝熱度+1。這是多人性化的設計,不是嗎? 我上篇博文中已經貼出了這類的代碼。有興趣的朋友給你們個傳送門。
我們繼續用部落格園作例子,我們知道部落格園的訪問量已經很大了(具體多大,俺不知道,反正以前發表評論經常逾時,官方團隊解決後還發表博文說咋解決的,結果評論裡一大票同學都說怎麼不用緩衝阿:)。
當網站訪問量達到一定程度後,一台機器很難處理太多的httprequest,這個時候我們必須使用多台機器。假如你的程式沒同時跑在多台機器上的話,你對緩衝的理解恐怕不會很深,因為誰都要會這種經曆:哎呀 sessio不能分布式阿? 哎呀媽呀,我的緩衝不能在兩台機器上阿,這可咋整?!
其實這也不能怪你,要怪就怪微軟吧。因為IIS,我們的web程式駐留在一個進程裡,每個httprequest會有一個線程來處理,所以你甚至都沒用過多線程。害人啊,哈哈。但隨著項目經驗的增加,特別是大項目的曆練後,也沒什麼了。之所以說是微軟的錯,是因為人家php阿,ruby阿,人家的服務端(apache,nginx等)都是多進程的。每個httprequest一個進程,總共開幾十個進程,處理並發。多進程就意味著資料共用問題,就像我們多台機器的情況一樣。 這時候需要藉助一個共用快取進程來供其他的web服務進程來訪問擷取緩衝。 這就是下面要說的 分布式緩衝。
如果說兩三年前你不知memcached為何物,或許情有可原,那時候還流行自己寫windows service。但現在滿世界的NoSQL,MongoDB,Memcached,Redis,你再不知道的話,真該說多看看部落格吧;看看新技術,你已經落後一個時代了。
上面提到的這個名詞都是玩緩衝的主。NoSQL是個新技術,NoSQL DB現在很多種,MongoDB就是一種,MongoDB是介於傳統關係型資料庫和記憶體資料庫的雜交資料庫,現在也算是很熱門的資料庫。MemCached是著名的分布式快取服務,而Redis(Remoting Dictionary Service),你懂了吧?!我們的快取服務器可以用memcached或redis,Memcached是純記憶體的,重啟進程會丟失所有緩衝,而redis可以把資料寫到硬碟裡,各有各的優點吧。Redis更適合存經過計算過的資料。而且Redis支援豐富的資料類型(list\set\hash\string),這要比memcached更靈活些。 他們都有.net的Driver,還有相關的Example和UnitTest,可以官網下載看看。
關於Redis的使用可以看代震軍大蝦的http://www.cnblogs.com/daizhj/archive/2011/02/21/1959511.html
隨著硬體的發展,記憶體的不斷增加,緩衝的應用越來越多,現在各種NoSQL資料庫應用而生,我們也要緊跟腳步,學習新的理念。關於NoSQL我這裡也不多介紹了,自己搜尋。
其他的緩衝,比如頁面的緩衝(OutputCache)我這裡就不作介紹了,緩衝其他同學補充。謝謝各位閱讀。歡迎大家交流,交流的方式希望是和諧的,最好不要有帶味的評論,帶來不好的討論氛圍,說錯誤的希望大家提出。