在高訪問量的web系統中,緩存幾乎是離不開的;可是一個適當、高效的緩存方案設計卻並不容易,如何才能設計一個好的緩存方案了?html
******************************************************************************************************前端
在高訪問量的web系統中,緩存幾乎是離不開的;可是一個適當、高效的緩存方案設計卻並不容易;因此接下來將討論一下應用系統緩存的設計方面應該注意哪些東西,包括緩存的選型、常見緩存系統的特色和數據指標、緩存對象結構設計和失效策略以及緩存對象的壓縮等等,以期讓有需求的同窗尤爲是初學者可以快速、系統的瞭解相關知識。html5
關係型數據庫的數據量是比較小的,以咱們經常使用的MySQL爲例,單表數據條數通常應該控制在2000w之內,若是業務很複雜的話,可能還要低一些。即使是對於Oracle這些大型商業數據庫來說,其能存儲的數據量也很難知足一個擁有幾千萬甚至數億用戶的大型互聯網系統。程序員
在實際開發中咱們常常會發現,關係型數據庫在TPS上的瓶頸每每會比其餘瓶頸更容易暴露出來,尤爲對於大型web系統,因爲天天大量的併發訪問,對數據庫的讀寫性能要求很是高;而傳統的關係型數據庫的處理能力確實捉襟見肘;以咱們經常使用的MySQL數據庫爲例,常規狀況下的TPS大概只有1500左右(各類極端場景下另當別論)。web
而對於一個日均PV千萬的大型網站來說,每一個PV所產生的數據庫讀寫量可能要超出幾倍,這種狀況下,天天全部的數據讀寫請求量可能遠超出關係型數據的處理能力,更別說在流量峯值的狀況下了;因此咱們必需要有高效的緩存手段來抵擋住大部分的數據請求!算法
正常狀況下,關係型數據的響應時間是至關不錯的,通常在10ms之內甚至更短,尤爲是在配置得當的狀況下。可是就如前面所言,咱們的需求是不通常的:當擁有幾億條數據,1wTPS的時候,響應時間也要在10ms之內,這是任何一款關係型數據都沒法作到的。chrome
那麼這個問題如何解決呢?最簡單有效的辦法固然是緩存!數據庫
本地緩存多是你們用的最多的一種緩存方式了,無論是本地內存仍是磁盤,其速度快,成本低,在有些場合很是有效。後端
可是對於web系統的集羣負載均衡結構來講,本地緩存使用起來就比較受限制,由於當數據庫數據發生變化時,你沒有一個簡單有效的方法去更新本地緩存;然而,你若是在不一樣的服務器之間去同步本地緩存信息,因爲緩存的低時效性和高訪問量的影響,其成本和性能恐怕都是難以接受的。數組
前面提到過,本地緩存的使用很容易讓你的應用服務器帶上「狀態」,這種狀況下,數據同步的開銷會比較大;尤爲是在集羣環境中更是如此!
分佈式緩存這種東西存在的目的就是爲了提供比RDB更高的TPS和擴展性,同時有幫你承擔了數據同步的痛苦;優秀的分佈式緩存系統有你們所熟知的Memcached、Redis(固然也許你把它當作是NoSQL,可是我我的更願意把分佈式緩存也當作是NoSQL),還有淘寶自主開發的Tair等。
對比關係型數據庫和緩存存儲,其在讀和寫性能上的差距可謂天壤之別;就拿淘寶的Tair來講,mdb引擎的單機QPS已在10w以上,ldb的也達到了5w~7w,而集羣的性能會更高(目前uic所用的Tair集羣QPS高達數十萬!)。
因此,在技術和業務均可以接受的狀況下,咱們能夠儘可能把讀寫壓力從數據庫轉移到緩存上,以保護看似強大,其實卻很脆弱的關係型數據庫。
這塊很容易被人忽略,客戶端緩存主要是指基於客戶端瀏覽器的緩存方式;因爲瀏覽器自己的安全限制,web系統能在客戶端所作的緩存方式很是有限,主要由如下幾種:
瀏覽器cookie
這是使用最多的在客戶端保存數據的方法,你們也都比較熟悉;
瀏覽器本地緩存
不少瀏覽器都提供了本地緩存的接口,可是因爲各個瀏覽器的實現有差別,因此這種方式不多被使用;此類方案有chrome的Google Gear,IE的userData、火狐的sessionStorage和globalStorage等;
flash本地存儲
這個也是平時比較經常使用的緩存方式;相較於cookie,flash緩存基本沒有數量和體積的限制,並且因爲基於flash插件,因此也不存在兼容性問題;不過在沒有安裝flash插件的瀏覽器上則沒法使用;
html5的本地存儲
因爲html5尚未普及,因此雖然其本地存儲功能強大,可是目前尚未實用性,這裏只是說起一下而已。
對於客戶端緩存的利用,這裏給出一個小例子;在三方應用系統中,有一個功能是布點在toolbar(全網工具欄)上的應用快速啓動圖標,其內容在服務端存儲,因用戶的定製行爲而不一樣,而且是隨着toolbar的加載而展現;由於當用toolbar加載時是不知道用戶是否有數據的,因此都要去服務端請求一次;這樣若是正常調用,該功能的接口天天將會有十幾億的http訪問量,這顯然是任何一個系統都沒法承受的!
在這種狀況下,應用了客戶端緩存的思路,在flash本地緩存中放置了用戶有無數據的一個標誌位。若是標誌位存在,說明用戶沒有數據,則不須要去服務端讀取;若是沒有標誌位,多是用戶有數據,也多是本地緩存丟失,這種狀況下則去服務端讀取數據,並更新本地標誌位;
通過這樣優化,本來須要十幾億的接口調用,降到了天天400萬便可(有數據的用戶只有400萬左右),即使是用戶更換瀏覽器,或者緩存丟失須要從新訪問服務器,給服務器的壓力也很小!在此基礎上,再加上合理的緩存過時時間,就能夠在數據準確和性能上作一個很好的折衷。
這裏主要是指數據庫的查詢緩存,大部分數據庫都是會提供,每種數據庫的具體實現細節也會有所差別,不過基本的原理就是用查詢語句的hash值作key,對結果集進行緩存;若是利用的好,能夠很大的提升數據庫的查詢效率!數據庫的其餘一些緩存將在後邊介紹。
如今可供咱們選擇使用的(僞)分佈式緩存系統不要太多,好比使用普遍的Memcached、最近炒得火熱的Redis,以及咱們淘寶本身的Tair等;這裏前面加個僞字,意思是想說,有些所謂的分佈式緩存其實還是以單機的思惟去作的,不能算是真正的分佈式緩存(你以爲只實現個主從複製能算分佈式麼?)。
既然有這麼多的系統可用,那麼咱們在選擇的時候,就要有必定的標準和方法。只有有了標準,才能衡量一個系統時好時壞,或者適不適合,選擇就基本有了方向。
下邊幾點是我我的覺的應該考慮的幾個點(其實大部分系統選型都是這麼考慮的,並不是只有緩存系統)。
廢話,容量固然是越大越好了,這還用說麼,有100G我幹嗎還要用10G?其實這麼說總要考慮一下成本啦,目前一臺普通的PC Server內容128G已經算是大的了,再大的話無論是從硬件仍是從軟件方面,管理的成本都會增長。單機來說,好比說主板的插槽數量,服務器散熱、操做系統的內存分配、回收、碎片管理等等都會限制內存卡的容量;即使使用多機的話,大量內存的採購也是很費money的!
有詩云:
山不在高,有仙則名;內存很少,夠用就行!
每一個系統在初期規劃的時候,都會大體計算一下所要消耗的緩存空間,這主要取決於你要緩存的對象數量和單個對象的大小。通常來講,你能夠採用對象屬性在內存中的存儲長度簡單加和的方法來計算單個對象的體積,再乘以緩存對象的數量和預期增加(固然,這裏邊有一個熱點數據的問題,這裏就不細討論了),大概得出須要使用的緩存空間;以後就能夠按照這個指標去申請緩存空間或搭建緩存系統了。
這裏說併發量,其實還不如說是QPS更貼切一些,由於咱們的緩存不是直接面向用戶的,而只面向應用的,因此確定不會有那個高的併發訪問(固然,多個系統共用一套緩存那就另當別論了);因此咱們關心的是一個緩存系統平均每秒可以承受多少的訪問量。
咱們之因此須要緩存系統,就是要它在關鍵時刻能抗住咱們的數據訪問量的;因此,緩存系統可以支撐的併發量是一個很是重要的指標,若是它的性能還不如關係型數據庫,那咱們就沒有使用的必要了。
對於淘寶的系統來講,咱們不妨按照下邊的方案來估算併發量:QPS = 日PV × 讀寫次數/PV ÷ (8 × 60 × 60)
這裏咱們是按照一天8個小時來計算的,這個值基於一個互聯網站點的訪問規律得出的,固然,若是你不一樣意這個值,能夠本身定義。
在估算訪問量的時候,咱們不得不考慮一個峯值的問題,尤爲是像淘寶這樣的電商網站,常常會由於一些大的促銷活動而使PV、UV衝到平時的幾倍甚至幾十倍!這也正是咱們的緩存系統發揮做用的關鍵時刻!
因此在計算出平均值以後,再乘以一個峯值係數,基本就能夠得出你的緩存系統須要承受的最高QPS,通常狀況下,這個係數定爲10是合理的。
響應時間固然也是必要的,若是一個緩存系統慢的跟蝸牛同樣,設置直接就超時了,那和咱們使用MySQL也沒啥區別了。
通常來講,要求一個緩存系統在1ms或2ms以內返回數據是不過度的,固然前提是你的數據不會太大;若是想更快的話,那你就有點過度了,除非你是用的本地緩存;由於通常而言,在大型IDC機房,一個TCP迴環(不攜帶業務數據)就要消耗掉0.5ms。
大部分的緩存系統,因爲是基於內存,因此響應時間都很短,可是問題通常會出如今數據量和QPS變大以後,因爲內存管理策略、數據查找方式、I/O模型、業務場景等方面的差別,響應時間可能會差別不少,因此對於QPS和響應時間這兩項指標,還要靠上線前充分的性能測試來進一步確認,不能只單純的依賴官方的測試結果。
通常分佈式緩存系統會包括服務端和客戶端兩部分,因此其使用成本上也要分爲兩個部分來說;
首先服務端,優秀的系統要是可以方便部署和方便運維的,不須要高端硬件、不須要複雜的環境配置、不能有過多的依賴條件,同時還要穩定、易維護;
而對於客戶端的使用成原本說,更關係到程序員的開發效率和代碼維護成本,基本有三點:單一的庫依賴、簡潔的配置和人性化的API。
另外有一點要提的是,無論是服務端仍是客戶端,豐富的文檔和技術支持也是必不可少的。
緩存系統的擴展性是指在空間不足的性狀況,可以經過增長機器等方式快速的在線擴容。這也是可以支撐業務系統快速發展的一個重要因素。
通常來說,分佈式緩存的負載均衡策略有兩種,一種是在客戶端來作,另一種就是在服務端來作。
客戶端負載均衡
在客戶端來作負載均衡的,諸如前面咱們提到的Memcached、Redis等,通常都是經過特定Hash算法將key對應的value映射到固定的緩存服務器上去,這樣的作法最大的好處就是簡單,無論是本身實現一個映射功能仍是使用第三方的擴展,都很容易;但由此而來的一個問題是咱們沒法作到failover。好比說某一臺Memcached服務器掛掉了,可是客戶端還會傻不啦嘰的繼續請求該服務器,從而致使大量的線程超時;固然,所以而形成的數據丟失是另一回事了。要想解決,簡單的可能只改改改代碼或者配置文件就ok了,可是像Java這種就蛋疼了,有可能還須要重啓全部應用以便讓變動可以生效。
若是線上緩存容量不夠了,要增長一些服務器,也有一樣的問題;並且因爲hash算法的改變,還要遷移對應的數據到正確的服務器上去。
服務端負載均衡
若是在服務端來作負載均衡,那麼咱們前面提到的failover的問題就很好解決了;客戶端可以訪問的全部的緩存服務器的ip和端口都會事先從一箇中心配置服務器上獲取,同時客戶端會和中心配置服務器保持一種有效的通訊機制(長鏈接或者HeartBeat),可以使後端緩存服務器的ip和端口變動即時的通知到客戶端,這樣,一旦後端服務器發生故障時能夠很快的通知到客戶端改變hash策略,到新的服務器上去存取數據。
但這樣作會帶來另一個問題,就是中心配置服務器會成爲一個單點。解決辦法就將中心配置服務器由一臺變爲多臺,採用雙機Stand by方式或者Zookeeper等方式,這樣可用性也會大大提升。
咱們使用緩存系統的初衷就是當數據請求量很大,數據庫沒法承受的狀況,可以經過緩存來抵擋住大部分的請求流量,因此一旦緩存服務器發生故障,而緩存系統又沒有一個很好的容災措施的話,全部或部分的請求將會直接壓倒數據庫上,這多是咱們所不能接受的。
並非全部的緩存系統都具備容災特性的,因此咱們在選擇的時候,必定要根據本身的業務需求,對緩存數據的依賴程度來決定是否須要緩存系統的容災特性。
Memcached嚴格的說還不能算是一個分佈式緩存系統,我的更傾向於將其當作一個單機的緩存系統,因此從這方面講其容量上是限制的;但因爲Memcached的開源,其訪問協議也都是公開的,因此目前有不少第三方的客戶端或擴展,在必定程度上對Memcached的集羣擴展作了支持,可是大部分都只是作了一個簡單Hash或者一致性Hash。
因爲Memcached內部經過chunk鏈的方式去管理內存數據,實現很簡潔,因此其讀寫性能都很是高,官方給出的數據,64KB對象的狀況下,單機QPS可達到15w(估計是極限條件下)以上。
Memcached集羣的多機之間是相互獨立的,沒有數據方面的通訊,因此也不具有failover的能力,也沒法在發生數據傾斜的時候也沒法自動調整
Memcached的多語言支持很是好,目前可支持C/C++、Java、C#、PHP、Python、Perl、Ruby等經常使用語言,也有大量的文檔和示例代碼可供參考,並且其穩定性也通過了長期的檢驗,應該說比較適合於中小型系統和初學者使用的緩存系統。
Redis也是眼下比較流行的一個緩存系統,在國內外不少互聯網公司都在使用(新浪微博就是個典型的例子),不少人把Redis當作是Memcached的替代品。下面就簡單介紹下Redis的一些特性;
Redis除了像Memcached那樣支持普通的<k,v>類型的存儲外,還支持List、Set、Map等集合類型的存儲,這種特性有時候在業務開發中會比較方便;
Redis源生支持持久化存儲,可是根據不少人的使用狀況和測試結果來看,Redis的持久化是個雞肋,就連官方也不推薦過分依賴Redis持久化存儲功能。就性能來說,在所有命中緩存時,Redis的性能接近memcached,可是一旦使用了持久化以後,性能會迅速降低,甚至會相差一個數量級。
Redis支持「集羣」,這裏的集羣仍是要加上引號的,由於目前Redis可以支持的只是Master-Slave模式;這種模式只在可用性方面有必定的提高,當主機宕機時,能夠快速的切換到備機,和MySQL的主備模式差很少,可是還算不上是分佈式系統;
此外,Redis支持訂閱模式,即一個緩存對象發生變化時,全部訂閱的客戶端都會收到通知,這個特性在分佈式緩存系統中是不多見的。
在擴展方面,Redis目前尚未成熟的方案,官方只給出了一個單機多實例部署的替代方案,並經過主備同步的模式進行擴容時的數據遷移,可是仍是沒法作到持續的線性擴容。
Tair是淘寶本身開發的緩存系統,並且也是一套真正意義上的分佈式而且能夠跨多機房部署,同時支持內存緩存和持久化存儲的解決方案;目前Tair已經在淘蝌蚪上開源。
Tair實現了緩存框架和緩存存儲引擎的獨立,在遵照接口規範的狀況下,能夠根據需求更換存儲引擎,目前支持mdb(基於memcached)、rdb(基於Redis)、kdb(基於kyoto cabinet,持久存儲,目前已不推薦使用)和rdb(基於gooogle的levelDB,持久化存儲)幾種引擎;
因爲基於mdb和rdb,因此Tair可以間距二者的特性,並且在併發量和響應時間上,也接近兩者的裸系統。
在擴展性和容災方面,Tair本身作了加強;經過使用虛擬節點Hash(一致性Hash的變種實現)的方案,將key經過Hash函數映射到到某個虛擬節點(桶)上,而後經過中心服務器(configserver)來管理虛擬節點到物理節點的映射關係;這樣,Tair不但實現了基於Hash的首次負載均衡,同時又能夠經過調整虛擬節點和物理節點的映射關係來實現二次負載均衡,這樣有效的解決了因爲業務熱點致使的訪問不均衡問題以及線性擴容時數據遷移麻煩;此外,Tair的每臺緩存服務器和中心服務器(configserver)也有主備設計,因此其可用性也大大提升。
這裏的內存數據庫只要是指關係型內存數據庫。通常來講,內存數據庫使用場景可大體分爲兩種狀況:
一是對數據計算實時性要求比較高,基於磁盤的數據庫很難處理;同時又要依賴關係型數據庫的一些特性,好比說排序、加合、複雜條件查詢等等;這樣的數據通常是臨時的數據,生命週期比較短,計算完成或者是進程結束時便可丟棄;
另外一種是數據的訪問量比較大,可是數據量卻不大,這樣即使丟失也能夠很快的從持久化存儲中把數據加載進內存;
但無論是在哪一種場景中,存在於內存數據庫中的數據都必須是相對獨立的或者是隻服務於讀請求的,這樣不須要複雜的數據同步處理。
對於本地磁盤或分佈是緩存系統來講,其緩存的數據通常都不是結構化的,而是半結構話或是序列化的;這就致使了咱們讀取緩存時,很難直接拿到程序最終想要的結果;這就像快遞的包裹,若是你不打開外層的包裝,你就拿不出來裏邊的東西;
若是包裹裏的東西有不少,可是其中只有一個是你須要的,其餘的還要再包好送給別人;這時候你打開包裹時就會很痛苦——爲了拿到本身的東西,必需要拆開包裹,可是拆開後還要很麻煩的將剩下的再包會去;等包裹傳遞到下一我的的手裏,又是如此!
因此,這個時候粒度的控制就很重要了;究竟是一件東西就一個包裹呢,仍是好多東西都包一塊呢?前者拆起來方便,後着節約包裹數量。映射到咱們的系統上,咱們的緩存對象中到底要放哪些數據?一種數據一個對象,簡單,讀取寫入都快,可是種類一多,緩存的管理成本就會很高;多種數據放在一個對象裏,方便,一塊全出來了,想用哪一個均可以,可是若是我只要一種數據,其餘的就都浪費了,網絡帶寬和傳輸延遲的消耗也很可觀。
這個時候主要的考慮點就應該是業務場景了,不一樣的場景使用不一樣的緩存粒度,折衷權衡;不要不在意這點性能損失,緩存通常都是訪問頻率很是高的數據,各個點的累積效應多是很是巨大的!
三方應用系統就出現過一次由於將一個訪問頻率很高的緩存對象設計的過於龐大,而將一臺Tair服務器的網卡撐爆的狀況。
固然,有些緩存系統的設計也要求咱們必須考慮緩存對象的粒度問題;好比說Memcached,其chunk設計要求業務要能很好的控制其緩存對象的大小;淘寶的Tair也是,對於尺寸超過1M的對象,處理效率將大爲下降;
像Tair、Redis這種提供同時提供了Map、List結構支持的系統來講,雖然增長了緩存結構的靈活性,但最多也只能算是半結構化緩存,還沒法作到像本地內存那樣的靈活性。
粒度設計的過粗還會遇到併發問題。一個大對象裏包含的多種數據,不少地方多要用,這時若是使用的是緩存修改模式而不是過時模式,那麼極可能會由於併發更新而致使數據被覆蓋;版本控制是一種解決方法(好比說Tair),可是這樣會使緩存更新失敗的機率大大增長,並且有些緩存系統也不提供版本支持(好比說用的很普遍的Memcached)。
同緩存粒度同樣,緩存的結構也是同樣的道理。對於一個緩存對象來講,並非其粒度越小,體積也越小;若是你的一個字符串就有1M大小,那也是很恐怖的;
數據的結構決定着你讀取的方式,舉個很簡單的例子,集合對象中,List和Map兩種數據結構,因爲其底層存儲方式不一樣,因此使用的場景也不同;前者更適合有序遍歷,然後者適合隨機存取;回想一下,你是否是曾經在程序中遇到過爲了merge兩個list中的數據,而不得不循環嵌套?
因此,根據具體應用場景去爲緩存對象設計一個更合適的存儲結構,也是一個很值得注意的點。
緩存的更新策略主要有兩種:被動失效和主動更新,下面分別進行介紹。
通常來講,緩存數據主要是服務讀請求的,並設置一個過時時間;或者當數據庫狀態改變時,經過一個簡單的delete操做,使數據失效掉;當下次再去讀取時,若是發現數據過時了或者不存在了,那麼就從新去持久層讀取,而後更新到緩存中;這便是所謂的被動失效策略。
可是在被動失效策略中存在一個問題,就是從緩存失效或者丟失開始直到新的數據再次被更新到緩存中的這段時間,全部的讀請求都將會直接落到數據庫上;而對於一個大訪問量的系統來講,這有可能會帶來風險。因此咱們換一種策略就是,當數據庫更新時,主動去同步更新緩存,這樣在緩存數據的整個生命期內,就不會有空窗期,前端請求也就沒有機會去親密接觸數據庫。
前面咱們提到主動更新主要是爲了解決空窗期的問題,可是這一樣會帶來另外一個問題,就是併發更新的狀況;
在集羣環境下,多臺應用服務器同時訪問一份數據是很正常的,這樣就會存在一臺服務器讀取並修改了緩存數據,可是還沒來得及寫入的狀況下,另外一臺服務器也讀取並修改舊的數據,這時候,後寫入的將會覆蓋前面的,從而致使數據丟失;這也是分佈式系統開發中,必然會遇到的一個問題。解決的方式主要有三種:
鎖控制:這種方式通常在客戶端實現(在服務端加鎖是另一種狀況),其基本原理就是使用讀寫鎖,即任何進程要調用寫方法時,先要獲取一個排他鎖,阻塞住全部的其餘訪問,等本身徹底修改完後才能釋放;若是遇到其餘進程也正在修改或讀取數據,那麼則須要等待;
鎖控制雖然是一種方案,可是不多有真的這樣去作的,其缺點顯而易見,其併發性只存在於讀操做之間,只要有寫操做存在,就只能串行。
版本控制:這種方式也有兩種實現,一種是單版本機制,即爲每份數據保存一個版本號,當緩存數據寫入時,須要傳入這個版本號,而後服務端將傳入的版本號和數據當前的版本號進行比對,若是大於當前版本,則成功寫入,不然返回失敗;這樣解決方式比較簡單;可是增長了高併發下客戶端的寫失敗機率;
多版本機制,即存儲系統爲每一個數據保存多份,每份都有本身的版本號,互不衝突,而後經過必定的策略來按期合併,再或者就是交由客戶端本身去選擇讀取哪一個版本的數據。不少分佈式緩存通常會使用單版本機制,而不少NoSQL則使用後者。
因爲獨立於應用系統,分佈式緩存的本質就是將全部的業務數據對象序列化爲字節數組,而後保存到本身的內存中。所使用的序列化方案也天然會成爲影響系統性能的關鍵點之一。
通常來講,咱們對一個序列化框架的關注主要有如下幾點:
序列化速度:即對一個普通對象,將其從內存對象轉換爲字節數組須要多長時間;這個固然是越快越好;
對象壓縮比:即序列化後生成對象的與原內存對象的體積比;
支持的數據類型範圍:序列化框架都支持什麼樣的數據結構;對於大部分的序列化框架來講,都會支持普通的對象類型,可是對於複雜對象(好比說多繼承關係、交叉引用、集合類等)可能不支持或支持的不夠好;
易用性:一個好的序列化框架必須也是使用方便的,不須要用戶作太多的依賴或者額外配置;
對於一個序列化框架來講,以上幾個特性很難都作到很出色,這是一個魚和熊掌不可兼得的東西(具體緣由後面會介紹),可是終歸有本身的優點和特長,須要使用者根據實際場景仔細考量。
咱們接下來會討論幾種典型的序列化工具;
首先咱們先針對幾組框架來作一個簡單的對比測試,看看他們在對象壓縮比和性能方面到底如何;
咱們先定義一個Java對象,該對象裏主要包含了咱們經常使用的int、long、float、double、String和Date類型的屬性,每種類型的屬性各有兩個;
測試時的樣本數據隨機生成,而且數據生成時間不計入測試時間;由於每種序列化框架的內部實現策略,因此即使是同一框架在處理不一樣類型數據時表現也會有差別;同時測試結果也會受到機器配置、運行環境等影響;限於篇幅,此處只是簡單作了一個對比測試,感興趣的同窗能夠針對本身項目中的實際數據,來作更詳細、更有針對性的測試;
首先咱們先來看下幾種框架壓縮後的體積狀況,以下表:
接下來再看一下序列化處理時間數據;以下表所示:
綜合來看,若是隻處理數值類型,幾種序列化框架的對象壓縮比相差驚人,Protobuf和kryo生成的本身數組只有Hessian和Java的五分之一或六分之一,加上字符串的處理後(對於大尺寸文檔,有不少壓縮算法均可以作到高效的壓縮比,可是針對對象屬性中的這種小尺寸文本,可用的壓縮算法並很少),差距縮小了大概一倍。而在處理時間上,幾種框架也有者相應程度的差距,兩者的增減性是基本一致的。
Java源生序列化是JDK自帶的對象序列化方式,也是咱們最經常使用的一種;其優勢是簡單、方便,不須要額外的依賴並且大部分三方系統或框架都支持;目前看來,Java源生序列化的兼容性也是最好的,可支持任何實現了Serializable接口的對象(包括多繼承、循環引用、集合類等等)。但隨之而來不可避免的就是,其序列化的速度和生成的對象體積和其餘序列化框架相比,幾乎都是最差的。
咱們不妨先來看一下序列化工具要處理那些事情:
首先,要記錄序列化對象的描述信息,包括類名和路徑,反序列化時要用;
要記錄類中全部的屬性的描述信息,包括屬性名稱、類型和屬性值;
若是類有繼承關係,則要對全部父類進行前述a和b步驟的處理;
若是屬性中有複雜類型,這還要對這些對象進行a、b、c步驟的處理;
記錄List、Set、Map等集合類的描述信息,同時要對key或value中的複雜對象進行a、b、c、d步驟的操做
可見,一個對象的序列化所須要作的工做是遞歸的,至關繁瑣,要記錄大量的描述信息,而咱們的Java源生序列化不但作了上邊全部的事情,並且還作的規規矩矩,甚至還「自做多情」的幫你加上了一些JVM執行時要用到的信息。
因此如今就是用腳都可以想明白,Java原生序列化幫你作了這麼多事情,它能不慢麼?並且還作得這麼規矩(迂腐?),結果能不大麼?
下面就基本是各個工具針對Java弱點的改進了。
Hessian的序列化實現和Java的原生序列化很類似,只是對於序列化反序列化自己並不須要的一些元數據進行了刪減;因此Hessian能夠像Java的源生序列化那樣,能夠支持任意類型的對象;可是在存儲上,Hessian並無作相應的優化,因此其生成的對象體積相較於Java的源生序列化並無降低太多;
好比,Hessian對於數值類型仍然使用了定長存儲,而在一般狀況下,常用的數據都是比較小的,大部分的存儲空間是被浪費掉的;
爲了標誌屬性區段的結束,Hessian使用了長度字段來表示,這在必定程度上會增大結果數據的體積;
因爲Hessian相較於Java源生序列化並無太大的優點,因此通常狀況下,若是系統中沒有使用Hessian的rpc框架,則不多單獨使用Hessian的序列化機制。
GPB最大的特色就是本身定義了一套本身數據類型,而且規定只容許用個人這套;因此在使用GPB的時候,咱們不得不爲它單獨定義一個描述文件,或者叫schema文件,用來完成Java對象中的基本數據類型和GPB本身定義的類型之間的一個映射;
不過也正是GPB對類型的自定義,也讓他能夠更好的針對這些類型作出存儲和解析上的優化,從而避免了Java源生序列化中的諸多弱點。
對於對象屬性,GPB並無直接存儲屬性名稱,而是根據schema文件中的映射關係,只保存該屬性的順序id;而對於,GPB針對經常使用的幾種數據類型採用了不一樣程度的壓縮,同時屬性區段之間採用特定標記進行分隔,這樣能夠大大減小存儲所佔用的空間。
對於數值類型,常見的壓縮方式有變長byte、分組byte、差值存儲等,通常都是根據屬性的使用特色來作定製化的壓縮策略。
GPB的另外一個優勢就是跨語言,支持Java、C、PHP、Python等目前比較大衆的語言;其餘相似的還有Facebook的Thrift,也須要描述文件的支持,同時也包含了一個rpc框架和更豐富的語言支持;
前面咱們提到,注入Hessian和GPB這些三方的序列化框架或多或少的都對Java原生序列化機制作出了一些改進;而對於Kryo來講,改進無疑是更完全一些;在不少評測中,Kryo的數據都是遙遙領先的;
Kryo的處理和Google Protobuf相似。但有一點須要說明的是,Kryo在作序列化時,也沒有記錄屬性的名稱,而是給每一個屬性分配了一個id,可是他卻並無GPB那樣經過一個schema文件去作id和屬性的一個映射描述,因此一但咱們修改了對象的屬性信息,好比說新增了一個字段,那麼Kryo進行反序列化時就可能發生屬性值錯亂甚至是反序列化失敗的狀況;並且因爲Kryo沒有序列化屬性名稱的描述信息,因此序列化/反序列化以前,須要先將要處理的類在Kryo中進行註冊,這一操做在首次序列化時也會消耗必定的性能。
另外須要提一下的就是目前kryo目前還只支持Java語言。
就Java原生序列化功能而言,雖然它性能和體積表現都很是差,可是從使用上來講倒是很是普遍,只要是使用Java的框架,那就能夠用Java原生序列化;誰讓人家是「親兒子」呢,即使是看在人家「爹」的份兒上,也得給人家幾分面子!
尤爲是在咱們須要序列化的對象類型有限,同時又對速度和體積有很高的要求的時候,咱們不妨試一下本身來處理對象的序列化;由於這樣咱們能夠根據要序列化對象的實際內容來決定具體如何去處理,甚至可使用一些取巧的方法,即便這些方法對其餘的對象類型並不適用;
有一點咱們能夠相信,就是咱們總能在特定的場景下設計出一個極致的方案!
關於更多序列化的信息,你們也能夠查看下jvm-serializers的評測。