写点什么

序列化和反序列化

2015 年 5 月 07 日

简介

文章作者服务于美团推荐与个性化组,该组致力于为美团用户提供每天 billion 级别的高质量个性化推荐以及排序服务。从 Terabyte 级别的用户行为数据,到 Gigabyte 级别的 Deal/Poi 数据;从对实时性要求毫秒以内的用户实时地理位置数据,到定期后台 job 数据,推荐与重排序系统需要多种类型的数据服务。推荐与重排序系统客户包括各种内部服务、美团客户端、美团网站。为了提供高质量的数据服务,为了实现与上下游各系统进行良好的对接,序列化和反序列化的选型往往是我们做系统设计的一个重要考虑因素。

本文内容按如下方式组织:

  • 第一部分给出了序列化和反序列化的定义,以及其在通讯协议中所处的位置;
  • 第二部分从使用者的角度探讨了序列化协议的一些特性;
  • 第三部分描述在具体的实施过程中典型的序列化组件,并与数据库组建进行了类比;
  • 第四部分分别讲解了目前常见的几种序列化协议的特性,应用场景,并对相关组件进行举例;
  • 最后一部分,基于各种协议的特性,以及相关 benchmark 数据,给出了作者的技术选型建议。

一、定义以及相关概念

互联网的产生带来了机器间通讯的需求,而互联通讯的双方需要采用约定的协议,序列化和反序列化属于通讯协议的一部分。通讯协议往往采用分层模型,不同模型每层的功能定义以及颗粒度不同,例如:TCP/IP 协议是一个四层协议,而 OSI 模型却是七层协议模型。在 OSI 七层协议模型中展现层(Presentation Layer)的主要功能是把应用层的对象转换成一段连续的二进制串,或者反过来,把二进制串转换成应用层的对象–这两个功能就是序列化和反序列化。一般而言,TCP/IP 协议的应用层对应与 OSI 七层协议模型的应用层,展示层和会话层,所以序列化协议属于 TCP/IP 协议应用层的一部分。本文对序列化协议的讲解主要基于 OSI 七层协议模型。

  • 序列化: 将数据结构或对象转换成二进制串的过程。
  • 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。

数据结构、对象与二进制串

不同的计算机语言中,数据结构,对象以及二进制串的表示方式并不相同。

数据结构和对象:对于类似 Java 这种完全面向对象的语言,工程师所操作的一切都是对象(Object),来自于类的实例化。在 Java 语言中最接近数据结构的概念,就是 POJO(Plain Old Java Object)或者 Javabean--那些只有 setter/getter 方法的类。而在 C 二进制串:序列化所生成的二进制串指的是存储在内存中的一块数据。C 语言的字符串可以直接被传输层使用,因为其本质上就是以’0’结尾的存储在内存中的二进制串。在 Java 语言里面,二进制串的概念容易和 String 混淆。实际上 String 是 Java 的一等公民,是一种特殊对象(Object)。对于跨语言间的通讯,序列化后的数据当然不能是某种语言的特殊数据类型。二进制串在 Java 里面所指的是 byte[],byte 是 Java 的 8 中原生数据类型之一(Primitive data types)。

二、序列化协议特性

每种序列化协议都有优点和缺点,它们在设计之初有自己独特的应用场景。在系统设计的过程中,需要考虑序列化需求的方方面面,综合对比各种序列化协议的特性,最终给出一个折衷的方案。

通用性

通用性有两个层面的意义。

  1. 技术层面,序列化协议是否支持跨平台、跨语言。如果不支持,在技术层面上的通用性就大大降低了。
  2. 流行程度,序列化和反序列化需要多方参与,很少人使用的协议往往意味着昂贵的学习成本;另一方面,流行度低的协议,往往缺乏稳定而成熟的跨语言、跨平台的公共包。

强健性 / 鲁棒性

以下两个方面的原因会导致协议不够强健。

  1. 成熟度不够,一个协议从制定到实施,到最后成熟往往是一个漫长的阶段。协议的强健性依赖于大量而全面的测试,对于致力于提供高质量服务的系统,采用处于测试阶段的序列化协议会带来很高的风险。
  2. 语言 / 平台的不公平性。为了支持跨语言、跨平台的功能,序列化协议的制定者需要做大量的工作;但是,当所支持的语言或者平台之间存在难以调和的特性的时候,协议制定者需要做一个艰难的决定–支持更多人使用的语言 / 平台,亦或支持更多的语言 / 平台而放弃某个特性。当协议的制定者决定为某种语言或平台提供更多支持的时候,对于使用者而言,协议的强健性就被牺牲了。

可调试性 / 可读性

序列化和反序列化的数据正确性和业务正确性的调试往往需要很长的时间,良好的调试机制会大大提高开发效率。序列化后的二进制串往往不具备人眼可读性,为了验证序列化结果的正确性,写入方不得同时撰写反序列化程序,或提供一个查询平台–这比较费时;另一方面,如果读取方未能成功实现反序列化,这将给问题查找带来了很大的挑战–难以定位是由于自身的反序列化程序的 bug 所导致还是由于写入方序列化后的错误数据所导致。对于跨公司间的调试,由于以下原因,问题会显得更严重。

  1. 支持不到位,跨公司调试在问题出现后可能得不到及时的支持,这大大延长了调试周期。
  2. 访问限制,调试阶段的查询平台未必对外公开,这增加了读取方的验证难度。

如果序列化后的数据人眼可读,这将大大提高调试效率, XML 和 JSON 就具有人眼可读的优点。

性能

性能包括两个方面,时间复杂度和空间复杂度。

  1. 空间开销(Verbosity), 序列化需要在原有的数据上加上描述字段,以为反序列化解析之用。如果序列化过程引入的额外开销过高,可能会导致过大的网络,磁盘等各方面的压力。对于海量分布式存储系统,数据量往往以 TB 为单位,巨大的的额外空间开销意味着高昂的成本。
  2. 时间开销(Complexity),复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈。

可扩展性 / 兼容性

移动互联时代,业务系统需求的更新周期变得更快,新的需求不断涌现,而老的系统还是需要继续维护。如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。

安全性 / 访问限制

在序列化选型的过程中,安全性的考虑往往发生在跨局域网访问的场景。当通讯发生在公司之间或者跨机房的时候,出于安全的考虑,对于跨局域网的访问往往被限制为基于 HTTP/HTTPS 的 80 和 443 端口。如果使用的序列化协议没有兼容而成熟的 HTTP 传输层框架支持,可能会导致以下三种结果之一:

  1. 因为访问限制而降低服务可用性;
  2. 被迫重新实现安全协议而导致实施成本大大提高;
  3. 开放更多的防火墙端口和协议访问,而牺牲安全性。

三、序列化和反序列化的组件

典型的序列化和反序列化过程往往需要如下组件。

  • IDL(Interface description language)文件:参与通讯的各方需要对通讯的内容需要做相关的约定(Specifications)。为了建立一个与语言和平台无关的约定,这个约定需要采用与具体开发语言、平台无关的语言来进行描述。这种语言被称为接口描述语言(IDL),采用 IDL 撰写的协议约定称之为 IDL 文件。
  • IDL Compiler:IDL 文件中约定的内容为了在各语言和平台可见,需要有一个编译器,将 IDL 文件转换成各语言对应的动态库。
  • Stub/Skeleton Lib:负责序列化和反序列化的工作代码。Stub 是一段部署在分布式系统客户端的代码,一方面接收应用层的参数,并对其序列化后通过底层协议栈发送到服务端,另一方面接收服务端序列化后的结果数据,反序列化后交给客户端应用层;Skeleton 部署在服务端,其功能与 Stub 相反,从传输层接收序列化参数,反序列化后交给服务端应用层,并将应用层的执行结果序列化后最终传送给客户端 Stub。
  • Client/Server:指的是应用层程序代码,他们面对的是 IDL 所生存的特定语言的 class 或 struct。
  • 底层协议栈和互联网:序列化之后的数据通过底层的传输层、网络层、链路层以及物理层协议转换成数字信号在互联网中传递。

序列化组件与数据库访问组件的对比

数据库访问对于很多工程师来说相对熟悉,所用到的组件也相对容易理解。下表类比了序列化过程中用到的部分组件和数据库访问组件的对应关系,以便于大家更好的把握序列化相关组件的概念。

|序列化组件|数据库组件|说明| |-----:| |IDL|DDL|用于建表或者模型的语言| |DL file|DB Schema|表创建文件或模型文件| |Stub/Skeleton|lib O/R mapping|将 class 和 Table 或者数据模型进行映射|

四、几种常见的序列化和反序列化协议

互联网早期的序列化协议主要有 COM 和 CORBA。

COM 主要用于 Windows 平台,并没有真正实现跨平台,另外 COM 的序列化的原理利用了编译器中虚表,使得其学习成本巨大(想一下这个场景, 工程师需要是简单的序列化协议,但却要先掌握语言编译器)。由于序列化的数据与编译器紧耦合,扩展属性非常麻烦。

CORBA 是早期比较好的实现了跨平台,跨语言的序列化协议。COBRA 的主要问题是参与方过多带来的版本过多,版本之间兼容性较差,以及使用复杂晦涩。这些政治经济,技术实现以及早期设计不成熟的问题,最终导致 COBRA 的渐渐消亡。J2SE 1.3 之后的版本提供了基于 CORBA 协议的 RMI-IIOP 技术,这使得 Java 开发者可以采用纯粹的 Java 语言进行 CORBA 的开发。

这里主要介绍和对比几种当下比较流行的序列化协议,包括 XML、JSON、Protobuf、Thrift 和 Avro。

一个例子

如前所述,序列化和反序列化的出现往往晦涩而隐蔽,与其他概念之间往往相互包容。为了更好了让大家理解序列化和反序列化的相关概念在每种协议里面的具体实现,我们将一个例子穿插在各种序列化协议讲解中。在该例子中,我们希望将一个用户信息在多个系统里面进行传递;在应用层,如果采用 Java 语言,所面对的类对象如下所示:

复制代码
class Address
{
private String city;
private String postcode;
private String street;
}
public class UserInfo
{
private Integer userid;
private String name;
private List<address> address;
}
</address>

XML&SOAP

XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点。 XML 历史悠久,其 1.0 版本早在 1998 年就形成标准,并被广泛使用至今。XML 的最初产生目标是对互联网文档(Document)进行标记,所以它的设计理念中就包含了对于人和机器都具备可读性。 但是,当这种标记文档的设计被用来序列化对象的时候,就显得冗长而复杂(Verbose and Complex)。 XML 本质上是一种描述语言,并且具有自我描述(Self-describing)的属性,所以 XML 自身就被用于 XML 序列化的 IDL。 标准的 XML 描述格式有两种:DTD(Document Type Definition)和 XSD(XML Schema Definition)。作为一种人眼可读(Human-readable)的描述语言,XML 被广泛使用在配置文件中,例如 O/R mapping、 Spring Bean Configuration File 等。

SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议。SOAP 在互联网影响如此大,以至于我们给基于 SOAP 的解决方案一个特定的名称–Web service。SOAP 虽然可以支持多种传输层协议,不过 SOAP 最常见的使用方式还是 XML+HTTP。SOAP 协议的主要接口描述语言(IDL)是 WSDL(Web Service Description Language)。SOAP 具有安全、可扩展、跨语言、跨平台并支持多种传输层协议。如果不考虑跨平台和跨语言的需求,XML 的在某些语言里面具有非常简单易用的序列化使用方法,无需 IDL 文件和第三方编译器, 例如 Java+XStream。

自我描述与递归

SOAP 是一种采用 XML 进行序列化和反序列化的协议,它的 IDL 是 WSDL. 而 WSDL 的描述文件是 XSD,而 XSD 自身是一种 XML 文件。 这里产生了一种有趣的在数学上称之为“递归”的问题,这种现象往往发生在一些具有自我属性(Self-description)的事物上。

IDL 文件举例

采用 WSDL 描述上述用户基本信息的例子如下:

复制代码
<xsd:complexType name='Address'>
<xsd:attribute name='city' type='xsd:string' />
<xsd:attribute name='postcode' type='xsd:string' />
<xsd:attribute name='street' type='xsd:string' />
</xsd:complexType>
<xsd:complexType name='UserInfo'>
<xsd:sequence>
<xsd:element name='address' type='tns:Address'/>
<xsd:element name='address1' type='tns:Address'/> 
</xsd:sequence>
<xsd:attribute name='userid' type='xsd:int' />
<xsd:attribute name='name' type='xsd:string' /> 
</xsd:complexTyp>

典型应用场景和非应用场景

SOAP 协议具有广泛的群众基础,基于 HTTP 的传输协议使得其在穿越防火墙时具有良好安全特性,XML 所具有的人眼可读(Human-readable)特性使得其具有出众的可调试性,互联网带宽的日益剧增也大大弥补了其空间开销大(Verbose)的缺点。对于在公司之间传输数据量相对小或者实时性要求相对低(例如秒级别)的服务是一个好的选择。由于 XML 的额外空间开销大,序列化之后的数据量剧增,对于数据量巨大序列持久化应用常景,这意味着巨大的内存和磁盘开销,不太适合 XML。另外,XML 的序列化和反序列化的空间和时间开销都比较大,对于对性能要求在 ms 级别的服务,不推荐使用。WSDL 虽然具备了描述对象的能力,SOAP 的 S 代表的也是 simple,但是 SOAP 的使用绝对不简单。对于习惯于面向对象编程的用户,WSDL 文件不直观。

JSON(Javascript Object Notation)

JSON 起源于弱类型语言 Javascript, 它的产生来自于一种称之为"Associative array"的概念,其本质是就是采用"Attribute-value"的方式来描述对象。实际上在 Javascript 和 PHP 等弱类型语言中,类的描述方式就是 Associative array。JSON 的如下优点,使得它快速成为最广泛使用的序列化协议之一。

  1. 这种 Associative array 格式非常符合工程师对对象的理解。
  2. 它保持了 XML 的人眼可读(Human-readable)的优点。
  3. 相对于 XML 而言,序列化后的数据更加简洁。 来自于的以下链接的研究表明:XML 所产生序列化之后文件的大小接近 JSON 的两倍。 http://www.codeproject.com/Articles/604720/JSON-vs-XML-Some-hard-numbers-about-verbosity
  4. 它具备 Javascript 的先天性支持,所以被广泛应用于 Web browser 的应用常景中,是 Ajax 的事实标准协议。
  5. 与 XML 相比,其协议比较简单,解析速度比较快。
  6. 松散的 Associative array 使得其具有良好的可扩展性和兼容性。

IDL 悖论

JSON 实在是太简单了,或者说太像各种语言里面的类了,所以采用 JSON 进行序列化不需要 IDL。这实在是太神奇了,存在一种天然的序列化协议,自身就实现了跨语言和跨平台。然而事实没有那么神奇,之所以产生这种假象,来自于两个原因。

  1. Associative array 在弱类型语言里面就是类的概念,在 PHP 和 Javascript 里面 Associative array 就是其 class 的实际实现方式,所以在这些弱类型语言里面,JSON 得到了非常良好的支持。
  2. IDL 的目的是撰写 IDL 文件,而 IDL 文件被 IDL Compiler 编译后能够产生一些代码(Stub/Skeleton),而这些代码是真正负责相应的序列化和反序列化工作的组件。 但是由于 Associative array 和一般语言里面的 class 太像了,他们之间形成了一一对应关系,这就使得我们可以采用一套标准的代码进行相应的转化。对于自身支持 Associative array 的弱类型语言,语言自身就具备操作 JSON 序列化后的数据的能力;对于 Java 这强类型语言,可以采用反射的方式统一解决,例如 Google 提供的 Gson。

典型应用场景和非应用场景

JSON 在很多应用场景中可以替代 XML,更简洁并且解析速度更快。典型应用场景包括:

  1. 公司之间传输数据量相对小,实时性要求相对低(例如秒级别)的服务。
  2. 基于 Web browser 的 Ajax 请求。
  3. 由于 JSON 具有非常强的前后兼容性,对于接口经常发生变化,并对可调式性要求高的场景,例如 Mobile app 与服务端的通讯。
  4. 由于 JSON 的典型应用场景是 JSON+HTTP,适合跨防火墙访问。总的来说,采用 JSON 进行序列化的额外空间开销比较大,对于大数据量服务或持久化,这意味着巨大的内存和磁盘开销,这种场景不适合。没有统一可用的 IDL 降低了对参与方的约束,实际操作中往往只能采用文档方式来进行约定,这可能会给调试带来一些不便,延长开发周期。 由于 JSON 在一些语言中的序列化和反序列化需要采用反射机制,所以在性能要求为 ms 级别,不建议使用。

IDL 文件举例

以下是 UserInfo 序列化之后的一个例子:

复制代码
{"userid":1,"name":"messi","address":[{"city":" 北京 ","postcode":"1000000","street":"wangjingdonglu"}]}

Thrift

Thrift 是 Facebook 开源提供的一个高性能,轻量级 RPC 服务框架,其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。 但是,Thrift 并不仅仅是序列化协议,而是一个 RPC 框架。相对于 JSON 和 XML 而言,Thrift 在空间开销和解析性能上有了比较大的提升,对于对性能要求比较高的分布式系统,它是一个优秀的 RPC 解决方案;但是由于 Thrift 的序列化被嵌入到 Thrift 框架里面,Thrift 框架本身并没有透出序列化和反序列化接口,这导致其很难和其他传输层协议共同使用(例如 HTTP)。

典型应用场景和非应用场景

对于需求为高性能,分布式的 RPC 服务,Thrift 是一个优秀的解决方案。它支持众多语言和丰富的数据类型,并对于数据字段的增删具有较强的兼容性。所以非常适用于作为公司内部的面向服务构建(SOA)的标准 RPC 框架。

不过 Thrift 的文档相对比较缺乏,目前使用的群众基础相对较少。另外由于其 Server 是基于自身的 Socket 服务,所以在跨防火墙访问时,安全是一个顾虑,所以在公司间进行通讯时需要谨慎。 另外 Thrift 序列化之后的数据是 Binary 数组,不具有可读性,调试代码时相对困难。最后,由于 Thrift 的序列化和框架紧耦合,无法支持向持久层直接读写数据,所以不适合做数据持久化序列化协议。

IDL 文件举例

复制代码
struct Address
1: required string city;
2: optional string postcode;
3: optional string street;
struct UserInfo
1: required string userid;
2: required i32 name;
3: optional list<address> address;
}
</address>

Protobuf

Protobuf 具备了优秀的序列化协议的所需的众多典型特征。

  1. 标准的 IDL 和 IDL 编译器,这使得其对工程师非常友好。
  2. 序列化数据非常简洁,紧凑,与 XML 相比,其序列化之后的数据量约为 1/3 到 1/10。
  3. 解析速度非常快,比对应的 XML 快约 20-100 倍。
  4. 提供了非常友好的动态库,使用非常简介,反序列化只需要一行代码。

Protobuf 是一个纯粹的展示层协议,可以和各种传输层协议一起使用;Protobuf 的文档也非常完善。 但是由于 Protobuf 产生于 Google,所以目前其仅仅支持 Java、C#### 典型应用场景和非应用场景 Protobuf 具有广泛的用户基础,空间开销小以及高解析性能是其亮点,非常适合于公司内部的对性能要求高的 RPC 调用。由于 Protobuf 提供了标准的 IDL 以及对应的编译器,其 IDL 文件是参与各方的非常强的业务约束,另外,Protobuf 与传输层无关,采用 HTTP 具有良好的跨防火墙的访问属性,所以 Protobuf 也适用于公司间对性能要求比较高的场景。由于其解析性能高,序列化后数据量相对少,非常适合应用层对象的持久化场景。

它的主要问题在于其所支持的语言相对较少,另外由于没有绑定的标准底层传输层协议,在公司间进行传输层协议的调试工作相对麻烦。

IDL 文件举例

复制代码
message Address
{
required string city=1;
optional string postcode=2;
optional string street=3;
}
message UserInfo
{
required string userid=1;
required string name=2;
repeated Address address=3;
}

Avro

Avro 的产生解决了 JSON 的冗长和没有 IDL 的问题,Avro 属于 Apache Hadoop 的一个子项目。 Avro 提供两种序列化格式:JSON 格式或者 Binary 格式。Binary 格式在空间开销和解析性能方面可以和 Protobuf 媲美,JSON 格式方便测试阶段的调试。 Avro 支持的数据类型非常丰富,包括 C#### 典型应用场景和非应用场景 Avro 解析性能高并且序列化之后的数据非常简洁,比较适合于高性能的序列化服务。

由于 Avro 目前非 JSON 格式的 IDL 处于实验阶段,而 JSON 格式的 IDL 对于习惯于静态类型语言的工程师来说不直观。

IDL 文件举例

复制代码
protocol Userservice {
  record Address {
   string city;
   string postcode;
   string street;
  }  
  record UserInfo {
   string name;
   int userid;
   array<Address> address = [];
  }
}

所对应的 JSON Schema 格式如下:

复制代码
{
"protocol" : "Userservice",
"namespace" : "org.apache.avro.ipc.specific",
"version" : "1.0.5",
"types" : [ {
"type" : "record",
"name" : "Address",
"fields" : [ {
"name" : "city",
"type" : "string"
}, {
"name" : "postcode",
"type" : "string"
}, {
"name" : "street",
"type" : "string"
} ]
}, {
"type" : "record",
"name" : "UserInfo",
"fields" : [ {
"name" : "name",
"type" : "string"
}, {
"name" : "userid",
"type" : "int"
}, {
"name" : "address",
"type" : {
"type" : "array",
"items" : "Address"
},
"default" : [ ]
} ]
} ],
"messages" : { }
}

五、Benchmark 以及选型建议

Benchmark

以下数据来自 https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking

解析性能

序列化之空间开销

从上图可得出如下结论:

  1. XML 序列化(Xstream)无论在性能和简洁性上比较差;
  2. Thrift 与 Protobuf 相比在时空开销方面都有一定的劣势;
  3. Protobuf 和 Avro 在两方面表现都非常优越。

选型建议

以上描述的五种序列化和反序列化协议都各自具有相应的特点,适用于不同的场景。

  1. 对于公司间的系统调用,如果性能要求在 100ms 以上的服务,基于 XML 的 SOAP 协议是一个值得考虑的方案。
  2. 基于 Web browser 的 Ajax,以及 Mobile app 与服务端之间的通讯,JSON 协议是首选。对于性能要求不太高,或者以动态类型语言为主,或者传输数据载荷很小的的运用场景,JSON 也是非常不错的选择。
  3. 对于调试环境比较恶劣的场景,采用 JSON 或 XML 能够极大的提高调试效率,降低系统开发成本。
  4. 当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro 之间具有一定的竞争关系。
  5. 对于 T 级别的数据的持久化应用场景,Protobuf 和 Avro 是首要选择。如果持久化后的数据存储在 Hadoop 子项目里,Avro 会是更好的选择。
  6. 由于 Avro 的设计理念偏向于动态类型语言,对于动态语言为主的应用场景,Avro 是更好的选择。
  7. 对于持久层非 Hadoop 项目,以静态类型语言为主的应用场景,Protobuf 会更符合静态类型语言工程师的开发习惯。
  8. 如果需要提供一个完整的 RPC 解决方案,Thrift 是一个好的选择。
  9. 如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,Protobuf 可以优先考虑。

参考文献

  1. http://www.codeproject.com/Articles/604720/JSON-vs-XML-Some-hard-numbers-about-verbosity
  2. https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking
  3. http://en.wikipedia.org/wiki/Serialization
  4. http://en.wikipedia.org/wiki/Soap
  5. http://en.wikipedia.org/wiki/XML
  6. http://en.wikipedia.org/wiki/JSON
  7. http://avro.apache.org/
  8. http://www.oracle.com/technetwork/java/rmi-iiop-139743.html

感谢丁晓昀对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流。

2015 年 5 月 07 日 09:1039925

评论

发布
暂无评论
发现更多内容

技术人员如何写好周报

猿话

2021字节、华为、滴滴Java内部面试题(含答案),新鲜出炉!

比伯

Java 编程 架构 面试 程序人生

电商网站商品管理(二)多种搜索方式

escray

elasticsearch elastic 28天写作 死磕Elasticsearch 60天通过Elastic认证考试

案例研究之聊聊 QLExpress 源码 (七)

小诚信驿站

聊聊架构 规则引擎 28天写作 QLExpress源码 聊聊源码

二本学渣考研失败,为什么Android要采用Binder作为IPC机制?已开源

欢喜学安卓

android 程序员 面试 移动开发

一文带你学会AQS和并发工具类的关系

伯阳

AQS java 并发 ReentrantLock 多线程高并发 lock锁

【得物技术】代码覆盖率原理与得物app实践

得物技术

测试 原理 代码 得物技术 覆盖率

这份30天获得40k+星,多次登上榜首的算法宝典,带你刷爆LeetCode

Crud的程序员

程序员 架构 算法

限时开放!阿里P8大师终于把这份微服务架构与实践第2版PDF分享出来了

云流

Java 编程 程序员 微服务 架构师

自动驾驶分级,小白能理解的那种(28天写作 Day8/28)

mtfelix

自动驾驶 28天写作

从姚安娜出道说起

三只猫

28天写作 社交泛娱乐

9. 细节见真章,Formatter注册中心的设计很讨巧

YourBatman

Converter ConversionService Formatter

我们为什么打比方

石云升

28天写作 确认偏误 打比方

使用nodejs和express搭建http web服务

程序那些事

HTTP nodejs 异步IO 程序那些事 web服务

架构师第8周作业

Geek_xq

在GitHub中向开源项目提交PR的过程

worry

GitHub pull request

矿机挖矿软件系统开发|矿机挖矿APP开发

开發I852946OIIO

系统开发

[5/28]产品运维保障体系的质量实践

俊毅

阿里表哥甩我一份Redis笔记,看完还进不了阿里让我卖豆腐去

互联网架构师小马

Java 数据库 nosql redis 面试

也谈Python编码格式

ITCamel

Python 编码格式

超越身边80%的人,其实没有你想象的那么难

架构精进之路

认知提升 成长笔记 七日更 28天写作

技术创新是PC市场发展基石,英特尔占据明显领先优势

intel001

APICloud AVM多端开发 |《生鲜电商app开发》项目源码教程

APICloud

前端开发 移动开发 APP开发 APICloud

为什么印度不会成为世界工厂?

JiangX

印度 28天写作 世界工厂

Python列表对象入门

老赵

28天写作

详解HDFS3.x新特性-纠删码

五分钟学大数据

hadoop hdfs

IO和NIO的对比篇

Java架构师迁哥

使用 kubectl-rabbitmq 部署和运维 K8S 上的 RabbitMQ 集群

郭旭东

RabbitMQ kubectl kubectl plugin

区块链2021狂想曲:迎接以技术为名的春天

脑极体

Spring Boot 集成Thymeleaf模板引擎

武哥聊编程

Java springboot SpringBoot 2 thymeleaf 28天写作

架构师第八周总结

Geek_xq

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

序列化和反序列化-InfoQ