分布式Java应用:基础与实践(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-16 16:54:11

点击下载

作者:林昊

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

分布式Java应用:基础与实践

分布式Java应用:基础与实践试读:

前言

软件系统得到用户认可后,访问量通常会产生爆发性的增长,互联网网站,例如淘宝、豆瓣等更是如此。在不断完善功能和多元化发展的同时,如何应对不断上涨的访问量、数据量是互联网应用面临的最大挑战。

对于用户而言,除了功能以外,网站访问够不够快,网站能否持续提供服务,也是影响用户访问量的重要因素,因此如何保证网站的高性能,及高可用也是我们要关注的重点。

为了支撑巨大的访问量和数据量,通常需要海量的机器,例如现在的google已经拥有了百万台以上的机器,这些机器耗费了巨大的成本(硬件采购成本、机器的电力成本、网络带宽成本等)。网站规模越大,运维成本就越高,这意味着商业公司所能获得的利润越低,而通常这会导致商业公司必须从多种角度关注如何降低网站运维成本。

本书的目的是从以上几个问题出发,介绍搭建高性能、高可用以及可伸缩的分布式Java应用所需的关键技术。

目标读者

本书涵盖了编写高性能、高可用以及可伸缩的分布式Java应用所需的知识点,适合希望掌握这些知识点的读者。

在介绍各个知识点时,作者尽量结合自己的工作,分享经验与心得,希望能够对那些有相关工作经验的读者有所帮助。

内容导读

本书按照介绍的知识点分为五个部分:第一部分介绍基于Java实现系统间交互的相关知识,这些知识在第1章中进行介绍;第二部分为基于SOA构建大型分布式Java应用的知识点,这些在第2章中介绍;第三部分为高性能Java应用的相关知识,这些在第3、4、5章中介绍;第四部分介绍高可用Java应用的相关知识,这些在第6章中介绍;第五部分介绍可伸缩Java应用,这些在第7章介绍,读者也可根据自己的兴趣选择相应的章节进行阅读。

网站业务多元化的发展会带来多个系统之间的通信问题,因此如何基于Java实现系统间的通信是首先要掌握的知识点,在本书的第1章中介绍了如何基于Java实现TCP/IP(BIO、NIO)、UDP/IP(BIO、NIO)的系统间通信、以及如何基于RMI、Webservice实现系统间的调用,在基于这些技术实现系统间通信和系统间调用时,性能也是要考虑的重点因素,本章也讲解了如何实现高性能的网络通信。

在解决了系统之间的通信问题后,多元化的发展带来的另外一个问题是多个系统间出现了一些重复的业务逻辑,这就要将重复的业务逻辑抽象为一个系统。这样演变后,会出现多个系统,这些系统如何保持统一、标准的交互方式是要解决的问题,SOA无疑是首选的方式,在本书的第2章中介绍了如何基于SOA来实现统一、标准的交互方式。

高性能是本书关注的重点,Java程序均运行在JVM之上,因此理解JVM是理解高性能Java程序所必须的。本书的第3章以Sun Hotspot JVM为例讲述了JVM执行Java代码的机制、内存管理的机制,以及多线程支持的机制。执行代码的机制包含了Sun hotspot将Java代码编译为class文件,加载class文件到JVM中,以解释方式执行class,以及client模式和server模式编译为机器码方式执行class的实现方式;内存管理的机制包含了Sun hotspot内存分配以及回收的机制,内存分配涉及的主要为堆上分配、TLAB分配以及栈上分配。内存回收涉及的有常见的垃圾回收算法、Sun Hotspot JVM中新生代可用的GC、旧生代可用的GC及G1;多线程支持的机制包含了多线程时资源的同步机制,以及线程之间交互的机制。

除JVM外,在编写分布式Java应用时,通常要使用到一些Sun JDK的类库。如何合理地根据需求来选择使用哪些类,以及这些类是如何实现的,是编写高性能、高可用的Java应用必须掌握的。在本书的第4章中介绍了Sun JDK中常用的集合包的集合类、并发包中的常用类(例如ConcurrentHashMap、ThreadPoolExecutor)、序列化/反序列化的实现方式,同时对这些类进行了基准的性能测试和对比。

掌握JVM和使用到的类库是编写高性能Java应用的必备知识,但除了编写之外,通常还会面临对已有系统进行调优。调优是个非常复杂的过程,在本书的第5章中介绍了寻找系统性能瓶颈的一些方法以及针对这些瓶颈常用的一些调优方法。寻找性能瓶颈的方法主要是根据系统资源的消耗寻找对应问题代码的方法,常用的一些调优方法主要是降低锁竞争、降低内存消耗等。

除了高性能外,高可用也是大型Java应用要关注的重点,在本书的第6章中介绍了一些构建高可用系统常用的方法,例如负载均衡技术、构建可容错的系统、对资源使用有限制的系统等。

在面对不断访问的访问量和数据量时,最希望能做到的是仅升级硬件或增加机器就可支撑,但要达到这个效果,在软件上必须付出巨大的努力。本书的第7章介绍了构建可伸缩系统的一些常用方法,主要包括支持垂直伸缩时常用的降低锁竞争等方法;以及支持水平伸缩时常用的分布式缓存、分布式文件系统等方法。

关于本书的代码

书中大部分代码只列出了关键部分或伪代码,完整的代码请从http://bluedavy.com下载。

关于本书的反馈

每次重看书稿,都会觉得这个知识点尚须补充,那个知识点尚须完善,但书不可能一直写下去。在提交终稿的时候,我不特别兴奋,倒是有一点遗憾和担心,遗憾书里的知识点还没有足够讲开,担心自己对某些知识点理解不够,误导了大家。为此,我在自己的网站(http://bluedavy.com)上开辟了一个专区,用于维护勘误,并将不断地完善书中涉及的知识点,欢迎读者反馈和交流。

分布&分享

分布式计算不是一门年轻的技术,早在上个世纪70年代末便已是计算机科学的一个独立分支了;它也不是一门冷僻的技术,从C/S模式到P2P模式,从集群计算到网格计算,乃至风靡当下的云计算,都是其表演的舞台。另一方面,Java 作为一门应网络而生的语言,对分布式计算有着天然的友好性,同时也是当今最流行的编程语言。然而令人稍感意外的是,以“分布式Java 应用”为专题的书籍并不多见,佳作则更少。至于国人所著者,请恕在下孤陋,尚未得见。不过细想之下,其实不足为奇。要开发一个高质量的分布式Java应用,以达到高性能、可伸缩、高可用、低延迟的要求,同时保证一致性、容错性、可恢复性和安全性,是何等的不易啊。它对程序开发的挑战在于:不仅需要对Java语言、类库、各种框架及相关工具极为熟悉,还需要对底层的JVM机制、各种网络协议有足够的了解;它对分析设计的挑战在于:不仅需要熟悉传统的应用架构模式,还需要掌握适用于分布式应用的架构模式和算法设计。此外,分布式应用对数据库管理员、测试分析人员等也提出了更高的要求。一本书若想涵盖这么多的内容,其难度可想而知。作者不仅需要具备起码的理论素养,更需要有丰富的实践经验。如果仅仅是纸上谈兵,对读者是无甚裨益的。

正因如此,半年多前收到林昊先生的部分书稿时,便窃感欣喜,认定这是一个很好的选题。作为淘宝网的系统架构师,他有着令人艳羡的得天独厚的实践机会。尤其难得的是,林昊亲身经历了淘宝网的快速成长和转型期,饱尝其间的成败甘苦,从中也获得了许多宝贵的第一手经验。如今,他选择将这些经验以书籍的形式与众共享,对有志于分布式应用开发的读者而言无疑是一大福音。

本书的基础部分介绍了分布式Java应用的基本实现方式(重点是SOA)、相关的JDK类库和第三方框架,并对JVM的基本机制进行了深入解析;实践部分则关注于高性能、高可用和可伸缩系统的构建等。全书文风朴实,并附有大量的代码、数据和图表,比较符合大多数程序员的口味,也非常具有实践指导意义。如果用挑剔的眼光看,本书在深度、广度和高度上还有继续改进的余地,比如:对关键性的并发设计和算法设计可以介绍得更深入些;尚未涉及分布式应用中的安全问题;在性能调优一章中对Java源码级别的优化介绍似嫌不足;未能高屋建瓴地总结和提炼出分布式应用独有的编程和设计原则、架构和思维模式,等等。当然,对于这样一本上乏经典可参、下需躬身实践的书籍来说,以上多属苛求。事实上,本人在审稿过程中也是获益良多。

另值一提的是,林昊先生利用业余时间写作,已是经年有余,其间数易其稿,且从善如流,充分体现了技术人员的求道精神。本书的主题在明是分布,在暗则是分享——分享一段成长经历、分享一份宝贵经验,无论对作者还是读者,都是善莫大焉。

郑晖

http://blog.zhenghui.org/《冒号课堂——编程范式与OOP思想》作者

2010年5月于广州

Get Architecture Done

提起诸如“高性能”、“高可用性”、“大规模并发”、“可扩展性”这些词汇,我相信多数技术人的心情都是激动而稍有点复杂的,当然,也或许是不屑一顾。毕竟不是谁都有机会面对这些富有挑战的技术场景,也不是每个架构师在面对这些挑战之前都能做好技术上的准备。那些意外故障总是不期而至,疲于奔命地解决问题的场景回顾起来对架构师来说犹如一场噩梦。

本书阐述当一个面向数以亿计用户的网站经过几年高速发展,技术团队不得不面临大规模、高并发、高扩展性等挑战带来的技术困境的时候,一个出色的架构师经过多年一线实践后累积的经过时间考验的解决方案以及宝贵的实战经验。在这本书里,你会看到作者在解决一些关乎Web应用问题的指导原则、实践方法、多重工具的综合运用以及作者本人的感悟。要强调的是,本书讲述的内容是一个Web 应用从小到大过程中遇到的棘手问题的解决之道,并非宏观解析,亦非屠龙之技。无论您面对的站点是大是小,皆会有参考作用,毕竟大站点会越来越复杂,而小站点总有一天也将变大。

如今到计算机书店里走一下,会发现Java 架构相关的技术图书虽已不少,但仍有理由相信本书内容填补了在Java架构实战方面的空白。在互联网应用大行其道的今天,有些名义上主题为Java架构的图书,要么单从Java 本身阐述,缺乏整体应用的大局观;要么是高屋建瓴,从编程思想的高度坐而论道,缺乏实践性;要么是闭门造车之作,缺乏验证性。本书作者林昊多年来致力于推动OSGi在国内的发展,不乏理论功底,而后加盟淘宝网(Taobao.com)的几年间奋战在架构一线,爬摸滚打积累了丰富的实践心得。所以,本书是一本不折不扣的“理论结合实践”之作。

考虑国内的技术图书出版环境以及必须尽力适应读者的预期,写书本身是一件十分耗费心力的事情,但能将知识传递给更多人无疑又是让人快乐的。现在,经过作者近两年的梳理与总结,这本书即将出版,相信读者在研读本书之后会有所收获并可运用到所面对的Web应用上,也期待将来有更多朋友能够分享架构实践经验,一展才华,不亦快哉!

冯大辉

http://dbanotes.net

2010年5月于杭州贝塔咖啡

高效写作,敏捷出版

——出版人感言

和林昊认识了五六年了。他还在深圳的时候,来武汉出差,我们俩一起吃一大盘船帮鱼头,憨憨的他,话不多,就知道笑。

说话间,他的第二本书就要上市了。这本书,我更多地放手让编辑去做,当然,我扮演的还是黑头黑面的教练。编辑们在手记里都提到了被我“强力推动”做某事。

天下无难事,天下无易事。难,是因为问题没有被细分到可以去解决的程度;易,是因为找到了解决问题的办法。

很多初涉写作的朋友,因为读书多,读一流书还不少,眼界一高,就看不上自己初出茅庐的文笔。这种追求完美的心态,导致很多人的写作没效率。其实,谁刚开始学走路就姿势优美呢?当编辑这么些年,不知见过多少作者毁在这追求一步走到金字塔塔尖的误区中了。可惜当时的我不明就里,没有带给他们切实的帮助。

写作,其实可以是高效的,前提是:学会接受自己刚试手时的不完美,每天都不能停止写。

高效的写作取决于高效的时间管理,高效的时间管理离不开专注与灵活这两种能力。所有与我们合作成功的作者,都是很忙的人,但他们忙得有效率,既专注又灵活。专注,让他们懂得抓主要矛盾;灵活,让他们知道借力,能随时理解并配合出版社的各项要求。

写作是件很辛苦的事情,而且看起来似乎金钱上的回报未必高(除了某些畅销书)。但是,所有在博文出书的作者,他们都深谙写作的好处。有哪些好处呢?

1.写作犹如教书,作者与读者教学相长。因为想把道理写给人看,要写明白,就会发现其实自己脑子里还有好多模糊点没搞清楚。因为要教读者,所以只能自己搞得更清楚。为了给读者一碗水,本来只有半桶水的作者,不得不让自己变得拥有一桶水。您瞧,这是谁赚了?

2.写作,光教人怎么干不算高明,那是授人以鱼;还要告诉人为什么要这么干,这是授人以渔。上升到规律层面,就得要求作者有较扎实的理论功底,能把一件事说圆了。而在这个过程中,不少作者会痛苦地发现自己的系统思维不足、理论功底不扎实。这就迫使他们不断思考,直到能把事情说得让月的时间。

2009年8月重新开始了这本书的撰写,在2009年10月下旬前按计划完成了第二章、第四章和第五章,但随后由于项目进入冲刺阶段、忙于校园招聘以及迁居等事情,再度停顿了本书的撰写。

记得在刚开始写这本书的时候,周筠老师就告诉我,要坚持写,就算每天只写一点也是好的,千万不要停顿!确实如此,在停顿了两次后,就很难再找到继续写这本书的动力和激情了,得感谢徐定翔编辑在之后给我的鼓励和督促,终于在2010年3月我又开始了写作。待完成了第六章的编写后,由于剩下的第七章中的部分知识点和自己的工作联系不是非常紧密,导致了不断的拖延,这时周筠老师和徐定翔编辑给我的一个建议起到了关键作用,就是先放下第七章,先做之前完成的六章的定稿。

回到自己熟悉的前 6 章,终于再次有了写作的动力,随着工作中对自己书中所涉及的知识点的不断实践,此时再回头看自己半年前甚至一年前写的初稿时,发现其中有不少的错误以及条理不够清晰的地方,于是进行了大刀阔斧的改动,实践所获得的积累此时起了巨大的推动作用,这 6 章定稿除了第三章以外的章节,完成得较为顺利。

6章定稿提交后,由白爱萍编辑带领的编辑小组再次对书稿进行细致的“田间管理”,给出了非常多的修改建议,印象中几乎平均每页都至少有两到三处需要修改的地方,正是他们的认真和专业,使得定稿中很多语言上以及技术上的错误得以纠正,感谢武汉博文的编辑们。

最后,在第三章定稿的修改过程中,得到了同事莫枢(http://rednaxelafx.javaeye.com)非常多的建议和帮助,在此非常感谢他的支持。

全书在编写的过程中,初稿、定稿也提交给了一些业内专家帮忙评审,主要有:郑晖、霍炬、曹祺、刘力祥,他们给出的很多意见一方面纠正了书中一些技术上的错误,另一方面也让书的条理性更加顺畅,衷心感谢他们的辛勤付出。尤其郑晖老师,在承诺为本书写推荐序时,又花时间把全稿通读一遍,他的耐心和专业精神,让我感佩不已。

书的撰写过程是如此漫长,每天晚上下班后、周末、假期,甚至过年期间,都成了写书的时间,感谢家人给我的包容、支持和理解,最要感谢的是我明年春节就将迎娶的准老婆:宗伟,感谢你忍受了我不断忘记买家里需要的各种东西,感谢你独立完成了新家的装修,更要感谢你允许我这么多的周末、假日都无法陪伴你,没有你的支持和鼓励,这本书是无法完成的。

回顾整个编写过程,从开始编写,到提交完全部终稿,经历了15个月的时间,写作时间大概为11个月,经过这 11 个月对这些知识点的不断实践、回顾和总结,它们在我的脑海中刻下了深深的烙印,的确,发表是最好的记忆。

林昊

2010年5月20日晚于杭州家中

实践是最好的成长发表是最好的记忆

——作者序

分布式Java应用需要开发人员掌握较多的知识点,通常分布式Java应用的场景还会对性能、可用性以及可伸缩有较高的要求,而这也就意味着开发人员需要掌握更多的知识点。我刚进淘宝的时候,曾经一直苦恼对于一个这样的分布式Java应用,我到底需要学习些什么。

随着在淘宝工作的不断开展,我的眼前终于慢慢呈现了高性能、高可用以及可伸缩的Java 应用所需知识点的全景,这张知识点的全景图现在已经演变成了本书的目录。当看到自己整理出的知识点的全景图时,很惊讶地发现其中有些知识点其实是我之前已经学习过的,但到了真正需要使用的时候有些是完全遗忘了,有些则是在使用时碰到了很多的问题,从这里我看到,当学习到的知识不去经过实践检验时,这些知识就不算真正属于自己。

幸运的是,在淘宝我得到了分布式Java应用的绝佳实践机会,于是所学习到的网络通信、高性能、高并发、高可用以及可伸缩的一些知识,有机会在实践中得到验证。正是这样的机会,让我对这些知识点有了更深刻本质的理解,并能将其中的一些知识真正吸收,变为自己的经验,所以我一个很真切的体会就是:实践是最好的成长。

经历了这段艰难的成长,自己也希望不要忘却在这个过程中的收获,胡适先生曾说:“发表是最好的记忆”(这句话也长期放在台湾技术作家侯捷老师的网站上),于是萌生了写这本书的想法,一方面想梳理自己通过实践所获得的成长,另一方面也希望与正在从事分布式 Java应用的技术人员分享一些实践的心得,同时给将要或打算从事分布式Java应用的技术人员提供一些参考。

从2008年11月确认要写这本书,到2010年5月完成这本书,历时一年半,过程可谓波折不断,前3个月的写作一帆风顺,顺利完成了第一章和第三章的撰写。

到了2009年3月底后,由于投入到了《OSGi原理与最佳实践》一书的编写中,停顿了将近3个自己和读者都信服。这就很好地训练了作者的系统思维能力。系统思维能力的提升,让一个人的视野更开阔,站得更高,看得更远。试想,又是谁赚了?

3.现在,已经进入了个人品牌时代。想拿张名片就让人对你刮目相看,土!至少得有个博客吧。看看现在,IT圈的朋友们,有几个没博客的?但,有几个人能写书咧?物以稀为贵。写了一本好书的人,声名远播,个人价值会放大。所以,现在愿意写书的人越来越多。

好处明白了,怎样才能做到高效写作?怎样才能做到敏捷出版?我们和作者们进行了一些探讨,虽然这方面的认识积累还不够,但,有一说一。比如,在和出版社共同确定了图书产品的市场定位后(需求分析的过程也颇熬人,编辑手记里有细述),作者可以尝试:

1.对自己的写作进行难度分级管理。忙的时候写难度低的,闲下来就写难度高的。这样就能保持写作的平稳状态,不至于忙的时候完全不写,闲的时候写一大堆,起伏太大就不易坚持。难度分级越细,越容易对写作进行模块化管理。写好一个模块就丢在那里,等着日后拼装。书也是个产品,把各个模块拼装成一本书的过程,和拼装宜家的家具没啥两样。

2.每天得坚持写,给自己规定每日最低写作量。我们通常希望作者的每日写作量不要低于500字。区区500字,也就是三篇微博的字数之和。能坚持每日为自己的主题写500字的,就一定能把这个最低量渐渐提高到1000字。

3.每个模块需要有写作的时间量限制,比如每5天必须完成一个小节,每15天必须完成一章中的一到两个核心小节,等等。

上述讲的是原创书的写作,其实在翻译和制作外版书时,这种模块化、敏捷运作的思路同样可以借鉴。我们用老办法做出来的产品,以往常常存在着周期和质量问题。最近我们出版的《编程之魂》就因为翻译质量挨骂了,这是因为这本书的制作还是采用的老模式,编辑和译者都单枪匹马,得不到及时的交流和帮助。挨骂不可怕,谁做产品会一帆风顺?重要的是通过思考,找到正确的方法。挨骂还是因为方法不对,方法不对,还是因为思考没到位。

林昊的这本书,我们还有不少工作没做到位,只是尽可能把团队现有的能力都贡献出来了,没有偷懒。这是进步。以往尽管也有能力不足之处,但偷懒也处处可见。

做林昊的书,再次体会到淘宝文化的魅力。开放的淘宝、重视知识积累和传承的淘宝,让我们淘到了不少林昊这样的“宝贝”作者,他们心态开放,接纳批评、消化批评的能力很强。我多次和林昊说,他的谦虚,他的开朗,他的认真,让我和同事们受益匪浅。

闻道有先后,写作有专攻。期待着更多的朋友和我们一起分享“高效写作,敏捷出版”带来的愉悦——快乐因创造价值而生。

有创造,就会有真正的成长。

周筠

2010年5月21日于武汉

第1章 分布式Java应用

大型应用通常会拆分为多个子系统来实现,对于Java来说,这些子系统可能部署在同一台机器的多个不同的JVM中,也可能部署在不同的机器上,但这些子系统又不是完全独立的,要相互通信来共同实现业务功能,对于此类Java应用,我们称之为分布式Java应用。

Martin Fowler在《企业应用架构模式》一书中曾经说过:“能不用分布式的情况下就不要用分布式”,当应用变为分布式Java应用时,会很大程度地增加应用实现的技术复杂度,对于分布式Java应用,通常有两种典型的方法来实现。

1.基于消息方式实现系统间的通信

当系统之间要通信时,就向外发送消息,消息可以是字节流、字节数组,甚至是 Java对象,其他系统接收到消息后则进行相应的业务处理。

消息方式的系统间通信,通常基于网络协议来实现,常用的实现系统间通信的协议有:TCP/IP 和UDP/IP。

TCP/IP是一种可靠的网络数据传输的协议。TCP/IP要求通信双方首先建立连接,之后再进行数据的传输。TCP/IP负责保证数据传输的可靠性,包括数据的可到达、数据到达的顺序等,但由于TCP/IP需要保证连接及数据传输的可靠,因此可能会牺牲一些性能。

UDP/IP 是一种不保证数据一定到达的网络数据传输协议。UDP/IP 并不直接给通信的双方建立连接,而是发送到网络上进行传递。由于UDP/IP不建立连接,并且不能保证数据传输的可靠,因此性能上表现相对较好,但可能会出现数据丢失以及数据乱序的现象。

TCP/IP和UDP/IP可用于完成数据的传输,但要完成系统间通信,还需要对数据进行处理。例如读取和写入数据,按照POSIX标准分为同步IO和异步IO两种,其中同步IO中最常用的是BIO(Blocking IO)和NIO(Non-Blocking IO)。

从程序角度而言,BIO就是当发起IO的读或写操作时,均为阻塞方式,只有当程序读到了流或将流写入操作系统后,才会释放资源。

NIO是基于事件驱动思想[1]的,实现上通常采用Reactor模式[2],从程序角度而言,当发起IO的读或写操作时,是非阻塞的;当Socket有流可读或可写入Socket时,操作系统会相应地通知应用程序进行处理,应用再将流读取到缓冲区或写入操作系统。对于网络IO而言,主要有连接建立、流读取及流写入三种事件,Linux 2.6以后的版本采用epoll[3]方式来实现NIO。

下面再来看看另一种方式——AIO。AIO为异步IO方式,同样基于事件驱动思想,实现上通常采用Proactor模式[4]。从程序角度而言,和NIO不同,当进行读写操作时,只须直接调用API的read或write 方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。较之NIO而言,AIO一方面简化了程序的编写,流的读取和写入都由操作系统来代替完成;另一方面省去了NIO中程序要遍历事件通知队列(Selector)的代价。Windows基于IOCP[5]实现了AIO,Linux目前只有基于epoll模拟实现的AIO。

Java对TCP/IP和UDP/IP均支持,在网络IO的操作上,Java 7以前的版本仅支持BIO和NIO两种方式,对AIO方式感兴趣的读者可自行下载Sun JDK 7进行尝试。

2.基于远程调用方式实现系统间的通信

当系统之间要通信时,可通过调用本地的一个Java接口的方法,透明地调用远程的Java实现。具体的细节则由Java或框架来完成,这种方式在Java中主要用来实现基于RMI和WebService的应用。

本章通过举例来介绍如何基于Java 的包及开源的产品来实现以上两种方式的系统间通信,这些是实现分布式Java应用的基础和必备知识,采用的例子如下。

示例程序由一个服务器端程序和一个客户端程序构成,是典型的请求-响应机制,即客户端发送请求,服务端响应。客户端读取用户的输入,并将输入的字符串信息发送给服务器端,服务器端接收到信息后响应,当客户端输入的是quit字符串时,则停止客户端和服务器端的程序。

1.1 基于消息方式实现系统间的通信

1.1.1 基于Java自身技术实现消息方式的系统间通信

基于Java自身包实现消息方式的系统间通信的方式有:TCP/IP+BIO、TCP/IP+NIO、UDP/IP+BIO以及UDP/IP+NIO 4种,下面分别介绍如何实现这4种方式的系统间通信。

TCP/IP+BIO

在Java中可基于Socket、ServerSocket来实现TCP/IP+BIO的系统间通信。Socket主要用于实现建立连接及网络IO的操作,ServerSocket主要用于实现服务器端端口的监听及Socket对象的获取。基于Socket实现客户端的关键代码如下:

服务器端关键代码如下:

上面是基于Socket、ServerSocket实现的一个简单的系统间通信的例子。而在实际的系统中,通常要面对的是客户端同时要发送多个请求到服务器端,服务器端则同时要接受多个连接发送的请求,上面的代码显然是无法满足的。

为了满足客户端能同时发送多个请求到服务器端,最简单的方法就是生成多个Socket。但这里会产生两个问题:一是生成太多的Socket会消耗过多的本地资源,在客户端机器多,服务器端机器少的情况下,客户端生成太多Socket会导致服务器端须要支撑非常高的连接数;二是生成Socket(建立连接)通常是比较慢的,因此频繁地创建会导致系统性能不足。鉴于这两个问题,通常采用连接池的方式来维护Socket是比较好的,一方面限制了能创建的Socket的个数;另一方面由于将Socket放入了池中,避免了重复创建Socket带来的性能下降问题。数据库连接池就是这种方式的典型代表,但连接池的方式会带来另一个问题,连接池中的Socket个数是有限的,但同时要用Socket的请求可能会很多,在这种情况下就会造成激烈的竞争和等待;还有一个需要注意的问题是合理控制等待响应的超时时间,如不设定超时会导致当服务器端处理变慢时,客户端相关的请求都在做无限的等待,而客户端的资源必然是有限的。因此这种情况下很容易造成当服务器端出现问题时,客户端挂掉的现象。超时时间具体设置为多少取决于客户端能承受的请求量及服务器端的处理时间。既要保证性能,又要保证出错率不会过高,对于直接基于TCP/IP+BIO的方式,可采用Socket.setSoTimeout来设置等待响应的超时时间。

为了满足服务器端能同时接受多个连接发送的请求,通常采用的方法是在accept获取Socket后,将此Socket放入一个线程中处理,通常将此方式称为一连接一线程。这样服务器端就可接受多个连接发送请求了,这种方式的缺点是无论连接上是否有真实的请求,都要耗费一个线程。为避免创建过多的线程导致服务器端资源耗尽,须限制创建的线程数量,这就造成了在采用BIO的情况下服务器端所能支撑的连接数是有限的。

TCP/IP+NIO

在Java中可基于java.nio.channels中的Channel和Selector的相关类来实现TCP/IP+NIO方式的系统间通信。Channel有SocketChannel和ServerSocketChannel两种,SocketChannel用于建立连接、监听事件及操作读写,ServerSocketChannel用于监听端口及监听连接事件;程序通过Selector来获取是否有要处理的事件。基于这两个类实现客户端的关键代码如下:

从上可见,NIO是典型的Reactor模式的实现,通过注册感兴趣的事件及扫描是否有感兴趣的事件发生,从而做相应的动作。

服务器端关键代码如下:

之后则可采取和客户端同样的方式对selector.select进行轮询,只是要增加一个对key.isAcceptable的处理,代码如下:

上面只是基于TCP/IP+NIO实现的一个简单例子,同样来看看基于TCP/IP+NIO如何支撑客户端同时发送多个请求及服务器端接受多个连接发送的请求。

对于客户端发送多个请求的需求,采用TCP/IP+NIO和采用TCP/IP+BIO的方式没有任何不同。但NIO方式可做到不阻塞,因此如果服务器端返回的响应能带上请求标识,那么客户端则可采用连接复用的方式,即每个SocketChannel在发送消息后,不用等响应即可继续发送其他消息,这种方式可降低连接池带来的资源争抢的问题,从而提升系统性能;对于连接不复用的情况,可基于Socket.setSoTimeout的方式来控制同步请求的超时;对于连接复用的情况,同步请求的超时可基于BlockingQueue、对象的wait/notify机制或Future机制来实现。

对于服务器端接受多个连接请求的需求,通常采用的是由一个线程来监听连接的事件,另一个或多个线程来监听网络流读写的事件。当有实际的网络流读写事件发生后,再放入线程池中处理。这种方式比TCP/IP+BIO的好处在于可接受很多的连接,而这些连接只在有真实的请求时才会创建线程来处理,这种方式通常又称为一请求一线程。当连接数不多,或连接数较多,且连接上的请求发送非常频繁时,TCP/IP+NIO的方式不会带来太大的优势,但在实际的场景中,通常是服务器端要支持大量的连接数,但这些连接同时发送的请求并不会非常多。

在基于Sun JDK开发Java NIO程序时,尤其要注意selector.select抛出IOException异常的处理[6]及selector.select 不阻塞就直接返回的情况[7]。这两种状况都有可能造成 CPU 消耗达到 100%,对于selector.select抛出IOException的状况,可以采用绕过的方法为捕捉异常并将当前Thread sleep一段时间,或是重新创建Selector。为避免selector.select不阻塞就直接返回,可采用bug库中提到的修改建议。

从上面可以看出,对于高访问量的系统而言,TCP/IP+NIO方式结合一定的改造在客户端能够带来更高的性能,在服务器端能支撑更高的连接数。

UDP/IP+BIO

Java对UDP/IP方式的网络数据传输同样采用Socket机制,只是UDP/IP下的Socket没有建立连接的要求,由于UDP/IP是无连接的,因此无法进行双向的通信。这也就要求如果要双向通信的话,必须两端都成为UDP Server。

在 Java 中可基于 DatagramSocket 和 DatagramPacket 来实现 UDP/IP+BIO 方式的系统间通信,DatagramSocket负责监听端口及读写数据。DatagramPacket作为数据流对象进行传输,基于这两个类实现客户端的关键代码如下:

服务器端代码和客户端代码的结构基本一致,这里就不列了。

由于UDP/IP通信的两端不建立连接,就不会有TCP/IP通信连接竞争的问题,只是最终读写流的动作是同步的。

对于服务器端同时接收多请求的需求,通常采取每接收到一个packet就放入一个线程中进行处理的方式来实现。

UDP/IP+NIO

在 Java 中可通过 DatagramChannel 和 ByteBuffer 来实现 UDP/IP+NIO 方式的系统间通信,DatagramChannel 负责监听端口及进行读写,ByteBuffer 则用于数据流传输。基于这两个类实现客户端的关键代码如下:

服务端代码和客户端代码基本一致,就不再一一描述。

从以上代码来看,对于UDP/IP方式,NIO带来的好处是只在有流要读取或可写入流时才做相应的IO操作,而不用像 BIO方式直接阻塞当前线程。

以上列举了基于Java 包实现一对一的系统间通信的方式,在实际的场景中,通常还会要将消息发送到多台机器,此时可以选择为每个目标机器建立一个连接,这种方式对于发送消息端会造成很大的网络流量压力。例如传输的消息是视频数据的场景,在网络协议上还有一个基于UDP/IP扩展出来的多播协议,多播协议的传输方式是一份数据在网络上进行传输,而不是由发送者给每个接收者都传一份数据,这样,网络的流量就大幅度下降了。

在Java中可基于MulticastSocket和DatagramPacket来实现多播网络通信,MulticastSocket是基于DatagramSocket派生出来的类,其作用即为基于UDP/IP实现多播方式的网络通信。在多播通信中,不同的地方在于接收数据端通过加入到多播组来进行数据的接收,同样发送数据也要求加入到多播组进行发送,多播的目标地址具有指定的地址范围,在224.0.0.0和239.255.255.255之间。基于多播方式实现网络通信的服务器端关键代码如下:

之后则可和UDP/IP+BIO一样通过receive和send方法来进行读写操作。

Client端代码和服务端代码基本一致,就不再列举了。

在Java应用中,多播通常用于多台机器的状态的同步。例如JGroups,默认基于UDP/IP多播协议,由于UDP/IP协议在数据传输时不够可靠,对于可靠性要求很高的系统,会希望采用多播方式,同时又要做到可靠。对于这样的需求,业界提出了一些能够确保可靠实现多播的方式:SRM(Scalable Reliable Multicast)[8]、URGCP(Uniform Reliable Group Communication Protocol),其中SRM是在UDP/IP多播的基础上增加了确认机制,从而保证可靠,eBay采用了SRM框架来实现将数据从主数据库同步到各个搜索节点机器[9]。

从上面的介绍来看,使用Java包来实现基于消息方式的系统间通信还是比较麻烦。为了让开发人员能更加专注对数据进行业务处理,而不用过多关注纯技术细节,开源业界诞生了很多优秀的基于以上各种协议的系统间通信的框架。这其中的佼佼者就是Mina了,1.1.2节将会介绍这个佼佼者。

1.1.2 基于开源框架实现消息方式的系统间通信

这一节讲述基于Mina如何实现消息方式的系统间通信,同时分析开源通信框架的优势。

Mina是Apache的顶级项目[10],基于Java NIO构建,同时支持TCP/IP和UDP/IP两种协议。Mina对外屏蔽了Java NIO使用的复杂性,并在性能上做了不少的优化。

在使用Mina时,关键的类为IoConnector、IoAcceptor、IoHandler及IoSession,Mina采用Filter Chain的方式封装消息发送和接收的流程,在这个Filter Chain过程中可进行消息的处理、消息的发送和接收等。

IoConnector负责配置客户端的消息处理器、IO事件处理线程池、消息发送/接收的Filter Chain等。

IoAcceptor负责配置服务器端的IO事件处理线程池、消息发送/接收的Filter Chain等。

IoHandler作为Mina和应用的接口,当发生了连接事件、IO事件或异常事件时,Mina都会通知应用所实现的IoHandler。

IoSession有点类似SocketChannel的封装,不过Mina对连接做了进一步的抽象,因此可进行更多连接的控制及流信息的输出。

基于Mina实现TCP/IP+NIO客户端的关键代码如下:

使用Mina后,客户端的代码变得简单多了。

服务器端关键代码如下:

采用 Mina 后,服务器端代码无须再关注建立连接时的 OP_ACCEPT 的事件,同样也无须去注册OP_READ、OP_WRITE这些Key的事件,取而代之的是更友好地使用接口。通过这些封装使用者可以非常方便地使用,而无须过多考虑Java NIO的用法。但Mina 2.0之前的版本中并未提供连接的管理(连接的创建、自动重连、连接的心跳、连接池等)、同步发送数据支持,因此在实际使用中通常还需在Mina的基础上进行封装。

在使用Mina 2.0之前的版本时,以下几个方面值得注意:

● 使用自定义的ThreadModel

通过SocketConnectorConfig.setThreadModel(ThreadModel.MANUAL)将线程模式改为自定义模式,否则Mina会自行启动一个最大线程数为16个的线程池来处理具体的消息,这对于多数应用而言都不适用,因此最好是自行控制具体处理消息的线程池。

● 合理配置IO处理线程池

在创建SocketAcceptor或SocketConnector时要提供一个线程池及最大的线程数,也就是Mina用于IO事件处理的线程数,通常建议将这个线程数配置为CPU核数+1。

● 监听是否成功写入操作系统的发送缓冲区

在调用IoSession.write时,Mina并不确保其会成功写入操作系统的发送缓冲区中(例如写入时连接刚好断开),为了确定是否成功,可采用如下方法:

这对于同步请求而言特别重要,通常同步请求时都会设置一个等待响应的超时时间,如果不去监听是否成功写入的话,那么同步的请求一直要等到设定的超时时间才能返回。

● 监听写超时

当接收消息方的接收缓冲区占满时,发送方会出现写超时的现象,这时Mina会向外抛出WriteTimeoutException,如有必要,可在IoHandler实现的exceptionCaught方法里进行处理。

● 借助Mina IoSession上未发送的bytes信息实现流控

当IoSession上堆积了过多未发送的byte时,会造成jvm内存消耗过多的现象。因此通常要控制IoSession上堆积的未发送的byte量,此值可通过Mina IoSession的getScheduledWriteBytes来获取,从而进行流控。

● messageReceived方法占用IO处理线程

在使用Thread.MANUAL的情况下,IOHandler里的messageReceived方法会占用Mina的IO处理线程,为了避免业务处理接收消息的速度影响IO处理性能,建议在此方法中另起线程来做业务处理。

● 序列化/反序列化过程会占用IO处理线程

由于Mina的序列化/反序列化过程是在FilterChain上做的,同样会占据IO处理线程。Mina将同一连接上需要发送和接收的消息放在队列中串行处理。如果序列化/反序列化过程耗时较长,就会造成同一连接上其他消息的接收或发送变慢。

● 反序列化时注意继承CumulativeProtocolDecoder

在使用NIO的情况下,每次读取的流并不一定完整,因此要通过继承CumulativeProtocolDecoder来确保当流没读完时,下次接着读,这同时也要求应用在协议头中保持此次发送流的长度信息。

● Mina 1.1.6及以前的版本中sessionClosed可能会不被调用的bug

在某些高压力的情况下,当连接断开时,Mina 1.1.6 及以前的版本并不会调用 IoHandler 中的sessionClosed 方法,这对于某些要在 sessionClosed 做相应处理的应用来说会出现问题,这个 bug[11]在Mina 1.1.7的版本中已修复。

除了Mina之外,JBoss Netty 也是现在一个广受关注的Java通信框架,其作者也是Mina的作者(Trustin Lee),据评测JBoss Netty的性能好于Mina[12],如读者感兴趣,可访问http://www.jboss.org/netty来了解更多的细节。

以上两节介绍了基于 Java自身包及开源通信框架来实现消息方式的系统间通信,Java系统内的通信都是以Java对象调用的方式来实现的,例如A a=new AImpl();a.call();,但当系统变成分布式后,就无法用以上的方式直接调用了,因为在调用端并不会有 AImpl 这个类。这时如果通过基于以上的消息方式来做,对于开发而言就会显得比较晦涩了,因此 Java 中也提供了各种各样的支持对象方式的系统间通信的技术,例如RMI、WebService等。同样,在Java中也有众多的开源框架提供了RMI、WebService的实现和封装,例如Spring RMI、CXF等,下面来看看基于远程调用方式如何实现系统间的通信。

1.2 基于远程调用方式实现系统间的通信

远程调用方式就是尽可能地使系统间的通信和系统内一样,让使用者感觉调用远程同调用本地一样,但其实并没有办法做到完全透明,例如由于远程调用带来的网络问题、超时问题、序列化/反序列化问题、调试复杂的问题等,在远程调用时要注意对这些问题的处理。

1.2.1 基于Java自身技术实现远程调用方式的系统间通信

在Java中实现远程调用方式的技术主要有RMI和WebService两种,下面分别来看看基于这两种技术如何实现远程调用方式的系统间通信。

RMI

RMI(Remote Method Invocation)是Java用于实现透明远程调用的重要机制。在远程调用中,客户端仅有服务器端提供的接口。通过此接口实现对远程服务器端的调用。

远程调用基于网络通信来实现,RMI同样如此,其机制如图1.1所示:图1.1 RMI机制

Sun JDK 6.0以前版本中的RMI实现均是基于TCP/IP+BIO方式的,RMI服务器端通过启动RMI注册对象在一个端口上监听对外提供的接口,其实现实例以字符串的方式绑定到RMI注册对象上。RMI客户端通过proxy的方式代理了对服务器端接口的访问,RMI客户端将要访问的服务器端对象字符串、方法和参数封装成一个对象,序列化成流后通过TCP/IP+BIO传输到RMI服务器端。RMI服务器端接收到客户端的请求对象后,解析其中的对象字符串、方法及参数,通过对象字符串从RMI注册对象上找到提供业务功能的实例,之后结合要访问的方法来反射获取到方法实例对象,传入参数完成对服务器端对象实例的调用,返回的结果则序列化为流以TCP/IP+BIO方式返回给客户端,客户端在接收到此流后反序列化为对象,并返回给调用者。

RMI要求服务器端的接口继承Remote接口,接口上的每种方法必须抛出RemoteException,服务器端业务类通过实现此接口提供业务功能,然后通过调用UnicastRemoteObject.exportObject来将此对象绑定到某端口上,最后将此对象注册到本地的LocateRegistry上,此时形成一个字符串对应于对象实例的映射关系。基于RMI实现示例中的服务器端代码如下。

RMI的客户端首先通过LocateRegistry.getRegistry来获取Registry对象,然后通过Registry.lookup字符串获取要调用的服务器端接口的实例对象,最后以接口的方式透明地调用远程对象的方法。按照以上描述,基于RMI实现客户端的关键代码如下:

从上面示例代码来看,基于RMI实现的客户端和服务端较之基于TCP/IP+NIO等实现的客户端和服务端简单很多,代码可维护性也高很多。

WebService

WebService 是一种跨语言的系统间交互标准。在这个标准中,对外提供功能的一方以 HTTP 的方式提供服务,该服务采用WSDL(Web Service Description Language)描述,在这个文件中描述服务所使用的协议、所期望的参数、返回的参数格式等。调用端和服务端通过 SOAP(Simple Object Access Protocol)方式来进行交互。

在Java中使用WebService须首先将服务器端的服务根据描述生成相应的WSDL文件,并将应用及此WSDL文件放入HTTP服务器中,借助Java辅助工具根据WSDL文件生成客户端stub代码。此代码的作用是将产生的对象请求信息封装为标准的 SOAP 格式数据,并发送请求到服务器端,服务器端在接收到SOAP格式数据时进行转化,反射调用相应的Java类,过程如图1.2所示:图1.2 WebService调用过程

Java SE6中集成了WebService,因此可以直接实现该方式的远程调用,服务器端通过@WebService来标记对外暴露的WebService实现类,通过调用Endpoint.publish将此WebService实现发布到指定的HTTP地址上。客户端通过wsimport来访问相应地址的wsdl文件,从而生成调用服务器端的辅助类,应用即可通过调用此类来实现远程调用了。基于WebService实现示例中的服务器端代码如下:

对外暴露的接口:

服务器端的实现类,通过@WebService来指定对外提供的WebService的名称和客户端生成的类名及包名:

发布WebService的类:

客户端通过JDK bin目录下的wsimport命令来生成辅助调用代码,执行如下命令生成辅助代码:

执行后,在当前目录下会生成book/chapter1/WebService/client/Business.java和book/chapter1/WebService/client/BusinessService.java的代码,基于这两个生成的代码编写客户端的关键代码如下:

WebService传输的数据协议采用SOAP,SOAP对于复杂的对象结构比较难支持,其好处是能够支持跨语言。

无论是采用RMI还是WebService,都封装了网络通信的细节,因此使用起来会较为简单,但如果想对通信细节做一些调优或控制,也会比较麻烦。

1.2.2 基于开源框架实现远程调用方式的系统间通信

Spring RMI

Spring RMI是Spring Remoting中的一个子框架,基于Spring RMI可以很简单地就实现RMI方式的Java远程调用,Spring RMI的工作原理如图1.3所示:图1.3 Spring RMI工作原理

基于Spring RMI实现示例中的服务器端代码如下。

对外提供的接口类:

服务器端的实现类:

Spring描述文件:

启动Spring容器,并让外部能够以RMI的方式访问到BusinessImpl:

客户端代码如下。

Spring描述文件:

客户端通过启动Spring容器,获取相应的Spring bean后即可以RMI方式调用远程的对象了:

CXF

CXF是Apache的顶级项目,也是目前Java社区中用来实现WebService流行的一个开源框架(尤其是收编了xfire后)。基于CXF可以非常简单地以WebService的方式来实现Java甚至是跨语言的远程调用,CXF的工作原理如图1.4所示:

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载