C++編程經驗(10):無鎖編程其實沒那么玄乎
890
2025-04-01
很多大廠面試MQ問題,不會局限在使用,更多考察實現原理。
TCP連接傳輸數據的基本形式二進制流。一般編程語言或網絡框架提供的API中,傳輸數據的基本形式是字節。二進制流和字節流本質上其實是一樣的。
而對于我們編寫的程序,需要通過網絡傳輸的數據是結構化的數據形式:比如,一條命令、一段文本或者是一條消息,都可用類表示。
因此要想使用網絡框架API傳輸結構化數據,必須實現結構化數據與字節流間的轉換。
結構化數據轉換成字節流,稱為序列化,反之就是反序列化。
序列化除了用于在網絡上傳輸數據,另外一個重要用途是將結構化數據保存在文件,因為在文件內保存數據的形式也是二進制,和網絡傳輸過程中的數據本質是一樣的。
很多海量數據場景,都需將對象序列化后,把它們暫時從內存轉移到磁盤,等要用時,再把數據從磁盤中讀取出來,反序列化成對象來使用,這樣不僅可以長期保存不丟失數據,而且可以節省有限的內存空間。
怎么來實現高性能的序列化和反序列化呢。
1 序列化技術選型
只是實現序列化和反序列功能,方法有很多,最常用的直接把一個對象轉換成字符串打印,就是一種序列化實現,字符串只要轉成字節序列,就可在網絡上傳輸或保存在文件。但這種實現的方式僅是能用。
有很多通用序列化實現,可直接拿來用。Java和Go語言都內置了序列化實現,也有一些流行的開源序列化實現,比如,
Google 的Protobuf、Kryo、Hessian
像JSON、XML這些標準的數據格式,也可以作為一種序列化實現
也可以自己來實現私有的序列化實現。
如何選型?
序列化后的數據最好是易于閱讀
實現的復雜度是否低
序列化和反序列化的速度越快越好
序列化后的信息密度越大越好,即同樣的一個結構化數據,序列化之后占用的存儲空間越小越好
當然,不會存在一種序列化實現在這四個方面都是最優的。因為易于閱讀和信息密度總是矛盾的,實現復雜度和性能也是矛盾。還是根據業務需求選擇合適的序列化實現。
JSON、XML這些序列化方法,可讀性最好,但信息密度最低
Kryo、Hessian這些通用二進制序列化實現,適用范圍廣,使用簡單,性能比JSON、XML要好,但不如專用的序列化實現
對強業務類系統,比如電商、社交類,特點是業務復雜,需求變化快,但對性能要求沒那么苛刻。推薦使用JSON這種實現簡單,數據可讀性好的序列化實現,這種實現使用起來非常簡單,序列化后的JSON數據我們都可以看得懂,無論是接口調試還是排查問題都非常方便。付出的代價就是多點CPU時間和存儲空間。
比如序列化一個User對象,它包含3個屬性,姓名zhangsan,年齡:23,婚姻狀況:已婚。
User: name: "zhangsan" age: 23 married: true
1
2
3
4
使用JSON序列化后:
{"name":"zhangsan","age":"23","married":"true"}
1
數據可直接看懂。
序列化的代碼也較簡單,直接調用JSON序列化框架提供的方法即可:
byte [] serializedUser = JsonConvert.SerializeObject(user).getBytes("UTF-8");
1
2
若JSON序列化性能達不到業務要求,可采用性能更好的二進制序列化,實現的復雜度和JSON序列化差不多,都很簡單,但序列化性能更好,信息密度也更高,只是失去可讀性。
比如Kryo序列化User對象:
kryo.register(User.class); Output output = new Output(new FileOutputStream("file.bin")); kryo.writeObject(output, user);
1
2
3
向Kryo注冊一下User類,然后創建一個流,最后調用writeObject方法,將user對象序列化后直接寫到流中。過程非常簡單。
2 壓榨序列化和反序列化的性能
絕大部分系統,使用上面這兩類通用序列化實現都可滿足需求,而像MQ這種解決通信問題的中間件,對性能要求非常高,通用序列化實現達不到性能要求,所以,很多MQ自己實現專用序列化和反序列化。
使用專用的序列化方法,可提高序列化性能,并有效減小序列化后的字節長度。
在專用序列化方法中,不必考慮通用性。比如,可固定字段順序,這樣在序列化后的字節里面就不必包含字段名,只要字段值即可,不同類型數據也可做針對性優化:
對于同樣的User對象,我們可以把它序列化成:
03 | 08 7a 68 61 6e 67 73 61 6e | 17 | 01 User | z h a n g s a n | 23 | true
1
2
這神奇的序列化方法是怎么表示User對象的呢。
首先需要標識對象類型,這里用個字節來表示類型,比如03表示User類型對象。
約定按name、age、married固定順序序列化這三屬性。
按順序,第一個字段name,不存字段名,直接存字段值“zhangsan”即可,由于名字長度不固定,第一個字節08表該名字長度8個字節,緊隨其后的8字節即zhangsan。
第二個字段年齡,一個字節即可,23的16進制17 。
最后的字段狀態,一個字節:01已婚,00未婚。
可以看到,同樣的一個User對象,JSON序列化后需要47個字節,這里只要12個字節就夠了。
專用序列化方法更高效,序列化出字節更少,在網絡傳輸過程速度更快。
但缺點是要為每種對象類型定義專門的序列化和反序列化方法,實現起來復雜,大部分情況下都不劃算。
3 總結
進程之間要通過網絡傳輸結構化的數據,需通過序列化反序列化實現結構化數據和二進制數據轉換。選型要綜合考慮數據可讀性,實現復雜度,性能和信息密度這幾點。
大多數情況下,選擇一個高性能的通用序列化框架都可以滿足要求,在性能可以滿足需求的前提下,推薦優先選擇JSON這可讀性好的序列化方法。
如果說需要超高性能或帶寬有限,可使用專用序列化方法,提升序列化性能,節省傳輸流量。不過實現復雜,大部分情況下并不劃算。
4 面試場景快問快答
在內存里存放的任何數據,最基礎的存儲單元也是二進制,即應用程序操作的對象,在內存中也是二進制存儲的,既都是二進制,為什不直接把內存中對象對應的二進制數據直接通過網絡發出去或保存在文件?為什么還需要序列化和反序列化呢?
內存中的對象數據應該具有語言獨特性,例如表達相同業務的User對象(id/name/age字段),Java和PHP在內存中的數據格式應該不一樣的,如果直接用內存中的數據,可能會造成語言不通。
通常兩個服務之間沒有嚴格要求語言必須一致,只要對序列化的數據格式進行了協商,任何2個語言直接都可以進行序列化傳輸、接收。
雖然都是二進制的數據,但是序列化的二進制數據是通過一定的協議將數據字段進行拼接。第一個優勢是:不同的語言都可以遵循這種協議進行解析,實現了跨語言。第二個優勢是:這種數據可以直接持久化到磁盤,從磁盤讀取后也可以通過這個協議解析出來。如果是內存中的數據不能直接存盤的,直接存盤后再讀出來我們根本無法辨識這是個什么數據。
本地應用通過基地址+偏移量的方式訪問內存中的成員變量,因為它知道偏移量代表什么。通過網絡發給對方的話,對方并不知道偏移量代表什么,所以無法解析。
舉一個例子,如果直接取LinkedList在內中的字節流作為序列化結果,那么只能包含一個頭節點和一個子節點,必須使用LinkedList自己實現的writeObject方法進行序列化。
面臨問題:
網絡字節序與主機字節序問題,業務要感知和處理大小端問題
平臺差異,各平臺對基本數據類型的長度定義不一致、結構體對齊策略不一致,不同os有大小端存儲之分,無法實現平臺兼容
連續內存問題,一個對象可能引用,指向其他對象,指針就是一個地址,傳輸后在另外的設備上是無效值。數據在內存中大多以地址鏈接的離散數據為主,且因為內存里的對象不是一串連續的字節流,而是通過地址相互引用,比如map,其值是一個地址,表示值在哪里,而不是值本身。
如果解決這些問題了,也變相實現了自己的序列化框架。
一個c/s的架構應用,需要實現client之間的點對點數據通信以及群組通信 實際上就是一個即時通訊應用 由于沒有即時通訊相關的經驗 還請老師能夠指導一下。其中的數據傳輸使用MQ轉發 還是基于netty自定義?自己兩種方式都琢磨了一下,基于MQ的話,topic tag會很多 只要涉及一端client 操作(比如:打開某個界面)需要同步到其他client的話 就需要對topic進行生產以及訂閱消費。 第二就是基于netty自定義,這種情況下c端和s端都要定義一個類似servlet或者springmvc里面的dispatcher根據相關參數分發到具體的業務方法
一般來說,即時通信類系統并不適合用消息隊列來實現。很多即時通訊軟件都是使用一些P2P技術,數據直接點對點傳輸,不經過服務端轉發的。
JSON 網絡
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。