
目前业界有各种各样的网络输出传输时的序列化和反序列化方案,它们在技术上的实现的初衷和背景有较大的区别,因此在设计的架构也会有很大的区别,最终在落地后的:解析速度、对系统的影响、传输数据的大小、可维护性及可阅读性等方面有着较大的区别,本文分享一些我在一些常见序列化技术的分析和理解: 文章分成3个部分: 1、列举常见的序列化和反序列化方案(ObjectXXStream、XML、JSON) 2、MySQL JDBC结果集的处理方案 3、Google Protocol Buffer处理方案 【一、常见的在API及消息通信调的用中Serialize方案】: 方案1、基于Java原生的ObjectOutputStream.write()和ObjectInputStream.read()来进行对象序列化和反序列化。 方案2、基于JSON进行序列化和反序列化。 方案3、基于XML进行序列化和反序列化。 【方案1浅析,ObjectXXXStream】: 优点: (1)、由Java自带API序列化,简单、方便、无第三方依赖。 (2)、不用担心其中的数据解析会丢失精度、丢失字段、Object的反序列化类型不确定等问题。 缺点: (1)、双方调试麻烦,发送方和接收方最好是同版本的对象描述,否则会有奇怪的问题,调试周期相对长,跨团队合作升级问题很多。 (2)、传递的对象中包含了元数据信息,占用空间较大。 【方案2浅析,JSON序列化】: 优点: (1)、简单、方便,无需关注要序列化的对象格式。 (2)、开源界有较多的组件可以支持,例如FastJSON性能非常好。 (3)、在现在很多RPC的框架中,基本都支持这样的方案。 缺点: (1)、对象属性中如果包含Object类型,在反序列化的时候如果业务也本身也不明确数据类型,处理起来会很麻烦。 (2)、由于文本类型,所以一定会占用较大的数据空间,例如下图。 (3)、比较比较依赖于JSON的解析包的兼容性和性能,在JSON的一些细节处理上(例如一些非标的JSON),各自处理方式可能不一样。 (4)、序列化无论任何数据类型先要转换为String,转成byte[],会增加内存拷贝的次数。 (5)、反序列化的时候,必须将整个JSON反序列化成对象后才能进行读取,大家应该知道,Java对象尤其是层次嵌套较多的对象,占用的内存空间将会远远大于数据本身的空间。 数据放大的极端案例1: 传递数据描述信息为: class PP { long userId = 102333320132133L; int passportNumber = 123456; } 此时传递JSON的格式为: { "userId":102333320132133, "passportNumber":123456 } 我们要传递的数据是1个long、1个int,也就是12个字节的数据,这个JSON的字符串长度将是实际的字节数(不包含回车、空格,这里只是为了可读性,同时注意,这里的long在JSON里面是字符串了),这个字符串有:51个字节,也就是数据放到了4.25倍左右。 数据放大极端案例2: 当你的对象内部有数据是byte[]类型,JSON是文本格式的数据,是无法存储byte[]的,那么要序列化这样的数据,只有一个办法就是把byte转成字符,通常的做法有两种: (1)使用BASE64编码,目前JSON中比较常用的做法。 (2)按照字节进行16进制字符编码,例如字符串:“FF”代表的是0xFF这个字节。 不论上面两种做法的那一种,1个字节都会变成2个字符来传递,也就是byte[]数据会被放大2倍以上。为什么不用ISO-8859-1的字符来编码呢?因为这样编码后,在最终序列化成网络byte[]数据后,对应的byte[]虽然没变大,但是在反序列化成文本的时候,接收方并不知道是ISO-8859-1,还会用例如GBK、UTF-8这样比较常见的字符集解析成String,才能进一步解析JSON对象,这样这个byte[]可能在编码的过程中被改变了,要处理这个问题会非常麻烦。 【方案2浅析,XML序列化】: 优点: (1)、使用简单、方便,无需关注要序列化的对象格式 (2)、可读性较好,XML在业界比较通用,大家也习惯性在配置文件中看到XML的样子 (3)、大量RPC框架都支持,通过XML可以直接形成文档进行传阅 缺点: (1)、在序列化和反序列化的性能上一直不是太好。 (2)、也有与JSON同样的数据类型问题,和数据放大的问题,同时数据放大的问题更为严重,同时内存拷贝次数也和JSON类型,不可避免。 XML数据放大说明: XML的数据放大通常比JSON更为严重,以上面的JSON案例来讲,XML传递这份数据通常会这样传: <Msg> <userId>102333320132133</userId> <passportNumber>123456<passportNumber> <Msg> 这个消息就有80+以上的字节了,如果XML里面再搞一些Property属性,对象再嵌套嵌套,那么这个放大的比例有可能会达到10倍都是有可能的,因此它的放大比JSON更为严重,这也是为什么现在越来越多的API更加喜欢用JSON,而不是XML的原因。 【放大的问题是什么】: (1)、花费更多的时间去拼接字符串和拷贝内存,占用更多的Java内存,产生更多的碎片。 (2)、产生的JSON对象要转为byte[]需要先转成String文本再进行byte[]编码,因为这本身是文本协议,那么自然再多一次内存全量的拷贝。 (3)、传输过程由于数据被放大,占用更大的网络流量。 (4)、由于网络的package变多了,所以TCP的ACK也会变多,自然系统也会更大,同等丢包率的情况下丢包数量会增加,整体传输时间会更长,如果这个数据传送的网络延迟很大且丢包率很高,我们要尽量降低大小;压缩是一条途径,但是压缩会带来巨大的CPU负载提高,在压缩前尽量降低数据的放大是我们所期望的,然后传送数据时根据RT和数据大小再判定是否压缩,有必要的时候,压缩前如果数据过大还可以进行部分采样数据压缩测试压缩率。 (5)、接收方要处理数据也会花费更多的时间来处理。 (6)、由于是文本协议,在处理过程中会增加开销,例如数字转字符串,字符串转数字;byte[]转字符串,字符串转byte[]都会增加额外的内存和计算开销。 不过由于在平时大量的应用程序中,这个开销相对业务逻辑来讲简直微不足道,所以优化方面,这并不是我们关注的重点,但面临一些特定的数据处理较多的场景,即核心业务在数据序列化和反序列化的时候,就要考虑这个问题了,那么下面我继续讨论问题。 此时提出点问题: (1)、网络传递是不是有更好的方案,如果有,为什么现在没有大面积采用? (2)、相对底层的数据通信,例如JDBC是如何做的,如果它像上面3种方案传递结果集,会怎么样? 【二、MySQL JDBC数据传递方案】: 在前文中提到数据在序列化过程被放大数倍的问题,我们是否想看看一些相对底层的通信是否也是如此呢?那么我们以MySQL JDBC为例子来看看它与JDBC之间进行通信是否也是如此。 JDBC驱动程序根据数据库不同有很多实现,每一种数据库实现细节上都有巨大的区别,本文以MySQL JDBC的数据解析为例(MySQL 8.0以前),给大家说明它是如何传递数据的,而传递数据的过程中,相信大家最为关注的就是ResultSet的数据是如何传递的。 抛开结果集中的MetaData等基本信息,单看数据本身: (1)JDBC会读取数据行的时候,首先会从缓冲区读取一个row packege,row package就是从网络package中拿到的,根据协议中传递过来的package的头部判定package大小,然后从网络缓冲中读取对应大小的内容,下图想表达网络传递的package和业务数据中的package之间可能并不是完全对应的。另外,网络中的package如果都到了本地缓冲区,逻辑上讲它们是连续的(图中故意分开是让大家了解到网络中传递是分不同的package传递到本地的),JDBC从本地buffer读取row package这个过程就是内核package到JVM的package拷贝过程,对于我们Java来讲,我们主要关注row package(JDBC中可能存在一些特殊情况读取过来的package并不是行级别的,这种特殊情况请有兴趣的同学自行查阅源码)。 (2)、单行数据除头部外,就是body了,body部分包含各种各样不同的数据类型,此时在body上放数据类型显然是占空间的,所以数据类型是从metadata中提取的,body中数据列的顺序将会和metdata中的列的顺序保持一致。 (3)、MySQL详细解析数据类型: 3.1、如果Metadata对应数据发现是int、longint、tinyint、year、float、double等数据类型会按照定长字节数读取,例如int自然按照4字节读取,long会按照8字节读取。 3.2、如果发现是其它的类型,例如varchar、binary、text等等会按照变长读取。 3.3、变长字符串首先读取1个字节标志位。 3.4、如果这个标志位的值小于等于250,则直接代表后续字节的长度(注意字符串在这里是算转换为字节的长度),这样确保大部分业务中存放的变长字符串,在网络传递过程中只需要1个字节的放大。 3.5、如果这个标志位是:251,代表这个字段为NULL 3.6、如果标志位是:252,代表需要2个字节代表字段的长度,此时加上标志位就是3个字节,在65536以内的长度的数据(64KB),注意,这里会在转成long的时候高位补0,所以2个字节可以填满到65536,只需要放大3个字节来表示这个数据。 3.7、如果标志位是:253,代表需要4个自己大表字段的长度,可以表示4GB(同上高位补0),这样的数据几乎不会出现在数据库里面,即使出现,只会出现5个字节的放大。 3.8、如果标志位是:254,8个字节代表长度,此时没有高位补0,最多可以读取Long.MAX_VALUE的长度,但是这个空间目前不可能有内存放得下,所以无需担心使用问题,此时9个字节的放大,源码如下图: (4)、我们先按照这个理解,MySQL在传递数据的过程中,对数据的放大是很小很小的,是不是真的这样呢?请下面第5点说明。 补充说明: a、在MySQL JDBC中对于ResultSetRow数据的解析(除对MySQL 8.0以上JDBC版本)有2个实现类:BufferRow、ByteArrayRow,这两种方式在读取单行数据在解析这个阶段是一样的逻辑,只不过解析存放数据的方式有所不同,BufferRow一个会解析成数据行的byte[],ByteArrayRow会解析成byte[][]二维数组,第二维就是每1个列的信息,这都是客户端行为,与网络传递数据的格式无关。(两者在不同场景下使用,例如其中一种场景是:ByteArrayRow在游标开启UPDATE模式的时候会启用,但这不是本文的重点,这里提到主要告知大家,无论哪一种方式,读取数据的方式是一致的) b、在MySQL JDBC中的RowData是ResultSet里面数据处理的入口,其实现类有3个:RowStatStatic、RowDataCursor、RowDataDynamic,这虽然有3个实现类,但是同样不会影响数据的格式,它们只是从缓冲区读取数据的方式有所不同:RowStatStatic、RowDataCursor会每次将缓冲区的数据全部读取到JDBC当中形成数组,RowDataCursor在处理上有一个区别在于数据库每次返回的是FetchSize大小的数据内容(实现的细节在上一篇文章中有提到);RowDataDynamic是需要行的时候再从pakcege中去读,package读取完成后就尝试读取下一个package。这些都不会影响数据本身在网络上的传递格式,所以文本提到的解析是目前MySQL JDBC比较通用的解析,与它内部的执行路径无关。 (5)、以BufferRow为例,当你发起getString('xxx')、getDate(int)的时候,首先它需要在内部找到是第几个列(传数字省略该动作),然后其内部会有一个lastRequestedIndex、lastRequestedPos分别记录最后读取的第几个字段和所在字节的位置,如果你传入的index比这个index大,则从当前位置开始,向后扫描,扫描规则和上面的数据库宽度一致,找到对应位置,拷贝出对应的byte[]数组,转换你要的对象类型。 PS:lastRequestedIndex、lastRequestedPos这种其实就是JDBC认为你绝大部分情况是从前向后读取的,因此这样读取对JDBC程序也是最友好的方案,否则指针向前移动,需要从0开始,理由很简单(数据的长度不是在尾部,而是在头部),因此指针来回来回移动的时候,这样会产生很多开销,同时会产生更多的内存拷贝出来的碎片。ByteArrayRow虽然可以解决这个问题,但是其本身会占用相对较大的空间另外,其内部的二维数组返回的byte[]字节是可以被外部所修改的(因为没有拷贝)。 另外,按照这种读取数据的方式,如果单行数据过大(例如有大字段100MB+),读取到Java内存里面来,即使使用CursorFetch和Stream读取,读取几十条数据,就能把JVM内存干挂掉。到目前为止,我还没看到MySQL里面可以“设置限制单行数据长度”的参数,后续估计官方支持这类特殊需求的可能性很小,大多也只能自己改源码来实现。 【回到话题本身:MySQL和JDBC之间的通信似乎放大很小?】 其实不然,MySQL传递数据给JDBC默认是走文本协议的,而不是Binary协议,虽然说它的byte[]数组不会像JSON那样放大,并不算真正意义上的文本协议,但是它很多种数据类型默认情况下,都是文本传输,例如一个上面提到的账号:102333320132133在数据库中是8个自己,但是网络传递的时候如果有文本格式传递将会是:15个字节,如果是DateTime数据在数据库中可以用8个字节存放,但是网络传递如果按照YYYY-MM-DD HH:MI:SS传递,可以达到19个字节,而当他们用String在网络传递的时候,按照我们前面提到的,MySQL会将其当成变长字符,因此会在数据头部加上最少1个自己的标志位。另外,这里增加不仅仅是几个字节,而是你要取到真正的数据,接收方还需要进一步计算处理才能得到,例如102333320132133用文本传送后,接收方是需要将这个字符串转换为long类型才能可以得到long的,大家试想一下你处理500万数据,每一行数据有20个列,有大量的类似的处理不是开销增加了特别多呢? JDBC和MySQL之间可以通过binary协议来进行通信的,也就是按照实际数字占用的空间大小来进行通信,但是比较坑的时,MySQL目前开启Binary协议的方案是:“开启服务端prepareStatemet”,这个一旦开启,会有一大堆的坑出来,尤其是在互联网的编程中,我会在后续的文章中逐步阐述。 抛开“开启binary协议的坑”,我们认为MySQL JDBC在通信的过程中对数据的编码还是很不错的,非常紧凑的编码(当然,如果你开启了SSL访问,那么数据又会被放大,而且加密后的数据基本很难压缩)。 对比传统的数据序列化优劣势汇总: 优势: (1)、数据全部按照byte[]编码后,由于紧凑编码,所以对数据本身的放大很小。 (2)、由于编码和解码都没有解析的过程,都是向ByteBuffer的尾部顺序地写,也就是说不用找位置,读取的时候根据设计也可以减少找位置,即使找位置也是移动偏移量,非常高效。 (3)、如果传递多行数据,反序列化的过程不用像XML或JSON那样一次要将整个传递过来的数据全部解析后再处理,试想一下,如果5000行、20列的结果集,会产生多少Java对象,每一个Java对象对数据本身的放大又是多少,采用字节传递后可以按需转变为Java对象,使用完的Java对象可以释放,这样就不用同时占用那样大的JVM内存,而byte[]数组也只是数据本身的大小,也可以按需释放。 (4)、相对前面提到的3种方式,例如JSON,它不需要在序列化和反序列化的时候要经历一次String的转换,这样会减少一次内存拷贝。 (5)、自己写代码用类似的通信方案,可以在网络优化上做到极致。 劣势: (1)、编码是MySQL和MySQL JDBC之间自定义的,别人没法用(我们可以参考别人的思路) (2)、byte编码和解码过程程序员自己写,对程序员水平和严谨性要求都很高,前期需要大量的测试,后期在网络问题上考虑稍有偏差就可能出现不可预期的Bug。(所以在公司内部需要把这些内容进行封装,大部分程序员无需关注这个内容)。 (3)、从内存拷贝上来讲,从rowBuffer到应用中的数据,这一层内存拷贝是无法避免的,如果你写自定义程序,在必要的条件下,这个地方可以进一步减少内存拷贝,但无法杜绝;同上文中提到,这点开销,对于整个应用程序的业务处理来讲,简直微不足道。 为什么传统通信协议不选择这样做: (1)、参考劣势中的3点。 (2)、传统API通信,我们更讲究快速、通用,也就是会经常和不同团队乃至不同公司调试代码,要设计binary协议,开发成本和调试成本非常高。 (3)、可读性,对于业务代码来讲,byte[]的可读性较差,尤其是对象嵌套的时候,byte[]表达的方式是很复杂的。 MySQL JDBC如果用binary协议后,数据的紧凑性是不是达到极致了呢? 按照一般的理解,就是达到极致了,所有数据都不会进一步放大,int就只用4个字节传递,long就只用8个字节传递,那么还能继续变小,难道压缩来做? 非也、非也,在二进制的世界里,如果你探究细节,还有更多比较神奇的东西,下面我们来探讨一下: 例如: long id = 1L; 此时网络传递的时候会使用8个字节来方,大家可以看下8个字节的排布: 我们先不考虑按照bit有31个bit是0,先按照字节来看有7个0,代表字节没有数据,只有1个字节是有值的,大家可以去看一下自己的数据库中大量的自动增长列,在id小于4194303之前,前面5个字节是浪费掉的,在增长到4个字节(2的32次方-1)之前前面4个自己都是0,浪费掉的。另外,即使8个字节中的第一个字节开始使用,也会有大量的数据,中间字节是为:0的概率极高,就像十进制中进入1亿,那么1亿下面最多会有8个0,越高位的0约难补充上去。 如果真的想去尝试,可以用这个办法:用1个字节来做标志,但会占用一定的计算开销,所以是否为了这个空间去做这个事情,由你决定,本文仅仅是技术性探讨: 方法1:表达目前有几个低位被使用的字节数,由于long只有8个字节,所以用3个bit就够了,另外5个bit是浪费掉的,也无所谓了,反序列化的时候按照高位数量补充0x00即可。 方法2:相对方法1,更彻底,但处理起来更复杂,用1这个字节的8个bit的0、1分别代表long的8个字节是被使用,序列化和反序列化过程根据标志位和数据本身进行字节补0x00操作,补充完整8个字节就是long的值了,最坏情况是9个字节代表long,最佳情况0是1个字节,字节中只占用了2个字节的时候,即使数据变得相当大,也会有大量的数据的字节存在空位的情况,在这些情况下,就通常可以用少于8个字节的情况来表达,要用满7个字节才能够与原数字long的占用空间一样,此时的数据已经是比2的48次方-1更大的数据了。 【三、Google Protocol Buffer技术方案】: 这个对于很多人来讲未必用过,也不知道它是用来干什么的,不过我不得不说,它是目前数据序列化和反序列化的一个神器,这个东西是在谷歌内部为了约定好自己内部的数据通信设计出来的,大家都知道谷歌的全球网络非常牛逼,那么自然在数据传输方面做得那是相当极致,在这里我会讲解下它的原理,就本身其使用请大家查阅其它人的博客,本文篇幅所限没法step by step进行讲解。 看到这个名字,应该知道是协议Buffer,或者是协议编码,其目的和上文中提到的用JSON、XML用来进行RPC调用类似,就是系统之间传递消息或调用API。但是谷歌一方面为了达到类似于XML、JSON的可读性和跨语言的通用性,另一方面又希望达到较高的序列化和反序列化性能,数据放大能够进行控制,所以它又希望有一种比底层编码更容易使用,而又可以使用底层编码的方式,又具备文档的可读性能力。 它首先需要定义一个格式文件,如下: syntax = "proto2"; package com.xxx.proto.buffer.test; message TestData2 { optional int32 id = 2; optional int64 longId = 1; optional bool boolValue = 3; optional string name = 4; optional bytes bytesValue = 5; optional int32 id2 = 6;} 这个文件不是Java文件,也不是C文件,和语言无关,通常把它的后缀命名为proto(文件中1、2、3数字代表序列化的顺序,反序列化也会按照这个顺序来做),然后本地安装了protobuf后(不同OS安装方式不同,在官方有下载和说明),会产生一个protoc运行文件,将其加入环境变量后,运行命令指定一个目标目录: protoc --java_out=~/temp/ TestData2.proto 此时会在指定的目录下,产生package所描述的目录,在其目录内部有1个Java源文件(其它语言的会产生其它语言),这部分代码是谷歌帮你生成的,你自己写的话太费劲,所以谷歌就帮你干了;本地的Java project里面要引入protobuf包,maven引用(版本自行选择): <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.6.1</version> </dependency> 此时生成的代码会调用这个谷歌包里面提供的方法库来做序列化动作,我们的代码只需要调用生成的这个类里面的API就可以做序列化和反序列化操作了,将这些生成的文件放在一个模块里面发布到maven仓库别人就可以引用了,关于测试代码本身,大家可以参考目前有很多博客有提供测试代码,还是很好用的。 谷歌编码比较神奇的是,你可以按照对象的方式定义传输数据的格式,可读性极高,甚至于相对XML和JSON更适合程序员阅读,也可以作为交流文档,不同语言都通用,定义的对象还是可以嵌套的,但是它序列化出来的字节比原始数据只大一点点,这尼玛太厉害了吧。 经过测试不同的数据类型,故意制造数据嵌套的层数,进行二进制数组多层嵌套,发现其数据放大的比例非常非常小,几乎可以等价于二进制传输,于是我把序列化后的数据其二进制进行了输出,发现其编码方式非常接近于上面的JDBC,虽然有一些细节上的区别,但是非常接近,除此之外,它在序列化的时候有几大特征: (1)、如果字段为空,它不会产生任何字节,如果整合对象的属性都为null,产生的字节将是0 (2)、对int32、int64这些数据采用了变长编码,其思路和我们上面描述有一些共通之处,就是一个int64值在比较小的时候用比较少的字节就可以表达了,其内部有一套字节的移位和异或算法来处理这个事情。 (3)、它对字符串、byte[]没有做任何转换,直接放入字节数组,和二进制编码是差不多的道理。 (4)、由于字段为空它都可以不做任何字节,它的做法是有数据的地方会有一个位置编码信息,大家可以尝试通过调整数字顺序看看生成出来的byte是否会发生改变;那么此时它就有了很强兼容性,也就是普通的加字段是没问题的,这个对于普通的二进制编码来讲很难做到。 (5)、序列化过程没有产生metadata信息,也就是它不会把对象的结构写在字节里面,而是反序列化的接收方有同一个对象,就可以反解析出来了。 这与我自己写编码有何区别? (1)、自己写编码有很多不确定性,写不好的话,数据可能放得更大,也容易出错。 (2)、google的工程师把内部规范后,谷歌开源的产品也大量使用这样的通信协议,越来越多的业界中间件产品开始使用该方案,就连MySQL数据库最新版本在数据传输方面也会开始兼容protobuf。 (3)、谷歌相当于开始在定义一个业界的新的数据传输方案,即有性能又降低代码研发的难度,也有跨语言访问的能力,所以才会有越来越多的人喜欢使用这个东西。 那么它有什么缺点呢?还真不多,它基本兼顾了很多序列化和反序列化中你需要考虑的所有的事情,达到了一种非常良好的平衡,但是硬要挑缺陷,我们就得找场景才行: (1)、protobuf需要双方明确数据类型,且定义的文件中每一个对象要明确数据类型,对于Object类型的表达没有方案,你自己必须提前预知这个Object到底是什么类型。 (2)、使用repeated可以表达数组,但是只能表达相同类型的数据,例如上面提到的JDBC一行数据的多个列数据类型不同的时候,要用这个表达,会比较麻烦;另外,默认情况下数组只能表达1维数组,要表达二维数组,需要使用对象嵌套来间接完成。 (3)、它提供的数据类型都是基本数据类型,如果不是普通类型,要自己想办法转换为普通类型进行传输,例如从MongoDB查处一个Docment对象,这个对象序列化是需要自己先通过别的方式转换为byte[]或String放进去的,而相对XML、JSON普通是提供了递归的功能,但是如果protobuf要提供这个功能,必然会面临数据放大的问题,通用和性能永远是矛盾的。 (4)、相对于自定义byte的话,序列化和反序列化是一次性完成,不能逐步完成,这样如果传递数组嵌套,在反序列化的时候会产生大量的Java对象,另外自定义byte的话可以进一步减少内存拷贝,不过谷歌这个相对文本协议来讲内存拷贝已经少很多了。 补充说明: 在第2点中提到repeated表达的数组,每一个元素必须是同类型的,无法直接表达不同类型的元素,因为它没有像Java那样Object[]这样的数组,这样它即使通过本地判定Object的类型传递了,反序列化会很麻烦,因为接收方也不知道数据是什么类型,而protobuf网络传递数据是没有metadata传递的,那么判定唯一的地方就是在客户端自己根据业务需要进行传递。 因此,如果真的有必要的话,可以用List<byte[]>表达一行数据的方案,也就是里面的每1个byte[]元素通过其它地方获得metadata,例如数据库的Metadata得到,然后再自己去转换,但是传递过程全是byte[];就JDBC来讲,我个人更加推荐于一行数据使用一个byte[]来传递,而不是每一项数据用一个byte[],因为在序列化和反序列化过程中,每一个数组元素都会在protobuf会被包装成对象,此时产生的Java对象数量是列的倍数,例如有40个列,会产生40倍的Java对象,很夸张吧。 总之,每一种序列化和反序列化方案目前都有应用场景,它们在设计之初决定了架构,也将决定了最终的性能、稳定性、系统开销、网络传输大小等等。
使用MySQL JDBC读取过较大数据量的人应该清楚(例如超过1GB),在读取的时候内存很可能会Java堆内存溢出,而我们的解决方案是statement.setFetchSize(Integer.MIN_VALUE)并确保游标是只读向前滚动的即可(为游标的默认值),也可以强制类型转换为com.mysql.jdbc.StatementImpl,然后调用其内部方法:enableStreamingResults()这样读取数据内存就不会挂掉了,这两者达到的效果是一致的。当然也可以使用useCursorFetch,但是这种方式测试结果性能要比StreamResult慢很多,为什么?在本文会阐述其大致的原理。 我在前面的部分文章和书籍中都有介绍过其MySQL JDBC在这一块内部处理的代码分成三个不同的类来完成的,不过我一直没有去深究过数据库和JDBC之间到底是如何通信的过程。有一段时间我一直认为这都属于服务端行为或者是客户端与服务端配合的行为,然后并不其然,今天我们来讲一下这个行为是怎么回事。 【先回顾一下简单的通信】: JDBC与数据库之间的通信是通过Socket完成的,因此我们可以把数据库当成一个SocketServer的提供方,因此当SocketServer返回数据的时候(类似于SQL结果集的返回)其流程是:服务端程序数据(数据库) -> 内核Socket Buffer -> 网络 -> 客户端Socket Buffer -> 客户端程序(JDBC所在的JVM内存) 到目前为止,IT行业中大家所看到的JDBC无论是:MySQL JDBC、SQL Server JDBC、PG JDBC、Oracle JDBC。甚至于是NoSQL的Client:Redis Client、MongoDB Client、Memcached,数据的返回基本也是这样一个逻辑。 【方式1:直接使用MySQL JDBC默认参数读取数据,为什么会挂?】 (1)MySQL Server方在发起的SQL结果集会全部通过OutputStream向外输出数据,也就是向本地的Kennel对应的socket buffer中写入数据,这是一次内存拷贝(内存拷贝这个不是本文的重点)。 (2)此时Kennel的Buffer有数据的时候就会把数据通过TCP链路(JDBC主动发起的Socket链路),回传数据,此时数据会回传到JDBC所在机器上,会先进入Kennel区域,同样进入到一个Buffer区。 (3)JDBC在发起SQL操作后,Java代码是在inputStream.read()操作上阻塞,当缓冲区有数据的时候,就会被唤醒,然后将缓冲区的数据读取到Java内存中,这是JDBC端的一次内存拷贝。 (4)接下来MySQL JDBC会不断读取缓冲区数据到Java内存中,MySQL Server会不断发送数据。注意在数据没有完全组装完之前,客户端发起的SQL操作不会响应,也就是给你的感觉MySQL服务端还没响应,其实数据已经到本地,JDBC还没对调用execute方法的地方返回结果集的第一条数据,而是不断从缓冲器读取数据。 (5)关键是这个傻帽就像一把这个数据读取完,根本不管家里放不放的下,就会将整个表的内容读取到Java内存中,先是FULL GC,接下来就是内存溢出。 【方式2:JDBC参数上设置useCursorFetch=true可以解决问题】 这个方案配合FetchSize设置,确实可以解决问题,这个方案其实就是告诉MySQL服务端我要多少数据,每次要多少数据,通信过程有点像这样: 这样做就像我们生活中的那样,我需要什么就去超市买什么,需要多少就去买多少。不过这种交互不像现在网购,坐在家里就可以把东西送到家里来,它一定要走路(网络链路),也就是需要网络的时间开销,假如数据有1亿数据,将FetchSize设置成1000的话,会进行10万次来回通信;如果网络延迟同机房0.02ms,那么10万次通信会增加2秒的时间,不算大。那么如果跨机房2ms的延迟时间会多出来200秒(也就是3分20秒),如果国内跨城市10~40ms延迟,那么时间将会1000~4000秒,如果是跨国200~300ms呢?时间会多出十多个小时出来。 在这里的计算中,我们还没有包含系统调用次数增加了很多,线程等待和唤醒的上下文次数变多,网络包重传的情况对整体性能的影响,因此这种方案看似合理,但是性能确不怎么样。 另外,由于MySQL方不知道客户端什么时候将数据消费完,而自身的对应表可能会有DML写入操作,此时MySQL需要建立一个临时表空间来存放需要拿走的数据。因此对于当你启用useCursorFetch读取大表的时候会看到MySQL上的几个现象: (1)IOPS飙升,因为存在大量的IO读取,如果是普通硬盘,此时可能会引起业务写入的抖动 (2)磁盘空间飙升,这块临时空间可能比原表更大,如果这个表在整个库内部占用相当大的比重有可能会导致数据库磁盘写满,空间会在结果集读取完成后或者客户端发起Result.close()时由MySQL去回收。 (3)CPU和内存会有一定比例的上升,根据CPU的能力决定。 (4)客户端JDBC发起SQL后,长时间等待SQL响应数据,这段时间就是服务端在准备数据,这个等待与原始的JDBC不设置任何参数的方式也表现出等待,在内部原理上是不一样的,前者是一直在读取网络缓冲区的数据,没有响应给业务,现在是MySQL数据库在准备临时数据空间,没有响应给JDBC。 【userCursor原理说明】: (1)在设置JDBC参数useCursorFetch=true后,通过Driver创建Connection的时候会自动将:detectServerPreparedStmts设置为true,这个对应JDBC参数是:useServerPrepStmts=true,也就是当设置useCursorFetch=true时useServerPrepStmt会被自动设置为true,源码片段(ConnectionPropertiesImpl类的postInitialization()中,也就是连接初始化的时候会用的): 内部多提供了另一个方法名,下面会提到: (2)当执行SQL时,会调用到使用prepareStatment方法去执行(即使你自己用Statement内部也会转换成PrepareStatemet,因为它要用服务端预编译),代码如下: 跟下代码,这里的userServerFetch()就是useCursorFetch参数的判定以及游标类型和版本的判定,而游标类型判定的就是为默认值。 (3)步骤1已提到detectServerPreparedStmts被设置为true,在prepareStatement的时候会选择其ServerPreparedStatement作为实现类:具体请参考ConnectionImpl.prepareStatement(String , int , int)的代码,代码太长也不难,就不贴了。 (4)我要说的是ServerPreparedStatement在创建的时候,会在SQL发送前加一个指令在前面,让服务器端预编译,这个指令就是1个int值:22(MysqlDefs.COM_PREPARE),如下: (5)这里仅仅是告知服务端预编译SQL,还没有指定游标也在服务器端,在真正发生execute、executeQuery,会调用到ServerPreparedStatement的serverExecute方法中。 (6)在步骤5描述的方法ServerPreparedStatement.serverExecute()方法中,会再一次判定useCursorFetch的判定,如果useCursorFetch成立,则在发送给服务端的package中,开启游标的指令:1(MysqlDefs.OPEN_CURSOR_FLAG),如下: 当开启游标的时候,服务端返回数据的时候,就会按照fetchSize的大小返回数据了,而客户端接收数据的时候每次都会把换缓冲区数据全部读取干净(可复用不开启游标方式的代码)。 PS:关于PreparedStatement在MySQL JDBC当中是有潜在问题的,无论是否开启服务端Prapare都有一些坑存在,这些我会在后续的一些文章当中逐步讲到。 【方式3:Stream读取数据】 我们知道第1种方式会导致Java挂掉,第2种方式效率低而且对MySQL数据库的影响较大,客户端响应也较慢,仅仅能够解决问题而已,那么现在来看下Stream读取方式。 前面提到当你使用statement.setFetchSize(Integer.MIN_VALUE)或com.mysql.jdbc.StatementImpl.enableStreamingResults()就可以开启Stream读取结果集的方式,在发起execute之前FetchSize不能再手工设置,且确保游标是FORWARD_ONLY的。 这种方式很神奇,似乎内存也不挂了,响应也变快了,对MySQL的影响也变小了,至少IOPS不会那么大了,磁盘占用也没有了。以前仅仅看到JDBC中走了单独的代码,认为这是MySQL和JDBC之间的另一种通信协议,殊不知,它竟然是“客户端行为”,没错,你没看错,它就是客户端行为。 它在发起enableStreamingResults()的时候,几乎不会做任何与服务端的交互工作,也就是服务端会按照方式1回传数据,那么服务端使劲向缓冲区怼数据,客户端是如何扛得住压力的呢? 在JDBC当中,当你开启Stream结果集处理的时候,它并不是一把将所有数据读取到Java内存中的,也就是图1中并不是一次性将数据读取到Java缓冲区的,而是每次读取一个package(这个package可以理解成Java中的一个byte[]数组),一次最多读取这么多,然后会看是否继续向下读取保证数据的完整性。业务代码是按照字节解析成行也业务方使用的。 服务端刚开始使劲向缓冲区怼数据,这些数据也会怼满客户端的内核缓冲区,当两边的缓冲区都被怼满的时候,服务端的1个Buffer尝试通过TCP传递数据给接收方时,此时由于消费方的缓冲区也是满的,因此发送方的线程会阻塞住,等待对方消费,对方消费一部分,就可以推送一部分数据过去。连起来看就是JDBC的Stream数据未来得及消费之前,缓冲区数据如果是满的,那么MySQL发送数据的线程就阻塞住了,这样确保了一个平衡(关于这一点,大家可以使用Java的Socket来尝试下是否是这样的)。 对于JDBC客户端,数据获取的时候每次都在本地的内核缓冲区当中,就在小区的快递包裹箱拿回家一个距离,那么自然比起每次去超市的RT要小得多了,而且这个过程是准备好的数据,所以没有IO阻塞的过程(除非MySQL服务端传递的数据还不如消费端处理数据来得快,那一般也只有消费端不做任何业务,拿到数据直接放弃的测试代码,才会发生这样的事情),这个时候不论:跨机房、跨地区、跨国家,只要服务端开始响应就会源源不断地传递数据过来,而这个动作即使是第1种方式也是必然需要经历的过程。 Stream读取方式是不是就没有问题了呢?肯定是有的,而且还不止一个两个坑,这篇文章我没法一一说清楚,也和每一个人所遇到的情况有所不同,也会遇到一些比较偏的问题和坑,在本文中主要针对对业务的影响程度来看: 【优缺点对比】: 读取方式 优点 缺点 默认参数读取 1、代码简单、JDBC逻辑简单 2、OLTP单行操作速度最佳 3、对MySQL的业务影响小 1、数据量大的时候内存会溢出 2、需要Java程序将所有的数据读取到JVM中才响应程序 3、一旦服务端开始返回数据(不是JDBC响应,是MySQL的服务端准备一条数据开始)无法cancel,且在数据准备好以前,cancel会被阻塞 useCursorFetch 1、相对方式1不会导致内存溢出 2、相对方式3对数据库影响时间更短 1、会占用数据库磁盘空间 2、占用更多的IOPS 3、需要MySQL Server将所有数据准备好,才会响应程序 4、网络RT会根据数据量产生数百倍乃至数千倍的放大。 5、数据准备阶段发起cancel操作会阻塞(可在MySQL服务端数据准备前cancel掉) 6、数据传输阶段发起cancel操作无效 stream读取 1、相对方式1不会内存溢出 2、相对方式3整体速度更快 3、在几种方式中,读取大数据量,响应第一条数据的时间是最短的 4、跨地域传送大量数据,不会放大RT 1、相对方式2,对数据库影响时间会更长一些 2、相对方式1,会多一些系统调用次数。 3、cancel无效,cancel不阻塞 【对业务的影响对比】:在MySQL 5.7下分别测试MyISAM、InnoDB两种存储引擎: MyISAM InnoDB useCursor 数据准备阶段: 单条操作:可读、可DDL、写操作阻塞 交叉操作:发起写操作阻塞,接着读操作会阻塞 交叉操作:DDL后,写操作阻塞,读操作不阻塞,但此时写操作阻塞阶段不同,不会阻塞读操作 PS:DDL需要等待数据准备阶段完成后才能执行下去,但在数据准备阶段DDL已在运行中。 读取数据过程中: 单条操作:可读、可做DDL、可写操作 交叉操作:写入后,可读、可DDL 交叉操作:DDL后,写操作阻塞,读操作不阻塞 数据准备阶段: 单条操作:可读、可写、可DDL 交叉操作:写操作,再读取和DDL不会阻塞 交叉操作:先DDL,读、写均会被阻塞 PS:DDL需要等待数据准备阶段完成后才能执行下去,但在数据准备阶段DDL已在运行中。 读取数据过程中: 单条操作:可读、可写、可DDL 交叉操作:写操作,再读取和DDL不会阻塞 交叉操作:先DDL,读、写均不会被阻塞 stream读取 整个Stream读取过程: 1、单条操作:可读、可做DDL、写操作阻塞 2、交叉操作:发起写操作阻塞,接着读操作会阻塞 3、交叉操作:先做DDL,读操作不阻塞,写操作阻塞,但此时写操作阻塞阶段不同,不会阻塞读操作 4、交叉操作:步骤3阻塞了写操作,此时将DDL Kill掉,写操作会进入步骤2的阻塞状态,阻塞掉所有的读取操作。 整个Stream读取过程: 单条操作:可读、可写、可DDL 交叉操作:写操作后,不阻塞读取和DDL 交叉操作:DDL后,读操作阻塞、写操作阻塞 PS:DDL本身可以在这个过程中运行但在Stream读取完成前它无法结束,要等待数据读取完成才结束(如果DDL本身比Stream要快),但DDL已到最后阶段,也就是说Stream读取的时候,DDL是在运行的,只是在最后阶段需获取meta锁时阻塞住了。 【理论上可以更进一步,只要你愿意】 理论上这种方式是比较好的了,但是就完美主义来讲,我们可以继续探讨一下,对于懒人来讲,我们连到小区楼下快递包裹箱去拿一下的动力也是没有的,我们心里想的就是要是谁给我拿到家里来送到我嘴巴里,连嘴巴都给我掰开多好。 在技术上理论上确实可以做到这样,因为JDBC从内核拷贝内存到Java当中是需要花时间的,要是有另一个人把这个事情做了,我在家里干别的事情的时候它就给我送到家里来了,我要用的时候就直接从家里来,这个时间岂不是省掉了。每错,对于你来讲确实省掉了,不过问题就是谁来送? 在程序中一定需要加一个线程来干这个事情,把内核的数据拷贝到应用内存,甚至于解析成行数据,应用程序直接使用,但这一定完美吗?其实这个中间就有个协调问题了,例如家里要炒菜,缺一包调料,原本可以自己到楼下买,但是非要让别人送家里,这个时候其它的菜都下锅了,就剩一包调料,那么你没别的办法,只能等这包调料送到家里来以后才能进行炒菜的下一道工序。所以,在理想情况下,它可以节约很多次内存拷贝时间,会增加一些协调锁的开销。 那么可以不可以直接从内核缓冲区读取数据呢? 理论上也是可以的,在解释这个问题之前,我们先了解下除了这一次内存拷贝还有那些: JDBC按照二进制将内核缓冲区的数据读取后,也会进一步解析成具体的结构化数据,由于此时要给业务方返回ResultSet的具体行的结构化数据,也就是生成RowData的数据一定会有一次拷贝,而且JDBC返回某些对象类型数据的时候(例如byte []数组),在某些场景的实现,它不希望你通过结果集修改返回结果中的byte []的内容(byte[1] = 0xFF)去修改ResultSet本身内容,可能还会再做1次内存拷贝,业务代码使用过程中还会存在拼字符串,网络输出等,又是一堆的内存拷贝,这些在业务层面是无法避免的,相对这点点拷贝来讲,简直微不足道,所以我们也没去干这事情,以为从整体上看几乎微不足道,除非你的程序瓶颈在这里。 因此从整体上看内存拷贝是无法避免的,多的这一次无非是系统级的调用,开销会更大一点,从技术上来讲,我们是可以做到直接从内核态直接读取数据的;但这个时候就需要按照字节将Buffer从的数据拿走才能让远程更多的数据传递过来,没有第三个位置存放Buffer了,否则又回到了内核到应用的内存拷贝上来了。 相对来讲,服务端倒是可以优化直接将数据通过直接IO的方式传递(不过这种方式数据的协议就和数据的存储格式一致了,显然只是理论上的), 要真正做到自定义的协议,又要通过内核态数据直接发送,需要通过修改OS级别的文件系统协议,来达到转换的目的。
遇到一个MySQL JDBC执行execute方法时指定queryTimeout的坑,比较恶心,算是它的BUG,也可以不算,^_^,为啥这么说?看一下下面的解释: 现象: 用同一个Connection执行大批量SQL的时候,导致了OOM现象。 细节现象描述: 1、SQL是从某个存储设备上拿到的,不会直接占用大量的内存,每次只会取最多1千条数据过去,也会判定容量不超过多少M。 2、每一批SQL执行会单独创建Statement对象,执行一批SQL后,会将这个Statement关闭掉。 3、SQL语句中只有insert,没有其它的语句。 疑问: 这尼玛是什么蛋疼的问题?所有代码也review并debug过,参数是自己理想状态,看了下MySQLJDBC中的StatementImpl.close()的代码会清理掉相应的结果集以及数据,不会留下啥垃圾。 dump内存: dump内存后发现几十万个CancelTask对象,它是StatementImpl的内部类,最终会放到ConnectionImpl中的一个静态Timer类型的对象中。 下面来分析这几个问题:这个对象是干什么的?在什么时候创建的?何时回销毁?坑在那里? 这个对象是干什么的?在什么时候创建的? 这个对象是用于将执行中的SQL取消掉的任务对象,当SQL执行前,通过Statement.setQueryTimeout(int)时(参数单位为秒),这个参数的值只要不是0,它就会在JDBC内部与MySQL通信前会创建一个任务,这个任务会放入到一个Timer的任务队列中(请参看博客中专门介绍Timer与TimerTask的文章)。 它何时回被销毁呢? 1、如果SQL语句在CancelTask还未被Timer调度前响应,则会在JDBC代码中执行调用CancelTask.cancel()方法。 2、如果SQL语句一直未响应,CancelTask在达到设置的设置的timeout值时会一般会被Timer调度,如果已经是cancel状态不执行取消SQL执行操作,直接从队列中移除,如果CancelTask还没有被cancel,则会向MySQL发送相应的取消命令,让其回收资源。Timer在调度这个任务的时候CancelTask内部会创建新的线程来处理,因此Timer很快就会认为任务执行完了,也就是和取消SQL本身的时间无关,Timer也会将这个任务对象从队列中移除,因为这个任务并不是循环执行的。 似乎销毁也是很完善的,那么坑到底在那里呢? 1、根据业务需要,这个Statement.setQueryTimeout(int)这个值设置得非常大。 2、当大批量的SQL同时执行时,每一个SQL都会创建一个CancelTask对象,虽然很快执行完,且会调用CancelTask.cancel()方法,但是CancelTask方法的源代码仅仅是将自己的状态修改为:CANCELLED,而并不会直接从队列中移除这个对象,只有等到超过queryTimeout的值时被Timer调度,才会从队列中移除。 注意:在MySQL JDBC 5.1.13版本有一个purge操作,但是这个操作对execute方法存在BUG,因为它在这个方法的try里面执行了这部分代码: if (timeoutTask != null) { if (timeoutTask.caughtWhileCancelling != null) { throw timeoutTask.caughtWhileCancelling; } timeoutTask.cancel(); timeoutTask = null;} 这里将timeoutTask设置为null了,但没有purge,导致了一个问题就是在finally里面不会进入if语句,从而不会执行purge操作,也会导致问题,这个问题一直延续到现在的最新版本5.1.34。不过executeQuery、executeUpdate方法是在5.1.13版本后修复了这个问题。 3、因此大批量的SQL同时运行时,并很快结束时,JDBC中存放了大量的CancelTask的生命周期如果自己不结束,这个对象是和Timer相关,那么Timer是什么级别的呢? 4、经过源码跟踪,虽然Timer定义在Connection中,但是static修饰的,也就是是全局级别的,换句话说:即使将这个Connection.close(),也不会释放掉这些CancelTask对象所占用的空间。(MySQL JDBC 于5.1.11版本修改为非静态成员变量,但是这个版本还没有做purge,因此还没有真正解决问题,关于5.1.13增加purge请参看上面的说明,而另外需要注意的是修改为非静态成员后,每一个连接都会有一个单独的线程Timer在后台运行,因此在设计上可能需要注意些什么)。 5、通过上面dump内存图看到,每一个CancelTask对象会占用7K左右的空间,29W个对象就会占用将近2G空间。 结论:只要在timeout值没有达到之前,超过一定数量的SQL被执行(不分单线程还是多线程),内存肯定就蹦了。 临时性的解决方法: 对某些大批量的SQL执行execute方法入口不设置timeout,或设置时间非常短的timeout,这个要根据实际场景来讲。 但这样可能会带来更多的问题,所以会陷入一个圈子中。终极方案有点蛋疼,因为这个取舍问题有点麻烦,哥有点想把源代码的这一块改一改,给官网提交了不少BUG,认可了,但没见他们改过。本文只是先让大伙知道有这么一个坑存在。 下面简单贴几小段MySQL JDBC的源码,有兴趣可以看下: 《代码段1:设置QueryTimeout》 public void setQueryTimeout(int seconds) throws SQLException { if (seconds < 0) { throw SQLError.createSQLException(Messages .getString("Statement.21"), //$NON-NLS-1$ SQLError.SQL_STATE_ILLEGAL_ARGUMENT); //$NON-NLS-1$ } this.timeoutInMillis = seconds * 1000; } 《代码段2:如果这个timeout不是0,就会创建一个新的Task》 if (locallyScopedConn.getEnableQueryTimeouts() && this.timeoutInMillis != 0 && locallyScopedConn.versionMeetsMinimum(5, 0, 0)) { timeoutTask = new CancelTask(this); ConnectionImpl.getCancelTimer().schedule(timeoutTask,this.timeoutInMillis); } 《代码段3:SQL执行完会调用Cancel.cancel()方法》if (timeoutTask != null) { timeoutTask.cancel(); } 《代码段4:java.util.Timer的添加任务到队列中的关键部分回顾》 void add(TimerTask task) { // Grow backing store if necessary if (size + 1 == queue.length) queue = Arrays.copyOf(queue, 2*queue.length); queue[++size] = task; fixUp(size); } 《代码段5:TimerTask是CancelTask的父类,其的cancel方法主要就是为了设置状态》 public boolean cancel() { synchronized(lock) { boolean result = (state == SCHEDULED); state = CANCELLED; return result; } } 关于Timer调度部分的源码我就不贴了,以前在其它文章中有描述。 总结下: 1、5.1.11版本后将Timer改为非静态成员,和Conenction绑定,但没有做purge操作,因此没有真正解决问题。另外,每一个连接都会在后台多一个线程出来。 2、5.1.13版本在finally以及相应执行完成部分添加了purge回收资源操作,但是对于execute方法是存在BUG的,这个BUG延续到现在最新版本5.1.34对于executeUpdate、executeQuery方法是可以正常完成了。 3、虽然已经可以顺利完成purge,但要考虑一下这个顺利完成的代价是不断地通过synchonized加锁对队列进行处理,这样也会带来一定得系统开销,所以呢根据实际场景如果能够不使用的情况下可尽量避免使用。
大家好,去年说要写本Java书,近期就快出版了。目前已经开始打印样书了,最快于本月中旬左右就可以在互动网www.china-pub.com上看到消息,其它各个网站何时会发售要看具体进货情况。 去年我预期是半年写好这本书,6个月左右确实将手稿写好,但由于是第一次写书,所以没有意料到许多review的成本也是很高的,另外需要在每次review过后与出版社沟通,一直拖到现在才准备出版(而且还只出了上册),很多小伙伴已经等得花儿都谢了,哈哈!我也有类似的感觉,去年写的有些内容现在已经快过时了,呵呵,不过还好重点不是技术点本身。 经过几次review下来后,书中改掉许多问题,也删掉了一些内容,肯定比手稿看起来要顺畅很多。不过肯定还有一些没有注意到的地方,希望大家能够谅解。 这本书在写的过程中越写越多,去年年底发现写的内容已经远远超过了计划的字数和篇幅。因此与出版社决定分为“上下册”出版,本次出版的就是上册,上册是完全讲解基础的,也是胖哥认为最好的。本次出版的也是上册,据出版社介绍,上册内容部分总共为490页。 这本书从写这一年多以来很久,很多小伙伴一直对这本书的名字很感兴趣,哈哈,本书的名字还是有点霸气的,叫做《Java特种兵》,不过我想说的是名字只是一个嚼头,并不是说看了这本书就成特种兵了,具体细节可以看书里面的前言部分,本书封面基本定下来,可参考下图: 这本书上册有2个篇,一个是基础篇、一个是源码篇。最近也有很多小伙伴问我到底要写什么,这里我就贴下目录: 上册目录如下: 第1篇 Java功底篇 第1章 扎马:看看功底如何 1.1 String的例子,见证下我们的功底 1.1.1 关于“==” 1.1.2 关于“equals()” 1.1.3 编译时优化方案 1.1.4 补充一个例子 1.1.5 跟String较劲上了 1.1.6 intern()/equals() 1.1.7 StringBuilder.append()与String“+”的PK.. 1.2 一些简单算法,你会如何理解 1.2.1 从一堆数据中找max和min 1.2.2 从100万个数字中找最大的10个数字 1.2.3 关于排序,实际场景很重要 1.2.4 数据库是怎么找数据的 1.2.5 Hash算法的形象概念 1.3 简单数字游戏玩一玩 1.3.1 变量A、B交换有几种方式 1.3.2 将无序数据Hash到指定的板块 1.3.3 大量判定“是|否”的操作 1.3.4 简单的数据转换 1.3.5 数字太大,long都存放不下 1.4 功底概述 1.4.1 什么是功底 1.4.2 功底有何用途 1.4.3 如何磨练功底 1.5 功底补充 1.5.1 原生态类型 1.5.2 集合类 1.6 常见的目录与工具包 1.7 面对技术,我们纠结的那些事儿 1.7.1 为什么我这里好用,哪里不好用 1.7.2 你的程序不好用,你会不会用,环境有问题 1.7.3 经验是否能当饭吃 1.8 老A是在逆境中迎难而上者 第2章 Java程序员要知道计算机工作原理 2.1 Java程序员需要知道计算机工作原理吗? 2.2 CPU的那些事儿 2.2.1 从CPU联系到Java 2.2.2 多核 2.2.3 Cache line 2.2.4 缓存一致性协议 2.2.5 上下文切换 2.2.6 并发与征用 2.3 内存 2.4 磁盘 2.5 缓存 2.5.1 缓存的相对性 2.5.2 缓存的用途和场景 2.6 关于网络与数据库 2.6.1 Java基本I/O 2.6.2 Java的网络基本原则 2.6.3 Java与数据库的交互 2.7 总结 第3章 JVM,Java程序员的OS 3.1 学习Java虚拟机对我们有什么好处 3.2 跨平台与字节码基本原理 3.2.1 javap命令工具 3.2.2 Java字节码结构 3.2.3 Class字节码的加载 3.2.4 字节码增强 3.3 从虚拟机的板块开始 3.3.1 Hotspot VM板块划分 3.3.2 “对象存放位置”小总结 3.3.3 关于永久代 3.4 常见的虚拟机回收算法 3.4.1 串行GC 3.4.2 ParallelGC与ParallelOldGC 3.4.3 CMS GC与未来的G1 3.4.4 简单总结 3.4.5 小小补充 3.5 浅析Java对象的内存结构 3.5.1 原始类型与对象的自动拆装箱 3.5.2 对象内存结构 3.5.3 对象嵌套 3.5.4 常见类型 & 集合类的内存结构 3.5.5 程序中内存拷贝和垃圾 3.5.6 如何计算对象大小 3.5.7 轻松玩一玩int[2][100]PK int[100][2] 3.6 常见的OOM现象 3.6.1 HeapSize OOM 3.6.2 PermGen OOM 3.6.3 DirectBuffer OOM 3.6.4 StackOverflowError 3.6.5 其他的一些内存溢出现象 3.7 常见的Java工具 3.7.1 jps. 3.7.2 jstat 3.7.3 jmap. 3.7.4 jstack. 3.7.5 jinfo. 3.7.6 JConsole. 3.7.7 Visual VM... 3.7.8 MAT(MemoryAnalyzer Tool)... 3.7.9 BTrace. 3.7.10 HSDB.. 3.7.11 工具总结... 3.8 总结.... 3.8.1 写代码... 3.8.2 心理上战胜虚拟机带来的恐惧 第4章 Java通信,交互就需要通信 4.1 通信概述.... 4.1.1 Java通信的基本过程... 4.1.2 Java通信的协议包装... 4.1.3 编写自定义通信协议... 4.1.4 Java的I/O流是不是很难学... 4.2 Java I/O与内存的那些事.... 4.2.1 常规I/O操作的运作过程... 4.2.2 DirectBuffer的使用... 4.2.3 关于Buffer 4.2.4 FileChannel的加锁... 4.3 通信调度方式.... 4.3.1 同步与异步... 4.3.2 阻塞与非阻塞... 4.3.3 Linux OS调度IO模型... 4.3.4 Java中的BIO、NIO.. 4.3.5 Java AIO.. 4.4 Tomcat中对I/O的请求处理.... 4.4.1 Tomcat的配置&一个请求的响应... 4.4.2 Request、Response对象生成... 4.4.3 拉与推... 第5章 Java并发,你会遇到吗.............. 5.1 基础介绍.... 5.1.1 线程基础... 5.1.2 多线程... 5.1.3 线程状态... 5.1.4 反面教材suspend()、resume()、stop() 5.1.5 调度优先级... 5.1.6 线程合并(Join)... 5.1.7 线程补充小知识... 5.2 线程安全.... 5.2.1 并发内存模型概述... 5.2.2 一些并发问题描述... 5.2.3 volatile. 5.2.4 final 5.2.5 栈封闭... 5.2.6 ThreadLocal 5.3 原子性与锁.... 5.3.1 synchronized. 5.3.2 什么是乐观锁... 5.3.3 并发与锁... 5.3.4 Atomic. 5.3.5 Lock. 5.3.6 并发编程核心AQS原理... 5.3.7 锁的自身优化方法... 5.4 JDK 1.6并发编程的一些集合类.... 5.5 常见的并发编程工具.... 5.5.1 CountDownLatch. 5.5.2 CyclicBarrier 5.5.3 Semaphor 5.5.4 其他工具简介... 5.6 线程池&调度池.... 5.6.1 阻塞队列模型... 5.6.2 ThreadPoolExecutor 5.6.3 调度器ScheduleThreadPoolExecutor 5.7 总结:编写并发程序要注意些什么.... 5.7.1 锁粒度... 5.7.2 死锁... 5.7.3 “坑”很多... 5.7.4 并发效率一定高吗... 5.8 其他的并发编程知识.... 5.8.1 ShutdownHook(钩子线程) 5.8.2 Future. 332 5.8.3 异步并不等价于多线程... 第6章 好的程序员应当知道数据库基本原理........................................... 6.1 开发人员为什么要知道数据库原理.... 6.2 从开发人员角度看数据库原理.... 6.2.1 实例与存储... 6.2.2 数据库基本原理... 6.2.3 索引基本原理... 6.2.4 数据库主从基本原理... 6.2.5 我们经常相信的那些经验... 6.3 从程序员角度看数据库优化方法.... 6.3.1 不同领域的SQL区别... 6.3.2 执行计划... 6.3.3 SQL逻辑的例子... 6.3.4 模型结构设计的优化... 6.3.5 临时表... 6.3.6 分页知识补充... 6.3.7 计算count值... 6.3.8 分布式事务探讨... 6.3.9 其他... 6.4 学会最基本的性能诊断.... 6.4.1 进入云数据库时代... 6.4.2 从程序员角度关注的数据库诊断信息... 6.5 数风流存储,还看今朝.... 第2篇 源码篇 第7章 源码基础 7.1 为何会出现框架.... 7.2 阅读框架前的技术储备.... 7.2.1 反射基础知识... 7.2.2 AOP基础... 7.2.3 ORM基础... 7.2.4 Annotation与配置文件... 第8章 部分JDBC源码讲解.. 8.1 JDBC通用接口规范.... 8.2 JDBC Driver注册.... 8.3 创建Connection. 8.4 SQL执行及处理.... 8.4.1 创建Statement 8.4.2 Batch设置批处理... 8.4.3 fetchSize与maxRows. 8.4.4 setQueryTimeout()与cancel() 第9章 部分Spring源码讲解 9.1 Spring MVC 9.1.1 Spring加载 9.1.2 Spring MVC处理一个简单请求 9.2 Spring事务管理器 9.2.1 JDBC事务的基本思想 9.2.2 Spring事务管理器的基本架构 9.2.3 Spring如何保存Connection 9.2.4 Spring如何保证程序中多次获取到的连接是同一个 9.3 思考:自己做框架有眉目了吗 第10章 看源码的一些总结. 10.1 高手看API的能力 10.2 通过源码能否量化性能与稳定性 10.3 思考相似方案和技术的优缺点 10.4 明确场景和业务,不做技术控 10.4.1 谈谈技术控的那些事.. 10.4.2 明确业务背景的例子.. 10.5 胖哥对框架的浅析... 10.5.1 框架由来的一个补充. 10.5.2 开源框架与扩展.. 10.5.3 框架与解决问题. 10.6 学海无涯,心境无限. 下册的手稿也已经写好,但是具体的目录和内容还会有所改动,不过不会有非常大的变动了,为了满足大家的好奇心,我写一下下册大概要写的内容(在上册的前言中也有): 1、设计篇,设计篇会提到设计模式和思想,但是胖哥会提出一些探讨性的观点和生活中的思想,然后我们再用一些实例来设计一些东西(2-3章)。 2、实现篇,主要讲解项目中的你我他,胖哥会讲讲自己所经历的一些项目中的稀奇古怪的事情,做事情的方法,人与人之间的合作,一些技术坑。 3、扩展篇,扩展篇比较杂,有走马观花式探讨一些集群、分布式的技术知识,也有探讨思想方法,看问题的角度,做IT人的心态等。 大概就这些了,下次大概就这些了,上册如果大家喜欢的话,下册我也会竭尽所能尽快review好,好了,就这些了! 等书出版的时候我再发个帖子,哈哈!
你在万网购买虚拟主机的时候,应该会给你一个数据库的信息,账号密码自然是在里面分配给你的数据库访问的账号和密码的。
额,不是很明确你的问题,可以截图或描述得更加明确一些吗?
DMS如果是登录前,是需要你自己明确数据库的连接地址的和账号密码的,如果是登录进去了,可以再右上角看到连接地址,但是密码是不显示在前端的(出于互联网安全考虑)。
请问一下,现在还有这类问题吗?如果有的话,麻烦回复一下,我们及时跟进。很少逛论坛,回复晚啦,不好意思。
确切来讲,不是不具有查询权限哈,是不具有mysql.db这个表的查询权限,这个字典表里面是数据库的元数据信息。
您应该是在DMS里面使用数据库的管理功能,对库进行管理,就肯定要查这个元数据表,普通的用户确实没有这个表的查询权限。
所以,在第二句提示里面告诉了您两种方案:
1、具有mysql.db查询权限或超级权限(因为超级权限肯定有mysql.db的权限,例如root账号)。
2、直接给用户SUPER角色,有这个角色,这些管理功能都可以用了。
两者用或字连接,自然是二选一就可以啦。您应该稍微花半分钟读一下文字内容,就可以知道如何解这个问题啦。
额,我来解释下吧,这个操作的意思,不是说没有zd这个数据库,这个提示很明确:
1、你执行的语句是:CREATE DATABASE
2、错误信息是:拒绝执行这样的语句,对于账号bdmxxx@xxxx
一般是导入账号权限的问题,一般来讲RDS早期售卖的实例提供给大家的账号权限是没有CREATE DATABASE权限的,近期RDS新的实例是提供了高权限账号的,通过高权限账号是可以执行授权语句的,不过通过高权限账号所创建的新数据库账号,是没有这样的授权能力的。
你是访问万网的实例吧,你从万网的虚拟主机上点击登录DMS,地址应该会带过来才是,如果你直接访问DMS需要自己输入。
这里让你输入的是数据库的地址+端口,就像你用客户端工具访问数据库,要填写数据库地址,或者你用程序访问数据库,要填写数据库地址一样的道理,如果你在万网购买过服务,他们应该会给你一个数据库,在万网的控制台上应该是看得到的。
你是访问万网的实例吧,你从万网的虚拟主机上点击登录DMS,地址应该会带过来才是,如果你直接访问DMS需要自己输入。
这里让你输入的是数据库的地址+端口,就像你用客户端工具访问数据库,要填写数据库地址,或者你用程序访问数据库,要填写数据库地址一样的道理。
这个提示就是说目标数据库的账号和密码有错误,所以登录不上,提示与解释一致,所以先别着急,目的是解决问题。
至于数据库的账号密码要确认下是否真的正确,如果是正确的拷贝过程中看下是否有特殊字符被粘贴进来,如果有,将其去掉。DMS每天就登录实例这个动作的次数是有数万次的,也是数据库的入口,一般来讲这个提示问题不大。
同学,在进入的首页,左侧就是表列表,在哪个位置点击左键或右键都有建表,在顶部的菜单,第一个“新建”里面第一个就是新建表啦,如下图:
DMS没有将所有的功能平铺展开,不过很多功能稍微点击下就有啦,DMS内部所拥有的功能和可以给予你的帮助不是传统软件工具所能比拟的,类似于建表、查询等类似功能,仅仅是DMS内部的基础功能的很小一部分,也是最为基础的一个部分,因为这是您工作的必需品,我们也会尽量在WEB交互上做到接近于客户端的效果。
如果还有问题,请联系下我。
PS:DMS如果仅仅是原有工具的功能拷贝是远远不够的,我们更多的是在这个基础上提供更加丰富和专业的数据库功能,为你提升效率和降低成本,例如如果您的业务数据误删除或丢了,可以帮你快速找回来,以前你可能需要很专业的DBA花较长时间才能搞定,但是在DMS产品上任何人稍微看看就能通过其中的数据追踪功能将数据迅速找回。这只是一个工作中常见的案例,还有涉及到数据处理的方方面面DMS都已经支持得不错或已经在规划中,可以提升业务的发展速度也降低你的成本,这样您们才可以更加专注于自己的产品和业务,这也是结合云计算这个时代的发展所提供的共享服务,且这种共享服务在云上会更加安全、有保障、可追溯。
-------------------------
也可以对比下终端和其它软件对同一个目标库的显示结果和导出结果看下,如果有什么问题,可以直接联系我。