在高访问量的web系统中,缓存几乎是离不开的;可是一个适当、高效的缓存方案设计却并不容易;因此接下来将讨论一下应用系统缓存的设计方面应该注 意哪些东西,包括缓存的选型、常见缓存系统的特色和数据指标、缓存对象结构设计和失效策略以及缓存对象的压缩等等,以期让有需求的同窗尤为是初学者可以快 速、系统的了解相关知识。html
关系型数据库的数据量是比较小的,以咱们经常使用的MySQL为例,单表数据条数通常应该控制在2000w之内,若是业务很复杂的话,可能还要低一些。即使是对于Oracle这些大型商业数据库来说,其能存储的数据量也很难知足一个拥有几千万甚至数亿用户的大型互联网系统。前端
在实际开发中咱们常常会发现,关系型数据库在TPS上的瓶颈每每会比其余瓶颈更容易暴露出来,尤为对于大型web系统,因为天天大量的并发访问,对 数据库的读写性能要求很是高;而传统的关系型数据库的处理能力确实捉襟见肘;以咱们经常使用的MySQL数据库为例,常规状况下的TPS大概只有1500左右 (各类极端场景下另当别论);下图是MySQL官方所给出的一份测试数据:html5
而对于一个日均PV千万的大型网站来说,每一个PV所产生的数据库读写量可能要超出几倍,这种状况下,天天全部的数据读写请求量可能远超出关系型数据的处理能力,更别说在流量峰值的状况下了;因此咱们必需要有高效的缓存手段来抵挡住大部分的数据请求!程序员
正常状况下,关系型数据的响应时间是至关不错的,通常在10ms之内甚至更短,尤为是在配置得当的状况下。可是就如前面所言,咱们的需求是不通常的:当拥有几亿条数据,1wTPS的时候,响应时间也要在10ms之内,这几乎是任何一款关系型数据都没法作到的。web
那么这个问题如何解决呢?最简单有效的办法固然是缓存!算法
本地缓存多是你们用的最多的一种缓存方式了,无论是本地内存仍是磁盘,其速度快,成本低,在有些场合很是有效;chrome
可是对于web系统的集群负载均衡结构来讲,本地缓存使用起来就比较受限制,由于当数据库数据发生变化时,你没有一个简单有效的方法去更新本地缓 存;然而,你若是在不一样的服务器之间去同步本地缓存信息,因为缓存的低时效性和高访问量的影响,其成本和性能恐怕都是难以接受的。数据库
前面提到过,本地缓存的使用很容易让你的应用服务器带上“状态”,这种状况下,数据同步的开销会比较大;尤为是在集群环境中更是如此!后端
分布式缓存这种东西存在的目的就是为了提供比RDB更高的TPS和扩展性,同时有帮你承担了数据同步的痛苦;优秀的分布式缓存系统有你们所熟知的 Memcached、Redis(固然也许你把它当作是NoSQL,可是我我的更愿意把分布式缓存也当作是NoSQL),还有国内阿里自主开发的Tair 等;数组
对比关系型数据库和缓存存储,其在读和写性能上的差距可谓天壤之别;memcached单节点已经能够作到15w以上的tps、Redis、google的levelDB也有不菲的性能,而实现大规模集群后,性能可能会更高!
因此,在技术和业务均可以接受的状况下,咱们能够尽可能把读写压力从数据库转移到缓存上,以保护看似强大,其实却很脆弱的关系型数据库。
这块很容易被人忽略,客户端缓存主要是指基于客户端浏览器的缓存方式;因为浏览器自己的安全限制,web系统能在客户端所作的缓存方式很是有限,主要由如下几种:
a、 浏览器cookie;这是使用最多的在客户端保存数据的方法,你们也都比较熟悉;
b、 浏览器本地缓存;不少浏览器都提供了本地缓存的接口,可是因为各个浏览器的实现有差别,因此这种方式不多被使用;此类方案有chrome的Google Gear,IE的userData、火狐的sessionStorage和globalStorage等;
c、 flash本地存储;这个也是平时比较经常使用的缓存方式;相较于cookie,flash缓存基本没有数量和体积的限制,并且因为基于flash插件,因此也不存在兼容性问题;不过在没有安装flash插件的浏览器上则没法使用;
d、 html5的本地存储;鉴于html5愈来愈普及,再加上其本地存储功能比较强大,因此在未来的使用场景应该会愈来愈多。
因为大部分的web应用都会尽可能作到无状态,以方便线性扩容,因此咱们能使用的除了后端存储(DB、NoSQL、分布式文件系统、CDN等)外,就只剩前端的客户端缓存了。
对客户端存储的合理使用,本来天天几千万甚至上亿的接口调用,一下就可能降到了天天几百万甚至更少,并且即使是用户更换浏览器,或者缓存丢失须要重 新访问服务器,因为随机性比较强,请求分散,给服务器的压力也很小!在此基础上,再加上合理的缓存过时时间,就能够在数据准确和性能上作一个很好的折衷。
这里主要是指数据库的查询缓存,大部分数据库都是会提供,每种数据库的具体实现细节也会有所差别,不过基本的原理就是用查询语句的hash值作key,对结果集进行缓存;若是利用的好,能够很大的提升数据库的查询效率!数据库的其余一些缓存将在后边介绍。
如今可供咱们选择使用的(伪)分布式缓存系统不要太多,好比使用普遍的Memcached、最近炒得火热的Redis等;这里前面加个伪字,意思是 想说,有些所谓的分布式缓存其实还是以单机的思惟去作的,不能算是真正的分布式缓存(你以为只实现个主从复制能算分布式么?)。
既然有这么多的系统可用,那么咱们在选择的时候,就要有必定的标准和方法。只有有了标准,才能衡量一个系统时好时坏,或者适不适合,选择就基本有了方向。
下边几点是我我的觉的应该考虑的几个点(其实大部分系统选型都是这么考虑的,并不是只有缓存系统):
废话,容量固然是越大越好了,这还用说么,有100G我干吗还要用10G?其实这么说总要考虑一下成本啦,目前一台普通的PC Server内存128G已经算是大的了,再大的话无论是从硬件仍是从软件方面,管理的成本都会增长。单机来说,好比说主板的插槽数量,服务器散热、操做 系统的内存分配、回收、碎片管理等等都会限制内存卡的容量;即使使用多机的话,大量内存的采购也是很费money的!
有诗云:山不在高,有仙则名;因此内存也不在多,够用就好!每一个系统在初期规划的时候,都会大体计算一下所要消耗的缓存空间,这主要取决于你要缓存 的对象数量和单个对象的大小。通常来讲,你能够采用对象属性在内存中的存储长度简单加和的方法来计算单个对象的体积,再乘以缓存对象的数量和预期增加(当 然,这里边有一个热点数据的问题,这里就不细讨论了),大概得出须要使用的缓存空间;以后就能够按照这个指标去申请缓存空间或搭建缓存系统了。
这里说并发量,其实还不如说是QPS更贴切一些,由于咱们的缓存不是直接面向用户的,而只面向应用的,因此确定不会有那个高的并发访问(固然,多个系统共用一套缓存那就另当别论了);因此咱们关心的是一个缓存系统平均每秒可以承受多少的访问量。
咱们之因此须要缓存系统,就是要它在关键时刻能抗住咱们的数据访问量的;因此,缓存系统可以支撑的并发量是一个很是重要的指标,若是它的性能还不如关系型数据库,那咱们就没有使用的必要了。
对于淘宝的系统来讲,咱们不妨按照下边的方案来估算并发量:
QPS = 日PV × 读写次数/PV ÷ (8 × 60 × 60)
这里咱们是按照一天8个小时来计算的,这个值基于一个互联网站点的访问规律得出的,固然,若是你不一样意这个值,能够本身定义。
在估算访问量的时候,咱们不得不考虑一个峰值的问题,尤为是像淘宝、京东这样大型的电商网站,常常会由于一些大的促销活动而使PV、UV冲到平时的 几倍甚至几十倍,这也正是缓存系统发挥做用的关键时刻;倍受瞩目的12306在站点优化过程当中也大量的引入了缓存(内存文件系统)来提高性能。
在计算出平均值以后,再乘以一个峰值系数,基本就能够得出你的缓存系统须要承受的最高QPS,通常状况下,这个系数定在10之内是合理的。
响应时间固然也是必要的,若是一个缓存系统慢的跟蜗牛同样,甚至直接就超时了,那和咱们使用MySQL也没啥区别了。
通常来讲,要求一个缓存系统在1ms或2ms以内返回数据是不过度的,固然前提是你的数据不会太大;若是想更快的话,那你就有点过度了,除非你是用的本地缓存;由于通常而言,在大型IDC内部,一个TCP回环(不携带业务数据)差很少就要消耗掉0.2ms至0.5ms。
大部分的缓存系统,因为是基于内存,因此响应时间都很短,可是问题通常会出如今数据量和QPS变大以后,因为内存管理策略、数据查找方式、I/O模 型、业务场景等方面的差别,响应时间可能会差别不少,因此对于QPS和响应时间这两项指标,还要靠上线前充分的性能测试来进一步确认,不能只单纯的依赖官 方的测试结果。
通常分布式缓存系统会包括服务端和客户端两部分,因此其使用成本上也要分为两个部分来说;
首先服务端,优秀的系统要是可以方便部署和方便运维的,不须要高端硬件、不须要复杂的环境配置、不能有过多的依赖条件,同时还要稳定、易维护;
而对于客户端的使用成原本说,更关系到程序员的开发效率和代码维护成本,基本有三点:单一的依赖、简洁的配置和人性化的API。
另外有一点要提的是,无论是服务端仍是客户端,丰富的文档和技术支持也是必不可少的。
缓存系统的扩展性是指在空间不足的性状况,可以经过增长机器等方式快速的在线扩容。这也是可以支撑业务系统快速发展的一个重要因素。
通常来说,分布式缓存的负载均衡策略有两种,一种是在客户端来作,另一种就是在服务端来作。
客户端负载均衡
在客户端来作负载均衡的,诸如前面咱们提到的Memcached、Redis等,通常都是经过特定Hash算法将key对应的value映射到固定 的缓存服务器上去,这样的作法最大的好处就是简单,无论是本身实现一个映射功能仍是使用第三方的扩展,都很容易;但由此而来的一个问题是咱们没法作到 failover。好比说某一台Memcached服务器挂掉了,可是客户端还会傻不啦叽的继续请求该服务器,从而致使大量的线程超时;固然,所以而形成 的数据丢失是另一回事了。要想解决,简单的可能只改改改代码或者配置文件就ok了,可是像Java这种就蛋疼了,有可能还须要重启全部应用以便让变动能 够生效。
若是线上缓存容量不够了,要增长一些服务器,也有一样的问题;并且因为hash算法的改变,还要迁移对应的数据到正确的服务器上去。
服务端负载均衡
若是在服务端来作负载均衡,那么咱们前面提到的failover的问题就很好解决了;客户端可以访问的全部的缓存服务器的ip和端口都会事先从一个 中心配置服务器上获取,同时客户端会和中心配置服务器保持一种有效的通讯机制(长链接或者HeartBeat),可以使后端缓存服务器的ip和端口变动即 时的通知到客户端,这样,一旦后端服务器发生故障时能够很快的通知到客户端改变hash策略,到新的服务器上去存取数据。
但这样作会带来另一个问题,就是中心配置服务器会成为一个单点。解决办法就将中心配置服务器由一台变为多台,采用双机stand by方式或者zookeeper等方式,这样可用性也会大大提升。
咱们使用缓存系统的初衷就是当数据请求量很大,数据库没法承受的状况,可以经过缓存来抵挡住大部分的请求流量,因此一旦缓存服务器发生故障,而缓存系统又没有一个很好的容灾措施的话,全部或部分的请求将会直接压倒数据库上,这可能会直接致使DB崩溃。
并非全部的缓存系统都具备容灾特性的,因此咱们在选择的时候,必定要根据本身的业务需求,对缓存数据的依赖程度来决定是否须要缓存系统的容灾特性。
Memcached严格的说还不能算是一个分布式缓存系统,我的更倾向于将其当作一个单机的缓存系统,因此从这方面讲其容量上是有限制的;但因为 Memcached的开源,其访问协议也都是公开的,因此目前有不少第三方的客户端或扩展,在必定程度上对Memcached的集群扩展作了支持,可是大 部分都只是作了一个简单Hash或者一致性Hash。
因为Memcached内部经过固定大小的chunk链的方式去管理内存数据,分配和回收效率很高,因此其读写性能也很是高;官方给出的数据,64KB对象的状况下,单机QPS可达到15w以上。
Memcached集群的不一样机器之间是相互独立的,没有数据方面的通讯,因此也不具有failover的能力,在发生数据倾斜的时候也没法自动调整。
Memcached的多语言支持很是好,目前可支持C/C++、Java、C#、PHP、Python、Perl、Ruby等经常使用语言,也有大量的文档和示例代码可供参考,并且其稳定性也通过了长期的检验,应该说比较适合于中小型系统和初学者使用的缓存系统。
Redis也是眼下比较流行的一个缓存系统,在国内外不少互联网公司都在使用(新浪微博就是个典型的例子),不少人把Redis当作是Memcached的替代品。
下面就简单介绍下Redis的一些特性;
Redis除了像Memcached那样支持普通的<k,v>类型的存储外,还支持List、Set、Map等集合类型的存储,这种特性有时候在业务开发中会比较方便;
Redis源生支持持久化存储,可是根据不少人的使用状况和测试结果来看,Redis的持久化是个鸡肋,就连官方也不推荐过分依赖Redis持久化 存储功能。就性能来说,在所有命中缓存时,Redis的性能接近memcached,可是一旦使用了持久化以后,性能会迅速降低,甚至会相差一个数量级。
Redis支持“集群”,这里的集群仍是要加上引号的,由于目前Redis可以支持的只是Master-Slave模式;这种模式只在可用性方面有必定的提高,当主机宕机时,能够快速的切换到备机,和MySQL的主备模式差很少,可是还算不上是分布式系统;
此外,Redis支持订阅模式,即一个缓存对象发生变化时,全部订阅的客户端都会收到通知,这个特性在分布式缓存系统中是不多见的。
在扩展方面,Redis目前尚未成熟的方案,官方只给出了一个单机多实例部署的替代方案,并经过主备同步的模式进行扩容时的数据迁移,可是仍是没法作到持续的线性扩容。
Tair是淘宝自主开发并开源的一款的缓存系统,并且也是一套真正意义上的分布式而且能够跨多机房部署,同时支持内存缓存和持久化存储的解决方案;咱们数平这边也有本身的改进版本。
Tair实现了缓存框架和缓存存储引擎的独立,在遵照接口规范的状况下,能够根据需求更换存储引擎,目前支持mdb(基于memcached)、 rdb(基于Redis)、kdb(基于kyoto cabinet,持久存储,目前已不推荐使用)和rdb(基于gooogle的levelDB,持久化存储)几种引擎;
因为基于mdb和rdb,因此Tair可以间距二者的特性,并且在并发量和响应时间上,也接近两者的裸系统。
在扩展性和容灾方面,Tair本身作了加强;经过使用虚拟节点Hash(一致性Hash的变种实现)的方案,将key经过Hash函数映射到到某个 虚拟节点(桶)上,而后经过中心服务器(configserver)来管理虚拟节点到物理节点的映射关系;这样,Tair不但实现了基于Hash的首次负 载均衡,同时又能够经过调整虚拟节点和物理节点的映射关系来实现二次负载均衡,这样有效的解决了因为业务热点致使的访问不均衡问题以及线性扩容时数据迁移 麻烦;此外,Tair的每台缓存服务器和中心服务器(configserver)也有主备设计,因此其可用性也大大提升。
这里的内存数据库只要是指关系型内存数据库。通常来讲,内存数据库使用场景可大体分为两种状况:
一是对数据计算实时性要求比较高,基于磁盘的数据库很难处理;同时又要依赖关系型数据库的一些特性,好比说排序、加合、复杂条件查询等等;这样的数据通常是临时的数据,生命周期比较短,计算完成或者是进程结束时便可丢弃;
另外一种是数据的访问量比较大,可是数据量却不大,这样即使丢失也能够很快的从持久化存储中把数据加载进内存;
但无论是在哪一种场景中,存在于内存数据库中的数据都必须是相对独立的或者是只服务于读请求的,这样不须要复杂的数据同步处理。
对于本地磁盘或分布是缓存系统来讲,其缓存的数据通常都不是结构化的,而是半结构话或是序列化的;这就致使了咱们读取缓存时,很难直接拿到程序最终想要的结果;这就像快递的包裹,若是你不打开外层的包装,你就拿不出来里边的东西;
若是包裹里的东西有不少,可是其中只有一个是你须要的,其余的还要再包好送给别人;这时候你打开包裹时就会很痛苦——为了拿到本身的东西,必需要拆开包裹,可是拆开后还要很麻烦的将剩下的再包会去;等包裹传递到下一我的的手里,又是如此!
因此,这个时候粒度的控制就很重要了;究竟是一件东西就一个包裹呢,仍是好多东西都包一块呢?前者拆起来方便,后着节约包裹数量。映射到咱们的系统 上,咱们的缓存对象中到底要放哪些数据?一种数据一个对象,简单,读取写入都快,可是种类一多,缓存的管理成本就会很高;多种数据放在一个对象里,方便, 一块全出来了,想用哪一个均可以,可是若是我只要一种数据,其余的就都浪费了,网络带宽和传输延迟的消耗也很可观。
这个时候主要的考虑点就应该是业务场景了,不一样的场景使用不一样的缓存粒度,折衷权衡;不要不在意这点性能损失,缓存通常都是访问频率很是高的数据,各个点的累积效应多是很是巨大的!
固然,有些缓存系统的设计也要求咱们必须考虑缓存对象的粒度问题;好比说Memcached,其chunk设计要求业务要能很好的控制其缓存对象的大小;淘宝的Tair也是,对于尺寸超过1M的对象,处理效率将大为下降;
像Redis这种提供同时提供了Map、List结构支持的系统来讲,虽然增长了缓存结构的灵活性,但最多也只能算是半结构化缓存,还没法作到像本地内存那样的灵活性。
粒度设计的过粗还会遇到并发问题。一个大对象里包含的多种数据,不少地方多要用,这时若是使用的是缓存修改模式而不是过时模式,那么极可能会由于并 发更新而致使数据被覆盖;版本控制是一种解决方法,可是这样会使缓存更新失败的几率大大增长,并且有些缓存系统也不提供版本支持(好比说用的很普遍的 Memcached)。
同缓存粒度同样,缓存的结构也是同样的道理。对于一个缓存对象来讲,并非其粒度越小,体积也越小;若是你的一个字符串就有1M大小,那也是很恐怖的;
数据的结构决定着你读取的方式,举个很简单的例子,集合对象中,List和Map两种数据结构,因为其底层存储方式不一样,因此使用的场景也不同; 前者更适合有序遍历,然后者适合随机存取;回想一下,你是否是曾经在程序中遇到过为了merge两个list中的数据,而不得不循环嵌套?
因此,根据具体应用场景去为缓存对象设计一个更合适的存储结构,也是一个很值得注意的点。
缓存的更新策略主要有两种:被动失效和主动更新,下面分别进行介绍;
通常来讲,缓存数据主要是服务读请求的,并设置一个过时时间;或者当数据库状态改变时,经过一个简单的delete操做,使数据失效掉;当下次再去读取时,若是发现数据过时了或者不存在了,那么就从新去持久层读取,而后更新到缓存中;这便是所谓的被动失效策略。
可是在被动失效策略中存在一个问题,就是从缓存失效或者丢失开始直到新的数据再次被更新到缓存中的这段时间,全部的读请求都将会直接落到数据库上; 而对于一个大访问量的系统来讲,这有可能会带来风险。因此咱们换一种策略就是,当数据库更新时,主动去同步更新缓存,这样在缓存数据的整个生命期内,就不 会有空窗期,前端请求也就没有机会去亲密接触数据库。
前面咱们提到主动更新主要是为了解决空窗期的问题,可是这一样会带来另外一个问题,就是并发更新的状况;
在集群环境下,多台应用服务器同时访问一份数据是很正常的,这样就会存在一台服务器读取并修改了缓存数据,可是还没来得及写入的状况下,另外一台服务 器也读取并修改旧的数据,这时候,后写入的将会覆盖前面的,从而致使数据丢失;这也是分布式系统开发中,必然会遇到的一个问题。解决的方式主要有三种:
a、锁控制;这种方式通常在客户端实现(在服务端加锁是另一种状况),其基本原理就是使用读写锁,即任何进程要调用写方法时,先要获取一个排他锁,阻塞住全部的其余访问,等本身彻底修改完后才能释放;若是遇到其余进程也正在修改或读取数据,那么则须要等待;
锁控制虽然是一种方案,可是不多有真的这样去作的,其缺点显而易见,其并发性只存在于读操做之间,只要有写操做存在,就只能串行。
b、版本控制;这种方式也有两种实现,一种是单版本机制,即为每份数据保存一个版本号,当缓存数据写入时,须要传入这个版本号,而后服务端将传入的版本号和数据当前的版本号进行比对,若是大于当前版本,则成功写入,不然返回失败;这样解决方式比较简单;可是增长了高并发下客户端的写失败几率;
还有一种方式就是多版本机制,即存储系统为每一个数据保存多份,每份都有本身的版本号,互不冲突,而后经过必定的策略来按期合并,再或者就是交由客户端本身去选择读取哪一个版本的数据。不少分布式缓存通常会使用单版本机制,而不少NoSQL则使用后者。
因为独立于应用系统,分布式缓存的本质就是将全部的业务数据对象序列化为字节数组,而后保存到本身的内存中。所使用的序列化方案也天然会成为影响系统性能的关键点之一。
通常来讲,咱们对一个序列化框架的关注主要有如下几点:
a 序列化速度;即对一个普通对象,将其从内存对象转换为字节数组须要多长时间;这个固然是越快越好;
b对象压缩比;即序列化后生成对象的与原内存对象的体积比;
c支持的数据类型范围;序列化框架都支持什么样的数据结构;对于大部分的序列化框架来讲,都会支持普通的对象类型,可是对于复杂对象(好比说多继承关系、交叉引用、集合类等)可能不支持或支持的不够好;
d易用性;一个好的序列化框架必须也是使用方便的,不须要用户作太多的依赖或者额外配置;
对于一个序列化框架来讲,以上几个特性很难都作到很出色,这是一个鱼和熊掌不可兼得的东西(具体缘由后面会介绍),可是终归有本身的优点和特长,须要使用者根据实际场景仔细考量。
咱们接下来会讨论几种典型的序列化工具;
首先咱们先针对几组框架来作一个简单的对比测试,看看他们在对象压缩比和性能方面到底如何;
咱们先定义一个Java对象,该对象里主要包含了咱们经常使用的int、long、float、double、String和Date类型的属性,每种类型的属性各有两个;
测试时的样本数据随机生成,而且数据生成时间不计入测试时间;由于每种序列化框架的内部实现策略,因此即使是同一框架在处理不一样类型数据时表现也会 有差别;同时测试结果也会受到机器配置、运行环境等影响;限于篇幅,此处只是简单作了一个对比测试,感兴趣的同窗能够针对本身项目中的实际数据,来作更详 细、更有针对性的测试;
首先咱们先来看下几种框架压缩后的体积状况,以下表:
单位:字节
工具 |
Java |
Hessian |
ProtoBuf |
Kryo |
仅数字 |
392 |
252 |
59 |
56 |
数字 + 字符串 |
494 |
351 |
161 |
149 |
接下来再看一下序列化处理时间数据;以下表所示:
单位:纳秒
工具 |
Java |
Hessian |
ProtoBuf |
Kryo |
仅数字 |
8733 |
6140 |
1154 |
2010 |
数字 + 字符串 |
12497 |
7863 |
2978 |
2863 |
综合来看,若是只处理数值类型,几种序列化框架的对象压缩比相差惊人,Protobuf和kryo生成的本身数组只有Hessian和Java的五 分之一或六分之一,加上字符串的处理后(对于大尺寸文档,有不少压缩算法均可以作到高效的压缩比,可是针对对象属性中的这种小尺寸文本,可用的压缩算法并 很少),差距缩小了大概一倍。而在处理时间上,几种框架也有者相应程度的差距,两者的增减性是基本一致的。
Java源生序列化
Java源生序列化是JDK自带的对象序列化方式,也是咱们最经常使用的一种;其优势是简单、方便,不须要额外的依赖并且大部分三方系统或框架都支持; 目前看来,Java源生序列化的兼容性也是最好的,可支持任何实现了Serializable接口的对象(包括多继承、循环引用、集合类等等)。但随之而 来不可避免的就是,其序列化的速度和生成的对象体积和其余序列化框架相比,几乎都是最差的。
咱们不妨先来看一下序列化工具要处理那些事情:
a、 首先,要记录序列化对象的描述信息,包括类名和路径,反序列化时要用;
b、 要记录类中全部的属性的描述信息,包括属性名称、类型和属性值;
c、 若是类有继承关系,则要对全部父类进行前述a和b步骤的处理;
d、 若是属性中有复杂类型,这还要对这些对象进行a、b、c步骤的处理;
e、 记录List、Set、Map等集合类的描述信息,同时要对key或value中的复杂对象进行a、b、c、d步骤的操做
可见,一个对象的序列化所须要作的工做是递归的,至关繁琐,要记录大量的描述信息,而咱们的Java源生序列化不但作了上边全部的事情,并且还作的规规矩矩,甚至还“自做多情”的帮你加上了一些JVM执行时要用到的信息。
因此如今就是用脚都可以想明白,Java原生序列化帮你作了这么多事情,它能不慢么?并且还作得这么规矩(迂腐?),结果能不大么?
下面就基本是各个工具针对Java弱点的改进了。
Hessian
Hessian的序列化实现和Java的原生序列化很类似,只是对于序列化反序列化自己并不须要的一些元数据进行了删减;因此Hessian能够像 Java的源生序列化那样,能够支持任意类型的对象;可是在存储上,Hessian并无作相应的优化,因此其生成的对象体积相较于Java的源生序列化 并无降低太多;
好比,Hessian对于数值类型仍然使用了定长存储,而在一般状况下,常用的数据都是比较小的,大部分的存储空间是被浪费掉的;
为了标志属性区段的结束,Hessian使用了长度字段来表示,这在必定程度上会增大结果数据的体积;
因为Hessian相较于Java源生序列化并无太大的优点,因此通常状况下,若是系统中没有使用Hessian的rpc框架,则不多单独使用Hessian的序列化机制。
Google Protobuf
GPB最大的特色就是本身定义了一套本身数据类型,而且规定只容许用个人这套;因此在使用GPB的时候,咱们不得不为它单独定义一个描述文件,或者叫schema文件,用来完成Java对象中的基本数据类型和GPB本身定义的类型之间的一个映射;
不过也正是GPB对类型的自定义,也让他能够更好的针对这些类型作出存储和解析上的优化,从而避免了Java源生序列化中的诸多弱点。
对于对象属性,GPB并无直接存储属性名称,而是根据schema文件中的映射关系,只保存该属性的顺序id;而对于,GPB针对经常使用的几种数据类型采用了不一样程度的压缩,同时属性区段之间采用特定标记进行分隔,这样能够大大减小存储所占用的空间。
对于数值类型,常见的压缩方式有变长byte、分组byte、差值存储等,通常都是根据属性的使用特色来作定制化的压缩策略。
GPB的另外一个优势就是跨语言,支持Java、C、PHP、Python等目前比较大众的语言;其余相似的还有Facebook的Thrift,也须要描述文件的支持,同时也包含了一个rpc框架和更丰富的语言支持;
Kryo
前面咱们提到,诸如Hessian和GPB这些三方的序列化框架或多或少的都对Java原生序列化机制作出了一些改进;而对于Kryo来讲,改进无疑是更完全一些;在不少评测中,Kryo的数据都是遥遥领先的;
Kryo的处理和Google Protobuf相似。但有一点须要说明的是,Kryo在作序列化时,也没有记录属性的名称,而是给每一个属性分配了一个id,可是他却并无GPB那样通 过一个schema文件去作id和属性的一个映射描述,因此一旦咱们修改了对象的属性信息,好比说新增了一个字段,那么Kryo进行反序列化时就可能发生 属性值错乱甚至是反序列化失败的状况;并且因为Kryo没有序列化属性名称的描述信息,因此序列化/反序列化以前,须要先将要处理的类在Kryo中进行注 册,这一操做在首次序列化时也会消耗必定的性能。
另外须要提一下的就是目前kryo目前还只支持Java语言。
如何选择?
就Java原生序列化功能而言,虽然它性能和体积表现都很是差,可是从使用上来讲倒是很是普遍,只要是使用Java的框架,那就能够用Java原生序列化;谁让人家是“亲儿子”呢,即使是看在人家“爹”的份儿上,也得给人家几分面子!
尤为是在咱们须要序列化的对象类型有限,同时又对速度和体积有很高的要求的时候,咱们不妨试一下本身来处理对象的序列化;由于这样咱们能够根据要序列化对象的实际内容来决定具体如何去处理,甚至可使用一些取巧的方法,即便这些方法对其余的对象类型并不适用;
有一点咱们能够相信,就是咱们总能在特定的场景下设计出一个极致的方案!