learn.lianglianglee.com/专栏/消息队列高手课/12 序列化与反序列化:如何通过网络传输结构化的数据?.md.html
2022-05-11 18:52:13 +08:00

933 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>12 序列化与反序列化:如何通过网络传输结构化的数据?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/消息队列高手课/00 开篇词 优秀的程序员,你的技术栈中不能只有“增删改查”.md">00 开篇词 优秀的程序员,你的技术栈中不能只有“增删改查”.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/00 预习 怎样更好地学习这门课?.md">00 预习 怎样更好地学习这门课?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/01 为什么需要消息队列?.md">01 为什么需要消息队列?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/02 该如何选择消息队列?.md">02 该如何选择消息队列?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/03 消息模型:主题和队列有什么区别?.md">03 消息模型:主题和队列有什么区别?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/04 如何利用事务消息实现分布式事务?.md">04 如何利用事务消息实现分布式事务?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/05 如何确保消息不会丢失.md">05 如何确保消息不会丢失.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/06 如何处理消费过程中的重复消息?.md">06 如何处理消费过程中的重复消息?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/07 消息积压了该如何处理?.md">07 消息积压了该如何处理?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/08 答疑解惑(一) 网关如何接收服务端的秒杀结果?.md">08 答疑解惑(一) 网关如何接收服务端的秒杀结果?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/09 学习开源代码该如何入手?.md">09 学习开源代码该如何入手?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/10 如何使用异步设计提升系统性能?.md">10 如何使用异步设计提升系统性能?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/11 如何实现高性能的异步网络传输?.md">11 如何实现高性能的异步网络传输?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/消息队列高手课/12 序列化与反序列化:如何通过网络传输结构化的数据?.md">12 序列化与反序列化:如何通过网络传输结构化的数据?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/13 传输协议:应用程序之间对话的语言.md">13 传输协议:应用程序之间对话的语言.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/14 内存管理:如何避免内存溢出和频繁的垃圾回收?.md">14 内存管理:如何避免内存溢出和频繁的垃圾回收?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/15 Kafka如何实现高性能IO.md">15 Kafka如何实现高性能IO.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/16 缓存策略如何使用缓存来减少磁盘IO.md">16 缓存策略如何使用缓存来减少磁盘IO.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/17 如何正确使用锁保护共享数据,协调异步线程?.md">17 如何正确使用锁保护共享数据,协调异步线程?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/18 如何用硬件同步原语CAS替代锁.md">18 如何用硬件同步原语CAS替代锁.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/19 数据压缩:时间换空间的游戏.md">19 数据压缩:时间换空间的游戏.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/20 RocketMQ Producer源码分析消息生产的实现过程.md">20 RocketMQ Producer源码分析消息生产的实现过程.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/21 Kafka Consumer源码分析消息消费的实现过程.md">21 Kafka Consumer源码分析消息消费的实现过程.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/22 Kafka和RocketMQ的消息复制实现的差异点在哪.md">22 Kafka和RocketMQ的消息复制实现的差异点在哪.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/23 RocketMQ客户端如何在集群中找到正确的节点.md">23 RocketMQ客户端如何在集群中找到正确的节点.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/24 Kafka的协调服务ZooKeeper实现分布式系统的“瑞士军刀”.md">24 Kafka的协调服务ZooKeeper实现分布式系统的“瑞士军刀”.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/25 RocketMQ与Kafka中如何实现事务.md">25 RocketMQ与Kafka中如何实现事务.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/26 MQTT协议如何支持海量的在线IoT设备.md">26 MQTT协议如何支持海量的在线IoT设备.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/27 Pulsar的存储计算分离设计全新的消息队列设计思路.md">27 Pulsar的存储计算分离设计全新的消息队列设计思路.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/28 答疑解惑我的100元哪儿去了.md">28 答疑解惑我的100元哪儿去了.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/29 流计算与消息通过Flink理解流计算的原理.md">29 流计算与消息通过Flink理解流计算的原理.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/30 流计算与消息在流计算中使用Kafka链接计算任务.md">30 流计算与消息在流计算中使用Kafka链接计算任务.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/31 动手实现一个简单的RPC框架原理和程序的结构.md">31 动手实现一个简单的RPC框架原理和程序的结构.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/32 动手实现一个简单的RPC框架通信与序列化.md">32 动手实现一个简单的RPC框架通信与序列化.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/33 动手实现一个简单的RPC框架客户端.md">33 动手实现一个简单的RPC框架客户端.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/34 动手实现一个简单的RPC框架服务端.md">34 动手实现一个简单的RPC框架服务端.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/35 答疑解惑(三):主流消息队列都是如何存储消息的?.md">35 答疑解惑(三):主流消息队列都是如何存储消息的?.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/加餐 JMQ的Broker是如何异步处理消息的.md">加餐 JMQ的Broker是如何异步处理消息的.md.html</a>
</li>
<li>
<a href="/专栏/消息队列高手课/结束语 程序员如何构建知识体系?.md">结束语 程序员如何构建知识体系?.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>12 序列化与反序列化:如何通过网络传输结构化的数据?</h1>
<p>你好,我是李玥。</p>
<p>最近有一些同学留言说,感觉这几节课讲的内容和消息关系不大。这里我解释一下,因为我们课程其中的一个目的,是让同学们不仅会使用消息队列,还可以通过对消息队列的学习,在代码能力上有一个提升,具备“造轮子”的能力。这样,你对消息队列的理解才能足够的深入,而不只是浮于表面。如果你细心可能也会发现,很多大厂在面试时,提到消息队列的问题,也不会仅仅局限在消息队列的使用上,他更多的会考察“你为什么这么实现”。</p>
<p>所以在进阶篇的上半部分,我会把开发一个消息队列需要用到的一些底层的关键技术给大家讲解清楚,然后我们再来一起分析消息队列的源代码。</p>
<p>在上节课中,我们解决了如何实现高性能的网络传输的问题。那是不是程序之间就可以通信了呢?这里面还有一些问题需要解决。</p>
<p>我们知道,在 TCP 的连接上,它传输数据的基本形式就是二进制流,也就是一段一段的 1 和 0。在一般编程语言或者网络框架提供的 API 中,传输数据的基本形式是字节,也就是 Byte。一个字节就是 8 个二进制位8 个 Bit所以在这里二进制流和字节流本质上是一样的。</p>
<p>那对于我们编写的程序来说它需要通过网络传输的数据是什么形式的呢是结构化的数据比如一条命令、一段文本或者是一条消息。对应到我们写的代码中这些结构化的数据是什么这些都可以用一个类Class或者一个结构体Struct来表示。</p>
<p>那显然,**要想使用网络框架的 API 来传输结构化的数据,必须得先实现结构化的数据与字节流之间的双向转换。**这种将结构化数据转换成字节流的过程,我们称为序列化,反过来转换,就是反序列化。</p>
<p>序列化的用途除了用于在网络上传输数据以外,另外的一个重要用途是,将结构化数据保存在文件中,因为在文件内保存数据的形式也是二进制序列,和网络传输过程中的数据是一样的,所以序列化同样适用于将结构化数据保存在文件中。</p>
<p>很多处理海量数据的场景中,都需要将对象序列化后,把它们暂时从内存转移到磁盘中,等需要用的时候,再把数据从磁盘中读取出来,反序列化成对象来使用,这样不仅可以长期保存不丢失数据,而且可以节省有限的内存空间。</p>
<p>这节课,我们就来聊聊,怎么来实现高性能的序列化和反序列化。</p>
<h2>你该选择哪种序列化实现?</h2>
<p>如果说,只是实现序列化和反序列的功能,并不难,方法也有很多,比如我们最常使用的,把一个对象转换成字符串并打印出来,这其实就是一种序列化的实现,这个字符串只要转成字节序列,就可以在网络上传输或者保存在文件中了。</p>
<p>但是,你千万不要在你的程序中这么用,这种实现的方式仅仅只是能用而已,绝不是一个好的选择。</p>
<p>有很多通用的序列化实现我们可以直接拿来使用。Java 和 Go 语言都内置了序列化实现也有一些流行的开源序列化实现比如Google 的 Protobuf、Kryo、Hessian 等;此外,像 JSON、XML 这些标准的数据格式,也可以作为一种序列化实现来使用。</p>
<p>当然,我们也可以自己来实现私有的序列化实现。</p>
<p>面对这么多种序列化实现,我们该如何选择呢?你需要权衡这样几个因素:</p>
<ol>
<li>序列化后的数据最好是易于人类阅读的;</li>
<li>实现的复杂度是否足够低;</li>
<li>序列化和反序列化的速度越快越好;</li>
<li>序列化后的信息密度越大越好,也就是说,同样的一个结构化数据,序列化之后占用的存储空间越小越好;</li>
</ol>
<p>当然,<strong>不会存在一种序列化实现在这四个方面都是最优的</strong>,否则我们就没必要来纠结到底选择哪种实现了。因为,大多数情况下,易于阅读和信息密度是矛盾的,实现的复杂度和性能也是互相矛盾的。所以,我们需要根据所实现的业务,来选择合适的序列化实现。</p>
<p>像 JSON、XML 这些序列化方法,可读性最好,但信息密度也最低。像 Kryo、Hessian 这些通用的二进制序列化实现,适用范围广,使用简单,性能比 JSON、XML 要好一些,但是肯定不如专用的序列化实现。</p>
<p>对于一些强业务类系统,比如说电商类、社交类的应用系统,这些系统的特点是,业务复杂,需求变化快,但是对性能的要求没有那么苛刻。这种情况下,我推荐你使用 JSON 这种实现简单,数据可读性好的序列化实现,这种实现使用起来非常简单,序列化后的 JSON 数据我们都可以看得懂,无论是接口调试还是排查问题都非常方便。付出的代价就是多一点点 CPU 时间和存储空间而已。</p>
<p>比如我们要序列化一个 User 对象,它包含 3 个属性,姓名 zhangsan年龄23婚姻状况已婚。</p>
<pre><code>User:
name: &quot;zhangsan&quot;
age: 23
married: true
</code></pre>
<p>使用 JSON 序列化后:</p>
<pre><code>{&quot;name&quot;:&quot;zhangsan&quot;,&quot;age&quot;:&quot;23&quot;,&quot;married&quot;:&quot;true&quot;}
</code></pre>
<p>这里面的数据我们不需要借助工具,是直接可以看懂的。</p>
<p>序列化的代码也比较简单,直接调用 JSON 序列化框架提供的方法就可以了:</p>
<pre><code>byte [] serializedUser = JsonConvert.SerializeObject(user).getBytes(&quot;UTF-8&quot;);
</code></pre>
<p>如果 JSON 序列化的性能达不到你系统的要求,可以采用性能更好的二进制序列化实现,实现的复杂度和 JSON 序列化是差不多的,都很简单,但是序列化性能更好,信息密度也更高,代价就是失去了可读性。</p>
<p>比如我们用 Kryo 来序列化 User 对象,它的代码如下:</p>
<pre><code>kryo.register(User.class);
Output output = new Output(new FileOutputStream(&quot;file.bin&quot;));
kryo.writeObject(output, user);
</code></pre>
<p>在这段代码里,先要向 Kryo 注册一下 User 类,然后创建一个流,最后调用 writeObject 方法,将 user 对象序列化后直接写到流中。这个过程也是非常简单的。</p>
<h2>实现高性能的序列化和反序列化</h2>
<p>绝大部分系统,使用上面这两类通用的序列化实现都可以满足需求,而像消息队列这种用于解决通信问题的中间件,它对性能要求非常高,通用的序列化实现达不到性能要求,所以,很多的消息队列都选择自己实现高性能的专用序列化和反序列化。</p>
<p>使用专用的序列化方法,可以提高序列化性能,并有效减小序列化后的字节长度。</p>
<p>在专用的序列化方法中,不必考虑通用性。比如,我们可以固定字段的顺序,这样在序列化后的字节里面就不必包含字段名,只要字段值就可以了,不同类型的数据也可以做针对性的优化:</p>
<p>对于同样的 User 对象,我们可以把它序列化成这样:</p>
<pre><code>03 | 08 7a 68 61 6e 67 73 61 6e | 17 | 01
User | z h a n g s a n | 23 | true
</code></pre>
<p>我解释一下,这个序列化方法是怎么表示 User 对象的。</p>
<p>首先我们需要标识一下这个对象的类型,这里面我们用一个字节来表示类型,比如用 03 表示这是一个 User 类型的对象。</p>
<p>我们约定,按照 name、age、married 这个固定顺序来序列化这三个属性。按照顺序,第一个字段是 name我们不存字段名直接存字段值“zhangsan”就可以了由于名字的长度不固定我们用第一个字节 08 表示这个名字的长度是 8 个字节,后面的 8 个字节就是 zhangsan。</p>
<p>第二个字段是年龄我们直接用一个字节表示就可以了23 的 16 进制是 17 。</p>
<p>最后一个字段是婚姻状态我们用一个字节来表示01 表示已婚00 表示未婚,这里面保存一个 01。</p>
<p>可以看到,同样的一个 User 对象JSON 序列化后需要 47 个字节,这里只要 12 个字节就够了。</p>
<p>专用的序列化方法显然更高效,序列化出来的字节更少,在网络传输过程中的速度也更快。但缺点是,需要为每种对象类型定义专门的序列化和反序列化方法,实现起来太复杂了,大部分情况下是不划算的。</p>
<h2>小结</h2>
<p>进程之间要通过网络传输结构化的数据,需要通过序列化和反序列化来实现结构化数据和二进制数据的双向转换。在选择序列化实现的时候,需要综合考虑数据可读性,实现复杂度,性能和信息密度这四个因素。</p>
<p>大多数情况下,选择一个高性能的通用序列化框架都可以满足要求,在性能可以满足需求的前提下,推荐优先选择 JSON 这种可读性好的序列化方法。</p>
<p>如果说我们需要超高的性能,或者是带宽有限的情况下,可以使用专用的序列化方法,来提升序列化性能,节省传输流量。不过实现起来很复杂,大部分情况下并不划算。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/消息队列高手课/11 如何实现高性能的异步网络传输?.md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/消息队列高手课/13 传输协议:应用程序之间对话的语言.md">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099795f7fe93cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>