分佈式系統的基石序列化筆記

瞭解序列化的意義java

  • Java 平臺容許咱們在內存中建立可複用的Java 對象,
  • 但通常狀況下,只有當JVM 處於運行時,這些對象纔可能存在,即,這些對象的生命週期不會比JVM 的生命週期更長。
  • 但在現實應用中,就可能要求在JVM中止運行以後可以保存(持久化)指定的對象,並在未來從新讀取被保存的對象。
  • Java 對象序列化就可以幫助咱們實現該功能
  • 簡單來講:
    • 序列化是把對象的狀態信息轉化爲可存儲傳輸的形式過程,也就是把對象轉化爲字節序列的過程稱爲對象的序列化
    • 反序列化是序列化的逆向過程,把字節數組反序列化爲對象,把字節序列恢復爲對象的過程成爲對象的反序列化

評價一個序列化算法優劣的兩個重要指標是:git

  • 序列化之後的數據大小
  • 序列化操做自己的速度及系統資源開銷(CPU、內存)

Java 語言自己提供了對象序列化機制,也是Java 語言自己最重要的底層機制之一,github

  • Java 自己提供的序列化機制存在兩個問題:
    • 序列化的數據比較大,傳輸效率低
    • 其餘語言沒法識別和對接

在Java 中,只要一個類實現了java.io.Serializable 接口,那麼它就能夠被序列化算法

  • 基於JDK 序列化方式實現
    • JDK 提供了Java 對象的序列化方式, 主要經過輸出流java.io.ObjectOutputStream 和對象輸入流java.io.ObjectInputStream來實現。
    • 被序列化的對象須要實現java.io.Serializable 接口。

序列化的高階認識:json

  • serialVersionUID 的做用
    • Java 的序列化機制是經過判斷類的serialVersionUID 驗證版本一致性的。
      • 在進行反序列化時,JVM 會把傳來的字節流中的serialVersionUID本地相應實體類的serialVersionUID 進行比較,
      • 若是相同就認爲是一致的,能夠進行反序列化,不然就會出現序列化版本不一致的異常,便是InvalidCastException
      • 若是沒有爲指定的class 配置serialVersionUID,那麼java 編譯器會自動給這個class 進行一個摘要算法
        • 相似於指紋算法,只要這個文件有任何改動,獲得的UID 就會大相徑庭的,能夠保證在這麼多類中,這個編號是惟一的
  • serialVersionUID 有兩種顯示的生成方式:
    • 一是默認的1L,好比:private static final long serialVersionUID = 1L;
    • 二是根據類名、接口名、成員方法及屬性等來生成一個64 位的哈希字段
  • 當實現java.io.Serializable 接口的類沒有顯式地定義一個serialVersionUID 變量時候:
    • Java 序列化機制會根據編譯的Class 自動生成一個serialVersionUID 做序列化版本比較用
      • 這種狀況下,若是Class 文件(類名,方法明等)沒有發生變化(增長空格,換行,增長註釋等等)
      • 就算再編譯屢次,serialVersionUID 也不會變化的
  • 靜態變量序列化
    • 序列化並不保存靜態變量

  • 父類的序列化
    • 一個子類實現了 Serializable 接口,它的父類都沒有實現 Serializable接口
      • 在子類中設置父類的成員變量的值,接着序列化該子類對象。
      • 再反序列化出來之後輸出父類屬性的值。結果應該是什麼?
        • 以下,結論:
          1. 當一個父類沒有實現序列化時,子類繼承該父類而且實現了序列化。
            • 在反序列化該子類後,是沒辦法獲取到父類的屬性值的
          2. 當一個父類實現序列化,子類自動實現序列化,不須要再顯示實現Serializable 接口
          3. 當一個對象的實例變量引用了其餘對象,序列化該對象時也會把引用對象進行序列化
            • 可是前提是該引用對象必須實現序列化接口

Transient 關鍵字:後端

  • Transient 關鍵字的做用是控制變量的序列化,
    • 在變量聲明前加上該關鍵字,能夠阻止該變量被序列化到文件中
    • 在被反序列化後,transient變量的值被設爲初始值,如 int 型的是 0,對象型的是 null
  • 繞開transient 機制的辦法
    • writeObject和readObject 這兩個私有的方法,既不屬於Object、也不是Serializable,爲何可以在序列化的時候被調用呢?
    • 緣由是,ObjectOutputStream使用了反射來尋找是否聲明瞭這兩個方法。
    • 由於ObjectOutputStream使用getPrivateMethod,因此這些方法必須聲明爲private 以致於供ObjectOutputStream 來使用

序列化的存儲規則api

  • 同一對象兩次(開始寫入文件到最終關閉流這個過程算一次,下面的演示效果是不關閉流的狀況才能演示出效果)寫入文件
  • 打印出寫入一次對象後的存儲大小和寫入兩次後的存儲大小,第二次寫入對象時文件只增長了 5 字節
  • Java 序列化機制爲了節省磁盤空間,具備特定的存儲規則
  • 當寫入文件的爲同一對象時,並不會再將對象的內容進行存儲,而只是再次存儲一份引用
  • 該存儲規則極大的節省了存儲空間

  • 序列化實現深克隆
    • 在Java 中存在一個Cloneable 接口,經過實現這個接口的類都會具有clone 的能力
    • 同時clone 是在內存中進行在性能方面會比咱們直接經過new 生成對象要高一些
      • 特別是一些大的對象的生成,性能提高相對比較明顯
  • 淺克隆
    • 被複制對象的全部變量都含有與原來的對象相同的值,而全部的對其餘對象的引用仍然指向原來的對象。
    • 新老對象指向同一個堆內存,改變其中一個另外一個也會隨之改變,顯然大多數狀況下這不是咱們想要的
  • 深克隆
    • 被複制對象的全部變量都含有與原來的對象相同的值,除去那些引用其餘對象的變量
    • 深拷貝把要複製的對象所引用的對象都複製了一遍
    • 使用序列化實現深拷貝
      • 原理是把對象序列化輸出到一個流中,而後在把對象從序列化流中讀取出來,這個對象就不是原來的對象了。

常見的序列化技術數組

  • JAVA 進行序列化有他的優勢,也有他的缺點:
    • 優勢:JAVA 語言自己提供,使用比較方便和簡單
    • 缺點:不支持跨語言處理、 性能相對不是很好,序列化之後產生的數據相對較大

XML 序列化框架數據結構

  • XML 序列化的好處在於可讀性好,方便閱讀和調試
  • 可是序列化之後的字節碼文件比較大,並且效率不高,適用於對性能不高
  • 並且QPS 較低的企業級內部系統之間的數據交換的場景,同時XML 又具備語言無關性
  • 因此還能夠用於異構系統之間的數據交換和協議。
  • 好比咱們熟知的Webservice,就是採用XML 格式對數據進行序列化的

JSON 序列化框架架構

  • JSON(JavaScript Object Notation)是一種輕量級的數據交換格式
  • 相對於XML 來講,JSON 的字節流更小,並且可讀性也很是好
  • 如今JSON數據格式在企業運用是最廣泛的

JSON 序列化經常使用的開源工具備不少

Hessian 序列化框架

  • Hessian 是一個支持跨語言傳輸的二進制序列化協議,
  • 相對於Java 默認的序列化機制來講,Hessian 具備更好的性能和易用性,並且支持多種不一樣的語言
    • 實際上Dubbo 採用的就是Hessian 序列化來實現,只不過Dubbo 對Hessian 進行了重構,性能更高

Protobuf 序列化框架

  • Protobuf 是Google 的一種數據交換格式,它獨立於語言、獨立於平臺
  • Protobuf 使用比較普遍,主要是空間開銷小性能比較好,很是適合用於公司內部對性能要求高的RPC 調用
  • 另外因爲解析性能比較高,序列化之後數據量相對較少,因此也能夠應用在對象的持久化場景中
  • 可是要使用Protobuf 會相對來講麻煩些由於他有本身的語法,有本身的編譯器

下載protobuf 工具

  • https://github.com/google/protobuf/releases
  • proto 的語法
    • 1. 包名
    • 2. option 選項
    • 3. 消息模型(消息對象、字段(字段修飾符-required/optional/repeated)字段類型(基本數據類型、枚舉、消息對象)、字段名、標識號)
syntax="proto2";
package com.gupaoedu.serial;
option java_package = "com.gupaoedu.serial";
option java_outer_classname="UserProtos";
message User {
required string name=1;
required int32 age=2;
}

Protobuf 原理分析

  • 核心原理: protobuf 使用varint(zigzag)做爲編碼方式, 使用T-LV做爲存儲方式

varint 編碼方式

  • varint 是一種數據壓縮算法,其核心思想是利用bit 位來實現數據壓縮
  • 好比:
    • 對於 int32 類型的數字,通常須要 4 個字節 表示;
    • 若採用Varint 編碼,對於很小的 int32 類型 數字,則能夠用 1 個字節
  • 假設咱們定義了一個int32 字段值=296:
    • 第一步,轉化爲2 進制編碼
    • 第二步,提取字節
      • 規則: 按照從字節串末尾選取7 位,並在最高位補1,構成一個字節
    • 第三步,繼續提取字節
      • 總體右移7 位,繼續截取7 個比特位,而且在最高位補0 。
      • 由於這個是最後一個有意義的字節了。補0 不影響結果
    • 第四步,拼接成一個新的字節串
      • 將原來用4 個字節表示的整數,通過varint 編碼之後只須要2 個字節了。
      • varint 編碼對於小於127 的數,能夠最大化的壓縮
  • varint 壓縮小數據
    • 好比咱們壓縮一個var32 = 104 的數據
    • 第一步,轉換爲2 進制編碼
    • 第二步,提取字節
      • 從末尾開始提取7 個字節而且在最高位最高位補0,由於這個是最後的7 位。
    • 第三步,造成新的字節
      • 也就是經過varint 對於小於127 如下的數字編碼,只須要佔用1 個字節。
  • zigzag 編碼方式
    • 對於負數的處理,protobuf 使用zigzag 的形式來存儲。
  • 在計算機中,定義了原碼、反碼和補碼。來實現負數的表示。
    • 數字 8 的二進制表示爲 0000 1000
    • 原碼
      • 經過第一個位表示符號(0 表示非負數、1 表示負數)
      • (+8) = {0000 1000}
        (-8) = {1000 1000}
    • 反碼
      • 由於第一位表示符號位,保持不變
      • 剩下的位,非負數保持不變、負數按位取反
        • (+8) = {0000 1000}原 ={0000 1000}反 非負數,剩下的位不變。因此和原碼是保持一致
        • (-8) = {1000 1000}原 ={1111 0111}反 負數,符號位不動,剩下爲取反
    • 可是經過原碼和反碼方式來表示二進制還存在一些問題
      • 第一個問題:
        • 0 這個數字,按照上面的反碼計算,會存在兩種表示
        • (+0) ={0000 0000}原= {0000 0000}反
          (-0) ={1000 0000}原= {1111 1111}反
      • 第二個問題:
        • 符號位參與運算,會獲得一個錯誤的結果,好比
          1 + (-1)=
        • {0000 0001}原 +{1 0000 0001}原 ={1000 0010}原 =-2
          {0000 0001}反+ {1111 1110}反 = {1111 1111}反 =-0
      • 無論是原碼計算仍是反碼計算。獲得的結果都是錯誤的。因此爲了解決這個問題,引入了補碼的概念
    • 補碼
      • 補碼的概念:第一位符號位保持不變,剩下的位非負數保持不變負數按位取反且末位加1
      • (+8) = {0000 1000}原 = {0000 1000}原 ={0000 1000}補
        (-8) = {1000 1000}原 ={1111 0111}反={1111 1000}末位加一(補碼)
      • 8+(-8)= {0000 1000}補 +{1111 1000}末位加一(補碼) ={0000 0000}=0
    • 經過補碼的方式,在進行符號運算的時候,計算機就不須要關心符號的問題,統一按照這個規則來計算。就沒問題
  • zigzag 原理
    • 好比咱們存儲一個 int32 = -2
      • 原碼{1 000 0010} ->取反 {1111 1101} ->總體加1 {111 1110}->{1111 1110}
      • zigzag 的核心思想是去掉無心義的0最大可能性的壓縮數據
        • 對於負數,第一位表示符號位,若是補碼的話,前面只能補1.
        • 就會致使陷入一個很尷尬的地步,負數彷佛沒辦法壓縮。
      • 因此zigzag 提供了一個方法,既然第一位是符號位,那麼乾脆把這個符號位放到補碼的最後
      • 因此上面這個-2,將符號位移到最末尾,
    • zigzag 算法定義了對於非負數形式,則把符號位移動到最後,其餘總體往左移動一位。
      • 對於非負數形式2,按照總體左移1 位,右邊補零的形式來表示以下
    • 而在zigzag 中的計算規則是:
      • 將-2 的二進制形式{1111 1110}按照正數的算法,左移一位,右邊補零獲得{11111100},以下圖左邊。
      • 按照負數的形式,講符號位移動到最右邊,右移31 位,獲得下面右圖。
      • 再將二者取異或算法。實現最終的壓縮。

  • 最後,-2 在的結果是3. 佔用一個比特位存儲。
  • 就是最大限度的去掉多餘的零,創造多餘零,壓縮算法

存儲方式

  • 存儲方式通過編碼之後的數據,大大減小了字段值的佔用字節數,而後基於T-LV的方式進行存儲
  • tag 的取值爲 field_number(字段數) << 3 | wire_type

Protocol總結:

  • Protocol Buffer 的性能好,主要體如今 序列化後的數據體積小 & 序列化速度快,最終使得傳輸效率高
  • 其緣由以下:
    • 序列化速度快的緣由:
      • 編碼 / 解碼 方式簡單(只須要簡單的數學運算 = 位移等等)
      • 採用 Protocol Buffer 自身的框架代碼 和 編譯器 共同完成
    • 序列化後的數據量體積小(即數據壓縮效果好)的緣由:
      • 採用了獨特的編碼方式,如Varint、Zigzag 編碼方式等等
      • 採用T - L - V 的數據存儲方式:減小了分隔符的使用 & 數據存儲得緊湊

序列化技術的選型

  • 技術層面
    1. 序列化空間開銷,也就是序列化產生的結果大小,這個影響到傳輸的性能
    2. 序列化過程當中消耗的時長,序列化消耗時間過長影響到業務的響應時間
    3. 序列化協議是否支持跨平臺,跨語言。由於如今的架構更加靈活,若是存在異構系統通訊需求,那麼這個是必需要考慮的
    4. 可擴展性/兼容性,在實際業務開發中,系統每每須要隨着需求的快速迭代來實現快速更新,
      • 這就要求咱們採用的序列化協議基於良好的可擴展性/兼容性,
      • 好比在現有的序列化數據結構中新增一個業務字段,不會影響到現有的服務
    5. 技術的流行程度,越流行的技術意味着使用的公司多,那麼不少坑都已經淌過而且獲得瞭解決,技術解決方案也相對成熟
    6. 學習難度和易用性
  • 選型建議
    1. 對性能要求不高的場景,能夠採用基於XML 的SOAP 協議
    2. 對性能和間接性有比較高要求的場景,那麼Hessian、Protobuf、Thrift、Avro 均可以。
    3. 基於先後端分離,或者獨立的對外的api 服務,選用JSON 是比較好的,對於調試、可讀性都很不錯
    4. Avro 設計理念偏於動態類型語言,那麼這類的場景使用Avro 是能夠的
相關文章
相關標籤/搜索