diff --git a/README.md b/README.md
index bf2ffa4..d6eefc8 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,18 @@
《Java工程师修炼之道》
+[
](https://api.gitsponsors.com/api/badge/link?p=ALQJxZauryyAQlf0IzyvqEw0lM+sJ1NbMZq6Pj0yXVBdL8fXZrXF01cpSpV4vg2m0zTQIr+meODxd3moQ+dRKqqP59cdgJ3A/y5/peB19b5FxkRxaENv1mQpNeBrd7ODD2h13/zbtoA1S3Bec3W/Qw==)
--
+购买纸质书籍可至:
+
-- [书籍](book)
+- [在线书籍](https://rowkey-books.gitbook.io/pragmatic-java-engineer/)
- [代码](source)
- [勘误](https://github.com/superhj1987/pragmatic-java-engineer/wiki/Mistakes)
-**已开源章节**
-
-> - [1.1 后端基础设施](book/chapter1-servertech/server-basic.md)
-> - [1.2 Java后端技术概览](book/chapter1-servertech/server-tech-tree.md)
-> - [1.3 如何学习后端技术](book/chapter1-servertech/how-to-study.md)
-> - [2.1 项目构建](book/chapter2-project/build.md)
-> - [2.2 代码版本控制](book/chapter2-project/vcs.md)
-> - [2.3 代码质量保证](book/chapter2-project/quality.md)
-> - [5.3 缓存](book/chapter5-datastore/cache.md)
-> - [8.1 调优准备](book/chapter8-profile/ready.md)
-> - [附录A: 代码构建常用命令](book/appendix/build-cmd.md)
-> - [附录B: Git常用命令](book/appendix/git-usage.md)
-> - [附录E: Java调优常用命令](book/appendix/java-profile.md)
-> - [附录F 如何应对在线故障](book/appendix/online-debug.md)
-> - [附录G 架构简明指南](book/appendix/arch-usage.md)
+### 内容介绍
-[**购买链接**](https://item.jd.com/12325207.html)
+[**前言**](book/README.md)
### 后续计划
@@ -42,70 +31,6 @@
- 补充RxJava的使用在Java开发利器中。
- 补充Java10和Kotlin的部分到Java新版本特性。
-**也欢迎大家提交内容,以丰富此书。^_^**
-
-### 内容介绍
-
-见 [**前言**](book/README.md)
-
-### 推荐
-
->2013年,我和本书作者的接触是从基于网易的一个大型互联网应用合作开始的,我见证了从第一行代码到整个系统服务于亿级用户的过程,并且相信这种经历对开发者来说是一笔巨大的财富,其中大量的开发和实战经验都会在本书中得到充分的体现,相信读者能从书中直接领略到丰富的实战知识。在与本书作者的合作过程中,其对Java技术的热爱与追求孜孜不倦,对问题刨根问底,直到理解透彻、灵活应用,这些都令我印象深刻。这些年,我与本书作者一直保持沟通交流、相互学习,他将近十年的实战经验沉底于本书以实现对后端技术的探索、布道,非常值得开发者与近高窗卧听秋。
->
->后端技术涉及内容非常广泛,Java语言也是互联网开发行业使用的主流语言,相信后续也将继续流行很长一段时间,而本书作者也一直从事Java后端开发工作。在本书中作者比较系统地从总体上描述了后端技术相关的理论知识,包括基础设施、网关服务及框架选型等基本原则,然后以实际经验进行示例说明,接着详细梳理了Java的后端技术,相信读者读完本书后会更全面地理解后端技术。互联网的业务建设需要不同角色的开发者共同协作完成,因此,系统工程化是开发者首先要共同遵守的规范或约定,包括代码规范、版本管理和代码质量检查等。
->
->开发框架的选型进一步地为工程化提供了基础,也能加速推进互联网开发,尽管是否重复造轮子是一个恒久的话题,但是没有永远的银弹,只要在合适的时间里根据团队的能力选择合适的技术框架就好。一般来讲,目前常用的框架包括基本的依赖注入、AOP、事务管理、连接池管理、数据操作、日志服务等,在众多的框架中,本书作者选用目前在Java领域使用最广泛的Spring做深入的分析,详细地说明各组件的基础知识、基本原理和实际使用案例,最难得的是把较多开发者遇到的坑都用真实的示例进行了说明,可以帮助开发者快速地跳过这些伤心地带,同时也把最佳实践画龙点睛地带给开发者。
->
->数据存储无疑是所有系统应用中非常重要的一环,应用的场景用例也和数据库的选型有极其重要的关系,开发者选择关系型数据库还是非关系型数据库是需要根据软件成本与人力成本来进行权衡的,比如是选择MySQL、Oracle等开源或商业的数据库。本书重点从数据库的基础知识、索引和表优化等方面以详尽的示例为更好地选择数据库的存储类型提供了更多的知识。
-
->早期的关系型数据库一般能满足数据达到一定规模的企业的需求,而在互联网业务领域,特别是移动互联网领域内的元数据或者日志数据等,达到亿数量级别是很常见的,这时通常使用非关系型数据库,在非关系型数据库里使用非常多的有MongoDB、HBase等分布式数据库系统。作者在自身的企业开发实践中,得到了大量的使用经验和最佳实践。为了加速后端应用,缓存热数据是加速业务、提高业务性能、提升用户体验的重要手段,通过使用本地缓存、远程缓存进行数据加速、数据预热或提高数据的命中率,是开发者在应用开发的过程中常会遇到的场景。
->
->“路漫漫其修远兮,吾将上下而求索”,后端技术每年都在不断发展,所用技术也有变化,近些年Java语言的发展速度不那么快了,但是总体是在不断前进发展的,本书作者带领的团队一直深耕此领域并希望通过本书为技术开发人员带来更多帮助。
->
->-- **尧飘海,网易云基础服务(蜂巢)首席架构师**
-
----
-
->Hey!新来的读者,为了吸引你的注意力我真是煞费苦心,但最终还是没能写出一句特别吸引眼球的话来,毕竟写序的我不是标题党出生。此刻我真的非常能理解你拿到新书之后那渴望知识的心情,所以你恨不得一个字的“序”也不要看到,直接到达“最有价值”的知识点。但作为一名资深转业码农(对!你没看错,是“转业”,不是“专业”)还是想说一句,你先看完序,5分钟后到达知识的战场,会更稳!
->
->相信你已经在看“序”了,那么我们来说点正经事。
->
->你的知识体系的养成有3个关键阶段:看山是山、看山不是山、看山还是山。本书的适用人群是“看山不是山”的那些人,如果你恰好处于这个阶段,恭喜你!书钱没白花。
->
->Java是一门非常容易入门的语言,初学者经过初期的学习之后基本能掌握DEMO级别的编程应用。相信读者你已经度过了这个阶段,但是Java庞大的体系可能会把你绕晕,又或者你还没看到Java的生态系统有多么复杂。此时,你需要本书。从事程序员这个工作,到比较高阶的时候,其实是不挑语言的,语言只是工具,而你可以在纷繁复杂中游刃有余。但几乎每一位高手都是先深入一个领域,再横向发展的。你可以不用着急后续的横向发展,先坚定自己学习Java的信心!因为,从广泛的应用场景、顶级的开源生态、未来可期的薪水和职位来说,Java都是非常不错的选择。
->
->敲黑板,画重点!下面来解释一下,为什么本书面向的是“看山不是山”的人群。在度过Java的入门期之后,会有一个烦恼,那就是面对Java这么庞大的体系,我们究竟应该学习什么?选择方向,往往比努力更重要!是使用J2SE编写桌面程序?是使用J2ME编写嵌入式应用?还是使用J2EE编写企业级应用?这些是我们那个泛黄的年代特有的烦恼。而现在的烦恼可能是学Android?还是学Java后端?即便大方向你已经十分坚定,而且选择了Java后端编程,但因为复杂的知识体系和Google发布的各种教程文档,眼前看到的已经不再是清晰的山脉,而是一片迷雾。此时,你需要本书,因为它给你指明了努力的方向。
->
->本书的结构、阐述的方式和大部分的“指南”书籍有较大的区别,本书是以笔记和要点的形式进行呈现的,用现在的话说就是捞干货。本书涵盖的知识,是以现代工程实践中的实际案例出发来组织的,所以知识点范围非常广泛,每一个点都对最关键的“Best Practice”简明扼要地进行了说明。你在阅读本书的时候需要一些相关经验,不然无法跟上作者的节奏,建议在有一定的知识准备后再阅读本书,这样你会受益匪浅。从另外一个角度看,在你有了一定的基础积累之后,本书可以帮助你全面地了解一个现代化的最先进的工程实践是怎样的。本书讲述了目前行业中最常用的,经过了实践的工程方案,这将是你快速进阶的最佳指引。
->
->-- **孙建,随身云(中华万年历)联合创始人&CEO**
-
----
-
->扎实的基础理论知识是内功底子,丰富的实践经验是招式。如本书作者所说,精妙的招式决定了你的武功下限,而深厚的内功底蕴会承载你所能企及的高度。那么,在后端技术栈中,内功与招式之间如何去关联起来,本书作者以其多年的钻研与实践结合心得,通过本书为你一一梳理。
->
->-- **阙杭宁,网易云信CTO**
-
----
-
->作者是一位技术人,有多年的Java技术积累,是极少数真正热爱技术的人。在随身云架构师的工作让他有机会站在更高的层次进行系统架构的工作,这些实践经验和平时感悟都沉淀在作者的著作和博客中,相信每位Java工程师都能从中获取帮助。
->
->-- **秦绪震,十露盘科技联合创始人,技术负责人**
-
----
-
->本书作者根据自身多年的JAVA后台开发经验, 提纲挈领的总结JAVA后台开发的各个关键技术点,这些知识点都是一个合格的JAVA工程师必须掌握的技能。它既可以作为新人的技术学习指南,也可以帮助老手对于自己的知识面进行查漏补缺,是一本非常好的技术指南。
->
->-- **饶洵(蜚天),阿里巴巴技术专家**
-
----
+## Star History
->作为一个在后端摸爬多年的Java开发工程师,这本书让我温故而知新。书中介绍的Java相关的知识技能树,不仅涵盖了我个人多年的Java开发技术知识点,也对我所陌生的一些知识点进行了详解,让我突然有一种继续学习的冲动。
->
->一个Java开发工程师的成长,不仅要对Java语言及其特性有深层次的理解,也需要掌握与Java相关的框架、生态及后端开发知识。这本书正是将后端开发工程师需要掌握的技能做了总结,对于提高开发技能有很好的指导作用。
->
->我推荐这本书,对于具有一定Java基础和后端开发知识的读者来说,该书不仅具有仔细学习的价值,同时也是一本可以经常翻阅的工具书籍,对于Java开发工程师的成长和进阶有很大的指导作用。
->
-> 一本好的技术书籍,不仅要仔细阅读、学习理解,还需要进行较多的实践,将所看所学进行应用,通过不断地实践,加深知识点印象,从而形成永久的记忆和技能。希望各位读者能够通过掌握书中的知识和技能,逐步成长为技术骨干和专家,从而创造更多的技术输出、产品输出,创造更多的财富。
->
-> -- **张小川,网易考拉海购架构师,供应链技术主管**
+[](https://star-history.com/#superhj1987/pragmatic-java-engineer&Date)
diff --git a/book/README.md b/book/README.md
index 24fcac5..4e60143 100644
--- a/book/README.md
+++ b/book/README.md
@@ -2,102 +2,38 @@
目前互联网行业如火如荼,进入这个行业的技术人员也越来越多。对于研发来说,从工程角度主要分为:前端工程师、客户端工程师(又分为iOS和Android工程师)、后端工程师、算法工程师等职位。本书所说的Java工程师指的是以Java做为主要开发语言的后端工程师。
-从2008年还未毕业时做一些小的项目至今做后端开发已经有差不多十年时间。经历过刚学Java时的迷茫,第一次写出Java程序时的激动,第一次写出一个Web系统的醍醐灌顶,一直到接触到Java更底层的东西,对Java有了一个系统的认识,对后端技术体系有了一个宏观的感受。这期间,用过各式各样的编程语言,尝试过各种开源软件,挖过各种坑,也填过各种坑。就单单针对后端的技术来说,自己的这些知识体系,还是觉得是有一定价值的。
+本书会针对Java后端开发工作中经常用到的关键技能点去做阐述,会尽量覆盖在实际工作中需要的所有技能点。但由于很多技能点并非一两篇文章就能讲述完成的,本书仅仅是做一些实践性的经验总结和阐述,更加详细和深入地学习则需要参考专门的书籍或者官方文档。
-此外,还记得笔者毕业后进入第一家公司时,入职培训的课程对于自己来说虽然不难,但确实让自己有种恍然大悟的感觉。业界的最佳实践和自己在学校里学到的、使用到的差别还是非常大的。直到后来加入当前的这家公司,做过一系列后端技术的培训课程,并且在校招的笔试和面试过程中,深刻体会到了学校中的知识与业界脱节之严重,在平时的社招中也遇到很多对后端技术缺乏系统性认识、技能点不足的工程师们,也经常被人问起如何学习Java后端技术。于是就打算写一下目前后端工程师一些比较主流前沿的技术以及实际工作中会用到的一些技能并串联起来,给刚上大学以后打算以Java后端为职业的学生、刚毕业入职的应届生以及初学者们一些入门的指引,避免走一些弯路,也给一些有经验的工程师们提供一个参考手册将零散的知识点串起来,减少在解决某些实际问题时无头绪搜索带来的时间成本,同时也是对自己的一个阶段性总结和查漏补缺。需要注意的一点是,像数据结构、计算机网络等计算机科学基础知识以及JavaSE基本用法,笔者认为是从事程序开发工作的Java工程师应该必备的知识点,因此并不包括在内。
+> - 《Effective Java(第3版)》:此书讲解了Java的一些高级特性和技巧。最新的第三本加入了Java8新语言特性部分。
-本书会针对Java后端开发工作中经常用到的关键技能点去做阐述,会尽量覆盖在实际工作中需要的所有技能点。但由于很多技能点并非一两篇文章就能讲述完成的,本书仅仅是做一些实践性的经验总结和阐述,更加详细和深入地学习则需要参考专门的书籍或者官方文档。因此,如果是对内容深度有要求的读者,那么本书并不适合。
-
-本书的大部分内容都来自笔者的博客以及平时工作、学习中的一些自我总结和笔记,记录了笔者进入这个行业以来的一些经验教训和思考,也是自己平时工作时会经常查阅的参考手册。
-
-## 读者对象
-
-- 未入门或者刚入门的Java工程师
-
- 包括未来以Java后端开发为职业方向的在校学生、刚毕业入职的Java工程师以及未形成知识体系的Java工程师。通过阅读本书能够对Java工程师的必备技能有一个全局的认识,逐步形成自己的Java技术体系。
+> - 《Java并发编程实战》:此书是并发编程经典书籍,涵盖了并发编程的各种知识点以及相关理论知识。
-- 有经验的Java工程师
-
- 有经验的Java工程师可以通过此书查漏补缺,巩固自己的开发技能,进一步加强自身的Java技术体系。
+> - 《七周七并发模式》:讲述了主流的七种并发编程模式。
-- 对Java后端开发感兴趣的非Java工程师
-
- 非Java工程师可以通过此书了解Java工程师的技能体系,尤其对于其他语言的后端工程师来说,本书的很多内容也是通用的,并不局限于Java开发。
-
-## 内容概览
-
-- 第一章 后端技术导言
-
- 本章主要从总体上描述后端技术的概念、组成、作用、需要的知识点,并给出了学习后端技术的建议。
-
-- 第二章 Java项目工程化
-
- 本章主要讲述Java项目工程化需要掌握的软件、技能等。
-
-- 第三章 开发框架
-
- 本章主要讲述Java后端开发中的一些主流框架的使用。
-
-- 第四章 Spring
-
- 本章主要讲述Spring核心、数据操作以及一些常用组件的使用。
-
-- 第五章 数据存储
-
- 本章主要讲述Java应用中数据存储上使用的一些软件、服务等。
-
-- 第六章 数据通信
-
- 本章主要讲述Java应用中数据传输、通信上使用的一些软件、服务等。
-
-- 第七章 Java编程进阶
-
- 本章主要介绍一些Java开发中的高级特性以及在Java开发中非常流行的类库。
-
-- 第八章 性能调优
-
- 本章主要讲述如何对Java应用的性能进行分析和调优,并给出了开发建议。
-
-- 第九章 安全技术
-
- 本章主要对Java开发中常用的加密技术、HTTP以及防范各种攻击的方案做了阐述。
-
-## 参考资料
-
-在写作本书以及平时的工作中,笔者阅读、参考过很多书籍,以下是其中具有代表性的。对于本书讲述不够深入的地方,可以参考这些书籍进一步的学习。
-
-- 《Effective Java(第2版)》:此书讲解了Java的一些高级特性和技巧,目前第三版的英文版已经在亚马逊上架。
-
-- 《Java并发编程实战》:此书是并发编程经典书籍,涵盖了并发编程的各种知识点以及相关理论知识。
-
-- 《七周七并发模式》:讲述了主流的七种并发编程模式。
-
-- 《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》:此书讲解了JVM的内存、GC、字节码、编译器等高级特性和优化实践。
+> - 《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》:此书讲解了JVM的内存、GC、字节码、编译器等高级特性和优化实践。
-- 《高性能MySQL(第3版)》:此书讲述了MySQL各种优化技巧,并结合原理给予讲解。
+> - 《高性能MySQL(第3版)》:此书讲述了MySQL各种优化技巧,并结合原理给予讲解。
-- 《Redis开发与运维》:此书在原理和实践层面对于Redis的使用优化做了详尽的描述。
+> - 《Redis开发与运维》:此书在原理和实践层面对于Redis的使用优化做了详尽的描述。
-- 《Redis设计与实现》:完整地讲解了Redis的内部运行机制,对Redis的大多数单机功能以及所有多机功能的实现原理进行了介绍,包括这些功能的核心数据结构以及关键的算法思想。旧版本(Redis 2.6)有免费电子版:http://origin.redisbook.com/ 。
+> - 《Redis设计与实现》:完整地讲解了Redis的内部运行机制,对Redis的大多数单机功能以及所有多机功能的实现原理进行了介绍,包括这些功能的核心数据结构以及关键的算法思想。旧版本(Redis 2.6)有免费电子版:http://origin.redisbook.com/。
-- 《深入分布式缓存》:此书涵盖了分布式原理、各种缓存软件/框架的使用以及相关技术在各大公司的典型实践。
+> - 《深入分布式缓存》:此书涵盖了分布式原理、各种缓存软件/框架的使用以及相关技术在各大公司的典型实践。
-- 《深入理解Elasticsearch(原书第2版)》:此书在原理层面讲述了对ES的使用和优化技巧。
+> - 《深入理解Elasticsearch(原书第2版)》:此书在原理层面讲述了对ES的使用和优化技巧。
-- 《Java性能权威指南》:此书是Java性能调优的权威书籍,几乎涵盖了Java调优的方方面面。
+> - 《Java性能权威指南》:此书是Java性能调优的权威书籍,几乎涵盖了Java调优的方方面面。
-- 《构建高性能Web站点》:此书从各种案例触发,讲解了高性能Web站点需要的各种优化技巧、实践经验等。
+> - 《构建高性能Web站点》:此书从各种案例出发,讲解了高性能Web站点需要的各种优化技巧、实践经验等。
-- 《白帽子讲Web安全》:本书基本涵盖了方方面面的Web安全技术,包括客户端安全、服务端安全等。
+> - 《白帽子讲Web安全》:本书基本涵盖了方方面面的Web安全技术,包括客户端安全、服务端安全等。
-- 《分布式系统:概念与设计(原书第5版)》:分布式系统理论的经典书籍,全面介绍了分布式系统的原理、体系结构、算法和设计。
+> - 《分布式系统:概念与设计(原书第5版)》:分布式系统理论的经典书籍,全面介绍了分布式系统的原理、体系结构、算法和设计。
-- 《Clean Architecture》: Uncle Bob的架构经典书籍,梳理了架构的定义、目的、架构设计原则、设计模式等,是架构入门的好书。
+> - 《Clean Architecture》: Uncle Bob的架构经典书籍,梳理了架构的定义、目的、架构设计原则、设计模式等,是架构入门的好书。
虽然以上书籍都是非常实用的参考资料,但就笔者自己来看,更为推崇的则是直接通过相关技术的官方文档来学习,既能够段炼自己的英文阅读能力,也能够直面相关技术的第一手文档,避免了在看相关书籍时被一些偶然的纰漏所误导。
-需要注意的是这里面的《Effective Java》(第三版英文版已经面世)和《Java并发编程实战》两本书都是基于Java的旧版本来写的,但是其讲述的很多东西并不过时,尤其是JDK底层源码、设计理论、优化思想等仍然适用于现在的Java开发。
-
此外,笔者的学习、工作笔记是平时工作中查阅网上资料并经过辨伪后记录下来的零散知识点,难免会有一些对网上零散资料的引用,特别对这些资料的原创者表示感谢,如果有侵权请联系我。
## 勘误和支持
@@ -112,22 +48,19 @@

-## 致谢
-
-由于工作以及个人身体等方面的原因,中间数次延期,历时一年多才完成本书。因此首先要特别感谢我的父母和妻子,在我写作此书的过程中给予了非常大的后勤支持和鼓励,让我能够专心地完成写作。
+## 联系方式
-同时要感谢中华万年历的同事们在平时的工作中给了我很多启发和思路,感谢公司的设计总监张喜亮抽出时间帮我修饰了一些图片;尤其要感谢CEO孙建在写作此书的过程中给予了我充分的信任和支持。
+邮箱:superhj1987@126.com
-还要感谢我的前同事,也是我刚毕业时的工作导师尧飘海,在百忙之中审阅并给此书写序;也要感谢前同事张小川、阙杭宁和好友秦绪震、饶洵抽出宝贵时间校对了此书并给出了宝贵的建议。
+博客:https://rowkey.cn
-最后,感谢电子工业出版社永恒的侠少找到我出版此书,并允许我一次次延期。也感谢付睿编辑的辛苦校对和修改,让本书得以顺利出版。
+微博:https://weibo.com/superhj1987
-也把此书献给我刚出生的女儿 - 依依。
+## 友情赞助
-## 联系方式
+友情赞助可扫码。
-邮箱:superhj1987@126.com
+
-博客:http://rowkey.me
-微博:http://weibo.com/superhj1987
+
\ No newline at end of file
diff --git a/book/SUMMARY.md b/book/SUMMARY.md
index 046c261..06814a8 100644
--- a/book/SUMMARY.md
+++ b/book/SUMMARY.md
@@ -17,7 +17,7 @@
- [3.2 对象关系映射](chapter3-framework/orm.md)
- [3.3 日志](chapter3-framework/log.md)
- [3.4 Web MVC](chapter3-framework/mvc.md)
- - [3.5 响应式Web框架](chapter3-framework/reactive-web.md)
+ - [3.5 总结](chapter3-framework/end.md)
* [第四章 Spring](chapter4-spring/README.md)
- [4.1 Spring核心组件](chapter4-spring/spring.md)
@@ -25,17 +25,19 @@
- [4.3 使用Spring Boot快速开发](chapter4-spring/spring-boot.md)
- [4.4 Spring常用组件](chapter4-spring/spring-common.md)
- [4.5 总结](chapter4-spring/end.md)
-
+
* [第五章 数据存储](chapter5-datastore/README.md)
- [5.1 关系型数据库-MySQL](chapter5-datastore/rds.md)
- [5.2 非关系型数据库](chapter5-datastore/nosql.md)
- [5.3 缓存](chapter5-datastore/cache.md)
- [5.4 搜索引擎-Elasticsearch](chapter5-datastore/search.md)
+ - [5.5 总结](chapter5-datastore/end.md)
* [第六章 数据通信](chapter6-datatrans/README.md)
- [6.1 RESTful架构风格](chapter6-datatrans/rest.md)
- [6.2 远程过程调用-RPC](chapter6-datatrans/rpc.md)
- [6.3 消息中间件](chapter6-datatrans/message.md)
+ - [6.4 总结](chapter6-datatrans/end.md)
* [第七章 Java编程进阶](chapter7-java/README.md)
- [7.1 Java内存管理](chapter7-java/java-mm.md)
@@ -48,7 +50,7 @@
* [第八章 性能调优](chapter8-profile/README.md)
- [8.1 调优准备](chapter8-profile/ready.md)
- [8.2 性能分析](chapter8-profile/analysis.md)
- - [8.3 性能调优](chapter8-profile/chapter8-profile.md)
+ - [8.3 性能调优](chapter8-profile/profile.md)
- [8.4 总结](chapter8-profile/end.md)
* [第九章 安全技术](chapter9-security/README.md)
@@ -68,4 +70,6 @@
* [附录F: 如何应对在线故障](appendix/online-debug.md)
-* [附录G: 架构简明指南](appendix/arch-usage.md)
\ No newline at end of file
+* [附录G: 架构简明指南](appendix/arch-usage.md)
+
+
diff --git a/book/appendix/arch-usage.md b/book/appendix/arch-usage.md
index 443c10c..3b8fe26 100644
--- a/book/appendix/arch-usage.md
+++ b/book/appendix/arch-usage.md
@@ -6,7 +6,7 @@
即:软件架构的目的就是解决软件复杂度(高性能、高可用、可扩展、低成本、安全、规模)带来的问题,将构建和维护系统需要的人力成本降到最低。
-因此,可以得出架构设计的关键思维就是判断和取舍(程序设计的关键思维是逻辑和实现),即如何选择技术、组合技术使得需要的人力资源最少。
+因此,可以得出架构设计的关键思维就是判断和取舍(程序设计的关键思维是逻辑和实现),即如何选择技术、组合技术使得需要的人力资源最少,达到降本增效的目的。
需要注意的一点是,脱离业务谈架构是不合理的,技术架构及其演进都是业务目标驱动的。
@@ -22,28 +22,10 @@
架构的目的就是解决复杂度,主要包括高性能、高可用以及可扩展三方面。此外,分布式系统是架构工作中面对的典型复杂系统,对于其中常见的问题有一些常用应对手段。
-### 高性能
-
-- 数据库集群
-- 缓存架构
-- 负载均衡
-- NoSQL
-
-**低延迟方案**[系统响应性能提升五板斧]
-
-- **异步**:队列缓冲、异步请求。
-- **并发**:利用多CPU多线程执行业务逻辑。
-- **就近原则**:缓存、梯度存储。
-- **减少IO**:合并细粒度接口为粗粒度接口、频繁的覆盖操作可以只做最后一次操作。这里一个需要特别注意的地方: **代码中尽量避免在循环中调用外部服务,更好的做法是使用粗粒度批量接口在循环外面只进行一次请求。**
-- **分区**:频繁访问的数据集规模保持在合理的范围。
-
-**高吞吐方案**
-
-- 分层调用
-- 异步并发
-
### 高可用
+> 高可用=系统构建在多机=分布式系统
+
- 冗余:同城多活或者异地多活
- 降级:需要对各个关键节点建立降级预案。能够在超出预估流量时,保证大部分用户的服务是正常的。包括一个请求经过的多有节点。以轮训实现的直播系统为例:
@@ -70,6 +52,29 @@ Kafka | 消息堆积
消息处理BG | 错误日志;JVM;消息处理数目,消息处理延时
基础资源 | 带宽、CPU利用率、内存、磁盘
+### 高性能
+
+> 分布式系统的副产品
+
+- 数据库集群
+- 缓存架构
+- 负载均衡
+- NoSQL:不局限于关系型数据库,在合适的场景下选择NoSQL数据库会带来性能的提升
+- 异构索引:分区情况下,为提升未按拆分键进行查询的场景的性能,通过构建异构索引表。先通过查询异构索引表得到目标记录的主键,然后再根据记录主键查询,从而避免全库全表扫描
+
+**低延迟方案**[系统响应性能提升]
+
+- **异步**:队列缓冲、异步请求。
+- **并发**:利用多CPU多线程执行业务逻辑。
+- **就近原则**:缓存、梯度存储。
+- **减少IO**:合并细粒度接口为粗粒度接口、频繁的覆盖操作可以只做最后一次操作。这里一个需要特别注意的地方: **代码中尽量避免在循环中调用外部服务,更好的做法是使用粗粒度批量接口在循环外面只进行一次请求。**
+- **分区**:频繁访问的数据集规模保持在合理的范围。
+
+**高吞吐方案**
+
+- 分层调用:接入层、逻辑层、数据层,通过Proxy或者Router对逻辑层做集群管理
+- 异步并发
+
### 可扩展
- 分层架构/简洁架构:单向依赖,职责清晰。
@@ -87,9 +92,7 @@ Kafka | 消息堆积
1. 海量请求问题
- - 高吞吐:分层调用、异步并发
- - 低延迟:系统响应性能提升五板斧(见上文)、NoSQL
- - 高可用:冗余、降级
+ 本质即如何达到高吞吐、低延迟、高可用,上文已经讲述。
1. 大量服务器管理
@@ -237,9 +240,11 @@ ps: 在容量预估中,机器数目的计算遵循DID原则:20倍设计、3
1. 上线双写:在业务系统里写代码,同时向新旧数据存储写入数据。此步骤完成后,需要进行一致性验证,包括存储维度和业务维度。前者指对比原数据存储和新数据存储中的数据对比,后者指从用户看到的数据维度进行对比。
2. 历史数据迁移:将历史数据从旧存储迁移到新存储。包括离线和在线两种。离线是编写批量处理程序或者依靠数据存储的同步机制从旧存储查询历史数据(开启双写以前的数据)插入到新存储中。在线指的是依赖数据存储的同步机制在线同步数据,如MySQL的binlog、MongoDB的OpLog。此过程,建议在部分数据迁移后就进行一致性验证,通过后再全量数据迁移。
-3. 切读:通过灰度的方式逐渐切换请求到新系统上,灰度可以通过在代码中埋入开关来逐步的放大读新系统的请求量。一般的流程:预发布环境(验证代码运行正常)->办公室环境/线上环境白名单(内部用户,验证功能正常)->线上环境百分比0.1%、1%、%10%(进一步验证功能正常以及性能和资源压力)->线上环境全量。此过程建议持续一到两周。
+3. 切读:通过灰度的方式逐渐切换请求到新系统上,灰度可以通过在代码中埋入开关来逐步的放大读新系统的请求量。一般的流程:预发布/Tcpcopy环境(验证代码运行正常)->办公室环境/线上环境uid白名单(内部用户,验证功能正常)->线上环境百分比0.1%、1%、%10%(进一步验证功能正常以及性能和资源压力)->线上环境全量。此过程建议持续一到两周。
4. 清理:数据迁移验证通过后,清理业务系统的双写代码和开关代码等逻辑代码、旧存储的数据和配套系统以及旧的资源等。
+某些情况下,可以先做历史数据搬迁,然后再写入新数据。需要谨慎的处理搬迁这段时间里产生的新数据,一般使用 queue 缓存写入的方式,称为“追数据”。
+
此外,如果是单一功能的在线数据迁移,可以参考Redis Cluster数据重分配的实现机制。
1. 离线程序迁移数据,并维护数据迁移状态:未迁移、迁移中、迁移完成。
diff --git a/book/appendix/build-cmd.md b/book/appendix/build-cmd.md
index a68fc24..8c08a71 100644
--- a/book/appendix/build-cmd.md
+++ b/book/appendix/build-cmd.md
@@ -24,7 +24,7 @@ Maven版本:3.3.9
- 部署非Maven项目的jar包
- `mvn deploy:deploy-file -DgroupId=[groupId] -DartifactId=[artifactId] -Dversion=[version] -Dpackaging=jar -Dfile=[jarFilePath] -Durl=[repositoryUrl]`
+ `mvn deploy:deploy-file -DgroupId=[groupId] -DartifactId=[artifactId] -Dversion=[version] -Dpackaging=jar -Dfile=[jarFilePath] -Durl=[repositoryUrl] -DrepositoryId=[repositoryId]`
- 安装非Maven项目的jar包到本地
diff --git a/book/appendix/git-usage.md b/book/appendix/git-usage.md
index e77bc20..6dc8973 100644
--- a/book/appendix/git-usage.md
+++ b/book/appendix/git-usage.md
@@ -87,10 +87,17 @@ Git的配置,分为三个级别:
ssh -T git@github.com #测试是否成功
#使用ssh-agent管理密码,避免后续需要身份验证的地方需要输入密码
- ssh-add -K private_key_path #添添加私钥到ssh-agent中,使用-K参数将密钥加入到密钥链中
+ ssh-add -K private_key_path #添添加私钥到ssh-agent中,使用-K参数将密钥加入到密钥链中(Mac OS特有参数)
ssh-add -l #查看当前计算机中存储的密钥
ssh-add -d public_key_path #将对应的私钥从ssh-agent删除
```
+
+1. 使用http/https协议访问Git仓库时缓存密码
+
+ ```
+ # 默认不缓存;cache缓存在内存中,默认15分钟失效;store,明文存储在磁盘上,永不过期;osxkeychain,Mac OS特有,加密存储在用户钥匙串中,永不过期;winstore和manager是Windows下特有,取决于安装的是git-credential-winstore/git-credential-manager(GitGUI自带)
+ git config --global credential.helper [cache|store|osxkeychain|winstore/manager]
+ ```
## 取得项目的Git仓库
@@ -179,7 +186,7 @@ Git的配置,分为三个级别:
```
git commit [file1] [file2] #提交会提示输入本次提交说明
- git commit -m [messag] #直接附带提交说明
+ git commit -m [message] #直接附带提交说明,message用双引号包裹是单行信息,用单引号则可以提交多行信息
git commit --amend #修改最后一次提交
git commit -v #提交时显示所有diff信息
git commit --amend -m [message] #使用一次新的commit,替代上一次提交,如果代码没有任何新变化,则用来改写上一次commit的提交信息
@@ -301,6 +308,7 @@ git branch #列出本地分支
git branch -r #列出远端分支
git branch -a #列出所有本地分支和远程分支
git branch -v #查看各个分支最后一个提交对象的信息
+git branch -vv #使用两个v,额外显示本地分支和远程分支的追踪关系
git branch --merge #查看已经合并到当前分支的分支
git branch --no-merge #查看为合并到当前分支的分支
diff --git a/book/appendix/java-profile.md b/book/appendix/java-profile.md
index c0b8795..d594e29 100644
--- a/book/appendix/java-profile.md
+++ b/book/appendix/java-profile.md
@@ -165,7 +165,7 @@
#-XX:+PrintCompilation #输出JIT编译情况,慎用
-XX:+TieredCompilation #启用多层编译,JDK8默认开启
-XX:CICompilerCount=4 #编译器数目增加
--XX:-UseBiasedLocking #取消偏向锁。偏向锁会触发进入Safepoint,引起停顿,因此高并发应用建议取消偏向锁。
+-XX:-UseBiasedLocking #取消偏向锁。偏向锁会触发进入Safepoint,引起停顿,因此高并发应用建议取消偏向锁
-XX:AutoBoxCacheMax=20000 #自动装箱的缓存数量,如int默认缓存为-128~127
-Djava.security.egd=file:/dev/./urandom #替代默认的/dev/random阻塞生成因子
-XX:+AlwaysPreTouch #启动时访问并置零内存页面,大堆时效果比较好
@@ -196,6 +196,8 @@
-XX:GCTimeLimit=98 #GC占用时间超过多少抛出OutOfMemoryError
-XX:GCHeapFreeLimit=2 #GC回收后小于百分之多少抛出OutO fMemoryError
-Xloggc:/home/logs/gc.log #GC日志路径,重启后会被清空
+-XX:+PrintCommandLineFlags #将每次JVM启动的参数输出到stdout,以供追溯。
+-XX:-OmitStackTraceInFastThrow #对一些特定的异常类型(NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException、ArrayStoreException、ClassCastException)的Fast Throw优化,如果检测到在代码里某个位置连续多次抛出同一类型异常的话,会用Fast Throw方式来抛出异常,不带上异常栈信息。在连续抛出大量重复异常并且很难回溯前面完整栈信息时可以关闭此选项使得不会进行Fast Throw优化
#-XX:+UseGCLogFileRotation #开启GC日志滚动输出
#-XX:NumberOfGCLogFiles=100 #轮转日志数目最大为100,超过则覆盖
#-XX:GCLogFileSize=100M #GC轮转日志最大尺寸100mb,超过则另起一个日志文件
diff --git a/book/appendix/media/apm.png b/book/appendix/media/apm.png
new file mode 100644
index 0000000..5283c89
Binary files /dev/null and b/book/appendix/media/apm.png differ
diff --git a/book/appendix/media/distribute-qa.jpg b/book/appendix/media/distribute-qa.jpg
new file mode 100644
index 0000000..b515d1f
Binary files /dev/null and b/book/appendix/media/distribute-qa.jpg differ
diff --git a/book/appendix/media/low-level.png b/book/appendix/media/low-level.png
new file mode 100644
index 0000000..f2c3568
Binary files /dev/null and b/book/appendix/media/low-level.png differ
diff --git a/book/appendix/media/sixthink.jpg b/book/appendix/media/sixthink.jpg
new file mode 100644
index 0000000..9f59159
Binary files /dev/null and b/book/appendix/media/sixthink.jpg differ
diff --git a/book/appendix/mongo-usage.md b/book/appendix/mongo-usage.md
new file mode 100644
index 0000000..ab522cc
--- /dev/null
+++ b/book/appendix/mongo-usage.md
@@ -0,0 +1,166 @@
+# 附录D: MongoDB常用命令
+
+MongoDB版本:3.2.7
+
+## 1. 基本操作
+
+* db.getMongo():取得当前服务器的连接对象。
+* db.createUser(user, writeConcern) :添加用户。
+* db.dropUser(username):删除用户。
+* db.system.users.find():查看系统所有用户列表。
+* db.system.users.remove({user:"mongouser"}): 删除用户。
+* db.getUsers():查看当前数据库的用户。
+* db.auth(usrename,password);验证用户。
+* db.getName():返回当操作数据库的名称。
+* db.createCollection(name):创建一个数据集。
+* db.currentOp():查看数据库的当前操作。
+* db.dropDataBase():删除当前数据库。
+* db.getCollection(collectonName):取得一个数据集合。
+* db.getCollenctionNames():取得所有数据集合的名称列表。
+* db.getLastError():返回最后一个错误的提示消息。
+* db.getLastErrorObj():返回最后一个错误的对象。
+* db.getReplicationInfo():获得复制集的信息。
+* db.printReplicationInfo():打印复制集的信息。
+* db.printCollectionStats():返回当前库的数据集合状态。
+* db.printSlaveReplicationInfo():打印从数据库的复制集信息。
+* db.printShardingStatus():打印分片状态。
+* db.commandHelp(command):显示命令的帮助信息。
+* db.runCommand(cmdObj):运行一个数据库命令。
+* db.setProfilingLevel(level,slowms):设置数据库的优化级别(0=off,1=slow,2=all)以及慢查询的耗时阈值。
+* db.getProfilingStatus():获取数据库的优化级别和慢查询的耗时阈值。
+* db.system.profile.find():查看收集到的慢查询。
+* db.version():返回当前程序的版本信息。
+* db.serverStatus().connections:连接数信息,其中current数值+available数值就是当前mongodb最大连接数。
+* db.serverStatus().mem:内存占用信息。
+* db.cloneDataBase(fromhost):从目标服务器克隆一个数据库。
+* db.copyDatabase(fromdb,todb,fromhost):复制数据库:fromdb-源数据库名称,todb-目标数据库名称,fromhost-源数据库服务器地址。
+* db.repairDatabase():修复当前数据库。
+* db.killOp():停止/杀死在当前库的当前操作。
+* db.shutdownServer():安全关闭当前服务程序。
+
+## 2. 数据集操作
+
+* db.test_collection.drop():删除数据集。
+* db.createCollection(“test_collection”):创建集合。
+* db.test_collection.renameCollection("test_collection1"):重命名集合。
+* db.test_collection.find({status:1}):返回test_collection数据集status=1的数据集。
+* db.test_collection.find({status:1}}).count():返回test_collection数据集中status=1的数据总数。
+* db.test_collection.find({status:1}).limit(3):返回test_collection数据集中status=1的前三条数据。
+* db.test_collection.find({status:1}).skip(2):返回test_collection数据集中status=1的从第三条开始的数据。
+* db.test_collection.find({status:1}).limit(24).skip(8):返回test_collection数据集中status=1的从第九条开始的24条数据。
+* db.test_collection.find({status:1}}).sort():返回test_collection数据集中status=1的有序数据。
+* db.test_collection.findOne([query]) 返回符合条件的一条数据。
+* db.test_collection.getIndexes():返回此数据集的索引信息。
+* db.test_collection.mapReduce(mayFunction,reduceFunction):执行MapReduce操作。
+* db.test_collection.remove(query):在数据集中删除一条数据。
+* db.test_collection.remove({}):清空数据集合。
+* db.test_collection.save(obj):往数据集中插入/更新一条数据。
+* db.test_collection.stats():返回此数据集的状态。
+* db.test_collection.storageSize():返回此数据集的存储大小
+* db.test_collection.totalIndexSize():返回此数据集的索引文件大小。
+* db.test_collection.totalSize():返回此数据集的总大小。
+* db.test_collection.update(query,object[,upsert_bool]):在此数据集中更新一条数据。
+* db.test_collection.createIndex(keys[,options]):创建索引。
+* db.test_collection.getIndexes():查看索引。
+* db.test_collection.dropIndex('[indexName]'):删除索引。
+
+## 3. MongoDB语法与关系型数据库SQL语法比较
+
+* db.test_collection.find({'name':'testname'}) <-> select * from test_collection where name='testname'
+* db.test_collection.find({$or:[{'name':'testname'},{'name':'testname2'}]}) <-> select * from test_collection where name='testname' or testname='testname2'
+* db.test_collection.find() <-> select * from test_collection
+* db.test_collection.find({'status':1}).count() <-> select count(*) from test_collection where status=1
+* db.test_collection.find().skip(10).limit(20) <-> select * from test_collection limit 10,20
+* db.test_collection.find({'status':{$in:[1,2]}}) <-> select * from test_collection where status in (1,2)
+* db.test_collection.find().sort({'status':-1}) <-> select * from test_collection order by status desc
+* db.test_collection.distinct('name',{'age':{$lt:25}}) <-> select distinct(name) from test_collection where age < 1
+* db.test_collection.group({key:{'name':true},cond:{'name':'foo'},reduce:function(obj,prev){prev.msum+=obj.star;},initial:{msum:0}}) <-> select name,sum(stat) from test_collection group by name
+* db.test_collection.find('this.age<25',{name:1}) <-> select name from test_collection where age < 20
+* db.test_collection.insert({'name':'testname','age':25})<->insert into test_collection ('name','age') values('testname',25)
+* db.test_collection.remove({}) <-> delete from test_collection
+* db.test_collection.remove({'age':25}) <-> delete from test_collection where age=25
+* db.test_collection.remove({'age':{$lt:20}}) <-> delete from test_collection where age<25
+* db.test_collection.remove({'age':{$lte:20}}) <-> delete from test_collection where age<=25
+* db.test_collection.remove({'age':{$gt:20}}) <-> delete from test_collection where age>25
+* db.test_collection.remove({'age':{$gte:20}}) <-> delete from test_collection where age>=2
+* db.test_collection.remove({'age':{$ne:20}}) <-> delete from test_collection where age!=25
+* db.test_collection.updateMany({'name':'testname'},{$set:{'age':30}}) <-> update test_collection set age=30 where name='testname'
+* db.test_collection.updateMany({'name':'testname'},{$inc:{'age':2}}) <-> update test_collection set age=age+2 where name='testname'
+* db.test_collection.find({name: /testname/}) <-> select * from test_collection where name like ‘%testname%’;
+* db.test_collection.find({name: /^testname/}) <-> select * from test_collection where name like ‘testname%’;
+
+## 4. 开启安全认证
+
+### 创建管理员用户
+
+```
+use admin
+db.createUser(
+ {
+ user: "root",
+ pwd: "root123",
+ roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] #roles设置为root则为超级用户权限
+ }
+)
+
+mongod --auth --port 27017 --dbpath /data/db --authenticationDatabase "admin"
+use admin;
+db.auth("root","root123");
+```
+
+### 创建用户
+```
+use test_db;
+db.createUser(
+ {
+ user: "mongouser",
+ pwd: "mongo123",
+ roles: [
+ { role: "readWrite", db: "test_db" }
+ ]
+ }
+)
+```
+
+### 修改密码
+
+```
+db.changeUserPassword("mongouser", "123456”)
+```
+
+### 获取某用户的权限信息
+
+```
+db.getUser("mongouser")
+```
+
+### 获取某角色的权限信息
+
+```
+db.getRole( "read", { showPrivileges: true } )
+```
+
+### 赋予权限
+
+```
+use test_db;
+db.grantRolesToUser(
+ "mongouser",
+ [
+ { role: "read", db: “test_db" }
+ ]
+)
+```
+
+### 删除权限
+
+```
+use test_db;
+db.revokeRolesFromUser(
+ "mongouser",
+ [
+ { role: "readWrite", db: "test_db" }
+ ]
+)
+```
+
diff --git a/book/appendix/mysql-usage.md b/book/appendix/mysql-usage.md
new file mode 100644
index 0000000..a274a78
--- /dev/null
+++ b/book/appendix/mysql-usage.md
@@ -0,0 +1,454 @@
+# 附录C: MySQL常用命令
+
+MySQL版本:5.5.19
+
+## 系统命令
+
+1. 启动MySQL
+
+ ```
+ mysqladmin start
+ /ect/init.d/mysql start
+ ```
+
+2. 重启MySQL
+
+ ```
+ mysqladmin restart
+ /ect/init.d/mysql restart
+ ```
+
+3. 关闭MySQL
+
+ ```
+ mysqladmin shutdown
+ /ect/init.d/mysql shutdown
+ ```
+
+4. 连接本机上的MySQL
+
+ 进入目录`mysql\bin`,键入命令`mysql -uroot -p`,回车后提示输入密码。使用`exit`退出MySQL。
+
+5. 修改MySQL密码
+
+ ```
+ mysqladmin -u用户名 -p旧密码 password 新密码
+ ```
+
+ 或进入MySQL命令行后设置
+
+ ```
+ set password for root=password("root");
+ ```
+
+6. 增加新用户
+
+ ```
+ grant select on 数据库.* to 用户名@登录主机 identified by "密码";
+ ```
+
+ 示例:增加一个用户test密码为123,让他可以在任何主机上登录,并对所有数据库有查询、插入、修改、删除的权限。以root用户连入MySQL,然后键入以下命令:
+
+ ```
+ grant select,insert,update,delete on *.* to test@"% " identified by "123";
+ ```
+
+7. 刷新MySQL的系统权限相关表
+
+ ```
+ flush privileges
+ ```
+
+ 新设置用户或更改密码后需刷新MySQL的系统权限相关表
+
+1. 查看MySQL支持的存储引擎
+
+ ```
+ show engines
+ ```
+
+1. 查看MySQL当前的默认存储引擎
+
+ ```
+ show variables like '%storage_engine%';
+ ```
+
+1. 查看Mysql服务器上的版本
+
+ ```
+ select version();
+ ```
+
+1. 查看数据库连接情况
+
+ ```
+ show variables like '%max_connections%’; #查看最大连接数设置
+ set global max_connections = 200; #设置最大连接数
+
+ select * from information_schema.processlist where db=''; #查看进程/连接列表,可指定数据库。和show processlist一样
+ ```
+
+1. 查看死锁信息
+
+ ```
+ show engine innodb status; #LATEST DETECTED DEADLOCK这一栏即死锁信息
+ ```
+
+1. 开启慢查询日志
+
+ ```
+ show variables like '%slow%’; //查看慢查询日志配置
+ set global slow_query_log='ON'; //开启慢查询
+ set global long_query_time=4;//这只慢查询语句的耗时阈值
+ ```
+
+1. 查询结果输出到文件
+
+ ```
+ [select query] into outfile '[filePath]';
+
+ pager cat > [filePath]; #所有查询结果都自动写入指定文件中,并前后覆盖
+
+ mysql -h [host] -u [user] -p [password] -P [port] -e "[query]" > [filePath]
+ ```
+
+## 数据操作
+
+首先登录到MySQL中,有关操作都是在MySQL的提示符下进行,而且每个命令以分号结束。
+
+1. 显示数据库列表
+
+ ```
+ show databases;
+ ```
+
+2. 显示库中的数据表
+
+ ```
+ show tables;
+ ```
+
+3. 显示数据表的结构
+
+ ```
+ describe 表名;
+ ```
+
+4. 建库
+
+ ```
+ create database 库名;
+
+ create database db_name default character set utf8 collate utf8_general_ci;
+ ```
+
+ create database 的语法:
+
+ ```
+ create {database | schema} [if not exists] db_name [create_specification [, create_specification ] ...] create_specification : [default] character set charset_name | [default] collate collation_name
+ ```
+
+5. 建表
+
+ ```
+ create table 表名(字段设定列表);
+ ```
+
+6. 删库和删表
+
+ ```
+ drop database 库名;
+ drop table 表名;
+ ```
+
+7. 将表中记录清空
+
+ ```
+ delete from 表名;
+ truncate table 表名; #不同于delete, 不用扫描全表
+ ```
+
+8. 显示表中的记录:
+
+ ```
+ select * from 表名;
+ ```
+
+1. 添加列
+
+ ```
+ alter table 数据表名 add 新列名 新列类型 default 0 comment;
+ ```
+
+1. 修改列名
+
+ ```
+ alter table 数据表名 change 原列名 新列名 新列类型;
+ ```
+
+1. 修改列
+
+ ```
+ alter table 表名 modify column 列名 类型;
+ ```
+
+1. 删除列
+
+ ```
+ alter table 表名 drop column 列名;
+ ```
+
+1. 修改表名
+
+ ```
+ rename table 旧表名 to 新表名;
+ ```
+
+1. 添加索引
+
+ ```
+ alter table table_name add index index_name (column_list);
+ alter table table_name add unique index_name (column_list);
+ alter table table_name add primary key (column_list);
+ ```
+
+1. 删除索引
+
+ ```
+ drop index index_name on talbe_name
+ alter table table_name drop index index_name
+ alter table table_name drop primary key
+ ```
+
+1. 查看索引
+
+ ```
+ show index from tblname;
+ show keys from tblname;
+ ```
+
+1. 复制表结构及数据到新表
+
+ ```
+ create table 新表 select * from 旧表;
+ ```
+
+1. 只复制表结构到新表
+
+ ```
+ create table 新表 select * from 旧表 where 1=2;
+ create table 新表 like 旧表;
+ ```
+
+1. 复制旧表的数据到新表(假设两个表结构一样)
+
+ ```
+ insert into 新表 select * from 旧表;
+ ```
+
+1. 复制旧表的数据到新表(假设两个表结构不一样)
+
+ ```
+ insert into 新表(字段1,字段2,.......) select 字段1,字段2,...... from 旧表
+ ```
+
+1. 设置表的自增主键起始值
+
+ ```
+ alter table table_name AUTO_INCREMENT = 10000;
+ ```
+
+## 数据的导入导出
+
+1. 文本数据转到数据库中
+
+ 文本数据应符合的格式:字段数据之间用tab键隔开,null值用空格字符来代替。例如:
+
+ ```
+ 1 name test 2017-1-1
+ ```
+
+ 数据传入命令
+
+ ```
+ load data local infile "文件名" into table 表名;
+ ```
+
+2. 导出数据库和表
+
+ 将数据库news中的所有表备份到news.sql文件,news.sql是一个文本文件,文件名任取。
+
+ ```
+ mysqldump --opt news > news.sql
+ ```
+
+ 将数据库news中的author表和article表备份到author.article.sql文件,author.article.sql是一个文本文件,文件名任取。
+
+ ```
+ mysqldump --opt news author article > author.article.sql
+ ```
+
+ 将数据库dbl和db2备份到news.sql文件,news.sql是一个文本文件,文件名任取。
+
+ ```
+ mysqldump --databases db1 db2 > news.sql
+ ```
+
+ 把host上的以用户user、密码pass的数据库dbname导入到文件file.dump中
+
+ ```
+ mysqldump -h host -u user -p pass --databases dbname > file.dump
+ ```
+
+ 将所有数据库备份到all-databases.sql文件,all-databases.sql是一个文本文件,文件名任取。
+
+ ```
+ mysqldump --all-databases > all-databases.sql
+ ```
+
+3. 导入数据
+
+ 导入数据库:
+
+ ```
+ mysql < all-databases.sql
+ ```
+
+ 在mysql命令行导入表:
+
+ ```
+ source news.sql;
+ ```
+
+## 编码操作
+
+- 查看数据库编码
+
+ ```
+ show create database db_name;
+ ```
+
+- 查看数据表编码,包括表使用的数据库引擎
+
+ ```
+ show create table tbl_name;
+ ```
+
+- 查看字段编码
+
+ ```
+ show full columns from tbl_name;
+ ```
+
+- 改变整个MySQL的编码, 启动MySQL的时候,mysqld_safe命令行加入
+
+ ```
+ --default-character-set=gbk;
+ ```
+
+- 改变某个库的编码,在MySQL提示符后输入命令
+
+ ```
+ alter database db_name default character set gbk;
+ ```
+
+- 把表默认的字符集和所有字符列(char,varchar,text)改为新的字符集
+
+ ```
+ alter table tbl_name convert to character set character_name [collate ...];
+ ```
+
+ 示例如下:
+
+ ```
+ alter table logtest convert to character set utf8 collate utf8_general_ci;
+
+ alter table table_name convert to character set utf8mb4 collate utf8mb4_bin; #使得数据库支持emoji
+ ```
+
+- 修改表的默认字符集
+
+ ```
+ alter table tbl_name default character set character_name [collate...];
+ ```
+
+- 修改字段的字符集
+
+ ```
+ alter table tbl_name change c_name c_name character set character_name [collate ...];
+ ```
+
+## 数据库元信息查询
+
+information_schema数据库中保存了各个数据库以及表的元信息,主要包括:
+
+- schemata表:提供了当前MySQL实例中所有数据库的信息。是`show databases`的结果来源。
+- tables表:提供了关于数据库中的表的信息,包括视图。详细表述了某个表属于哪个schema、表的类型、表使用的引擎以及创建时间等信息。是`show tables from [schemaName]`和`show table status from [schemaName] like '[tableName]'`的结果来源。
+- columns表:提供了表中的列信息。详细表述了某张表的所有列以及每个列的信息。是`show columns from [tableName]`的结果来源。
+- statistics表:提供了关于表索引的信息。是`show index from [tableName]`的结果来源。
+- user_privileges表:给出了关于用户权限的信息。该信息源自mysql.user授权表。
+- schema_privileges表:给出了关于方案(数据库)权限的信息。该信息来自mysql.db授权表。
+- table_privileges表:给出了关于表权限的信息。该信息源自mysql.tables_priv授权表。
+- column_privileges表:给出了关于列权限的信息。该信息源自mysql.columns_priv授权表。
+- table_constraints表:描述了存在约束的表,以及表的约束类型。
+
+例如,可通过tables查询某个数据表的创建时间:
+
+```
+select create_time from tables where table_schema='数据库名' and table_name='表名';
+```
+## 数据库性能信息查询
+
+performance_schema数据库用于收集数据库服务器性能参数,其中所有表的存储引擎为performance_schema。此功能MySQL5.5是默认关闭的,从5.6版本后变为默认开启。此功能开关如下:
+
+```
+[mysqld]
+performance_schema=ON/OFF
+```
+
+其中的常用数据表如下:
+
+- setup_actors:配置用户纬度的监控,默认监控所有用户。
+- setup_consumers:配置events的消费者类型,即收集的events写入到哪些统计表中。
+- setup_objects:配置监控对象,默认对mysql,performance_schema和information_schema中的表都不监控,而其它DB的所有表都监控。
+- setup_timers:配置每种类型指令的统计时间单位。MICROSECOND表示统计单位是微妙,CYCLE表示统计单位是时钟周期,时间度量与CPU的主频有关,NANOSECOND表示统计单位是纳秒。但无论采用哪种度量单位,最终统计表中统计的时间都会转换到皮秒。(1秒=1000000000000皮秒)
+- file_instances:文件实例, 记录了系统中打开了文件的对象,包括ibdata文件,redo文件,binlog文件,用户的表文件等。
+- rwlock_instances: 读写锁同步对象实例,记录了系统中使用读写锁对象的所有记录,其中name为 wait/synch/rwlock/*。
+- socket_instances:活跃会话对象实例,记录了thread_id,socket_id,ip和port,其它表可以通过thread_id与socket_instance进行关联,获取IP-PORT信息,能够与应用对接起来。
+- - socket_summary_by_instance、socket_summary_by_event_name:socket聚合统计表。
+- events_waits_current:记录了当前线程等待的事件。
+- events_waits_history:记录了每个线程最近等待的10个事件。
+- events_waits_history_long:记录了最近所有线程产生的10000个事件。
+- events_stages_current:记录了当前线程所处的执行阶段。同events_waits_current一样,events_stages_history、events_stages_history_long是历史记录。
+- events_statements_current:通过 thread_id+event_id可以唯一确定一条记录。Statments表只记录最顶层的请求,SQL语句或是COMMAND,每条语句一行。event_name形式为statement/sql/*,或statement/com/*。同events_waits_current一样,events_statements_history、events_statements_history_long是历史记录。
+- events_waits_summary_by_thread_by_event_name:按每个线程和事件来统计,thread_id+event_name唯一确定一条记录。包括总的等待时间、最小等待时间、平均等待时间以及最大等待时间。
+- table_lock_waits_summary_by_table:聚合了表锁等待事件,包括internal lock 和 external lock。
+- table_io_waits_summary_by_table:根据wait/io/table/sql/handler,聚合每个表的I/O操作(逻辑IO纬度)。
+- users:记录用户连接数信息。
+- hosts:记录了主机连接数信息。
+- accounts:记录了用户主机连接数信息。
+- threads:监视服务端的当前运行的线程。
+
+几个常用应用示例如下:
+
+- 统计哪个SQL执行最多
+
+ ```
+ SELECT SCHEMA_NAME,DIGEST_TEXT,COUNT_STAR,SUM_ROWS_SENT,SUM_ROWS_EXAMINED,FIRST_SEEN,LAST_SEEN FROM events_statements_summary_by_digest ORDER BY COUNT_STAR desc LIMIT 1\G
+ ```
+
+- 哪个SQL平均响应时间最长
+
+ ```
+ SELECT SCHEMA_NAME,DIGEST_TEXT,COUNT_STAR,AVG_TIMER_WAIT,SUM_ROWS_SENT,SUM_ROWS_EXAMINED,FIRST_SEEN,LAST_SEEN FROM events_statements_summary_by_digest ORDER BY AVG_TIMER_WAIT desc LIMIT 1\G
+ ```
+
+- 哪个索引没有使用过
+
+ ```
+ SELECT OBJECT_SCHEMA, OBJECT_NAME, INDEX_NAME FROM table_io_waits_summary_by_index_usage WHERE INDEX_NAME IS NOT NULL AND COUNT_STAR = 0 AND OBJECT_SCHEMA <> 'mysql' ORDER BY OBJECT_SCHEMA,OBJECT_NAME;
+ ```
+
+需要注意的是,MySQL5.5开启此功能即使没有数据可收集,也会有性能损失,慎重开启。MySQL5.6后做了改善,直到收集信息才会激活。
+
+
diff --git a/book/appendix/online-debug.md b/book/appendix/online-debug.md
index 777c887..41e2603 100644
--- a/book/appendix/online-debug.md
+++ b/book/appendix/online-debug.md
@@ -4,7 +4,7 @@
## 应对思路
-第一时间上报给自己的直属领导或者相关负责人,由其把控后续流程,并在可能的情况下及时周知问题、影响范围、解决方案、预计恢复时间等。
+第一时间上报给自己的直属领导或者相关负责人,由其把控后续流程,并在可能的情况下及时周知所有可能受影响方:问题、影响范围、解决方案、预计恢复时间等。
1. 根据经验来分析。如果应急团队中有人对相应的问题有经验,并能确定能够通过某种手段恢复系统的正常运行,那么应该第一时间恢复(回滚等),同时务必要保留现场,以备后续对问题的定位和修复;如果没有人有经验,则需要使用比较粗暴的办法保证服务可用,如定时重启、限流、降级等。
2. 业务负责人、技术负责人、核心研发人员、架构师、运维工程师以及运营人员对问题的原因进行快速分析。分析的过程需要首先考虑系统近期的变化,包括以下几方面:
diff --git a/book/chapter1-servertech/README.md b/book/chapter1-servertech/README.md
index a365089..f1644f6 100644
--- a/book/chapter1-servertech/README.md
+++ b/book/chapter1-servertech/README.md
@@ -13,7 +13,7 @@
- 稳定性:也叫做鲁棒性、健壮性,即服务在异常和危险情况下保持稳定的能力。
- 容错性:在服务出现错误或者异常的时候,能够继续提供一定服务的能力,主要强调的是容许误差、故障的能力。显然,此指标会影响可用率,容错性越好,那么系统的可用率也就会越高。
- 扩展性:主要指的服务的动态扩展能力,即通过扩展(而非修改)现有系统的能力来满足需求的能力。
-- 维护性:指的是修正服务错误、修改服务功能的能力。
+- 维护性:指的是监控/修正服务错误、修改服务功能的能力,主要和运维监控方面的工作相关。
- 安全性:保障系统以及用户数据安全性的能力,包括保障系统不被非法入侵、用户数据不被泄漏等。
除此之外很多书籍和资料还会有可靠性、可用性等说法,其实本质上是以上指标的另一种说法而已。一般来说后端服务的设计目标主要包括高可用、低延迟、高吞吐、高并发、可容错、可扩展、可维护、稳定、安全。后端技术也基本是围绕着这几个目标来进行的。
diff --git a/book/chapter1-servertech/how-to-study.md b/book/chapter1-servertech/how-to-study.md
index e8478c3..edfc372 100644
--- a/book/chapter1-servertech/how-to-study.md
+++ b/book/chapter1-servertech/how-to-study.md
@@ -10,6 +10,12 @@
- 自我总结
- 学会规划
+开篇之前,这里先给出一个高效学习方法框架(摘录自[《技术人如何更好地把控发展趋势?》](https://mp.weixin.qq.com/s/Cedl9lIk2mAd9b_NUCnj_g)),如下图所示。
+
+
+
+本文所讲的内容与此方法论不谋而合。
+
## 1.3.1 扎实的计算机基础知识
- 数据结构和算法:程序是由数据和算法组成的,因此这两个东西是计算机软件的基础。诸如B树、哈希表、栈以及七大排序算法、查找算法这些,在很多软件的代码中都可以看得到。有时候,一个优秀的工程师和一个普通工程师的区别也就在于是否能够合理使用合适的数据结构和算法。
@@ -58,7 +64,7 @@
这一点主要说的是要学会用计算机思维来思索问题。所谓的计算机思维本质就是冯诺依曼体系所描述:程序存储,顺序执行。
-经常听到的程序员买苹果的段子就是按照for循环、if else等逻辑来判断苹果好坏、计数,并做防御性检查等,这就是计算机思维的体现。而之前一个很火的电影《天才枪手》中记忆选择题答案如果使用计算机思维,那么用两位bit可以表示四个答案,再将四位bit转换为一个十六进制数字,记忆答案就可以减少一般的存储量。
+经常听到的程序员买苹果的段子就是按照for循环、if else等逻辑来判断苹果好坏、计数,并做防御性检查等,这就是计算机思维的体现。而之前一个很火的电影《天才枪手》中记忆选择题答案如果使用计算机思维,那么用两位bit可以表示四个答案,再将四位bit转换为一个十六进制数字,记忆答案就可以减少一半的存储量。
如此,诸如二进制存储、防御编程、循环遍历、位运算、多进程/线程以及常用的数据结构和算法等都应该成为不自觉的意识。能够在遇到问题时,下意识地用这些东西来思考,将人类语言的需求转化为计算机语言。
@@ -122,9 +128,18 @@ method1 | | | | | ...

- 初/中级程序员:这一层级的程序员包括入门没有多久的新手以及相关技能不熟练的程序员。已经掌握了基本的程序编写技能,但无法保证代码质量,需要在高级以上程序员的指导才能达到工作目标。从职业发展来看,这一层级应该是一个过渡层级,长期处于此层的程序员处在非常不利的状态。
-- 高级程序员:渡过了第一层级之后,除了掌握了基本的编码技能,还熟练掌握了设计模式、常用类库等工作常用技能,并能够熟练的将需求实现成程序,保质保量的完成交付。此层级的程序员才真正成为工程师,是工作的中坚力量,除了能够很好地完成自己的工作外,也能够指导初/中级程序员完成工作。
-- 项目经理、架构师:高级程序员再往前发展,会有两个方向。走项目管理会成为项目经理,主要负责项目进度把控、协调沟通等,比较偏向于管理;走技术路线,会发展为资深程序员、架构师,能够把控一个系统的整体架构或者精通某一领域技术,做好技术选型和系统设计工作。
-- 部门经理/技术总监、技术专家:这一层级的部门经理和技术总监已经基本脱离了程序员的含义,都不再是单纯的技术工作,管理占了日常工作的大部分。相比起之前,更需要的是对团队整体的把控,包括做事、带人、看方向等。而深钻某一领域技术的程序员在这一层级会成为技术专家,为技术团队提供领域技术的指导咨询工作。
-- CTO: 这一层级是程序员最顶级的职位。其需要的能力包括资源整合规划能力、技术战略规划能力、领导艺术、企业文化和制度建设能力。其中资源整合包括技术资源整合、知识整合、自我行销、人际关系整合。
+- 高级程序员:渡过了第一层级之后,除了掌握了基本的编码技能,还熟练掌握了设计模式、常用类库等工作常用技能,并能够熟练的将需求实现成程序,保质保量的完成交付。此层级的程序员才真正成为工程师,是工作的中坚力量,除了能够很好地完成自己的工作外,也能够指导初/中级程序员完成工作。能力图谱如下:
+
+- 项目经理/技术经理、技术专家/架构师:高级程序员再往前发展,会有两个方向。走管理会成为项目经理或者技术经理,前者主要负责项目进度把控、协调沟通等,后者则除了项目管理之外兼具团队管理的职责。能力图谱如下:
+
+ 这里需要强调的是,不同于管理路线,如果走技术路线,会发展为资深程序员、技术专家、架构师,能够把控一个系统的整体架构或者精通某一领域技术,做好技术选型和系统设计工作或者为技术团队提供自己精通领域技术的指导咨询工作。后续也会在技术路线上不断加深或者转型为管理岗。
+- 部门经理/技术总监:这一层级的部门经理和技术总监已经基本脱离了程序员的含义,都不再是单纯的技术工作,管理占了日常工作的大部分。相比起之前,更需要的是对团队整体的把控,包括做事、带人、看方向等。能力图谱如下:
+
+- CTO: 这一层级是程序员最顶级的职位。其需要的能力包括资源整合规划能力、技术战略规划能力、领导艺术、企业文化和制度建设能力。其中资源整合包括技术资源整合、知识整合、自我行销、人际关系整合。如下:
+
+ 能力图谱如下:
+
+
+一步步走到金字塔顶部需要不断的学习和进步,包括正确的态度、正确的方法以及持续的努力。本文所述只是笔者自己的体会,也是自己一直在践行的东西。除此之外,肯定还有很多其他优秀的方法和思想能够促进这个过程。
+
-一步步走到金字塔顶部需要不断的学习和进步,包括正确的态度、正确的方法以及持续的努力。本文所述只是笔者自己的体会,也是自己一直在践行的东西。除此之外,肯定还有很多其他优秀的方法和思想能够促进这个过程。
\ No newline at end of file
diff --git a/book/chapter1-servertech/media/cto-model.png b/book/chapter1-servertech/media/cto-model.png
new file mode 100644
index 0000000..a91d676
Binary files /dev/null and b/book/chapter1-servertech/media/cto-model.png differ
diff --git a/book/chapter1-servertech/media/cto.jpg b/book/chapter1-servertech/media/cto.jpg
new file mode 100644
index 0000000..93f975c
Binary files /dev/null and b/book/chapter1-servertech/media/cto.jpg differ
diff --git a/book/chapter1-servertech/media/programmer-pyramid.png b/book/chapter1-servertech/media/programmer-pyramid.png
index 9e0e1f6..6df8c96 100644
Binary files a/book/chapter1-servertech/media/programmer-pyramid.png and b/book/chapter1-servertech/media/programmer-pyramid.png differ
diff --git a/book/chapter1-servertech/media/study.png b/book/chapter1-servertech/media/study.png
new file mode 100644
index 0000000..d79e69e
Binary files /dev/null and b/book/chapter1-servertech/media/study.png differ
diff --git a/book/chapter1-servertech/media/td.jpg b/book/chapter1-servertech/media/td.jpg
new file mode 100644
index 0000000..3708f36
Binary files /dev/null and b/book/chapter1-servertech/media/td.jpg differ
diff --git a/book/chapter1-servertech/media/te.jpg b/book/chapter1-servertech/media/te.jpg
new file mode 100644
index 0000000..6284ab0
Binary files /dev/null and b/book/chapter1-servertech/media/te.jpg differ
diff --git a/book/chapter1-servertech/media/tech-tree.png b/book/chapter1-servertech/media/tech-tree.png
index 74463f3..993bc6d 100644
Binary files a/book/chapter1-servertech/media/tech-tree.png and b/book/chapter1-servertech/media/tech-tree.png differ
diff --git a/book/chapter1-servertech/media/tm.jpg b/book/chapter1-servertech/media/tm.jpg
new file mode 100644
index 0000000..bac1210
Binary files /dev/null and b/book/chapter1-servertech/media/tm.jpg differ
diff --git a/book/chapter1-servertech/server-tech-tree.md b/book/chapter1-servertech/server-tech-tree.md
index 672e682..808f56f 100644
--- a/book/chapter1-servertech/server-tech-tree.md
+++ b/book/chapter1-servertech/server-tech-tree.md
@@ -68,7 +68,7 @@
- 算法: 经典的排序和查找算法在平时的开发工作中经常会用到,如:冒泡排序、插入排序、选择排序、归并排序、快速排序、希尔排序、堆排序以及二分查找等。此外,在函数/方法的算法实现中要注意递归和迭代各自的优缺点。而衡量算法性能无外乎空间复杂度和时间复杂度。
- 业务相关算法:除了上面的基本算法之外,业务中还会经常涉及到一些更为复杂的算法,如:压缩算法、LRU缓存算法、缓存一致性、编译原理中的状态机等。此外,目前越来越火的机器学习中有很多算法也是在很多业务场景中有很大用途的,如:用于文本分词的结巴分词和中科院ICTCLAS;用于关键词提取的TF-IDF和TextRank;用于计算文本相似度的主题模型、Word2Vec、余弦相似度以及欧几里得距离;用于文本分类的朴素贝叶斯;用于推荐的聚类、协同过滤、用户画像、隐语义模型等。
- 计算机网络: TCP/IP协议是网络最根本的协议,其七层/四层协议栈的设计都是非常精华的东西,连接的建立、断开以及连接的各种状态的转换都是排查、解决网络问题的根本依据。从TCP/IP往上,HTTP协议是现在绝大多数后端应用对外提供的协议,发展到现在已经将要步入HTTP2.0时代,带来了持久连接、连接复用等令人振奋的新特性。此外,基于HTTP的HTTPS协议由于其安全性在逐渐的成为后端服务对外开放的主流协议。业务层面,基于HTTP协议的RESTful规范正成为对外接口的主流规范,而OAuth2.0协议也在成为开放平台对外的主流协议。除了HTTP之外,SMTP是另一个基于TCP/IP的应用协议,主要用在发送邮件上。
-- 设计模式: 在软件开发中,前人的经验形成了很多经典设计模式供我们使用,能够使得软件的实现可服用、可扩展、可维护。经典的工厂模式、简单工厂模式、单例模式、观察者模式、代理模式、建筑者模式、门面模式、适配器模式、装饰器模式在日常的很多开发场景下都具有很重要的意义。
+- 设计模式: 在软件开发中,前人的经验形成了很多经典设计模式供我们使用,能够使得软件的实现可复用、可扩展、可维护。经典的工厂模式、简单工厂模式、单例模式、观察者模式、代理模式、建筑者模式、门面模式、适配器模式、装饰器模式在日常的很多开发场景下都具有很重要的意义。
## 1.2.7 数据
@@ -135,22 +135,7 @@
此外,从分布式系统分层模型来看,LVS+Nginx部分是接入层,Tomcat部分是逻辑层,Redis+MySQL则是数据存储层。
-## 1.2.11 架构技能
-
-架构设计是很多研发工程师眼里一个比较高大上的工作,很多人的职业理想也是成为架构师。那什么是架构,具有哪些能力才能成为架构师呢?
-
-引用一句从网上看到的对架构师的比较形象的描述(无法找到出处):架构师的工作,就是在黑暗与混乱里寻找形状,并制造某种色彩。架构的主要工作就是在众多技术、组件中能够识别每一种技术的优劣点、适用场景,从而做出理性的判断和取舍(程序设计的工作主要是逻辑和实现),将构建和维护系统需要的人力成本降到最低(《Clean Architecture》一书对于软件架构的目的描述)。因此,要成为一名合格的架构师,一般来说需要具有以下能力:
-
-- 基础技术能力:精通某种技术,能够从本质上理解并能够触类旁通。架构师需要具有良好的性能优化能力,能够在应用上线前预估到可能出现的性能问题并给出解决方案;架构师需要具有扎实的调试能力,不管是在代码开发阶段还是线上环境,在应用发生bug或者线上故障时,能够快速识别问题并解决。
-- 设计能力:能够识别问题的本质,区分来自需求方的需求是伪需求还是真正的需求,避免把解决方案当做问题;精通设计模式和各种技术架构,但又不滥用;具有解耦系统的能力,使得系统之间松耦合,原来只能串行的任务可以并行开展。
-- 技术选型能力:平等对待所有技术,只有合适与不合适,没有喜欢与不喜欢;了解主流技术的优缺点,能够辨别是否需要造轮子。
-- 技术前瞻能力:能够预料到需求可能产生怎样的变化,做好前瞻性设计;能清楚地知道系统的瓶颈在什么地方,不断地定位技术难度、研发进度、性能、内存等各方面的瓶颈,不断调整骨干力量解决瓶颈,在风险爆发之前就消除隐患。
-- 沟通能力:架构师需要和业务的各种角色打交道,从而能够在充分理解业务的基础上做好架构设计,因此如何与各种角色的同事做好沟通、理解各方的诉求是一个非常关键的能力。
-- 管理能力:此能力在笔者看来并非是架构师必须的,但确实是能够锦上添花的,包括项目管理和团队管理。对于前者,需要合理划分系统模块、知人善用、有效沟通、控制成本、合理把握进度;对于后者,则需要多倾听团队成员的意见、主动承担责任、多站在成员角度考虑问题、为成员的成长负责。
-
-此外,附录F给出了做架构设计的简明指南供参考。
-
-## 1.2.12 后端技术体系
+## 1.2.11 后端技术团队体系
对于一个以业务为主的后端技术团队,随着业务的发展会逐渐形成三个主要技术体系。如下:
diff --git a/book/chapter2-project/media/15020897352634.jpg b/book/chapter2-project/media/15020897352634.jpg
deleted file mode 100644
index 9a1c209..0000000
Binary files a/book/chapter2-project/media/15020897352634.jpg and /dev/null differ
diff --git a/book/chapter2-project/media/15020953343968.jpg b/book/chapter2-project/media/15020953343968.jpg
deleted file mode 100644
index 47ce848..0000000
Binary files a/book/chapter2-project/media/15020953343968.jpg and /dev/null differ
diff --git a/book/chapter2-project/media/add-commit.png b/book/chapter2-project/media/add-commit.png
deleted file mode 100644
index 60ebb77..0000000
Binary files a/book/chapter2-project/media/add-commit.png and /dev/null differ
diff --git a/book/chapter2-project/media/gitblit.png b/book/chapter2-project/media/gitblit.png
deleted file mode 100644
index 3bd3f51..0000000
Binary files a/book/chapter2-project/media/gitblit.png and /dev/null differ
diff --git a/book/chapter2-project/media/gitflow.png b/book/chapter2-project/media/gitflow.png
index d3ceef3..c00a64c 100644
Binary files a/book/chapter2-project/media/gitflow.png and b/book/chapter2-project/media/gitflow.png differ
diff --git a/book/chapter2-project/quality.md b/book/chapter2-project/quality.md
index a156630..92805ae 100644
--- a/book/chapter2-project/quality.md
+++ b/book/chapter2-project/quality.md
@@ -203,7 +203,7 @@ public class AppServiceImplTest {
AppServiceImpl service = new AppServiceImpl();
service.setAppDao(mock);
App app = service.getAppById(1);//调用测试方法
- assertEquals(expectedApp, App); //断言测试结果
+ assertEquals(expectedApp, app); //断言测试结果
Easymock.verify(mock); //验证Mock对象被调用
}
@@ -353,7 +353,7 @@ PowerMock的工作过程和EasyMock类似,不同之处在于需要在类层次
- 该方法能否用一个main方法就能直接运行。
- 该方法的参数能否不依赖外部环境而进行自由模拟。
-对于一些将访问代码糅杂在业务逻辑里无法单元测试的方法,可以通过将需要的数据从上下文中抽离出来达到不依赖外部环境的目的,如:
+对于一些将访问代码(简单的顺序调用,一般需要和上下文打交道)和业务逻辑糅杂在一起、多线程、异步等很难进行单元测试的方法,可以使用谦卑对象模式,通过将需要的数据从上下文中抽离出来达到不依赖外部环境的目的,如:
```
@Resource
@@ -368,12 +368,19 @@ public void methodA(){
可以重构为
```
-public void methodA(User u){
- ….//业务逻辑
+public void methodA(){
+ User u = userDao.getById(1);
+ methodA_1(u);
+}
+
+public void methodA_1(User u){
+
+ ….//业务逻辑
+
}
```
-这样,可以自由模拟User数据,来对此方法做单元测试。
+这样,可以自由模拟User数据,来对抽离出的方法methodA_1做单元测试。
此外,编写单元测试还需要注意:
@@ -430,12 +437,26 @@ public class MyClassTest {
1. 变量命名按照Java通用方式Camel命名法。常量尤其注意使用全大写_下划线连接单词的方式,如 USER_KEY。
1. 变量和类命名务必具有意义,能让人一眼看出表示的意思。如userList表示用户列表,不要使用list、set、button这种无意义的命名方式。
-1. 数据库的一个表对应一个领域类,以entity、domain或者meta做为包名都可以。
-1. 数据访问层命名形如xxxDao,这里应该封装所有与数据库层相关的东西,如表名、各个列等。
-1. service类是封装业务逻辑的类,其中的方法要和此业务逻辑相关。比如UserService就是和User相关的业务方法。
-1. 当一个东西具有缓存和实际值两种的时候,务必保证存储和获取的接口只有一个。比如获取用户的level,应该只在UserLevelService中暴露一个接口提供
-1. 当一个方法的主逻辑代码超过30行,务必考虑封装方法或者类。
+1. 当一个方法的主逻辑代码超过30行(经验值),务必考虑封装方法或者类。
1. 类内方法定义的顺序依照开发者的关心程度和承载的信息价值依次是:公有方法或保护方法、私有方法、getter/setter方法。
+1. 数据领域类POJO的字段
+ - "不会为空"时使用基本数据类型,需要注意字段默认值不能具有特殊意义。
+ - "可为空"时使用包装类型,需要做好空值判断。
+1. 方法尤其是对外提供的接口(RPC、SDK)如果返回值可能为空,务必使用Optional包装返回值,否则认为返回值不会为空。
+1. “异常”编码规范
+ - 优先考虑使用错误码返回值而不是异常表示错误:用错误码返回值处理可能会发生的事情,用异常捕捉处理不期望发生的事情
+ - 在抛出异常时,需要尽可能精确地描述问题和相关信息。如:尽可能的使用最具体的异常来声明方法,这样才能使得代码更容易理解。
+ - 当在方法上声明抛出异常时,需要进行文档说明。在Javadoc中加入throws声明,描述抛出异常的场景。
+ - 不要捕获Throwable。
+ - 不要忽略异常,至少要记录异常的信息。
+ - 不要记录并抛出异常。仅仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。
+ - 包装异常时不要抛弃原始的异常。一定要把原始的异常设置为cause(Exception有构造方法可以传入cause)。
+1. MVC规范
+ - 数据库的一个表对应一个领域类,以entity、domain或者meta做为包名都可以。
+ - 数据访问层命名形如xxxDao,这里应该封装所有与数据库层相关的东西,如表名、各个列等。
+ - controller或者servlet中不要掺杂复杂业务逻辑代码(放到service中)
+ - service类是封装业务逻辑的类,其中的方法要和此业务逻辑相关。比如UserService就是和User相关的业务方法。
+ - 当一个东西具有缓存和实际值两种的时候,务必保证存储和获取的接口只有一个。比如获取用户的level,应该只在UserLevelService中暴露一个接口提供
对于这些规范,可以使用CheckStyle、PMD、FindBugs等工具进行保证,使用相关的Maven插件即可。此外,阿里巴巴也推出了其Java编码规范对应的Intellij插件。
@@ -473,7 +494,6 @@ public class MyClassTest {
1. 代码的可复用:代码的可复用是优秀工程师的一个本能。但切记不能为了复用而复用。
1. 程序需要状态,对象不需要状态,如果对象有了状态,就会引发多线程问题。程序的状态应该统一由数据库、缓存、任务队列这些外部容器来容纳,处理时以局部变量的形式在线程内部流转直至被回收。
1. 在做商业计算时(金融、电商交易),务必要注意选择的数据类型的存储数值范围,防止用户传入的数值溢出造成归零或者负值,从而造成交易损失。比如直播平台用户送礼物的场景,设计的接口的参数为礼物ID和礼物数目。那么如果使用int类型计算总的花费,当礼物数目非常大的时候就会造成花费超出int的存储范围而溢出为<=0的值,使得用户可以无限送礼物。
-2. 优先考虑使用返回值而不是异常表示错误。一般用错误码返回值处理可能会发生的事情,用异常捕捉处理不期望发生的事情。
1. 编码时,在每次调用一个方法时多考虑以下问题:
- 会不会出现空指针?
diff --git a/book/chapter2-project/vcs.md b/book/chapter2-project/vcs.md
index de0208e..b1a3cdb 100644
--- a/book/chapter2-project/vcs.md
+++ b/book/chapter2-project/vcs.md
@@ -27,7 +27,7 @@ SVN版本:1.10.0
### 流程与常用命令
-
+
一个通常较简单的SVN工作流程如上图所示。
@@ -211,39 +211,48 @@ ssh-keygen -t rsa -C [userName]
按照官方文档搭建一个Git服务器是比较繁琐的。面对这些复杂的操作步骤以及Git本身上手的门槛,很多人望而却步,造成了之前Git并没有那么普及的状况。而一切从Github横空出世开始改变了,这个看似简单的网站给很多人带来了简单极致方便的代码托管服务,同时也给全世界带来了一股开源的风潮。很多初创公司或者小公司直接选择使用Github建立自己的代码仓库。
-与此同时,许多Github的开源实现层出不穷,如今你只需要下载一个类似的实现部署到服务器上,就能拥有自己的“Github”。
+与此同时,许多GitHub的开源实现层出不穷,如今你只需要下载一个类似的实现部署到服务器上,就能拥有自己的“GitHub”。
目前比较知名、用的较多的Github实现,有以下几个:
-- [Gitlab](https://about.gitlab.com/): Ruby on rails实现,是目前最为出名也最为强大的github克隆实现,并且除了版本管理之外,集成了项目管理、持续集成等等很多功能。官网提供了很多操作系统下的一键安装包。遗憾的是,Gitlab现在已经商业化,一些强大的功能只有在其收费版本中才能体验到。
+- [GitLab](https://about.gitlab.com/): Ruby on rails实现,是目前最为出名也最为强大的GitHub克隆实现,并且除了版本管理之外,集成了项目管理、持续集成等等很多功能。官网提供了很多操作系统下的一键安装包。遗憾的是,GitLab现在已经商业化,一些强大的功能只有在其收费版本中才能体验到。

-- [Gitbucket](https://github.com/gitbucket/gitbucket):基于Scala编写,极易安装,扔一个war包到Tomcat就完成部署,完全可以和其他如Maven、Jenkins并存在一个JavaEE容器中。虽然功能没有Gitlab那么强大,但胜在简单。更重要的是对于Java工程师来说是友好的,有不满意的直接可以做二次开发。
+- [GitBucket](https://github.com/gitbucket/gitbucket):基于Scala编写,极易安装,扔一个war包到Tomcat就完成部署,完全可以和其他如Maven、Jenkins并存在一个JavaEE容器中。虽然功能没有GitLab那么强大,但胜在简单。更重要的是对于Java工程师来说是友好的,有不满意的直接可以做二次开发。

### 工作流
-由于Git是分布式版本控制系统,虽然带来了很多优势,但是也同时带来了协同工作的复杂性。因此需要一套基于Git的工作流来规范整个协同流程。目前,比较流行的有以下两种:
+由于Git是分布式版本控制系统,虽然带来了很多优势,但是也同时带来了协同工作的复杂性。因此需要一套基于Git的工作流来规范整个协同流程。目前,比较流行的有以下三种:
-- Github workflow
+- 三驾马车
+- GitHub workflow
- GitFlow
-#### Github workflow
+#### 三驾马车
-Github workflow是基于github一种常用的工作方式,比较简单。流程分为以下几步:
+三驾马车模式使用三个分支:开发分支、预发布分支/测试分支以及发布分支。经常用在客户端软件开发中。
+
+- 开发分支:所有开发人员提交代码的分支。
+- 预发布分支/测试分支:当开发分支提交的功能达到一定量时,将开发分支合并到预发布分支,预发布分支仅做缺陷修复等与发布相关的工作,不做新功能开发。所有的变动都要合并回开发分支。此分支代码发布测试版本,供测试。
+- 发布分支:当预发布分支的功能测试无误后,合并入发布分支,做为正式发布版本。此分支可以做缺陷修复,变动需要合并回预发布分支和开发分支。
+
+#### GitHub workflow
+
+GitHub workflow是基于GitHub一种常用的工作方式,比较简单。流程分为以下几步:
- 检出新的分支。
- 在开发分支上完成开发工作,commmit并push到远程库分支中。
-- 向主分支发起pull request
-- 在pull request中发起讨论和修改
+- 向主分支发起Pull Request
+- 在Pull Request中发起讨论和修改
- 将开发分支部署测试环境经测试无误后merge回主分支。
-这里最为核心的就是基于pull request的协作方式,在类似于Github这种应用中,还可以基于此来进行code review、任务沟通等工作。
+这里最为核心的就是基于Pull Request的协作方式,在类似于GitHub这种应用中,还可以基于此来进行code review、任务沟通等工作。
#### GitFlow
-相比起Github workflow,GitFlow根据Git原来的命令和语义,做了一层语义抽象,多了很多新的定义,从源代码管理角度对通常意义上的软件开发活动进行了约束。流程如下图所示:
+相比起GitHub workflow,GitFlow根据Git原来的命令和语义,做了一层语义抽象,多了很多新的定义,从源代码管理角度对通常意义上的软件开发活动进行了约束。流程如下图所示:
-
+
其中,GitFlow中定义了两大类分支:
diff --git a/book/chapter3-framework/README.md b/book/chapter3-framework/README.md
new file mode 100644
index 0000000..95cad3a
--- /dev/null
+++ b/book/chapter3-framework/README.md
@@ -0,0 +1,21 @@
+# 第三章 开发框架
+
+说到Java开发,开发框架是逃不开的的一个话题。也是得益于Java生态圈中各种框架的层出不穷,Java的生命力才如此旺盛。在无数次被声称“Java将亡”后依然活跃在开发领域,各种Java框架都功不可没。
+
+在Java刚开始出现的时候,写一个简单的Class,配上main方法就能完成一个可运行的程序。然而随着应用规模的越来越大以及参与人数的增多,以往一个类能够完成的功能,现在需要有许多类来完成。这时候如果没有一个开发规范或者说是约定,那么随着功能的增多,也就越来越难以维护。此外,由于开发人员水平的层次不齐,需要一些公共组件来保证一定程度的代码质量。开发框架就是一种基于约定、屏蔽了底层细节、规范了开发流程的技术。
+
+在一个可用的Java Web应用中,一般由以下几种框架构成:
+
+- IOC框架:依赖注入/控制反转,即将依赖从代码层面转移到了容器配置层面。在Java中是使用的最为普遍的框架。
+- ORM框架:对象关系映射,即将数据库的表映射到Java中的对象的一种数据库操作框架。
+- LOG框架:日志框架。即记录应用运行、异常日志的框架。
+- Web框架:一般指的是model-view-controller的分层Web开发框架,将业务代码做了逻辑分层,各司其职,能够做到灵活的配置和扩展。
+
+其层次结构如下图所示:
+
+
+
+如图,除了上述的四种框架,还有安全框架、AOP、缓存操作框架也是关键的开发框架。
+
+这里需要说明的一点是,虽然使用开发框架有很多好处,但任何事情都不会是绝对的优劣,都是一种权衡利弊后做出的取舍。就MVC框架来说,当提供的业务接口需要特别高的并发的时候,可以考虑不使用框架,直接使用Servlet处理请求并直接输出流数据(不走视图解析引擎和JSON等序列化)。毕竟请求经过框架的一层层调用性能肯定会受到影响。其他的诸如ORM框架也类似。
+
diff --git a/book/chapter3-framework/end.md b/book/chapter3-framework/end.md
new file mode 100644
index 0000000..bbe9e7d
--- /dev/null
+++ b/book/chapter3-framework/end.md
@@ -0,0 +1,11 @@
+# 总结
+
+## 学习资料
+
+- Spring: [跟开涛学Spring3](http://www.open-open.com/doc/view/5407635b943d410c9cfde409c90450b7)
+- Spring MVC: [跟开涛学SpringMvc](http://www.cnblogs.com/kaitao/archive/2012/07/16/2593441.html)
+- MyBatis: [MyBatis实战教程](http://www.yihaomen.com/article/java/302.htm) [MyBatis学习](http://limingnihao.iteye.com/blog/781671)
+
+对于这些框架或者是一些常用的软件,个人最推崇的还是阅读**官方文档**来学习。当然,看这些资料能让你入门地更加快速一些。
+
+
diff --git a/book/chapter3-framework/ioc.md b/book/chapter3-framework/ioc.md
new file mode 100644
index 0000000..dff9e2e
--- /dev/null
+++ b/book/chapter3-framework/ioc.md
@@ -0,0 +1,337 @@
+# 3.1 依赖注入
+
+IOC,控制翻转(Inversion of Control),又叫依赖注入(Dependency Inject)。即将代码里对象之间的依赖关系转移到容器中,这样就能够很灵活的通过面向接口的编程方式改变真正的实现类。
+
+如下是用代码来维护依赖关系的,如果此时后面有了另外的IUser的实现类,那么如果要使用这个新的实现类则需要修改代码重新set。
+
+```
+public interface IUser{
+ void say();
+}
+
+public class AdminUser implements IUser{
+ public void say(){
+ System.out.println("I'm admin");
+ }
+}
+
+public class IOCTest{
+ private IUser user;
+
+ public void setUser(IUser user){
+ this.user = user;
+ }
+
+ public IUser getUser(){
+ return this.user;
+ }
+
+ public void test(){
+ this.user.say();
+ }
+
+ public static void main(String[] args){
+ IOCTest test = new IOCTest();
+ test.setUser(new AdminUser());
+
+ test.test();
+ }
+}
+```
+而IOC的作用就是讲这些依赖关系的维护从代码里拿出来,通过容器来维护这些关系。一个典型的例子,伪代码如下:
+
+```
+IOCContainer container = new IOCContainer("..");//可以通过一个上下文配置文件,也可以通过扫描注解
+
+IOCTest iocTest = container.getInstance("iocTest");
+iocTes.test();
+```
+
+一个典型的上下文配置文件如下:
+
+```
+
+
+
+
+```
+
+这样当想要改变实现类时只需要修改配置文件即可。
+
+对于依赖注入,JSR330(Dependency Injection for Java)做了一些规范。该规范主要是面向依赖注入使用者,而对注入器实现、配置并未作详细要求。目前Spring、Guice已经开始兼容该规范。JSR-330规范并未按JSR惯例发布规范文档,只发布了API源码。其指定了获取对象的一种方法,该方法与构造器、工厂以及服务定位器(例如 JNDI)这些传统方法相比可以获得更好的可重用性、可测试性以及可维护性。此方法的处理过程就是依赖注入。
+
+目前市面上常见的IOC框架有以下几个:
+
+- Google Guice
+- PicoContainer
+- Dagger
+- SpringFramework
+
+其中,兼容JSR330标准的Guice易用性最好;Pico比较轻量,不过需要手工添加Bean类到容器,用起来有点烦;Dagger使用注解处理工具,其性能非常好,是一种很有前途的DI方案;SpringFramework历史非常悠久,有自己的一套依赖注入体系, 依赖于Spring强大的生态是目前用的最广泛的依赖注入框架,目前已经兼容JSR330规范。
+
+此外,基本所有的IOC框架都支持构造器注入、setter注入以及字段注入三种方式。
+
+## 3.1.1 JSR330
+
+如果要使用JSR330提供的注解等功能。可引入依赖:
+
+```
+
+ javax.inject
+ javax.inject
+ 1
+
+```
+
+主要提供了以下几个注解和类:
+
+1. @Inject
+
+ 注解@Inject标识了可注入的构造器、方法或字段。可以用于静态或实例成员。一个可注入的成员可以被任何访问修饰符(private、package-private、protected、public)修饰。注入顺序为构造器、字段、方法。超类的字段、方法将优先于子类的字段、方法被注入。对于同一个类的字段是不区分注入顺序的,同一个类的方法亦同。如:
+
+ ```
+ public class IOCTest {
+
+ private IUser user;
+
+ @Inject
+ public void setUser(IUser user) {
+ this.user= user;
+ }
+ }
+ ```
+
+1. @Qualifier
+
+ @Qualifier是一个元注解,用来构建自定义限定符,任何人都可以定义新的限定器注解。一个限定器注解如下:
+
+ - 是被@Qualifier、@Retention(RUNTIME)标注的,通常也被@Documented标注。
+ - 可以拥有属性。
+ - 可能是公共 API 的一部分,就像依赖类型一样,而不像类型实现那样不作为公共 API 的一部分。
+ - 如果标注了 @Target 可能会有一些用法限制。本规范只是指定了限定器注解可以被使用在字段和参数上,但一些注入器配置可能使用限定器注解在其他一些地方(例如方法或类)上。
+
+1. @Named
+
+ @Named就是使用上面讲的Qualifier的一个限定器注解。可以指定依赖的组件的名称。
+
+ ```
+ public class IOCTest {
+
+ private IUser user;
+
+ @Inject
+ public void setUser(@Named("adminUser") IUser user) {
+ this.user= user;
+ }
+ }
+ ```
+
+ 此外,@Named还可以用做标注一个组件。如
+
+ ```
+ @Named
+ public class AdminUser implements IUser{
+ public void say(){
+ System.out.println("I'm admin").
+ }
+ }
+ ```
+
+1. Provider
+
+ 接口Provider用于提供类型T的实列。Provider一般情况是由注入器实现的。对于任何可注入的T而言,都可以注入 Provider。与直接注入T相比,注入 Provider 使得:
+
+ - 可以返回多个实例。
+ - 实例的返回可以延迟化或可选
+ - 打破循环依赖。
+ - 可以在一个已知作用域的实例内查询一个更小作用域内的实例。
+
+ ```
+ public class IOCTest {
+
+ private IUser user;
+
+ @Inject
+ public void setUser(Provider user) {
+ this.user= user;
+ }
+ }
+ ```
+
+1. @Scope
+
+ 注解 @Scope是一个元注解,用于标识作用域注解。一个作用域注解是被标识在包含一个可注入构造器的类上的,用于控制该类型的实例如何被注入器重用。缺省情况下,如果没有标识作用域注解,注入器将为每一次注入都创建(通过注入类型的构造器)新实例,并不重用已有实例。如果多个线程都能够访问一个作用域内的实例,该实例实现应该是线程安全的。作用域实现由注入器完成。
+
+1. @Singleton
+
+ @Singleton是基于Scope注解实现的一个作用域注解。表示注入器只实例化一次的类型。该注解不能被继承。如:
+
+ ```
+ @Singleton
+ public class AdminUser implements IUser{
+ public void say(){
+ System.out.println("I'm admin").
+ }
+ }
+ ```
+
+## 3.1.2 Guice
+
+Guice是Google开源的轻量级ioc框架,兼容JSR330规范。其用 Module 來定义所有元件的实际类别。依赖定义部分可以使用JSR330的注解。
+
+```
+public class IOCTest {
+
+ private IUser user;
+
+ @Inject
+ public void setUser(IUser user) {
+ this.user= user;
+ }
+}
+
+public class IOCTestModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(IUser.class).to(AdminUser.class);
+ bind(IOCTest.class).to(IOCTest.class);
+ }
+}
+
+public static void main(String[] args) {
+ Injector injector = Guice.createInjector(new IOCTestModule());
+ IOCTest test = injector.getInstance(IOCTest.class);
+ ...
+}
+```
+
+## 3.1.3 PicoContainer
+
+PicoContainer是一个“微核心”(micro-kernel)的容器,它利用了Inversion of Control模式和Template Method模式,提供面向组件的开发、运行环境。PicoContainer是“极小”的容器,只提供了最基本的特性。其最重要的特性是实例化任意对象。这些通过它的API完成,这些API类似于HashMap。向PicoContainer指定java.lang.Class对象,之后能够获得对象实例。如:
+
+```
+MutablePicoContainer pico = new DefaultPicoContainer();
+
+pico.addComponent(AdminUser.class); //通过class注册
+pico.addComponent(new AdminUser()) //通过type注册
+pico.addComponent(IOCTest.class);
+
+IUser user = (IUser) pico.getComponent(AdminUser.class);
+IOCTest test = (IOCTest)pico.getComponent(IOCTest.class);
+```
+
+不过目前PicoContainer的发展几乎已经停滞。笔者仅仅在Intellij的插件开发中见过对它的使用。
+
+## 3.1.4 Dagger
+
+Dagger是Google开源的一个框架(早先的版本是由Square创建的,现版本由Google维护),支持Android和Java,现在已经更新到2.0。它使用生成代码实现完整依赖注入的框架(在编译期),极大减少了使用者的编码负担,且其相对于其他大部分IOC框架来说没有使用反射,性能有一定的提升。相对于都出自Google的Guice来说,其更加轻量级,没有Guice中一些相对高级的功能功,如AOP等。
+
+Dagger的依赖注入有自己的一些注解配置,如下:
+
+```
+public class IOCTest {
+
+ private IUser user;
+
+ @Inject
+ public void setUser(IUser user) {
+ this.user= user;
+ }
+}
+
+@Module
+public class TestModule {
+ @Provides IUser provideUser() {
+ return new AdminUser();
+ }
+}
+
+@Component(modules = TestModule.class)
+public interface TestComponent {
+ void inject(IOCTest test);
+}
+```
+
+使用如下:
+
+```
+IOCTest test = new IOCTest();
+TestComponent component = DaggerActivityComponent.builder().activityModule(new TestModule()).build();
+component.inject(test);
+test.test();
+...
+```
+
+## 3.1.5 Spring Framework
+
+Spring的IOC应该是Java开发中最为常用的功能之一。其XML配置的一个例子如下:
+
+```
+
+
+
+
+
+
+
+
+```
+
+使用代码:
+
+```
+ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xm");
+IOCTest iocTest = context.getBean(IOCTest.class);
+iocTes.test();
+```
+
+此外,从Spring2.0开始引入了对注解的支持,并且后来逐步兼容了JSR330规范。这里简单对比以下Spring自身的依赖注入注解与JSR330。
+
+Spring | Jsr330 | 备注
+----|-----|------|----
+@Autowired | @Inject | @Inject注解没有required属性
+@Component | @Named | JSR_330标准并没有提供复合的模型,只有一种方式来识别组件
+@Scope(“singleton”) | @Singleton | JSR-330默认的作用域类似Spring的prototype,而Spring默认是单例的。如果要使用非单例的作用域,开发者应该使用Spring的@Scope注解。java.inject也提供一个@Scope注解,然而,这个注解仅仅可以用来创建自定义的作用域时才能使用。
+@Qualifier | @Qualifier/@Named | javax.inject.Qualifier仅仅是一个元注解,用来构建自定义限定符的。而String的@Qualifier等限定符可以通过javax.inject.Named来实现
+
+除上述的注解外,Spring还有注入@Value、@Required等注解,这在JSR330中都没有对应的东西。
+
+这里需要补充的一点是,Spring的IOC目前早已支持JSR250(common annotations)中提供的注解:Resource、PostConstruct、PreDestroy。这里和Spring自带的注解做一下对比。
+
+Spring | JSR250 | 备注
+----|-----|------|----
+@Autowired | @Resource | @Resource是先根据Bean的名称去匹配Bean,获取不到的话再根据类型去匹配;而@Autowired则是根据类型匹配,通过名称则需要Spring的@Qualifier注解的配合。
+@PostContruct | init-method | Spring中的XML配置中的init-method可以有同样的作用,即在Bean构造完后做一些初始化动作。@PostContruct具有更高优先级,同时存在的话会先执行。
+@PreDesroy | destroy-method | Spring中的XML配置中的destroy-method可以有同样的作用,即在Bean销毁前做一些收尾工作。@PreDesroy注解具有更高优先级,同时存在的话会先执行。
+
+这里需要说明的是,Spring对JSR250的支持的实现是在org.springframework.context.annotation.CommonAnnotationBeanPostProcessor此类中。
+
+## 3.1.6 循环依赖
+
+IOC中一个常见的问题就是循环依赖,如下:
+
+```
+class TestA {
+ @Inject TestB b;
+}
+
+class TestB {
+ @Inject TestC c;
+}
+
+class TestC {
+ @Inject TestA a;
+}
+```
+
+以上几个IOC框架,对于循环的处理:
+
+- 通过替换为Provider,并且在构造器或者方法中调用Provider的get方法来打破循环依赖
+- 如果不依赖于Provider,对于构造器中的循环依赖是无法解决的,会抛出异常。
+- 对于方法或字段注入的情况,将其依赖的一边放置到单例作用域中(可以缓存),可以使得循环依赖能够被注入器解析。
+
diff --git a/book/chapter3-framework/log.md b/book/chapter3-framework/log.md
new file mode 100644
index 0000000..a2cf2d1
--- /dev/null
+++ b/book/chapter3-framework/log.md
@@ -0,0 +1,651 @@
+# 3.3 日志
+
+日志在应用开发中是一个非常关键的部分。有经验的工程师能够凭借以往的经验判断出哪里该打印日志、该以何种级别打印日志。这样就能够在线上发生问题的时候快速定位并解决问题,极大的减少应用的运维成本。
+
+使用控制台输出其实也算日志的一种,在容器中会打印到容器的日志文件中。但是,控制台输出过于简单,缺乏日志中级别控制、异步、缓冲等特性,因此在开发中要杜绝使用控制台输出作为日志(System.out.println)。而Java中已经有很多成熟的日志框架供大家使用:
+
+- JDK Logging
+- Apache Log4j
+- Apache Log4j2
+- Logback
+
+此外,还有两个用于实现日志统一的框架:Apache Commons-Logging、SLF4j。与上述框架的不同之处在于,其只是一个门面,并没有日志框架的具体实现,可以认为是日志接口框架。
+
+对于这些日志框架来说,一般会解决日志中的以下问题:
+
+- 日志的级别: 定义日志级别来区分不同级别日志的输出路径、形式等,帮助我们适应从开发调试到部署上线等不同阶段对日志输出粒度的不同需求。
+- 日志的输出目的地:包括控制台、文件、GUI组件,甚至是套接口服务器、UNIX Syslog守护进程等。
+- 日志的输出格式:日志的输出格式(JSON、XML)。
+- 日志的输出优化:缓存、异步等。
+
+这里需要说的是,目前有几个框架提供了占位符的日志输出方式,然而其最终是用indexOf去循环查找再对信息进行拼接的,会消耗CPU。建议使用正确估算大小的StringBuilder拼装输出信息,除非是实在无法确定日志是否输出才用占位符。
+
+## 3.3.1 JDK Logging
+
+JDK Logging就是JDK自带的日志操作类,在java.util.logging包下面,通常被简称为JUL。
+
+### 配置
+
+JDK Logging配置文件默认位于$JAVA_HOME/jre/lib/logging.properties中,可以使用系统属性java.util.logging.config.file指定相应的配置文件对默认的配置文件进行覆盖。
+
+```
+handlers= java.util.logging.FileHandler,java.util.logging.ConsoleHandler
+.handlers = java.util.logging.FileHandler,java.util.logging.ConsoleHandler #rootLogger使用的Handler
+.level= INFO #rootLogger的日志级别
+
+##以下是FileHandler的配置
+java.util.logging.FileHandler.pattern = %h/java%u.log
+java.util.logging.FileHandler.limit = 50000
+java.util.logging.FileHandler.count = 1
+java.util.logging.FileHandler.formatter =java.util.logging.XMLFormatter #配置相应的日志Formatter。
+
+##以下是ConsoleHandler的配置
+java.util.logging.ConsoleHandler.level = INFO
+java.util.logging.ConsoleHandler.formatter =java.util.logging.SimpleFormatter #配置相应的日志Formatter。
+
+#针对具体的某个logger的日志级别配置
+me.rowkey.pje.log.level = SEVERE
+
+#设置此logger不会继承成上一级logger的配置
+me.rokey.pje.log.logger.useParentHandlers = false
+```
+
+这里需要说明的是logger默认是继承的,如me.rowkey.pje.log的logger会继承me.rowkey.pje的logger配置,可以对logger配置handler和useParentHandlers(默认是为true)属性, 其中useParentHandler表示是否继承父logger的配置。
+
+JDK Logging的日志级别比较多,从高到低为:OFF(2^31-1)—>SEVERE(1000)—>WARNING(900)—>INFO(800)—>CONFIG(700)—>FINE(500)—>FINER(400)—>FINEST(300)—>ALL(-2^31)。
+
+### 使用
+
+JDK Logging的使用非常简单:
+
+```
+public class LoggerTest{
+
+ private static final Logger LOGGER = Logger.getLogger(xx.class.getName());
+
+ public static void main(String[] args){
+ LOGGER.info("logger info");
+ }
+}
+...
+```
+
+### 性能优化
+
+JDK Logging是一个比较简单的日志框架,并没有提供异步、缓冲等优化手段。也不建议大家使用此框架。
+
+## 3.3.2 Log4j
+
+Log4j应该是目前Java开发中用的最为广泛的日志框架。
+
+### 配置
+
+Log4j支持XML、Proerties配置,通常还是使用Properties:
+
+```
+root_log_dir=${catalina.base}/logs/app/
+
+# 设置rootLogger的日志级别以及appender
+log4j.rootLogger=INFO,default
+
+# 设置Spring Web的日志级别
+log4j.logger.org.springframework.web = ERROR
+
+# 设置default appender为控制台输出
+log4j.appender.default=org.apache.log4j.ConsoleAppender
+log4j.appender.default.layout=org.apache.log4j.PatternLayout
+log4j.appender.default.layout.ConversionPattern=[%-d{HH\:mm\:ss} %-3r %-5p %l] >> %m (%t)%n
+
+# 设置新的logger,在程序中使用Logger.get("myLogger")即可使用
+log4j.logger.myLogger=INFO,A2
+
+# 设置另一个appender为按照日期轮转的文件输出
+log4j.appender.A2=org.apache.log4j.DailyRollingFileAppender
+log4j.appender.A2.File=${root_log_dir}log.txt
+log4j.appender.A2.Append=true
+log4j.appender.A2.DatePattern= yyyyMMdd'.txt'
+log4j.appender.A2.layout=org.apache.log4j.PatternLayout
+log4j.appender.A2.layout.ConversionPattern=[%-d{HH\:mm\:ss} %-3r %-5p %l] >> %m (%t)%n
+
+log4j.logger.myLogger1 = INFO,A3
+
+# 设置另一个appender为RollingFileAppender,能够限制日志文件个数
+log4j.appender.A3 = org.apache.log4j.RollingFileAppender
+log4j.appender.A3.Append = true
+log4j.appender.A3.BufferedIO = false
+log4j.appender.dA3.File = /home/popo/tomcat-yixin-pa/logs/pa.log
+log4j.appender.A3.Encoding = UTF-8
+log4j.appender.A3.layout = org.apache.log4j.PatternLayout
+log4j.appender.A3.layout.ConversionPattern = [%-5p]%d{ISO8601}, [Class]%-c{1}, %m%n
+log4j.appender.A3.MaxBackupIndex = 3 #最大文件个数
+log4j.appender.A3.MaxFileSize = 1024MB
+```
+
+如果Log4j文件不直接在classpath下的话,可以使用PropertyConfigurator来进行配置:
+
+```
+PropertyConfigurator.configure("...");
+
+```
+
+Log4j的日志级别相对于JDK Logging来说,简化了一些:DEBUG < INFO < WARN < ERROR < FATAL。
+
+这里的logger默认是会继承父Logger的配置(rootLogger是所有logger的父logger),如上面myLogger的输出会同时在控制台和文件中出现。如果不想这样,那么只需要如下设置:
+
+```
+log4j.additivity.myLogger=false
+```
+
+### 使用
+
+程序中对于Log4j的使用也非常简单:
+
+```
+import org.apache.log4j.Logger;
+
+
+private static final Logger LOGGER = Logger.getLogger(xx.class.getName());
+...
+LOGGER.info("logger info");
+...
+```
+
+这里需要注意的是,虽然Log4j可以根据配置文件中日志级别的不同做不同的输出,但由于字符串创建或者拼接也是耗资源的,因此,下面的用法是不合理的。
+
+```
+LOGGER.debug("...");
+```
+
+合理的做法应该是首先判断当前的日志级别是什么,再去做相应的输出,如:
+
+```
+if(LOGGER.isDebugEnabled()){
+ LOGGER.debug("...");
+}
+```
+当然,如果是必须输出的日志可以不做此判断,比如catch异常打印错误日志的地方。
+
+### 性能优化
+
+Log4j为了应对某一时间里大量的日志信息进入Appender的问题提供了缓冲来进一步优化性能:
+
+```
+log4j.appender.A3.BufferedIO=true
+#Buffer单位为字节,默认是8K,IO BLOCK大小默认也是8K
+log4j.appender.A3.BufferSize=8192
+```
+
+以上表示当日志内容达到8k时,才会将日志输出到日志输出目的地。
+
+除了缓冲以外,Log4j还提供了AsyncAppender来做异步日志。但是AsyncAppender只能够通过xml配置使用:
+
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## 3.3.3 Log4j2
+
+2015年8月,官方正式宣布Log4j 1.x系列生命终结,推荐大家升级到Log4j2,并号称在修正了Logback固有的架构问题的同时,改进了许多Logback所具有的功能。Log4j2与Log4j1发生了很大的变化,并不兼容。并且Log4j2不仅仅提供了日志的实现,也提供了门面,目的是统一日志框架。其主要包含两部分:
+
+- log4j-api: 作为日志接口层,用于统一底层日志系统
+- log4j-core : 作为上述日志接口的实现,是一个实际的日志框架
+
+### 配置
+
+Log4j2的配置方式只支持XML、JSON以及YAML,不再支持Properties文件,其配置文件的加载顺序如下:
+
+- log4j2-test.json/log4j2-test.jsn
+- log4j2-test.xml
+- log4j2.json/log4j2.jsn文件
+- log4j2.xml
+
+如果想要自定义配置文件位置,需要设置系统属性log4j.configurationFile。
+
+```
+System.setProperty("log4j.configurationFile", "...");
+或者
+-Dlog4j.configurationFile="xx"
+```
+
+配置文件示例:
+
+```
+
+
+
+
+
+
+
+
+
+ %d %p %C{1.} [%t] %m%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+上面的monitorInterval使得配置变动能够被实时监测并更新,且能够在配置发生改变时不会丢失任何日志事件;additivity和Log4j一样也是为了让Looger不继承父Logger的配置;Configuration中的status用于设置Log4j2自身内部的信息输出,当设置成trace时,你会看到Log4j2内部各种详细输出。
+
+Log4j2在日志级别方面也有了一些改动:TRACE < DEBUG < INFO < WARN < ERROR < FATAL, 并且能够很简单的自定义自己的日志级别。
+
+```
+
+
+
+
+```
+
+上面的intLevel值是为了与默认提供的标准级别进行对照的。
+
+### 使用
+
+使用方式也很简单:
+
+```
+private static final Logger LOGGER = LogManager.getLogger(xx.class);
+
+LOGGER.debug("log4j debug message");
+```
+
+这里需要注意的是其中的Logger是log4j-api中定义的接口,而Log4j1中的Logger则是类。
+
+相比起之前我们需要先判断日志级别,再输出日志,Log4j2提供了占位符功能:
+
+```
+LOGGER.debug("error: {} ", e.getMessage());
+```
+
+### 性能优化
+
+在性能方面,Log4j2引入了基于LMAX的Disruptor的无锁异步日志实现进一步提升异步日志的性能:
+
+```
+
+
+
+```
+
+需要注意的是,由于默认日志位置信息并没有被传给异步Logger的I/O线程,因此这里的includeLocation必须要设置为true。
+
+和Log4j一样,Log4j2也提供了缓冲配置来优化日志输出性能。
+
+```
+
+
+
+ %d %p %C{1.} [%t] %m%n
+
+
+
+```
+
+## 3.3.4 Logback
+
+Logback是由Log4j创始人设计的又一个开源日志组件,相对Log4j而言,在各个方面都有了很大改进。
+
+Logback当前分成三个模块:
+
+- logback-core是其它两个模块的基础模块。
+- logback-classic是Log4j的一个改良版本。logback-classic完整实现SLF4J API使你可以很方便地更换成其它日志系统如Log4j或JDK Logging。
+- logback-access访问模块与Servlet容器集成提供通过HTTP来访问日志的功能。
+
+### 配置
+
+Logback的配置文件如下:
+
+```
+
+
+
+
+
+
+
+ ${root_log_dir}app.log
+ true
+
+ %date [%level] [%thread] %logger{80} [%file : %line] %msg%n
+
+
+ ${root_log_dir}app.log.%d{yyyy-MM-dd}.%i
+ 30 #只保留最近30天的日志文件
+ #每天的日志按照100MB分割
+ 100MB
+
+ 20GB#日志总的大小上限,超过此值则异步删除旧的日志
+
+
+
+
+ ${root_log_dir}mylog.log
+ true
+
+ %date [%level] [%thread] %logger{80} [%file : %line] %msg%n
+
+ #下面的日志rolling策略和ROLLING_FILE_APPENDER的等价,保留最近30天的日志,每天的日志按照100MB分隔,日志总的大小上限为20GB
+
+ mylog.log-%d{yyyy-MM-dd}.%i
+ 100MB
+ 30
+ 20GB
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Logback的配置文件读取顺序(默认都是读取classpath下的):logback.groovy -> logback-test.xml -> logback.xml。如果想要自定义配置文件路径,那么只有通过修改logback.configurationFile的系统属性。
+
+```
+System.setProperty("logback.configurationFile", "...");
+或者
+-Dlogback.configurationFile="xx"
+```
+
+Logback的日志级别:TRACE < DEBUG < INFO < WARN < ERROR。如果logger没有被分配级别,那么它将从有被分配级别的最近的祖先那里继承级别。root logger 默认级别是 DEBUG。
+
+Logback中的logger同样也是有继承机制的。配置文件中的additivit也是为了不去继承rootLogger的配置,从而避免输出多份日志。
+
+为了方便Log4j到Logback的迁移,官网提供了log4j.properties到logback.xml的转换工具:。
+
+### 使用
+
+Logback由于是天然与SLF4J集成的,因此它的使用也就是SLF4J的使用。
+
+```
+import org.slf4j.LoggerFactory;
+
+private static final Logger LOGGER=LoggerFactory.getLogger(xx.class);
+
+LOGGER.info(" this is a test in {}", xx.class.getName())
+```
+
+SLF4J同样支持占位符。
+
+此外,如果想要打印json格式的日志(例如,对接日志到Logstash中),那么可以使用logstash-logback-encoder做为RollingFileAppender的encoder。
+
+```
+
+...
+
+```
+
+### 性能优化
+
+Logback提供了AsyncAppender进行异步日志输出,此异步appender实现上利用了队列做缓冲,使得日志输出性能得到提高。
+
+```
+
+ ${root_log_dir}app.log
+ true
+
+ %date [%level] [%thread] %logger{80} [%file : %line] %msg%n
+
+
+ ${root_log_dir}app.log.%d
+
+
+
+ 0
+
+ 512
+
+
+
+
+```
+
+这里需要特别注意以下两个参数的配置:
+
+- queueSize:队列的长度,该值会影响性能,需要合理配置。
+- discardingThreshold:日志丢弃的阈值,即达到队列长度的多少会丢弃TRACT、DEBUG、INFO级别的日志,默认是80%,设置为0表示不丢弃日志。
+
+此外,由于是异步输出,为了保证日志一定会被输出以及后台线程能够被及时关闭,在应用退出时需要显示关闭logback。有两种方式:
+
+- 在程序退出的地方(ServletContextListener的contextDestroyed方法、Spring Bean的destroy方法)显式调用下面的代码。
+
+ ```
+ LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
+ loggerContext.stop();
+ ```
+
+- 在logback配置文件里,做如下配置。
+
+ ```
+
+
+
+ ....
+
+ ```
+
+## 3.3.5 日志门面
+
+前面的四个框架是实际的日志框架。对于开发者而言,每种日志都有不同的写法。如果我们以实际的日志框架来进行编写,代码就限制死了,之后就很难再更换日志系统,很难做到无缝切换。
+
+Java开发中经常提到面向接口编程,所以我们应该是按照一套统一的API来进行日志编程,实际的日志框架来实现这套API,这样的话,即使更换日志框架,也可以做到无缝切换。
+
+这就是Commons-Logging与SLF4J这种日志门面框架的初衷。
+
+### Apache Commons-Logging
+
+Apache Commons-Logging经常被简称为JCL,是Apache开源的日志门面框架。Spring中使用的日志框架就是JCL,使用起来非常简单。
+
+```
+import org.apache.commons.logging.LogFactory;
+
+private static final Log LOGGER = LogFactory.getLog(xx.class);
+
+LOGGER.info("...");
+```
+
+使用JCL需要先引入JCL的依赖:
+
+```
+
+ commons-logging
+ commons-logging
+ xx
+
+```
+
+再来看一下如何让JCL使用其他日志实现框架:
+
+1. 这里当没有其他日志jar包存在的时候,JCL有自己的默认日志实现,默认的实现是对JUL的包装,即当没有其他任何日志包时,通过JCL调用的就是JUL做日志操作。
+2. 使用Log4j作为日志实现框架,那么只需要引入Log4j的jar包即可。
+3. 使用Log4j2作为日志实现,那么除了Log4j2的jar包,还需要引入Log4j2与Commons-Logging的集成包(使用SPI机制提供了自己的LogFactory实现):
+
+ ```
+
+ org.apache.logging.log4j
+ log4j-jcl
+ xx
+
+ ```
+
+3. 使用Logback作为日志实现,那么由于Logback的调用是通过SLF4J的,因此需要引入jcl-over-slf4j包(直接覆盖了JCL的类),并同时引入SLF4J以及Logback的jar包。
+
+ ```
+
+ org.slf4j
+ jcl-over-slf4j
+ xx
+
+ ```
+
+### SLF4J
+
+SLF4J(Simple Logging Facade for Java)为Java提供的简单日志Facade。允许用户以自己的喜好,在工程中通过SLF4J接入不同的日志实现。与JCL不同的是,SLF4J只提供接口,没有任何实现(可以认为Logback是默认的实现)。
+
+SLF4J的使用前提是引入SLF4J的jar包:
+
+```
+
+
+ org.slf4j
+ slf4j-api
+ xx
+
+```
+
+再看一下SLF4J如何和其他日志实现框架集成。
+
+1. 使用JUL作为日志实现,需要引入slf4j-jdk14包。
+
+ ```
+
+ org.slf4j
+ slf4j-jdk14
+ xx
+
+ ```
+
+1. 使用Log4j作为日志实现,需要引入slf4j-log4j12和log4j两个jar包。
+
+ ```
+
+
+ org.slf4j
+ slf4j-log4j12
+ xx
+
+
+
+
+ log4j
+ log4j
+ xx
+
+ ```
+
+1. 使用Log4j2作为日志实现,需要引入log4j-slf4j-impl依赖。
+
+ ```
+
+
+ org.apache.logging.log4j
+ log4j-api
+ xx
+
+
+ org.apache.logging.log4j
+ log4j-core
+ xx/version>
+
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+ xx
+
+ ```
+
+1. 使用Logback作为日志实现,只需要引入logback包即可。
+
+## 3.3.6 日志集成
+
+上面说到了四种日志实现框架和两种日志门面框架。面对这么多的选择,即便是一个刚刚开始做的应用,也会由于依赖的第三方库使用的日志框架五花八门而造成日志配置和使用上的烦恼。得益于JCL和SLF4J,我们可以很容易的把日志都统一为一种实现,从而可以进行集中配置和使用。这里就以用Logback统一日志实现为例:
+
+1. 配置好Logback的依赖:
+
+ ```
+
+
+ org.slf4j
+ slf4j-api
+ xx
+
+
+
+ ch.qos.logback
+ logback-core
+ xx
+
+
+
+ ch.qos.logback
+ logback-classic
+ xx
+
+ ```
+
+1. 切换Log4j到SLF4J
+
+ ```
+
+ org.slf4j
+ log4j-over-slf4j
+ xx
+
+ ```
+
+1. 切换JUL到SLF4J
+
+ ```
+
+ org.slf4j
+ jul-to-slf4j
+ xx
+
+ ```
+
+1. 切换JCL到SLF4J
+
+ ```
+
+ org.slf4j
+ jcl-over-slf4j
+ xx
+
+ ```
+
+这里需要注意的是,做了以上配置后,务必要排除其他日志包的存在,如Log4j。此外,在日常开发中经常由于各个依赖的库间接引入了其他日志库,造成日志框架的循环转换。比如同时引入了log4j-over-slf4j和slf4j-log4j12的情况,当使用SLF4J调用日志操作时就会形成循环调用。
+
+笔者目前比较推崇的是使用SLF4J统一所有框架接口,然后都转换到Logback的底层实现。但这里需要说明的是Logback的作者是为了弥补Log4j的各种缺点而优化实现了SLF4J以及Logback,但不知为何作者又推出了Log4j2以期取代Log4j和Logback。所以,如果是一个新的项目,那么直接跳过Log4j和Logback选择Log4j2也是一个不错的选择, 官网也提供了Log4j到Log4j2的迁移说明。
+
+
diff --git a/book/chapter3-framework/media/framework.png b/book/chapter3-framework/media/framework.png
new file mode 100644
index 0000000..6068904
Binary files /dev/null and b/book/chapter3-framework/media/framework.png differ
diff --git a/book/chapter3-framework/media/spring-mvc.png b/book/chapter3-framework/media/spring-mvc.png
new file mode 100644
index 0000000..3416b0c
Binary files /dev/null and b/book/chapter3-framework/media/spring-mvc.png differ
diff --git a/book/chapter3-framework/media/web-framework.jpg b/book/chapter3-framework/media/web-framework.jpg
new file mode 100644
index 0000000..59ecd89
Binary files /dev/null and b/book/chapter3-framework/media/web-framework.jpg differ
diff --git a/book/chapter3-framework/mvc.md b/book/chapter3-framework/mvc.md
new file mode 100644
index 0000000..3427e2d
--- /dev/null
+++ b/book/chapter3-framework/mvc.md
@@ -0,0 +1,595 @@
+# 3.4 Web MVC
+
+Web开发最终肯定是需要一个Web开发框架的,而MVC(Model View Controller)是Web开发中最常用的框架。从Struts1到Struts2,再到现在的Spring MVC、Jersy等等,都是MVC模式的实现框架。它将Web开发分为了模型、视图以及控制器三层,做到了职责分离,使得应用的模块能够高内聚、低耦合。
+
+- 模型:代表业务数据和业务逻辑或者可以控制这些数据访问的模块
+- 视图:对模型的展现
+- 控制器:定义应用的各种行为
+
+目前,互联网领域主要以Spring MVC为主要的Web开发框架,因此本节主要讲述Spring MVC的使用。Spring版本为4.3.7.RELEASE。
+
+## 3.4.1 为什么是Spring MVC
+
+Spring MVC是Spring Web的一个重要模块。其结构简单,强大不失灵活,性能也很优秀。相比起其他的框架,具有但不限于以下特点:
+
+- 学习门槛低,易上手。
+- 由于Spring MVC框架封装的比较好,因此使用Spring MVC很容易写出优秀的程序。
+- Spring MVC继承了Spring框架的灵活性,非常易于扩展。
+- 框架的各个组件之间松耦合。
+- 支持多种视图展现。
+- 可以很方便的使用Spring生态下的组件。
+
+## 3.4.2 Spring MVC处理流程
+
+
+
+如图所示是Spring MVC的几个关键组件。对于一个用户的Web请求的处理流程一般如下:
+
+1. 用户发起请求到DispatchServlet(在web.xml中配置, 是Spring mvc的前置控制器)。
+2. 从HandlerMapping中匹配此次请求信息的handler,匹配的条件包括:请求路径、请求方法、header信息等。常用的几个HandlerMapping有:
+
+ - SimpleUrlHandlerMapping:简单的映射一个URL到一个Handler。
+ - RequestMappingHandlerMapping: 扫描RequestMapping注解,根据相关配置,绑定URL到一个Handler。
+
+3. 获取到对应的Handler(Controller,控制器)后, 调用相应的方法。这里牵扯到一个关键组件:HandlerAdapter,是Controller的适配器,Spring MVC最终是通过HandlerAdapter来调用实际的Controller方法的。常用的有以下几个:
+
+ - SimpleControllerHandlerAdapter: 处理实现了Controller接口的Controller。
+ - RequestMappingHandlerAdapter: 处理类型为HandlerMethod的handlder, 这里使用RequestMapping注解的Controller的方法就是一种HandlerMethod。
+
+4. Handler执行完毕,返回相应的ModelAndView。
+5. 使用配置好的ViewResolver来解析返回结果。常用的几个ViewResolver:
+
+ - UrlBasedViewResolver: 通过配置文件,根据URL把一个视图名交给到一个View来处理。
+ - InternalResourceViewResolver类: 根据配置好的资源路径,解析为jsp视图,支持jstl。
+ - FreeMarkerViewResolver:Freemarker的视图解析器。
+
+6. 生成视图返回给用户。常用的几个的视图类如下:
+
+ - MappingJackson2JsonView:使用mappingJackson输出JSON数据的视图,数据来源于ModelMap
+ - FreeMarkerView: 使用FreeMarker模板引擎的视图。
+ - JSTLView: 输出jsp页面,可以使用jsp标准标签库。
+
+此外,还有HandlerInterceptor和HandlerExceptionResolver两个重要组件。
+
+- HandlerInterceptor是请求路径上的拦截器,需要自己实现这个接口,以拦截请求,做一些对Handler的前置和后置处理工作。
+- HandlerExceptionResolver是异常处理器。可以实现自己的处理器在全局层面拦截Handler抛出的Exception再做进一步的处理。Spring MVC自带了几个处理器:
+
+ - SimpleMappingExceptionResolver: 可以将不同的异常映射到不同的jsp页面。
+ - ExceptionHandlerExceptionResolver: 解析使用了@ExceptionHandler注解的方法来处理异常。
+ - ResponseStatusExceptionResolver:处理@ResponseStatus注解的Exception。
+ - DefaultHandlerExceptionResolver:默认的处理器,包括不支持method、不支持mediaType等等。
+
+## 3.4.3 典型配置
+
+要使用Spring MVC,首先就是需要配置DispatchServlet负责分发所有请求。
+
+```
+
+
+ mvc-dispatcher
+ org.springframework.web.servlet.DispatcherServlet
+
+ contextConfigLocation
+
+ classpath:mvc-dispatcher-servlet.xml
+
+
+ 1
+
+
+ mvc-dispatcher
+ /
+
+```
+
+上面配置中,对应于servlet-mapping的url-pattern配置为/,是表示默认的URL映射,即当此次request匹配不到其他Servlet时,会默认进入此Servlet,包括静态资源请求。此外,对应于contextConfigLocation参数的mvc-dispatcher-servlet.xml则是对Spring MVC的一些配置。如果想要使用Spring的注解配置,则可以这么配置:
+
+```
+
+ mvc-dispatcher
+ org.springframework.web.servlet.DispatcherServlet
+
+ contextClass
+ org.springframework.web.context.support.AnnotationConfigWebApplicationContext
+
+
+ contextConfigLocation
+
+ me.rowkey.config.SpringWebConfig
+
+
+ 1
+
+```
+
+接着需要配置MVC相关的组件,如下:
+
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ classpath:xx.properties
+
+
+
+```
+
+1. mvc:default-servlet-handler是配置默认的Servlet作为静态资源的Handler。
+2. context:annotation-config是开启Spring的注解配置功能。
+3. mvc:annotation-driven是开启MVC的注解驱动,如创建了RequestMappingHandlerMapping和RequestMappingHandlerAdapter来处理注解handler。此外,上面配置了一些MessageConverter来处理各种使用了@ResposneBody标记返回数据的Handler。
+4. 配置了一个InternalResourceViewResolver处理返回数据。
+5. 配置exceptionResolver来处理异常。
+6. 配置CommonsMultipartResolver来处理文件上传。
+7. 配置PropertyPlaceholderConfigurer来读取相关资源文件,从而使得在配置文件中可以使用占位符填充,在代码中可以使用@Value注解来引用properties中的值。
+
+此外,对应于MVC的namespace,还有以下几个常用配置:
+
+- mvc:interceptors: 配置拦截器
+
+ ```
+
+
+
+
+
+
+ ```
+
+- mvc:argument-resolvers: 配置自己实现的参数解析器,可用于参数的名称转换,如:API传递的参数是underScore时,可以统一转换为lowCamel。
+
+这里需要注意的是,如果倾向于使用注解配置,那么对应于这些XML配置,Spring都提供了对应的注解,可以参考官方文档。一个简单的注解配置如下:
+
+```
+@EnableWebMvc
+@Configuration
+@ComponentScan("me.rowkey.pje.web")
+public class SpringWebConfig {
+
+}
+
+@ControllerAdvice
+public class ExceptionCatcher {
+ @ExceptionHandler
+ @ResponseBody
+ public String paramMissing(RuntimeException e) {
+ ...
+ }
+
+}
+```
+
+上面的@ControllerAdvice是Spring Web3.2后引入的注解。主要是为了统一对Controller添加@ExceptionHandler、@InitBinder、@ModelAttribute等注解,相比起之前写一个Base Controller做好相关配置然后每一个Controller去继承这种方式简化了很多。
+
+## 3.4.4 零XML配置
+
+自从Servlet3.0开始,可以完全脱离XML对Spring Web项目进行配置,如下所示:
+
+```
+public class MyWebApplicationInitializer implements
+ WebApplicationInitializer {
+
+ @Override
+ public void onStartup(ServletContext appContext)
+ throws ServletException {
+ AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
+ rootContext.register(SpringWebConfig.class);
+
+ appContext.addListener(new ContextLoaderListener(rootContext));
+
+ ServletRegistration.Dynamic dispatcher = appContext.addServlet(
+ "dispatcher", new DispatcherServlet(rootContext));
+ dispatcher.setLoadOnStartup(1);
+ dispatcher.addMapping("/");
+
+ }
+}
+```
+
+以上就不需要再配置任何XML文件即可使得Spring MVC项目得以运行。原理如下:
+
+1. Servlet3.0加入了一个特性: 容器启动时会使用Java的SPI(Service Provider Interface)机制在启动的时候去读取META-INF/services下的javax.servlet.ServletContainerInitializer文件,并会对其中列出的每一个ServletContainerInitializer进行实例化并调用onStartup方法。
+2. Spring Web在自己的classpath:META-INF/services下的javax.servlet.ServletContainerInitializer文件中加入了org.springframework.web.SpringServletContainerInitializer一行,于是此类被实例化并调用。
+3. SpringServletContainerInitializer使用HandlesTypes声明自己处理实现了WebApplicationInitializer的类,于是上面我们新建的MyWebApplicationInitializer会被实例化并调用onStartup方法。
+
+## 3.4.5 单元测试
+
+Spring MVC支持对Controller的单元测试。
+
+```
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(locations = {
+ "classpath:mvc-dispatcher-servlet.xml",
+})
+@WebAppConfiguration
+public class ControllerJUnitBase {
+
+ @Resource
+ private RequestMappingHandlerMapping handlerMapping;
+
+ @Resource
+ private RequestMappingHandlerAdapter handlerAdapter;
+
+ /**
+ * 执行request对象请求的action
+ *
+ * @param request
+ * @param response
+ * @return
+ * @throws Exception
+ */
+ public ModelAndView excuteAction(HttpServletRequest request, HttpServletResponse response) throws Exception
+
+ {
+ HandlerExecutionChain chain = handlerMapping.getHandler(request);
+
+ final ModelAndView model = handlerAdapter.handle(request, response, chain.getHandler());
+ return model;
+ }
+
+ @Test
+ public void test throws Exception(){
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.setRequestURI("/api/user/login");
+ request.addParameter("mobile", "180xxxx3360");
+ request.setMethod("POST");
+
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ final ModelAndView mav = this.excuteAction(request, response);
+ Assert.assertEquals("user_login", mav.getViewName());
+ }
+}
+
+```
+
+## 3.4.6 Web参数验证
+
+Web开发中对前端传入的参数验证是一个关键的环节,对于每一个Controller都单独做验证是一种方式,但是更好的方式则是用一套框架将这个验证流程统一起来。
+
+在Spring Web开发中,常用的验证方式主要有两种:
+
+- 支持Spring框架定义的Validator接口定义的校验。
+- 支持JSR303 Bean Validation定义的校验规范。
+
+### Spring Validator
+
+此种方式是Spring框架自带的。
+
+首先需要实现org.springframework.validation.Validator接口。
+
+```
+pulic class User{
+ private String name;
+
+ ...
+}
+
+public class UserValidator implements Validator {
+
+ @Override
+ public boolean supports(Class> clazz) {
+ return clazz.equals(User.class);
+ }
+
+ @Override
+ public void validate(Object target, Errors errors) {
+ ValidationUtils.rejectIfEmpty(errors, "name", "user.name.required", "用户名不能为空");
+
+ User user = (User)target;
+ int length = user.getName().length();
+ if(length > 10){
+ errors.rejectValue("name", "user.name.too_long", "用户名不能超过{20}个字符");
+ }
+ }
+}
+```
+
+其次,需要设置Validator并触发校验。在Controller里增加方法并以@InitBinder注解,并在对应的Controller method中触发。
+
+```
+@InitBinder
+protected void initBinder(WebDataBinder binder){
+ binder.setValidator(new UserValidator());
+}
+
+@RequestMapping (method = RequestMethod.POST)
+public String reg(@Validated User user, BindingResult result){
+ //校验没有通过
+ if(result.hasErrors()){
+ return "user";
+ }
+
+ if(user != null){
+ userService.saveUser(user);
+ }
+
+ return "user";
+}
+```
+
+如此,从页面提交的User对象可以通过我们实现的UserValidator类来校验,校验的结果信息存入BindingResult对象中。
+
+### JSR303 Bean Validation
+
+在Spring3.1中增加了对JSR303 Bean Validation规范的支持,不仅可以对Spring的MVC进行校验,也可以对Hibernate的存储对象进行校验,是一个通用的校验框架。
+
+这里必须要引入hibernate-validator,并开启MVC注解支持(), 它是JSR303规范的具体实现。
+
+此外,需要对要校验的meta类的属性做注解Constraints限制。JSR303定义的Constraint如下:
+
+- @Null:验证对象是否为空
+- @NotNull:验证对象是否为非空
+- @AssertTrue:验证 Boolean 对象是否为 true
+- @AssertFalse:验证 Boolean 对象是否为 false
+- @Min:验证 Number 和 String 对象是否大等于指定的值
+- @Max:验证 Number 和 String 对象是否小等于指定的值
+- @DecimalMin:验证 Number 和 String 对象是否大等于指定的值,需要注意小数的精度问题
+- @DecimalMax: 验证 Number 和 String 对象是否小等于指定的值,需要注意小数的精度问题
+- @Size: 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
+- @Digits: 验证 Number 和 String 的构成是否合法
+- @Past: 验证 Date 和 Calendar 对象是否在当前时间之前
+- @Future: 验证 Date 和 Calendar 对象是否在当前时间之后
+- @Pattern: 验证 String 对象是否符合正则表达式的规则
+
+此外,hibernate-validator也提供了一些注解支持,如:
+
+- @NotEmpty: 验证对象不为null也不为empty。
+- @NotBlank: 验证对象不为null也不为empty,连续的空格也认为是empty。
+- @Range:验证对象在指定的范围内。
+
+配置很简单,只要对被校验的meta注解Constraint即可。
+
+```
+pulic class User{
+ @NotNull
+ private String name;
+
+ ...
+}
+```
+
+然后,在Controller的对应方法中,给对应的参数加@Valid注解。
+
+```
+public String doRegister(@Valid User user, BindingResult result){
+
+ //校验没有通过
+ if(result.hasErrors()){
+ return "user";
+ }
+
+ if(user != null){
+ userService.saveUser(user);
+ }
+
+ return "user";
+}
+```
+
+这样就可以完成针对输入数据User对象的校验了,校验结果保存在BindingResult对象中。需要注意的一点是,BindingResult参数如果放在验证参数的后面,那么错误信息是会绑定到此BindingResult上的,否则会抛出MethodArgumentNotValidException异常。
+
+## 3.4.7 异步Servlet
+
+Servlet3.0引入了异步Servlet,即Connector的线程只负责将请求派发到业务逻辑线程池即可,业务逻辑处理完成后再通过Servlet的异步上下文句柄将结果返回并响应, 能够避免Web Server的连接池被长期占用而引起性能问题。此外,异步Servlet还经常用在长轮训消息通知:客户端发起HTTP请求,设置一个较长的超时事件,服务端接受请求后如果有消息则直接返回,如果没有则hold住请求,一直等到有消息到达再发送响应。而客户端如果请求超时没有获取到消息,则继续循环/递归进行再一次的请求(超时时间可以自适应调整)。
+
+Spring MVC提供了对异步Servlet的支持。
+
+1. 在web.xml启用异步支持。
+
+ ```
+
+ Set Character Encoding
+ org.springframework.web.filter.CharacterEncodingFilter
+ true
+ ...
+
+
+ Set Character Encoding
+ /*
+
+
+
+ mvc-dispatcher
+ org.springframework.web.servlet.DispatcherServlet
+ ...
+ true
+
+ ```
+
+ 这里需要注意除了Servlet之外,也要把所有经过的filter的async-supported都设置为true。
+
+1. 实现异步Controller。
+
+ 返回结果为java.util.concurrent.Callable即为异步Contoller。
+
+ ```
+ @Controller
+ @RequestMapping("/async")
+ public class CallableController {
+ @RequestMapping("/test")
+ @ResponseBody
+ public Callable callable() {
+
+ return new Callable() {
+ @Override
+ public String call() throws Exception {
+ Thread.sleep(1000);
+ return "Asyn Controller Result";
+ }
+ };
+ }
+ }
+ ```
+
+1. 配置异步Controller使用的线程池和超时参数。
+
+ ```
+
+
+
+
+
+ ```
+
+ 如此,处理业务的线程池即为myExecutor, 业务线程超时时间为2秒。
+
+此外,如果想要针对具体的业务分别使用不同的线程池,那么可以通过返回org.springframework.web.context.request.async.DeferredResult进行。
+
+```
+private Executor executor = Executors.newFixedThreadPool(200); //业务线程池
+
+@RequestMapping("/deferred")
+@ResponseBody
+public DeferredResult quotes() {
+ DeferredResult deferredResult = new DeferredResult(2000L); //设置超时时间为2秒
+ deferredResult.onCompletion(new Runnable() {
+ @Override
+ public void run() {
+ System.out.println("Deferred Result done !!!");
+ }
+ });
+
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ deferredResult.setResult("Deferred Result");
+ }
+ });
+
+ return deferredResult;
+}
+```
+
+如此,请求将会被挂起,直到在其他线程中将DeferredResult放入数据才会响应, 并且能够给DeferredResult设置回调方法,在数据返回后做相应的后续操作。
+
+还需要注意的是,当使用异步Servlet时,Tomcat等Servlet容器的线程池大小可以设置为1。
+
+```
+
+```
+
+## 3.4.8 使用提示
+
+1. 如果遇到一个项目中即提供了JSON API又提供了Web页面,那么单单配置一个ViewResolver是不行的。这时可以使用Spring MVC的视图协商器ContentNegotiatingViewResolver。配置如下:
+
+ ```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ data
+ status
+ desc
+
+
+
+
+
+
+ ```
+
+ 上面ContentNegotiationManager中配置的PathExtensionContentNegotiationStrategy表示根据path的扩展名来匹配返回的数据格式,如*.json就返回的JSON数据格式,此外,还配置了HeaderContentNegotiationStrategy根据header里的accept信息匹配返回数据格式,最后配置一个FixedContentNegotiationStrategy作为以上都无效时的默认返回数据格式。接着,ContentNegotiatingViewResolver又配置了一个解析为jsp页面的ViewResolver来处理返回格式为text/html的请求,如果此ViewResolver匹配不上,那么最后使用默认视图defaultViews来对ModelMap的值做JSON解析,上面的配置则是仅仅取ModelMap中对应于data、status以及desc的值作为JSON的视图字段。
+
+1. 如果使用视图解析器解析handler的数据并返回响应视图,那么当Controller中的参数具有HttpServletResponse时,假若Controller没有返回值,那么此次请求是不会走到视图解析器的。如下:
+
+ ```
+ @RequestMapping(value = "test", method = RequestMethod.GET, headers = "Accept=text/html")
+ public void testApi(ModelMap modelMap, HttpServletRequest request, HttpServletResponse response, String id, int status){
+ ......
+ }
+ ```
+
+ Spring中对于这种含有HttpServletResponse参数的Controller认为是视图是由handler自己生成的。如果仍然想要走视图解析器,则必须要返回一个值。
+
+1. API请求返回JSON/JSONP/XML数据的时候,可以通过@ResponseBody来注解Controller方法,并配置好对应的MessageConvertor。
+1. Spring的Controller、Service、DAO等都是单实例的,因此Controller、Service、DAO等各层组件,应该设计为有行为无状态、有方法无属性,即使有属性,也只是对下一层组件的持有。而与之对比,项目中的Entity、Domain、DTO等各种实体,有状态无行为,有属性无方法,即使有方法,也只是getter和setter等,围着状态打转。
+1. org/springframework/web/servlet路径下的DispatcherServlet.properties中配置了Spring MVC兜底使用的组件。即当项目的配置中缺少某一类组件的时候,Spring MVC会使用此文件中的相应组件来补充。
+
diff --git a/book/chapter3-framework/orm.md b/book/chapter3-framework/orm.md
new file mode 100644
index 0000000..f144586
--- /dev/null
+++ b/book/chapter3-framework/orm.md
@@ -0,0 +1,293 @@
+# 3.2 对象关系映射
+
+ORM(Object Relational Mapping),对象关系映射,是一种为了解决面向对象与关系型数据库不匹配而出现的技术,使开发者能够用面向对象的方式使用关系型数据库。
+
+目前最为常用的ORM框架主要是MyBatis(前身是ibatis)和Hibernate。两者对比如下:
+
+1. MyBatis非常简单易学,Hibernate相对较复杂,门槛较高。
+1. 相比起Hibernate,MyBatis灵活性更好。
+1. 系统数据处理量巨大,性能要求极为苛刻的情况下,MyBatis能够高度定制化SQL,因此会有更好的可控性和表现。
+1. MyBatis需要手写SQL语句,也可以生成一部分,Hibernate则基本上可以自动生成,偶尔会写一些HQL。同样的需求,MyBatis的工作量比Hibernate要大很多。类似的,如果涉及到数据库字段的修改,Hibernate修改的地方很少,而MyBatis要把那些SQL mapping的地方一一修改。
+1. 以数据库字段一一对应映射得到的PO和Hibernte这种对象化映射得到的PO是截然不同的,本质区别在于这种PO是扁平化的,不像Hibernate映射的PO是可以表达立体的对象继承,聚合等等关系的,这将会直接影响到你的整个软件系统的设计思路。
+
+Hibernate现在主要用在传统企业应用的开发,互联网领域中由于流量大、并发高的缘故因此主要以MyBatis为主。笔者也推荐使用MyBatis做为ORM框架。
+
+一般的ORM包括以下几个部分:
+
+- 一个规定Mapping Metadata的工具, 即数据库中的表、列与对象以及对象属性的映射。
+- 一个对持久类对象进行CRUD操作的API。
+- 一个语言或API用来规定与类和类属性相关的查询。
+- 一种技术可以让ORM的实现同事务对象一起进行缓存、延迟加载等操作。
+
+## 3.2.1 Mapping
+
+MyBatis支持XML配置:
+
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+其中,environment元素体中包含了事务管理和连接池的配置,能够根据不同的环境使用不同的数据库配置。这里的dataSource设置为POOLED时使用了MyBatis自己提供的数据连接池。mappers元素则是包含一组mapper映射器(这些 mapper 的 XML 文件包含了 SQL 代码和映射定义信息)。
+
+MyBatis也支持注解映射Mapper:
+
+```
+public interface TestUserMapper {
+
+ @Select("SELECT * FROM test_user WHERE id = #{id}")
+ TestUser selectUser(int id);
+
+}
+```
+
+这里需要注意的是,命名空间现在是必需的。并且MyBatis对所有的命名配置元素的解析如果是全限定名则直接用;而如果是一个简单的名称,全局唯一的话没有问题,如果有重复类则会报错。
+
+此外,MyBatis提供了mybatis-spring这个类库用于集成Spring和MyBatis。配置如下:
+
+```
+
+
+
+
+
+
+
+ ...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## 3.2.2 CRUD以及属性的查询
+
+MyBatis的核心是SqlSessionFactory,要使用它最基本的操作API,首先获取到SqlSessionFactory,然后再去获取相应的Mapper。
+
+```
+String resource = "classpath:mybatis-config.xml";
+InputStream inputStream = Resources.getResourceAsStream(resource);
+SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
+
+SqlSession session = sqlSessionFactory.openSession();
+TestUserMapper mapper = session.getMapper(TestUserMapper.class);
+TestUser testUser = mapper.selectUser(1);
+...
+```
+
+此外,在Spring中可以使用MyBatis提供的org.mybatis.spring.SqlSessionTemplate进行操作。
+
+```
+SqlSessionTemplate sqlTemplate = new SqlSessionTemplate(sqlSessionFactory);
+TestUser testUser = (TestUser)this.sqlSessionTemplate.selectOne("me.rowkey.pje.mybatis.TestMapper.selecUser", uid);
+```
+
+这里需要注意的是,SqlSession是非线程安全的,而SqlSessionTemplate则是线程安全的。
+
+MyBatis对应于CRUD有以下几个映射语句:
+
+- insert映射插入语句
+- update映射更新语句
+- delete映射删除语句
+- select映射查询语句
+
+依赖于上述的四种操作,可以完成各种crud以及对属性之类的查询。MyBatis会自动把数据库查询结果注入到返回对象中。
+
+这里需要注意的是上述的`select * from test_user where id = #{id}`,其中的#{id}是告诉MyBatis创建预处理语句属性并以它为背景设置安全的值, 对应于PreparedStatement中的?。此外,在MyBatis中还存在另一个符号$,如下:
+
+```
+
+```
+
+\$在这里的作用只是做字符串替换,不会修改或转义字符串。因此,能使用\#的地方就不要用$,除非是像order by这种不是参数的地方。
+
+此外,MyBatis对于insert、update、delete以及select都提供了很多选项,用来支持注入缓存、自动映射、列名和属性名的转换等优化功能。
+
+## 3.2.3 缓存
+
+MyBatis支持一级缓存和二级缓存:
+
+- Mybatis默认开启一级缓存,是SqlSession级别的,Session结束那么缓存即清空。另外还支持Statement级别,即缓存只对当前的Statement有效(没什么用)。
+- MyBatis的二级缓存是Mapper级别的,被多个SqlSession共享,需要手动开启。
+
+```
+
+
+
+
+
+
+
+```
+
+上述配置中的useCache设置为true即开启二级缓存,对应mapper中的所有select语句的结果集将会被缓存。也可以针对某个select设置useCache为false关闭二级缓存,设置flushCache为true使得数据直接flush到数据库中防止出现脏读数据。此外,二级缓存也可以配置成使用自定义的缓存实现。
+
+```
+
+```
+
+需要注意的是MyBatis中的缓存设计初衷是针对单点应用的,都是本地缓存,现在的分布式应用慎重开启,只单纯把MyBastis作为一个ORM框架,在Service层自己实现缓存机制是更好的选择。
+
+## 3.2.4 结果映射
+
+MyBatis默认会自动映射查询结果,根据SQL返回的列名并在Java类中查找相同名字的属性(忽略大小写)。还可以全局配置对数据库列的LowUnderscore(a_column)命名转换为LowCamel命名(aColumn)。
+
+```
+
+
+
+
+
+```
+
+除此之外,MyBatis还支持自定义映射。
+
+```
+public class UserDto{
+ private long uid;
+ private String userName;
+
+ //getter and setters
+ ....
+}
+
+
+
+
+
+
+
+
+
+```
+
+上面使用的resultMap即做了自定义的映射工作,uid会自动映射, userName会使用自定义映射。
+
+## 3.2.5 SQL语句构建器
+
+虽然Mybatis已经做了很多封装,可以大大简化SQL编写的工作,但是很多时候在代码拼接SQL是无法避免的。如果用字符串自己进行拼接,那么各种+号、括号、引号、格式化问题会带来很多的麻烦,一不小心就会出错。针对这种状况,MyBatis提供了SQL语句构建类org.apache.ibatis.jdbc.SQL, 可以大大简化动态SQL编写的问题。
+
+```
+new SQL() {{
+ SELECT("user.id, user.user_name");
+ SELECT("user.sign, user.gender");
+ FROM("user_account user");
+ INNER_JOIN("user_base_info uinfo on user.uid = uinfo.uid");
+ WHERE("user.user_name like ?");
+ OR();
+ WHERE("uinfo.nick_name like ?");
+ ORDER_BY("user.create_time");
+ }}.toString();
+```
+
+等同于
+
+```
+"SELECT user.id, user.user_name, "
+"user.sign, user.gender " +
+"FROM user_account user " +
+"INNER JOIN user_base_info uinfo on user.uid = uinfo.uid" +
+"WHERE (user.user_name like ?) " +
+"OR (uinfo.nick_name like ?) " +
+"ORDER BY user.create_time";
+```
+
+## 3.2.6 使用提示
+
+1. 当几个SQL语句都包含同样的部分SQL逻辑时。可以使用来进行复用,如:
+
+ ```
+
+ from test_meta where status = #{status}
+
+
+
+ ```
+
+2. 动态SQL中的空字符串判断
+
+ 在动态SQL中判断是否是空字符串时,MyBatis的内建机制不太好用。建议使用以下方式:
+
+ ```
+
+ ```
+
+3. 多ResultMap复用
+
+ 一个应用中由于功能的不同经常会有多个mapper.xml,如果一个文件需要另一个文件中的resultMap的定义,可以直接引用,而不需要再重新定义一遍。如:
+
+ ```
+
+
+ ...
+
+
+
+
+
+ ...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+以上便在9999端口开放了JMX协议并发布了一个MBean,其底层通讯是基于RMI的。这样通过serviceUrl:"service:jmx:rmi:///jndi/rmi://127.0.0.1:9099/kmconnector"以及mbeanName:"suishen.libs.admin.jmx.impl:name=AdminBean"即可调用服务。
+
+## 4.4.4 Spring Quartz
+
+Spring Quartz是Spring自己实现的一套定时调度框架,支持Cron表达式。需要注意的是相比起真正的Quartz框架,Spring Quartz是轻量级的,缺乏分布式等高级特性。
+
+典型的使用Spring Quartz的XML配置如下:
+
+```
+
+
+
+
+
+
+
+```
+
+其中task:scheduler配置了Quartz使用的scheduler的线程池大小为10。配置的任务则是每5分钟执行userDisableService这个Bean的enableService方法。
+
+当然,Spring Quartz也支持注解配置,开关如下:
+
+```
+
+```
+
+当然,也可以使用@注解EnableScheduling来开启Spring Quartz的注解支持。
+
+这样就可以在bean中使用@Scheduled注解来配置定时任务。
+
+```
+@Service
+public class UserDisableService{
+
+ @Scheduled(cron = "* */5 * * * ?")
+ public viod enableService(){
+ ...
+ }
+}
+```
+
+## 4.4.5 Spring CORS
+
+CORS(Cross-Origin Resource Sharing)是为了解决浏览器中跨域请求的问题。简单的Get请求可以使用JSONP解决,而对于其他稍微复杂的请求则需要后端应用支持CORS。Spring4.2之后提供了@CrossOrigin注解实现对CORS的支持。
+
+- 在Controller方法上配置
+
+ ```
+ @CrossOrigin(origins = {"http://localhost:8088"})
+ @RequestMapping(value = "/corsTest", method = RequestMethod.GET)
+ public String greetings() {
+ return "cors test";
+ }
+ ```
+
+- 在Controller上配置,那么此Controller中所有的method都支持CORS
+
+ ```
+ @CrossOrigin(origins = "http://localhost:8088", maxAge = 3600)
+ @Controller
+ @RequestMapping("/api/")
+ public class TestController {
+ @RequestMapping(value = "/corsTest", method = RequestMethod.GET)
+ public String greetings() {
+ return "cors test";
+ }
+ }
+ ```
+
+- Java Config全局配置
+
+ ```
+ @Configuration
+ @EnableWebMvc
+ public class SpringWebConfig extends WebMvcConfigurerAdapter {
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ //对所有URL配置
+ //registry.addMapping("/**");
+
+ //针对某些URL配置
+ registry.addMapping("/api/**").allowedOrigins("http://localhost:8888")
+ .allowedMethods("PUT", "DELETE")
+ .allowedHeaders("header1", "header2", "header3")
+ .exposedHeaders("header1", "header2")
+ .allowCredentials(false).maxAge(3600);
+ }
+ }
+ ```
+
+- XML全局配置
+
+ ```
+
+
+
+
+
+ ```
+
+- Spring Boot中的Filter全局配置
+
+ ```
+ @Bean
+ public FilterRegistrationBean corsFilter() {
+ UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
+ CorsConfiguration corsConfig = new CorsConfiguration();
+ config.setAllowCredentials(true);
+ config.addAllowedOrigin("http://localhost:8888");
+ config.addAllowedOrigin("http://localhost:8088");
+ config.addAllowedHeader("header1");
+ config.addAllowedMethod("GET");
+ configSource.registerCorsConfiguration("/**", corsConfig); // CORS 配置对所有接口都有效
+
+ FilterRegistrationBean filterBean = new FilterRegistrationBean(new CorsFilter(configSource));
+ filterBean.setOrder(0);
+
+ return filterBean;
+ }
+ ```
+
+这里需要注意的是,使用此种方式配置cors,实质上是Spring在handler中加入了一个CorsInterceptor,而这个拦截器其执行是在自定义的拦截器后面的。因此如果跨域请求被前面的拦截器拦住了,那么不会走到CorsInterceptor,会报跨域错误。
+
diff --git a/book/chapter4-spring/spring-data.md b/book/chapter4-spring/spring-data.md
new file mode 100644
index 0000000..3ea24e2
--- /dev/null
+++ b/book/chapter4-spring/spring-data.md
@@ -0,0 +1,219 @@
+# 4.2 数据操作
+
+Spring提供了对常用数据软件/服务操作的封装组件,包括关系型数据库、NoSQL数据库、Redis、Elasticsearch、 Cassandra等。本章主要讲述其中最为常用的JDBC、Redis以及MongoDB。
+
+## 4.2.1 Spring JDBC
+
+3.2节讲了ORM框架,其实Spring也提供了对ORM的支持,包括对Hibernate、JDO以及JPO的支持。相关组件的层次结构如下图所示:
+
+
+
+比较常用的是Spring ORM下面一层的Spring JDBC以及Spring TX。
+
+1. Spring JDBC提供了对JDBC操作的封装,也是Java开发中经常用到的数据库操作工具, 经常用在需要灵活组装SQL的场景下使用,其核心类是JdbcTemplate,提供了很多数据CRUD操作。
+
+ 注入一个数据库连接池即可构造JdbcTemplate。
+
+ ```
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ 注入JdbcTemplate到DAO中,可以进行数据库操作。
+
+ ```
+ @Repository
+ public class UserDao {
+ @Resource
+ private JdbcTemplate jdbcTemplate;
+
+ public User getById(long id) {
+ return jdbcTemplate.queryForObject("select * from test_user where id = ?", new Object[]{id}, User.class);
+ }
+
+ public List getByName(String name) {
+ return jdbcTemplate.queryForList("select * from test_user where name= ?", new Object[]{name}, User.class);
+ }
+ }
+ ```
+
+1. Spring TX提供了对事务的支持
+
+ 使用tx:annotation-driven开启Spring的注解事务,并配置transaction-manager。
+
+ ```
+
+
+
+
+
+ ```
+
+ 使用@Transactional注解一个方法使其开启事务。
+
+ ````
+ @Service
+ class UserServiceImpl implements IUserService {
+
+ //处理删除用户业务逻辑,使用@Transactional注解实现该方法的事务管理
+ @Transactional
+ public void delUser(long id) {
+ ...
+ }
+ }
+ ```
+
+ 需要注意的是,开启了事务的方法在数据库错误时应该抛出异常,否则是无法做到事务回滚的。
+
+## 4.2.2 Spring Data Redis
+
+基于jedis之上对Redis操作的封装。
+
+1. 配置jedis连接池
+
+ ```
+
+
+
+
+
+
+
+ ```
+1. 配置连接工厂
+
+ ```
+
+
+
+
+
+
+ ```
+1. 构造RedisTemplate即可使用它来做各种操作
+
+ ```
+
+
+
+
+ redisTemplate.opsForValue().set("test_key", "test_value"); //set操作
+ redisTemplate.opsForValue().getOperations().delete("test_key"); //del操作
+ redisTemplate.opsForHash().put("testKey", "testField", "testValue"); //hset操作
+ ```
+
+ 需要注意的一点:如果直接使用RedisTemplate,那么redis的key使用的是String的JDK序列化字节数组,并非是String.getBytes()得到的字节数组。可以通过RedisTmplate的defaultSerializer、keySerializer、valueSerializer、hashKeySerializer以及hashValueSerializer几个属性设置想要使用的序列化机制。其支持的几种序列化机制如下:
+
+ - StringRedisSerializer: 简单的字符串序列化,使用的是String.getBytes()方法。StringRedisTemplate就是使用它作为key、value、hashKey以及hashValue的序列化实现。
+ - GenericToStringSerializer: 可以将任何对象泛化为字符串并序列化,对于每一种对象类型都有不同的实现。
+ - JacksonJsonRedisSerializer: JSON的序列化方式,使用Jakson Mapper将object序列化为JSON字符串。
+ - Jackson2JsonRedisSerializer: 跟JacksonJsonRedisSerializer同样是JSON序列化方式,使用的是jackson databind。
+ - JdkSerializationRedisSerializer: 使用JDK自带的序列化机制,也是直接使用RedisTemplate时用的序列化机制。
+
+## 4.2.3 Spring Data MongoDB
+
+基于Mongo Java Driver对MongoDB的操作封装。使用流程如下:
+
+1. 定义定义Mongo对象并构造数据库工厂,对应的是MongoDB官方jar包中的Mongo,replica-set设置集群副本的ip地址和端口。
+
+ ```
+
+
+
+
+
+
+
+ ```
+
+1. 配置映射相关信息,包括映射上下文、类型映射、映射转换等。
+
+ ```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+1. 构造MongoTemplate,即可使用mongoTemplate进行数据库操作。
+
+ ```
+
+
+
+
+
+
+ ```
+
+ 这里需要注意上面配置的writeResultChecking和writeConcern是为了安全写入。
+
+ 使用MongoTemplate需要给数据库实体类加@Document,并指定集合的名字, 如果不加此注解或者没指定collection, 那么默认使用类的简单名称首字母小写做为集合的名字。
+
+ ```
+ @Document(collection = "test_user")
+ public class User{
+ private String id;
+
+ private String userName;
+
+ private String nickName;
+
+ ...
+ }
+
+ User user = new User();
+ user.setName("test");
+ user.setNickName("测试用户");
+
+ mongoTemplate.insert(user); //插入数据
+
+ user.setName("test2")
+ mongoTemplate.save(user); //保存数据
+
+ Criteria criteria = Criteria.where("name").is("test2"); //根据name查询数据
+ mongoTemplate.findOne(Query.query(criteria), User.class);
+ ```
+
+这里需要注意几点:
+
+- mongo的配置slave-ok为true时,如果readPreference为primary,会自动转换readPreference为secondaryPreferred。建议不要设置slave-ok此选项,直接用readPreference来控制读。
+- Spring Data MongoDB会默认在每个collection中添加_class字段,来标识原始来源类型,可以在defaultMongoTypeMapper将typeKey设置为null去掉此字段。
+- Spring Data MongoDB会把实体类中的id属性转换为_id(MongoDB默认使用的主键field),因此当你使用MongoTemplate做了数据操作后,再使用Mongo Java Driver查询的时候要使用_id而不是id。
+- 实体类中的id如果为空,MongoDB会自动生成ObjectId作为id,读取数据时id被赋值为ObjectId的字符串值。
+- 要注意对MongoDB安全写的配置,根据业务场景对MongoTemplate的writeResultCheckin和writeConcern配置合适的值。
+
diff --git a/book/chapter4-spring/spring.md b/book/chapter4-spring/spring.md
new file mode 100644
index 0000000..8bb0701
--- /dev/null
+++ b/book/chapter4-spring/spring.md
@@ -0,0 +1,662 @@
+# 4.1 Spring核心组件
+
+Spring框架的核心包括IOC、AOP以及辅助工具SpringEL等。
+
+首先要明确一个概念,Spring的IOC容器是ApplicationContext。常用的ApplicationContext如下:
+
+- ClassPathXmlApplicationContext: 从ClassPath路径下加载XML配置的上下文。
+- FileSystemXmlApplicationContext:从文件系统中加载XML配置的上下文。
+- XmlWebApplicationContext:Web开发中从XML中记载Web上下文,区别于上面之处在于此上下文是基于ServletContext的。
+- AnnotationConfigWebApplicationContext:从注解类中加载Web上下文。
+
+对于这些上下文中的实例在Spring中被叫做Bean。一个Bean的生命周期管理如下图所示:
+
+
+
+图中所示为Spring中的几个关键类、接口以及对应的方法,箭头的方向表示执行的顺序,描述了一个Bean从开始初始化到销毁会经历的一些方法调用。
+
+## 4.1.1 双亲上下文
+
+Spring中的ApplicaitonContext是父子层次结构的,存在多个上下文的时候,会有一个根上下文做为其他上下文的父亲。
+
+```
+
+ org.springframework.web.context.ContextLoaderListener
+ ...
+
+```
+
+如上,如果在web.xml中使用Listener监听器来加载Spring的配置,Spring会创建一个全局的WebApplicationContext上下文,称为根上下文,保存在 ServletContext中,key是`WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE`属性的值。可以使用工具类取出上下文:`WebApplicationContextUtils.getWebApplicationContext(ServletContext)`或者`WebApplicationContextUtils.getRequiredWebApplicationContext(ServletContext)`。
+
+而很多情况下,我们还会配置一个或者多个DispatcherServlet,每个DispatcherServlet有一个自己的WebApplicationContext上下文。这个上下文是私有的,继承了根上下文中所有东西。保存在ServletContext中,key是 `"org.springframework.web.servlet.FrameworkServlet.CONTEXT" + Servlet名称`。当一个Request对象产生时,会把这个WebApplicationContext上下文保存在Request对象中,key是`DispatcherServlet.class.getName() + ".CONTEXT"`。可以使用工具类取出上下文:`RequestContextUtils.getWebApplicationContext(request)`或者`WebApplicationContextUtils.getWebApplicationContext(servletContext,attrname)`。
+
+为了避免双亲上下文,可以不使用Listener监听器来加载Spring的配置,直接改用DispatcherServlet来加载Spring的配置。
+
+## 4.1.2 事件机制
+
+Spring中提供了事件机制,用于监听容器事件的发生,在事件发生时做一些处理工作。
+
+1. 容器事件监听器:实现ApplicationListener接口的类, 可以监听容器的事件,包括:
+
+ - ContextStartedEvent: 上下文启动事件
+ - ContextRefreshedEvent:上下文刷新完毕事件
+ - ContextStoppedEvent:上下文停止事件
+ - ContextClosedEvent:上下文关闭事件
+
+ ```
+ @Service
+ public class TestListener implements ApplicationListener {
+ @Override
+ public void onApplicationEvent(ContextStartedEvent event) {
+ ...
+ }
+ }
+
+
+ ```
+
+1. 具有意识的Bean:形如xxAware的接口,实现了此种接口的Bean即为有意识,能够注入/获取到xx代表的事物。如,实现ApplicationContextAware接口的类,会自动注入当前的ApplicationContext。
+
+ ```
+ public class ApplicationContextHolder implements ApplicationContextAware {
+ private static ApplicationContext applicationContext;
+
+ public static ApplicationContext getContext() {
+ return ApplicationContextHolder.applicationContext;
+ }
+
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ ApplicationContextHolder.applicationContext = applicationContext;
+ }
+ }
+
+
+ ```
+
+ 此外,在Bean生命周期图中的BeanNameAware和BeanFactoryAware也是两个具有意识的Bean。
+
+ - 实现了BeanNameAware的Bean能够感知到自己在BeanFactory中注册的名称。
+ - 实现了BeanFactoryAware的bean能够感知到自己所属于的BeanFactory。
+
+## 4.1.3 初始化和销毁
+
+如果想要在应用初始化的时候做一些初始化方法,可以使用以下几种初始化方式:
+
+1. 直接在Bean的构造方法里做初始化工作。
+1. 使用@PostConstruct注解,指明在Bean构造器方法执行后执行的方法。
+1. Bean实现InitializingBean接口,在afterPropertiesSet做初始化工作。
+1. 在XML中使用init-method指定Bean构造完成后调用的方法。
+
+需要注意的是方法1和2并不保证在执行时依赖的其他Bean已经注入进来。因此如果想在某些Bean初始化完毕并注入进来之后再进行初始化工作,可以配合使用@DependsOn注解。
+
+```
+@Service
+@DependsOn("configService")
+public class AccountService implements Constants {
+
+ @Resource
+ private ConfigService configService;
+
+ @PostConstruct
+ public void init() throws IOException {
+ ...
+ }
+
+}
+```
+
+也可以使用BeanFactoryPostProcessor和BeanPostProcessor来做一些更为前置的初始化工作, 典型的应用场景就是实现自己的注解。
+
+- 实现BeanFactoryPostProcesso接口可以在在Spring容器加载了Bean的定义之后,在Bean实例化之前执行,能够修改Bean的定义属性。如可以把Bean的scope从singleton改为prototype,也可以把property的值给修改掉。可以通过接口的参数获取到相关Bean的定义信息。
+- 实现BeanPostProcesso接口可以在Spring容器实例化Bean之后,在执行Bean的初始化方法前后,添加一些自己的处理逻辑。Spring内置了几个BeanPostProcessor实现:
+
+ - CommonAnnotationBeanPostProcessor:支持@Resource注解的注入
+ - RequiredAnnotationBeanPostProcessor:支持@Required注解的注入
+ - AutowiredAnnotationBeanPostProcessor:支持@Autowired注解的注入
+ - ApplicationContextAwareProcessor:用来为bean注入ApplicationContext等容器对象
+
+根据Bean的生命周期。以上提到的初始化方法的优先级为: BeanFactoryPostProcessor > Constructor > BeanPostProcessor.postProcessBeforeInitialization > @PostConstruct > InitializingBean > init-method。
+
+此外,如果是想要在所有Bean都初始化完毕后做一次初始化工作,那么可以使用上一节所说的ApplicationListener,监听ContextRefreshedEvent。
+
+```
+@Service
+public class BootstrapService implements ApplicationListener {
+ @Override
+ public void onApplicationEvent(ContextRefreshedEvent event) {
+ ...//初始化代码
+ }
+}
+```
+
+而要在销毁Bean之前做一些收尾工作,有以下三种方式:
+
+- 使用@PreDestroy注解,指明在容器关闭后执行的方法。
+- 实现DisposableBean接口,在destroy方法做销毁工作。
+- 在XML中使用destroy-method指定bean销毁时调用的方法。
+
+根据Bean生命周期,可知优先级为:@PreDestroy > DisposableBean > destroy-method。
+
+## 4.1.4 动态构造Bean
+
+当要创建的Bean不能直接通过构造方法、setter方法、字段注入完成,还需要做一些初始化工作的时候,普通创建Bean的方式就力不从心了。 Spring提供了三种方式解决这个问题:
+
+1. 定义Bean时,指明factory-bean和factory-method
+
+ ```
+ public class TestFactory{
+ public User getUser(){
+ return new User();
+ }
+ }
+
+
+
+
+ ```
+
+2. 定义Bean直接使用类的静态方法
+
+ ```
+ public class TestFactory{
+ public static User getStaticUser(){
+ return new User();
+ }
+ }
+
+
+ ```
+
+3. 实现FactoryBean接口
+
+ ```
+ public interface FactoryBean {
+ T getObject() throws Exception;
+
+ Class> getObjectType();
+
+ boolean isSingleton();
+ }
+
+ public class TestFactory implements FactoryBean {
+ @Override
+ public User getObject() throws Exception {
+ return new User("test");
+ }
+
+ @Override
+ public Class> getObjectType() {
+ return User.class;
+ }
+
+ @Override
+ public boolean isSingleton() {
+ return true;
+ }
+ }
+
+
+ ```
+
+针对第3种方式的FactoryBean,Spring自带了几个实现。:
+
+- PropertyPathFactoryBean: 用来获取目标Bean的属性值(实际是其getter方法的返回值)。PropertyPathFactoryBean返回值(基本数值类型或者对象)在最外层,则把返回值注册为容器中一个Bean,Bean名字为id 。
+
+ ```
+
+
+
+
+
+
+
+ ```
+
+- FieldRetrievingFactoryBean:用来获取类的静态属性值或者对象的实例属性值。
+
+ ```
+
+ ```
+ 上面即获取类java.sql.Connection的静态属性值TRANSACTION_SERIALIZABLE。
+
+- MethodInvokingFactoryBean:用来调用类的静态方法或者调用Bean对象的实例方法。若有返回值则可注册为容器中的Bean或者作为依赖注入其它Bean中。
+
+ ```
+
+
+
+
+
+ .....
+
+
+
+ ```
+
+同样的,可以用inner Bean的方式将上面的FactoryBean构造的实例注入到其他bean中:
+
+```
+
+
+
+
+
+
+
+
+```
+
+当然,现在Spring提供了@Bean注解,完全可以在代码中做这种动态生成Bean的工作。
+
+```
+@Configuration
+public class SpringCoreConfig{
+
+ @Bean("testUser")
+ public User TestUser(AdminUser adminUser) { //参数可以直接引用bean
+
+ User user = new User();
+ user.setUsername(adminUser.getUser().getUsername());
+
+ ...
+
+ return user;
+ }
+}
+```
+
+## 4.1.5 注入集合、枚举、类的静态字段
+
+1. 对于在XML中已经存在的Bean注入List、Map等集合这种需求,可以这样配置:
+
+ ```
+
+
+
+
+ list1
+ list2
+ list3
+
+
+
+
+
+
+ ```
+
+2. 注入枚举、静态值
+
+ 对于枚举和静态值的注入,使用FieldRetrievingFactoryBean是一种办法。但其实当注入的字段值属于此字段对应类的静态字段时,只需要注明静态字段名即可。如:
+
+ ```
+ public enum Status{
+ ENABLE,DISABLE
+ }
+
+ public class ParentUser{
+ public static final ParentUser INSTANCE = new ParentUser();
+ }
+
+ public class User{
+ private Status status;
+
+ privata ParentUser parentUser;
+
+ private void setStatus(Status status){
+ this.status = status;
+ }
+
+ private Status getStatus(){
+ return this.status;
+ }
+
+ private void setParentUser(ParentUser parentUser){
+ this.parentUser = parentUser;
+ }
+
+ private ParentUser getParentUser(){
+ return this.parentUser;
+ }
+ }
+
+
+
+
+
+ ```
+
+## 4.1.6 AOP
+
+AOP(Aspect Oriented Programming)是为了解决某些场景下代码重复问题的一种编程技术,允许程序模块化横向切割关注点或横向切割典型的责任划分。能够封装多个类中不同单元的相同功能,把应用业务逻辑和系统服务分开,经常用在日志和事务管理上。
+
+说到AOP不得不提的就是Aspectj这个AOP框架,它是一种编译期AOP框架(即在编译的时候对被代理的类进行增强)。它自己有一套AOP各个组件的概念定义。其中的几个关键的概念如下:
+
+- 方面:Aspect,横切多个类的某个功能描述。比如,一个日志模块可以被称作日志的AOP切面。
+- 连接点:JointPoint, 程序执行过程中的某个函数调用。代表一个应用程序的某个位置,在这个位置我们可以插入一个AOP切面,它实际上是个应用程序执行Spring AOP的位置。
+- 通知: Advice, 是个在方法执行前或执行后要做的动作,实际上是程序执行时要通过SpringAOP框架触发的代码段。Spring切面可以应用五种类型的通知:
+
+ - before:前置通知,在一个方法执行前被调用。
+ - after: 在方法执行之后调用的通知,无论方法执行是否成功。
+ - after-returning: 仅当方法成功完成后执行的通知。
+ - after-throwing: 在方法抛出异常退出时执行的通知。
+ - around: 在方法执行之前和之后调用的通知。
+
+- 切入点: PointCut, 是映射到一个或一组连接点的指示符,通知将在这些位置执行。可以通过表达式或匹配的方式指明切入点。Spring AOP支持的AspectJ切入点指示符如下:
+
+ - execution:用于匹配方法执行的连接点;
+ - within:用于匹配指定类型内的方法执行;
+ - this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;
+ - target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;
+ - args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;
+ - @within:用于匹配所以持有指定注解类型内的方法;
+ - @target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;
+ - @args:用于匹配当前执行的方法传入的参数持有指定注解的执行;
+ - @annotation:用于匹配当前执行方法持有指定注解的方法;
+ - bean:Spring AOP扩展的,AspectJ没有此指示符,用于匹配特定名称的Bean对象的执行方法;
+ - reference pointcut:表示引用其他命名切入点,只有@ApectJ风格支持,Schema风格不支持。
+
+ 其中类型匹配的语法如下所示:
+
+ - *:匹配任何数量字符;
+
+ - ..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
+
+ - +:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。
+
+ 匹配类型表达式:`注解? 类的全限定名字`;匹配方法执行表达式:`注解? 修饰符? 返回值类型 类型声明?方法名(参数列表) 异常列表?`
+
+ 此外,多个切入点表达式是可以组合的,AspectJ使用 且(&&)、或(||)、非(!)来组合切入点表达式,为了方便xml配置,Spring AOP 提供了and、or、not来代替&&、||、!。
+
+需要注意的是Spring AOP是一种运行期的AOP框架,对Aspectj的支持只是使用了其定义的各个组件定义和注解,底层实现和它并没有多少关系。
+
+使用Spring AOP有三种方式:
+
+1. Spring AOP接口
+
+ 此种方式基于Spring提供的aop接口:
+
+ - 前置通知:MethodBeforeAdvice
+ - 后置通知:AfterReturningAdvice
+ - 环绕通知:MethodInterceptor
+ - 异常通知:ThrowsAdvice
+
+ 针对这些接口,进行实现并配置即可。此种方式需要对每一个被代理的bean创建一个ProxyFactoryBean并手动指定目标对象与通知,比较繁琐,笔者也不推荐使用,因此不再详细叙述。
+
+1. 依赖于Aspectj的XML配置
+
+ Spring从2.0开始开始支持Aspectj的XML配置方式,在标签下使用、、配置,自动完成动态代理工作,大大简化了aop的开发工作。如下:
+
+ ```
+ public class TestServiceAop{
+ public void doAround(ProceedingJoinPoint pjp) throws Throwable {
+ System.out.println("===========进入around环绕方法!=========== \n");
+
+ // 调用目标方法之前执行的动作
+ System.out.println("调用方法之前: 执行!\n");
+
+ // 调用方法的参数
+ Object[] args = pjp.getArgs();
+ // 调用的方法名
+ String method = pjp.getSignature().getName();
+ // 获取目标对象
+ Object target = pjp.getTarget();
+ // 执行完方法的返回值:调用proceed()方法,就会触发切入点方法执行
+ Object result = pjp.proceed();
+
+ System.out.println("输出:" + args[0] + ";" + method + ";" + target + ";" + result + "\n");
+ System.out.println("调用方法结束:之后执行!\n");
+ }
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+1. 依赖于Aspectj注解的AOP配置
+
+ Spring现在也支持使用Aspectj的注解来做AOP,比起XML配置的方式,更加简单。
+
+ ```
+ @Component
+ @Aspect
+ public class UserRestOperationLogAop {
+
+ @Pointcut("execution(public * me.rowkey.pje.spring.service..*.add*(..)) " +
+ "|| execution(public * me.rowkey.pje.spring.service..*.delete*(..)) " +
+ "|| execution(public * me.rowkey.pje.spring.service..*.update*(..))" +
+ "|| execution(public * me.rowkey.pje.spring.service..*.create*(..))" +
+ "|| execution(public * me.rowkey.pje.spring.service..*.modify*(..))")
+ public void pointCut() {
+ }
+
+ @Around(value = "pointCut()")
+ public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
+ System.out.println("===========进入around环绕方法!=========== \n");
+
+ // 调用目标方法之前执行的动作
+ System.out.println("调用方法之前: 执行!\n");
+
+ // 调用方法的参数
+ Object[] args = pjp.getArgs();
+ // 调用的方法名
+ String method = pjp.getSignature().getName();
+ // 获取目标对象
+ Object target = pjp.getTarget();
+ // 执行完方法的返回值:调用proceed()方法,就会触发切入点方法执行
+ Object result = pjp.proceed();
+
+ System.out.println("输出:" + args[0] + ";" + method + ";" + target + ";" + result + "\n");
+ System.out.println("调用方法结束:之后执行!\n");
+ }
+ }
+
+
+
+
+ ```
+
+Spring中AOP的实现原理包括两种方式:JDK动态代理和CGLIB。
+
+- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。
+- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP。
+- 如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换。
+
+也可以强制AOP使用CGLIB,如下
+
+```
+//XML配置
+
+
+//注解配置
+@EnableAspectJAutoProxy(proxyTargetClass = true)
+```
+
+## 4.1.7 XML配置进阶
+
+XML是Spring一开始就支持的配置方式,也是最主流的配置方式。从Spring 2.0开始,基于文件的配置就从DTD转换到了XSD,目的就是让大家能够更好地利用XSD做到配置的灵活性和便利性。这里首先要注意的是在加载XML文件时,通常会有前缀classpath:和者classpath*:这俩的区别在于当classpath中存在同样路径的多个文件时,前者只会去加载第一个找到的资源,而后者则会去加载所有找到资源。路径的对照表如下:
+
+前缀 | 例子 | 说明
+----|-----|------
+classpath: | classpath:applicationContext.xml | 从classpath中加载
+file: | file:/data/applicationContext.xml | 作为 URL 从文件系统中加载。
+http: | http://host/applicationContext.xml | 作为 URL 加载。
+(none)| /data/applicationContext.xml | 根据上下文的不同而不同: FileSystemXmlApplicationContext对应文件系统;GenericWebApplicationContext对应ServletContext中的资源;ClassPathXmlApplicationContext对应Classpath下的资源。
+
+之前IOC和MVC两章都有一些基本的Spring xml配置示例,这里主要讲解一些可能不是所有人都知道的知识点。首先看下面的一个配置示例:
+
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+对上述示例配置的说明如下:
+
+- xsi:schemaLocation:是为了指出XSD文件可以在本地jar包中找到,无须去网络位置获取。
+- p:xx:对属性赋值的简化,作用于属性注入。
+- c:xx:构造注入的简化。
+- 使用``引用null值。
+- 从Spring3开始,beans多了一个profile属性可以根据不同的profile使用不同的bean,可以用在本地、测试、生产环境使用不同的日志、数据库配置这种场景下。上面的bean2即表示在profile为dev时,才会初始化这个bean。其中,profile通过spring.profiles.active和spring.profiles.default这俩Java运行属性或者SPRING_PROFILES_DEFAULT和SPRING_PROFILES_ACTIVE环境变量来指定。此外,对于ContextLoaderListener,在web.xml可以配置其默认profile:
+
+ ```
+
+ spring.profiles.default
+ local
+
+ ```
+
+ 对于DispatchServlet,在web.xml中配置:
+
+ ```
+
+
+ spring.profiles.default
+ local
+
+
+ ```
+
+除了beans之外,spring还提供了很多额外的namespace以简化配置。
+
+- context: 此namespace下的配置主要是对上下文的配置简化。
+
+ - annotation-config:开启注解装配。隐式地向Spring容器注册AutowiredAnnotationBeanPostProcessor、RequiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor以及PersistenceAnnotationBeanPostProcessor这4个BeanPostProcessor。
+ - component-scan: 指定注解扫描的包路径。此配置包含了annotation-config的功能,因此如果配置了component-scan,那么annotation-config是可以省略的。
+ - property-holder:根据指定的location扫描properties文件以及系统属性和环境变量,存储里面的键值对, 在XML配置里可以使用占位符${}引用。在代码中,可以使用注解@Value + 占位符引用。
+
+- util:此namepace下主要是提供了一些构建Bean的工具。
+
+ - constant:根据表达式创建指定静态字段作为bean,可替代FieldRetrievingFactoryBean的使用。
+ - property-path: 获取指定path的属性作为bean,可替代PropertyPathFactoryBean的使用。
+ - properties: 从指定位置加载properties文件,创建一个java.util.Properties实例bean。后面就可以引用此bean的属性。可替代PropertiesFactoryBean的使用。
+ - list:创建一个列表bean。可替代ListFactoryBean的使用。
+ - map:创建一个map bean。可替代MapFactoryBean的使用。
+ - set:创建一个set bean。可替代SetFactoryBean的使用。
+
+- mvc
+
+ - annotation-driven: 开启MVC的注解支持,创建了和MVC、注解相关的一系列Bean,如RequestMappingHandlerMapping、RequestMappingHandlerAdapter等。
+
+ - message-convertors: annotation-driven的子元素,用来配置HttpMessageConvertor。
+ - argument-resolvers: annotation-driven的子元素,用来配置项目用到的自定义HandlerMethodArgumentResolver。
+
+ - interceptors:配置项目用到的拦截器
+ - content-negotiation: view-resolvers的子元素,配置内容协商视图。
+
+ ```
+
+
+
+
+
+
+
+
+ ```
+
+## 4.1.8 注解+代码配置
+
+现在利用注解+代码配置变得越来越流行。基本上对应着每一个XML配置都有相应的注解配置与之对应。常用的注解配置如下:
+
+- @Configuration: 表示此类的用途是用来定义Bean的。
+- @Bean:注解到方法上,创建、初始化一个Bean。对应于配置。
+- @Profile:对应于, 指定某个profile激活的Bean等。
+- @ComponentScan:对应于配置,扫描对应的包。
+- @EnableWebMVC: 对应于配置,开启MVC注解支持。配合@Configuration使用。被注解的对象实现WebMvcConfigurer接口(一般继承WebMvcConfigurerAdapter即可)可以自定义配置拦截器、视图协商器、MessageConverters等等。
+- @Import: @Import允许将其它JavaConfig形式的配置类引入到当前的@Configuration标注的配置类当中,对应于原来XML中的 ,也可以通过@ImportResource将XML形式定义的配置引入当前JavaConfig形式的配置类当中。
+- @PropertySource:配合@Configuration使用,用来加载.properties内容到Environment,比如: @PropertySource("classpath:config.properties"),需要容器中配置一个PropertySourcesPlaceholderConfigurer。
+
+一个配置示例如下:
+
+```
+@Configuration
+@ImportResource({
+ "classpath:applicationContext-core.xml"
+})
+
+@Import(SpringExtraConfig.class)
+@ComponentScan(basePackages = {
+ "me.rowkey.pje.spring.mvc"
+}, excludeFilters = {
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "test\\.mvc\\.web\\..*"),
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "test\\.mvc\\.spring\\..*")
+})
+@@PropertySource("classpath:config.properties")//内容:sms.url=http://xxx/api/ams
+@EnableWebMVC
+public class SpringCoreConfig extends WebMvcConfigurerAdapter{
+
+ //配置拦截器
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+
+ registry.addInterceptor(new MyCustomInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
+ }
+
+ //Bean注解里面的name如果为空,那么bean的id为方法的名字
+ @Bean
+ public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
+ return new PropertySourcesPlaceholderConfigurer();
+ }
+
+ @Bean("httpClientUtil")
+ public HttpClientUtil httpClientUtil() {
+
+ return new HttpClientUtil(100, 2000, 1000);
+ }
+
+ @Profile("local")
+ @Bean("smsService")
+ public ISmsService devSmsService(@Value("${sms.url}") String smsUrl,HttpClientUtil httpClientUtil) {
+ //return new SmsLocalService(httpClientUtil()); //通过内部方法引用
+
+ return new SmsLocalService(httpClientUtil);
+ }
+
+ @Profile("prod")
+ @Bean("smsService")
+ public ISmsService prodSmsService(@Value("${sms.url}") String smsUrl,HttpClientUtil httpClientUtil) {
+
+ return new SmsProdService(httpClientUtil);
+ }
+}
+```
+
+
diff --git a/book/chapter5-datastore/cache.md b/book/chapter5-datastore/cache.md
index f78656f..bcfe316 100644
--- a/book/chapter5-datastore/cache.md
+++ b/book/chapter5-datastore/cache.md
@@ -225,7 +225,7 @@ Object grabResutl = jedis.eval(REDIS_SCRIPT_GRAB_GIFT, Lists.newArrayList("test:
其中,volatile-lru是3.0版本之前的默认淘汰策略,之后的版本默认策略改成了noeviction。
-为了配合LRU的淘汰策略,Redis的内部数据结构中有一个lru字段记录了对象最后一次被访问的时间。可以通过object idletime [key]来在不更新lru字段的情况下查看相应key的空闲时间。进一步的可以结合使用scan+object idletile [key]来查询哪些健长时间未被访问,以判定热点key和冷key。
+为了配合LRU的淘汰策略,Redis的内部数据结构中有一个lru字段记录了对象最后一次被访问的时间。可以通过object idletime [key]来在不更新lru字段的情况下查看相应key的空闲时间。进一步的可以结合使用scan+object idletime [key]来查询哪些健长时间未被访问,以判定热点key和冷key。
这里需要注意的是Redis中为了节省内存占用使用了整数对象池(即共享整数对象),但当淘汰策略为LRU时,由于无法对对象池的同一个对象设置多个访问时间戳,因此不再会使用整数对象池。
diff --git a/book/chapter5-datastore/end.md b/book/chapter5-datastore/end.md
new file mode 100644
index 0000000..6573d47
--- /dev/null
+++ b/book/chapter5-datastore/end.md
@@ -0,0 +1,19 @@
+# 总结
+
+## 学习资料
+
+### Redis
+
+- [Redis命令](http://redisdoc.com/): 使用当然要看这份权威文档,也是平常开发中最常用的参考资料。
+
+- [Redis设计与实现](http://redisbook.com/):可以通过此文档来学习Redis的原理。当然,自己去看Redis的源代码更是不错的选择。
+
+ 学习内容:
+
+ + 常用命令以及数据结构
+ + 内部数据结构
+ + 内存映射数据库结构
+ + Redis数据类型
+ + 功能的实现
+ + 内部运作机制
+
diff --git a/book/chapter5-datastore/media/elk.png b/book/chapter5-datastore/media/elk.png
new file mode 100644
index 0000000..2480bfb
Binary files /dev/null and b/book/chapter5-datastore/media/elk.png differ
diff --git a/book/chapter5-datastore/media/explain.png b/book/chapter5-datastore/media/explain.png
new file mode 100644
index 0000000..5dcb3ad
Binary files /dev/null and b/book/chapter5-datastore/media/explain.png differ
diff --git a/book/chapter5-datastore/media/fst.png b/book/chapter5-datastore/media/fst.png
new file mode 100644
index 0000000..6dcc443
Binary files /dev/null and b/book/chapter5-datastore/media/fst.png differ
diff --git a/book/chapter5-datastore/media/hbase-process.png b/book/chapter5-datastore/media/hbase-process.png
new file mode 100644
index 0000000..e03395b
Binary files /dev/null and b/book/chapter5-datastore/media/hbase-process.png differ
diff --git a/book/chapter5-datastore/media/hbase_data.jpg b/book/chapter5-datastore/media/hbase_data.jpg
new file mode 100644
index 0000000..70dbe0c
Binary files /dev/null and b/book/chapter5-datastore/media/hbase_data.jpg differ
diff --git a/book/chapter5-datastore/media/mha.jpg b/book/chapter5-datastore/media/mha.jpg
new file mode 100644
index 0000000..2f71fab
Binary files /dev/null and b/book/chapter5-datastore/media/mha.jpg differ
diff --git a/book/chapter5-datastore/media/mmm.jpg b/book/chapter5-datastore/media/mmm.jpg
new file mode 100644
index 0000000..fc69e4d
Binary files /dev/null and b/book/chapter5-datastore/media/mmm.jpg differ
diff --git a/book/chapter5-datastore/media/mongo-explain.png b/book/chapter5-datastore/media/mongo-explain.png
new file mode 100644
index 0000000..2dc83da
Binary files /dev/null and b/book/chapter5-datastore/media/mongo-explain.png differ
diff --git a/book/chapter5-datastore/media/rs-arch.png b/book/chapter5-datastore/media/rs-arch.png
new file mode 100644
index 0000000..57831fb
Binary files /dev/null and b/book/chapter5-datastore/media/rs-arch.png differ
diff --git a/book/chapter5-datastore/media/sharded-cluster-production-architecture.png b/book/chapter5-datastore/media/sharded-cluster-production-architecture.png
new file mode 100644
index 0000000..48a5bb0
Binary files /dev/null and b/book/chapter5-datastore/media/sharded-cluster-production-architecture.png differ
diff --git a/book/chapter5-datastore/nosql.md b/book/chapter5-datastore/nosql.md
new file mode 100644
index 0000000..8c019f8
--- /dev/null
+++ b/book/chapter5-datastore/nosql.md
@@ -0,0 +1,571 @@
+# 5.2 非关系型数据库
+
+上一节讲了关系型数据库,事实上很多业务中的数据表并不要求ACID和事务,可以考虑使用NoSQL数据库,这样能够彻底解决水平扩展问题。
+
+非关系型数据库是相对关系型数据库来讲的,又称为NoSQL数据库,也可以叫做Not Only SQL数据库。相比起传统的SQL关系型数据库,其最大的特点就是适合存储非结构化或半结构化的数据,适合存储大规模数据。以键值对存储,且结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,这样就不会局限于固定的结构,可以减少一些时间和空间的开销。但是,其没有关系型数据库那种严格的数据模式,并不适合复杂的查询以及需要强事务管理(如交易)的业务,主要用来做数据分析、ETL、报表、数据挖掘、推荐、日志处理等非交易场景。
+
+常用的NoSQL数据库主要有以下几种:
+
+- KV数据库:主要以(key,value)键值对存储数据的数据库。以Redis、SSDB为代表。
+- 文档数据库:总体形式上也是键值对的形式,但是值里面又可以有各种数据结构:数组、键值对、字符串等等。以MongoDB、CouchDB为代表。
+- 稀疏大数据库(列数据库):也叫作稀疏大数据库,一般是用来存储海量数据的。相对于行数据库,这种数据库是以列为单位存储数据在介质上的。以HBase、Cassendra为代表。
+
+还有诸如图数据库、对象数据库、XML数据库都属于NoSQL数据库。
+
+此外,这里需要提到CAP这个理论,它的核心在于一个分布式系统不可能同时满足以下三点,最多能够较好地同时满足两个:
+
+- Consistency:一致性。所有节点在同一时间(不是严格的同一时间)具有相同的数据。对指定的客户端来说,读操作保证能够返回最新的写操作结果。
+- Available:可用性。每次请求都能在期望时间内获取到正确的响应,但不保证获取的数据为最新数据,可以允许数据不一致。
+- Partition tolerance:分区容错性。系统中网络分区现象(丢包、拥塞、连接中断)的发生不会影响系统的继续正常运作。
+
+上述阐述是我在参考了Brewer于2000年以及Lynch于2002年对CAP的定义和声明后自己的一个理解。简单来说其实就是在发生分区的情况下,C和A只能满足一个。而NoSQL这种分布式数据库,肯定是有分区的,因此主要看是满足C,还是满足A。
+
+根据CAP理论,可以将数据库分为三类:
+
+- CA:传统关系型数据库的单点模式,满足数据的一致性和高可用性,但没有可扩展性。
+- CP: MongoDB、HBase,满足数据的一致性和分区性,通常性能不是特别高,发生网络分区时,通常会禁止写或仅允许写部分节点。
+- AP: Cassendra,具有较好的性能和可扩展性,但各节点的之间数据同步比较慢,能保证数据的最终一致性。
+
+此外,基于CAP理论,为了获得可扩展性和高可用性,BASE原则(AP方案的延伸)提出了对NoSQL数据库的可用性及一致性的弱要求:
+
+1. Basically Availble: 基本可用,允许系统暂时不一致。
+2. Soft state: 软状态/柔性事务,可以理解为"无连接"的,暂时允许一些不准确的地方和数据的变换。
+3. Eventual Consistency: 最终一致性,当所有服务逻辑执行完成后,系统最后将回到一个一致的状态。
+
+可以看出,和ACID关注数据完整性和一致性相比,BASE的首要目标是可用性,保证在短时间内,即使有不同步的风险,也要允许新数据能够被存储。
+
+## 5.2.1 KV数据库
+
+KV数据库是主要以(key,value)键值对存储数据的数据库,可以通过key快速查询到其value,包括Redis和SSDB。
+
+1. Redis
+
+ Redis开始是作为缓存使用的,但是由于其具有持久化的特性,因此很多时候被用作数据库。由于其主要的特性是缓存,因此本节不做讲述。
+
+1. SSDB
+
+ 对于SSDB,需要先提到LevelDB。LevelDB是来自Google的一个快速、轻量级的KV存储类库,能够实现持久化的存储。它是一个嵌入式的数据库,没有分布式支持、没有schemal也不支持事务,仅仅是一些有序的key到value的映射,在使用SSD硬盘做为存储介质的情况下能发挥最大优势。其使用了LSM(Log Structure Merge Tree)作为底层数据存储结构,每次更新会记录提交Log,并提交到In-Memory Table中。之后Memory的数据落地是延迟的,并采用的一定的策略定期将磁盘中的数据进行compact。是针对写进行优化的一种存储设计。
+
+ SSDB是LevelDB的Redis兼容协议封装(使用LevelDB作为存储引擎),并且实现了主从同步和持久化的队列服务,支持Redis客户端。目前支持key-string、key-set以及key-hashmap三种数据结构,可以作为Redis之外的另一种选择。由于其是基于文件存储系统的,因此其支持的容量可以很大,但也受限于文件存储,性能上相比Redis还是有一定的差距,且由于要进行compact,CPU的使用率有时候会很高。如果用SSD硬盘作为存储设备的话性能则会更加接近Redis。
+
+ 此外,还需要提到的是RocksDB,这个数据库存储引擎来自于Facebook,是基于LevelDB存储引擎构建的一个可嵌入式的支持持久化的key-value存储系统,也可作为 C/S 模式下的存储数据库,但主要目的还是嵌入式。其主要针对闪存做了低延迟优化,并提供了随CPU数目进行线性扩展的能力。360开源的类似SSDB的Pika即为在RocksDB上的Redis兼容协议封装数据库。
+
+## 5.2.2 文档数据库
+
+文档数据库总体形式上也是键值对的形式,但是值里面又可以有各种数据结构:数组、键值对、字符串等等,可以认为是JSON格式的数据存储,因此能够对任意字段建立索引,实现关系数据库的某些功能。目前Java中最常用的文档数据库是MongoDB,因此这里主要针对MongoDB讲述, MongoDB版本为3.2.7。
+
+MongoDB是一个分布式文件存储的数据库,是非关系型数据库中最像关系型数据库的。其具有以下特点:
+
+- 灵活模式: 数据以JSON格式存储
+- 高可用性:复制集保证高可用
+- 可扩展性: 通过Sharded cluster保证可扩展性
+
+### 关键概念
+
+1. 数据库(Database)
+
+ 对应于MySQL的Database。
+
+1. 集合(Collection)
+
+ 对应于MySQL的Table,存在于数据库中,没有固定的结构,可以插入不同格式和类型的数据。
+
+1. 文档(Document)
+
+ 对应于MySQL的Row,是一组键值对(BSON)。不需要设置相同的字段,并且相同的字段不需要相同的数据类型。集合由文档构成。
+
+1. 域(Field)
+
+ 对应于MySQL的Column。
+
+1. 主键(Primary Key)
+
+ 和MySQL相同,MongoDB会自动将_id字段设置为主键。
+
+### 存储引擎
+
+一开始MongoDB仅仅支持MMAP存储引擎,到了目前的MongoDB 3.2已经支持多存储引擎。
+
+1. MMAPv1
+
+ 这是MongoDB最开始的存储引擎,也是3.2版本之前的默认存储引擎。就是比较简单的内存映射文件,支持集合级别的并发控制,不支持压缩。
+
+1. WiredTiger
+
+ 这是MongoDB 3.0引入的新的存储引擎,支持MongoDB的所有特性,是3.2版本的默认存储引擎。提供文档级别(Document-Level)的并发控制、检查点(CheckPoint)、数据压缩和本地数据加密( Native Encryption)等功能。
+
+1. In-Memory
+
+ In-Memory存储引擎用于将数据只存储在内存中,只将少量的元数据和诊断日志(Diagnostic)存储到硬盘文件中,由于不需要Disk的IO操作就能获取所需的数据,In-Memory存储引擎大幅度降低了数据查询的延迟(Latency);其支持文档级别的并发控制。此存储引擎是MongoDB企业版本的特性,且只能用在MongoDB 64位版本上。
+
+综上,如果你使用的版本支持WiredTiger,那就使用WiredTiger。
+
+可以通过mongod参数指定存储引擎。
+
+```
+mongod --storageEngine wiredTiger | inMemory
+```
+
+### 索引
+
+MongoDB的索引数据结构也是B树,和之前讲过的MySQL索引基本一致。不过这里需要注意的是由于其底层采用的是内存映射文件,因此其是一种非聚集索引,虽然MongoDB本身并没有这种定义。
+
+MongoDB也提供了explain工具来查看查询的执行计划。
+
+
+
+其中的winnningPlan就是将要使用的执行计划。这里和索引相关的字段如下:
+
+- indexName: 出现此字段则表明命中了索引。
+- indexBounds: 描述了使用索引的情况,给出了索引的遍历范围。
+
+MongoDB从2.4版本开始支持全文索引:
+
+```
+db.user_note.createIndex({title:"text"})
+
+db.user_note.find({$title:{$search:"basketball"}})
+```
+
+此外,对比关系型数据库,MongoDB还可以对数组元素和子文档字段做索引。
+
+```
+{
+ uid:xx,
+ name:'xx',
+ tags:['xx','xx','xx'],
+ address:[
+ {
+ "province": "xx",
+ "city":"xx"
+ },
+ {
+ "province": "xx",
+ "city":"xx"
+ }
+ ]
+}
+```
+
+以上面的集合ssy_user为例。
+
+- db.ssy_user.ensureIndex({"tags":1})即可对数组tags做索引,称为多键索引。执行db.ssy_user.find({"tags":"xx"})查询即可命中此索引。其本质上是为数组中的所有值都建立了单独的索引。需要注意的是,在数组上建立索引的代价比较大,慎用。
+- db.ssy_user.ensureIndex({"address.province":1}即对子文档的address字段做了索引。此种方式支持任意深度的索引。需要注意的是这个和子文档上建立的索引效果是不同的。db.ssy_user.ensureIndex({"address":1}只会在完全匹配子文档的查询(包括字段顺序)才会起作用。而字段上的索引只有在查询相应的字段时才起作用。
+
+还需要注意的是对于复合索引来说,最多可以包含一个数组字段。
+
+### TTL
+
+MongoDB 2.2版本后引入了TTL集合,即支持对集合进行失效时间设置。当超过指定时间后,集合会自动清除超时的文档,在保存一些诸如Session会话信息和存储缓存数据的时候非常有用。
+
+```
+db.user_login_log.ensureIndex({"create_time": 1},{expireAfterSeconds: 300})
+```
+
+以上即表示已create_time为标准,300秒后删除相应的文档。
+
+### 固定集合
+
+MongoDB中的Capped Collections是具有固定大小的集合,其吞吐性能较出色。固定集合其实就是类似于一个环,当空间用完,那么就从头开始存储。
+
+```
+db.createCollection("cappedLogCollection",{capped:true,size:10000,max:1000})
+```
+
+上面则创建了一个固定集合,其最大占用空间为10000bytes,最大存储文档数目为1000。size优先级高于max。
+
+可以将普通集合转换为一个固定集合:
+
+```
+db.runCommand({"convertToCapped":"user_note",size:10000})
+```
+
+固定集合和普通集合的一个区别是其文档是按照插入顺序存储的,默认情况下的查询也是按照插入顺序返回的。可以通过$natural调整顺序。此外,固定集合是不予允许删除文档的,只能通过drop删除所有文档。
+
+```
+db.cappedCollection.find().sort( { $natural: -1 } )
+```
+
+上面即以插入倒序返回文档。
+
+固定文档一般用于存储日志信息、缓存等。
+
+### 自增ID
+
+MongoDB没有类似MySQL一样的自增id功能。其_id默认就是ObjectId, 是自动生成的12字节唯一标识。如果想要使用自增ID, 那么需要通过编程实现。思路如下:
+
+1. 创建一个计数器集合,如 mongo_ids。具有两列:collectionName和seqId。
+2. 使用query:{ collectionName: 'user_note'},update: {$inc:{seqId:1}},进行db.mongo_ids.findAndModify获取自增Id。
+
+ ```
+ db.mongo_ids.findAndModify(
+ {
+ query:{collectionName: 'user_note' },
+ update: {$inc:{seqId:1}},
+ new:true
+ });
+ ```
+
+### 聚合操作
+
+MongoDB中的聚合使用aggregate(),用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。类似SQL语句中的count(*)。
+
+user_note集合如下:
+
+```
+{
+ _id:xxxx,
+ createUser: xxx,
+ status: xx
+ ...
+}
+```
+
+下面即可计算每个用户的笔记数目:
+
+```
+db.user_note.aggregate
+(
+ [
+ {$group :
+ {
+ _id : "$createUser",
+ num_tutorial : {$sum : 1}
+ }
+ }
+ ]
+)
+```
+
+除了sum,MongoDB还支持avg、min、max等聚合表达式。
+
+使用aggregate()进行聚合操作,查询速度较快。但MongoDB不允许单个聚合操作占用过多的系统内存(阈值为20%),如果超过限制,那么则会直接终止操作。此外,聚合操作返回的结果集必须小于16MB。
+
+辅助aggregate()的还有管道,即将上一文档的处理输出到下一个管道。常用的管道操作符如下:
+
+- $project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。
+- $match:用于过滤数据,只输出符合条件的文档。$match使用MongoDB的标准查询操作。
+- $limit:用来限制MongoDB聚合管道返回的文档数。
+- $skip:在聚合管道中跳过指定数量的文档,并返回余下的文档。
+- $sort:将输入文档排序后输出。
+- $group:将集合中的文档分组,可用于统计结果。
+
+除了普通的聚合之外,MongoDB还提供了MapReduce来做聚合操作。MapReduce简单的说就是将大批量的工作(数据)分解(Map)执行,然后再将结果合并成最终结果(Reduce)。能够在多台Server上并行执行,可以非常灵活地计算复杂的聚合逻辑。但是其非常慢,适用于离线大规模数据分析。
+
+```
+db.user_note.mapReduce(
+ function() { emit(this.createUser,1); },
+ function(key, values) {return Array.sum(values)},
+ {
+ query:{status:"normal"},
+ out:"note_total"
+ }
+).find()
+```
+
+以上即可查询每个用户有多少状态为normal的记事。其中:
+
+- Map函数必须调用 emit(key, value) 返回键值对。
+- Reduce函数将key-values变成key-value,也就是把values数组变成一个单一的值value。
+
+### 安全写入
+
+上面提到MongoDB是CP特性,牺牲了可用性。但其实通过安全写入机制,配置合适的WriteConcern是可以得到一定的可用性的。
+
+MongoDB中一个写入操作的流程如下:
+
+1. 数据首先写入到MongoDB缓存中。
+2. 缓存定时异步刷写到Journal日志文件中, 这里Journal是MongoDB的预写日志,相当于MySQL的redo log。
+3. Journal定时异步刷写到MongoDB的数据文件中。
+4. 如果是插入操作或更新操作中包含索引列,同时会维护索引结构。
+5. 如果是副本集,数据还会写到oplog定容集合(Capped Collections)中,这里的oplog相当于MySQL的binlog,用于从节点同步数据。
+
+WriteConcern这个参数描述了MongoDB在返回一个写操作成功前应该提供的保证,即在执行到何种程度的时候,MongoDB可以认为写操作成功并返回响应给客户端。越严格的安全写级别,也意味着要在执行更多步骤后才返回客户端响应,也就意味着更长的响应时间。安全写级别主要包括两个选项:w和j,其中w指返回前需要确认的次数(w=1表示只需要主节点确认,w=2表示主节点和至少一个从节点确认,w=majority表示当数据写入到replica set的大多数节点之后向客户端返回,j=1表示需要成功写到journal文件)
+
+一般来说,使用默认的设置w=1、j=0即可,即主节点确认收到写请求即返回。这样就能牺牲一定的数据一致性来得到一定的可用性。
+
+因此,要确保写入正确,至少使用WriterConcern.ACKNOWLEDGED;对于不重要的数据,则可以使WriteConcern.UNACKNOWLEDGED省去等待网络的时间。
+
+此外,对于读的可用性,则主要与ReadPreference配置有关:
+
+- primary:默认模式,一切读操作都路由到replica set的primary节点
+- primaryPreferred:正常情况下都是路由到primary节点,只有当primary节点不可用(failover)的时候,才路由到secondary节点。
+- secondary:一切读操作都路由到replica set的secondary节点
+- secondaryPreferred:正常情况下都是路由到secondary节点,只有当secondary节点不可用的时候,才路由到primary节点。
+- nearest:从延时最小的节点读取数据,不管是primary还是secondary。对于分布式应用且MongoDB是多数据中心部署,nearest能保证最好的data locality。
+
+默认的,ReadPreference为primary,是强一致性的。设置ReadPreference为secondary那么可以保证可用性,但无法保证一致性(除非WriteConcern的w为secondary总数)。
+
+可知,CP的划分对于MongoDB其实并非那么准确,在使用的时候根据配置的不同能够得到不同的特性。
+
+### 高可用
+
+MongoDB支持主从模式,在主服务器上开启mongod进程时加入参数--master,在从服务器上开启mongod进程时加入--slave和--source指定主服务器即可,这样在主数据库更新时,数据被复制到从数据库中。下面以在单台机器开启两个节点做主从为例。
+
+```
+$ cd /data && mkdir mongodata_master mongodata_slave
+
+$ ./mongodb/bin/mongod --port 27017 --dbpath /data/mongodata_master --master &
+
+$ ./mongodb/bin/mongod --port 27018 --dbpath /data/mongodata_slave --slave --source localhost:27017 &
+```
+
+此外,MongoDB还支持主主配置,可以实现数据的双向同步,但是在大部分情况下官方不推荐使用。主主模式下,两个节点都可以写数据。
+
+```
+$cd /data && mkdir mongodata_27050 mongodata__27051
+
+$ ./mongodb/bin/mongod --port 27050 --dbpath /data/mongodata_27050 --master --slave --source localhost:27051 > /dev/null &
+
+$ ./mongodb/bin/mongod --port 27051 --dbpath /data/mongodata_27051 --master --slave --source localhost:27050 > /dev/null &
+```
+
+这里需要注意的是,主从模式只是简单的一种高可用部署,当主节点或者一个从节点挂掉时,仍然需要人工干预才能恢复系统的运行。与之相比,官方更加推荐使用复制集的集群模式。
+
+复制集与主从相比,多了心跳监测,当主节点挂点,会在集群内发起主节点的选举机制,自动选举一位新的主服务器。使用也比较简单,只需要把--master和--slave、--source参数去掉换成--replSet参数并指定复制集名称即可。如下:
+
+```
+./mongodb/bin/mongod --port 27017 --dbpath /data/mongodata_master --replSet testReplset
+```
+
+此外,由于复制集的选举机制,必须保证成员数目为奇数。如果是偶数的话,主节点宕机就会导致其他节点变为只读。也可以使用一个仲裁节点(Arbiter),也是一个复制集的成员,但并不存储用户数据。
+
+### 复制集(Replica Set)
+
+MongoDB复制集由一组mongod实例(进程)组成,包含一个Primary节点、多个Secondary节点以及可选择的Arbiter节点(用于仲裁)。总体结构图如下所示:
+
+
+
+MongoDB Driver(客户端)的所有数据都写入Primary,Secondary从Primary同步写入的数据,以保持复制集内所有成员存储相同的数据集,提供数据的高可用。这类似于MySQL中的一主多从配置。其中,一个复制集节点一般会经历STARTUP(加入复制集)、STARTUP2(初始同步中)、PRIMARY/SECONDARY/ARBITER(主节点/从节点/仲裁节点)。其他的UNKNOWN表示节点从未与复制集有过通信,DOWN表示节点与复制集失去了连接,REMOVED表示曾经是复制集的一员。
+
+需要注意的是,MongoDB默认是开启链式复制的,即在ping过程中是有可能选择就近的从节点作为同步源的。能够作为同步源的节点必须为正常状态、数据比当前节点新且数据和主节点差距不大(通过oplog时间戳判断)。
+
+复制集保证了MongoDB的高可用性。官方推荐在读密集应用中,如果无法承载大量的并发请求,可以通过增加复制集的规模,分发读请求到secondary成员上提高应用性能。但需要注意的是,复制集规模再大也无法让读变得更快,只能使得总体的QPS变高。如果想要提升读的性能,则需要分片。
+
+需要注意的是当使用复制集时,如果有新的从节点加入、从节点同步数据失败、从节点无法追上主节点oplog或者主动触发(resync命令)时,那么需要进行初始同步(查询所有源数据库的所有集合的数据将其插入到自己的集合副本中并建立_id索引),其耗时取决于数据库的数据量和两个节点之间的网络状况,同步过程会影响其他节点,也会增加主节点的网络IO负载。官方建议的方式是通过从其他节点物理复制数据(数据文件必须能够追上主节点oplog)再加入集群来减少影响。
+
+此外,这里需要注意oplog集合的大小应根据DB规模及应用写入需求合理配置。配置得太大,会造成存储空间的浪费;配置得太小,可能造成Secondary的init sync一直无法成功(oplog为固定集合,在Primary写入太快的情况下有可能不足以存储所有从节点还未同步的oplog)。
+
+### 分片集(Sharded cluster)
+
+即MongoDB的分布式特性,通过分片来构成分布式集群。由三个组件构成:
+
+- shard: 数据结点,每一个数据结点存储一部分分片数据,每个shard都可以做复制集。
+- mongos:路由结点,做为客户端和分片集群间的查询路由。
+- config servers:配置结点,存储了集群的元数据和配置信息。
+
+来自官网的总体结构图如下所示:
+
+
+
+主要支持2种数据分布策略:
+
+- 范围分片(Range based sharding): 能很好的满足范围查询的需求,但如果shardkey有明显递增(或者递减)趋势,则新插入的文档多会分布到同一个chunk,无法扩展写的能力。
+- Hash分片(Hash based sharding):根据用户的shard key计算hash值(64bit整型),根据hash值按照范围分片的策略将文档分布到不同的chunk。与范围分片互补,能将文档随机的分散到各个chunk,充分的扩展写能力,弥补了范围分片的不足,但不能高效地服务范围查询,所有的范围查询要分发到后端所有的Shard才能找出满足条件的文档。
+
+Sharded cluster保证了MongoDB的可扩展性。官方推荐,在写密集型应用中,如果无法承载大量的并发请求,可通过部署Sharded cluster,增加shard数目来提高应用性能。当然,由于分片集很复杂,一开始可以先使用复制集,等业务增长到单个复制集无法支撑其数据存储量或者吞吐负载时再考虑部署分片集。
+
+### GridFS
+
+GridFS是MongoDB中的一个内置功能,用于存储和获取超过16M(BSON文件限制)的文件,将其存储在集合中,是文件存储的一种。
+
+GridFS会将文件分割为小的文件片段(默认为255kb,最后一个片段除外), 每一个片段存储为一个单独的文档。
+
+GridFS使用两个集合存储一个文件:fs.file用来存储文件元信息(filename,content_type,还有用户自定义的属性);fs.chunks用来存储文件存储片段。GridFS同时也会给这两个集合创建索引以提高读写性能。
+
+```
+./mongofiles -d gridfs put test.dat
+
+//fs.files
+{
+ "filename": "test.dat",
+ "chunkSize": NumberInt(211120),
+ "uploadDate": ISODate("2017-05-01T11:32:33.557Z"),
+ "md5": "7b762939321e146569b07f72c62cca4f",
+ "length": NumberInt(646)
+}
+
+//fs.chunks
+{
+ "files_id": ObjectId("334a7asdad19f54bfec8a2fe44b"),
+ "n": NumberInt(0),
+ "data": "Mongo Binary Data"
+}
+```
+
+此外,在某些场景下,把文件存储在GridFS中要比直接存储在系统的文件系统里性能要更好,如下:
+
+- GridFS对目录下的文件数目没有限制,可以避开一些文件系统的限制,存放大量小文件。
+- GirdFS可以仅仅读取文件的部分信息,这样当读取大文件时就不需要把这个文件都加载到内存中。
+- GirdFS可以自动对文件做分布式管理。
+
+### 使用提示
+
+对MongoDB本身的使用需要注意:
+
+- 建议开启Journaling功能,特别是对于可用性要求较高的用户。
+- MongoDB只支持单一文档的原子性。
+- MongoDB默认情况下是没有认证功能的,可以采取防火墙的方式进行保护,也可以进行相关的安全认证配置。
+- 生产环境开启profiling功能,以监测慢请求。
+- MongoDB对内存的要求比较高,生产环境中MongoDB所在的主机应该配置尽量大的内存。
+- 通常MongoDB单点支持的最大并发连接数上限为20000(可修改)。因此有一种说法是,千万以下选择MySQL,千万以上选择MongoDB,亿级以上选择Hadoop。
+
+使用MongoDB进行数据操作需要注意:
+
+- MongoDB默认情况下是区分大小写的。
+- 确保输入正确的数据类型,输入错误数据类型不会出现提示。
+- MongoDB的更新在默认情况下会使用类似于传统数据库的LIMIT语句,即LIMIT 1。如果想要一次更新许多文档,需要把multi设为true。
+- MongoDB的翻页效率比较低。使用find().skip().limit()的方式,务必要使用索引,否则导致全表扫描几百万数据几乎不可用。
+- 不要使用负向查询$nin, $not,不会命中索引。
+- 查询条件不要使用算数运算符,如$mod,不会使用索引
+- MongoDB中存储的文档必须有一个"_id"键,不指定值时,默认是个ObjectId对象。
+- 和MySQL一样,要减少不必要的或重复的索引,以避免更新或插入时对索引的维护开销。
+- 使用写入文档数组或者Bulk Write进行批量写,减少网络请求次数。
+- 作为主键的_id值应该避免使用随机值(md5、uuid等),而应该使用自增值(比如默认的ObjectId)。
+- 使用内嵌数组时需要注意数组不能过大,否则对其查询、修改都会带来CPU的大量消耗。如果有过大的可能(即使是只有几个文档),合理的设计是使用传统关系型数据库的设计,将数组拆分成多个文档。
+
+MongoDB的常用命令可见附录D。
+
+此外,Java中可以使用Spring Data MongoDB对MongoDB进行数据操作,在4.2.3中已经做过讲述。
+
+## 5.2.3 列数据库
+
+与传统的行数据库相比,列数据库的特点就在其是以列为存储单位的,以此方便存储结构化和半结构化数据和做数据压缩,对针对某一列或者某几列的查询有非常大的IO优势。HBase是最常用的列数据库之一。
+
+HBase是一个分布式的、面向列的开源数据库,该技术来源于Google论文“Bigtable:一个结构化数据的分布式存储系统”。BigTable是基于GFS的,HBase是基于HDFS的,是Hadoop生态的核心组件之一。它存储数据行在一个表里,一个数据行拥有一个可选择的键和任意数量的列。表是疏松的存储的,因此用户可以给行定义各种不同的列。主要用于需要随机访问,实时读写大数据。
+
+这里需要注意的是,HBase是面向OLAP的一种数据库,受限于底层的数据结构,如果不做二次优化,一般是不推荐用于面向用户的业务应用的。
+
+概括来看,HBase适用于以下存储场景:
+
+1. 半结构化或非结构化数据
+
+ 对于数据结构字段不够确定或杂乱无章很难按一个概念去进行抽取的数据适合用HBase。当业务发展需要增加存储比如一个用户的email、address信息时,RDBMS需要停机维护,而HBase支持动态增加.
+
+1. 记录非常稀疏
+
+ RDBMS的行有多少列是固定的,为null的列浪费了存储空间。HBase为null的Column不会被存储,这样既节省了空间又提高了读性能。
+
+1. 多版本数据
+
+ 根据Row key和Column key定位到的Value可以有任意数量的版本值,因此对于需要存储变动历史记录的数据,用HBase就非常方便。
+
+1. 超大数据量
+
+ 当数据量越来越大,RDBMS数据库无法支撑,就出现读写分离和各种分库分表策略,会带来业务复杂度的增加、无法join等问题。采用HBase只需要加机器即可,HBase会自动水平切分扩展,跟Hadoop的无缝集成保障了其数据可靠性(HDFS)和海量数据分析的高性能(MapReduce)。
+
+### 关键概念
+
+1. Row key
+
+ 行主键, HBase不支持条件查询和Order by等查询,读取记录只能按Row key(及其range)或全表扫描,因此Row key需要根据业务来设计以利用其存储排序特性(Table按Row key字典序排序如1,10,100,11,2)提高性能。
+
+2. Column Family
+
+ 列族,在表创建时声明,每个Column Family为一个存储单元。
+
+3. Column
+
+ 列,HBase的每个列都属于一个列族,以列族名为前缀,如列user:name和user:city属于user列族,note:title和note:type属于note列族。
+
+ Column可以动态新增,同一Column Family的Columns会聚簇在一个存储单元上,并依Column key排序,因此设计时应将具有相同I/O特性的Column设计在一个Column Family上以提高性能。
+
+4. Timestamp
+
+ HBase通过row和column确定一份数据,这份数据的值可能有多个版本,不同版本的值按照时间倒序排序,即最新的数据排在最前面,查询时默认返回最新版本。Timestamp默认为系统当前时间(精确到毫秒),也可以在写入数据时指定该值。
+
+5. Value
+
+ 每个值通过4个键唯一索引:tableName + RowKey + ColumnKey + Timestamp => value。
+
+6. 存储类型
+
+ - TableName是字符串
+ - RowKey和ColumnName是二进制值(Java 类型 byte[])
+ - Timestamp是一个64位整数(Java 类型 long)
+ - value是一个字节数组(Java类型 byte[])。
+
+HBase一个HTable的存储结构如下所示:
+
+```
+SortedMap{
+ RowKey, List()
+ SortedMap(
+ Column, List(
+ Value, Timestamp
+ )
+ )
+ )
+}
+```
+
+即HTable按Row key自动排序,每个Row包含任意数量个Columns,Columns之间按Column key自动排序,每个Column包含任意数量个Value。
+
+### 关键实现
+
+HBase有几个本身架构设计的组件需要了解:
+
+- Zookeeper群:HBase集群中不可缺少的重要部分,主要用于存储Master地址、协调Master和RegionServer等上下线、存储临时数据等等。
+- Master群:Master主要是做一些管理操作,如:region的分配,手动管理操作下发等等,一般数据的读写操作并不需要经过Master集群,所以Master一般不需要很高的配置即可。
+- RegionServer群:RegionServer群是真正数据存储的地方,每个RegionServer由若干个region组成,而一个region维护了一定区间rowkey值的数据。简称RS。
+- HLog: 是HBase实现WAL方式产生的日志信息,其内部是一个简单的顺序日志,每个RS上的region都共享一个HLog,所有对于该RS上的region数据写入都被记录到该HLog中。以备在RS出现意外崩溃的时候,可以尽量多的恢复数据。
+- MemStore: 可以看做是HBase的内部缓存,每次数据写入完成HLog后,都会写入对应regison的MemStore。**数据结构使用的是基于数组实现的跳跃表。**
+- HFile: HBase的数据底层存储。每次数据从MemStore中flush,最终都会形成一个HFile。**数据结构使用的是针对磁盘顺序读写优化过的类B+树。**
+
+基于以上几个集群,先看一下如何定位RegionServer,这里牵扯到两个特殊的表:Meta和Root, 用于存储数据库的元信息。
+
+- Meta表中记录了Rowkey是在哪个Region的范围以及各个Region是在哪个RegionServer上等待信息,是查询HBase时首先要访问的表。其rowkey设计为:region所在的表名 + region的StartKey + 时间戳,三者的MD5值是HBase在HDFS上存储的region的名字。此外,当Region被拆分、合并或者重新分配的时候,都需要来修改这张表的内容。
+- Root表: 记录了Meta表的Region信息, Root表只有一个region,其位置信息记录在Zookeeper中
+
+一个Region的定位过程如下所示:
+
+1. 读取ZooKeeper中Root表的位置信息。
+1. 通过Root表获取META表的位置。
+1. 读取.META表中用户表的位置。
+1. 读取数据。
+
+如果已经读取过一次,则Root表和Meta都会缓存到本地,直接去用户表的位置读取数据。
+
+定位到RS之后,就可以进行数据写入和读取。
+
+数据的写入首先要写HLog以实现WAL,写完HLog之后,按照rowkey的值排序写入MemStore,即成功返回。存储的数据是LSM的数据结构,需要不定期的进行compact以减少文件碎片数,提高性能。这里需要注意的是,Hlog在数据flush到磁盘成为HFile后,会被移动到.oldlogs这个目录下,会有一个HLog监控线程监控该目录下的HLog,根据设置删除过期的HLog,以防Hlog浪费存储空间。此外,HBase基于的文件系统HDFS是append only的,因此数据的删除和过期一开始只是被标记为删除,在compact时才真正的删除。总体过程如下图所示:
+
+
+
+另外一点需要注意的是,在数据flush的时候,对应region上的访问都是被拒绝的,因此控制flush的时机是非常重要的。主要有以下几种方式会触发flush:
+
+- 通过全局内存控制,触发MemStore刷盘操作。通过参数hbase.regionserver.global.memstore.upperLimit进行设置,内存下降到hbase.regionserver.global.memstore.lowerLimit配置的值后,即停止MemStore的刷盘操作。
+- HBase提供API接口,运行通过外部调用进行MemStore的刷盘
+- 前面提到MemStore的大小通过hbase.hregion.memstore.flush.size进行设置,当region中MemStore的数据量达到该值时,会自动触发MemStore的刷盘操作。
+- WAL达到阈值,会引起MemStore的flush。WAL的最大值由hbase.regionserver.maxlogs * hbase.regionserver.hlog.blocksize(2GB by default)决定.
+
+还需要注意的是,一个列族达到阈值触发flush的时候,也会导致其他的列族flush,因此列族的数量越少越好。
+
+相比起数据的写入,数据的读取相对来说比较简单:HBase首先检查请求的数据是否在MemStore,不在的话就到HFile中查找(先利用布隆过滤器过滤掉一部分无效查询),最终返回merged的一个结果给用户。
+
+### 使用提示
+
+- HBase一个表中同一列族的数据存储在一起,因此其本质是面向列族的存储。如果只有一个列族,那么就成了和MySQL一样的行式存储。
+- RowKey的设计越短越好,尽量不要超过16个字节。
+- 避免使用时序或者单调(递增/递减)行键,否则会导致连续到来的数据会被分配到同一region中,可以采取在rowKey前面添加md5散列值的方式。
+- 列族的数量越少越好,否则会造成在数据查询的时候读取更多的文件,消耗更多的I/O。
+- 同一个表中不同列族所存储的记录数量的差别(列族的势)会造成记录数量少的列族的数据分散在多个region上,影响查询效率。
+- 尽量最小化行键和列族的大小,避免HBase的索引过大,加重系统存储的负担。
+- HColumnDescriptor设置版本的数量,避免设置过大,版本保留过多。
+- 列族可以通过设置ttl,来实现过期失效。
+- 为避免热点数据产生和后续文件split影响业务使用,一般采用hash + partition的方式预分配region,首先使用md5 hash,然后再按照首字母partition为32份,就可以预分配32个region。
+- region的数量选择可以参考此公式:一个RS的内存消耗 = memstore大小 * region数量 * 列簇数量。
+- HBase支持多种形式的数据压缩:GZip、LZO、Snappy等。其中Snappy的压缩率最低,但是编解码速率最高,对CPU的消耗也最小,一般使用Snappy即可。
+- 对于有随机读的业务,建议开启Row类型的过滤器,使用空间换时间,提高随机读性能。
+- 避免全表扫描HBase数据。
+- 在存储列表时,HBase有两种典型的用法,一种是高表模式,与传统的Mysql模式非常类似,列表中的每一项存一行,每一行有固定的属性列;另一种是宽表模式,一个列表存一行,列表中的每一项存成一个单独的列,各种属性都打包到列内部的value中。
+
+
+
+
+
+
+
+
+
+
diff --git a/book/chapter5-datastore/rds.md b/book/chapter5-datastore/rds.md
new file mode 100644
index 0000000..2814883
--- /dev/null
+++ b/book/chapter5-datastore/rds.md
@@ -0,0 +1,489 @@
+# 5.1 关系型数据库
+
+关系型数据库是采用关系模型来组织数据的数据库。所谓关系模型指的就是二维表格模型,而一个关系型数据库就是由二维表及其之间的联系所组成的一个数据组。目前,用的最为普遍的有MySQL、Oracle、PostgreSQL等。这里针对Java开发中最常用的MySQL进行阐述。MySQL版本为5.5.19。
+
+## 5.1.1 存储引擎
+
+MySQL主要有两种存储引擎:MyISAM和InnoDB。
+
+1. MyISAM
+
+ MySQL 5.5之前的默认引擎,特点是:
+
+ - 不支持行锁,读取时对需要读到的所有表加锁,写入时则对表加排它锁。
+ - 不支持事务
+ - 不支持外键
+ - 不支持崩溃后的安全恢复
+ - 在表有读取查询的同时,支持往表中插入新纪录
+ - 支持BLOB和TEXT的前500个字符索引,支持全文索引
+ - 持延迟更新索引,极大提升写入性能
+ - 对于不会进行修改的表,支持压缩表,极大减少磁盘空间占用
+
+1. InnoDB
+
+ MySQL 5.5后的默认引擎,特点是:
+
+ - 支持行锁,采用MVCC来支持高并发,有可能死锁
+ - 支持事务
+ - 支持外键
+ - 支持崩溃后的安全恢复
+ - 不支持全文索引
+
+由于MyISAM缓存有表meta-data(行数等)在做COUNT(\*)时对于一个结构很好的查询是不需要消耗多少资源的。而对于InnoDB来说,就没有这种缓存。而当你需要行锁定、事务时,使用InnoDB则是更好的选择, 也具有更高级的安全性。此外,MyISAM和InnoDB使用的索引也是有区别的,前者为非聚簇索引,后者为聚簇索引。
+
+总体来说,MyISAM适合读密集型的表,而InnoDB适合写密集型的表。经常在数据库做主从分离的情况下,选择MyISAM引擎做为从库的存储引擎。不过,MySQL发展到现在,MyISAM引擎基本已经停滞维护了,因此使用MySQL的时候,InnoDB是存储引擎的第一选择。
+
+## 5.1.2 字符集和校对规则
+
+在创建数据库、数据表的时候会指定character和collate。如下:
+
+```
+CRREATE DATABASE db_name DEFAULT CHARSET SET utf8 COLLATE utf8_general_ci;
+
+CREATE TABLE `note` (
+ `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
+ `content` varchar(128) CHARSET utf8 COLLATE utf8_bin NOT NULL COMMENT '内容', //对某一个字段设置字符集和校对规则
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
+```
+
+字符集指的是一种从二进制编码到某类字符符号的映射。校对规则则是指某种字符集下的排序规则。MySQL中每一种字符集都会对应一系列的校对规则。
+
+MySQL采用类似继承的方式来设定字符集默认值,每个数据库以及每张数据表都有自己的默认值,它们逐层继承,最终最靠底层的默认设置将影响你创建的对象。比如,创建数据库时,将根据服务器上的character_set_server来设置数据库的默认字符集,同样的道理,根据database的字符集来指定库中所有表的字符集。不管是对数据库,还是表和列,只有当它们没有显式指定字符集时,默认字符集才会起作用。
+
+MySQL提供了很多字符集供使用,应该根据存储的内容选择能满足需求的最小的字符集。如果存储的内容是英文字符等拉丁语系字符的话,那么使用默认的latin1编码即可;如果需要存储汉字、俄文、阿拉伯语等非拉丁语系字符,则建议使用UTF8字符集。而校对规则一般来说只需要考虑是否以大小写敏感的方式比较字符串或者是否用字符串编码的二进制来比较大小,其对应的校对规则的后缀分别是_cs、_ci和_bin。大小写敏感和二进制校对规则的不同之处在于,二进制校对规则直接使用字符的字节进行比较,而大小写敏感的校对规则在多字节字符集时,如德语,有更复杂的比较规则。一般我们选择大小写不敏感的校对规则即可。
+
+utf8是我们常用的字符集,其对应的常用校对规则如下:
+
+- utf8_general_ci:utf8的默认校对规则。是一个遗留的校对规则,不支持扩展,仅能够在字符之间进行逐个比较,因此进行的比较速度很快。
+- utf8_unicode_ci:根据Unicode校对规则算法(UCA)执行,最主要的特色是支持扩展,即可以把一个字母看作与其它字母组合相等。例如,在德语和一些其它语言中‘ß’等于‘ss’。但其仅部分支持Unicode校对规则算法,一些字符还是不能支持。并且不能完全支持组合的记号。正确性要比utf8_general_ci好。
+- utf8_bin: 将字符串每个字符串用二进制数据编译存储,区分大小写,而且可以存二进制的内容。
+
+通常情况下, utf8_general_ci的准确性足够我们使用。
+
+此外,如果数据库需要支持emoji字符,那么字符集应该用utf8m4,校对规则使用utf8m4_bin(如果使用utf8m4_general_ci的话,会出现某些emoji表情无法区分的问题)。utf8m4是MySQL在5.5.3之后增加的编码,mb4就是most bytes 4的意思,专门用来兼容四字节的unicode。它是utf8的超集,除了将编码改为utf8mb4外不需要做其他转换。笔者建议,为了兼容性,建议使用utf8mb4编码。
+
+## 5.1.3 索引
+
+数据库索引是数据库使用非常关键的技术,合理正确的使用索引能够大大提高数据库的查询性能。
+
+### 数据结构
+
+MySQL索引使用的数据结构主要有:BTree索引和哈希索引。对于哈希索引来说,底层数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择使用哈希索引,查询性能最快;其余大部分索引场景,则建议选择BTree。
+
+MySQL的BTree索引使用的是B树中的B+Tree,但对于主要的两种存储引擎其实现方式是不同的。
+
+- MyISAM:B+Tree叶节点的data域存放的是数据记录的地址。索引检索时,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。这称为非聚簇索引。
+- InnoDB: 其数据文件本身就是索引文件。相比起MyISAM索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被成为聚簇索引(也叫聚集索引)。而其余的索引都为辅助索引,辅助索引data域存储相应记录主键的值而不是地址也是和MyISAM不同的地方。根据主索引搜索时,直接找到key所在结点即可取出数据;根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议用非单调的字段作为主键,会造成主索引频繁分裂。
+
+### 索引使用
+
+先介绍一个常用的MySQL命令: explain,此命令能够打印出SQL语句的执行计划,从而能够判断要执行的SQL语句是否能够命中索引,而做进一步调整。在优化SQL查询时,最好的方式就是使用此命令来判断执行计划是否合理。如:
+
+```
+explain select * from test_user where id=1;
+```
+
+结果如下:
+
+
+
+这里需要注意的是type、key以及extra这三列:
+
+- type: 显示了连接使用了哪种类别,有无使用索引。如果为ALL, 那么说明要进行全表扫描。
+- key:key列显示MySQL实际决定使用的键(索引)。如果没有选择索引,键是NULL。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。如果这里为Null就说明没有命中索引。
+- Extra: 此列如果出现Using filesort(需要进行额外的步骤来发现如何对返回的行排序)或者Using temporary(需要创建一个临时表来存储结果),说明查询需要优化。
+
+这里需要说明的是,MySQL自身是有查询优化器的,优化器的作用就是在一个查询所有可能的执行方式中找到这其中最好的执行计划。因此explain获取到的执行计划并非是固定的,它会随着数据分布情况的变动,执行计划也有可能改变。而且当数据库计算出使用索引所耗费的时间长于全表扫描或其它操作时(比如当表中索引字段数据重复率太高),将不会使用索引。
+
+1. 最左前缀原则
+
+ MySQL中的索引可以以一定顺序引用多个列,这种索引叫做联合索引。如User表的name和city加联合索引就是(`name`,`city`)。而最左前缀原则指的是,如果查询的时候查询条件精确匹配索引的左边连续一个或几个列时,此列就可以被用到。如下:
+
+ ```
+ select * from user where name=xx and city=xx; //可以命中索引
+
+ select * from user where name=xx; //可以命中索引
+
+ select * from user where city=xx;//无法命中索引
+ ```
+
+ 这里需要注意的是,查询的时候如果两个条件都用上了,但是顺序不同,如city = xx and name = xx, 现在的查询引擎会自动优化为匹配联合索引的顺序,是能够命中索引的。
+
+ 由于最左前缀原则的原因,在创建联合索引时,索引字段的顺序需要考虑字段值去重之后的个数,较多的放前面。ORDER BY子句也遵循此规则。
+
+1. 避免where子句中对字段施加函数,如to_date(create_time) > xxxxx,会造成无法命中索引。
+
+1. 使用InnoDB时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。
+
+1. 合理利用索引覆盖
+
+ 覆盖索引(covering index)指一个查询语句的执行只需要从辅助索引中就可以得到查询记录,而不需要查询聚集索引中的记录,也可以称之为实现了索引覆盖。简单来说就是查询条件命中了索引,而查询字段也属于索引中的字段,那么就实现了索引覆盖。当实现覆盖索引的时候,explain命令的Extra会显示using Index。
+
+1. 避免冗余索引
+
+ 冗余索引指的是索引的功能相同,能够命中A就肯定能命中B,那么A就是冗余索引。如(`name`,`city`)和(`name`)这俩索引就是冗余索引,能够命中后者的查询肯定是能够命中前者的。大多数情况下都应该尽量扩展已有的索引而不是创建新索引。
+
+ MySQL5.7版本后,可以通过查询sys库的schemal_redundant_indexes表来查看冗余索引。
+
+1. 对打算加索引的列设置为NOT NULL,否则将导致引擎放弃使用索引而进行全表扫描
+
+1. 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗。MySQL 5.7后可以通过查询sys库的schema_unused_indexes视图来查询哪些索引从未被使用。
+
+1. 联表查询必须存在的情况下,可以使用索引提高性能
+
+ 联表的索引使用要注意:
+
+ - 确保ON和USING字句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。当表A和表B用列c关联的时候,如果优化器关联的顺序是A、B,只需要在B的c字段建立索引即可。这里的关联顺序指的是执行SQL语句时需要先查询的表,如:select * from A join B on A.c = B.c where A.d=xx此语句会先查询A表,因此关联顺序为A、B。
+ - 确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化。
+
+1. 使用limit offset查询缓慢时,可以借助索引来提高性能。
+ ```
+ SELECT * FROM test_user a JOIN (select id from test_user limit ?, ?) b
+ ON a.id = b.id
+ ```
+
+ 如此,id为表test_user的主键,可以使用到主键索引首先扫描出分页的id列表,再根据id关联查询到对应的记录,能够提高查询性能。
+
+1. 查询条件的字段使用正确的数据类型,否则MySQL会自动做类型转换,导致无法命中索引。例如test_user表中mobile列为字符串类型,查询的时候如果没有加'',那么就是强制类型转换。
+
+1. 使用InnoDB时,需要根据场景合理设计主键,以避免使用辅助索引时的回表查询。如:查询某个用户某一段时间的笔记,相比起使用user_id,create_time做联合索引,可以直接使用[user_id][time_compress][seq]作为note_id主键(类似于HBase中RowKey的设计,与使用联合主键相比能够节省创建联合主键需要的磁盘block数)。其中,user_id可以固定为一定的位数,time_compress可以选择笔记创建时间(精确到秒)的保序压缩方式(对数值类型的数据进行某种编码以缩小字符串长度且保证顺序不变)如36进制,seq选择两位存储表示同一秒内笔记的创建序号。这样,查询语句select * from user_note where note_id like '[uid][timePrefix]%'或者select * from user_note where note_id between [note_id1] and [note_id2]可以命中主键索引,避免了回表查询和随机读。当然,这种方案会使得主键占用空间变大。
+
+索引可以加快查询速度,但索引也是有代价的:索引文件本身要消耗存储空间,并且在被索引的表上INSERT和DELETE会变慢,另外,MySQL在运行时也要消耗资源维护索引,因此索引并不是越多越好。两种情况下不建议建索引:
+
+- 表记录比较少,例如一两千条甚至只有几百条记录的表,没必要建索引。
+- 索引的选择性较低。所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值:
+
+ ````
+ Index Selectivity = Cardinality / #T
+ ````
+
+ 显然选择性的取值范围为(0, 1],选择性越高的索引价值越大,这是由B+Tree的性质决定的。在MySQL 5.6后,MySQL库下的innodb_index_stats表的stat_value字段记录了某张表在某个索引的不同取值的记录个数,innodb_table_stats的n_rows字段记录了某张表总的记录数目,两者相除即为索引的区分度。
+
+此外,需要提到的是MySQL也支持全文索引(5.6.24之前MyISAM引擎支持,之后InnoDB也开始支持)。
+
+```
+CREATE TABLE user_note (
+ id INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ title VARCHAR(200),
+ FULLTEXT(title)
+) TYPE=MYISAM;
+
+SELECT * FROM `user_note` WHERE MATCH(`title`) AGAINST('篮球')
+```
+
+## 5.1.4 查询缓存
+
+my.cnf加入以下配置,重启MySQL开启查询缓存:
+
+```
+query_cache_type = 1
+query_cache_size = 600000
+```
+
+MySQL命令行执行以下命令,也可开启查询缓存:
+
+```
+set global query_cache_type = 1;
+set global query_cache_size = 600000;
+```
+
+如上,开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。因此任何两个查询在任何字符上的不同都会导致缓存不会命中。此外,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL库中的系统表,其查询结果也不会被缓存。
+
+缓存建立之后,MySQL的查询缓存系统会跟踪查询中涉及的每个表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。
+
+缓存虽然能够提升数据库的查询性能,但是缓存也同时带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。因此,开启缓存查询慎重,尤其是对于写密集的应用。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十兆比较合适。此外,还可以通过SQL_CACHE和SQL_NO_CACHE来控制某个查询语句是否需要进行缓存。
+
+```
+select sql_no_cache count(*) from test_user;
+```
+
+## 5.1.5 binlog
+
+MySQL的binlog是用来做POINT-IN-TIME的恢复和主从复制的,由数据库上层生成,是SQL执行的逻辑日志,在事务提交完成后进行一次写入。
+
+binlog有三种格式:
+
+1. Statement
+
+ MySQL的默认binlog格式。每一条会修改数据的SQL都会记录到bin-log中,Slave在复制的时候SQL进程会解析成和原来Master端执行过的相同的SQL再次执行。
+
+ 此格式产生的日志量比较少,能够节省IO和存储资源,日志具有较高的可阅读性。但其需要在记录语句信息的同时也要记录语句执行时的上下文信息,以保证在Slave端重放时能够得到和Master端同样的结果。此外,并非所有的UPDATE语句都能够被复制,尤其是在包含不确定操作的时候,并且Slave的数据表必须和Master保持一致。
+
+1. Row
+
+ 日志中会记录成每一行数据被修改的形式,然后在Slave端再对相同的数据进行修改。其不需要记录语句执行的上下文信息,但是需要记录每一条记录的改动,因此在受影响的记录很多时,日志量会非常大;由于加密,日志的可阅读性较低。
+
+1. Mixed
+
+ Statement和Row的结合。会根据执行的每一条具体的SQL语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种。
+
+## 5.1.6 事务
+
+关系型数据库是需要遵循ACID规则的。
+
+- A (Atomici)原子性:即事务要么全部做完,要么全部都不做。只要其中一个操作失败,就认为事务失败,需要回滚。
+- C (Consistency) 一致性: 数据库要一直处于一致的状态
+- I (Isolation) 独立性:并发的事务之间不会互相影响
+- D (Durability) 持久性:一旦事务提交后,它所做的修改将会永久的保存在数据库上
+
+为了达到以上事务特性,数据库定义了几种事务隔离级别:
+
+1. 未授权读取(Read Uncommitted):会产生脏读,可以读取未提交的记录,实际情况下不会使用。
+1. 授权读取(Read Committed):会存在不可重复读以及幻读的现象。不可重复读重点在修改,即读取过的数据,两次读的值不一样;幻读则侧重于记录数目变化,多次执行同一个查询,返回的记录不完全相同。
+1. 可重复读取(Repeatable Read):解决了不可重复读的问题,会存在幻读现象。InnoDB使用mvcc+gap lock(innoDB行锁的一种)避免了幻读问题。
+1. 串行(Serializable):也称可串行读,此级别下读操作会隐式获取共享锁,保证不同事务间的互斥。消除了脏读、幻读,但事务并发度急剧下降。
+
+这里需要注意的是MySQL的默认事务级别为Repeatable Read,而JDBC的默认事务级别是Read Committed,因此使用的时候要特别注意。此外,由于Read Committed有不可重复读的问题,因此不能在Statement格式的binlog下使用,必须设置为Mixed或者Row。
+
+事务隔离的实现基于锁机制和并发调度。其中并发调度使用的是MVCC(多版本并发控制),通过保存修改行的旧版本的信息,来支持并发一致性读和回滚等特性。
+
+### 锁机制
+
+MySQL为了解决并发、数据安全的问题,使用了锁机制。
+
+可以按照锁的粒度把数据库锁分为行级锁和表级锁。
+
+1. 表级锁:是MySQL中锁定粒度最大的一种锁,对当前操作的整张表加锁,实现简单,资源消耗较少,加锁快,不会出现死锁。但锁定粒度大,触发锁冲突的概率最高,并发度最低。MyISAM和InnoDB引擎都支持表级锁。
+1. 行级锁:行级锁是MySQL中锁定粒度最细的一种锁,只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高;但加锁的开销也最大,加锁慢,会出现死锁。InnonDB支持行级锁,包括:
+
+ - Record Lock: 对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项。
+ - Gap Lock: 对索引项之间的'间隙'加锁,锁定记录的范围(对第一条记录前的间隙或最后一条记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据, 这样就防止了别的事务新增幻影行。
+ - Next-key Lock:锁定索引项本身和索引范围。即Record Lock和Gap Lock的结合,可解决幻读问题。
+
+虽然使用行锁具有粒度小、并发读高等有点,但是表级锁有时候也是有必要的:
+
+- 事务更新大表中的大部分数据直接使用表级锁效率会更高。
+- 事务比较复杂,使用行锁很可能引起死锁导致回滚。
+
+表级锁和行级锁可进一步划分为:排他锁和共享锁。如下:
+
+- 共享锁(S):又称为读锁,是读取操作创建的锁。其他用户可以读取数据,可以再加共享锁,读取到的数据也是同一版本的;但任何事务都不能获取数据上的排他锁,不能对数据进行修改。获取共享锁的事务只能读取数据不能修改数据。可以使用`SELECT ... LOCK IN SHARE MODE;`来强制获取共享锁,否则绝大部分查询操作是不会获取锁的(串行事务级别除外)。
+- 排他锁(X):又称写锁,一个事务对数据加上排他锁后,其他事务不能再对此数据加任何其他类型的锁。获取排他锁的事务既能读取数据也能修改数据。InnoDB对CUD(insert、update、delete)操作涉及的数据会默认加排他锁。对于查询语句可以使用`SELECT ... FOR UPDATE`加排他锁。
+
+InnoDB中还有两个表锁:
+
+- 意向共享锁(IS):表示事务准备给数据行加入共享锁,一个数据行加共享锁前必须先取得该表的IS锁
+- 意向排他锁(IX):表示事务准备给数据行加入排他锁,事务在一个数据行加排他锁前必须先取得该表的IX锁。
+
+这里的意向锁是表级锁,表示的是一种意向,仅仅表示事务正在读或写某一行记录,在真正进行加行锁时才会判断是否冲突。意向锁是InnoDB自动加的,不需要用户干预。
+
+InnoDB的锁机制兼容表如下:
+
+ 请求锁模式/当前锁模式 | X | IX | S | IS
+----|-----|------|---- | ----
+X | 冲突 | 冲突 | 冲突 | 冲突
+IX | 冲突 | 兼容 | 冲突 | 兼容
+S | 冲突 | 冲突 | 兼容 | 兼容
+IS | 冲突 | 兼容 | 兼容 | 兼容
+
+当一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之如果请求不兼容,则该事务就等待锁释放。
+
+需要注意的是InnoDB的行级锁是基于索引实现的,如果查询语句未命中任何索引,那么InnoDB则会使用表级锁。此外,InnoDB行级锁是针对索引加的锁,不是针对数据记录,因此即使是访问不同行的记录,如果使用到了相同的索引键仍然会出现锁冲突。还需要注意的是,如果使用`SELECT ... LOCK IN SHARE MODE;`或者`SELECT ... FOR UPDATE`来使用锁的时候,如果表没有定义任何索引,那么InnoDB会创建一个隐藏的聚簇索引并使用这个索引来加记录锁。
+
+此外,不同于MyISAM总是一次性获得所需的全部锁,InnoDB的锁是逐步获得的,当两个事务都需要获得对方持有的锁,导致双方都在等待,就产生了死锁。发生死锁后,InnoDB一般都可以检测到,并使一个事务释放锁回退,另一个则可以获取锁完成事务。我们可以采取以下的方式避免死锁:
+
+1. 通过表级锁来减少死锁产生的概率。
+1. 多个程序尽量约定以相同的顺序访问表(也是解决并发理论哲学家就餐问题的一种思路)。
+1. 同一个事务尽可能做到一次锁定所需要的所有资源。
+
+### 事务隔离案例
+
+从某一账户转账n元给另一个账户,数据库执行流程如下:
+
+- 先从数据库读取自己的账户金额
+
+ ```
+ select amount from user_account where id = 1;
+ ```
+
+- 判断余额是否大于n,如果小于n,则结束事务。大于n,那么更新余额记录。
+
+ ```
+ update user_account set amount=amount - n where id = 1;
+ ```
+
+- 向对方账户增加n
+
+ ```
+ update user_account set amount = amount + n where id = xx;
+ ```
+
+注意第一步,如果是上面这种使用方式,那么除了Serializable隔离级别,同时并发的两个事务,都可能会读取到相同的余额,而后面不管怎么做都会使得整体的逻辑是错误的。可以使用Serializable来解决,但是Serializable隔离级别一开始会给查询加共享锁,并发事务也会同时用有相同记录的共享锁,从而造成两个事务形成死锁,都不能进行后续的update操作。
+
+比较好的方式就是在第一步使用:
+
+```
+select amount from user_account where id = 1 for update;
+```
+
+此语句会直接给目标记录加排他锁,这样就防止出现并发事务读取到同样的余额数据造成业务错误。
+
+当然,在业务应用层面做并发控制是另一种思路。
+
+## 5.1.7 大表优化
+
+当MySQL单表记录数过大时,数据库的CRUD性能都会下降,需要一些优化措施:
+
+1. 限定数据的范围
+
+ 对于大数据量的数据表,全表扫描肯定是不可接受的。务必禁止不带任何限制数据范围条件的查询语句存在。比如:用户查询订单历史数据时,可以控制在最近一个月的数据中进行筛选。
+
+1. 读写分离
+
+ 经典的数据库拆分方案主库负责写,从库负责读。
+
+1. 缓存
+
+ - 使用MySQL的查询缓存
+ - 对重量级、更新少的数据考虑使用应用级别的缓存。
+
+1. 垂直分区
+
+ 根据数据库里面的数据表的相关性进行拆分。例如:用户表中既有用户的登录信息又有其基本信息,那么可以拆为两个单独的表做分表,甚至放到单独的库做分库。垂直分区的优点在于可以使得行数据变小,在查询时减少读取的Block的数,减少IO次数。此外,可以简化表的结构,易于维护。
+
+ 垂直分区的缺点在于主键会出现冗余,需要管理冗余列,并会引起join操作,可以通过在应用层进行join来解决。此外,垂直分区会让事务管理变得复杂。
+
+1. 水平分区
+
+ 水平分区指的是保持数据表结构不变,通过某种策略将数据分片存储。这样每片数据分散到不同的表或者库中,达到分布式的目的。可以支撑非常大的数据量。
+
+ 如果某个表的数据是时间序列的,比如订单、交易记录等,通常比较合适用时间范围分片,因为具有时效性的数据,我们往往关注其近期的数据,查询条件中往往带有时间字段进行过滤。可以使用跨度短的时间范围分片活跃数据,跨度长的范围分片历史数据。此外,订单表包含正在处理和已完成的订单,而正在处理的订单是频繁被访问查询的,已完成订单则相对来说很少被访问,那么订单状态也可以作为分片的因子,可使得频繁访问的处理中订单能够保证一个相对小的规模,从而提高处理速度。这里分离频繁和不频繁使用的数据也是大表优化的一个原则。
+
+ 需要注意的是,分表仅仅解决了单一表数据过大的问题,但由于表的数据还是在同一机器上,其实对于提升MySQL并发能力没有什么意义。所以水平分区最好就分库。
+
+ 水平分区能够支持非常大的数据量存储,应用端的改造也较少;但分片事务难以解决,跨节点join性能差,逻辑复杂。
+
+ MySQL 5.1之后提供的分区表也是水平分区,用户需要在建表的时候加上分区参数,对应用是透明的, 无需修改代码。
+
+ ```
+ CREATE TABLE `user_note` (
+ id BIGINT(20) NOT NULL,
+ uid BIGINT(20) NOT NULL,
+ content varchar(1024) NOT NULL,
+ create_time DATE NOT NULL
+ )
+ PARTITION BY RANGE( YEAR(create_time) ) (
+ PARTITION p0 VALUES LESS THAN (2013),
+ PARTITION p1 VALUES LESS THAN (2016),
+ PARTITION p4 VALUES LESS THAN MAXVALUE
+ );
+ ```
+
+ 除此之外,数据库分片主要包括两种分片方案:客户端代理和中间件代理。
+
+ - 客户端代理
+
+ 分片逻辑在应用端,封装在jar包中, 通过修改或者封装JDBC层来实现。当当网的sharding-jdbc(最新版已经更名为sharding-sphere,sharding-jdbc是其中的一个组件)、阿里的TDDL是目前比较为人所知的实现。
+
+ - 中间件代理
+
+ 在应用和数据中间加了一个代理层。分片的逻辑统一维护在中间件服务中。阿里的Cobar、360的Atlas、网易的DDB、开源的MyCat和Kingshard都是这种架构的实现。当当网最新的sharding-sphere也提供了sharding-proxy实现这一层的功能。
+
+ 笔者推荐如非特别必要,不要对数据进行分片,拆分会带来逻辑、部署、运维的各种复杂度,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,毕竟减少了一次和中间件的网络IO。而对于具有框架自研和运维能力的中大型公司,采用中间件代理则是更好的选择。
+
+## 5.1.8 高可用
+
+MySQL默认支持主从配置,可以一主一从,也可以一主多从。但是这个主从仅仅是实现了读写分离,并不能解决高可用的问题。常用的高可用方案如下:
+
+1. MHA
+
+ MHA,Master High Availability,是目前MySQL中一个相对成熟的高可用解决方案,在互联网公司中被经常使用。此种方案可以保障一主多从的高可用,管理结点会定时探测集群中的Master节点,当Master出现故障时,自动将最新数据的Slave提升为新的Master,然后将所有其他的Slave重新指向新的Master。整个故障转移过程对应用程序完全透明。架构如下图所示:
+
+ 
+
+1. MMM
+
+ MMM,Multi-Master Replication Manager, 是双主的架构方案,同时只有一个主允许写,另一个主允许读, 一个主挂掉,其下面的Slave同样挂掉。此方案无法严格保证数据一致性,适用于对数据一致性要求不高的业务场景。架构如下图所示:
+
+ 
+
+上面水平分区中讲的中间件代理基本也都是采用这两种方案或者类似方案做的高可用保证。
+
+除了上述两种方案,还有双主配SAN存储、双主配DRBD、NDB CLUSTER、镜像等高可用方案由于成本、复杂性等原因并未被广泛应用。此外,orchestrator(https://github.com/github/orchestrator)则是目前更为先进的一种MySQL高可用工具,其基于Raft协议实现选主,如果是刚开始用可以直接选择此方案。
+
+此外,在主从模式下,经常遇到的一个问题是Slave数据滞后于Master,写入Master后从Slave读取不到最新的数据。可以使用MySQL 5.5后引入的半同步复制机制,使得MySQL客户端请求时阻塞一直到数据至少已经同步到一个Slave(只是接收到binlog,并没有完成提交)或者超时,如此可以在一定程度上解决数据一致性的问题。
+
+## 5.1.9 使用提示
+
+### MySQL使用
+
+MySQL本身的使用和配置有以下注意点:
+
+- MySQL默认的连接数是100,调整MySQL的最大连接数能够一定程度上提高并发能力。
+- 使用慢查询日志监测导致数据库响应缓慢的SQL语句。
+- MySQL有一个常见的连接缓慢原因,就是会根据域名做反向DNS查询。可在mysqld中加入skip-name-resolve选项解决(同时需要在MySQL用户权限中把root@127.0.0.1的都配置上),还可以通过在hosts中加入本机域名的映射来解决。
+- 在MySQL命令行中命令以\G结束,改变结果显示方式为列。
+- 通过提升CPU和内存、使用SSD,能显著提升MySQL性能。
+- 可以使用PT工具箱(Percona Toolkit)对MySQL进行管理,包括检查主从复制的数据一致性、检查重复索引、在线DDL、定位IO占用高的表文件等。
+- MySQL 5.6在内核层支持了Online DDL,使得在线加列、加索引都已不会阻塞业务,但其仍然会使主从复制有延迟。能够进一步解决在线加列问题的方案有:
+ - 冗余字段,即开始设计表的时候就预留出字段。
+ - 加列时先在从机添加,然后再切换主从。
+ - 修改数据字典表,新列并不真正在物理上添加,只在更新时做填充数据操作。腾讯互娱数据库团队的TMySQL基于此原理做了实现。
+ - 使用MySQL5.7新增的JSON类型,添加相应的key即能够达到加列的效果。
+- MySQL的性能并没有很多人想象的那么弱,在优化得当的情况下,一般单数据库是能支撑住一万以内的QPS的。
+
+### DDL
+
+在设计数据库的时候有以下注意点和技巧:
+
+- 禁用存储过程、函数、触发器、外键约束,尽量依赖于业务层面做,能够具有良好的可扩展性。
+- 允许为null的列,在非查询时会无法匹配,如where status != 'FINISH',那么status为null的行也无法匹配。
+- 使用枚举或整数类型代替字符串类型。
+- ID使用BIGINT即可,对应于Java中的long类型,足够使用。
+- 对整数类型指定宽度,比如INT(11)、BIGINT(20),并不影响存储大小,int依然使用32位(4个字节)存储空间,bigint依然使用64位(8个字节)存储空间,宽度仅表示显示的长度。
+- 避免使用DECIMAL和浮点数数据类型,可以使用BIGINT代替(浮点数乘以一个倍数)。
+- 数据库中的表最好带有创建和更新时间戳,及所创建/修改行的用户标示,以审计跟踪数据的变动。
+- 不要对数据真的进行删除,可以给它打上一个被删除的标记或者做版本化修改。
+- 单表不要有太多字段,建议在15以内。
+- 用整形而不是字符串存储IP。
+
+### DML
+
+在数据查询的时候有以下注意点和技巧:
+
+- 在查询语句中不要使用全属性选择器*。
+- 尽量避免使用负向查询(!和<>)和or查询,使用in代替。in的查询效率是log(n)。
+- 对于连续条件的查询,使用between而不是in。
+- 模糊匹配查询时,当为后缀模糊查询时能够使用索引,如like 'a%',而前缀则不行。
+- 查询记录数目时,使用count(1)和count(*)是等价的,在有索引的时候都会去选择合适的索引,没有索引的时候则全表扫描。而count(column)则是统计columne不为null的记录数目。
+- 避免多于两表join,尽量使用冗余策略解决联表问题。
+- 避免列运算,如select * where age + 1 > 100;
+- 批量插入代替循环单条插入,能够减少网络IO带来的开销。
+- 一个很耗时的SQL会堵死整个库,可以拆开多个小的语句进行,以减少锁的时间。
+- 使用UNION时,除非确实需要服务器去重,否则就一定要使用UNION ALL。否则MySQL会给临时表加上DISTINCT选项导致整个临时表的数据做唯一性检查,影响查询性能。
+- 插入一条记录后,如果这张表的主键是自增的,推荐使用SELECT LAST_INSERT_ID()来获取这个自增值,LAST_INSERT_ID是基于Connection的,只要Connection对象不变,获取到到自增id就是正确的,也不需要加锁。此外,在JDBC中,构建statement时,参数autoGeneratedKeys传入Statement.RETURN_GENERATED_KEYS,执行语句后,可以通过statement.getGeneratedKeys()来获取自增id。
+- 在某一数据列为bit类型。那么在用MySQL命令行查询的时候是无法看到其值的。需要如下查询:
+
+ ```
+ select bin(bit_column + 0) from test_user; //显示二进制
+
+ select bit_column + 0 from test_user;//显示十进制
+ ```
+
+更多的MySQL常用命令可以见附录C。
+
+此外,Java中对MySQL的数据操作可以使用3.2种讲述的ORM框架,也可以使用4.2.1中讲述的Spring JDBC。
+
+### 数据库连接池
+
+在Java应用中使用关系型数据库时,会使用数据库连接池以避免频繁创建、销毁连接带来的性能开销。市面上有很多数据库连接池,主流的有下面几个:
+
+- HikariCP: 是BoneCP的作者推出的BoneCP的替代品,号称有了质的变化,革命性的变更。
+- Druid: 阿里巴巴开源的数据库连接池,还提供了一些配套监控工具、统计功能和Web界面,性能也较好。
+- DBCP:老牌的数据库连接池,现在到了DBCP2,是基于commons-pool之上的封装。
+- C3P0:是与Hibernate一起发布的开源数据库连接池。
+
+表格综合对比如下:
+
+. | 是否支持PSCache | 监控 | 扩展性 | sql拦截及解析 | 代码 |特点
+----|-----|------|---- | ----| ---| ---
+HikariCP | 否| JMX | 弱 | 无 | 简单 | 功能简单,性能好,起源于BoneCP
+Druid | 是 | JMX/Log/HTTP | 好 | 支持 | 中等 | 功能全面,方便对JDBC接口进行监控跟踪
+DBCP | 是 | JMX | 弱 | 无 | 简单 | 依赖于commons-pool
+C3P0 | 是 | JMX/Log | 弱 | 无 | 复杂 | 历史久远,代码逻辑复杂,且不易维护
+
+上表中的PSCache指的prepareStatement缓存,是connection私有的,key为prepare执行的SQL和Catalog等,value对应的为prepareStatement,支持此特性可以减少解析SQL的开销,对性能会有大概20%的提升。
+
+综上,如果对监控、SQL统计有需求,推荐使用Druid;如果特别关注性能,那么推荐使用HikariCP。
+
diff --git a/book/chapter5-datastore/search.md b/book/chapter5-datastore/search.md
new file mode 100644
index 0000000..43682b9
--- /dev/null
+++ b/book/chapter5-datastore/search.md
@@ -0,0 +1,259 @@
+# 5.4 搜索引擎
+
+前几节讲过的MySQL和MongoDB都有全文检索的功能,但如果真的需要全文检索的需求,搜索引擎才是最好的方案。目前Java开发中最常用的搜索引擎是Solr和Elasticsearch。
+
+其中,Solr是Apache旗下的开源搜索引擎,基于Lucene开发。支持层面搜索、命中醒目显示和多种输出格式,包括 XML/XSLT和 JSON 格式,是之前用的比较广泛的搜索引擎。但近几年Elasticsearch已经开始逐步取代Solr,正逐渐成为主流的搜索引擎选型。因此,本节主要针对Elasticsearch讲述。
+
+Elasticsearch是一个分布式、RESTful的搜索以及分析服务器,其和Solr一样也是基于Lucence的。其主要优点如下:
+
+1. 轻量级,下载后一条命令即可启动。
+2. 可以存储任意结构的JSON对象,是无模式的。
+3. 可以使用不同的index参数创建不同的索引文件,实现多索引文件支持。
+4. 天然具有分布式特性,可以自动发现ES结点。
+5. 不仅仅是搜索引擎,还有数据分析、聚合、可视化等特性。
+6. 可以把ES直接当做NoSQL数据库使用,存储JSON文档。
+7. 为各种操作都提供了RESTFul接口。
+8. 有强大的聚合查询功能。
+
+Elasticsearch目前已经到了5.x版本,但由于其改动较大,目前用的还不太广泛。本节使用的版本为2.3.3。
+
+## 5.4.1 Apache Lucene
+
+Apache Lucene是一个Java开源全文检索库,是很多搜索引擎的检索实现机制。其中的几个关键概念如下:
+
+- 文档:document, 索引与搜索的主要数据载体
+- 字段:field, 文档的一个片段
+- 词项:term, 搜索时的一个单位,一般为一个词
+- 词条:token,词项在字段中的一次出现
+
+Lucene将索引信息组织为词典(term dictionary)。每一个条目由词项和此词出现过的文档集合组成,这样就能够通过词反向查询文档信息。如下:
+
+词项 | 文档
+----|-----
+篮球 | [1,2]
+足球 | [1,4,5]
+乒乓球 | [7]
+
+为了加快在词典中查找词项的速度,Lucene对词典做前缀索引,版本4.0后使用FST(finite state transducers)这个数据结构保存在内存中,会在每次打开索引时全量装载到内存中。
+
+
+
+上图即一个FST(通过http://examples.mikemccandless.com/fst.py此网址生成),是一个有向无环图,依次插入的单词为baskeball、football、pingpong,之后可以很方便的查询这些单词。其用途和Hashmap、Tries树基本相同,但是FST非常省内存。
+
+Lucene中的索引由多个段segment组成,每个段只被创建一次但被查询多次。索引期间,段不可被修改。因此,文档的删除也仅仅是把信息保存在一个单独的文件中,段本身并没有改变。Lucene会对多个段进行合并,称为segments merge。目的是为了减少segment的数目,把小的segment合并为大的segment,减少搜索的segment数。此外,merge也会把被删除的信息清理掉。合并可以强制执行,也可以由Lucene的内在机制自动执行。但这个合并的过程非常消耗IO,不建议强制执行。
+
+倒排索引中的key是文档中的词,这些词都是通过分析器来对文档进行分析从而得到的。分析器包括:分词器、过滤器和字符映射器。
+
+- 分词器:将文本切割成词条,输出词条流
+- 过滤器:用来处理词条流中的词条,移除、修改词条
+- 字符映射器:做文本预处理工作,比如去除文本的HTML标签。
+
+Lucene提供了一套查询语言用于对文档在字段中进行查询,支持通配符查询和模糊查询。
+
+基于Lucene提供的这些强大的全文索引功能,Solr、Elasticsearch在其上封装出了独立的服务,增加了分布式、容错、分词器实现、数据持久化等等特性和组件,就形成了搜索引擎。
+
+## 5.4.2 关键概念
+
+1. 节点、集群
+
+ 每一个运行的ES服务实例叫做节点(Node)。多个ES实例以集群(Cluster)方式运行,以一个整体对外提供服务,能够容错并提供巨大的数据量支持。ES天然支持集群,配置非常简单。
+
+1. 索引
+
+ 索引(idnex)是ES存储数据的地方,类比于关系数据库的database,是被索引文档的集合。可以向索引写入/读取文档。此外,ES提供了索引的别名(Alias)机制,一个索引可以有多个别名,一个别名可以对应多个索引。
+
+1. 文档
+
+ 文档(document)是ES中的主要实体,没有固定的模式,可以看作为JSON对象,类比于关系型数据库的Row。所有的搜索最终都是对文档的搜索,文档由字段构成,每个字段有一个名字和一个或者多个字段值。
+
+1. 分片
+
+ Shard,是ES提供分布式搜索的基础。ES将一个完整的Index分成若干部分存储在相同或者不同的节点上,组成Index的部分即Shards,每一个Shard都是一个Lucence实例。每个Shard都有一个编号,Doc落入哪个Shard默认与文档的ID有关,也可以通过配置路由(route)来自定义。
+
+1. 副本
+
+ ES会为索引的每一个Shard创建冗余副本(Replica),用来提高系统的容错性,并可以自动对搜索请求进行负载均衡,提高ES的查询效率。ES支持在任意时间点添加或移除副本,可随时调整副本数量。
+
+1. Recovery
+
+ ES在有节点加入或退出时会根据机器的负载对索引分片进行重新分配,挂掉的节点重新启动时也会进行数据恢复。
+
+1. 网关
+
+ ES默认先把索引存放到内存中,当内存满了时再持久化到本地硬盘。Gateway对索引快照进行存储,当这个ES集群关闭再重新启动时就会从Gateway中读取索引备份数据, 此外还存储了集群状态、索引设置的各种信息。
+
+1. Zen Discovery
+
+ ES的自动发现节点机制,也可以叫做共识系统。ES是一个基于p2p的系统,它先通过广播寻找存在的节点,再通过多播协议来进行节点之间的通信,同时也支持点对点的交互,并进行Master的选举。
+
+与MySQL的相关概念类比如下:
+
+MySQL | Elasticsearch
+----|-----
+Database | Index
+Table | Type
+Row | Document
+Column | Field
+Schema | Mapping(Type的字段处理规则:是否分词,如何分词,是否压缩等)
+Index | Everything is indexed
+SQL | Query DSL
+INSERT into | POST http://
+DELETE from | DELETE http://
+UPDATE table SET ... | PUT http://
+SELECT * from | GET http://
+group by、avg、sum | Aggregations
+distinct | cardinality
+数据迁移 | reindex操作
+
+## 5.4.3 查询优化
+
+ES中的结点都是对等的(从用户角度来看,Master节点和其他节点并没有什么不同,所有结点都有同样的Cluster State信息),因此索引请求可以随便发到哪个结点上,但之后此结点成为coordinating node经过信息查询后,都把请求分发到包含对应编号主Shard的Node上执行操作,操作成功后再在备Shard上执行操作, 最后coordinating node结点等所有结果返回,合并结果返回给请求客户端。
+
+ES的更新和查询的底层过程如下:
+
+- 当索引增加、更新时,请求的结果都先存入indexing buffer(不能被搜索到)。当buffer满了或者到了时间周期,就会将buffer中的内容写入到磁盘上的segment(对Lucene index reader调用了reopen,并没有fsync保证持久化),开始可以被搜索到。
+- 当有删除请求到来时,被删除的文档ID会记录到一个单独的文件中。
+- 每次查询的时候,会扫描内存(缓存doc)、硬盘中的Doc,然后过滤掉deleted文件中的Doc。
+
+依照上述过程indexing buffer生成segment默认的周期是1秒,使得被索引的文档可以被近实时地搜索到,达到准实时读取。但如果有意外,有可能会造成数据丢失。ES提供了translog来保证一定程度的数据完整性,每一个Shard都会有自己的translog。任一索引和删除操作在被Lucenece处理后都会写入translog。而为了防止translog过大,会有一个flush过程,此过程会触发Lucence的commit将内存中的文档存入磁盘并开始新的translog, 非常耗费时间和资源,因此控制flush发生的时机是影响索引的关键点之一。相关配置参数如下:
+
+- index.translog.flush_threshold_size:translog一旦达到此值就会进行一次flush,可以调大此值甚至关闭,手动进行translog flush。
+- index.translog.flush_threshold_ops:translog数据达到多少时进行一次flush,可以调大此值甚至关闭,手动进行translog flush。
+- index.translog.flush_threshold_period: 距离上一次flush的时间间隔阈值。
+
+在打开的索引中,只要以上满足任意一条tranlog不为空即可触发flush。此外,还有两个参数也关系着ES的性能:
+
+- index.translog.interval: translog的提交周期,默认为5s
+- index.refresh_interval: 指的多久进行一次数据刷新使得索引可以被使用。可以设置为-1关闭,手动进行refresh。
+
+还有以下因素也会影响索引质量,从而影响检索速度:
+
+1. 分片数目
+
+ 分片数目过多会导致检索时打开的文件较多、多台服务器之间通讯,过少则单个分片索引过大,检索速度慢。分片数根据数据总量/单片的数据数目来计算,单片的数据数目需要通过单节点单索引来进行测试,一般不超过10G即可。
+
+ ```
+ PUT /my_index
+ {
+ "settings": {
+ "number_of_shards" : 1
+ }
+ }
+ ```
+
+1. 副本数
+
+ 如果有副本存在,那么索引过程会同时同步到副本中。在对索引的安全性没有那么高的要求的情况下,可以在索引过程中将副本数设置为0,待索引完成后再改回去,可以在一定程度上提高索引效率。
+
+ ```
+ PUT /my_index
+ {
+ "settings": {
+ "number_of_replicas" : 0
+ }
+ }
+ ```
+
+1. 分词
+
+ 分词选择的词库要适量,并非越多越好,词库越多,词表越大那么分的词就会变多,从而索引也会变大。根据业务场景选择和特征相关的词库,能够使得词表变下,索引大小也会减少许多。
+
+1. 索引段
+
+ 每次搜索请求,会搜素所有的段,因此segment的数目过多会导致搜索时读取的文件数过多,影响搜索效率。虽然有对segment自动做合并的机制,但可能的情况下把segment的数目设置为1可以更好地提高检索速度。
+
+ ```
+ curl -X POST 'http://localhost:9200/my_index/_forcemerge?max_num_segments=1'
+ ```
+
+ 上述命令即可将索引的段合并为1个。
+
+## 5.4.4 内存优化
+
+ES是一个Java应用,因此内存和GC是影响ES性能的关键因素。
+
+ES的倒排索引是先在内存中生成,然后定期以segment file的形式刷到磁盘中的。这样每个segment都会有一些索引数据存储在heap里,如:词项索引。segment越多,占用的heap也越多且无法gc,因此ES的数据存储并非仅仅消耗磁盘空间,当segment memory占用过多时就需要考虑删除、归档数据或者扩容。
+
+官方建议设置的heap size不要超过系统可用内存的一半,heap以外的内存操作系统会用来cache数据。JVM的xms和xmx也要设置为和heap一样大,避免动态扩展。
+
+为了减少segment memory的占用,有以下方法:
+
+1. 删除不用的索引。
+2. 定期对不再更新的索引做force merge,即对segment file强制做合并可以节省大量内存占用。
+
+此外,为了防止内存频繁swap影响ES性能,有以下两种方式:
+
+- 关闭系统的swap功能`sudo swapoff -a`,但由于会关闭Linux系统的swap功能,所以最好结合机器的情况来选定是否执行此配置。
+- 配置bootstrap.mlockall: trure,让JVM锁住内存,同时需要运行Elasticsearch的Linux用户有锁定内存区域的权限。
+
+ ES的启动用户esUser,修改/etc/security/limits.conf文件
+
+ ```
+ esUser soft memlock unlimited
+ esUser hard memlock unlimited
+ ```
+
+## 5.4.5 集群
+
+5.4.2中讲述了集群的概念,具体的一个ES集群中的节点分为以下几种:
+
+- Master:负责轻量级的管理集群的状态的工作,当群集的拓扑结构改变时把索引分片分派到相应的节点上。节点配置node.master:true, 作为Master候选节点。为了防止在网络分区时发生脑裂现象,提供了minimum_master_nodes参数限制能够成为Master的最小跟随Master候选节点数。一般建议设置为N/2+1,N为候选Master节点总数。
+- Data:是数据的承载者,对索引的数据存储、查询、聚合等操作提供支持。节点配置node.data:true,成为Data节点。
+- Client: 节点配置node.master:false node.data:false,如此节点仅仅作为“路由结点”,负责转发请求给Master或者Data。
+- Tribe: 通过tribe.*配置,是一个特殊的Client结点,用来跨集群的请求路由和协调。
+
+ES的节点默认兼具Master候选和Data功能。官方建议,小集群下如此使用比较方便,当集群越来越大,应该分离这些节点的用途,做不同的针对配置。
+
+- 单独的Master节点CPU、内存消耗都很低,配置一般即可。
+- 单独的数据节点是主要工作所在,需要较高的CPU、内存和IO配置。
+- 单独的Client节点只需要做路由和结果聚合,对CPU和内存要求较高。此外需要注意,虽然Client节点数目越多越能分担Master和Data节点作为协调节点的功能,但过多会使得Master在同步state时等待变长。
+
+除了节点,集群还有一个重要的概念就是Shard以及其副本,此部分在5.4.2中已经讲述。
+
+以上最终形成ES的集群。集群的健康状况可以通过_cat/health API来查询,有以下几种状态:
+
+- green:集群一切都可用。
+- yellow:所有主要分片可用,但不是所有复制分片都可用。此时能够正常对外服务。
+- red:部分数据不可用。此时,集群是能够提供部分服务的,会在现有存活分片中执行请求。需要尽快修复故障分片,防止数据的丢失。
+
+## 5.4.6 ELK
+
+ELK, 即Elasticsearch + Logstash + Kibana, 是一套开源的日志管理方案,可以构建日志集中分析平台。其一个一般的架构如下图所示:
+
+
+
+- Logstash:多个独立的agent负责收集不同来源的数据,一个中心agent负责汇总和分析数据。
+- Elasticsearch:用于存储最终的数据,并提供搜索功能。
+- Kibana:提供一个简单、丰富的web界面,数据来自于Elasticsearch,支持各种查询、统计和展示。
+
+还可以在远程Logstash和中心Logstash之间加入Redis、Kafka等中间代理层作为缓冲和中间存储,提高系统性能和可靠性。
+
+## 使用提示
+
+1. ES性能体现在分布式上,一个生产系统建议至少三个节点以上。
+1. ES不是一个实时的存储服务(索引文件不变,任意时刻的数据是快照数据),不要用在实时业务场景中。如果真的需要实时,则需要使用get操作,并且指定realtime为true。
+1. ES字段是否索引只能在创建索引时配置,不能在字段创建后再给字段“加索引”。
+1. 索引字段有“索引(indexed)”和“存储(stored)”两个属性,只有被“索引”的字段才能在查询/排序条件中使用,只有被“存储”的字段才能在请求的时候返回字段内容。其中必须保证索引字段都存储(stored)才能使用update操作,update原理是先从索引中get到原文档内容,然后与传入的欲更新字段合并,作为一个新的文档index回去,如果有字段不是stored,那么update之后该字段就丢失了。
+1. 倒排词典的索引是常驻内存的,需要监控数据节点上segment memory的使用情况。
+1. 线上业务最好根据自身应用场景开启索引的慢查询日志。
+1. 要定期删除不再使用的索引并做好冷数据的迁移。
+1. 如果不使用_all字段,那么关闭掉这个属性,否则在创建索引和增大索引大小的时候会使用额外更多的CPU。
+1. 可以不索引的字段就不索引(indexed: no),可以减小倒排索引文件,提高读写性能。
+1. 搜索用不到的字段不要写入ES。如果需要这些字段的信息,可以使用Redis、HBase这种key->value查询非常高效的数据中间件存储这些信息。查询时先根据条件从ES检索,再根据ID去另外的数据中间件里获取完整的信息。如此,能够减少对磁盘缓存的占用,从而提升ES查询效率。
+1. 善用bool query,类似于之前版本的filtered query,可以在进行复杂的倒排算法之前先减少计算空间。
+1. 使用批量查询和批量读取减少网络IO。
+1. 可以使用多个别名命名同一个index,然后针对不同的别名做不同的操作权限控制。
+1. ES官方推荐内存配置不要超过系统可用内存的一半,并且不要超过32GB。这里的32GB限制是为了能够开启JVM的压缩指针。
+1. ES的Java客户端有两种类型:NodeClient是集群中的一个结点,会同步路由等信息,影响启动速度;TransportClient,仅仅做为一个客户端,启动速度比较快,但是其不知道集群的任何信息,因此查询时需要先发送请求到某个节点,然后再由ES转发到文档所在的节点做处理,查询需要消耗更多资源。
+1. 避免返回大量结果集的搜索和聚合。可以采用scan和scroll api来补充缺失数据。
+1. 使用ES默认的from+size方式做分页查询时分页数量越多性能显著下降,可以使用离线读取索引快照(一次查询请求后维护一个快照的搜索上下文,无法满足实时查询场景)的scroll方式或者5.x之后的searchAfter方法(无法随机跳转)来提升分页查询的性能。
+1. 根据字段的取值来设置字段类型,减少索引文件的overhead,如小于7个枚举值可以用byte。
+1. 避免在ES中存储大字段,大字段会在段文件中保存,会影响读写性能。
+1. ES设置的mapping对存储内容无效,只是在建索引时用于类型检查/转换。存储内容,存的是json,返回的数据格式是json反序列化时自动推测的,不会按照预置的mapping字段类型返回。
+1. 将mapping中的dynamic设置为strict,在出现未配置的字段时抛出异常,避免因为字段自动映射错误而导致重建索引(mapping默认关闭了自动映射功能,避免第一次推断的类型错误)。
+2. 避免使用match操作,match操作不走缓存,每次都会计算,非常耗CPU。
+
+
+
+
+
diff --git a/book/chapter5-datastore/zk.md b/book/chapter5-datastore/zk.md
new file mode 100644
index 0000000..e057923
--- /dev/null
+++ b/book/chapter5-datastore/zk.md
@@ -0,0 +1,16 @@
+# 5.5 Zookeeper
+
+ZooKeeper是Hadoop的子项目,是一个服务协调服务,旨在解决大规模分布式应用场景下服务协调同步的问题。它可以为同在一个分布式系统中的其他服务提供:分布式锁服务、命名服务、配置管理、集群管理等功能。其是CP特性的分布式系统,还经常被用做微服务治理的注册中心(Dubbo+Zookeeper)。
+
+
+## 客户端
+
+- Zookeepre Client
+- Curator
+
+
+## 使用
+
+- ZK一般用来做临时数据存储介质,即时丢失也不影响业务运行,尽量不要类似数据库那样用来做持久化存储。
+- 使用zookeeper时,尽量避免大量节点监控一个节点的行为: [羊群效应](https://blog.csdn.net/wk022/article/details/88129479)。
+
diff --git a/book/chapter6-datatrans/README.md b/book/chapter6-datatrans/README.md
new file mode 100644
index 0000000..60b7c8d
--- /dev/null
+++ b/book/chapter6-datatrans/README.md
@@ -0,0 +1,15 @@
+# 第六章 数据通信
+
+数据存储在各个系统中,系统虽然大部分功能都是自身实现的,但是很多时候也需要依赖第三方服务、提供服务给第三方和客户端、前端。这时候就需要数据通信,即如何将数据从一个系统转移到另一个系统。
+
+需要注意的是,这里讲的数据通信指的主要是系统之间的数据通信。因此,从底层介质来讲,基本都是基于网络进行的。而网络传输则主要通过TCP和HTTP两种协议。
+
+其中,TCP是比较底层的传输协议,基于此协议需要自己做很多开发工作。而HTTP是TCP之上的应用协议,基于HTTP的数据传输机制实现较容易,但无法针对特殊场景做底层的优化,性能上不如TCP协议的数据传输机制。
+
+基于以上协议,目前常用的系统间数据传输方案主要包括以下几种:
+
+- RESTful:符合REST(Representational State Transfer)架构风格的设计,主要指API的设计。
+- RPC: 远程过程调用,可以基于TCP协议,也可以基于HTTP协议。
+- 消息中间件:利用消息队列,做为数据传输的介质,基本上都是基于TCP协议。
+
+
diff --git a/book/chapter6-datatrans/end.md b/book/chapter6-datatrans/end.md
new file mode 100644
index 0000000..ae0eec6
--- /dev/null
+++ b/book/chapter6-datatrans/end.md
@@ -0,0 +1,39 @@
+# 总结
+
+## 学习资料
+
+### JMS
+
+最为经典,也比较简单的一个消息中间件规范,ActiveMQ是其一个实现。但由于自身的一些局限,不再推荐使用。
+
++ 大规模分布式消息中间件简介:
++ JMS Overview:
++ Basic JMS API Concepts:
++ The JMS API Programming Model:
++ Creating Robust JMS Applications:
++ Using the JMS API in Java EE Applications:
++ Further Information about JMS:
+
+### RabbitMQ
+
+RabbitMQ是AMQP(The Advanced Message Queuing Protocol)协议的实现。适用于需要事务管理、对消息丢失很敏感的应用场景。对比kafka来看,RabbitMQ更为强调消息的可靠性、事务等。通过阅读官方文档学习即可:[官方文档](http://www.rabbitmq.com/documentation.html)
+
+### Kafka
+
+基于日志的消息队列,首推当然是官方文档:
+
+- [kafka中文教程](http://www.orchome.com/kafka/index):比较不错的中文教程
+
+ 学习内容:
+
+ + 开始学习kafka
+ + 入门
+ + 接口
+ + 配置
+ + 设计
+ + 实现
+ + 什么是kafka
+ + 什么场景下使用kafka
+
+- [kafka-study](https://github.com/superhj1987/kafka-study): 笔者在学习kafka时的一些笔记
+
diff --git a/book/chapter6-datatrans/media/amqp.png b/book/chapter6-datatrans/media/amqp.png
new file mode 100644
index 0000000..9ba6436
Binary files /dev/null and b/book/chapter6-datatrans/media/amqp.png differ
diff --git a/book/chapter6-datatrans/media/disruptor.png b/book/chapter6-datatrans/media/disruptor.png
new file mode 100644
index 0000000..b2b445e
Binary files /dev/null and b/book/chapter6-datatrans/media/disruptor.png differ
diff --git a/book/chapter6-datatrans/media/dubbo.png b/book/chapter6-datatrans/media/dubbo.png
new file mode 100644
index 0000000..2ec066f
Binary files /dev/null and b/book/chapter6-datatrans/media/dubbo.png differ
diff --git a/book/chapter6-datatrans/media/hessian.png b/book/chapter6-datatrans/media/hessian.png
new file mode 100644
index 0000000..dd6d33d
Binary files /dev/null and b/book/chapter6-datatrans/media/hessian.png differ
diff --git a/book/chapter6-datatrans/media/kafka-rabbit.png b/book/chapter6-datatrans/media/kafka-rabbit.png
new file mode 100644
index 0000000..ff8c079
Binary files /dev/null and b/book/chapter6-datatrans/media/kafka-rabbit.png differ
diff --git a/book/chapter6-datatrans/media/kafka.png b/book/chapter6-datatrans/media/kafka.png
new file mode 100644
index 0000000..ebab48a
Binary files /dev/null and b/book/chapter6-datatrans/media/kafka.png differ
diff --git a/book/chapter6-datatrans/media/msg-pub.png b/book/chapter6-datatrans/media/msg-pub.png
new file mode 100644
index 0000000..59519d2
Binary files /dev/null and b/book/chapter6-datatrans/media/msg-pub.png differ
diff --git a/book/chapter6-datatrans/media/msg-queue.png b/book/chapter6-datatrans/media/msg-queue.png
new file mode 100644
index 0000000..98695c6
Binary files /dev/null and b/book/chapter6-datatrans/media/msg-queue.png differ
diff --git a/book/chapter6-datatrans/media/oauth.png b/book/chapter6-datatrans/media/oauth.png
new file mode 100644
index 0000000..c98c527
Binary files /dev/null and b/book/chapter6-datatrans/media/oauth.png differ
diff --git a/book/chapter6-datatrans/media/rabbitmq.png b/book/chapter6-datatrans/media/rabbitmq.png
new file mode 100644
index 0000000..09f7241
Binary files /dev/null and b/book/chapter6-datatrans/media/rabbitmq.png differ
diff --git a/book/chapter6-datatrans/media/restful.png b/book/chapter6-datatrans/media/restful.png
new file mode 100644
index 0000000..c51b7a7
Binary files /dev/null and b/book/chapter6-datatrans/media/restful.png differ
diff --git a/book/chapter6-datatrans/media/rmi.png b/book/chapter6-datatrans/media/rmi.png
new file mode 100644
index 0000000..a0ae430
Binary files /dev/null and b/book/chapter6-datatrans/media/rmi.png differ
diff --git a/book/chapter6-datatrans/media/rpc.png b/book/chapter6-datatrans/media/rpc.png
new file mode 100644
index 0000000..d002be8
Binary files /dev/null and b/book/chapter6-datatrans/media/rpc.png differ
diff --git a/book/chapter6-datatrans/media/thrift.png b/book/chapter6-datatrans/media/thrift.png
new file mode 100644
index 0000000..195e8c9
Binary files /dev/null and b/book/chapter6-datatrans/media/thrift.png differ
diff --git a/book/chapter6-datatrans/message.md b/book/chapter6-datatrans/message.md
new file mode 100644
index 0000000..184cecb
--- /dev/null
+++ b/book/chapter6-datatrans/message.md
@@ -0,0 +1,635 @@
+# 6.3 消息中间件
+
+消息中间件,也可以叫做中央消息队列(区别于本地消息队列),是一种独立的队列系统。经常用来解决内部服务之间的异步调用问题。调用方把请求放到队列中即可返回,然后等待服务提供方去队列中去获取请求进行处理,然后通过回调等机制把结果返回给调用方即可。
+
+异步调用就是消息中间件一个非常常见的应用场景。此外,常用的消息队列的应用场景还有以下几个:
+
+* 解耦: 一个业务的非核心流程需要依赖其他系统,但结果并不重要,有通知即可。
+* 最终一致性: 指的是两个系统的状态保持一致,可以有一定的延迟,只要最终达到一致性即可。经常用在解决分布式事务上。
+* 广播: 这是消息队列最基本的功能。生产者只需要发布消息,订阅者都会受到消息。
+* 错峰与流控: 当上下游系统处理能力不同的时候就需要类似消息队列的方式做为缓冲区来隔开两个系统。
+
+其实,这里消息队列的概念和操作系统中进程间通信方式的消息队列本质是相同的。消息队列有生产者和消费者两种角色,前者生产消息,后者消费消息。
+
+对于生产者生产消息、消费者消费消息,大体有两种消息模型:队列和发布订阅。
+
+- 队列模型中:一个由消费者组成的池从服务器读取消息,每一个消息都可以达到其中的某一个消费者。
+
+ 
+
+- 发布-订阅模型中,消息被广播到所有消费者中,所有订阅了某个topic(主题)的消费者都能够拿到消息。
+
+ 
+
+目前主流的消息中间件,有以下几个:
+
+* ActiveMQ
+* RabbitMQ
+* Kafka
+
+此外,ZeroMQ也经常会被提到的消息队列,不过其本质是一个网络编程的Pattern库,将常见的网络请求形式(分组管理,链接管理,发布订阅等)模式化、组件化,位于socket之上、MQ之下。对于MQ来说,网络传输只是它的一部分,更多需要处理的是消息存储、路由、Broker服务发现和查找、事务、消费模式(ack、重投等)、集群服务等。
+
+还需要提到的是消息中间件对于生产和消费的传输保障一般有三种语义支持:
+
+- At most once,至多一次,消息可能丢失,但绝不会重复传输;
+- At least once,至少一次,消息绝不会丢,但是可能会重复;
+- Exactly once,精确一次,每条消息肯定会被传输一次且仅一次。
+
+对于大多数消息中间件而言,一般只提供At most once和At least once两种传输保障。如果想达到第三种则需要下游的业务进行配合,由业务做去重处理或者业务消息本身就具有幂等性,例如:以唯一ID作为标识设置一个去重表。
+
+## 6.3.1 简单消息中间件-ActiveMQ
+
+对于ActiveMQ,需要先讲述JMS规范。
+
+JMS(Java Message Service)是一个简单的消息中间件规范,专用于JavaEE。它主要做了接口上的规范(定义了API接口)以及消息传输模型和消息类型的规范。但其并没有对这些给予实现,也完全没有给出服务器端的架构,甚至可以不使用服务器直接在客户端之间传输消息;也没有规定消息的顺序、安全、重发等特性。
+
+JMS可以看做是一种与厂商无关的API,使得Java程序能够与不同厂商的消息组件很好地进行通信。其定义的一个规范流程如下:
+
+- 获得JMS connection factory,通过我们提供特定环境的连接信息来构造factory。
+- 使用factory构造JMS connection。
+- 启动connection。
+- 通过connection创建JMS session。
+- 指定JMS destination。
+- 创建JMS producer或者创建JMS message并提供destination。
+- 创建JMS consumer或注册JMS message listener。
+- 发送和接收JMS message。
+- 关闭所有JMS相关资源,包括connection、session、producer、consumer等。
+
+ActiveMQ是对JMS的一个实现,提供了标准的、面向消息的、能够跨越多语言和多系统的应用集成的消息中间件功能。除了JMS标准规定的特性之外,ActiceMQ还提供了诸如消息持久化、主从集群、消息组通信、有序消息管理、消息延迟接收、消息优先级等附加特性。
+
+在AvtiveMQ中消费者使用长连接的方式消费队列消息。
+
+Spring JMS提供了对符合JMS规范的消息队列的使用封装:
+
+消息生产者示例如下:
+
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+在相关Bean中引入jmsTemplate即可调用相关方法实现消息发送。
+```
+jmsTemplate.convertAndSend("test_jms_queue",message);
+```
+
+消息消费者示例:
+
+```
+
+
+
+
+
+
+@Component("activeMQListener")
+public class ActiveMQMessageListener implements javax.jms.MessageListener.MessageListener {
+ @Override
+ public void onMessage(Message message) {
+ ...//消息处理逻辑
+ }
+}
+```
+实现javax.jms.MessageListener.MessageListener即可处理收到的消息。
+
+由于ActiveMQ性能较差,在大规模的互联网应用中并不推荐使用。
+
+## 6.3.2 完备消息中间件-RabbitMQ
+
+RabbitMQ基于AMQP协议,是一个完备的消息中间件。
+
+### AMQP
+
+AMQP, The Advanced Message Queuing Protocol, 是一个比较复杂的消息中间件协议。它是一个和语言无关的,在金融行业使用的新兴消息中间件,是一个异步消息传递所使用的应用层协议规范。其现在的目标是为通用消息队列架构提供通用构建工具。
+
+与JMS相比,AMQP并不是API, AMQP客户端能够无视消息的来源任意发送和接受信息。AMQP提供了一个模型,旨在统一消息模式,包括队列、发布/订阅、事务等,还包括路由、扩展性等额外特性,能够让客户端和消息中间件之间进行消息通信及消息确认。如下:
+
+
+
+如上图所示,AMQP工作模式可以描述为:消息首先被发布到信箱(Exchange),然后信箱将会把消息拷贝分发到应用了规则(即绑定Bindings)的队列。AMQP 消息中间件既可以将消息分发到订阅了某些队列的消费者,也可以让消费者自己根据需要从队列中拉取消息。其中:
+
+- 信箱Exchanges、队列Queues和绑定Bindings被统称为 AMQP 实体。
+ - 信箱:用于消息发送的实体, 可以将一条消息路由到零个或多个队列。具有名称、持久性、自动删除等属性,这里的自动删除指的是信箱会在所有队列都不再使用它的时候被删除。
+ - 队列:和其它消息务队列软件很类似,会存储供应用程序消费的消息。队列会和信箱共享一些属性,但也有一些额外的属性,包括名称、持久性、连接的专一性、自动删除、消息的TTL等。这里的连接专一性指的队列只被一个连接使用,并在连接断开时删除;自动删除指的是当最后一个消费者取消订阅后,队列会被删除。
+ - 绑定:是信箱用于路由消息到队列的一些规则。可以认为绑定就是连接信箱和队列的路径。
+
+- 在发布消息时,发布者可以定义各种消息属性。其中的一些属性可能会被消息中间件使用,余下的则由接收消息的应用程序使用。
+- 当消息传递到消费者时,消费者会给消息中间件发送确认通知,可以自动回复或者选择在需要的时候回复。当使用消息确认机制时,只有在消息中间件接收到来自消费者的确认时才会将消息从队列中移除。
+- 消息的负载均衡是在消费者之间进行的,即当多个消费者监听同一个Queue时,使用Round Robin策略将消息只传输给一个消费者。
+- AMQP对于消费者的实现包括push和pull两种方式。
+
+此外,需要注意AMQP是一个抽象协议,不负责处理具体的数据。它的实体和路由策略都可以根据需要定义并可以在不需要的时候删除这些AMQP实体。
+
+### RabbitMQ介绍
+
+RabbitMQ版本:3.6.6。
+
+RabbitMQ是实现了AMQP的开源消息队列,服务器端用Erlang语言编写,支持多种客户端。从生产者接收消息并传递给消费者,在这个过程中,根据规则进行路由、缓存以及持久化。消费者通过push方式获取消息,即队列里有消息就会推送给消费者。其架构基于AMQP模型:
+
+
+
+比起AMQP模型,RabbitMQ增加以下几个概念:
+
+- Broker: 消息队列服务器实体, 一个RabbitMQ实例就是一个broker。
+- VHost:虚拟主机,一个Broker里可以开设多个VHost,用作不同用户的权限分离。
+- Channel:消息通道,在客户端的每个连接里,可建立多个Channel,每个Channel代表一个会话任务。
+
+RabbitMQ的使用过程如下:
+
+- 客户端连接到消息队列服务器,打开一个Channel。
+- 客户端声明一个Exchange,并设置相关属性。
+- 客户端声明一个Queue,并设置相关属性。
+- 客户端使用routingKey,在Exchange和Queue之间建立好绑定关系。
+- 客户端投递消息到Exchange。
+- Exchange接收到消息后,就根据消息的key和已经设置的Binding,进行消息路由,将消息投递到一个或多个队列里。
+
+RabbitMQ具有较高的可用性、稳定性以及可靠性,具备了一个成熟的MQ应该具有的特性。适用于可靠性要求极高、对消息有事务要求的业务场景。但其性能和分布式能力稍弱,一般RabbitMQ的单机QPS在万级别之内,中小规模场景可选。
+
+需要注意的是,RabbitMQ也仅仅只能保证At Least Once(消息至少被消费一次)的消费,无法从自身去进行消息去重。
+
+### RabbitMQ交换机
+
+消息由Client发送,RabbitMQ接收到消息之后通过交换机转发到对应的队列上面。消费者会从队列中获取未被读取的数据处理。
+
+RabbitMQ的Exchange包含以下几种:
+
+- Direct Exchange:直连交换机。当消息的routigKey和Binding的routingKey直接匹配, 那么转发消息到routingKey指定的队列。
+- Fanout Exchange:扇形交换机。采取广播模式,转发消息到所有与该交换机绑定的队列,速度最快。
+- Topic Exchange:主题交换机。使用匹配模式按规则转发消息,即判断消息的routingKey和binding的routingKey是否符合通配符匹配。是最灵活的交换机。
+- Headers Exchange:头部交换机 ,如果消息的头部信息和Binding的参数表中匹配的话,消息将会路由到该队列。
+
+### 消息持久化
+
+RabbitMQ支持消息的持久化,包括三个方面:
+
+- Exchange持久化,在声明时指定durable => 1。
+- Queue持久化,在声明时指定durable => 1。
+- 消息持久化,在投递时指定delivery_mode => 2。
+
+这里如果Exchange和Queue都是持久化的,那么它们之间的Binding也是持久化的。如果Exchange和Queue两者之间有一个持久化,一个非持久化,就不允许建立绑定。
+
+### 负载均衡和高可用
+
+RabbitMQ本身并不具有负载均衡机制,用户连接到RabbitMQ集群的任意节点都可以访问集群中的任意消息队列,但一个消息队列只存储在一个物理节点上,其它节点只存储该队列的元数据,这使得当队列里只有一个队列时,系统性能受限于单个节点的网络带宽和主机性能。可以采取以下方案实现RabbitMQ的简单负载均衡:
+
+1. 建立多个消息队列,每个物理节点上消息队列数相同。
+1. Exchange的类型设置为Direct,建立多个Binding,每个队列对应一个Key。
+1. 每个生产者建立到每个物理节点的连接。
+1. 每个消费者订阅所有消息队列,。
+1. 发送消息时随机选择一个Key,并使用该Key对应的队列所有在节点的连接发送该消息。
+1. 当某个节点挂掉后,发送者将消息随机发送到其余节点,并一直监控该挂掉的节点是否重起,重启后,即可向该节点发消息。
+
+此外,RabbitMQ提供了镜像Mirror来实现队列的高可用。当某个RabbitMQ节点故障时,只要其它节点里存在该故障节点的队列镜像,该队列就能继续正常工作不会丢失数据。但使用该功能也会有些副作用,它这种通过冗余数据保障可靠性的方式会降低系统的性能,因为往一个队列发数据也就会往这个队列的所有镜像队列发数据,这必然产生大量RabbitMQ节点间数据的交互,降低吞吐率。可以采取以下方式做优化:
+
+1. 每个队列只有一个镜像,镜像的位置为“下一个节点”。
+1. 消费者端监控所有链接,当发现某个节点挂掉时,自动连接到镜像节点,而当故障节点恢复时自动连接回来。
+
+如此,虽然可靠性有所降低,但是性能会有很大的提高,也能保证一定程度的可靠性。
+
+
+### 使用
+
+Spring Rabbit和Spring AMQP对RabbitMQ的使用做了封装。
+
+生产者配置:
+
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+这样引入amqpTemplate即可调用相关方法发送消息。
+
+```
+amqpTemplate.convertAndSend("testQueueKey",message);
+```
+
+消费者配置:
+
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@Component("amqpListener")
+public class AmqpMessageListener implements org.springframework.amqp.core.MessageListener {
+ @Override
+ public void onMessage(Message message) {
+
+ }
+}
+```
+如上,实现org.springframework.amqp.core.MessageListener即可接受消息进行处理。
+
+需要注意的是,如果启用生产者与MQ间的确认机制、消费者与MQ间的确认机制、QoS是会影响整体吞吐量的,慎重开启。
+
+此外,经过测试可知增加VHost的数量是可以提高RabbitMQ的性能的。
+
+## 6.3.3 日志消息中间件-Kafka
+
+Kafka是由LinkedIn公司开发的一个分布式的消息系统,后来成为Apache的开源项目,其使用Scala编写,以可水平扩展和高吞吐率而被广泛使用。目前越来越多的开源分布式处理系统如Apache Storm、Spark等都支持与Kafka集成。
+
+Kafka基于文件append,以顺序读写的方式来写入读取文件,性能(吞吐量、TPS)是非常高的,也支持多订阅者,当失败时能自动平衡消费者。但由于Kafka设计的初衷就是处理日志的,因此是允许消息丢失的,对事务的支持也不好,也并不能保证消息的顺序性。此外,Kafka中消费者是通过pull的方式去获取消息的,因此在实时性上是不如RabbitMQ的,但是其使用Zero-Copy的技术,保证了一定的pull性能。
+
+综上,Kafka适用于海量消息场景,允许极端情况下少量丢失,如日志。单机QPS可以维持在十万级别,甚至可以达到百万级。不过,Kafka迭代到现在,对可靠性要求较高的场景也有了支持,需要对Kafka根据具体场景对其监控和治理能力进行适当定制完善。对比RabbitMQ的核心是Routing,Kafka的核心则在于Streaming。
+
+Kafka版本0.9.0。
+
+### 关键概念和架构
+
+几个概念如下:
+
+- Topic:Kafka将消息以category的方式保存在一起,称为Topic。
+- Producer: 向topic产生消息的进程称为Producer。
+- Consumer: 处理topic上的消息的进程称为Consumer。
+- ConsumerGroup: 每个Consumer属于一个特定的Consumer Group,一条消息可以发送到多个不同的Consumer Group,但是一个Consumer Group中只能有一个Consumer能够消费该消息。
+- Broker: Kafka集群由一个或者多个Server组成,称为Broker。
+- Partition: 分区,是一个物理上的概念,一个Topic可以分为多个Partition,每个Partition内部是消息有序的。
+
+Kafka的架构如下如所示:
+
+
+
+消息的发送和接受流程如下:
+
+- 通过Zookeeper管理集群配置、选举Leader; 在Consumer Group变化时进行rebalance。
+- 生产者以push方式向所选择的Topic发布消息并负责选择哪一个消息被指定到Topic的哪一个Partition中。这个可以通过round-robin简单地做负载均衡或者按照一些语义分区机制(例如基于消息中的一些key)来做。
+- 消费者使用pull方式订阅消息。基于Consumer group可以实现两种消费模式:
+
+ - 所有的消费者实例都在同一个消费者Group中,那么就类似于传统的队列,同一Group中的Consumer对同一消息仅仅能获取到一次。
+ - 每一个消费者实例都在不同的消费者Group中,那么就类似于发布-订阅模型,所有消息被广播到所有消费者。
+
+需要提到的一点:Kafka的所有消息都是顺序append到日志文件中的,每条消息在文件中的位置称为offset(偏移量),offset为一个long型的数字,它唯一标记一条消息。因此,Consumer对offset的管理是读取消息的关键点之一。
+
+### 分布式
+
+对于每一个Topic,Kafka集群都保存了一个分区Log,每一个分区都是一个提交日志,一系列有序的、不可变顺序的消息连续地追加到日志的尾部。如此,使用分区使得Kafka可以在单个服务器上扩展,能够承载大量的数据。
+
+这些分区分布在Kafka集群的服务器上,其中的每一个服务器都控制一组分区上的数据和请求, 可以并行的接受请求(同一分区不允许并发访问)。每一个分区通过一定数量的服务器冗余提高容错率。
+
+每一个分区都有一个服务器作为“Leader”,零个或者多个服务器作为“Followers”。Leader控制所有的读写请求,Followers被动地去冗余Leader。如果Leader发生了故障,那么Followers中的一个会自动地成为新的Leader。每一个服务器对于其中的一部分分区是做为Leader,对于其他的分区则是做为Follower,这样就能很好的在集群内部做好负载均衡。
+
+对于Leader和Follower之间的数据同步,还牵扯到HW(high watermark)和LEO(log end offset)的概念。
+
+- HW: 指的消费者能够看到的此Partition的位置,offset小于此值的消息被认为是commit的,可以供消费者消费。
+- LEO: 每一个replica的log中最后一条消息的offset。
+
+Follower拉取Leader数据,当数据完全同步时则阻塞。当有新的数据到来,Leader会解锁阻塞的Follower通知他们新的消息,Follower开始同步消息并更新自己的LEO。Leader选取所有ISR(in-sync replicas,指的alive和追赶Leader的replica)中最小的LEO作为自己的HW,消费者最多只能消费到HW所在的位置。由此可见,Kafka的数据复制机制既不是完全的同步复制,也不是单纯的异步复制,其使用ISR的这种方式能够很好地均衡吞吐率和数据的安全性。
+
+由于分区的原因Kafka无法保障全局消息有序,但通过指定分区到一个消费者Group中的消费者,这样每一个分区只被这个Group中的一个Consumer消费,能够保证一个分区中的消息顺序。当然,使用一个只有一个分区的Topic能够保证消息全局有序。
+
+### 使用
+
+生产者示例:
+
+```
+Properties props = new Properties();
+props.put("bootstrap.servers", "localhost:9092");
+props.put("acks", "all");
+props.put("request.timeout.ms", "10000");
+props.put("retries", 10);
+props.put("batch.size", 16384);
+props.put("linger.ms", 1);
+props.put("buffer.memory", 33554432);
+props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
+props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
+props.put("compression.codec", "none");
+props.put("partitioner.class", "org.apache.kafka.clients.producer.internals.DefaultPartitioner");
+
+Producer producer = new KafkaProducer<>(props);
+producer.send(new ProducerRecord("test_topic", "message_key", "message_value"));
+...
+producer.close();
+```
+
+其中的几个关键配置:
+
+- acks:用来控制一个Produce请求怎样才能算完成。0表示Producer直接返回,不会等待一个来自Broker的ack;1表示在Leader replica已经接收到数据后,Producer会得到一个ack;all表示在所有的ISR(in-sync replicas,指的alive和追赶Leader的replica)都接收到数据后,Producer才得到一个ack。
+- batch.size: 批量发送消息(同一个分区)时的一个batch的大小,单位bytes。过小的值会使得发送频繁,降低吞吐量;过大的值则会浪费内存;设置为0则不进行批量发送。
+- linger.ms: 消息记录缓冲的时间,即延时发送消息记录等待其他消息记录一起做为一个batch。当满足batch.size时,会忽略此值直接发送,否则延时此时间再发送。
+- buffer.memory:用于缓冲消息记录的内存大小,单位bytes。
+- key.serializer:消息的key的序列化类。
+- value.serailizer: 消息的value的序列化类。
+- compression.codec:Producer的数据的压缩方式,包括none、gzip、snappy和lz4四种。
+- partitioner.class:用来把消息分到各个Partition中的实现类,默认对key进行hash。
+
+这里需要注意的是,此处使用的是kafka-clients库中的KafkaProducer,是纯Java的实现。它是在Kafka0.8.2版本之后被引入的,与之前Scala版本的kafka库中的Producer配置参数有不少区别。
+
+消费者的API分为两种:
+
+1. Hight Level
+
+ 此种级别的API, 封装了很多底层细节,使用Zookeeper保存offset信息。
+
+ ```
+ Properties props = new Properties();
+ props.put("zookeeper.connect", "zk1.dmp.com:2181,zk2.dmp.com:2181,zk3.dmp.com:2181");
+ props.put("zookeeper.session.timeout.ms", "3000");
+ props.put("zookeeper.sync.time.ms", "200");
+ props.put("group.id", "test_group");
+ props.put("auto.commit.interval.ms", "600");
+
+ String topic = "test_topic";
+ ConsumerConnector connector = Consumer.createJavaConsumerConnector(new ConsumerConfig(props));
+ Map topics = new HashMap();
+ int partitionNum = 3;//分区数目
+ topics.put(topic, partitionNum);
+ Map>> streams = connector.createMessageStreams(topics);
+ List> partitions = streams.get(topic);
+ Executor threadPool = Executors.newFixedThreadPool(partitionNum);
+ for (final KafkaStream partition : partitions) {
+ threadPool.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ ConsumerIterator it = partition.iterator();
+ while (it.hasNext()) {
+ MessageAndMetadata item = it.next();
+ byte[] messageBody = item.message();
+ }
+ }
+ });
+ }
+ ```
+
+2. Low Level
+
+ Kafka的low-level接口主要是对SimpleConsumer的使用,使用场景如下:
+
+ - 读取一个消息多次。
+ - 在一个进程中仅仅消费某一个Topic中几个Partition的数据.
+ - 管理事务以确保一个消息处理且仅仅被处理一次。
+
+ 使用这个接口需要注意以下几点:
+
+ - 在应用中必须跟踪记录offset以确保能够确定上次消费到的位置。
+ - 必须设置哪一个Broker是要操作的Topic和Partition的Leader。
+ - 必须自己控制Broker的Leader的改变。
+
+ 使用步骤:
+
+ - 找出一个active状态的Broker并且找出哪一个Broker是哪些Topic和Partition的Leader,必须知道读哪个Topic的哪个Partition。
+ - 找到负责该Partition的Broker Leader,从而找到存有该Partition副本的那个Broker。
+ - 自己去写request并fetch数据。
+ - 获取数据。
+ - 需要识别和处理Broker Leader的改变。
+
+Kafka 0.9.0之后的kafka-clients提供了新的KafkaConsumer实现, 不再区分Hight Level和Low Level。
+
+```
+Properties props = new Properties();
+props.put("bootstrap.servers", "localhost:9092");
+props.put("group.id", "test_group");
+props.put("enable.auto.commit", "true");
+props.put("auto.commit.interval.ms", "1000");
+props.put("session.timeout.ms", "30000");
+props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
+props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
+KafkaConsumer consumer = new KafkaConsumer<>(props);
+consumer.subscribe(Arrays.asList("test_topic"));
+while (true) {
+ ConsumerRecords records = consumer.poll(100);
+ for (ConsumerRecord record : records){
+ ...
+ }
+}
+```
+
+如果需要自己手动控制offset的保存,可以将`enable.auto.commit`设置为false,在相应的地方`consumer.commitSync()`进行提交。
+
+如果只需要消费某几个Partition的数据:
+
+```
+String topic = "test_topic";
+TopicPartition partition0 = new TopicPartition(topic, 0);
+TopicPartition partition1 = new TopicPartition(topic, 1);
+consumer.assign(Arrays.asList(partition0, partition1));
+...
+```
+
+如果需要自定义offset存储实现和对offset的控制:
+
+- 将`enable.auto.commit`设置为false。
+- 获取ConsumerRecord的offset并保存。
+- 使用Consumer的seek(TopicPartition, long)设置offset。
+
+此外,在开发Consumer程序时还有以下几个注意点:
+
+1. 同一Consumer Group消费过的数据无法再次消费。如果想要再次消费数据,要么换另一个groupId,要么使用镜像或者使用Low Level API或者新的Consumer API去设置partion和offset。此外,Kafka本身也提供了工具来粗略地重新设置offset:
+
+ ```
+ ./kafka-run-class.sh kafka.tools.UpdateOffsetsInZK earliest config/consumer.properties page_visits
+ ```
+ 参数解释如下:
+
+ - [earliest | latest],表示将offset置到哪里
+ - consumer.properties ,这里是配置文件的路径
+ - Topic名,这里是page_visits
+
+1. Kafka 0.8.2版本引入了native offset storage,将offset管理从Zookeeper移出,并且可以做到水平扩展,相比起之前用Zookeepr存储offset, 避免了对Zookeeper的频繁写入(低效操作)。
+
+1. 上面Consumer的High Level API中,Consumer中的Stream指的是来自一个或多个服务器上的一个或者多个Partition的消息。每一个Stream都对应一个单线程处理。因此,Client能够设置满足自己需求的Stream数目。总之,一个Stream也许代表了多个服务器Partition的消息的聚合,但是每一个Partition都只能到一个Stream。
+
+1. Consumer和Partition的数目需要配合设置。
+
+ - 如果Consumer比分区多,是浪费,因为Kafka的设计是在一个分区上是不允许并发的,所以Consumer数不要大于分区数。
+ - 如果Consumer比分区少,一个Consumer会对应于多个分区,需要合理分配Consumer数和Partition数,否则会导致Partition里面的数据被取的不均匀。
+ - 如果Consumer从多个Partition读到数据,不保证消息的有序。
+ - 增减Consumer、Broker、分区会导致rebalance,所以rebalance后Consumer对应的分区会发生变化。
+
+ 综上,负载低的情况下可以每个线程消费多个Partition。但负载高的情况下,Consumer线程数最好和Partition数量保持一致。如果还是消费不过来,可以在增加Partition数的同时增加Consumer数或者通过某些手段提升消息处理能力。需要注意的是,由于Kafka主要是磁盘读写,Partition的增多如果是单个磁盘那么在并发性能上会有瓶颈,可以通过增加磁盘来扩展并发能力。
+
+1. 注意session.timeout.ms、max.poll.interval.ms、heartbeat.interval.ms三个参数的配置。session.timeout.ms指的是心跳最大的时间间隔,heartbeat.interval.ms则是心跳频率,这两个值越小则能够让客户端越快地检测到rebalance;max.poll.interval.ms则表示两次poll数据之间的最大间隔,如果对一批数据的处理时间过长使得两次poll的时间间隔超过这个值就会认为consumer出现了问题进而触发rebalance,可以通过增大此值或者减少每一次获取的记录数目(max.poll.records)来避免不必要的rebalance,也可以在处理消息时将消息放到内存队列中异步处理来减少poll时间间隔。需要注意的是,在kafka-client 0.10版本之前,心跳是随着poll进行的,并不是单独的后台进程,需要同时考虑如何避免心跳超时的同时不影响心跳的频率,网上可见的一种方式就是Spring kafka的手动提交offset和心跳。
+
+由于Kafka是对磁盘进行读/写,可以考虑优化系统的Page Cache来提升其读写性能。
+
+还需要提到一点,由于Kafka是使用Scala编写的,因此阿里巴巴基于其原理(后来有了自己的设计)使用Java语言实现了MetaQ并开源,现在已经变成RocketMQ,可以作为Kafka的替代品。
+
+此外,在Kafka监控管理上,可以选择Yahoo开源的kafka-manager、Linkedin开源的kafka-monitor和burrow(监控Consumer的lag)。
+
+### Kafka vs RabbitMQ
+
+
+
+
+## 6.3.4 本地消息队列
+
+区别于消息中间件,本地消息队列指的是JVM内的队列实现,常用的包括下面几个:
+
+1. BlockingQueue
+
+ 阻塞队列,实现了FIFO特性,是生产者消费者模式的首选,需要使用轮询pull的方式,周期性地从队列中取元素。
+。一般使用LinkedBlockingQueue这个实现即可,其可以指定元素的容量,也可以不指定容量,但是需要注意无界队列使用不当(生产速度大于消费速度)很容易引起OOM, 建议使用有界队列。其有以下两种使用方式:
+
+ - 配合使用take和put。take是取出元素,如果队列为空则阻塞直到有新的元素放入;put是放入元素,如果队列已满,则阻塞直到队列有空间。
+ - 配合使用poll和offer。这里是使用它们带timeout的方法。这样,poll可以在指定的时间内来获取元素,超时没有元素则返回;offer则是在指定时间内放入元素,超时则返回。
+
+ 同样实现了BlockingQueue接口的ArrayBlockingQueue是一个有界队列,其底层存储基于数组实现。与LinkedBlockingQueue相比,其在生产和消费的时候直接将对象插入或删除,不会包装为Node,生产和消费使用的是同一个锁,且可以设置为公平访问队列(阻塞的生产者线程或消费者线程当队列可用时可以按照阻塞的先后顺序访问队列)。通常来说, LinkedBlockingQueue的吞吐量是大于ArrayBlockingQueue的。
+
+ 此外,SynchronousQueue也是一个阻塞队列,但其并不存储元素,一个put操作必须等待一个take操作,否则不能继续添加元素,适合用于任务比较少的传递性场景。
+
+1. ConcurrentLinkedQueue
+
+ 非阻塞队列,元素按FIFO原则进行排序,采用CAS操作,来保证元素的一致性。配合使用offer和poll并使用轮询pull的方式,周期性地从队列中取元素即可实现消息队列的功能。
+
+ 由于此类是一个无界队列,因此使用的时候需要特别注意消费者的速度要匹配得上生产者的速度,否则会产生oom。
+
+1. Disruptor
+
+ Disruptor是LMAX开源的并发框架,其设计为一个无锁的高性能线程交互的消息处理库,经常做为消息队列来使用。其使用了以下技术来达到低延迟高性能:
+
+ - CAS: 避免了使用锁产生的竞争和等待。
+ - 缓存行填充:避免了伪共享导致的缓存失效问题。
+ - RingBuffer: 一个环形队列,用来在不同线程间传递数据的buffer。
+ - 内存屏障:使用内存屏障(一个CPU指令,允许对数据什么时候对其他进程可见作出假设),避免了锁的开销。
+
+ 架构如下图:
+
+ 
+
+ 大体流程如下:
+
+ - 生产者放入元素时需要申请下一个可写入的空闲槽的sequence。
+ - 生产者拿到空闲sequence后去获取到对应的槽里的对象并做操作。
+ - 消费者线程申请下一个可用元素是通过waitFor Sequence Barrier。
+ - Sequence Barrier拿到可用sequence后返回给消费者。
+
+ 消费者使用示例如下:
+
+ ```
+ //事件
+ class TestEvent {
+ private long value;
+
+ public void set(long value) {
+ this.value = value;
+ }
+ }
+
+ //事件处理器
+ class TestEventHandler implements EventHandler {
+ public void onEvent(TestEvent event, long sequence, boolean endOfBatch) {
+ System.out.println("TestEvent: " + event); //处理消息
+ }
+ }
+
+ //事件工厂
+ EventFactory factory = new TestEventFactory(){
+ public TestEvent newInstance() {
+ return new TestEvent();
+ }
+ };
+
+ // 构造Discruptor
+ Disruptor disruptor = new Disruptor<>(factory, 1023, Executors.defaultThreadFactory());
+
+ // 设置事件处理handler
+ disruptor.handleEventsWith(new TestEventHandler());
+
+ // 启动所有相关线程
+ disruptor.start();
+ ```
+
+ 生产者如下:
+
+ ```
+ //事件生产者封装,并实现事件的转换
+ class TestEventProducerWithTranslator {
+ private final RingBuffer ringBuffer;
+
+ public TestEventProducerWithTranslator(RingBuffer ringBuffer) {
+ this.ringBuffer = ringBuffer;
+ }
+
+ private static final EventTranslatorOneArg TRANSLATOR =
+ new EventTranslatorOneArg() {
+ public void translateTo(TestEvent event, long sequence, ByteBuffer bb) {
+ event.set(bb.getLong(0));
+ }
+ };
+
+ public void onData(ByteBuffer bb) {
+ ringBuffer.publishEvent(TRANSLATOR, bb); //发布消息
+ }
+ }
+
+ TestEventProducerWithTranslator producer = new TestEventProducerWithTranslator(disruptor.getRingBuffer());
+
+ ByteBuffer bb = ByteBuffer.allocate(8);
+ bb.putLong(0, l);
+ producer.onData(bb); //发布消息
+ ```
+
+ 使用Disruptor需要注意的一点就是,new Disruptor有另外一个构造方法:
+
+ ```
+ Disruptor(
+ final EventFactory eventFactory,
+ final int ringBufferSize,
+ final ThreadFactory threadFactory,
+ final ProducerType producerType,
+ final WaitStrategy waitStrategy)
+ ```
+
+ 其中的producerType指的是生产者模式,包括单生产者和多消费者两种生产者模式两种。虽然根据单一写原则单生产者模式具有更高的并发性能,但是即使你这里使用了单生产者模式,Disruptor也是不会给你构造单生产者运行环境的。
+
+ 此外,waitStrategy则指的是消费者等待ringbuffer中的sequence可用时使用的等待策略,常用的有以下几种:
+
+ - BlockingWaitStrategy:默认的等待策略,使用了锁和竞态条件,是最慢的一种等待策略。适用于关注CPU资源大于关注吞吐和低延迟的应用场景。
+ - SleepingWaitStrategy:先使用一个循环和Thread.yield()做等待,最后使用LockSupport.parkNanos(1L),兼顾了性能和CPU使用。比较适用于对低延迟没有特别关注的场景,如异步日志。
+ - YieldingWaitStrategy: 先做几次循环,再使用Thread.yield()做等待。可以用于低延迟应用且所在机器的CPU使用了超线程技术。
+ - BusySpinWaitStrategy:是一个不停循环等待的策略,也是性能最高的策略。适用于所在机器CPU没有使用超线程技术的系统。
+
diff --git a/book/chapter6-datatrans/rest.md b/book/chapter6-datatrans/rest.md
new file mode 100644
index 0000000..62ae614
--- /dev/null
+++ b/book/chapter6-datatrans/rest.md
@@ -0,0 +1,186 @@
+# 6.1 RESTful
+
+REST,全称表现层状态转移(Representational State Transfer), 指的是资源在网络中以某种表现形式进行状态转移,是一种架构风格。其描述的是在网络中Client和Server的一种交互形式。简单来说就是用HTTP URL来定位资源,用HTTP的各种method来描述操作。其关键的三个概念如下:
+
+- Resource: 资源,主要指的是数据。
+- Representational:数据的表现形式,如JSON、XML、HTML等。
+- State Transfer:状态变化, 通过HTTP method来描述。
+
+REST经常被用来规范API的设计以及数据传输的格式,可以统一给各种客户端提供接口,包括Web、iOS、Android和其他的服务。REST不需要显式的前端页面,只需要按照格式返回数据即可。符合REST风格的API称为RESTful API,符合RESTFul规范的架构称为RESTful架构。如下图所示:
+
+
+
+## 6.1.1 操作
+
+RESTful是基于HTTP协议的,其主要依赖于HTTP协议的几种method来表示CRUD(create、read、update和delete,即数据的增删查改)操作:
+
+- GET: 从服务器上获取资源
+- POST: 创建新的资源
+- PUT: 更新服务器资源
+- DELETE: 删除服务器资源
+
+这里需要注意两点:
+
+- GET、PUT和DELETE应该是幂等的,即相同的数据和参数下,执行一次或多次产生的效果是一样的。
+- 对于POST和PUT操作,应该返回最新的资源,删除操作则一般不必要。
+- 所有的操作都是无状态的,即所有的资源,都可以通过URL定位,这个定位与其他资源无关,也不会因为其他资源的变化而改变。
+
+除了上述方法之外,还有一个PATCH方法也用于更新资源的部分属性,但并用的并不多,用POST即可。
+
+此外,HTTP 1.1的几个头部也是应该注意的:
+
+- Accept: 客户端要求服务器返回什么样表现形式的数据。RESTFul API需要根据此头部返回合适的数据。
+- If-Match: 在对资源做更新和删除操作时,客户端提供If-Match头,值为服务端上次对此资源返回的Etag, 服务端对比Etag如果一致才做更新和删除,否则返回412。
+- If-None-Match: 和If-Match相反,如果不匹配上次的Etag才返回数据,匹配的话则返回304,多用于Get请求。
+- If-Modified-Since:值为时间,如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304,多用于Get请求。
+
+## 6.1.2 返回码
+
+HTTP本身已经提供了很多StatusCode来表示各种状态。RESTFul接口需要遵循这些定义,返回合适的状态码和数据。当然,如果是内部使用,统一返回200,在返回数据里自定义一套status code也是可以的。
+
+HTTP的状态码大体分为几个区间:
+
+- 2XX:请求正常处理并返回。
+- 3XX:重定向,请求的资源位置发生变化。
+- 4XX:客户端发送的请求有错误。
+- 5XX:服务器端错误。
+
+在自己设计返回码的时候最好也遵循此范围设计,以下是其中几个常用的状态码:
+
+- 200:表示请求成功。
+- 301:资源已经永久移动到新的地址,新的URL会在响应头中返回。
+- 302:资源临时被移动到新的地址,新的URL会在响应头中返回。
+- 304:表明资源未改变。主要配合请求头中的If-None-Match和If-Modified-Since使用。
+- 400:错误请求,表示请求中有语法错误。
+- 401:请求的资源需要认证,请求没有提供认证信息或者认证错误。
+- 403:资源被禁止访问。
+- 404:资源不存在。
+- 502:错误的网关,通常是作为代理的服务器无法收到远程服务器的正确响应。
+- 503:服务不可用。
+
+## 6.1.3 资源
+
+资源是RESTful API的核心,其以URI(统一资源标识符)标识,而URL则不仅能够标识一个资源,还能够定位资源。RESTful中使用HTTP URL标识并定位一个资源。原则上只使用名词来指定资源,而且推荐使用复数。以对记事的CRUD API的设计为例:
+
+- 获取所有记事列表:GET /api/notes?page=1&per_page=20
+- 获取某人的所有记事列表:GET /api/users/{uid}/notes
+- 获取标记为星的记事:GET /api/users/{uid}/notes?star=1
+- 创建记事:POST /api/notes
+- 删除某一个记事:DELET /api/notes/{note_id}
+- 更新某一个记事:PUT /api/notes/{note_id}
+
+可知:
+
+- 资源分为单个资源和资源集合,尽量使用复数来表示资源,单个资源通过添加ID等标识符来表示。
+- 资源使用嵌套结构,类似于目录路径的方式,可以体现出之间的关系。
+- 一个资源可以有不同的URL,如上可以获取所有的记事列表,也可以获取某人的所有记事列表。
+- 对于GET方法,一定不能设计为可以改变资源的操作。如get /api/deleteNote?id=xx。
+- URL是对大小写敏感的,尽量使用小写字母,单词间用下划线连接。
+- 使用Query参数来控制返回结果,如上面返回星标记事的接口。此外,像排序方向、排序使用的字段都是可以放在query参数中的。
+- 分页参数使用Query参数(page、per_page)控制,在返回数据中返回当前页、下一页、上一页、总页数等分页相关信息。
+
+如果需要区分版本号,可以放在路径中,如/api/v2/**,也可以放在header的Accept字段或者Query参数中:
+
+```
+Accept: version=2.0;...
+```
+
+对于一些很难设计为CRUD操作的URL, 如登录、送礼物等,有以下处理方式:
+
+- 使用POST,如POST /api/login。
+- 把动作转换成资源: 登录就是创建了一个Session或者Token,那么就可以设计为 POST /api/sessions。
+
+此外,对于数据的提交格式和返回格式,目前以JSON格式为主,其可读性、紧凑性、多语言支持都较好;数据提交的方式也应该使用application/JSON的内容格式并在body里放置JSON数据。
+
+```
+...
+Content-type: application/json
+Accept: application/json
+...
+
+{
+ 'title':'xxx',
+ 'content':'xxx'
+ ...
+}
+```
+
+## 6.1.4 安全性
+
+HTTP本身是对数据不做任何安全处理的,因此建议首先从根本上使用HTTPS加强数据的安全性。此外,这里的安全性还要保证数据的完整性;保证接口的授权访问,保证接口只提供给授权过的应用访问以及过滤掉不必要的请求;保证数据的授权访问,只允许资源拥有者删除、更新自己的资源。
+
+### 数据的完整性
+
+数据完整性主要是指在对数据进行修改时,要保证要修改的数据和服务器数据是一致的。可以通过Etag这个HTTP中的头部字段来解决。
+
+Etag表示的是资源的唯一版本号, 请求资源时,RESTful api应该把资源数据以及资源的Etag一起返回。api请求方修改资源时应该提交If-Match头,这样服务器通过对比Etag可以防止数据被错误修改,类似于并发中CAS的原理。但是要绝对保证数据的完整性,还得需要配合严格的并发控制才能做到。
+
+### 接口访问控制
+
+接口访问控制可以保证接口的授权访问,拒绝不合法的请求。可以通过以下几种方式:
+
+- 在Request headers中添加特殊的标识符,如果不含有此header的请求直接拒绝。这可以做简单的接口访问控制。
+- 过滤Requst query和body, 做白名单验证,即只允许出现哪些参数,如果有非法参数,可以抛弃或者直接拒绝请求。
+
+上面只是比较简单的接口访问控制策略,无法彻底拒绝未授权的请求。我们可以通过为每一个授权应用分配app_secret(私有的,不公开),访问时对请求进行签名验证的方式实现更为严格的接口访问控制,这种方法也叫做HMAC。请求签名生成的一个例子如下:
+
+```
+app_sign = MD5(METHOD & PATH & timestamp & app_secret)
+```
+其中,METHOD指的是此次请求的方法,PATH指的URL中的path部分,timestamp是请求时间戳,app_secret是分配请求方的私钥,此外还有一个分配给请求方的app_id。这样,app_id、timestamp、app_sign随着请求一起发送(可以作为query参数也可以作为header),服务器接收到请求后使用同样的算法计算出app_sign进行对比,如果相同则正常请求,否则返回401 Unauthorized。由此既可以保证接口的授权访问,还能够基于时间戳防止重放攻击。当然,app_sign的生成算法可以加入更多的因子,如request_body、query等。但需要注意的是这个算法越复杂,对接口的性能影响就越大,需要做权衡。
+
+### 数据的授权访问-OAuth
+
+数据的授权访问其实也是接口访问控制的一部分。主要关注点在于对资源的操作权限做控制。基于HTTP做授权访问的核心就是验证一个请求是否是合法用户发起的,主要的有HTTP Basic Auth、OAuth。其中Basic Auth会把用户的用户名和密码直接暴露在网络中并不安全,因此RESTful api主要使用OAuth做数据的授权访问控制。
+
+OAuth2.0的验证流程如下图所示:
+
+
+
+- 得到授权码code。
+- 使用授权码换取access_token和refesh_token,通常refresh_token比access_token有效期长。
+- 使用access_token获取用户openid。
+- 使用access_token和用户openid调用用户授权接口。
+- 使用refresh_token获取新的access_token。
+
+当然,如果是提供给内部应用的API,可以做适当简化,比如用户登录直接返回access_token,凭借此access_token调用授权接口即可。
+
+## 6.1.5 限流
+
+RESTful api应该有限流机制,否则会造成API被滥用甚至被DDOS攻击。可以根据不同的授权访问做不同的限流,以减少服务器压力。
+
+限流的情况可以通过下面几个头部字段返回给请求方:
+
+- X-RateLimit-Limit: 用户每个小时允许发送请求的最大值。
+- X-RateLimit-Remaining:当前时间窗口剩下的可用请求数目。
+- X-RateLimit-Rest: 时间窗口重置的时候,到这个时间点可用的请求数量就会变成 X-RateLimit-Limit 的值。
+
+对于未登录的用户根据IP或者设备ID来限流,对于登录用户根据用户标识。对于超过流量的请求,返回403 forbiden或者429 Too many requests都可以。
+
+## 6.1.6 超文本API
+
+RESTful还有一个非常关键的特性就是超文本API(Hypermedia API),指的是服务器需要在每一个API接口的返回结果中都要提供与下一步操作相关的资源链接, 客户端借助这些实现表现层状态转移。这种设计也被称为 HATEOAS(Hypermedia as the Engine of Application State)。
+
+除此之外,这样做还能够让客户端和服务端解耦,客户端只需要依次遍历返回结果中的超链接就能完成一系列业务逻辑;当服务端做了业务逻辑改动后,也只需要修改服务器返回的资源链接即可。
+
+## 6.1.7 编写文档
+
+RESTful API一般是对接第三方的,因此,文档说明是非常必要的。因此对每一个接口都详细的说明参数含义、数据返回格式和字段意义并举出实际的例子都是非常关键的。
+
+Java Web开发中,我们可以使用Swagger UI + Spring Fox来基于注释生成RESTful API文档。
+
+## 6.1.8 RESTful API实现
+
+Spring MVC、Jersey、Play Framework等主流的Web开发框架都支持RESTful的接口编写。这里我们以Spring MVC为例。
+
+```
+@RequestMapping(value = "/api/notes/{noteId}", method = RequestMethod.GET, headers = "Accept=application/json")
+@ResponseBody
+public UserNote getUserNoteInfo(@PathVariable long noteId) {
+
+ return ...;
+}
+```
+
+此外,OAuth的实现可以使用Spring Security OAuth, 其基于Spring Secutiry实现了OAuth服务。不过,Spring Security OAuth使用稍显复杂,完全可按照OAuth2.0的流程使用Spring MVC + Redis进行实现。
+
diff --git a/book/chapter6-datatrans/rpc.md b/book/chapter6-datatrans/rpc.md
new file mode 100644
index 0000000..5b66a21
--- /dev/null
+++ b/book/chapter6-datatrans/rpc.md
@@ -0,0 +1,351 @@
+# 6.2 RPC
+
+RPC, Remote Procedure Call,故名思议就是远程过程调用,一般都有跨语言支持。大规模分布式应用中普遍使用RPC来做内部服务、模块之间的数据通信,还有助于解耦服务、系统的垂直拆分,使得系统可扩展性更强,并能够让Java程序员用与开发本地程序一样的语法与方式去开发分布式应用程序。
+
+RPC分为客户端(服务调用方)和服务端(服务提供方),都运行在自己的JVM中。客户端只需要引入要使用的接口,接口的实现和运行都在服务端。RPC主要依赖的技术包括序列化、反序列化和数据传输协议。是一种定义与实现相分离的设计:
+
+
+
+目前Java使用比较多的RPC方案主要有RMI、Hessian、Dubbo以及Thrift。
+
+这里需要提出的一点就是,这里的RPC主要指的内部服务之间的调用,因此虽然上一节的RESTful也可以用于内部服务间的调用(跨语言、跨网段、跨防火墙),但其主要用途还是为外部系统提供服务,因此本节没有将其包含在内。
+
+## 6.2.1 RMI
+
+RMI,remote method invoke, 远程方法调用。是JAVA自带的远程方法调用工具,其基于TCP连接,可以使用任意端口,不易跨网段调用,不能穿越防火墙。但它是JAVA语言最开始时的设计,后来很多框架的原理都基于RMI。其调用逻辑如下图所示:
+
+
+
+1. 服务注册:服务端注册服务绑定到注册中心registry。
+2. 服务查找:客户端根据服务名从注册中心查询要使用的接口获取引用。
+3. 服务调用:Stub序列化调用参数并将其发送给Skeleton,后者调用服务方法,并将结果序列化返回给Stub。
+
+其序列化和反序列化使用的都是JDK自带的序列化机制。
+
+这里服务注册管理中心是在服务端的。其实这个可以完全独立出来作为一个单独的服务,其他的RPC框架很多都是选择zookeepr充当此角色。
+
+可以使用Spring那一节讲的RmiServiceExporter和RmiProxyFactoryBean来使用RMI。
+
+## 6.2.2 Hessian
+
+Hessian是一个基于HTTP协议的RPC方案,其序列化机制是自己实现的,负载均衡和容错需要依赖于Web容器/服务。其体系结构和RMI类似,不过并没有注册中心Registry这一角色,而是通过使用地址来显式调用。其中需要使用HessianProxyFactory根据配置的地址create一个代理对象。使用此代理对象去调用服务。
+
+
+
+和RMI一样,可以使用Spring那一节讲的HessianServiceExporter和HessianProxyFactoryBean来使用。
+
+## 6.2.3 Thrift
+
+Thrift是Facebook开源的RPC框架,现已进入Apache开源项目。其采用接口描述语言(IDL)定义 RPC 接口和数据类型,通过编译器生成不同语言的代码(支持 C++,Java,Python,Ruby等),数据传输采用二进制格式,是自己实现的序列化机制。没有注册中心的概念。
+
+
+
+Thrift的使用需要先编写接口的IDL,然后使用它自带的工具生成代码。
+
+```
+namespace java me.rowkey.pje.datatrans.rpc.thrift
+
+typedef i32 int
+service TestService
+{
+ int add(1:int n1, 2:int n2),
+}
+
+//代码生成
+thrift --gen java TestService.thrift
+```
+
+以上即可在gen-java目录下生成TestService的Java代码TestService.java, 其中的核心是接口TestService.Iface,实现此类即可提供服务。需要注意的是Thrift有一个问题就是在接口比较多的时候,生成的Java代码文件太大。
+
+服务提供方:
+```
+TProcessor tprocessor =
+ new TestService.Processor(new TestServiceImpl());
+
+TServerSocket serverTransport = new TServerSocket(8088);
+TServer.Args tArgs = new TServer.Args(serverTransport);
+tArgs.processor(tprocessor);
+tArgs.protocolFactory(new TBinaryProtocol.Factory());
+
+// 简单的单线程服务模型
+TServer server = new TSimpleServer(tArgs);
+server.serve();
+```
+
+服务消费方:
+
+```
+TTransport transport = new TSocket("localhost", 8088, TIMEOUT);
+TestService.Client testService =
+ new TestService.Client(new TBinaryProtocol(transport));
+transport.open();
+
+int result = testService.add(1,2);
+...
+```
+
+这里需要说明的一点就是,Thrift提供了多种服务器模型、数据传输协议以及传输层供选择:
+
+- 服务提供者的服务模型除了上面用的TSimpleServer简单单线程服务模型,还有几个常用的模型:
+
+ - TThreadPoolServer:线程池服务模型,使用标准的阻塞式IO,预先创建一组线程处理请求。
+ - TNonblockingServe:非阻塞式IO。
+ - THsHaServer: 半同步半异步的服务端模型。
+
+- 数据传输协议除了上面例子使用的BinaryProtocol二进制格式,还有下面几种:
+
+ - TCompactProtocol : 压缩格式。
+ - TJSONProtocol : JSON格式。
+ - TSimpleJSONProtocol : 提供JSON只写协议, 生成的文件很容易通过脚本语言解析。
+
+- 传输层除了上面例子的TServerSocket和TSocket,还有
+
+ - TFramedTransport:以frame为单位进行传输,非阻塞式服务中使用。
+ - TFileTransport:以文件形式进行传输。
+ - THttpClient: 以HTTP协议的形式进行传输。
+
+## 6.2.4 Dubbo
+
+Dubbo是阿里开源的服务治理框架。与前面讲的几个RPC协议相比,Dubbo不仅仅是一个RPC框架,还包含了服务治理方面的很多功能:
+
+- 服务注册
+- 服务自动发现
+- 负载均衡
+- 集群容错
+
+这里仅仅针对Dubbo的RPC协议来讲,其传输是基于TCP协议的,使用了高性能的NIO框架Netty,序列化可以有多种选择,默认使用Hessian的序列化实现。Dubbo默认使用Zookeeper作为服务注册、管理中心。
+
+
+
+一个基于Spring XML配置的使用例子如下:
+
+- 服务提供者XML配置
+
+ ```
+
+
+
+
+
+
+
+ ```
+
+- 服务消费者XML配置
+
+ ```
+
+
+
+
+
+
+
+ ```
+
+ 在相关bean中注入emailService即可使用。
+
+## 6.2.5 序列化
+
+序列化是RPC的一个很关键的地方,序列化、反序列的速度、尺寸大小都关系着RPC的性能。包括上面提到的几个序列化协议,现在使用较为普遍的Java序列化协议有以下几种:
+
+1. Java Serialiazer
+
+ JDK自带的序列化机制, 使用起来比较方便。但是其是对象结构到内容的完全描述,包含所有的信息,因此速度较慢,占用空间也比较大,且只支持Java语言。一般不推荐使用。
+
+ 需要注意的是字段serialVersionUID的作用是为了在序列化时保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性。否则如果你在序列化后更改/删除了类的字段,那么再反序列化时就会抛出异常;而如果设置了此字段的值,那么会将不一样的field以type的预设值填充。
+
+ ```
+ //序列化
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ ObjectOutputStream out = new ObjectOutputStream(bout);
+ out.writeObject(obj);
+ byte[] bytes = bout.toByteArray();
+
+ //反序列化
+ ObjectInputStream bin = new ObjectInputStream(new ByteArrayInputStream(bytes));
+ bin.readObject();
+ ```
+
+1. Hessian
+
+ 底层是基于List和Hashmap实现的,着重于数据,附带简单的类型信息的方法,支持多种语言,兼容性比较好, 与JDK序列化相比高效且空间较小;但其在序列化的类有父类的时候,如果有字段相同,父类的值会覆盖子类的值,因此使用Hessian时一定要注意子类和父类不能有同名字段。
+
+ 需要注意的一点,Hessian的实现里有v1和v2两种版本的协议支持,并不兼容,推荐使用Hessian2相关的类。
+
+ 与后来出现的其他二进制序列化工具相比,其速度和空间都不是优势。
+
+ ```
+ //序列化
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Hessian2Output out = new Hessian2Output(os);
+ out.startMessage();
+ TestUser user = new TestUser();
+ out.writeObject(user);
+ out.completeMessage();
+ out.flush();
+ byte[] bytes = os.toByteArray();
+ out.close();
+ os.close();
+
+ //反序列化
+ ByteArrayInputStream ins = new ByteArrayInputStream(bytes);
+ Hessian2Input input = new Hessian2Input(ins);
+ input.startMessage();
+ TestUser newUser = (TestUser)input.readObject();
+ input.completeMessage();
+ input.close();
+ ins.close();
+ ```
+
+1. MsgPack
+
+ MsgPack是一个非常高效的对象序列化库,支持多种语言,有点像JSON,但是非常快,且占用空间也较小,号称比Protobuf还要快4倍。
+
+ 使用MsgPack需要在序列化的类上加@Message注解;为了保证序列化向后兼容,新增加的属性需要加在类的最后面,且要加@Optional注解,否则反序列化会报错。
+
+ 此外,MsgPack提供了动态类型的功能,通过接口Value来实现动态类型,首先将字节数组序列化为Value类型的对象,然后用converter转化为本身的类型。
+
+ MsgPack不足的一点就是其序列化和反序列都非常消耗资源。
+
+ ```
+ //TestUser.java
+ @Message
+ public class TestUser{
+ private String name;
+ private String mobile;
+ ...
+ }
+
+ TestUser user = new TestUser();
+ MessagePack messagePack = new MessagePack();
+
+ //序列化
+ byte[] bs = messagePack.write(user);
+
+ //反序列化
+ user = messagePack.read(bs, TestUser.class);
+ ```
+
+1. Kryo
+
+ Kryo是一个快速高效的Java对象图形序列化框架,使用简单、速度快、序列化后体积小。实现代码非常简单,远远小于MsgPack。但其文档较少,跨语言支持也较差,适用于Java语言。目前Kryo的版本到了4.x, 对于之前2.X之前版本的很多问题都做了修复。
+
+ ```
+ Kryo kryo = new Kryo();
+
+ // 序列化
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Output output = new Output(os);
+ TestUser user = new TestUser();
+ kryo.writeObject(output, user);
+ output.close();
+ byte[] bytes = os.toByteArray();
+
+ // 反序列化
+ Input input = new Input(new ByteArrayInputStream(bytes));
+ TestUser newUser = kryo.readObject(input, TestUser.class);
+ input.close();
+ ```
+
+1. Thrift
+
+ 上面讲的Thrift RPC框架其内部的序列化机制可以单独使用,主要是对TBinaryProtocol的使用。和接口的生成方式类似,需要先定义IDL,再使用Thrift生成。其序列化性能比较高,空间占用也比较少。但其设计目标并非是单独做为序列化框架使用的,一般都是整体作为RPC框架使用的。
+
+ 定义IDL:
+
+ ```
+ //TestUser.thrift
+ namespace java me.rowkey.pje.datatrans.rpc.thrift
+
+ struct TestUser {
+ 1: required string name
+ 2: required string mobile
+ }
+
+ thrift --gen java TestUser.thrift
+ ```
+
+ 使用生成的TestUser类做序列化和反序列化:
+
+ ```
+ TestUser user = new TestUser(); //由thrift代码生成引擎生成
+
+ //序列化
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ user.write(new TBinaryProtocol(new TIOStreamTransport(bos)));
+ byte[] result = bos.toByteArray();
+ bos.close();
+
+ //反序列化
+ ByteArrayInputStream bis = new ByteArrayInputStream(result);
+ TestUser user = new TestUser();
+ user.read(new TBinaryProtocol(new TIOStreamTransport(bis)));
+ bis.close();
+ ```
+
+ 需要注意的是由于Thrift序列化时,丢弃了部分信息,使用ID+Type来做标识,因此对新增的字段属性, 采用ID递增的方式标识并以Optional修饰来添加才能做到向后兼容。
+
+1. Protobuf
+
+ Protobuf是Google开源的序列化框架,是Google公司内部的混合语言数据标准,用于RPC系统和持续数据存储系统,非常轻便高效,具有很好的可扩展性、也具有良好的向后兼容和向前兼容性。与上述的几种序列化框架对比,序列化数据紧凑、速度快、空间占用少、资源消耗较低、使用简单,但其缺点在于需要静态编译生成代码、可读性差、缺乏自描述、向后兼容有一定的约束限制。
+
+ 这里需要注意目前ProtoBuf的版本到了3.x,比2.x支持更多语言但更简洁。去掉了一些复杂的语法和特性,更强调约定而弱化语法。因此,如果是首次使用就直接使用3.x版本。这里也针对Protobuf 3来讲。
+
+ 首先需要编写.proto文件,并使用Protobuf代码生成引擎生成Java代码。
+
+ ```
+ //TestUser.proto
+ syntax = "proto3";
+ option java_package = "me.rowkey.pje.datatrans.rpc.proto";
+ option java_outer_classname = "TestUserProto";
+ message TestUser
+ {
+ string name=1;
+ string mobile=2;
+ }
+
+ protoc --java_out=./ TestUser.proto
+ ```
+
+ 即生成TestUserProto.java,使用此类,即可完成序列化和反序列化:
+
+ ```
+ //序列化
+ TestUserProto.TestUser testUser =
+ TestUserProto.TestUser.newBuilder()
+ .setMobile("xxx")
+ .setName("xxx")
+ .build();
+
+ byte[] bytes = testUser.toByteArray();
+
+ //反序列化
+ testUser = TestUserProto.TestUser.parseFrom(bytes);
+ ```
+
+综上,对以上几个序列化框架做对比如下:
+
+ | 优点 | 缺点
+----|-----|------
+Java | JDK自带实现,包含对象的所有信息| 速度较慢,占用空间也比较大,只支持Java语言
+Hessian | 支持语言比较多,兼容性较好 | 较慢
+MsgPack | 使用简单,速度快,体积小| 兼容性较差,耗资源
+Kryo | 速度快,序列化后体积小 | 跨语言支持较差,文档较少
+Thrift | 高效 | 需要静态编译;是Thrift内部序列化机制,很难和其他传输层协议共同使用
+Protobuf | 速度快 | 需要静态编译
+
+在兼顾使用简单、速度快、体积小且主要使用在Java开发的场景下,Kryo是比较好的方案;如果特别要求占用空间、性能,那么Protobuf则是更好的选择。此外,JSON其实也是一种序列化方式,如果比较关注阅读性的话,那么JSON是更好的选择。
+
+## 6.2.6 提示
+
+面对这些RPC框架,选择的时候应该从以下几方面进行考虑:
+
+- 是否允许代码侵入:即是否需要依赖相应的代码生成器生成代码,比如Thrift需要,而Dubbo、Hessian就不需要。
+- 是否需要长连接、二进制序列化获取高性能:如果需要性能比较高,那么果断选取基于TCP的Thrift、Dubbo。
+- 是否需要跨网段、跨防火墙:这种情况一般就需要选择基于Http协议的,Hessian和Thrift的HTTP Transport。
+- 是否需要跨语言调用:Thrift、Hessian对于语言的支持是比较丰富的,而Dubbo目前只支持Java语言。
+
+此外,除了上述框架之外,Google推出的基于HTTP 2.0的gRPC框架也开始得到了应用,其序列化协议基于Protobuf, 网络框架使用了Netty4。但其需要生成代码,可扩展性也比较差。
+
+
diff --git a/book/chapter7-java/README.md b/book/chapter7-java/README.md
new file mode 100644
index 0000000..7e9aaab
--- /dev/null
+++ b/book/chapter7-java/README.md
@@ -0,0 +1,18 @@
+# 第七章 Java编程进阶
+
+根据网络可以找到的资料以及笔者能够打听到的消息,目前国内外著名的几个大型互联网公司的主要语言选型如下:
+
+1. Google: C/C++、Go、Python、Java、JavaScript,不得不提的是Google贡献给Java社区的Guava包质量非常高,非常值得学习和使用。
+1. Youtube、豆瓣: Python。
+1. Fackbook、Yahoo、Flickr、新浪:PHP(优化过的PHP VM)。
+1. 网易、阿里、搜狐: Java、PHP、Node.js。
+1. Twitter: Ruby -> Java,之所以如此就在于与JVM相比,Ruby的runtime是非常慢的。并且Ruby的应用比起Java还是比较小众的。不过最近Twitter有往Scala上迁移的趋势。
+
+可见,虽然最近这些年很多言论都号称Java已死或者不久即死,但是Java的语言应用占有率一直居高不下。与高性能的C/C++相比,Java具有GC机制,并且没有那让人望而生畏的指针,上手门槛相对较低;而与上手成本更低的PHP、Ruby等脚本语言来说,又比这些脚本语言有性能上的优势(暂且忽略FB自己开发的HHVM)。而且,Java也在不断的吸收其他语言的优势,优化自身的实现和使用。如果说Java编程是Java工程师最为基础的技能点,那么掌握其中的高级特性则是利用Java语言优势的关键。这些技能可以提高Java工程师的开发效率、代码质量以及Java应用的性能。本章就主要讲述相关知识:
+
+ - Java内存管理:了解Java是如何做内存管理的才能从根本上掌握Java的编程技巧,避免一些内存问题的出现。
+ - Java网络编程:了解网络编程模型有助于使用Java做网络编程并能够更好的优化实现。
+ - Java并发编程:并发是提升应用性能非常关键的手段。
+ - Java开发利器:了解Java中常用的工具类库,能够大大提升编程开发效率。
+ - Java新版本特性:Java7、8、9带来了一些新特性提升开发效率和程序性能。
+
diff --git a/book/chapter7-java/concurrent.md b/book/chapter7-java/concurrent.md
new file mode 100644
index 0000000..51fbc6b
--- /dev/null
+++ b/book/chapter7-java/concurrent.md
@@ -0,0 +1,223 @@
+# 7.3 Java并发编程
+
+随着计算机技术的发展,CPU正从以前通过提升频率转变为通过增加核数来提升整体性能,而并发就是利用多核特性的关键技术。本节主要讲述Java语言下的并发编程。
+
+## 7.3.1 并发原理
+
+要了解并发编程,需要先了解并发问题产生的原因,从原理层面理解并发。
+
+### 并发与并行
+
+并发和并行是看起来差不多实则大不一样的两个概念。并发指的是同时应对多件事件的能力,而并行则指同时做多件事情的能力。比如你能够一边吃饭一边看电视这叫做并发,但是你同时是不可能做多件事情的,所以不是并行。而一个班级的学生一起来打扫教室的卫生,同时在扫地、擦黑板以及拖地这就是并行的。
+
+对于程序员来说通常说的并行大多指的是任务级别的并行,而除此之外,计算机在其他层面也有其他的并行实现:
+
+- 位级并行: 32位的计算机能够同时处理32位数的运算,而8位的计算机却要进行多次计算。
+- 指令级并行:虽然从表面上看CPU都是在串行执行的,但是内部使用了流水线、乱序执行和猜测执行。
+- 数据级并行:可以并行的在大量数据上施加同一类操作,图像处理就是一种非常适合数据级并行的场景。
+
+对于我们常说的任务级并行,其依赖的基础是计算机的多处理器架构,主要有以下三种:
+
+1. SMP: Symmetric Multi-Processor,对称多核架构,也可以叫做统一内存访问架构(UMA,是相对于后面的NUMA来讲的), 其主要特性就在于所有CPU平等地(无主次或者从属关系)共享所有资源,包括内存、IO、总线等。如下图所示:
+
+ 
+
+
+ 此种架构,所有CPU都公用一个总线访问内存,并且每个CPU都有自己的缓存。由于缓存相互独立,有一个关键的问题即缓存一致性问题,即如何保证内存中相同地址的数据在各个缓存中保持一致。解决这个问题有很多协议,常见的MESI协议定义了一些缓存读写需要遵循的规范:
+
+ - Modified:本CPU写,则直接写到Cache,不产生总线事务;其它CPU写,则不涉及本CPU的Cache,其它CPU读,则本CPU需要把Cache line中的数据提供给它,而不是让它去读内存。
+ - Exclusive:只有本CPU有该内存的Cache,而且和内存一致。本CPU的写操作会导致转到Modified状态。
+ - Shared:多个CPU都对该内存有Cache,而且内容一致。任何一个CPU写自己的这个Cache都必须通知其它的CPU。
+ - Invalid:一旦Cache line进入这个状态,CPU读数据就必须发出总线事务,从内存读。
+
+ 通过扩展CPU数目可以提升此种架构的性能,但是相关实验证明,SMP服务器CPU利用率最好的情况是2至4个CPU。
+
+2. NUMA(Non-Uniform Memory Access)
+
+ 非一致性内存访问相对于SMP来讲,是其由多个CPU组构成的,每一个CPU组由多个CPU组成,都有自己独立的内存、总线、IO等。不同的CPU组之间可以通过互连模块互相通信和交互,使得每一个CPU都可以访问整个服务器的所有内存,但是访问本地内存的效率远远高于远程内存。
+ 
+
+3. MPP(Massive Parallel Processing)
+
+ 大规模并行处理架构也可以叫做分布式内存架构,即CPU单元是地理上隔离的,其交互使用网络来进行(受控的数据交换),其设计初衷就是消除共享资源。每个节点是一个独立的SMP服务器,只能访问自己的本地资源,是一种完全无共享的结构,因而扩展能力最好,经常用做海量数据分析架构,如大数据领域的Impala、Presto都是MPP架构。从总体结构上看,有点类似于Hadoop大数据集群(本质上是不同的两个概念)。
+
+ 
+
+本文的并发是宏观上的一个概念,到了微观层面由于CPU数目的限制仍然是串行或者部分串行的。
+
+### Java内存模型
+
+提到Java中的并发问题,其关键的一点就在于其内存模型,如下图所示:
+
+
+
+如上图所示,有点类似于SMP,但Java内存模型是屏蔽了底层硬件环境的差异给Java程序提供了统一的内存访问模式,可以认为是一种虚拟内存环境。在Java程序中,所有线程都共享主内存,但是对于每一个线程都有自己的工作内存(一个虚拟的概念,包括寄存器、栈、写缓冲区、缓存以及其他硬件、编译器优化等),工作内存与主内存通过一些规定的操作来交互同步数据。而线程只能访问自己的工作内存。因此在多线程环境下,很容易造成工作内存数据不一致而引起并发问题。
+
+基于Java内存模型,Java的运行时内存可以做以下类比:主内存就是堆,用来保存对象信息;工作内存就是栈用来保存线程私有的对象地址、基本类型、局部变量、PC指针等信息。当然,这个类比并不具有实际意义,因为这两者就不相关。
+
+以上经常造成的一个现象就是:当多个线程同时访问同一内存时,在每个线程的工作内存中的缓存不一致。比如主内寸地址A处的数据,在线程1中缓存为a1,在线程2中缓存为a2,开始时a1=a2,但当某一个线程修改了自己工作内存中的值,却又没有采取相应的内存同步措施时就会造成A地址的值在线程1、2中不一样。
+
+### 重排序
+
+在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,可以分为三种:
+
+- 编译器优化的重排序:在不改变单线程程序语义的前提下,可以重新安排语句执行顺序。
+- 指令级并行的重排序:对应于上面所说的指令级并行技术。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
+- 内存重排序:由于处理器使用缓存、读/写缓冲区,使得加载和存储操作看上去可能是在乱序执行。
+
+
+
+指令和内存重排序都属于处理器重排序。
+
+这些基础层面的重排序会遵循以下规范以保证程序的正确性:
+
+- 数据依赖性:即如果两个操作访问同一个变量,且这两个操作中有一个为写操作,那么这两个操作之间就存在数据依赖性。此时,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。这里需要注意的是不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
+- as-if-serial语义:不管怎么重排序,单线程程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守此语义。
+
+编译器和处理器的重排序由于未考虑到多线程、多处理器的情况,因此在并发环境下会造成不可预知的问题。
+
+### 并发问题
+
+如上文所述,并发首先带来的问题就是并发的正确性问题,也就是线程安全的问题。这里的线程安全指的是“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的。”
+
+此外,多线程之间还存在一个线程同步的问题,即线程之间如何通信协作。
+
+以上两种问题都是Java编程中需要解决的东西。
+
+## 7.3.2 并发思路
+
+除了重排序和Java内存模型,可变数据是引起线程不安全的最根本原因。如果一个数据不可变,那么无论多少线程同时访问都是不会产生问题的,就好比每个线程都拿到了一个copy,但这copy都是不可变的,也就不可能存在并发问题。针对这些,已经有一些技术方案能够解决线程安全的问题。概括来看,都是围绕着在并发过程中如何处理原子性、可见性以及有序性来进行的。
+
+### happens-before
+
+为了在一定程度上解决上文提到的编译器、处理器的重排序问题,从Java5开始提出了happens-before(先行发生原则)的概念,通过此概念阐述操作之间的内存可见性:如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这两个操作既可以在一个线程内也可以在不同线程之间。通过此原则,可以判断数据是否存在竞争、线程是否安全。
+
+- 程序次序法则:在一个线程内如果编码中A操作写在B操作之前,那么happens before。
+
+- 监视器锁法则: 对一个监视器的解锁一定发生在后续对同一监视器加锁之前。锁必须是同一锁。
+
+- volatile变量法则:写volatile变量一定发生在后续对它的读之前。
+
+- 线程启动法则: Thread.start一定发生在线程中的动作之前。
+
+- 线程终结法则: 线程中的任何动作一定发生在括号中的动作之前(其他线程检测到这个线程已经终止,从Thread.join调用成功返回,Thread.isAlive()返回false)。
+
+- 线程中断法则:一个线程调用另一个线程的interrupt一定发生在另一线程发现中断之前,通过Thread.interrupted()方法检测到是否有中断发生。
+
+- 对象终结法则:一个对象的构造函数结束一定发生在对象的finalize()方法之前。
+
+- 传递性:A发生在B之前,B发生在C之前,A一定发生在C之前。
+
+此原则在一定程度上解决了可见性和有序性的问题,是Java内存模型中的“天然的”先行发生关系,无须任何同步器协助就已经存在。如果两个操作不在上述的规则中,也无法通过以上规则推导出来,那么虚拟机就可以随意对它们进行重排序。需要注意的是,这里的先行发生原则和时间先后顺序之间基本没有关系。
+
+### 原子性
+
+原子性指的是对于此对象的操作要么成功要么失败,不会存在中间状态。使用具有原子性对象的方法是线程安全的。
+
+Java中类型的原子性如下:
+
+- 对象类型
+ - 对象地址原子读写,线程安全。
+ - 并发读不可变状态,线程安全。
+ - 并发读写可变状态,非线程安全。
+
+- 基本类型
+ - int、char数值读写,线程安全。
+ - long、double读/写分为高低位两部分,非线程安全。
+ - i++等组合操作,非线程安全。
+
+以上,对于具有原子性的操作是可以保证线程安全的;对不具有原子性的操作,可以使用synchronized或者volatile关键字使其具有原子性。但需要注意的是:当变量的值由自身的上一个决定时,如n=n+1、n++ 等,volatile关键字将失效。而对于基本数据类型则可以使用其对应的原子包装类如AtomicLong、AtomicInteger来获得读写、自增等操作的原子性。
+
+### 可见性
+
+可见性指的是当一个对象在多个线程工作内存中存有副本时,如果一个内存修改了共享变量,其他线程也能够看到被修改后的值。
+
+- final:初始化final字段确保可见性,这里需要注意final修饰基本数据类型,可以保证此字段的可见性,但是如果修饰的是对象,那么需要保证此对象是状态不可变的才能保证可见性,即保证对象的每个字段也是final的。
+- volatile: 此关键字的语义保证了新值能够立即同步到主内存,并且每次使用前都立即从主内存刷新。volatile保证了多线程操作时变量的可见性。
+- synchronized: 通过锁定和解锁某个监视器同步所有变量的值,确保了同步块内读写字段的可见性。
+- 如上文所述,happens-before解决了大多数可见性问题。
+
+### 有序性
+
+- happens-before解决了Java中“天然的”有序问题:在一个线程中所有操作都是有序的;其他符合happens-before原则的或者可以推导出的操作都是有序的。
+- volatile可以创建内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置)禁止指令重排序。
+- synchronized“一个变量在同一时刻只允许一条线程对其进行lock操作”,使得持有同一锁的两个同步块只能串行地进入。
+
+### 提示
+
+以上,可见使用volatile可以在一定程度上解决并发问题,并且由于其开销较少,在其语义能够解决问题的情况下,优先选择volatile,不过其相对于非volatile的变量来说也是有开销的,这也是Effective Java中的DCL(Double Check Locking)的实现多使用了一个本地变量能够提高25%效率的原因。此外,可以发现synchronized貌似是万能的,但是万能的东西通常会伴随着性能问题。
+
+## 7.3.3 并发工具
+
+JDK自身以及一些第三方类库提供了一些工具类帮助我们来解决并发问题,进行并发编程。
+
+### 锁
+
+锁是通过互斥同步来解决线程安全问题的。万能的synchronized关键字是锁的一种。除此之外,ReentrantLock是另一种锁方案。字面上即为可重入锁。这里的可重入指的是:同一线程,外层函数获得锁之后内层递归函数如果仍然有获取该锁的代码,不受影响。synchronized也是可重入的。
+
+ReentrantLock与synchronized相比,更加灵活,且具有等待可中断、可实现公平锁、可以绑定多个条件等特性:
+
+- 等待可中断: 当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
+- 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock默认也是非公平的,但听过配置可以使用公平锁。
+- 绑定多个条件:synchronized中,锁对象的wait、notify或者notifyAll可以实现一个隐含的条件,如果要和多于一个条件进行关联,那么不得不额外的添加一个锁。而ReentrantLock可以通过newCondition方法绑定多个条件。
+
+此外,ReentrantLock通过使用condition的await和signal可以做到线程间通信的目的。
+
+### 无锁
+
+除了使用锁解决并发问题,还有一些无锁技术可以解决并发问题。
+
+- CAS: compare and swap,这是一种类似于乐观锁的机制,需要操作系统的支持。每次更新值得时候都使用旧值与变量的当前值做比较,如果相同则进行更新,否则重试直到成功。上面提到的AtomicInteger、AtomicLong等原子包装类型就使用了CAS。
+- ThreadLocal:本地存储变量,这样每一个线程都有一份数据的拷贝,也就不会存在并发问题了。
+- 不可变对象:不可变对象自然不会有并发问题。
+
+这里还需要提到AQS(全称AbstractQueuedSynchronizer,基于CAS来保证线程安全),是Java并发包中的一个类,提供了一些模板方法供子类实现,从而实现了不同的同步器:Sync、FairSync、NonfairSync等。ReentrantLock、ReentrantReadWriteLock以及ThreadPoolExecutor都使用了AQS。
+
+### 并发集合
+
+并发集合指的就是Java自身提供的java.util.concurrent下的集合类,以下是常用的几个:
+
+- ConcurrentHashMap:是HashMap的线程安全版本,与使用Collecitons.synchronizedXX方法包装不安全的HashMap相比,ConcurrentHashMap是依据bucket做的锁,锁的粒度减小使得性能得到了提高。
+- CopyOnWriteArrayList:此类是ArrayList的线程安全版本,用了持久化数据结构,即当写入list的时候会使用原list的一个副本来进行操作。
+- LinkedBlockingQueue:阻塞队列实现。通过阻塞来解决线程安全问题。
+
+### 同步器
+
+同步器的类也位于java.util.concurrent包下。主要有以下几个:
+
+- CountDownLatch:使用它可以实现类似计数器同步的功能。比如某一个任务需要等待其他n个任务执行完毕之后才能执行,就可以利用CountDownLatch来实现。
+- CyclicBarrier:可以实现让一组线程等待至某个状态之后再全部同时执行。Cyclic指的是当所有等待线程都被释放以后,CyclicBarrier可以被循环使用。调用await()方法之后,线程就处于Barrier状态。
+- Semaphore:基于计数的信号量,可以控制同时访问的线程个数。通过acquire()获取一个许可,如果获取不到就等待。后续通过release()释放一个许可。当计数值为1时,成为一种类互斥锁。
+
+### 并发框架
+
+- Executor:这个是Java自带的一个并发框架,封装了并发常用的一些操作。通过调用Executors的newXXX方法可以创建各种ExecutorService(可以看做线程池),包括单线程、固定数目线程池、可扩展线程池、定时调度线程池等,之后使用ExecutorService的execute和submit方法运行任务,使用scheduleWithFixedDelay和scheduleAtFixedRate周期性运行任务。这里需要注意的一点是,submit和周期性调度方法都对任务的异常做了捕获(本质是FutureTask对异常做了捕获),当任务代码抛出异常时,无法拿到错误,并且当使用的是周期调度时任务抛出异常后即会停止,不会再周期进行下去。所以需要对任务(Runnable或者Callable)捕获异常并处理。Spring实现的ThreadPoolTaskExecutor同样是这种行为,但ThreadPoolTaskScheduler则使用ErrorHandler对异常做了处理。
+
+- Fork/Join:此框架是Java7带来的一个并发框架。原理类似于MapReduce,是基于分治法的一种方案,任务可以递归地分解为子集。这些子集可以并行处理,然后每个子集的结果被归并到一个结果中。和ThreadPoolExecutor类一样,它也实现了Executor和ExecutorService接口。但与我们平常使用ThreadPoolExecutor相比,其Work Stealing(工作窃取)可以使得各个线程的负载能够均衡,不会存在任务分配不均的情况。
+
+- Actor: 一种并发模式,在JVM上的实现是Akka框架。它的“任其崩溃”哲学在很多场景下都很有意义:每个Actor之间都是互相独立的,互不影响,只要考虑最自然的业务逻辑即可,无须做“防御式编程”。除此之外,Actor也能够做分布式通信工具,作为RPC的一种方案。这里需要注意的是,标准的Actor(如Erlang中的)是和线程没有关系的,其是一个独立于操作系统的一种实体,可以认为是和线程并列的概念。但是Akka的实现中,Actor并不具有线程的特性,无法被系统调度,也无法主动让出CPU。因此,在JVM上,Actor的使用需要慎重。况且,如下图所示,Actor最终的处理还是依靠的线程池。
+
+ 
+
+此外,在消息中间件一节讲过的Disruptor也是一种高性能并发框架。
+
+## 7.3.4 并发编程建议
+
+1. 给线程命名,这样可以帮助调试。
+
+1. 为了充分利用又不过分利用CPU,线程数的计算公式为CPU数目*(W+C)/C,其中W为线程平均等待时间,C为线程平均运行/计算时间。此外,还需要考虑内存的限制(线程栈的大小)以及系统对线程数目的限制。
+
+1. 使用不可变类,所有属性和类都是final不可变的,可以保证线程安全。
+
+1. 总是按照一个全局的固定的顺序获取多把锁,可以避免死锁的产生。实例可以参考经典的哲学家就餐问题。
+
+1. 最小化同步的范围,而不是将整个方法同步,只对关键部分做同步。
+
+1. 分段锁:ConcurrentHashMap就是采用的此种方式。
+
+1. 如果可以,更偏向于使用 volatile 而不是 synchronized。
+
+1. 使用更高层次的并发工具,而不是使用wait()和notify()来实现线程间通信,如BlockingQueue、CountDownLatch 及 Semaphore。
+
+1. 优先使用并发集合,而不是对集合进行同步,并发集合能够提供更好的可扩展性。
+
diff --git a/book/chapter7-java/end.md b/book/chapter7-java/end.md
new file mode 100644
index 0000000..9f3ffdc
--- /dev/null
+++ b/book/chapter7-java/end.md
@@ -0,0 +1,34 @@
+# 7.6 总结
+
+本章主要讲述了Java开发中的一些高级特性,包括内存管理、网络编程、并发编程,并介绍了常用的Java工具库以及Java7、8、9一些值得使用的新特性。
+
+了解并掌握这些,能够使得在构建Java应用时能够使用更高级的技能,从而可以提升编码效率和编码质量。
+
+除此之外,在平时的Java开发中,还有一些容易被忽视的点也需要大家了解:
+
+- float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用java.math.BigDecimal。但是如果使用BigDecimal(double val)此构造方法那么由于小数的double底层存储是一个不确定的数字使得构造的BigDecimal也不是一个确定的数字,应该使用BigDecimal(String val)构造方法做精确计算。
+- 使用基于数组的集合时,如ArrayList、HashMap时必须指定初始化大小,否则大小不足时,会成倍扩容。
+- String自带的split方法是基于正则的,尽量避免使用。
+- DateFormat类以及子类是非线程安全的,在多线程环境下不能使用单例。
+- 能够避免使用正则表达式的地方尽量避免使用。正则运算对CPU的消耗是非常大的,而且会在某些偶然场景下触发死循环正则运算。
+- JSON的序列化和反序列化也都非常消耗CPU,除非必须得用,尽量避免使用,尤其只为了打印类的表示信息时。
+- 做时间差值相关的统计时为了防止时间调整带来的影响,推荐使用System.nanoTime()而不是System.currentTimeMillis()来记录时间值。其返回的是纳秒,来源于CPU时钟。但需要注意此值仅可用于测量同一台机器的时间差值,切忌用在不同机器上。
+
+## 学习资料
+
+### 书籍
+
+- [《Java核心技术(卷1)》](https://book.douban.com/subject/3146174/):学习Java必备的黄皮书,入门推荐书籍
+- [《Java核心技术(卷2)》](https://book.douban.com/subject/3360866/):黄皮书之高级特性
+- [《Java并发编程实战》](https://book.douban.com/subject/10484692/): 对Java并发库讲得非常透彻
+- [《Effective Java》](https://book.douban.com/subject/3360807/):Java之父高司令都称赞的一本Java进阶书籍
+- [《写给大忙人看的Java SE 8》](https://book.douban.com/subject/26274206/):涵盖了Java8带来以及Java7中被略过的新的Java特性,值得一看
+
+### 资料
+
+- Socket编程:
+- NIO:
+- 序列化:
+- RPC框架:
+- 并发编程:
+
diff --git a/book/chapter7-java/java-mm.md b/book/chapter7-java/java-mm.md
new file mode 100644
index 0000000..78624cc
--- /dev/null
+++ b/book/chapter7-java/java-mm.md
@@ -0,0 +1,343 @@
+# 7.1 Java内存管理
+
+对于一个Java程序员来说,大多数情况下的确是无需对内存的分配、释放做太多考虑,对JVM也无需有多么深的理解的。但是在写程序的过程中却也往往因为这样而造成了一些不容易察觉到的内存问题,并且在内存问题出现的时候,也不能很快的定位并解决。因此,了解并掌握Java的内存管理是一个合格的Java程序员必需的技能,也只有这样才能写出更好的程序,更好地优化程序的性能,避免出现内存问题。
+
+对于Java来说,最终是要依靠字节码运行在JVM上的。常见的JVM有以下几种:
+
+- Oracle HotSpot
+- Oracle JRockit(原来的Bean JRockit)
+- IBM J9
+- Dalvik(Android)
+- ART(Android4.4后引入)
+
+其中以HotSpot应用最广泛,开源的OpenJDK也是使用的此JVM。目前Oracle JDK的最新GA版本已经到了9,但鉴于现在JDK8、9并未完全普及,因此本文仅仅针对HotSpot虚拟机的JDK7版本来讲。
+
+## 7.1.1 JVM虚拟机内存
+
+Java内存管理指的就是对JVM虚拟机的内存管理,需要从内存组成、内存分配过程、对象访问等几个方面来进行讲述。
+
+### Java运行时内存区
+
+Java的运行时内存组成如下图所示:
+
+
+
+这些组成部分有一些是线程私有的,其他则是线程共享的。
+
+1. 线程私有
+
+ - 程序计数器: 当前线程所执行的字节码的行号指示器。
+ - Java虚拟机栈: Java方法执行的内存模型,每个方法被执行时都会创建一个栈帧,存储局部变量表、操作栈、动态链接、方法出口等信息。每个线程都有自己独立的栈空间;线程栈只存基本类型和对象地址;方法中局部变量存放在线程空间中。
+ - 本地方法栈: Native方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一。
+
+2. 线程共享
+
+ - Java堆:存放对象实例,几乎所有的对象实例及其属性都在这里分配内存。此外,JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer),也是每个线程的缓冲区。默认设定为占用Eden Space的1%。在编译器做逃逸分析的时候,根据分析的结果,决定是否在栈上还是堆上分配内存,如果是堆上则再分析是否在TLAB上分配内存。在TLAB上分配由于是线程私有,没有锁的开销,效率比较高。
+ - 方法区:存储已经被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等数据,也被称作永久代。这里需要注意的是Java7已经把字符串常量池移动到了堆中,调用String的intern方法时,如果堆中的存在相同的字符串对象,则会直接保存对象的引用,不会重新创建对象。
+ - 直接内存:NIO、Native函数直接分配的堆外内存。DirectBuffer引用也会使用此部分内存。
+
+### 内存分配过程
+
+1. 编译器通过逃逸分析,确定对象是在栈上分配还是在堆上分配。如果是在栈上分配,则将对象打散(每一个字段做为一个局部变量)分配在栈上。
+1. 如果tlab_top + size <= tlab_end,则在在TLAB上直接分配对象并增加tlab_top 的值,如果现有的TLAB不足以存放当前对象则3。
+1. 重新申请一个TLAB,并再次尝试存放当前对象。如果放不下,则4。
+1. 在Eden区加锁(这个区是多线程共享的),如果eden_top + size <= eden_end则将对象存放在Eden区,增加eden_top 的值,如果Eden区不足以存放,则5。
+1. 执行一次Young GC(Minor GC)。
+1. 经过Young GC之后,如果Eden区任然不足以存放当前对象,则直接分配到老年代。
+
+### 对象访问
+
+Java是面向对象的一种编程语言,那么如何通过引用来访问对象呢?一般有两种方式:
+
+1. 通过句柄访问
+
+ 
+
+ 此方式,引用保存的是句柄的地址,需要先定位句柄,再定位对象的实例和类型地址。
+
+2. 直接指针
+
+ 
+
+ 此种方式引用直接保存的是对象实例的地址,对象实例中保存了类型数据的指针。是HotSpot虚拟机采用的方式。
+
+### Java对象大小
+
+一个Java对象的内存占用包括对象头和对象成员变量两部分。其中对象头主要包括mark word(对象的锁信息、GC信息、hash值等)、类信息引用(对象所属的类)、数组长度(数组类型特有)以及padding(填充以按照8字节对齐)。而成员变量包括对其他对象的引用以及基本数据类型。由此可知,一个Java空对象(new Object())的内存占用就是其对象头的占用内存。那么在32位JVM上,mark word是4字节,类信息引用也为4字节,总共占用8字节。64位JVM上,mark word为8字节,类信息引用在开启压缩指针下占用4字节,总共占用12字节,然而由于Java对象是按照8字节对齐分配的,因此实际占用16字节,和未开启压缩指针是一样的。此外,数组也是对象,其有一个记录数组长度的int类型,之后是数组的每一个元素:基本数据类型或者引用。
+
+还需要说明一下提到的压缩指针技术(可以通过-XX:+UseCompressedOops开启)。此技术主要用来在64位JVM中节省对象引用/指针的内存占用(相比32位,64位对象指针内存占用会翻倍)。由于Java对象是8字节对齐, 起始地址最低三位都是0, 开启了压缩指针的64位JVM上会把基地址的偏移量右移三位后保存到32位地址, 访问的时候再把32位地址左移三位,于是就可以使用32位地址访问最大32GB(4GB * 8)的内存(堆+永久代/元空间)。
+
+### 内存溢出
+
+在JVM申请内存的过程中,会遇到无法申请到足够内存,从而导致内存溢出的情况。一般有以下几种情况:
+
+- 虚拟机栈和本地方法栈溢出
+ - StackOverflowError: 线程请求的栈深度大于虚拟机所允许的最大深度。循环递归会触发此种OOM。
+ - OutOfMemoryError: 虚拟机在扩展栈时无法申请到足够的内存空间,一般可以通过不停地创建线程触发此种OOM。
+- Java堆溢出: 当创建大量对象并且对象生命周期都很长的情况下,会引发OutOfMemoryError。
+- 方法区溢出:方法区存放Class等元数据信息,如果产生大量的类(使用CGLIB),那么就会引发此内存溢出,OutOfMemoryError:PermGen space,在使用Hibernate等动态生成类的框架时会容易引起此种情况。
+
+## 7.1.2 垃圾收集理论
+
+在通常情况下,我们掌握Java的内存管理就是为了应对网站/服务访问慢,慢的原因一般有以下几点:
+
+- 内存:垃圾收集占用CPU;放入了太多数据,造成内存泄露。
+- 线程死锁。
+- IO速度太慢。
+- 依赖的其他服务响应太慢。
+- 复杂的业务逻辑或者算法造成响应的缓慢。
+
+其中,垃圾收集对性能的影响一般有以下几个:
+
+- 内存泄露
+- 程序暂停
+- 程序吞吐量显著下降
+- 响应时间变慢
+
+### 垃圾收集的一些基本概念
+
+- Concurrent Collector: 收集的同时可运行其他的工作进程。
+- Parallel Collector: 使用多CPU进行垃圾收集。
+- Stop-the-word(STW): 收集时必须暂停其他所有的工作进程。
+- Sticky-reference-count:对于使用“引用计数”(reference count)算法的GC,如果对象的计数器溢出,则起不到标记某个对象是垃圾的作用了,这种错误称为sticky-reference-count problem,通常可以增加计数器的bit数来减少出现这个问题的几率,但是那样会占用更多空间。一般如果GC算法能迅速清理完对象,也不容易出现这个问题。
+- Mutator:mutate的中文是变异,在GC中即是指一种JVM程序,专门更新对象的状态的,也就是让对象“变异”成为另一种类型,比如变为垃圾。
+- On-the-fly:On-the-fly引用计数垃圾回收,用来描述某个GC的类型。此GC不用标记而是通过引用计数来识别垃圾。
+- Generational GC:这是一种相对于传统的“标记-清理”技术来说,比较先进的GC。特点是把对象分成不同的generation,即分成几代人,有年轻的,有年老的。这类GC主要是利用计算机程序的一个特点,即“越年轻的对象越容易死亡”,也就是存活的越久的对象越有机会存活下去。
+- Safepoint:指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定,信息能够被很好地描述,可以让JVM安全地进行一些操作,包括GC、取消偏向锁、类重定义、线程/堆dump、代码反优化等。这些操作触发进入Safepoint即意味着JVM进程的阻塞停顿(JNI调用不受影响)。Safepoint的位置主要有:
+ - 一个方法返回前。
+ - 调用方法的call指令之后。
+ - 抛出异常的位置。
+ - 循环的末尾,可以防止大循环的时候一直不进入Safepoint,而其他线程在等待。
+
+ 需要特别说明的是最后一点,如果是被JIT编译后的二进制码,则只有在无界循环的末尾才会进入Safepoint,而循环计数使用long类型,即使有次数限制,也被认为是无界循环。有界循环如果没有方法调用或者方法调用被内联则需要一直到循环结束才会进入Safepoint。
+- Safe region: 触发进入Safepoint的操作时,正在运行的线程能够进入到Safepoint,阻塞或者Sleep的线程则无法主动进入。这种线程会被标记为进入了Safe region,其中的引用不会被修改,可以看做一个扩大的Safepoint。等线程被唤醒离开Safe region时则需要判断触发进入Safepoint的操作(GC、类重定义等)是否完成,如果未完成,则挂起。
+
+### 吞吐量与响应时间
+
+牵扯到垃圾收集,还需要搞清楚吞吐量与响应时间的含义:
+
+- 吞吐量是对单位时间内完成的工作量的量度。如:每分钟的Web服务器请求数量。
+- 响应时间是提交请求和返回该请求的响应之间使用的时间。如:访问Web页面花费的时间。
+
+吞吐量与访问时间的关系很复杂,有时可能以响应时间为代价而得到较高的吞吐量,而有时候又要以吞吐量为代价得到较好的响应时间。而在其他情况下,一个单独的更改可能对两者都有提高。
+
+通常,平均响应时间越短,系统吞吐量越大;平均响应时间越长,系统吞吐量越小;但是,系统吞吐量越大,未必平均响应时间越短;因为在某些情况(例如不增加任何硬件配置)吞吐量的增大,有时会把平均响应时间作为牺牲,来换取一段时间处理更多的请求。
+
+针对于Java的垃圾回收来说,不同的垃圾回收器会不同程度地影响这两个指标。例如:并行的垃圾收集器,其关注的是吞吐量,会在一定程度上牺牲响应时间;而并发的收集器,则主要关注的是请求的响应时间,会牺牲吞吐量。
+
+
+
+如图,吞吐量优先的垃圾回收期,有可能某一次请求会特别慢;而响应时间优先的垃圾回收器则会尽量使得每一次的响应时间维持在差不多的水平。
+
+### GC流程和算法
+
+GC的一般流程如下:
+
+- 找出堆中活着的对象。
+- 释放死对象占用的资源。
+- 定期调整活对象的位置。
+
+在找出活着的对象以及释放死对象的有以下算法:
+
+- Mark-Sweep:标记-清除
+- Mark-Sweep-Compact:标记-整理
+- Copying Collector:复制算法
+
+1. Mark-标记
+
+ 从“GC roots”开始扫描(这里的roots包括线程栈、静态常量等),给能够沿着roots到达的对象标记为“live”,最终所有能够到达的对象都被标记为“live”,而无法到达的对象则为“dead”。效率和存活对象的数量是线性相关的。
+
+2. Sweep-清除
+
+ 扫描堆,定位到所有“dead”对象,并清理掉。效率和堆的大小是线性相关的。
+
+3. Compact-压缩
+
+ 对于对象的清除,会产生一些内存碎片,这时候就需要对这些内存进行压缩、整理。包括:relocate(将存货的对象移动到一起,从而释放出连续的可用内存)、remap(收集所有的对象引用指向新的对象地址)。效率和存活对象的数量是线性相关的。
+
+4. Copy-复制
+
+ 将内存分为“from”和“to”两个区域,垃圾回收时,将from区域的存活对象整体复制到to区域中。效率和存活对象的数量是线性相关的。
+
+其中,Copy对比Mark-Sweep:
+
+1. 内存消耗:Copy需要两倍的最大live set内存;Mark-Sweep则只需要一倍。
+2. 效率上:Copy与live set成线性相关,效率高;Mark-Sweep则与堆大小线性相关,效率较低。
+
+### 分代收集
+
+分代收集是目前比较先进的垃圾回收方案。有以下几个相关理论:
+
+- 分代假设:大部分对象的寿命很短,“朝生夕死”,重点放在对新生代对象的收集,而且新生代通常只占整个空间的一小部分。
+- 把新生代里活的很长的对象移动到老年代。
+- 只有当老年代满了才去收集。
+- 收集效率明显比不分代高。
+
+HotSpot虚拟机的分代收集,分为一个Eden区、两个Survivor区以及Old Generation/Tenured区,其中Eden以及Survivor共同组成New Generatiton。结构如下:
+
+
+
+- Eden区是分配对象的区域。
+- Survivor是minor/younger gc后存储存活对象的区域。
+- Old Generation区域存储长时间存活的对象, 也被称作Tenured区。
+
+分代收集中典型的垃圾收集算法组合描述如下:
+
+- 新生代通常使用Copy算法收集,会Stop The World。
+- 老年代收集一般采用Mark-Sweep-Compact, 有可能会Stop The World,也可以是concurrent或者部分concurrent。
+
+通常将对New Generation进行的回收称为Minor GC;对Old Generation进行的回收称为Major GC。但由于Major GC除并发GC外均需对整个堆以及Permanent Generation进行扫描和回收,因此又称为Full GC。那么何时进行Minor GC、何时进行Major GC? 一般的过程如下:
+
+
+
+- 对象在Eden Space完成内存分配。
+- 当Eden Space满了,再创建对象,会因为申请不到空间,触发Minor GC,对New Generation(Eden + S0 或 Eden + S1)进行垃圾回收。
+- Minor GC时,Eden Space不能被回收的对象被放入到空的Survivor(S0或S1,Eden肯定会被清空),另一个Survivor里不能被GC回收的对象也会被放入这个Survivor,始终保证一个Survivor是空的。
+- 在上一步时,如果发现Survivor区满了,则这些对象被copy到old区,或者Survivor并没有满,但是有些对象已经足够Old,也被放入Old区。
+- 当Old区被放满之后,进行Full GC。
+
+但这个具体还需要看JVM是采用的哪种GC方案。新生代New Generation的垃圾回收器均是在Eden Space分配不下时触发GC,而老年代Old Generation的GC有以下几种情况:
+
+1. 对于Serial Old、Parallel Old而言触发机制为:
+
+ - Old Generation空间不足。
+ - Permanent Generation空间不足、
+ - Minor GC时的悲观策略。
+ - Minor GC后在Eden上分配内存仍然失败。
+ - 执行Heap Dump时。
+ - 外部调用System.gc。可通过-XX:+DisableExplicitGC来禁止触发gc,但需要注意的是禁用System.gc()会引起使用NIO时的OOM,所以此选项慎重使用。
+
+1. 对于CMS而言触发机制为:
+
+ - 当Old Generation空间使用到一定比率时触发。通过CMSInitiatingOccupancyFaction来设置。默认值是根据如下公式计算出来的:
+
+ ```
+ ((100 -MinHeapFreeRatio) +(double)(CMSTriggerRatio* MinHeapFreeRatio) / 100.0)/ 100.0,
+ ```
+ 其中,MinHeapFreeRatio默认值为40,CMSTriggerRatio默认值为80。
+ - 当Permanent Generation采用CMS收集且空间使用到一定比率触发,Permanent Generation采用CMS收集需设置:-XX:+CMSClassUnloadingEnabled。可通过CMSInitiatingPermOccupancyFraction来设置。它是根据如下公式计算出来的:
+
+ ```
+ ((100 -MinHeapFreeRatio) +(double)(CMSTriggerPermRatio *MinHeapFreeRatio) / 100.0)/ 100.0
+ ```
+ MinHeapFreeRatio默认值为40,CMSTriggerPermRatio默认值为80。
+ - HotSpot根据成本计算决定是否需要执行CMS GC,可通过-XX:+UseCmsInitiatingOccupancyOnly来去掉这个动态执行的策略。
+ - 外部调用System.gc,且设置了ExplicitGCIInvokesConcurrent或者ExplicitGCInvokesConcurrentAndUnloadsClasses。
+
+## 7.1.3 HotSpot垃圾收集器
+
+
+
+上图即为HotSpot虚拟机的垃圾收集器组成。
+
+### Serial收集器
+
+- -XX:+UserSerialGC参数打开此收集器、
+- Client模式下新生代默认的收集器。
+- 较长的Stop The World时间。
+- 简单而高效。如果堆小于100MB选择此收集器即可。
+
+此收集器的一个工作流程如下如所示:
+
+
+
+在标记复制阶段会Stop The World。
+
+### ParNew收集器
+
+- -XX:+UserParNewGC开启此收集器。
+- +UseConcuMarkSweepGC时默认开启。
+- Serial收集器的多线程版本。
+- 默认线程数与CPU数目相同,可以通过-XX:ParrallelGCThreads指定线程数目。
+
+
+
+不同于Serial收集器,GC阶段是多线程并发进行的。
+
+### Parallel Scavenge收集器
+
+- 是Server模式的默认新生代收集器。
+- 是新生代并行收集器。
+- 采用Copy算法。
+- 主要关注的是吞吐量,吞吐量优先。
+- 使用-XX:MaxGCPauseMillis和-XX:GCTimeRatio两个参数精确控制吞吐量。
+- -XX:UseAdaptiveSizePolicy打开GC自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整Survivor区大小、新生代晋升到老年代的年龄等参数以提供最合适的停顿时间或最大的吞吐量。
+- Parallel是本章讲述的收集器中唯一不进行保留内存(Reserved Memory)释放的收集器,因此当想要达到内存的垂直伸缩时,不能使用此收集器。
+
+其收集流程和ParNew类似, 不同之处在于其各种参数是自适应调节的。
+
+### Serial Old收集器
+
+- Serial的老年代版本。
+- Client模式的默认老年代收集器。
+- CMS收集器的后备预案,Concurrent Mode Failure时使用、
+- -XX:+UseSerialGC开启此收集器。
+
+流程和Serial类似,GC采用标记整理算法。
+
+### Parallel Old收集器
+
+- -XX:+UseParallelGC和-XX:+UseParallelOldGC启用此收集器。
+- Server模式的默认老年代收集器。
+- Parallel Scavenge的老年代版本,使用多线程和“标记-整理”算法。
+- 关注吞吐量,适合CPU资源敏感的场景。
+- 使用Parallel Scavenge + Parallel Old可以达到最大吞吐量保证。
+
+流程和Parallel Scavenge类似,GC采用标记整理算法。
+
+### CMS收集器
+
+- 是并发低停顿收集器,关注的是业务响应时间。
+- -XX:UseConcMarkSweepGC 开启CMS收集器,默认使用ParNew作为新生代收集器,SerialOld作为收集失败的垃圾收集器。
+- 以获取最短回收停顿时间为目标的收集器,重视响应速度,希望系统停顿时间最短,适合互联网应用。
+
+其垃圾回收有四步,如下图所示:
+
+
+- 初始标记:Stop The World。只标记GC roots能直接关联到的对象,速度很快。
+- 并发标记:进行GC roots tracing,与用户线程并发进行。
+- 重新标记:Stop The World,修正并发标记期间因程序继续运行导致变动的标记记录。
+- 并发清除。
+
+CMS有以下的缺点:
+
+- CMS是唯一不进行compact的垃圾收集器,当CMS释放了垃圾对象占用的内存后,它不会把活动对象移动到老年代的一端。
+- 对CPU资源非常敏感。不会导致线程停顿,但会导致程序变慢,总吞吐量降低。CPU核越多越不明显。适用于业务应用CPU使用率不高的场景。
+- 无法处理浮动垃圾。可能出现“Concurrent Mode Failure”失败, 导致另一次Full GC ,可以通过调整-XX:CMSInitiatingOccupancyFraction来控制内存占用达到多少时触发GC。
+- 大量空间碎片。这个可以通过设置-XX:UseCMSCompacAtFullCollection(是否在Full GC时开启compact)以及-XX:CMSFullGCsBeforeCompaction(在进行compact前Full GC的次数)来一定程度解决此问题。
+
+### G1收集器
+
+G1算法在Java6中还是试验性质的,在Java7中已经正式引入,在Java8中开始有了性能上的巨大提升。这里做一下简单介绍:
+
+- -XX:+UseG1GC可以打开此垃圾回收器。
+- 是并发收集器,关注的是业务响应时间。
+- 使用标记-清理算法进行GC。
+- 不会产生碎片。
+- 可预测的停顿时间。
+- 化整为零:将整个Java堆划分为多个大小相等的独立区域。
+- -XX:MaxGCPauseMillis=200可以设置最大GC停顿时间,当然JVM并不保证一定能够达到,只是尽力。
+- 与CMS相比,其适合用于大内存的JVM垃圾回收(一般情况下以4G堆大小为界)。
+
+### GC日志简介
+
+为了便于排查问题,我们通过将JVM 启动参数配置为 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:[log_ path],以记录 GC 日志。输出日志如下:
+
+1403682.561: [GC [PSYoungGen: 1375104K->11376K(1386176K)] 4145665K->2782002K(4182400K), 0.0174410 secs] [Times: user=0.27 sys=0.00, real=0.02 secs]
+
+- 1403682.561:发生的时间点,JVM运行的时间长度,以度为单位,也可以格式化成固定的时间格式(使用-XX:+PrintGCDateStamps)。
+- PSYoungGen:发生了何种类型的GC,此处代表发生了新生代的GC。
+- 1375104K:回收前的大小。
+- 11376K:回收后的大小。
+- 1386176K:YOUNG代的大小。
+- 4145665 K:回收前总的占用大小。
+- 2782002K:回收后的占用大小。
+- 4182400K:总占用大小。
+- 0.0174410:垃圾收集停顿时间。
+- 0.27和0.00:代表在用户态(user)和系统状(sys)的CPU运行时间。
+- 0.02 secs:代表实际的GC的运行时间。
+
+这里需要注意:上面实际GC的运行时间小于用户态和系统态的时间总和,是由于前者仅指CPU的运行时间,包括等待或IO阻塞的时间,而现在的GC是采用多线程收集的,同时机器也是多个CPU,因此,大部分是二者之和要比前面的值大。如果是采用串形化收集器的话,二者时间几乎相差不多。
+
+
+
diff --git a/book/chapter7-java/media/15019263548187.jpg b/book/chapter7-java/media/15019263548187.jpg
new file mode 100644
index 0000000..aeaa265
Binary files /dev/null and b/book/chapter7-java/media/15019263548187.jpg differ
diff --git a/book/chapter7-java/media/15019265858293.jpg b/book/chapter7-java/media/15019265858293.jpg
new file mode 100644
index 0000000..042663a
Binary files /dev/null and b/book/chapter7-java/media/15019265858293.jpg differ
diff --git a/book/chapter7-java/media/15019267044076.jpg b/book/chapter7-java/media/15019267044076.jpg
new file mode 100644
index 0000000..641b241
Binary files /dev/null and b/book/chapter7-java/media/15019267044076.jpg differ
diff --git a/book/chapter7-java/media/15019283989310.jpg b/book/chapter7-java/media/15019283989310.jpg
new file mode 100644
index 0000000..4584bcf
Binary files /dev/null and b/book/chapter7-java/media/15019283989310.jpg differ
diff --git a/book/chapter7-java/media/15019284927662.jpg b/book/chapter7-java/media/15019284927662.jpg
new file mode 100644
index 0000000..94f6c43
Binary files /dev/null and b/book/chapter7-java/media/15019284927662.jpg differ
diff --git a/book/chapter7-java/media/15019285125010.jpg b/book/chapter7-java/media/15019285125010.jpg
new file mode 100644
index 0000000..e019640
Binary files /dev/null and b/book/chapter7-java/media/15019285125010.jpg differ
diff --git a/book/chapter7-java/media/15019285253507.jpg b/book/chapter7-java/media/15019285253507.jpg
new file mode 100644
index 0000000..59c8341
Binary files /dev/null and b/book/chapter7-java/media/15019285253507.jpg differ
diff --git a/book/chapter7-java/media/15019285424762.jpg b/book/chapter7-java/media/15019285424762.jpg
new file mode 100644
index 0000000..a8cbd39
Binary files /dev/null and b/book/chapter7-java/media/15019285424762.jpg differ
diff --git a/book/chapter7-java/media/15019285553513.jpg b/book/chapter7-java/media/15019285553513.jpg
new file mode 100644
index 0000000..2fb0220
Binary files /dev/null and b/book/chapter7-java/media/15019285553513.jpg differ
diff --git a/book/chapter7-java/media/15019297032081.jpg b/book/chapter7-java/media/15019297032081.jpg
new file mode 100644
index 0000000..ae5f874
Binary files /dev/null and b/book/chapter7-java/media/15019297032081.jpg differ
diff --git a/book/chapter7-java/media/15019298385062.jpg b/book/chapter7-java/media/15019298385062.jpg
new file mode 100644
index 0000000..d1aecdb
Binary files /dev/null and b/book/chapter7-java/media/15019298385062.jpg differ
diff --git a/book/chapter7-java/media/15258321731258.jpg b/book/chapter7-java/media/15258321731258.jpg
new file mode 100644
index 0000000..d7c6dd6
Binary files /dev/null and b/book/chapter7-java/media/15258321731258.jpg differ
diff --git a/book/chapter7-java/media/access-direct.png b/book/chapter7-java/media/access-direct.png
new file mode 100644
index 0000000..3b3be1b
Binary files /dev/null and b/book/chapter7-java/media/access-direct.png differ
diff --git a/book/chapter7-java/media/access-object.png b/book/chapter7-java/media/access-object.png
new file mode 100644
index 0000000..c427d2f
Binary files /dev/null and b/book/chapter7-java/media/access-object.png differ
diff --git a/book/chapter7-java/media/actor.png b/book/chapter7-java/media/actor.png
new file mode 100644
index 0000000..a5cf2ca
Binary files /dev/null and b/book/chapter7-java/media/actor.png differ
diff --git a/book/chapter7-java/media/disruptor.png b/book/chapter7-java/media/disruptor.png
new file mode 100644
index 0000000..079a0bd
Binary files /dev/null and b/book/chapter7-java/media/disruptor.png differ
diff --git a/book/chapter7-java/media/hotspot-collector.png b/book/chapter7-java/media/hotspot-collector.png
new file mode 100644
index 0000000..b9a854b
Binary files /dev/null and b/book/chapter7-java/media/hotspot-collector.png differ
diff --git a/book/chapter7-java/media/hotspot-gc.png b/book/chapter7-java/media/hotspot-gc.png
new file mode 100644
index 0000000..def840c
Binary files /dev/null and b/book/chapter7-java/media/hotspot-gc.png differ
diff --git a/book/chapter7-java/media/java-mm.png b/book/chapter7-java/media/java-mm.png
new file mode 100644
index 0000000..511040f
Binary files /dev/null and b/book/chapter7-java/media/java-mm.png differ
diff --git a/book/chapter7-java/media/java-runtime-memory.png b/book/chapter7-java/media/java-runtime-memory.png
new file mode 100644
index 0000000..e3427ff
Binary files /dev/null and b/book/chapter7-java/media/java-runtime-memory.png differ
diff --git a/book/chapter7-java/media/jshell.png b/book/chapter7-java/media/jshell.png
new file mode 100644
index 0000000..c8a38d5
Binary files /dev/null and b/book/chapter7-java/media/jshell.png differ
diff --git a/book/chapter7-java/media/mpp.png b/book/chapter7-java/media/mpp.png
new file mode 100644
index 0000000..c0cd46a
Binary files /dev/null and b/book/chapter7-java/media/mpp.png differ
diff --git a/book/chapter7-java/media/numa.png b/book/chapter7-java/media/numa.png
new file mode 100644
index 0000000..16c4b32
Binary files /dev/null and b/book/chapter7-java/media/numa.png differ
diff --git a/book/chapter7-java/media/output-time.png b/book/chapter7-java/media/output-time.png
new file mode 100644
index 0000000..48e19a2
Binary files /dev/null and b/book/chapter7-java/media/output-time.png differ
diff --git a/book/chapter7-java/media/reorder.png b/book/chapter7-java/media/reorder.png
new file mode 100644
index 0000000..2c13a90
Binary files /dev/null and b/book/chapter7-java/media/reorder.png differ
diff --git a/book/chapter7-java/media/smp.png b/book/chapter7-java/media/smp.png
new file mode 100644
index 0000000..6f44467
Binary files /dev/null and b/book/chapter7-java/media/smp.png differ
diff --git a/book/chapter7-java/network.md b/book/chapter7-java/network.md
new file mode 100644
index 0000000..6b00dd4
--- /dev/null
+++ b/book/chapter7-java/network.md
@@ -0,0 +1,145 @@
+# 7.2 Java网络编程
+
+Java的网络编程主要指的是网络IO的编程。网络IO会牵扯到同步、异步、阻塞、非阻塞这几个词。这几个概念很容易被混淆和误用。因此需要先做几点说明:
+
+- IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者。
+- 阻塞和非阻塞,是函数/方法的实现方式,即在数据就绪之前是立刻返回还是等待,即发起IO请求是否会被阻塞。
+- 一个网络IO读过程是数据从网卡→内核缓冲区→用户内存的过程。同步与异步的区别主要在于数据从内核缓冲区→用户内存这个过程需不需要用户进程等待,即实际的IO读写是否阻塞请求进程。
+
+最适合IO模型的例子应该是咱们平常生活中的去餐馆吃饭这个场景,下文就结合这个来讲解一下经典的几个IO模型。
+
+## 7.2.1 IO模型
+
+UNIX环境下的经典I/O模型包括同步阻塞、同步非阻塞、I/O复用、信号驱动以及异步非阻塞五种。
+
+### 同步阻塞
+
+去餐馆吃饭,点一个自己最爱吃的盖浇饭,然后在原地等着一直到盖浇饭做好,自己端到餐桌就餐。这就是典型的同步阻塞。当厨师给你做饭的时候,你需要一直在那里等着。
+
+网络编程中,读取客户端的数据需要调用recvfrom。在默认情况下,这个调用会一直阻塞直到数据接收完毕,就是一个同步阻塞的IO方式。这也是最简单的IO模型,在通常fd(文件描述符)较少、就绪很快的情况下使用是没有问题的。
+
+
+
+### 同步非阻塞
+
+接着上面的例子,你每次点完饭就在那里等着,突然有一天你发现自己真傻。于是,你点完之后,就回桌子那里坐着,然后估计差不多了,就问老板饭好了没,如果好了就去端,没好的话就等一会再去问,依次循环直到饭做好。这就是同步非阻塞。
+
+这种方式在编程中对socket设置O_NONBLOCK即可。这里需要提一点:此方式仅仅针对网络IO有效,对磁盘IO并没有作用。因为本地文件IO就没有被认为是阻塞,我们所说的网络IO的阻塞是因为网路IO有无限阻塞的可能,而本地文件除非是被锁住,否则是不可能无限阻塞的,因此只有锁这种情况下,O_NONBLOCK才会有作用。而且,磁盘IO时要么数据在内核缓冲区中直接可以返回,要么需要调用物理设备去读取,这时候进程的其他工作都需要等待。因此,后续的IO复用和信号驱动IO对文件IO也是没有意义的。
+
+
+
+此外,需要说明的一点是Nginx和NodeJS中对于本地文件的IO是用线程的方式模拟非阻塞的效果的,而对于静态文件的IO,使用Zero-Copy(例如sendfile)的效率是非常高的。
+
+### IO复用
+
+接着上面的列子,你点一份饭然后循环的去问好没好显然有点得不偿失,还不如就等在那里直到准备好,但是当你点了好几样饭菜的时候,你每次都去问一下所有饭菜的状态(未做好/已做好)肯定比你每次阻塞在那里等着好多了。当然,你问的时候是需要阻塞的,一直到有准备好的饭菜或者你等的不耐烦(超时)。这就引出了IO复用,也叫多路IO就绪通知。这是一种进程预先告知内核的能力,让内核发现进程指定的一个或多个IO条件就绪了,就通知进程。使得一个进程能在一连串的事件上等待。
+
+
+
+IO复用的实现方式目前主要有select、poll和epoll。
+
+select和poll的原理基本相同:
+
+- 注册待侦听的fd,这里的fd创建时最好使用非阻塞。
+- 每次调用都去检查这些fd的状态,当有一个或者多个fd就绪的时候返回、
+- 返回结果中包括已就绪和未就绪的fd。
+
+相比select,poll解决了单个进程能够打开的文件描述符数量有限制这个问题:select受限于FD_SIZE的限制,如果修改则需要修改这个宏重新编译内核;而poll通过一个pollfd数组向内核传递需要关注的事件,避开了文件描述符数量限制。
+
+此外,select和poll共同具有的一个很大的缺点就是包含大量fd的数组被整体复制于用户态和内核态地址空间之间,开销会随着fd数量增多而线性增大。
+
+select和poll就类似于上面说的就餐方式。但当你每次都去询问时,老板会把所有你点的饭菜都轮询一遍再告诉你情况,当大量饭菜很长时间都不能准备好的情况下是很低效的。于是,老板有些不耐烦了,就让厨师每做好一个菜就通知他。这样每次你再去问的时候,他会直接把已经准备好的菜告诉你,你再去端。这就是事件驱动IO就绪通知的方式-epoll。
+
+epoll的出现,解决了select、poll的缺点:
+
+- 基于事件驱动的方式,避免了每次都要把所有fd都扫描一遍。
+- epoll_wait只返回就绪的fd。
+- epoll使用mmap内存映射技术避免了内存复制的开销。
+- epoll的fd数量上限是操作系统的最大文件句柄数目,这个数目一般和内存有关,通常远大于1024。
+
+目前,epoll是Linux2.6下最高效的IO复用方式,也是Nginx、NodeJS的IO实现方式。而在freeBSD下,kqueue是另一种类似于epoll的IO复用方式。
+
+此外,对于IO复用还有一个水平触发和边缘触发的概念:
+
+- 水平触发:当就绪的fd未被用户进程处理后,下一次查询依旧会返回,这是select和poll的触发方式。
+- 边缘触发:无论就绪的fd是否被处理,下一次不再返回。理论上性能更高,但是实现相当复杂,并且任何意外的丢失事件都会造成请求处理错误。epoll默认使用水平触发,通过相应选项可以使用边缘触发。
+
+### 信号驱动
+
+上文的就餐方式还是需要你每次都去问一下饭菜状况。于是,你再次不耐烦了,就跟老板说,哪个饭菜好了就通知我一声吧。然后就自己坐在桌子那里干自己的事情。更甚者,你可以把手机号留给老板,自己出门,等饭菜好了直接发条短信给你。这就类似信号驱动的IO模型。
+
+
+
+流程如下:
+
+- 开启套接字信号驱动IO功能。
+- 系统调用sigaction执行信号处理函数(非阻塞,立刻返回)。
+- 数据就绪,生成sigio信号,通过信号回调通知应用来读取数据。
+
+此种io方式存在的一个很大的问题:Linux中信号队列是有限制的,如果超过这个数字问题就无法读取数据。
+
+### 异步非阻塞
+
+之前的就餐方式,到最后总是需要你自己去把饭菜端到餐桌。这下你也不耐烦了,于是就告诉老板,能不能饭好了直接端到你的面前或者送到你的家里(外卖)。这就是异步非阻塞IO了。
+
+
+
+对比信号驱动IO,异步IO的主要区别在于:信号驱动由内核告诉我们何时可以开始一个IO操作(数据在内核缓冲区中),而异步IO则由内核通知IO操作何时已经完成(数据已经在用户空间中)。
+
+异步IO又叫做事件驱动IO,在Unix中,POSIX1003.1标准为异步方式访问文件定义了一套库函数,定义了AIO的一系列接口。使用aio_read或者aio_write发起异步IO操作,使用aio_error检查正在运行的IO操作的状态。但是其实现没有通过内核而是使用了多线程阻塞。此外,还有Linux自己实现的Native AIO,依赖两个函数:io_submit和io_getevents,虽然IO是非阻塞的,但仍需要主动去获取读写的状态。
+
+需要特别注意的是:AIO是IO处理模式,是一种接口标准,各家操作系统可以实现也可以不实现。目前Linux中AIO的内核实现只对文件IO有效,如果要实现真正的AIO,需要用户自己来实现。
+
+## 7.2.2 Java网络编程模型
+
+上文讲述了UNIX环境的五种IO模型。基于这五种模型,在Java中,随着NIO和NIO2.0(AIO)的引入,一般具有以下几种网络编程模型:
+
+- BIO
+- NIO
+- AIO
+
+### BIO
+
+BIO是一个典型的网络编程模型,是通常我们实现一个服务端程序的过程,步骤如下:
+
+- 主线程accept请求阻塞。
+- 请求到达,创建新的线程来处理这个套接字,完成对客户端的响应。
+- 主线程继续accept下一个请求。
+
+这种模型有一个很大的问题是:当客户端连接增多时,服务端创建的线程也会暴涨,系统性能会急剧下降。因此,在此模型的基础上,类似于
+tomcat的bio connector,采用的是线程池来避免对于每一个客户端都创建一个线程。有些地方把这种方式叫做伪异步IO(把请求抛到线程池中异步等待处理)。
+
+### NIO
+
+JDK1.4开始引入了NIO类库,这里的NIO指的是Non-blcok IO,主要是使用Selector多路复用器来实现。Selector在Linux等主流操作系统上是通过epoll实现的。
+
+NIO的实现流程,类似于select:
+
+- 创建ServerSocketChannel监听客户端连接并绑定监听端口,设置为非阻塞模式。
+- 创建Reactor线程,创建多路复用器(Selector)并启动线程。
+- 将ServerSocketChannel注册到Reactor线程的Selector上。监听accept事件。
+- Selector在线程run方法中无限循环轮询准备就绪的Key。
+- Selector监听到新的客户端接入,处理新的请求,完成tcp三次握手,建立物理连接。
+- 将新的客户端连接注册到Selector上,监听读操作。读取客户端发送的网络消息。
+- 客户端发送的数据就绪则读取客户端请求,进行处理。
+
+相比BIO,NIO的编程非常复杂。
+
+### AIO
+
+JDK1.7引入NIO2.0,提供了异步文件通道和异步套接字通道的实现。其底层在Windows上是通过IOCP,在Linux上是通过epoll来实现的(LinuxAsynchronousChannelProvider.java、UnixAsynchronousServerSocketChannelImpl.java)。
+
+- 创建AsynchronousServerSocketChannel,绑定监听端口。
+- 调用AsynchronousServerSocketChannel的accpet方法,传入自己实现的CompletionHandler。包括上一步都是非阻塞的。
+- 连接传入,回调CompletionHandler的completed方法,在里面,调用AsynchronousSocketChannel的read方法,传入负责处理数据的CompletionHandler。
+- 数据就绪,触发负责处理数据的CompletionHandler的completed方法。继续做下一步处理即可。
+- 写入操作类似,也需要传入CompletionHandler。
+
+其编程模型相比NIO有了不少的简化。
+
+
+### 7.2.3 使用Netty进行网络编程
+
+使用原生的JDK进行网络编程尤其是异步编程是非常繁琐的,而且要面对TCP传输中的诸如
+粘包的问题。Netty即是一个封装了底层网络编程细节的非常好的网络编程框架。现在很多
+与网络IO相关的编程都是基于此框架进行编写的,如在第三章提到过的Vert.x其底层也是Netty。
\ No newline at end of file
diff --git a/book/chapter7-java/newjava.md b/book/chapter7-java/newjava.md
new file mode 100644
index 0000000..0bfff9c
--- /dev/null
+++ b/book/chapter7-java/newjava.md
@@ -0,0 +1,554 @@
+# 7.5 Java新版本特性
+
+JDK7、JDK8是目前用的最为广泛的JDK版本,但根据笔者的面试经历和亲身体验来看,发现有不少人在高版本的JVM上还用着旧版本的编程方式。因此,这里所说的新版本主要指的是Java6以后的版本,包括Java7、Java8、Java9以及Java10。其中,Java9和Java10截止发稿前并未得到广泛应用,本章只会做一些简述。此外,还会介绍一下JVM上的又一个新贵-Kotlin语言,也是谷歌选择的安卓官方开发语言。
+
+## 7.5.1 Java 7
+
+不像JDK 5和8,JDK7只是一个小的版本,没有做太大的改动,只是引入了一些小的改变和特性。其中fork/join框架和内存的一些改动在前面的章节中已经讲过。其他的主要如下:
+
+1. 改进的通用实例创建类型推断
+
+ ```
+ Map> map = new HashMap<>();
+ ```
+
+ 如上钻石运算符<>的引入使得后面的创建实例不必指定类型,可以从引用的声明中推断类型。
+
+1. 自动资源管理
+
+ Java中的InputStream、SQL connection等资源都是需要手动关闭的,以前我们必须通过try final来实现。Java7提供了try-with-resource这个新的语言特性。允许try语句本身申请更多的资源,这些资源作用于try代码块,并自动关闭。只要资源实现了AutoClosable接口即可。
+
+ ```
+ try (BufferedReader br = new BufferedReader(new FileReader("/data/data.txt"))) {
+ br.read();
+ ...
+ }
+ ```
+
+1. switch语句支持字符串
+
+ ```
+ String type = "text";
+ switch (type){
+ case "text":
+ System.out.println("text type");
+ break;
+ case "image":
+ System.out.println("image type");
+ break;
+ }
+ ```
+
+ 需要注意的是最终switch还是编译成对字符串hashcode的switch,然后再做字符串比较。
+
+1. 允许在同一个catch块中捕获多个异常
+
+ ```
+ try {
+ ...
+ } catch (FileNotFoundException | AccessException e) {
+ e.printStackTrace();
+ }
+ ```
+
+1. Path && Files
+
+ Java7中文件IO发生了很大的变化,专门引入了很多新的类来简化对文件的操作。其中最为核心的包括Path、Paths以及Files。这些类的出现是为了取代原来的基于java.io.File的文件IO操作方式。
+
+ Path用来取代File来表示文件路径和文件; Paths是其辅助工具类。
+
+ ```
+ Path path = Paths.get("/data/test.dat");
+ Path path1 = FileSystems.getDefault().getPath("/data", "test.dat");
+ ```
+
+ Files利用Path做文件创建、读取、写入等各种操作, 几乎包含了所有可能的文件操作,使用起来非常方便。
+
+ - 创建、读写文件
+
+ ```
+ if(!Files.exists(path)){
+ Files.createFile(path);
+ }
+
+ BufferedReader reader = Files.newBufferedReader(path1, StandardCharsets.UTF_8); //获取文件的BufferedReader来读取文件内容
+
+ BufferedWriter writer = Files.newBufferedWriter(Paths.get("/data/test.txt"), StandardCharsets.UTF_8);////获取文件的BufferedWriter来写文件
+ ```
+ - 遍历文件夹:
+
+ ```
+ Path dir = Paths.get("/data");
+ DirectoryStream stream = Files.newDirectoryStream(dir)
+ for(Path e : stream){
+ System.out.println(e.getFileName());
+ }
+
+ Stream stream = Files.list(dir);
+ Iterator it = stream.iterator();
+ while(it.hasNext()){
+ Path curPath = it.next();
+ System.out.println(curPath.getFileName());
+ }
+ ```
+ - 复制文件:
+
+ ```
+ Files.copy(Path source, Path target, CopyOption options);
+ Files.copy(InputStream in, Path target, CopyOption options);
+ Files.copy(Path source, OutputStream out);
+ ```
+
+ 此外,JDK7还提供了WatchService用来监控文件目录的变动。
+
+ ```
+ WatchService watchService = FileSystems.getDefault().newWatchService();
+ //监控目录下的文件变动
+ Paths.get("[dirPath]").register(watchService, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE);
+
+ Executors.newSingleThreadExecutor().execute(() -> {
+ while (true) {
+ // 等待直到获得事件信号
+ WatchKey signal;
+ try {
+ signal = watchService.take();
+ } catch (InterruptedException x) {
+ return;
+ }
+
+ for (WatchEvent> event : signal.pollEvents()) {
+ WatchEvent.Kind> kind = event.kind();
+
+ if (kind == OVERFLOW) {
+ continue;
+ }
+
+ WatchEvent ev = (WatchEvent) (event);
+ if (kind == ENTRY_MODIFY) {
+ System.out.println(ev.context().getFileName() + "content changed");
+ }
+ }
+ // 为监控下一个通知做准备
+ signal.reset();
+ }
+ });
+ ```
+1. 整型字面量支持用下划线分开, 并且支持二进制字面量(0b做为前缀)。
+
+ ```
+ int i = 0b111;
+ int j = 1_000_000;
+ ```
+
+## 7.5.2 Java 8
+
+Java8是一个创新的版本,带了很多崭新的特性,改动也比较大。
+
+1. 接口的默认方法
+
+ Java8允许开发者通过使用关键字default向接口中加入非抽象方法。这一新的特性被称之为扩展方法。
+
+ ```
+ public interface TestUserService {
+ default void test(){
+ ...
+ }
+ }
+ ```
+
+ 此外,也允许向接口添加静态方法。
+
+ ```
+ public interface TestUserService {
+ static String testStatic() {
+ ...
+ }
+ }
+ ```
+
+1. Optional
+
+ Java借鉴Guava的Optional类实现了自己的Optional,除了方法名做了一些改动,用法基本一致。和Guava中的Optional.transform一样,Java8的Optional也提供了map和flatMap可以对Optional对象做一系列转换操作, 并且还提供了filter方法来做过滤。
+
+ ```
+ User user = new User();
+ user.setName("testUser");
+
+ Optional optional = Optional.of(user);
+ user = optional.orElse(new User());
+ user = optional.orElseThrow(RuntimeException::new);
+ user = optional.orElseGet(User::new);
+
+ Optional testUserOptional =
+ optional.filter(u -> u.getName() != null)
+ .map(u -> {
+ TestUser testUser = new TestUser();
+ testUser.setName(u.getName());
+
+ return testUser;
+ });
+
+ Optional userOptional = testUserOptional.flatMap(tu -> {
+ User curUser = new User();
+ curUser.setName(tu.getName());
+
+ return Optional.of(curUser);
+ });
+ ```
+
+ map和flatMap的区别在于前者传入的mapper Function返回值可以是任何值而后者则必须是Optional。
+
+1. Effective final
+
+ Java8引入了一个叫做Effective final的特性,即隐形推导某个变量是否是final。比如,在使用Runnable匿名类新建线程的时候,如果run方法里使用到了外边的变量,那么之前的版本必须声明为final,而在Java8中,不需要声明,但必须得确保此变量的确是初始化后没有再被变过的。
+
+ ```
+ List list = ..;
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ list.add("test");
+ }
+ });
+ ```
+
+1. Lambda表达式
+
+ Lambda表达式是Java8最主要的特性,使得Java可以使用函数式编程,大大简化代码行数。如下:
+
+ ```
+ List list = Lists.newArrayList();
+ list.sort((n1, n2) -> Long.compare(n2, n1));
+ ```
+ 其中sort部分的参数就是一个Lambda表达式,相比起之前的匿名内部类,代码变得简短和便于阅读。一个Lambda表达式由参数列表、箭头和函数体组成。函数体可以是一个表达式,也可以是一个代码块。
+
+ 这里需要提到一个概念叫做函数接口(FunctionalInterface), 是指的仅仅包含一个抽象方法的接口,可以认为任何一个Lambda表达式都可以等价转换为对应的函数式接口。可以将任意只包含一个抽象方法的接口用作Lambda表示式,但是使用@FunctionalInterface有助于编译器检查函数接口的合法性。Ruunable就是一个函数接口。
+
+ ```
+ @FunctionalInterface
+ public interface Runnable {
+ void run();
+ }
+
+ Runnable run = () -> System.out.println(); //打印一个换行符
+ ```
+ 除了Runnable之外,Java8自带的几个常用函数接口如下:
+
+ - Predicate:接收一个参数并返回Boolean。
+ - Consumer: 接收一个参数,不返回值。
+ - Function: 接受一个参数并产生一个结果。
+ - Supplier: 不接收参数,对于给定的泛型类型产生一个实例。
+
+ 使用方法引用的话,上面的代码能够进一步简化:
+
+ ```
+ Runnable run = System.out::println;
+ ```
+
+ 这里的方法引用是用关键字::来传递方法和构造函数的引用,形式为类名::方法名,主要分为四种:
+
+ - 静态方法引用: Integer::valueOf
+ - 实例方法应用: System.out::println
+ - 构造方法引用: User::new
+ - 某个类型的任意对象的实例方法引用:User::getName
+
+1. Stream
+
+ Stream也是Java8引入的一个非常重要的特性。代表着一系列可以在其上进行多种操作的元素。这些操作可以是连续的也可以是中断的,中断操作返回操作结果;而连续操作返回流本身,这样就可以形成链式风格的流式操作。
+
+ 流是创建在数据源上的,java.util.Collection、list集合和set集合都是数据源的一种,产生的流可以是串行的也可以是并行的。通过集合类的Stream方法可以产生串行流;parallelStream方法产生并行流。
+
+ 一个Stream操作如下:
+
+ ```
+ List stringList = Lists.newArrayList("1", "0", "5",null);
+ List intList = stringList.stream() //创建Stream
+ .filter(Objects::nonNull) //过滤null
+ .map(Integer::valueOf) //转换Stream
+ .filter(e -> e > 0) //过滤
+ //.reduce((e1, e2) -> e1 + e2) //消减
+ .collect(Collectors.toList());//聚合
+
+ ```
+
+ Stream支持的主要连续操作如下
+
+ - filter: 接受一个predicate来过滤流中的所有元素。
+ - sorted: 返回流的已排序版本。
+ - map: 通过指定的Function将流中的每个元素转变为另外的对象。
+ - flatMap: 每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合。
+ - peek: 生成一个包含原Stream的所有元素的新Stream 。
+ - limit: 对一个Stream进行截断操作。
+ - skip: 返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream。
+
+ Stream支持的主要中断操作如下:
+
+ - reduce:使用指定的function对流中元素实施消减,返回一个包括所有被消减元素的Optional。
+ - collect:把流中的元素聚合到其它数据结构中。
+ - match:anyMatch、allMatch等各种匹配操作可以用来检测是否某种predicate和流中元素相匹配。
+ - count:返回流中的元素数量。
+ - findFirst: 返回Stream中的第一个元素。
+ - max和min:使用给定的比较器(Operator),返回Stream中的最大/最小值。
+
+ 使用Stream有几点需要注意:
+
+ - 流并不存储值,它只是某数据源的一个视图, 对流的操作会产生一个结果,但流的数据源不会被修改。
+ - 使用Stream时,要遵循先做filter再map的原则。
+ - 多数Stream操作(包括过滤filter、映射map、排序sorted以及去重distinct)都以惰性方式实现(延迟计算),没有中断操作流计算不会进行,性能好于迭代。
+ - 不同与其他Stream操作,flatMap不是惰性计算的。
+ - Stream只能被“消费”一次,一旦遍历过就会失效。
+ - 慎重使用并行Stream,其底层是使用的ForkJoinPool的commonPool,不做任何配置的情况下,所有并行流都公用的同一个线程池(默认线程数为机器CPU数目,可通过系统属性`java.util.concurrent.ForkJoinPool.common.parallelism`设置),而且此线程池还会被其他机制依赖,可能会发生不可预料的现象。
+
+1. Map
+
+ Java8对Map也做了一些改动,包括方法的增加以及底层实现的改动。
+
+ ```
+ Map map = new HashMap<>();
+ map.putIfAbsent("1", "a");
+ map.forEach((k, v) -> {
+ System.out.println(k + v);
+ });
+ map.computeIfAbsent("2", e -> e + "1");
+ map.computeIfPresent("1", (k, v) -> k + v);
+
+ map.remove("1","a")
+
+ map.getOrDefault("2","b");
+
+ map.merge("1", "a", (k, v) -> k + v);
+
+ ```
+
+ - putIfAbsent: 当key没有对应的value时才放入新的值,可以防止旧值被覆盖。
+ - forEach: 方便了对map做遍历。
+ - computeIfAbsent: key没有value时进行计算生成新的值。
+ - computeIfPresent: 当key有对应的value时,使用key和value生成新value。
+ - remove(k,v):仅当k对应的value等于v时,才会删除k。
+ - getOrDefault: 获取不到值时使用传入的默认值。
+ - merge: 如果key对应的值存在,那么将对应的value和传入的value使用后面的function合并后作为新值,否则设置为传入的value。
+
+ Java8对HashMap的实现做了优化,主要的一点是在解决冲突时,当链表大于8时会使用红黑树存储,这样极大加快了冲突时的查询速度。此外,计算key的hash整数值时也改为高16位异或低16位:(h = k.hashCode()) ^ (h >>> 16),提升了速度。同时,HashMap扩容机制也有了改动:HashMap底层数组的大小是2的n次方(元素的位置索引=hash mod [数组容量]=hash & [数组容量-1]),按照2次幂进行扩展(扩容后数组的大小变为原来大小的两倍),于是扩容后元素的位置要么是在原位置(hash & [旧数组容量]=0),要么是在原位置+旧数组大小的位置(hash & [旧数组容量]=1)。这样,由于0或1的取值是随机均等的,可以避免Java7中需要rehash重新计算哈希值的情况。同时,Java8也保证在旧数组索引相同的元素如果在新数组的索引相同,其相对位置不会倒置(之前的Java版本实现会倒置)。
+
+ ConcurrentHashMap则除了这个改动,还去掉了segments字段,将锁的粒度进一步细化,从而降低了并发冲突的概率,size方法也使用新增的counterCells做了优化。
+
+ 使用Map还有一点需要注意的是,ConcurrentHashMap的keySet方法被修改了, 如果用到这个方法且声明的时候使用的ConcurrentHashMap,那么从旧版本JDK升级到JDK8时会报错,可以使用ConcurrentMap声明或者不要使用此方法。
+
+1. Date API
+
+ 鉴于原有Java时间API的各种问题,Java8在java.time包下提供了新的date和time的API, 是由Joda Time的作者写的。
+
+ 使用LocalDateTime表示日期时间,和时区无关, 可以灵活的创建、进行时间计算、输出/解析格式字符串。
+
+ ```
+ LocalDateTime.now() //当前时间
+ LocalDateTime dateTime = LocalDateTime.of(2017, Month.JUNE,21,12,0,0); //2017-6-21 12:00:00
+
+ dateTime.plus(1, ChronoUnit.DAYS); //后面的一天
+ dateTime.plusHours(1); //后面的一小时
+ dateTime.minus(1, ChronoUnit.HOURS); //前面的1小时
+ dateTime.truncatedTo(ChronoUnit.HOURS);//截断时间到小时
+
+ dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); //输出格式化字符串
+ dateTime = LocalDateTime.parse("2016-06-21 12:00:00",DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); //解析格式化字符串
+ ```
+ 还有两个类LocalTime和LocalDate,前者表示时间后者表示日期,都是不可变的,工作原理类似于LocalDateTime。
+
+ 和之前的java.text.DateFormat相比,DateTimeFormatter都是不可变且线程安全的,可以放心在多线程环境下使用。
+
+ 此外,还有Instant类用于表示时间轴上的时间点,ZoneId类用于表示时区。它们联合使用可以用来创建java.util.Date对象或者将instant和本地时间转换。
+
+ ```
+ long currentMills = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+
+ Date udate = Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant());
+
+ Instant instant = Instant.now(); //当前时间
+ //intant = Instant.of(currentMills); //从时间戳转换
+ LocalDateTime localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
+ ```
+
+1. 注解
+
+ Java8中的Annotations是可重复,即可以将相同的注解在同一类型上使用多次。
+
+ 在注解声明时使用@Repeatable,可以使同一个注解类型同时使用多次
+
+ ```
+ @interface TestAnnotations {
+ TestAnnotation[] value();
+ }
+
+ @Repeatable(TestAnnotations.class)
+ @interface TestAnnotation {
+ String value();
+ }
+
+ @TestAnnotation("1")
+ @TestAnnotation("2")
+ class User{
+
+ }
+ ```
+
+ 使用@Repeatable标注注解,Java编译器会隐式的在该注解使用中加入@TestAnnotations。
+
+ 这样就可以通过反射获取类的注解信息, 但这个是不能直接通过TestAnnotation去拿的,需要使用TestAnnotations或者用Java8新增加的getAnnotationsByType方法。
+
+ ```
+ User.class.getAnnotation(TestAnnotations.class);
+ User.class.getAnnotationsByType(TestAnnotation.class);
+ ```
+ 此外,Java8中注解的@Target使用范围扩展到两种新的类型:
+
+ - TYPE_PARAMETER: 注解能写在类型变量的声明语句中。
+ - TYPE_USE:注解能写在使用类型的任何语句中。
+
+ 定义一个注解,将其target设置为ElementType.TYPE_PARAMETER或ElementType.TYPE_USE,或者两个都包含时,那么此注解就成为类型注解,可以写在使用类型的任何地方, 主要供开发工具、编译器在编译期做一些检查、转换等工作。
+ ```
+ @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
+ public @interface TypeAnnotationsTest {
+ }
+
+ @TypeAnnotationsTest LocalDateTime dateTime = LocalDateTime.now();
+ ```
+
+1. CompletableFuture
+
+ 类似于Guava提供的ListenableFuture, Java8提供了CompletableFuture简化异步编程的复杂性,提供了函数式编程的能力,支持流式调用。
+
+ ```
+ static int cal(int loop){
+ ..//计算
+ return ...;
+ }
+
+ CompletableFuture.supplyAsync(() ->
+ cal(10))
+ .thenCompose((i) -> CompletableFuture.supplyAsync(() -> cal(i)))
+ .thenApply((i) -> Integer.toString(i))
+ .thenApply((str) -> "result : " + str)
+ .thenAccept(System.out::println)
+ .get();
+ ```
+
+ - supplyAsync: 提交一个异步任务。
+ - thenCompose: 组合两个CompletableFuture为一个。
+ - thenApply: 异步任务完成后的回调。
+ - thenAccept: 异步任务完成后的回调,不同于thenApply,其参数中的函数式接口是一个Consumer,没有输出。
+ - get: 等待整个异步任务完成。
+
+ 这里需要注意的是,如果不指定具体的线程池(以Async结尾并且没有指定Executor的一些方法),那么CompletableFuture和并行Stream一样,都是使用的ForkJoin的commonPool。
+
+ 此外,CompletableFuture相比之前的Future多了一个complete方法,可以指定完成的时间点,主动触发任务的完成。
+
+Java8在JVM内存方面也有一些改动:
+
+- 取消掉了方法区(永久代),使用“元空间”替代,元空间只与系统内存相关,也可以通过MaxMeatSize设置,防止无限耗尽系统内存。
+- Java 8 update 20引入G1回收器中的字符串去重(String deduplication)。使得G1回收器能识别出堆中那些重复出现的字符串并将它们指向同一个内部的char[]数组,以避免同一个字符串的多份拷贝,那样堆的使用效率会变得很低。可以使用-XX:+UseStringDeduplication这个JVM参数开启这个特性。
+
+## 7.5.3 Java 9
+
+Java9是Java中一个大的版本,已于2017.09.21发布生产可用版本。其带来的新特性主要如下:
+
+1. JShell
+
+ Java9带来了许多编程语言都具有的REPL,JShell。极大地方便了Java语言的学习、调试等。
+
+ 
+
+1. 模块化
+
+ Java9带来的最大变化。Jigsaw在Java 7的时候被移除,在Java 9中又回来了。主要的目的是为更小的设备提供可伸缩性,改进 JDK 和 Java SE 的安全性,提升大型应用的性能,并使得其更易于构建。
+
+ 需要注意的是,此特性只是模块化JDK源代码,不会改变JRE和JDK的真实结构。允许开发者根据项目的需要自定义组件,使用jlink工具创建一个只包含所需模块的最小运行时环境,从而减少rt.jar的大小,使得Java更加容易地应用到小型计算设备中。
+
+1. 轻量级的JSON API
+
+ 目前有很多第三方的JSON API,如Google的Gson、阿里的FastJson。Java 9将JSON API集成到了JDK的实现中。
+
+1. 简化了的进程API
+
+ Java9会新增一些进程相关的api来增强管理本地系统进程的能力,使得不依赖调用外部程序等变通方案就能够方便灵活地获取本地进程信息、操作进程,提升与操作系统的交互能力。
+
+1. 提升访问对象时的线程竞争处理
+
+ 线程竞争的锁机制限制了许多Java应用的性能。Java9改善了锁的争用机制。
+
+1. 代码分段缓存
+
+ Java 9的JIT以分段的形式缓存代码。这样能够有更短的扫描时间和更少的碎片,从而提供更好的性能;GC扫描垃圾时可以直接跳过永驻代码,从而提升效率。
+
+1. 用于更大项目构建的智能Java编译工具
+
+ Java9提供了Smart Java Compiler,也叫sjavac, 在多核处理器情况下提升JDK的编译速度,可用于更大项目的构建。最终目的是取代javac成为Java环境默认的编译器。
+
+1. HTTP/2 客户端
+
+ Java9将重新实现一个HTTP客户端, 支持HTTP 2.0和WebSocket,旨在取代现在的HttpURLConnection, 以解决其自身的不少问题。不过此特性在Java9中标注为Incubator版本(并不是最终的api,后面可能会大的改动或者删除)。
+
+1. 接口私有方法
+
+ Java8给接口引入了默认方法和静态方法,Java9进一步引入了接口的私有方法,进一步提高可重用性。
+
+ ```
+ public interface ITest{
+ private void test(){
+ ...
+ }
+ }
+ ```
+
+1. 响应式编程
+
+ Java9引入了新的API: java.util.concurrent.Flow, 支持响应式的发布订阅框架。FLow类包括以下几个接口:
+
+ - Flow.Publisher:消息、事件的生产者。
+ - Flow.Subscriber:消息、事件的订阅者/接受者。
+ - Flow.Subscription:连接生产者和订阅者的消息控制链路。
+ - Flow.Processor:可以兼任生产者和订阅者的组件。
+
+1. 集合工厂方法
+
+ 和Guava中的Lists、Sets等类似,提供了一系列集合方法。
+
+ ```
+ Set.of(1,2,3);
+ List.of("a","b");
+ ```
+
+1. Stream API
+
+ Stream接口新增了几个方法:
+
+ - dropWhile: 丢弃元素直到第一个不匹配的元素,即返回去掉匹配断言的最长前缀元素集合后的Stream。
+ - takeWhile: 接受元素直到第一个不匹配的元素,即返回匹配断言的最长前缀元素集合构成的Stream。
+ - ofNullable:返回包含一个元素的Stream。
+ - iterate:根据seed(种子)、next(产生下一个元素的方法)、hasNext(是否继续产生元素)参数迭代生成有序Stream。
+
+ Optional也加入了stream方法,使得其可以做为Stream进行处理。
+
+ ```
+ Optional.of(1).stream()
+ ```
+
+由于Java9刚刚发布,使用并不够广泛,不对其使用进行详述。
+
+### 7.5.4 Java10
+
+### 7.5.5 Kotlin
+
+得益于谷歌和甲骨文公司目前的形式,Kotlin的形势非常不错的,至少在安卓开发领域未来是可期的。
+
+1. 无缝与Java互操作
+
+ 可以直接使用Java库能够大大降低切换语言的成本。因此,如果想选择Kotlin,可以先试着用Kotlin写单元测试,之后等熟悉了此语言,可以进行混写。最后则完全切换到Kotlin。
+
+1. 天然对协程的支持
+
+1. 能够编译成原生二进制程序
+
+1. 天然支持函数式编程和面向对象编程
+
+1. 主流三方框架的天然支持
\ No newline at end of file
diff --git a/book/chapter7-java/study.md b/book/chapter7-java/study.md
new file mode 100644
index 0000000..c251c97
--- /dev/null
+++ b/book/chapter7-java/study.md
@@ -0,0 +1,18 @@
+# 学习资料
+
+## 书籍
+
+- [《Java核心技术(卷1)》](https://book.douban.com/subject/3146174/):学习Java必备的黄皮书,入门推荐书籍
+- [《Java核心技术(卷2)》](https://book.douban.com/subject/3360866/):黄皮书之高级特性
+- [《Java并发编程实战》](https://book.douban.com/subject/10484692/): 对Java并发库讲得非常透彻
+- [《Effective Java》](https://book.douban.com/subject/3360807/):Java之父高司令都称赞的一本Java进阶书籍
+- [《写给大忙人看的Java SE 8》](https://book.douban.com/subject/26274206/):涵盖了Java8带来以及Java7中被略过的新的Java特性,值得一看
+
+## 资料
+
+- Socket编程:
+- NIO:
+- 序列化:
+- RPC框架:
+- 并发编程:
+
diff --git a/book/chapter7-java/weapons.md b/book/chapter7-java/weapons.md
new file mode 100644
index 0000000..c95bdff
--- /dev/null
+++ b/book/chapter7-java/weapons.md
@@ -0,0 +1,885 @@
+# 7.4 Java开发利器
+
+在Java开发中,有很多成熟的开源工具库供大家选用,覆盖了很多常见的但JDK中没有提供的功能和使用场景。学习并熟练使用这些类库,能让你在编码过程中少走很多弯路,并且也能学习到很多漂亮的编码方式和风格。比较常用的有以下几个:
+
+- Apache Commons:Apache开源的Java相关工具库,囊括了编码、文本、网络等等一系列工具类。
+- Guava:Google贡献的一个服务于Java6、7的类库,囊括了集合、字符串、缓存等一系列工具类。
+- Joda Time: 为了弥补JDK自带日期类使用上的不方便而创造的一个日期时间工具库,已经得到了广泛运用。
+- FastJson: 阿里开源的JSON序列化、反序列化类库,使用比较方便,性能也比较好。
+- Orika: 简单快速高效的Java Bean映射、复制框架。
+- MapDB: 专为Java设计的高性能的数据库,可以被用作多级缓存。
+- FastUtils:扩充了Java的集合类,提供了很多快速、压缩、支持基本数据类型的集合类以及大规模集合。
+- JCTools: 提供很多并发集合类,适用于高并发的业务场景。
+- Relections:org.relections提供了一系列对于运行时元数据的查询接口,大大简化了Java自带反射API的使用。
+- Lombok:一个简化开发工作的开发工具,能够节省很多重复、繁琐代码的编写。使用@Data可以省去编写getter、setter、hashCode、equals、toString等,使用@Slf4j自动生成Slf4j log对象。其原理是JDK中注解的编译时解析机制,javac会在执行的时候去调用实现了相关API(Pluggable Annotation Processing API)的程序,从而可以对编译器做一些增强。
+
+此外,还有其他大厂出品的工具类库如:Twitter的commons、Linkedin的linkedin-utils等,基本上也都是对一些常用工具的封装。
+
+还需要说明的是,前文提到过的Spring框架中也提供了很多可用的工具,如StringUtils、StopWatch、ReflectionUtils、ResourceUtils等,在Spring应用中完全可以拿来用。
+
+本章主要讲述其中最为常用的Apache Commons、Google Guava、Joda Time、FastJson、Orika以及MapDB。
+
+## 7.4.1 Apache Commons
+
+Apache Commons是Apache开源的一个可复用Java组件库。包含了多达50个子项目。其中开发中常用的有以下几个:
+
+- BeanUtils: 提供了对于Java Bean进行各种操作,克隆对象、属性。
+- Codec: 处理常用的编码解码方法的工具类包等。
+- Collections: 扩展Java集合框架的操作。
+- IO: 输入输出工具的封装。
+- Lang: Java基本对象(java.lang)方法的工具类包。
+- HttpClient: 低层次对HTTP协议操作的封装, 提供HTTP客户端与服务器的各种通讯操作。
+
+### BeanUtils
+
+BeanUtils基于JDK的java.beans,提供了一系列对Java Bean的操作:读取(get)和设置(set)Bean属性值、动态定义和访问bean属性等。
+
+先讲述一下Java Bean的定义,符合Bean规范的Java类需要符合以下要求:
+
+- 类必须是public访问权限,且需要有一个public的无参构造方法,方便利用Java的反射动态创建对象实例。
+- Bean的属性都是私有字段。
+- Bean的属性值只能通过setter方法设置。
+- 读取Bean的属性需要通过getter方法。
+
+对于Bean的操作,主要是BeanUtils这个类。
+
+BeanUtils将property分成simple(简单类型:String、int)、indexed(索引类型:数组、ArrayLsit)以及Maped(Map类型)三种类型,可以直接get和set Java Bean中的一个属性的值。
+
+这里需要注意的是,此类其实最终是调用的BeanUtilsBean,但是BeanUtilsBean2继承BeanUtilsBean并做了升级,提供了任何类型到字符串的转换。因此,使用的时候建议直接使用BeanUtilsBean2。
+
+```
+BeanUtilsBean beanUtilsBean = new BeanUtilsBean2();
+beanUtilsBean.getConvertUtils().register(false, false, 0);//错误不抛出异常、不使用Null做默认值,数组的默认大小为0
+
+User user = new User();
+beanUtilsBean.setProperty(user, "name", "testName");//设置属性的值
+beanUtilsBean.getProperty(user,"name");//获取属性的值
+```
+
+这里需要注意的是,如果类型是indexed,那么属性名[index]可以直接获取某个元素的值,而对于Maped类型,属性名(key值)可以获取某一个key对应的value。
+
+此外,还提供了复制以及克隆Bean的功能。
+
+```
+User user = new User();
+user.setName("test");
+User user2 = new User();
+
+beanUtilsBean.copyProperties(user2,user);
+User user3= (User)beanUtilsBean.cloneBean(user);
+```
+但这里的复制是浅复制,2个Bean的同一个属性可能拥有同一个对象的引用。
+
+还有Map和Bean之间的转换。
+
+```
+Map map = beanUtilsBean.describe(user);//bean->map
+beanUtilsBean.populate(user, map) //map->bean
+
+```
+
+此外,还有一个PropertyUtils和BeanUtils功能几乎一致。不同的是BeanUtils在对Bean赋值是会进行自动类型转化,只要属性名相同,类型会尝试转换,而PropertyUtils则会报错。
+
+### Codec
+
+常用的解码、编码方法封装,包括Base64、MD5、Sha1、URL。
+
+1. Base64
+
+ ```
+ Base64.encodeBase64String(byte[] binaryData);
+ Base64.decodeBase64(String base64String);
+ ```
+1. MD5
+
+ ```
+ DigestUtils.md5Hex(final byte[] data);
+ ```
+
+1. Sha1
+
+ ```
+ DigestUtils.sha1Hex(final byte[] data);
+ ```
+1. URL
+
+ ```
+ URLCodec.encode(final String str);
+ URLCodec.decode(final String str);
+ ```
+
+### Collections
+
+Collections为JDK的集合类提供了更为丰富的工具类、接口以及实现。最新的版本4,包名修改为:org.apache.commons.collections4。
+
+1. CollectionUtils: 提供了一些方面的操作方法,如判断集合非空、对集合的并集、交集、差集的操作。
+
+ ```
+ List list = getList();
+ List list2 = getList2();
+
+ if(CollectionUtils.isNotEmpty(list)){ //判断非空
+ CollectionUtils.union(list,list2)//并集
+ CollectionUtils.subtract(list,list2)//差集
+ CollectionUtils.retainAll(list,list2)//交集
+ }
+ ```
+
+1. 提供了一些新的集合类型。
+
+ ```
+ //得到集合里按顺序存放的key之后的某一Key
+ OrderedMap map = new LinkedMap();
+ map.put("1", "1");
+ map.put("2", "1");
+ map.put("3", "1");
+ map.firstKey(); // returns "1"
+ map.nextKey("1"); // returns "2"
+
+ //双向map
+ BidiMap bidi = new TreeBidiMap();
+ bidi.put("6", "6");
+ bidi.get("6"); // returns "6"
+ bidi.getKey("6"); // returns "6"
+ ```
+### IO
+
+提供了一些IO工具类, 是对java.io的扩展,操作文件非常方便。
+
+1. IOUtils:对IO stream操作的封装。
+
+ ```
+ InputStream is = new URL( "http://baidu.cim" ).openStream();
+ try{
+ IOUtils.toString(is, "utf-8");
+ IOUtils.readLines(is, "utf-8");
+ }finally {
+ IOUtils.closeQuietly(is);
+ }
+ ```
+
+1. FileUtils:对文件操作的封装。
+
+ ```
+ File file = new File("/data/data.txt");
+ List lines = FileUtils.readLines(file, "UTF-8"); //读取成字符串集合
+ byte[] fileBytes = FileUtils.readFileToByteArray(file); //读取成字节数组
+ FileUtils.writeByteArrayToFile(file,fileBytes); //字节写入文件
+ FileUtils.writeStringToFile(file, "test"); //字符串写入文件
+ ```
+
+1. FileSystemUtils:对文件系统的操作封装。
+
+ ```
+ FileSystemUtils.freeSpaceKb("/data"); //查看相应路径的剩余空间
+ ```
+
+### Lang
+
+一些公共的工具集合,涵盖了字符串操作、字符操作、 JVM交互操作、归类、异常和位域校验等等。现在最新的版本为3, 包名改成了org.apache.commons.lang3。
+
+1. StringUtils && StringEscapeUtils
+
+ StringUtils继承自Object,是null safe的,即遇到null的String对象,会把它处理掉不会抛出异常;StringEscapeUtils是对字符串做转义的工具类,包括HTML、JS、XML等等。
+
+ ```
+ String str = ...;
+
+ StringUtils.isEmpty(str); //判断字符串为空,多个连续空格不为空
+ StringUtils.isBlank(str); //判断字符串为空,多个连续空格为空
+
+ StringUtils.trim(str);//以strip开头的方法都是trim方法的扩展,不过可以自定义stripChars,不局限于空白符。
+
+ StringUtils.equals(str,"test");//支持null
+
+ StringUtils.contains("str","test"); //子字符串匹配
+
+ StringUtils.split(str,";"); //根据字符/字符串分隔字符串
+ StringUtils.join(new String[]{"1","2"},"-"); //根据字符连接字符串
+
+ StringEscapeUtils.escapeHtml4(str); //对字符串中的html标签做转义
+ ```
+
+ 这里需要注意的是其split方法,相比字符串自带的split方法使用正则,此方法直接使用了完整的字符串来做匹配,且会丢弃空字符串。
+
+1. ArrayUtils
+
+ ArrayUtils是一个对数组进行特殊处理的类。ArrayUtils扩展了JDK中的Arrays,提供了更多的功能。
+
+ ```
+ String[] strs = new String[]{"1", "4", "2"};
+
+ ArrayUtils.nullToEmpty(strs); //如果数组为null,则返回长度为0的数组
+ ArrayUtils.reverse(strs); //反转数组
+
+ ArrayUtils.addAll(strs,"3"); //数组添加元素
+ ```
+
+ 需要注意:使用addAll添加元素,是需要数组拷贝的,慎用。
+
+1. RandomUtils && RandomStringUtils
+
+ 提供了生成随机数、字符串的操作封装。
+
+ ```
+ RandomUtils.nextInt(0,10); //随机一个整数,从0到10,不包括10。
+ RandomStringUtils.random(3); //随机三个字母的字符串出来。
+ ```
+
+ 这里需要注意的是这俩类都使用了Random这个类,但其是伪随机的,在要求严格的环境下,尽量不要使用这俩类,去使用SecureRandom。
+
+1. NumberUtils
+
+ 为数字提供了一些操作封装, 是null safe的。
+
+ ```
+ String numberStr = "123";
+ long n = NumberUtils.toLong(numberStr); //将字符串转化为long, 如果字符串格式不对或者为Null,则返回0,并不会抛出异常
+ long max = NumberUtils.max(new Long[]{1L,5L,10L}); //计算数组最大值
+ ```
+1. DateUtils && DateFormatUtils
+
+ 是对日期时间操作的封装。
+
+ DateUtils提供了很多日期计算。
+
+ ```
+ DateUtils.addDays(new Date(),3); //计算三天后的时间
+ DateUtils.addHours(new Date(),3); //三个小时候的时间
+
+ DateUtils.truncate(new Date(), Calendar.HOUR); //截断日期到小时,后面的分、秒都为0
+ ```
+
+ DateFormatUtils提供了Date到字符串表示的操作。
+
+ ```
+ DateFormatUtils.format(new Date(),"yyyyMMdd"); //以yyyyMMdd的格式输出日期
+ ```
+
+1. MethodUtils
+
+ 通过此工具类可以调用类的方法,实现原理基于反射。
+
+ ```
+ MethodUtils.invokeStaticMethod(StringUtils.class,"isNotBlank","test"); //调用静态方法
+ MethodUtils.invokeMethod(user,"getName"); //调动实例方法
+ ```
+
+1. StopWatch
+
+ StopWatch是一个秒表类。
+
+ ```
+ StopWatch stopWatch = new StopWatch();
+ stopWatch.start(); //开始计时
+ stopWatch.split(); //截断每一次的分段计时
+ stopWatch.getSplitTime(); //获取分段计时
+ stopWatch.suspend(); //暂停秒表
+ stopWatch.resume(); //恢复计时
+ stopWatch.stop(); //停止秒表
+ stopWatch.getTime(); //获得总共计时
+ ```
+
+1. ImmutablePair && ImmutableTriple
+
+ 这俩类都是不可变的,经常是用在返回值是两个或者三个的场景下,是对多返回值的通用封装。
+
+ ```
+ ImmutablePair pair = ImmutablePair.of(user,user1);
+ pair.getLeft();
+ pair.getRight();
+ ImmutableTriple triple = ImmutableTriple.of(user,user1,user2);
+ triple.getLeft();
+ triple.getMiddle();
+ triple.getRight();
+ ```
+
+### HttpClient
+
+提供HTTP客户端与服务器的各种通讯操作, 包括支持各种HTTP Method、SSL连接、Cookie、Session保持等。此工具类现在已经从Apache Commons移到Apache HttpComponents中。包名改为:org.apache.http。
+
+1. 连接池
+
+ HttpClient提供了HTTP连接池的支持,连接池依赖HTTP 1.1的keep alive机制,对HTTP 1.0需要做兼容配置。此外,也支持HTTPS请求。需要注意的是使用连接池能够减少频繁创建、销毁连接的消耗提高性能,但是由于连接池是有锁的,为了提升并发性能,最好对于每一个服务都创建一个连接池。
+
+ ```
+ RegistryBuilder schemeRegistry = RegistryBuilder.create();
+ schemeRegistry.register("http", PlainConnectionSocketFactory.getSocketFactory());
+
+ //对https的支持
+ SSLContext sslcontext = SSLContext.getInstance("TLS");
+ sslcontext.init(new KeyManager[0], new TrustManager[]{new SimpleTrustManager()}, null);
+ SSLConnectionSocketFactory sf = new SSLConnectionSocketFactory(sslcontext);
+ schemeRegistry.register("https", sf);
+
+ //连接池配置
+ PoolingHttpClientConnectionManager pool = new PoolingHttpClientConnectionManager(schemeRegistry.build());
+ pool.setMaxTotal(1000); //最大连接数支持
+ pool.setDefaultMaxPerRoute(100); //每一个路由的最大连接数
+ pool.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(5000).build());
+ ```
+1. HttpRequestBase
+
+ HttpClient支持HTTP的各种方法, 都是HttpRequestBase的子类。
+
+ - HttpGet
+ - HttpPost
+ - HttpPut
+ - HttpDelet
+
+ 以上类都有一个参数为URL字符串的构造方法。此外,对post、put这种需要传递数据的方法,HttpClient是使用HttpEntity来实现的。常用的几个HttpEntity如下:
+
+ - UrlEncodedFormEntity:最常见的表单提交,Content-type为application/x-www-form-urlencoded。
+ - MultipartFormEntity: 提交文件时常用的方式,Content-type为multipart/form-data
+ - StringEntiry:自包含的Entity, 传递JSON数据时可以使用。
+
+ ```
+ StringEntity entity = new StringEntity("{\"name\";\"test\"}", "UTF-8");
+ entity.setContentType("application/json")
+
+ HttpPost method = new HttpPost(url);
+ method.setEntity(entity);
+ ```
+
+1. Cookie
+
+ Cookie的支持需要依赖CookieStore。HttpClientUtil内置了BasicCookieStore。
+
+ ```
+ CookieStore cookieStore = new BasicCookieStore()
+ cookieStore.addCookie(Cookie cookie);
+ List cookies = cookieStore.getCookies();
+ ```
+
+1. HttpClientBuilder
+
+ HttpClient的创建依赖于HttpClientBuilder, 能够对HttpClient的Cookie以及Connect Timeout、Socket Timout、Keep Alive的策略进行配置。
+
+ ```
+ HttpClientBuilder httpClientBuilder = HttpClients.custom().setDefaultCookieStore(cookieStore);//cookie支持
+ httpClientBuilder.setConnectionManager(pool); //设置连接池
+ httpClientBuilder.setDefaultSocketConfig(pool.getDefaultSocketConfig());
+ httpClientBuilder.setDefaultRequestConfig(
+ RequestConfig.custom()
+ .setConnectTimeout(3000)
+ .setSocketTimeout(5000)
+ .build());
+
+ //对keep alive的策略配置
+ httpClientBuilder.setKeepAliveStrategy(new ConnectionKeepAliveStrategy() {
+ public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
+ HeaderElementIterator it = new BasicHeaderElementIterator(response
+ .headerIterator(HTTP.CONN_KEEP_ALIVE));
+ while (it.hasNext()) {
+ HeaderElement he = it.nextElement();
+ String param = he.getName();
+ String value = he.getValue();
+ if (value != null && param.equalsIgnoreCase("timeout")) {
+ try {
+ return Long.parseLong(value) * 1000;
+ } catch (NumberFormatException ignore) {
+ }
+ }
+ }
+ // 否则保持活动5秒
+ return 5 * 1000;
+ }
+ });
+
+ HttpClient httpClient = httpClientBuilder.build();
+ ```
+
+1. 使用
+
+ ```
+ method.setProtocolVersion(HttpVersion.HTTP_1_1); //设置使用http 1.1
+ request.addHeader("User-Agent", agentHeader); //设置ua
+ request.addHeader("Connection", "keep-alive"); //为了keepalive支持http 1.0
+
+ HttpResponse res = httpClient.execute(request);
+
+ byte[] byteResult = EntityUtils.toByteArray(res.getEntity());
+ ```
+
+ 最终可以通过返回的HttResponse拿到接口返回的body、header等信息。
+
+此外,Apache HttpComponents还提供了AsyncHttpClient用于异步通讯场景, 使用流程和HttpClient类似。不同之处如下:
+
+1. 连接池管理器多了IO线程的配置且连接的Registry也不同。
+
+ ```
+ Registry sessionStrategyRegistry = RegistryBuilder
+ .create()
+ .register("http", NoopIOSessionStrategy.INSTANCE)
+ .register("https", new SSLIOSessionStrategy(SSLContexts.createDefault()))
+ .build();
+
+ // 配置io线程
+ IOReactorConfig ioReactorConfig = IOReactorConfig.custom()
+ .setIoThreadCount(Runtime.getRuntime().availableProcessors())
+ .build();
+
+ // 设置连接池
+ ConnectingIOReactor ioReactor;
+ ioReactor = new DefaultConnectingIOReactor(ioReactorConfig);
+ PoolingNHttpClientConnectionManager conMgr = new PoolingNHttpClientConnectionManager(
+ ioReactor, null, sessionStrategyRegistry, null);
+ conMgr.setMaxTotal(100);
+ conMgr.setDefaultMaxPerRoute(100);
+
+ // 连接配置:忽略传输错误,默认编码使用utf-8
+ ConnectionConfig connectionConfig = ConnectionConfig.custom()
+ .setMalformedInputAction(CodingErrorAction.IGNORE)
+ .setUnmappableInputAction(CodingErrorAction.IGNORE)
+ .setCharset(Consts.UTF_8).build();
+ conMgr.setDefaultConnectionConfig(connectionConfig);
+ ```
+
+1. 使用CloseableHttpAsyncClient。
+
+ ```
+ RequestConfig requestConfig = RequestConfig.custom()
+ .setConnectTimeout(3000)
+ .setSocketTimeout(1000).build();
+
+ CloseableHttpAsyncClient asyncClient = HttpAsyncClients.custom().setConnectionManager(conMgr)
+ .setDefaultCookieStore(new BasicCookieStore())
+ .setDefaultRequestConfig(requestConfig)
+ .build();
+ ```
+
+1. 使用时,需要先启动Client,并且提供了回调使用方式。
+
+ ```
+ asyncClient.start(); //需要先启动Client
+
+ // 通过future获取结果
+ HttpGet httpGet = new HttpGet("http://www.baidu.com");
+ Future responseFuture = asyncClient.execute(httpGet, null);
+ try {
+ HttpResponse httpResponse = responseFuture.get();
+ HttpEntity httpEntity = httpResponse.getEntity();
+ ...
+ } catch (InterruptedException | ExecutionException e) {
+ e.printStackTrace();
+ }
+
+ // 回调方式获取结果
+ final HttpGet httpGet2 = new HttpGet("http://www.baidu.com");
+ asyncClient.execute(httpGet2, new FutureCallback() {
+ @Override
+ public void completed(HttpResponse httpResponse) {
+ HttpEntity httpEntity = httpResponse.getEntity();
+ ...
+ }
+
+ @Override
+ public void failed(Exception e) {
+
+ }
+
+ @Override
+ public void cancelled() {
+
+ }
+ });
+
+ ...
+
+ asyncClient.close();
+ ```
+
+ 需要注意的是,无论是HttpClient还是AsyncHttpClient, 连接池都是有锁的。虽然支持对每一个route设置最大连接数,但如果是高并发场景,最好对于每一个服务都创建一个单独的HttpClient实例,使用不同的连接池。
+
+ 还需要提到的是,如果想要使用Http缓存提高请求的性能,可以使用Apache HttpComponents提供的httpclient-cache中的CachingHttpClient。
+
+## 7.4.2 Guava
+
+Guava是Google开源的一个涵盖了字符串处理、缓存、并发库、事件总线、IO等常用操作的Java核心库,也是Google自己很多Java项目依赖的工具库。其中的Guava Cache缓存已经在4.3.1讲过。
+
+1. Preconditions
+
+ 对条件做前置判断,经常用在方法的最前面,来对参数进行校验,不符合则抛出异常。
+
+ ```
+ Preconditions.checkArgument(user != null,"user null error"); //user为null则抛出IllegalArgumentException
+ Preconditions.checkNotNull(user); //user为null则抛出NullPointerException
+ ```
+
+1. Optional
+
+ 使用Optional表示可能为null的T类型引用,能够显著地降低代码抛出NullPointerException的可能。其中其of方法和fromNullable,前者传入的值不能为空,后者则可以传入null值,表示引用缺失。提供了isPresent()方法判断是否引用缺失,在调用get之前务必先调用isPresent()。但Optional不能乱用,建议仅仅用在对外的API和接口的返回值上。
+
+ ```
+ String str = ...;
+ Optional optional = Optional.fromNullable(str);
+ if(optional.isPresent()){
+ String tmp = optional.get();
+ }
+
+ str = optional.or("default string");
+ str = optional.or(new Supplier() {
+ @Override
+ public String get() {
+ return "default string";
+ }
+ });
+ str = optional.orNull();
+
+ // 对optional中的value做转换操作
+ optional.transform(new Function() {
+ @Override
+ public Object apply(String input) {
+ return "transformed string";
+ }
+ });
+ ```
+
+1. Objects && MoreObjects
+
+ 是对Java中Object的操作的扩展,后者是对前者的升级,都是null safe的。包括非空判断、相等判断、空值处理、hashcode计算等。
+
+ ```
+ User user = new User();
+ User user1 = new User();
+ ...
+
+ Objects.equal(user,user1); //equal判断
+ Objects.hashCode(user); //获取对象实例的哈希值
+ MoreObjects.toStringHelper(user).add("name","testName").toString(); //辅助编写类的toString方法
+ MoreObjects.firstNonNull(user,new User()); //取第一个非空的实例,可以用来在设置第一个元素为null时的默认值。
+ ```
+
+1. ComparisonChain
+
+ 提供了链式的比较器,执行比较操作直至发现非零的结果, 之后的比较将被忽略。
+
+ ```
+ ComparisonChain.start()
+ .compare(user.getName(),user1.getName())
+ .compare(user.getAge(),user1.getAge())
+ .result();
+ ```
+
+1. Strings && Joiner && Splitter && CaseFormat
+
+ 这几个类都是对字符串的操作,包括分割、连接、格式转换等。
+
+ ```
+ Strings.nullToEmpty(str); //如果字符串为null,则转换为空字符串
+ Strings.repeat(str,3); //重复字符串成新的字符串
+
+ Joiner joiner = Joiner.on(";").skipNulls();
+ joiner.join("1", null, "2", "3","4"); //使用;拼接字符串
+
+ Splitter.on(';')
+ .trimResults()
+ .omitEmptyStrings()
+ .split("1;2;3;4"); //分隔字符串
+
+ CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, "user_name"); // 将字符串从low underscore命名格式转换为low camel命名。
+ ```
+
+ 其中的命名格式转换还支持LOWER_HYPHEN、UPPER_CAMEL以及UPPER_UNDERSCORE。
+
+1. ImmutableList && Multiset && Multimap
+
+ 这些是Guava实现的新的集合类型。
+
+ ImmutableList是不可变集合(保证线程绝对安全)的一种。除此之外,还有ImmutablesSet、ImmutableMap,用法都类似。
+
+ ```
+ ImmutableList list = ImmutableList.of("1","2","3");
+ ```
+ Multiset可以多次添加相等的元素,主要统计给定元素的个数。
+
+ ```
+ Multiset set = HashMultiset.create();
+ set.add("1");
+ set.add("1");
+ System.out.println(set.count("1"));
+ ```
+ Multimap是一个key映射多值的Map,类似于`Map>、Map>`。主要是使用其两个子类:ListMultimap和SetMultimap,前者允许重复值,后者则不允许。
+
+ ```
+ Multimap multimap = ArrayListMultimap.create();
+ multimap.put("test","1");
+ multimap.put("test","2");
+ multimap.get("test"); //["1","2"];
+ ```
+
+1. Lists && Maps
+
+ JDK默认提供了Collections作为集合工具类。Guava针对其没有提供的集合工具操作做了扩展和实现。Lists、Maps是List和Map的工具类。最大的一个特性是提供的静态工厂方法相比先创建ArrayList再添加元素或者设置参数的初始化方式要简单优雅很多。
+
+ ```
+ Lists.newArrayList("1", "2", "3");
+ Lists.newArrayListWithCapacity(100);
+ ```
+
+1. ListenableFuture
+
+ ListenableFuture继承了JDK concurrent包下的Future接口,可以大大简化并发逻辑的编写。可以注册回调方法,在运算(多线程执行)完成的时候进行调用, 或者在运算(多线程执行)完成后立即执行。
+
+ ```
+ ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
+ ListenableFuture future = service.submit(new Callable() {
+ @Override
+ public String call() throws Exception {
+ return "result";
+ }
+ });
+ Futures.addCallback(future, new FutureCallback() {
+ @Override
+ public void onSuccess(Object result) {
+ ...
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ ...
+ }
+ });
+ ```
+
+ 还支持链式操作:
+
+ ```
+ Futures.transform(future, new AsyncFunction() {
+ @Override
+ public ListenableFuture apply(Object input) throws Exception {
+ return ...;
+ }
+ }, new Executor() {
+ @Override
+ public void execute(Runnable command) {
+
+ }
+ });
+ ```
+
+1. EventBus
+
+ Guava实现的事件总线机制,为了替代通用型的发布-订阅模型,不适用于进程间通信。
+
+ EventBus定义了事件生产者和事件监听者的角色,类似于生产者和消费者。
+
+ - 事件生产者:管理和追踪监听者以及向监听者分发事件。
+ - 事件监听者:注册到总线上,按照Event监听。只要以ChangeEvent为唯一参数创建方法,并用Subscribe注解标记即可实现一个监听者。register到EventBus上即可开始监听Event。
+
+ ```
+ class EventBusListener {
+ @Subscribe
+ public void recordCustomerChange(ChangeEvent e) {
+ System.out.println(e.getSource());
+ }
+ }
+
+ EventBus eventBus = new EventBus(); //还可以使用AsyncEventBus异步发送event
+ eventBus.register(new EventBusListener());
+
+ eventBus.post(new ChangeEvent("test event"));
+ ```
+
+1. RateLimiter
+
+ 使用令牌桶的速率限制器,经常用于限制对一些物理资源或者逻辑资源的访问速率。与JDK中的Semaphore相比,Semaphore 限制了并发访问的数量而不是使用速率。
+
+ ```
+ final RateLimiter rateLimiter = RateLimiter.create(2.0); //桶容量为2且每秒新增2个令牌
+
+ Executor executo = Executors.newFixedThreadPool(10);
+
+ for (Runnable task : tasks) {
+ rateLimiter.acquire(); // 也许需要等待直到有令牌可用
+ executor.execute(task);
+ }
+ }
+
+ ```
+
+ 需要注意的是RateLimiter并不提供公平性的保证,没有先来先得的概念。此外,其请求的许可数不会影响到请求本身的限制,但会影响下一次请求的限制(高开销的任务下一个请求会经历额外的限制,从而来偿付高开销任务)。如:acquire(1) 和acquire(1000)的限制效果一样,但后者的下一次请求会受到额外限制。
+
+此外,Guava还提供了散列、函数式风格、IO、区间、数学运算、Service框架、BloomFilter
+等等许多非常有用的工具类,都大大提高了开发效率和代码质量。其中,Guava的Optional、Objects这些由于用的非常广泛,Java8都吸收并做了类似的实现。
+
+## 7.4.3 Joda Time
+
+JDK自带的Date、Calendar类使用起来非常麻烦,并且日期与字符串之间的转换很慢且非线程安全。Joda Time就是为了解决这些问题而创造的日期时间库,使用起来非常简单方便。和之前Apache Commons提供的DateUtils相比,如果想继续使用Java日期,可以选择DateUtils;如果想彻底改变的话就可以使用Joda Time。
+
+1. 初始化时间
+
+ ```
+ DateTime dateTime=new DateTime(2017, 6, 21, 18, 00,0); //2017.06.21 18:00:00
+ ```
+1. 输出格式化字符串
+
+ ```
+ dateTime.toString("yyyy-MM-dd");
+ ```
+
+1. 解析时间字符串
+
+ ```
+ DateTimeFormatter format = DateTimeFormat .forPattern("yyyy-MM-dd");
+ DateTime dateTime = DateTime.parse("2017-06-21", format);
+ ```
+
+1. 时间计算
+
+ ```
+ dateTime.plusDays(1) // 增加天
+ .plusYears(1)// 增加年
+ .plusMonths(1)// 增加月
+ .plusWeeks(1)// 增加星期
+ .minusMillis(1)// 减分钟
+ .minusHours(1)// 减小时
+ .minusSeconds(1);// 减秒数
+
+ DateTime.Property month = dateTime.monthOfYear();
+ month.isLeap(); //判断是否是闰月
+
+ ```
+1. 与Java Date转换
+
+ ```
+ dateTime = new DateTime(new Date());
+ dateTime = new DateTime(Calendar.getInstance());
+ Date date = dateTime.toDate();
+ ```
+
+## 7.4.4 FastJson
+
+FastJson是阿里巴巴开源的JSON处理器,官方测试称性能超过MappingJackson。使用起来比较简单方便。
+
+1. 序列化
+
+ ```
+ User user = new User();
+ user.setName("testUser");
+ user.setGender("M");
+ Strign userJson = JSON.toJSONString(user);
+ ```
+
+1. 反序列化
+
+ ```
+ user = JSON.parseObject(str,User.class);
+ JSONObject jo = JSON.parseObject("{\"name\":\"test\"}");
+ ```
+
+1. 构造JSONObject以及取值
+
+ ```
+ JSONObject jo = new JSONObject();
+ jo.put("name","test");
+ jo.getString("name");
+ jo.getString("nickName"); //返回为null, 不会抛异常
+ ```
+1. 属性名称转化
+
+ 很多时候会遇到JSON字符串中的属性和Java Bean中的属性名称不一致的情况。这时候如果直接调用,则会出错,需要做名称转换,可以使用@JSONField注解配置别名:
+
+ ```
+ @JSONField(name = "nick")
+ private String nickName;
+ ```
+
+ 此外,FastJson默认提供了JSON属性Low Underscore到Java Bean字段Low Camel命名的转化。如果想要实现序列化的时候到Low Camel的转换除了可以使用@JSONField,还可以使用SerializeConfig, 设置其PropertyNamingStrategy。同样的,ParseConfig也能设置此策略。
+
+ ```
+ SerializeConfig config = new SerializeConfig();
+ config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
+ String str = JSON.toJSONString(user, config);
+ System.out.println(str); // {\"nick_name\":\"testNick\"}
+ ```
+ PropertyNamingStrategy还支持KebabCase(短横线连接单词)、PascalCase(大写字母开头)以及CamelCase(驼峰)。
+
+1. JSONPath
+
+ FastJson1.2.0之后提供了JSONPath,方便取值,类似于XPath。主要是为了简化取值逻辑,方便嵌套取值、过滤取值、获取集合长度等。
+
+ ```
+ String jsonStr = "{\"name\":\"testName\",\"interests\":[\"music\",\"basketball\"]," +
+ "\"notes\":[{\"title\":\"note1\",\"contentLength\":200},{\"title\":\"note2\",\"contentLength\":100}]}";
+ JSONObject jsonObject1 = JSON.parseObject(jsonStr);
+ System.out.println(JSONPath.eval(jsonObject1, "$.interests.size()")); //集合长度
+ System.out.println(JSONPath.eval(jsonObject1, "$.interests[0]")); //集合取值
+ System.out.println(JSONPath.eval(jsonObject1, "$.notes[contentLength > 100].title")); //集合过滤取值
+ System.out.println(JSONPath.eval(jsonObject1, "$.notes['title']")); //只取某一个属性的值
+ ```
+
+使用FastJson时需要注意FastJson在序列化和反序列化默认是开启ASM的(安卓下不会开启)。可以通过下面的代码关闭:
+
+```
+SerializeConfig.getGlobalInstance().setAsmEnable(false); // 序列化的时候关闭ASM
+ParserConfig.getGlobalInstance().setAsmEnable(false); // 反序列化的时候关闭ASM
+```
+
+## 7.4.5 Orika
+
+Orika是一个快速、高效的Java Bean映射框架,主要用于在VO、PO等各种Bean之间复制属性,并且是深复制。相比起7.4.1中提到的BeanUtils使用反射,Orika是使用代码生成进行复制的。因此其性能好于BeanUtils和Dozer(使用反射,对反射信息做了缓存)。
+
+```
+MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
+MapperFacade mapper = mapperFactory.getMapperFacade();
+
+User user = new User();
+user.setName("test");
+User user1 = mapper.map(user, User.class);
+```
+
+不同类之间复制,如果属性名不一致,可以通过自定义映射来复制,属性名相同的直接可以复制。
+
+```
+MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
+mapperFactory.classMap(User.class, TestUser.class)
+ .field("name", "testName")
+ .byDefault()
+ .register();
+
+MapperFacade mapper = mapperFactory.getMapperFacade();
+
+User user = new User();
+user.setName("test");
+TestUser testUser = mapper.map(user, TestUser.class);
+```
+
+### 7.4.6 MapDB
+
+MapDB将Java中常用的Maps, Sets, Lists, Queues等其他集合做了JVM堆外(堆外内存、磁盘)的存储实现。很多时候被用作多级缓存。如下:
+
+```
+DB db = DBMaker.memoryDB().make();
+
+HTreeMap diskCache = db.hashMap("testCache")
+ .expireStoreSize(10 * 1024)
+ .expireMaxSize(1000)
+ .expireAfterCreate(10, TimeUnit.SECONDS)
+ .createOrOpen();
+
+HTreeMap cache = db.hashMap("testCache")
+ .expireMaxSize(100)
+ .expireOverflow(diskCache)
+ .createOrOpen();
+```
+
+需要注意的是,最新的MapDB使用的是Kotlin语言实现其主要逻辑。
+
+### 7.4.7 使用Hystrix做熔断
+
+除了HystrixBadRequestException异常之外,所有从run()方法抛出的异常都算作失败,并触发降级getFallback()和断路器逻辑。
+ HystrixBadRequestException用在非法参数或非系统故障异常等不应触发回退逻辑的场景。
+
+请求缓存可以让(CommandKey/CommandGroup)相同的情况下,直接共享结果,降低依赖调用次数,在高并发和CacheKey碰撞率高场景下可以提升性能.
+
+```
+HystrixRequestContext context = HystrixRequestContext.initializeContext();
+```
+
+Servlet容器中,可以直接实用Filter机制Hystrix请求上下文
+
+信号量隔离:SEMAPHORE
+ 隔离本地代码或可快速返回远程调用(如memcached,redis)可以直接使用信号量隔离,降低线程隔离开销.
+
+使用hystrix-javanica的Java注解
\ No newline at end of file
diff --git a/book/chapter8-profile/analysis.md b/book/chapter8-profile/analysis.md
new file mode 100644
index 0000000..445575e
--- /dev/null
+++ b/book/chapter8-profile/analysis.md
@@ -0,0 +1,170 @@
+# 8.2 性能分析
+
+在系统层面能够影响应用性能的一般包括三个因素:CPU、内存和IO,可以从这三方面进行程序的性能瓶颈分析。不过如果经过分析发现这三个因素都有异常,且请求流量突然飙升,则有可能是流量攻击(通过access log定位被攻击接口)或者业务流量提升造成的响应缓慢,那么则可以通过限流、降级或者增加服务结点来解决。
+
+需要说明的是,当发生性能问题时,在排查上述三个因素之前最好先去排查一下业务日志。很多时候业务日志抛出的异常或者有经验的开发者埋好的日志能够直接定位到性能降低的原因,如Redis或者MySQL被慢查询拖垮的时候,业务日志会抛出获取不到连接或者连接数已满的异常。在Tomcat做为容器时,有一点需要注意:日志文件catalina.out和localhost.{yyyy-MM-dd}.log都会输出业务异常信息,前者是标准输出和标准出错(如果应用的日志配置不包括控制台Console,那么catalina.out里不会有业务日志),所有输出到这两个位置的都会进入catalina.out,包含tomcat运行自己输出的日志以及应用里向console输出的日志,而后者主要是应用初始化时(Listener、Filter、Servlet)未处理的异常最后被Tomcat捕获而输出的日志,这些异常会导致应用无法启动。除了这些,业务日志方面的分析并没有太多的技巧,故不在此详述。
+
+## 8.2.1 CPU分析
+
+当程序响应变慢的时候,首先使用top、vmstat、ps等命令查看系统的CPU使用率是否有异常,从而可以判断出是否是CPU繁忙造成的性能问题。其中,主要通过us(用户进程所占的%)这个数据来看异常的进程信息。当us接近100%甚至更高时,可以确定是CPU繁忙造成的响应缓慢。一般说来,CPU繁忙的原因有以下几个:
+
+- 线程中有无限空循环、无阻塞、正则匹配或者单纯的计算。
+- 发生了频繁的GC。
+- 多线程的上下文切换。
+
+这里需要注意的是一个进程的CPU使用率是其所有线程之和,Linux下所有线程最终是以轻量级进程的形式存在系统中的,CPU使用率高需要配合mpstat具体分析,是单线程应用程序引起的还是某些线程都处于繁忙状态。
+
+确定CPU使用率最高的进程之后就可以使用jstack来打印出异常进程的堆栈信息:
+
+**jstack [pid]**
+
+
+
+接下来需要注意的一点是,使用jstack只能打印出进程的信息,这些信息里面包含了此进程下面所有线程的堆栈信息。因此,进一步需要确定是哪一个线程耗费了大量CPU,此时可以使用`top -p [processId] -H`来查看,也可以直接通过`ps -p [pid] -Leo pid,lwp,pcpu --sort -pcpu`(ps这里是CPU平均使用率)来显示所有进程,包括LWP的资源耗费信息。最后,通过在jstack的输出文件中查找对应的LWP的十六进制id(`printf %0x [processId]`)即可以定位到相应的堆栈信息(tid指Java Thread id,nid指native线程的id)。其中需要注意的是线程的状态:RUNNABLE、WAITING等。对于Runnable的进程需要注意是否有耗费CPU的计算;对于Waiting的线程一般是锁的等待操作。
+
+此外,使用jstack查看线程栈时需要注意:JVM只能在SafePoint转储出一个线程的栈;由于jstack dump实现机制每次只能转储出一个线程的栈信息,因此输出信息中可能会看到一些冲突的信息,如一个线程正在等待的锁并没有被其他线程持有,多个线程持有同一个锁等。
+
+也可以使用jstat来查看对应进程的GC信息,以判断是否是GC造成了CPU繁忙。
+
+**jstat -gcutil [pid]**
+
+
+
+
+还可以使用vmstat,通过观察内核状态的上下文切换(cs)次数,来判断是否是上下文切换造成的CPU繁忙。
+
+**vmstat 1 5**
+
+
+
+此外,有时候可能会由JIT引起一些CPU飚高的情形,如大量方法编译等。这里可以使用-XX:+PrintCompilation这个参数输出JIT编译情况,以排查JIT编译引起的CPU问题。
+
+## 8.2.2 内存分析
+
+对Java应用来说,内存主要是由堆外内存和堆内内存组成。
+
+1. 堆外内存
+
+ 堆外内存主要是JNI、Deflater/Inflater、DirectByteBuffer(NIO中会用到)使用的。对于这种堆外内存的分析,还是需要先通过vmstat、sar、top、pidstat等查看swap和物理内存的消耗状况再做判断。对于JNI、Deflater这种调用可以通过Google-preftools来追踪资源使用状况。
+
+2. 堆内内存
+
+ 此部分内存为Java应用主要的内存区域。通常与这部分内存性能相关的有:
+
+ - 创建的对象:这个是存储在堆中的,需要控制好对象的数量和大小,尤其是大的对象很容易进入老年代。
+ - 全局集合:全局集合通常是生命周期比较长的,因此需要特别注意全局集合的使用。
+ - 缓存:缓存选用的数据结构不同,会很大程序影响内存的大小和GC。
+ - ClassLoader:主要是动态加载类容易造成永久代内存不足。
+ - 多线程:线程分配会占用本地内存,过多的线程也会造成内存不足。
+
+ 以上使用不当很容易造成:
+
+ - 频繁GC -> Stop the world,使你的应用响应变慢。
+ - OOM,直接造成内存溢出错误使得程序退出。OOM又可以分为以下几种:
+ - Heap space:堆内存不足。
+ - PermGen space:永久代内存不足。
+ - Native thread:本地线程没有足够内存可分配。
+
+ 排查堆内存问题的常用工具是jmap,是JDK自带的。一些常用用法如下:
+
+ - 查看JVM内存使用状况:`jmap -heap `。
+ - 查看JVM内存存活的对象:`jmap -histo:live `。
+ - 把heap里所有对象都dump下来,无论对象是死是活:`jmap -dump:format=b,file=xxx.hprof `
+ - 先做一次Full GC,再dump,只包含仍然存活的对象信息:`jmap -dump:format=b,live,file=xxx.hprof `
+
+ 此外,不管是使用jmap命令产生的还是在OOM时产生的dump文件,都可以使用Eclipse的MAT(MEMORY ANALYZER TOOL)来分析,可以看到具体的堆栈和内存中对象的信息。当然JDK自带的jhat也能够查看dump文件,并启动Web端口供浏览器浏览。界面如下:
+
+ 
+
+ 此外,唯品会开源的VJTools中提供了vjmap工具是一个加强版jmap,能够分代打印出堆内存的对象实例占用信息。
+
+## 8.2.3 IO分析
+
+通常与应用性能相关的包括:文件IO和网络IO。
+
+1. 文件IO
+
+ 可以使用系统工具pidstat、iostat、vmstat来查看IO的状况。这里可以看一张使用vmstat的结果图。
+
+ 
+
+ 这里主要注意bi和bo这两个值,分别表示块设备每秒接收的块数量和块设备每秒发送的块数量,由此可以判定IO繁忙状况。进一步的可以通过使用strace工具定位对文件IO的系统调用。通常,造成文件IO性能差的原因不外乎:
+
+ - 大量的随机读写
+ - 设备慢
+ - 文件太大
+
+2. 网络IO
+
+ 查看网络IO状况,一般使用的是netstat工具。可以查看所有连接的状况、数目、端口信息等。例如:当TIME_WAIT或者CLOSE_WAIT连接过多时,会影响应用的响应速度。前者需要优化内核参数,后者一般是代码Bug,没有释放网络连接。
+
+ netstat -anpt
+
+ 
+
+ 此外,还可以使用tcpdump来具体分析网络IO的数据。当然,tcpdump出的文件直接打开是一堆二进制的数据,可以使用Wireshark查看具体的连接以及其中数据的内容。
+
+ tcpdump -i eth0 -w tmp.cap -tnn dst port 8080 #监听8080端口的网络请求并打印日志到tmp.cap中
+
+ 还可以通过查看/proc/interrupts来获取当前系统使用的中断的情况。
+
+ 
+
+ 各个列依次是:
+
+ irq的序号, 在各自CPU上发生中断的次数,可编程中断控制器,设备名称(request_irq的dev_name字段)
+
+ 通过查看网卡设备的中断情况可以判断网络IO的状况。
+
+ 此外,使用sar -n DEV能够进一步看到网卡设备的收发数据包数目和字节数,由此判断是否超过网卡限制,从而进一步分析IO状况。
+
+## 8.2.4 其他分析工具
+
+上面分别针对CPU、内存以及IO讲了一些系统/JDK自带的分析工具。除此之外,还有一些综合分析工具或者框架可以更加方便我们对Java应用性能的排查、分析、定位等。
+
+- VisualVM
+
+ 这个工具应该是Java开发者们非常熟悉的一款Java应用监测工具,原理是通过JMX接口来连接JVM进程,从而能够看到JVM上的线程、内存、类等信息。
+ 
+ 如果想进一步查看GC情况,可以安装Visual GC插件。此外,VisualVM也有Btrace的插件,可以可视化直观的编写Btrace代码并查看输出日志。
+ 与VisualVm类似的,jconsole也是通过JMX查看远程JVM信息的一款工具,更进一步的,通过它还可以显示具体的线程堆栈信息以及内存中各个年代的占用情况,也支持直接远程执行MBEAN。当然,VisualVM通过安装jconsole插件也可以拥有这些功能。
+ 
+
+ 但由于这俩工具都是需要UI界面的,因此一般都是通过本地远程连接服务器JVM进程。服务器环境下,一般并不用此种方式。
+
+- Java Mission Control(JMC)
+
+ 此工具是JDK7 u40开始自带的,原来是JRockit上的工具,是一款采样型的集诊断、分析和监控与一体的非常强大的工具。但此工具基于JFR(jcmd JFR.start name=xx duration=60s settings=template.jfc filename=xx.jfr),而开启JFR需要商业证书(jcmd VM.unlock_commercial_features)。
+
+ 
+
+- Btrace && Greys
+
+ 这里不得不提的是Btrace这个神器,它使用java attach api + java agent + instrument api实现了JVM的动态追踪。在不重启应用的情况下可以加入拦截类的方法以打印日志等。但是此工具使用起来需要自己编写脚本,比较麻烦。推荐使用原理和Btrace类似的Greys:。Greys的使用示例如下:
+
+ 
+
+- arthas
+
+ 阿里开源的Java诊断工具箱,基于greys-atonomy而来。包括在线诊断、反编译字节码、查看最耗费资源的Java线程等。
+
+- Jwebap
+
+ Jwebap是一款JavaEE性能检测框架,基于ASM增强字节码实现。支持:HTTP请求、JDBC连接、method的调用轨迹跟踪以及次数、耗时的统计。由此可以定位最耗时的请求、方法,并可以查看JDBC连接的次数、是否关闭等。但此项目是2006年的一个项目,已经将近10年没有更新。根据笔者使用,已经不支持JDK7编译的应用。如果要使用,建议基于原项目二次开发,同时也可以加入对Redis连接的轨迹跟踪。当然,基于字节码增强的原理,也可以实现自己的JavaEE性能监测框架。
+
+ 
+
+ 上图来自笔者公司二次开发过的Jwebap,已经支持JDK8和Redis的连接追踪。
+
+- awesome-scripts
+
+ 这里有一个笔者参与的开源的项目:,封装了很多常用的性能分析命令,比如上文讲的打印繁忙Java线程堆栈信息、Greys命令等。
+
+- charles
+
+ HTTP协议抓包工具,主要用于查看移动端通过HTTP协议(支持HTTPs)访问服务端接口的具体数据。其通过将自己设置成系统(移动端)的网络访问代理服务器,从而实现了网络数据包的截取和分析。
+
+- tPacketcapture
+
+ TCP协议抓包工具。主要用于Android端捕获TCP数据包。生成的文件可以通过Wireshark分析。
+
diff --git a/book/chapter8-profile/end.md b/book/chapter8-profile/end.md
index d705897..cb2accb 100644
--- a/book/chapter8-profile/end.md
+++ b/book/chapter8-profile/end.md
@@ -7,4 +7,11 @@
- 不能停机或者大面积停机
- 需要尽快恢复
-附录F给出了更为宽泛的应对在线故障的思路。
\ No newline at end of file
+附录F给出了更为宽泛的应对在线故障的思路。
+
+## 学习资料
+
+- [《Linux服务器性能调整》](https://book.douban.com/subject/4027746/)
+- [《Java性能权威指南》](https://book.douban.com/subject/26740520/)
+- [《深入理解Java虚拟机(第二版)》](https://book.douban.com/subject/24722612/)
+
diff --git a/book/chapter8-profile/media/flags-1.png b/book/chapter8-profile/media/flags-1.png
new file mode 100644
index 0000000..7d351c9
Binary files /dev/null and b/book/chapter8-profile/media/flags-1.png differ
diff --git a/book/chapter8-profile/media/flags-2.png b/book/chapter8-profile/media/flags-2.png
new file mode 100644
index 0000000..6b9eea9
Binary files /dev/null and b/book/chapter8-profile/media/flags-2.png differ
diff --git a/book/chapter8-profile/media/greys.png b/book/chapter8-profile/media/greys.png
new file mode 100644
index 0000000..60753c7
Binary files /dev/null and b/book/chapter8-profile/media/greys.png differ
diff --git a/book/chapter8-profile/media/interrupts.png b/book/chapter8-profile/media/interrupts.png
new file mode 100644
index 0000000..ca60f75
Binary files /dev/null and b/book/chapter8-profile/media/interrupts.png differ
diff --git a/book/chapter8-profile/media/io.png b/book/chapter8-profile/media/io.png
new file mode 100644
index 0000000..acc9b77
Binary files /dev/null and b/book/chapter8-profile/media/io.png differ
diff --git a/book/chapter8-profile/media/jconsole.png b/book/chapter8-profile/media/jconsole.png
new file mode 100644
index 0000000..c0b96e2
Binary files /dev/null and b/book/chapter8-profile/media/jconsole.png differ
diff --git a/book/chapter8-profile/media/jhat.png b/book/chapter8-profile/media/jhat.png
new file mode 100644
index 0000000..835670c
Binary files /dev/null and b/book/chapter8-profile/media/jhat.png differ
diff --git a/book/chapter8-profile/media/jmc.png b/book/chapter8-profile/media/jmc.png
new file mode 100644
index 0000000..e90a820
Binary files /dev/null and b/book/chapter8-profile/media/jmc.png differ
diff --git a/book/chapter8-profile/media/jstack.jpg b/book/chapter8-profile/media/jstack.jpg
new file mode 100644
index 0000000..e7b58f1
Binary files /dev/null and b/book/chapter8-profile/media/jstack.jpg differ
diff --git a/book/chapter8-profile/media/jstat.jpg b/book/chapter8-profile/media/jstat.jpg
new file mode 100644
index 0000000..08ae8e8
Binary files /dev/null and b/book/chapter8-profile/media/jstat.jpg differ
diff --git a/book/chapter8-profile/media/jwebap.png b/book/chapter8-profile/media/jwebap.png
new file mode 100644
index 0000000..3d58b0a
Binary files /dev/null and b/book/chapter8-profile/media/jwebap.png differ
diff --git a/book/chapter8-profile/media/netstat.png b/book/chapter8-profile/media/netstat.png
new file mode 100644
index 0000000..adf6251
Binary files /dev/null and b/book/chapter8-profile/media/netstat.png differ
diff --git a/book/chapter8-profile/media/system.png b/book/chapter8-profile/media/system.png
new file mode 100644
index 0000000..17fd0c4
Binary files /dev/null and b/book/chapter8-profile/media/system.png differ
diff --git a/book/chapter8-profile/media/visualvm.png b/book/chapter8-profile/media/visualvm.png
new file mode 100644
index 0000000..c9d11e3
Binary files /dev/null and b/book/chapter8-profile/media/visualvm.png differ
diff --git a/book/chapter8-profile/media/vmstat.jpg b/book/chapter8-profile/media/vmstat.jpg
new file mode 100644
index 0000000..8c9e78c
Binary files /dev/null and b/book/chapter8-profile/media/vmstat.jpg differ
diff --git a/book/chapter8-profile/media/vvm-cpu.png b/book/chapter8-profile/media/vvm-cpu.png
new file mode 100644
index 0000000..83b40c8
Binary files /dev/null and b/book/chapter8-profile/media/vvm-cpu.png differ
diff --git a/book/chapter8-profile/media/vvm-memory.png b/book/chapter8-profile/media/vvm-memory.png
new file mode 100644
index 0000000..637e3be
Binary files /dev/null and b/book/chapter8-profile/media/vvm-memory.png differ
diff --git a/book/chapter8-profile/media/vvm-monitor.png b/book/chapter8-profile/media/vvm-monitor.png
new file mode 100644
index 0000000..a1255ed
Binary files /dev/null and b/book/chapter8-profile/media/vvm-monitor.png differ
diff --git a/book/chapter8-profile/profile.md b/book/chapter8-profile/profile.md
new file mode 100644
index 0000000..0e9d303
--- /dev/null
+++ b/book/chapter8-profile/profile.md
@@ -0,0 +1,225 @@
+# 8.3 性能调优
+
+与性能分析相对应,性能调优同样分为三部分。
+
+## 8.3.1 CPU调优
+
+CPU调优主要是合理安排运算过程,达到充分但不过度使用CPU的目的。
+
+- 不要存在一直运行的线程(无限循环),可以使用sleep休眠一段时间。这种情况普遍存在于一些pull方式消费数据的场景下,当一次pull没有拿到数据的时候建议sleep一下,再做下一次pull。
+- 轮询的时候可以使用wait/notify机制代替循环请求。
+- 避免正则表达式匹配、过多的计算。例如,避免使用String的format、split、replace方法;避免使用正则去判断邮箱格式(有时候会造成死循环);避免序列/反序列化。
+- 结合JVM和代码,避免产生频繁的GC,尤其是Full GC。
+
+此外,使用多线程的时候,还需要注意以下几点:
+
+- 使用线程池,减少线程数以及线程的切换。
+- 多线程对于锁的竞争可以考虑减小锁的粒度(使用ReetrantLock)、拆分锁(类似ConcurrentHashMap分bucket上锁), 或者使用CAS、ThreadLocal、不可变对象等无锁技术。此外,多线程代码的编写最好使用JDK提供的并发包、Executors框架以及ForkJoin等,此外Disruptor和Actor在合适的场景也可以使用。
+
+## 8.3.2 内存调优
+
+内存的调优主要就是对JVM的调优。
+
+### JVM内存配置
+
+- 合理设置各个代的大小。避免新生代设置过小(不够用,经常minor GC并进入老年代)以及过大(会产生碎片),同样也要避免Survivor设置过大和过小。
+- 选择合适的GC策略。需要根据不同的场景选择合适的GC策略。这里需要说的是,CMS并非全能的。除非特别需要再设置,毕竟CMS的新生代回收策略ParNew并非最快的,且会产生碎片。此外,G1直到JDK8的出现也并没有得到广泛应用,并不建议使用。
+- 老年代优先使用Parallel GC(-XX:+UseParallel[Old]GC),可以保证最大的吞吐量。由于CMS会产生碎片,确实有必要才改成CMS或G1。
+- 垃圾回收的最佳状态是只有Young GC,也就是避免生命周期很长的对象的存在。
+- 注意内存墙(严重阻碍处理器性能发挥的内存瓶颈),一般讲单点应用堆内存设置为4G到5G即可,依靠可扩展性提高并发能力。
+- 设置JVM的内存大小有一个经验法则:完成Full GC后,应该释放出70%的内存。
+- 配置堆内存和永久代/元空间内存之和小于32GB,从而可以使用压缩指针节省对象指针的占用。
+- 打开GC日志并读懂GC日志,以便于排查问题。GC日志文件可以使用GC Histogram(gchisto)生成图表和表格。
+
+ ```
+ -XX:PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:$CATALINA_BASE/logs/gc.log
+ ```
+
+其中,对于第一点,具体的还有一些建议:
+
+- 年轻代大小选择
+
+ - 响应时间优先的应用,尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生GC的频率是最小的。同时,也能够减少到达年老代的对象。
+ - 吞吐量优先的应用,也尽可能的设置大,因为对响应时间没有要求,垃圾收集可以并行进行,适合8CPU以上服务器部署的应用使用。
+
+- 年老代大小选择
+
+ - 响应时间优先的应用,年老代一般都是使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,会造成内存碎片、高回收频率以及应用暂停而使用传统的标记整理方式。如果堆大了,则需要较长的GC时间。最优化的方案,一般需要参考并发垃圾收集信息、持久代并发收集次数、传统GC信息以及花在年轻代和年老代回收上的时间比例等数据获得。
+ - 吞吐量优先的应用应该有一个很大的年轻代和一个较小的年老代。这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代存放长期存活对象。
+
+这里还需要着重说一下使用并发收集器时较小堆引起的碎片问题。因为年老代的并发收集器使用标记清除算法,所以不会对堆进行压缩。尤其当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记整理方式进行回收。如果出现“碎片”,需要进行如下配置:-XX:+UseCMSCompactAtFullCollection,开启对年老代的压缩(合并相邻空间)。同时使用-XX:CMSFullGCsBeforeCompaction=xx设置多少次Full GC后,对年老代进行压缩。
+
+### 开发建议
+
+- 使用基本数据类型而不是其包装类型能够节省内存。
+- 小对象allocate的代价很小,通常10个CPU指令;收集掉新对象也非常廉价;不用担心活的很短的小对象。
+- 大对象分配的代价以及初始化的代价很大;不同大小的大对象可能导致Java堆碎片,尤其是CMS, ParallelGC 或 G1还好;尽量避免分配大对象。
+- 避免改变数据结构大小,如避免改变数组或array backed collections / containers的大小;对象构建(初始化)时最好显式批量定数组大小;改变大小导致不必要的对象分配,可能导致Java堆碎片。
+- 避免保存重复的String对象,同时也需要小心String.subString()与String.intern()的使用, 中间过程会生成不少字符串。
+- 尽量不要使用finalizer。
+- 释放不必要的引用:ThreadLocal使用完记得释放以防止内存泄漏,各种stream使用完也记得close。
+- 使用对象池避免无节制创建对象,造成频繁GC。但不要随便使用对象池,除非像连接池、线程池这种初始化/创建资源消耗较大的场景。对象池可能潜在的问题:
+ - 增加了活对象的数量,可能增加GC时间。
+ - 访问(多线程)对象池需要锁,可能带来可扩展性的问题。
+ - 小心过于频繁的对象池访问。
+- 缓存失效算法,可以考虑使用SoftReference、WeakReference保存缓存对象。
+- 谨慎热部署/加载的使用,尤其是动态加载类等。
+- 打印日志时不要输出文件名、行号,因为日志框架一般都是通过打印线程堆栈实现,生成大量String。此外,打印日志时,先判断对应级别的日志是否打开,再做操作,否则也会生成大量String。
+
+ ```
+ if (logger.isInfoEnabled()) {
+ logger.info(msg);
+ }
+ ```
+
+### JavaEE容器内存
+
+在部署JavaEE容器时经常会有一种争论:使用大内存容器好还是多个小的容器集群好?这个是需要根据业务场景区别对待的。通常,大内存容器有以下问题:
+
+- 一旦发生Full GC,会非常耗时。
+- 一旦GC,dump出的堆快照太大,无法分析。
+
+因此,如果可以保证程序的对象大部分都是朝生夕死的,老年代不会发生GC,那么使用大内存容器是可以的。但是在伸缩性和高可用却比不上使用小内存(相对来说)容器集群。使用小内存容器集群则有以下优势:
+
+- 可以根据系统的负载调整容器的数量,以达到资源的最大利用率,
+- 可以防止单点故障。
+
+### GC的庞氏骗局
+
+虽然GC在大多数情况下还是正常的,但有时候JVM也会发生欺骗你的场景,JVM不停的在垃圾回收,可是每次回收完后堆却还是满的,很明显程序内存被使用完了,已经无法正常工作了,但JVM就是不抛出OutOfMemoryError(OOM)这个异常来告诉程序员内部发生了什么,只是不停地尝试帮我们做垃圾回收,直至把服务器的资源耗光。
+
+出现这种现象的一种典型情况就是GC的GCTimeLimit和GCHeapFreeLimit参数设置不合适。GCTimeLimit的默认值是98%,也就是说如果大于等于98%的时间都用花在GC上,则会抛出OutOfMemoryError。GCHeapFreeLimit是回收后可用堆的大小,默认值是2%,也就是说只要有多余2%的内存可用就认为此次gc是成功的。如果GCTimeLimit设置过大或者GCHeapFreeLimit设置过小那么就会造成GC的庞式骗局,不停地进行垃圾回收。
+
+## 8.3.3 IO调优
+
+文件IO上需要注意:
+
+- 考虑使用异步写入代替同步写入,可以借鉴Redis的AOF机制。
+- 利用缓存,减少随机读。
+- 尽量批量写入,减少IO次数和寻址。
+- 使用数据库代替文件存储。
+
+网络IO上需要注意:
+
+- 和文件IO类似,使用异步IO、多路复用IO/事件驱动IO代替同步阻塞IO。
+- 批量进行网络IO,减少IO次数。
+- 使用缓存,减少对网络数据的读取。
+- 使用协程: Quasar。
+
+## 8.3.4 其他优化建议
+
+- 算法、逻辑上是程序性能的首要,遇到性能问题,应该首先优化程序的逻辑处理。
+- 优先考虑使用返回值而不是异常表示错误。虽然现代JVM已经做了大量优化工作,但毕竟异常是有代价的,需要在合适的地方使用。如果使用异常并且比较关注性能,可以通过覆盖掉异常类的fillInStackTrace()方法为空方法,使其不拷贝栈信息。
+- 查看自己的代码是否对内联是友好的。这里所说的内联友好指的方法的大小不超过35字节(默认的内联阈值,不建议修改)、不是虚方法(虚方法指的是在运行期才能确定执行对象的方法,最新的JVM对非虚方法会通过CHA类层次分析来判断是否可以内联)。
+
+此外,网上有一些过时的建议:
+
+- 变量用完设置为null,加快内存回收。这种用法大部分情况下并没有意义。一种情况除外:如果有个Java方法没有被JIT编译但里面仍然有代码会执行比较长时间,那么在那段会执行长时间的代码前显式将不需要的引用类型局部变量置null是可取的。
+- 方法参数设置为final,这种用法也没有太大的意义,尤其在JDK8中引入了effective final,会自动识别final变量。
+
+## 8.3.5 JVM参数配置
+
+JVM的参数设置在很大程度上影响Java应用的性能。而JVM参数众多,很容易混淆和设置错误。这里针对Oracle/Sun JDK 7讲述一些需要注意的JVM参数设置。
+
+1. 启动参数默认值
+
+ Java有很多的启动参数,而且很多版本都并不一样。但是现在网上充斥着各种资料,如果不加辨别的全部使用,很多是没有效果或者本来就是默认值的。一般的,我们可以通过使用java -XX:+PrintFlagsInitial来查看所有可以设置的参数以及其默认值。也可以在程序启动的时候加入-XX:+PrintCommandLineFlags来查看与默认值不相同的启动参数。如果想查看所有启动参数(包括和默认值相同的),可以使用-XX:+PrintFlagsFinal。
+ 
+ 
+
+ 输出里“=”表示使用的是初始默认值,而“:=”表示使用的不是初始默认值,可能是命令行传进来的参数、配置文件里的参数或者是Ergonomics(自动优化机制)自动选择了别的值。最后一列中product表示所有平台的默认值都一样,pd_product则表示默认值因平台不同而不同。
+
+ 此外,还可以使用jinfo命令显示启动的参数。
+
+ ```
+ jinfo -flags [pid] #查看目前启动使用的有效参数
+
+ jinfo -flag [flagName] [pid] #查看对应参数的值
+ ```
+
+ 这里需要指出的是,当你配置JVM参数时,最好是先通过以上命令查看对应参数的默认值再确定是否需要设置。也最好不要配置你搞不清用途的参数,毕竟默认值的设置是有它的合理之处的。
+
+2. 动态设置参数
+
+ 当Java应用启动后,定位到了是GC造成的性能问题,但是你启动的时候并没有加入打印GC的参数,很多时候的做法就是重新加参数然后重启应用。但这样会造成一定时间的服务不可用。最佳的做法是能够在不重启应用的情况下,动态设置参数。使用jinfo可以做到这一点(本质上还是基于JMX的)。
+
+ ```
+ jinfo -flag [+/-][flagName] [pid] #启用/禁止某个参数
+ jinfo -flag [flagName=value] [pid] #设置某个参数
+ ```
+ 对于上述的GC的情况,就可以使用以下命令打开HeapDump并设置Dump路径。
+
+ ```
+ jinfo -flag +HeapDumpBeforeFullGC [pid]
+ jinfo -flag +HeapDumpAfterFullGC [pid]
+ jinfo -flag HeapDumpPath=/home/dump/dir [pid]
+ ```
+ 同样的也可以动态关闭。
+
+ ```
+ jinfo -flag -HeapDumpBeforeFullGC [pid]
+ jinfo -flag -HeapDumpAfterFullGC [pid]
+ ```
+ 其他的参数设置类似。
+
+ 这里需要注意的是,并非所有的参数通过此种方式设置都能生效。1中PrintFlagsFinal的信息的最后一列的值除了product和pd_product,还会出现manageable(运行时可以动态更改标识的值)。只有最后一列是此值的选项通过jinfo设置JVM才会响应更改,否则即使设置成功,也并不生效。大部分影响GC算法行为的标识都是在启动时生效,无法通过jinfo动态改变。
+
+3. -verbose:gc 与 -XX:+PrintGCDetails
+
+ 很多GC推荐设置都同时设置了这两个参数,其实,只要打开了-XX:+PrintGCDetails,前面的选项也会同时打开,无须重复设置。
+
+4. -XX:+DisableExplicitGC
+
+ 这个参数的作用就是使得System.gc()变为空调用,很多推荐设置里面都是建议开启的。但是,如果你用到了NIO或者其他使用到堆外内存的情况,使用此选项会造成OOM。可以用XX:+ExplicitGCInvokesConcurrent或XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses(配合CMS使用,使得System.gc()触发一次并发GC)代替。
+
+ 此外,还有一个比较有意思的地方。如果你不设置此选项的话,当你使用了RMI的时候,会周期性地来一次Full GC。这个现象是由于分布式GC造成的。
+
+5. MaxDirectMemorySize
+
+ 此参数是设置的堆外内存的上限值。当不设置的时候为-1,此值为-Xmx减去一个survivor space的预留大小。
+
+6. 由于遗留原因,作用相同的参数
+
+ - -Xss 与 -XX:ThreadStackSize。
+ - -Xmn 与 -XX:NewSize,此外这里需要注意的是设置了-Xmn的话,NewRatio就没作用了。
+
+7. -XX:MaxTenuringThreshold
+
+ 使用工具查看此值默认值为15,但是选择了CMS的时候,此值会变成4。当此值设置为0时,所有Eden里的活对象在经历第一次Minor GC的时候就会直接晋升到Old Gen,Survivor Space直接就没用。
+
+8. -XX:HeapDumpPath
+
+ 使用此参数可以指定-XX:+HeapDumpBeforeFullGC、-XX:+HeapDumpAfterFullGC、-XX:+HeapDumpOnOutOfMemoryError触发HeapDump时文件的存储位置。
+
+还有一点需要注意的是,对于栈大小(-Xss 与 -XX:ThreadStackSize)的设置。在Linux x64上ThreadStackSize的默认值是1024KB,给Java线程创建栈会用这个参数指定的大小。如果把-Xss或者-XX:ThreadStackSize设为0,就是使用“系统默认值”。而在Linux x64上HotSpot VM给Java栈定义的“系统默认”大小也是1MB。所以普通Java线程的默认栈大小怎样都是1MB。这里有一个需要注意的地方就是Java的栈大小和之前提到过的操作系统的操作系统栈大小(ulimit -s):这个配置只影响进程的初始线程;后续用pthread_create创建的线程都可以指定栈大小。HotSpot VM为了能精确控制Java线程的栈大小,特意不使用进程的初始线程(primordial thread)作为Java线程。
+
+附录E的最后一部分给出了一个Tomcat的JVM参数配置示例。
+
+## 8.3.6 JVM性能增强
+
+JDK7、8在JVM的性能上做了一些增强以提高Java应用的性能。
+
+1. 多层编译
+
+ 通过-XX:+TieredCompilation开启JDK7的多层编译(JDK8中默认开启)。多层编译结合了客户端C1编译器和服务端C2编译器的优点(客户端编译能够快速启动和及时优化,服务器端编译可以提供更多的高级优化),是一个非常高效利用资源的切面方案。
+
+ 在开始时先进行低层次的编译,同时收集信息,在后期再进一步进行高层次的编译进行高级优化。
+
+ 需要注意的一点:这个参数会消耗比较多的内存资源,因为同一个方法被编译了多次,存在多份native内存拷贝,建议把代码缓存调大一点儿(-XX:+ReservedCodeCacheSize,InitialCodeCacheSize)。否则有可能由于代码缓存不足,JIT编译的时候不停的尝试清理代码缓存,丢弃无用方法,消耗大量资源在JIT线程上。
+
+1. Compressed Oops
+
+ 压缩指针在JDK7中的Server模式下已经默认开启。
+
+1. Zero-Based Compressed Ordinary Object Pointers
+
+ 当使用了上述的压缩指针时,在64位JVM上,会要求操作系统保留从一个虚拟地址0开始的内存。如果操作系统支持这种请求,那么就开启了Zero-Based Compressed Oops。这样可以使得无须在Java堆的基地址添加任何地址补充即可把一个32位对象的偏移解码成64位指针(64位地址=堆的基地址+偏移量)。
+
+1. 逃逸分析(Escape Analysis)
+
+ Server模式的编译器会根据代码的情况,来判断相关对象的逃逸类型,从而决定是否在堆中分配空间,是否进行标量替换(在栈上分配原子类型局部变量)。此外,也可以根据调用情况来决定是否自动消除同步控制,如StringBuffer。这个特性从Java SE 6u23开始就默认开启。
+
+1. NUMA Collector Enhancements
+
+ 这个主要针对的是The Parallel Scavenger垃圾回收器。使其能够利用NUMA架构的机器的优势来更快的进行GC。可以通过-XX:+UseNUMA开启支持。
+
diff --git a/book/chapter8-profile/ready.md b/book/chapter8-profile/ready.md
index 64e1665..389a9f7 100644
--- a/book/chapter8-profile/ready.md
+++ b/book/chapter8-profile/ready.md
@@ -257,6 +257,7 @@ HotSpot VM内部有一些线程进行JVM的管理、监控、垃圾回收工作
此外,这里面的%iowait在Linux下的计算方式是CPU空闲、并且有仍未完成的IO请求的时间占总时间的比例。因此,%iowait升高并不一定代表IO设备有瓶颈,有可能是CPU没有可以运行的进程造成的。需要结合await、svctm等其他指标来判断。如下如所示:

+
1. top
@@ -282,9 +283,9 @@ HotSpot VM内部有一些线程进行JVM的管理、监控、垃圾回收工作
5 root 34 19 0 0 0 S 0.0 0.0 0:00.17 ksoftirqd/1
```
- 通过此命令,可以相对全面的查看系统负载的来源。同时,top命令支持排序,可以按照不同的列排序,方便查找出诸如内存占用最多的进程、CPU占用率最高的进程等。但top命令是一个瞬时输出的值,最好是通过定时存储到文件来进行对比诊断。需要注意的是,对于每一个进程的%CPU这一列,其默认是Irix Mode,为单CPU衡量的一个值,最大值为100%。可以使用I指令切换模式为Solaris Mode,此值在多处理器环境下,为占的总的CPU的使用率,例如,4核CPU中%CPU最高值是400%。
+ 通过此命令,可以相对全面的查看系统负载的来源。同时,top命令支持排序,可以按照不同的列排序,方便查找出诸如内存占用最多的进程、CPU占用率最高的进程等。但top命令是一个瞬时输出的值,最好是通过定时存储到文件来进行对比诊断。需要注意的是,对于每一个进程的%CPU这一列,其默认是Irix Mode,此值在多处理器环境下,为占的所有CPU的使用率之和,最大值为100% * CPU核数。可以使用I指令切换模式为Solaris Mode,为将所有CPU看做一个整体的单CPU衡量的一个值,最大值为100%,一般情况下其值等于Irix模式下的%CPU/CPU核数。
- 还需要注意的是,使用ps命令也能够拿到某个进程的CPU使用率,但是其是从进程创建开始就计算,为该进程处于Running状态的时间占进程总时间的百分比,可以看做平均CPU使用率。而Top的%CPU是不断刷新计算的(数据来源于/proc/pid/stats),可以认为是实时的。
+ 还需要注意的是,使用ps命令也能够拿到某个进程的CPU使用率,但是其是从进程创建开始就计算,为该进程处于Running状态的时间占进程总时间的百分比,可以看做平均CPU使用率。而Top的%CPU是不断刷新计算的(数据来源于/proc/[pid]/stat),可以认为是实时的。
## 8.1.4 JDK常用诊断工具
diff --git a/book/chapter9-security/README.md b/book/chapter9-security/README.md
new file mode 100644
index 0000000..b592f54
--- /dev/null
+++ b/book/chapter9-security/README.md
@@ -0,0 +1,13 @@
+# 第九章 安全技术
+
+在表面平静的互联网应用下面,其实并不是那么平静。每天都有无数的安全漏洞被爆出,有的是被白帽子黑客发现提醒大家,而有的则早已被人利用做了一些“坏”的事情;每天也有无数的安全攻击在进行,或是对竞品进行DDOS, 或是插入一些非安全代码来获取用户数据。
+
+随着攻击现象的越来越多,安全问题正越来越受到各个互联网公司的重视,都从各个方面加固自身的安全壁垒,包括硬件层面、软件层面、行政层面等。作为Java开发工程师也需要了解并能够掌握常用安全技术的使用。只有这样,才能减少应用被入侵的可能性,从而最大化避免损害公司利益。
+
+本章主要讲述Java开发中常用的安全技术以及一些常见Web安全问题的解决思路。包括:
+
+- Java加密
+- HTTPS
+- 常见Web安全问题
+
+
diff --git a/book/chapter9-security/https.md b/book/chapter9-security/https.md
new file mode 100644
index 0000000..af14972
--- /dev/null
+++ b/book/chapter9-security/https.md
@@ -0,0 +1,103 @@
+# 9.2 HTTPS
+
+很多时候后端的应用都会直接提供HTTP接口以供浏览器、客户端、第三方来调用。但由于HTTP协议是明文传输、且没有任何的身份认证机制和数据完整性保证,因此数据很容易被中间人进行劫持、监听、篡改等。虽然能够通过对传输的业务数据加密避免这一点,但HTTPS才是最根本的解决方案。这也是现在好多互联网公司都在进行全站HTTPS迁移的原因,也是AppStore要求应用调用的api接口都要换成HTTPS的动机所在。
+
+HTTP和HTTPS的对比如下图所示:
+
+
+
+可以看到,HTTPS相比HTTP多了一个安全加密层,不仅对数据进行了加密,还对数据完整性提供了保护,并且也提供了身份验证的功能。
+
+上一节最后提到,非对称加密很多情况下会与对称加密一起使用。HTTPS就是一个典型的应用场景。简单说就是:其中一方先生成一个对称加密密钥,然后通过非对称加密的方式来发送这个密钥,这样双方之后的通信就可以用对称加密这种高效率的算法进行加解密。
+
+## 9.2.1 SSL/TLS
+
+SSL和TLS都是用于保障端到端之间连接的安全性的,位于应用层和传输层之间。SSL现在已经改名为TSL, 主流版本为TLS1.2。其结构如下图所示:
+
+
+
+- 握手层:端与端之间协商密码、连接状态等连接参数,并完成身份验证。
+- 记录层:对数据的封装,数据交给传输层之前,会经过分片、压缩、认证、加密。
+
+## 9.2.2 CA
+
+CA是HTTPS依赖的关键组件,即Certificate Authority,证书中心。客户端从CA获取服务器公钥,并能够保证此公钥不会被中间人篡改。
+
+CA对公钥完整性的保证依赖的一个机制就是颁发证书,证书包括以下内容:
+
+- 证书的发布机构
+- 证书的有效期
+- 公钥
+- 证书所有人
+- 数字签名
+
+使用CA的一个流程如下:
+
+1. 首先将公钥与个人信息用一个Hash算法生成一个消息摘要,然后CA再用它的私钥对消息摘要加密,最终形成数字签名。
+2. 客户端接收到证书时,用同样的Hash算法再次生成一个消息摘要,然后用CA的公钥对证书进行解密,之后再对比两个消息摘要即可保证数据未被篡改。
+
+此外,为了保证CA的权威性以及其自身公钥的权威性,CA机构是一个树形的结构,父节点是信用高的CA,它会对子节点的CA做信用背书。
+
+## 9.2.3 交互过程
+
+HTTPS的交互过程如下图所示:
+
+
+1. 客户端向服务器发送请求,将客户端的功能和首选项传送给服务器,包括客户端支持的SSL版本、加密组件列表等。
+2. 服务器发送选择的连接参数(从客户端加密组件中筛选出的加密组件内容和压缩方法)以及证书(包含公钥等信息)给客户端。
+3. 客户端读取证书中的所有人、有效期等信息并进行校验,然后通过预置的CA验证证书合法性,有问题则提示。
+4. 客户端生成用于数据加密的对称密钥,然后用服务器的公钥进行加密并发送给服务端。
+5. 服务器使用自己的私钥解密数据,获得用于数据加密的对称密钥。
+6. 安全的通道建立完毕,后续基于对称加密传输数据。
+
+## 9.2.4 性能优化
+
+虽然HTTPS安全性比HTTP要高很多,但是由于建立通信通道要首先交互很多次,应用的性能会受到不少的影响,因此优化HTTPS的性能非常关键。
+
+1. 算法选择
+
+ HTTPS的通信过程中有不少算法参与的地方,算法的性能直接决定了HTTPS的性能:
+
+ - 数字签名:选择ECDSA算法,它的签名性能远超过RSA,而且签名是在服务端做的。服务器发送给客户端的证书链包含所有中间证书。
+ - 密钥交换: ECDHE具有更好的性能,并且其支持前向保密(Forward Secrecy),可以避免中间人保存下来客户端和服务端之间的所有通信数据,并且能开启TLS false start。
+ - 对称加密:AES256-GCM-SHA384的性能比较好,建议选择此算法进行数据加密。
+
+1. TLS缓冲区
+
+ TLS缓冲区大小即一个TLS Record的大小,在Nginx中默认是16k。如果HTTP的数据是320K,那么就会被拆分为20个TLS Record,然后每个TLS Record会被TCP层拆分为多个TCP包传输发送给客户端。
+
+ 如果此值过小,那么TLS Record Head的负载就增加,会降低连接的吞吐量;而如果此值过大,拆分出的TCP包就比较大,传输过程中容易出现丢包,整个TLS Record到达客户端的时间就会加长。
+
+ 由于在TCP慢启动的过程中TCP连接的拥塞窗口cwnd较小,TCP连接吞吐量也小,因此可以把TLS Record Size设置小一点;而在TCP连接结束慢启动之后,吞吐量上来了,TLS Record Size就可以增大一些。
+
+1. TLS False Start
+
+ 主要指的是客户端这边的TLS False Start。开启此选项,那么客户端在发送 Change Cipher Spec、Finished 之后,可以立即发送应用数据,无需等待服务端的 Change Cipher Spec、Finished。这样,应用数据的发送实际上并未等到握手全部完成,从而节省出一个RTT时间,可以提高一定的性能。
+
+ 但开启此选项,需要满足以下条件:
+
+ - 客户端和服务端都需要支持NPN/ALPN(浏览器要求)。
+ - 需要采用支持前向保密的密码套件(ECDHE)。
+
+1. Session Cache && Session Ticket
+
+ 服务端对于一次Session是有缓存的。如果能够在它的Session Cache中找到对应Session ID的session-state(存储协商好的密码套件等信息),那么服务端就不必再经历一系列Session建立过程。因此,开启Session Cache是提升连接性能的有效措施之一。
+
+ 此外,服务端可以通过某种机制将session-state加密后作为ticket发给客户端。客户端凭借该ticket就可以恢复先前的会话了。这就是Session Ticket机制。开启此机制也能够减少Session的建立过程,提高性能。
+
+以上优化措施都是运维层面的,对于Java开发来说,可以通过在应用层做预连接,在网页端或者客户端用户发起访问请求之前提前把这个握手过程完成,从而减少延迟,能一定程度提高连接的性能。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/book/chapter9-security/java-encrypt.md b/book/chapter9-security/java-encrypt.md
new file mode 100644
index 0000000..6934f58
--- /dev/null
+++ b/book/chapter9-security/java-encrypt.md
@@ -0,0 +1,328 @@
+# 9.1 Java加密
+
+当需要加密用户数据、验证请求发起者身份、散列数据的时候,都是需要加解密算法的。JDK对常用的加解密算法都已经做了支持。包括:
+
+- 单向加密算法
+- 对称加密算法
+- 非对称加密算法
+
+## 9.1.1 单向加密算法
+
+单向加密算法指的是接收一段明文,然后以一种不可逆的方式将它转换成一段密文,简单说就是能够加密数据,但是不能够解密回原来的明文的加密方式。常用的包括:
+
+* MD5
+* SHA
+* HMAC
+
+### MD5
+
+MD5, 全称Message Digest algorithm 5,信息摘要算法,是为计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护以及散列数据的功能。虽然现在有彩虹表能够一定程度的使用碰撞进行解密,但在使用合适的salt的情况下,被解密的几率是微乎其微的。
+
+MD5能够将无论多长的数据最后都编码成128位数据,且对同样的数据最后的加密结果会一直保持一致。因此常用于文件校验、密码加密、散列数据等。
+
+JDK中的实现如下:
+
+```
+byte[] data = ...; //明文数据
+MessageDigest md5 = MessageDigest.getInstance("md5");
+md5.update(data); //加密后的数据
+
+```
+上面说的加salt指的是:对明文MD5后,拼接一个随机字符串(用于存储用户密码时,可以选择用户的信息做为盐),然后再进行一次MD5,如下:
+
+```
+密文 = MD5( MD5(明文) + salt)
+```
+
+### SHA
+
+SHA, 全称Secure Hash Algorithm,安全散列算法,被广泛地应用于电子商务等信息安全领域。其用途和MD5类似,但是安全性要高于MD5, 且其最终的加密结果是160位数据。
+
+其JDK代码实现:
+
+```
+MessageDigest sha = MessageDigest.getInstance("SHA1");
+sha.update(data);
+```
+
+### HMAC
+
+HMAC,Hash Message Authentication Code,散列消息鉴别码,是基于密钥的hash算法的认证协议。
+
+HMAC的认证的原理:使用一个密钥生成一个固定大小的小数据块,即MAC,并将其加入到消息中,然后传输。接收方利用与发送方共享的密钥进行鉴别认证等。经常用于对api参数进行请求验证:分配给授权调用方一个密钥,调用方使用密钥和接口的相关信息散列计算出请求签名,然后将签名连同数据一起发给服务方;服务方根据调用方标识,使用其密钥和同样的散列算法计算出签名和传来的签名做比较,以验证请求的合法性。
+
+1. 初始化密钥
+
+ ```
+ KeyGenerator keyGenerator = KeyGenerator.getInstance("HmacMD5");
+
+ SecretKey secretKey = keyGenerator.generateKey();
+ byte[] secret = secretKey.getEncoded();
+ ```
+
+1. HMAC加密
+
+ ```
+ SecretKey secretKey = new SecretKeySpec(secret, "HmacMD5");
+ Mac mac = Mac.getInstance(secretKey.getAlgorithm());
+ mac.init(secretKey);
+
+ byte[] data = ...;//明文
+ mac.doFinal(data);
+ ```
+
+这里需要注意的是,MAC算法除了HmacMD5,还有:
+
+- HmacSHA1
+- HmacSHA256
+- HmacSHA384
+- HmacSHA512
+
+## 9.1.2 对称加密算法
+
+对称加密又称为单密钥加密,是采用单钥密码系统的加密方法,同一个密钥既可以加密,也可以解密。一般的通信加密流程如下:
+
+
+Java中常用的对称加密算法是DES、AES以及PBE。
+
+### DES
+
+DES,全称Data Encryption Standard,即数据加密标准,其使用64位的密钥把64位的明文输入块经过16轮一系列替换和移位后变为64位的密文输出块。此算法的入口参数有三个:
+
+- Key:8个字节共64位,是DES算法的工作密钥,但DES实际使用其中的56比特。
+- Data:8个字节64位,是要被加密或被解密的数据。
+- Mode:DES的工作方式,包括加密或解密。
+
+加密:
+
+```
+byte[] secret = ..;//密钥
+DESKeySpec keySpec= new DESKeySpec(secret);
+SecretKey key = SecretKeyFactory.getInstance("DES").generateSecret(keySpec);
+
+Cipher cipher = Cipher.getInstance("DES");
+cipher.init(Cipher.ENCRYPT_MODE, key);
+byte[] encryData = cipher.doFinal(data); //加密数据
+```
+
+解密:
+
+```
+Cipher cipher = Cipher.getInstance("DES");
+cipher.init(Cipher.DECRYPT_MODE, key);
+cipher.doFinal(encryData); //解密数据
+```
+## AES
+
+AES是DES的升级版,相比DES,其具有以下特点:
+
+- 运算速度快。
+- 对内存的需求非常低。
+- 支持可变分组长度,分组长度可设定为32比特的任意倍数,最小值为128比特,最大值为256比特、
+- 密钥长度可设定为32比特的任意倍数,范围为128bit到256bit。
+
+加解密代码和DES基本一致。
+
+```
+byte[] secret = ..;//密钥
+SecretKey key = new SecretKeySpec(secret, "AES");
+
+Cipher cipher = Cipher.getInstance("AES");
+cipher.init(Cipher.ENCRYPT_MODE, key);
+byte[] encryData = cipher.doFinal(data); //加密数据
+
+Cipher cipher = Cipher.getInstance("AES");
+cipher.init(Cipher.DECRYPT_MODE, key);
+cipher.doFinal(encryData); //解密数据
+```
+## PBE
+
+PBE, 全称Password-based encryption,基于密码加密,是一种简便的加密方式。密钥由用户自己掌管,不借助任何物理媒体;采用salt杂凑多重加密等方法保证数据的安全性。
+
+1. 生成salt
+
+ ```
+ byte[] salt = new byte[8];
+ SecureRandom random = new SecureRandom();
+ random.nextBytes(salt);
+ ```
+1. 加密
+
+ ```
+ String password = ..;//用户口令
+ PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
+ SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWITHMD5andDES");
+ SecretKey secretKey = keyFactory.generateSecret(keySpec);
+
+ PBEParameterSpec paramSpec = new PBEParameterSpec(salt, 200); //迭代200次
+ Cipher cipher = Cipher.getInstance("PBEWITHMD5andDES");
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
+
+ byte[] encryData = cipher.doFinal(data);
+ ```
+
+1. 解密
+
+ ```
+ PBEParameterSpec paramSpec = new PBEParameterSpec(salt, 200); //迭代200次
+ Cipher cipher = Cipher.getInstance("PBEWITHMD5andDES");
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, paramSpec);
+
+ cipher.doFinal(encryData);
+ ```
+
+## 9.1.3 非对称加密算法
+
+非对称加密需要两个密钥,一个是公开的,称为公钥;另一个是私有的,称为私钥。公钥用来加密数据,只有私钥才能解密;私钥一般用来签名, 公钥用来验证签名。安全性要比对称加密高很多,但是其运算消耗资源多、效率慢,因此很多情况下都是结合对称加密一起使用。一个简单的非对称加密通信过程如下:
+
+
+
+Java中常用的非对称加密算法是RSA和DH。
+
+### RSA
+
+RSA, 是以算法发明者的名字命名的, 其安全性依赖于大数分解的困难,主要用于认证和数据加解密,也可以用于密钥交换。一般流程如下:
+
+1. A构建一对密钥,将公钥公布给B,将私钥保留。
+2. A使用私钥加密数据并签名,发送给B。
+3. B使用A的公钥、签名来验证收到的密文是否有效,有效则使用A的公钥对数据解密。
+4. B使用A的公钥加密数据,发送给A。A使用自己的私钥进行解密。
+
+代码示例如下:
+
+1. 生成公钥和私钥
+
+ ```
+ KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
+ keyPairGen.initialize(1024);
+
+ KeyPair keyPair = keyPairGen.generateKeyPair();
+
+ // 公钥
+ byte[] publicKey = keyPair.getPublic().getEncoded();
+
+ // 私钥
+ byte[] privateKey = keyPair.getPrivate().getEncoded();
+ ```
+
+1. 公钥加密数据
+
+ ```
+ X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(publicKeyBytes);
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ Key publicKey = keyFactory.generatePublic(x509KeySpec);
+
+ // 对数据加密
+ Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
+ cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+ String plainText = "plain text.";
+ byte[] encryData = cipher.doFinal(plainText.getBytes());
+ ```
+
+1. 私钥解密数据
+
+ ```
+ PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ Key privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
+
+ // 对数据解密
+ Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
+ cipher.init(Cipher.DECRYPT_MODE, privateKey);
+ cipher.doFinal(encryData);
+ ```
+
+如上,私钥使用加密模式,公钥使用解密模式,即可实现私钥签名、公钥验证的流程。
+
+### DH
+
+DH, 全称Diffie-Hellman算法,是一个密钥交换协议。主要用来做密钥交换,一般不用做认证和加解密数据。其一般的使用流程如下:
+
+1. A构建一对密钥,将公钥公布给B,将私钥保留。
+2. B通过A公钥构建密钥对儿,将公钥公布给A,将私钥保留。
+3. AB双方互通本地密钥算法。
+4. AB双方公开自己的公钥,使用对方的公钥和刚才产生的私钥加密数据,同时可以使用对方的公钥和自己的私钥对数据解密。
+
+可见相比起RSA,DH要多发送一个DH公钥,并且其最终对数据的加解密是依赖于本地对称加密算法。
+
+Java代码如下:
+
+1. 初始化A的公钥和私钥
+
+ ```
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DH");
+ keyPairGenerator.initialize(1024); //密钥字节数
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ byte[] aPublicKey = keyPair.getPublic().getEncoded(); //A的公钥
+ byte[] aPrivateKey = keyPair.getPrivate().getEncoded(); //A的私钥
+ ```
+
+1. 初始化B的公钥和私钥
+
+ ```
+ X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(aPublicKey);
+ KeyFactory keyFactory = KeyFactory.getInstance("DH");
+ PublicKey aPubKey = keyFactory.generatePublic(x509KeySpec);
+
+ DHParameterSpec dhParamSpec = ((DHPublicKey) aPubKey).getParams(); //由A的公钥构建B的密钥对
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(keyFactory.getAlgorithm());
+
+ keyPairGenerator.initialize(dhParamSpec);
+
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ byte[] bPublicKey = keyPair.getPublic().getEncoded(); //B的公钥
+ byte[] bPrivateKey = keyPair.getPrivate().getEncoded(); //B的私钥
+ ```
+
+1. 用A公钥和B的私钥构建密文
+
+ ```
+ String plainText = "你好";
+
+ KeyFactory keyFactory = KeyFactory.getInstance("DH");
+ X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(aPublicKey);
+ PublicKey pubKey = keyFactory.generatePublic(x509KeySpec);
+
+ PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(bPrivateKey);
+ Key priKey = keyFactory.generatePrivate(pkcs8KeySpec);
+
+ KeyAgreement keyAgree = KeyAgreement.getInstance(keyFactory.getAlgorithm());
+ keyAgree.init(priKey);
+ keyAgree.doPhase(pubKey, true);
+
+ SecretKey secretKey = keyAgree.generateSecret("DES"); //本地密钥
+ Cipher cipher = Cipher.getInstance(secretKey.getAlgorithm());
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+
+ byte[] encryData = cipher.doFinal(plainText.getBytes());
+ ```
+
+ DH需要对称加密算法对数据加密,这里使用DES。
+
+1. 用B公钥,A私钥解密
+
+ ```
+ KeyFactory keyFactory = KeyFactory.getInstance("DH");
+ X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(bPublicKey);
+ PublicKey pubKey = keyFactory.generatePublic(x509KeySpec);
+
+ PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(aPrivateKey);
+ Key priKey = keyFactory.generatePrivate(pkcs8KeySpec);
+
+ KeyAgreement keyAgree = KeyAgreement.getInstance(keyFactory.getAlgorithm());
+ keyAgree.init(priKey);
+ keyAgree.doPhase(pubKey, true);
+
+ SecretKey secretKey = keyAgree.generateSecret("DES"); //本地密钥
+
+ Cipher cipher = Cipher.getInstance(secretKey.getAlgorithm());
+ cipher.init(Cipher.DECRYPT_MODE, secretKey);
+
+ cipher.doFinal(encryData);
+ ```
+
+
+
+
diff --git a/book/chapter9-security/media/15021208790317.jpg b/book/chapter9-security/media/15021208790317.jpg
new file mode 100644
index 0000000..1424eeb
Binary files /dev/null and b/book/chapter9-security/media/15021208790317.jpg differ
diff --git a/book/chapter9-security/media/15021212669511.jpg b/book/chapter9-security/media/15021212669511.jpg
new file mode 100644
index 0000000..80fc6bd
Binary files /dev/null and b/book/chapter9-security/media/15021212669511.jpg differ
diff --git a/book/chapter9-security/media/15021213698677.jpg b/book/chapter9-security/media/15021213698677.jpg
new file mode 100644
index 0000000..94929c7
Binary files /dev/null and b/book/chapter9-security/media/15021213698677.jpg differ
diff --git a/book/chapter9-security/media/15021224587629.jpg b/book/chapter9-security/media/15021224587629.jpg
new file mode 100644
index 0000000..0b7ab72
Binary files /dev/null and b/book/chapter9-security/media/15021224587629.jpg differ
diff --git a/book/chapter9-security/media/15021224733618.jpg b/book/chapter9-security/media/15021224733618.jpg
new file mode 100644
index 0000000..31195ec
Binary files /dev/null and b/book/chapter9-security/media/15021224733618.jpg differ
diff --git a/book/chapter9-security/media/15021231454651.jpg b/book/chapter9-security/media/15021231454651.jpg
new file mode 100644
index 0000000..e2b4940
Binary files /dev/null and b/book/chapter9-security/media/15021231454651.jpg differ
diff --git a/book/chapter9-security/media/15021240597009.jpg b/book/chapter9-security/media/15021240597009.jpg
new file mode 100644
index 0000000..01bbcf5
Binary files /dev/null and b/book/chapter9-security/media/15021240597009.jpg differ
diff --git a/book/chapter9-security/media/15021346270345.jpg b/book/chapter9-security/media/15021346270345.jpg
new file mode 100644
index 0000000..0c175f8
Binary files /dev/null and b/book/chapter9-security/media/15021346270345.jpg differ
diff --git a/book/chapter9-security/media/15021347980488.jpg b/book/chapter9-security/media/15021347980488.jpg
new file mode 100644
index 0000000..98628be
Binary files /dev/null and b/book/chapter9-security/media/15021347980488.jpg differ
diff --git a/book/chapter9-security/media/15027803778485.jpg b/book/chapter9-security/media/15027803778485.jpg
new file mode 100644
index 0000000..c235f01
Binary files /dev/null and b/book/chapter9-security/media/15027803778485.jpg differ
diff --git a/book/chapter9-security/media/reflect-ddos.png b/book/chapter9-security/media/reflect-ddos.png
new file mode 100644
index 0000000..7b18e18
Binary files /dev/null and b/book/chapter9-security/media/reflect-ddos.png differ
diff --git a/book/chapter9-security/web-security.md b/book/chapter9-security/web-security.md
new file mode 100644
index 0000000..7520fff
--- /dev/null
+++ b/book/chapter9-security/web-security.md
@@ -0,0 +1,258 @@
+# 9.3 Web安全
+
+Java开发很大的一个应用场景就是Web,即使不是Web, 很多时候也是采用的和Web类似的处理方式。因此了解目前常见的Web安全问题并做防范是非常关键的。
+
+Web安全问题,从大的方面可以分为:
+
+- 客户端安全:通过浏览器进行攻击的安全问题。
+- 服务端安全:通过发送请求到服务端进行攻击的安全问题。
+
+常见的客户端安全问题有:
+
+- 跨站脚本攻击
+- 跨站点请求伪造
+
+常见的服务端安全问题有:
+
+- SQL注入
+- 基于约束条件的SQL攻击
+- DDOS攻击
+- Session fixation
+
+本文主要针对这些问题进行讲述。
+
+## 9.3.1 跨站脚本攻击
+
+跨站脚本攻击,全称Cross Site Script(XSS),故名思议是跨越两个站点的攻击方式。一般指的是攻击方通过“HTML”注入的方式篡改了网页,插入了恶意的脚本,从而在用户浏览网页或者移动客户端使用WebView加载时,默默地做了一些控制操作。
+
+XSS可以说是客户端安全的首要问题,稍有不注意就会漏出相关接口被利用。
+
+一个XSS攻击的例子,如下:
+
+- 一个Java应用提供了一个接口可以上传个人动态,动态内容是富文本的。
+- 攻击者上传的内容如下:
+
+ `
`
+
+- 在服务端和客户端程序未做任何过滤的情况下,其他用户访问这个动态的页面时,就会执行这个脚本。
+
+如果脚本不是一个alert,而是换成跳转到一个具有删除操作的URL或者脚本获取用户的Cookie然后发送到远程服务器上,可想而知危害有多大。
+
+防范此种攻击的常用方式有以下几种:
+
+- 对任何允许用户输入的地方做检查,防止其提交脚本相关特殊字符串,如script、onload、onerror等。客户端和服务端都要做检查。
+- 做输入过滤,即将特殊字符都过滤掉或者换成HTML转义后的字符。Java中可以使用Apache commons-lang中的StringEscapeUtils的escape前缀的方法来做转义。
+- 给Cookie属性设置上HttpOnly,可以防止脚本获取到Cookie。
+- 对输出内容做过滤。这个可在客户端做,也可在服务端做。服务端主要就是转义HTML字符,客户端可以使用escape方法来过滤。
+
+## 9.3.2 跨站点请求伪造
+
+跨站点请求伪造,全称Cross Site Request Forgery,简称CSRF。也是一种常见的攻击方式。
+
+此种攻击方式,主要是通过诱导用户点击某些链接,从而隐含地发起对其他站点的请求,进而进行数据操作。
+
+一个攻击示例如下:
+
+- 一个用户登录了一个站点,访问http://xx/delete_notes?id=xx即可删除一个笔记。
+- 攻击者在它的站点中构造一个页面,HTML页面含有以下内容:
+
+ `
`
+
+- 当用户被诱导访问攻击者的站点时就发起了一个删除笔记的请求。
+
+对于CSRF攻击的常用解决方案有以下几种:
+
+- 对重要请求要求验证码输入,这样就能防止在用户不知情的情况下,被发送请求。
+- 使用类似防盗链的机制,对header的refer进行检验以确认请求来自合法的源。
+- 对重要请求都附带一个服务端生成的随机token, 提交时对此token进行验证。这也是业界一个很普遍的做法。
+
+## 9.3.3 SQL注入
+
+SQL注入攻击是一个很常见的攻击方式,原理是通过发送特殊的参数,拼接服务端的SQL字符串,从而达到改变SQL功能的目的。
+
+一个攻击例子如下:
+
+- 服务端登录验证使用下面的方式,其中userName和userPwd都是用户直接上传的参数
+
+ ```
+ String sql = "select * from user where user_name = '" + userName + "' and pwd = " + userPwd;
+ ```
+- 用户提交userName为admin'--,userPwd随便字符串xxx
+- 拼接好之后的SQL语句变成了:`select * from user where user_name = 'admmin'--' and pwd = 'xxx'`(--为SQL语句的注释), 这样只要存在user_name为admin的用户,此语句就能成功执行并返回admin用户的信息。
+
+这里需要说明的是,如果服务器的请求错误信息没有做进一步封装,直接把原始的数据库错误返回,那么有经验的攻击者通过返回结果多次尝试就会有机会找出SQL注入的机会。
+
+防范此种攻击的方案有以下几个:
+
+- 在Java中构造SQL查询语句时,杜绝拼接用户参数,尤其是拼接SQL查询的where条件。全部使用PreparedStatement预编译语句, 通过?来传递参数。
+- 在业务层面,过滤、转义SQL特殊字符,Apache commons-lang中的StringEscapeUtil提供了escapeSQL的功能(最新的lang3已经删除此方法,因为其只是简单的替换'为'')。
+
+## 9.3.4 基于约束条件的SQL攻击
+
+基于约束条件的SQL攻击基于的原理如下:
+
+- 在处理SQL中的字符串时,字符串末尾的空格字符都会被删除,包括WHERE子句和INSERT语句,但LIKE子句除外。
+- 在任意INSERT查询中,SQL会根据varchar(n)来限制字符串的最大长度,即超过n的字符串只保留前n个字符。
+
+如此,我们设计一个用户表(暂且忽略设计的合理性),对其中的用户名和密码字段都设置为25个字符限制:
+
+```
+CREATE TABLE test_user (
+ `user_name` varchar(25),
+ `pwd` varchar(25)
+);
+```
+有一个user_name为`user_test`的用户注册,于是向数据库添加一条记录。
+
+```
+insert into test_user values("user_test","111111");
+```
+
+接着,一个user_name为'user_test 1'(中间留有25个空格)的用户再来注册。一般的业务逻辑如下:
+
+- 判断用户名是否存在
+
+ ```
+ select * from test_user where user_name = 'user_test 1'
+ ```
+ 因为查询语句不会截断字符串,因此这样获取不到记录,表示用户不存在。
+
+- 用户名不存在,那么插入新用户。
+
+ ```
+ insert into test_user values("user_test 1","123456")
+ ```
+
+这样,由于`user_name`约束为25个字符,那么新用户的`user_name`成为了'user_test '(后面是16个空格字符)。现在数据库记录如下(第二个记录后面是16个空格):
+
+user_name | pwd
+----|-----
+user_test | 111111
+user_test | 123456
+
+这样,当使用`user_name='user_test'`和`pwd='123456'`登录时,能匹配到第二条记录,登录是成功的。但是用户信息使用的第一条的记录,于是攻击者就获取到了第一个用户的操作权限。
+
+防范此种攻击的措施如下:
+
+- 为具有唯一性的那些列添加UNIQUE索引。
+- 在数据库操作前先将输入参数修剪为特定长度。
+
+## 9.3.5 DDOS攻击
+
+DDOS,全称Distributed Denial of Service, 分布式拒绝服务攻击。攻击者利用很多台机器同时向某个服务发送大量请求,人为构造并发压力,从而使得服务被冲垮,无法为正常用户提供服务。常见的DDOS攻击包括:
+
+- SYN flood
+- UDP flood
+- ICMP flood
+
+其中SYN flood是最为经典的DDOS攻击。其利用了TCP连接三次握手时需要先发送SYN的机制,通过发送大量SYN包使得服务端建立大量半连接,消耗非常多的CPU和内存。针对这种攻击,很多解决方案就是在TCP层就使用相关算法识别异常流量,直接拒绝建立连接。但是,如果攻击者控制很多机器对一个资源消耗比较大的服务接口发起正常访问请求,那么这个方式就无效了。
+
+由于难于区分是否是正常用户的请求,因此DDOS是非常难以防范的,但仍有一些措施能够尽量地减少DDOS带来的影响,如下:
+
+- 合理使用缓存、异步等措施提高应用性能。应用抗并发的能力越强,就越不容易被DDOS冲垮服务。
+- 合理使用云计算相关组件,自动识别高峰流量并做自动扩容。
+- 在应用中限制来自某一IP或者某一设备ID的请求频率。超过此频率就将其放入黑名单,下次请求直接拒绝服务。Java中可以通过Redis的incr和expire操作来达到。如下:
+
+ ```
+ String ip = NetworkUtil.getClientIP(request, false); //获取客户端ip地址
+ String key = "ddos." + ip;
+ long count = suishenRedisTemplate.incr(key); //incr不会影响expire
+ if (count > 10000) {
+ throw new AccessException("access too frequently with ip: "
+ + StringUtils.defaultString(ip));
+ } else {
+ if (count == 1) {
+ suishenRedisTemplate.expire(key, 10);
+ }
+ return true;
+ }
+ ```
+
+ 上述代码即可将同一IP的请求限制在十秒钟10000次。
+
+ 此逻辑越靠近访问链路的前面效果越好,比如直接在Nginx中拦截效果就要比在业务应用中做要好。
+
+还需要提到的是DDOS一个新的变种,反射型DDOS攻击,也被称为放大攻击。原理如下图所示:
+
+
+
+此种攻击,攻击者并不直接攻击目标服务IP,而是伪造被攻击者的IP,发送请求包到网上一些开放的特殊服务的服务器(放大器。这些服务器由于协议的特点并不会验证源IP的真伪,于是会将数倍于请求报文的回复数据发送到被攻击者的IP,从而对后者间接形成DDOS攻击。任何设计不完善的、基于UDP请求的协议或者ICMP协议都能形成放大器,包括DNS请求、Ping请求、NTP monlist请求、SSDP协议(简单服务发现协议)等。此种攻击不需要大量的肉鸡、难以追踪,正变得越来越流行。防范此种攻击通常的手段就是进行DDOS流量清洗和增加ACL过滤规则。
+
+## 9.3.6 Session fixation
+
+Session fixation攻击,故名思议就是会话固定攻击。在我们平时的Web开发中都是基于Session做用户会话管理的。在浏览器中,Session的ID一般是存储在Cookie中的,甚至直接附带在query参数中。如果Session在未登录变为登录的情况下不发生改变的话,Session fixation攻击就形成了。
+
+一个攻击示例如下:
+
+- 攻击者进入网站http://xx.com。
+- 攻击者发送http://xx.com?JSESSIONID=123456给一个用户。
+- 用户点击此链接进入网站,由于URL后面有JSESSIONID,因此直接使用此做为Session的ID。
+- 用户成功登陆后,攻击者就可以利用伪造的Session ID获取用户的各种操作权限。
+
+此种攻击的关键点就在于Tomcat使用JSESSIONID做为Session ID。因此,防范此种攻击的核心之一就在于不能使用客户端传来的Session ID。此外还有以下方法:
+
+- 不要接受由GET或者POST参数指定的Session ID值。
+- 针对每一个请求都生成新的Session。
+- 只接受服务端生成的Session ID。
+- 为Session指定过期时间。
+
+Java Web项目中,可以实现一个拦截器, 将使用query参数传递JSESSIONID的请求的Session删除掉:
+
+```
+public void doFilter(ServletRequest request, ServletResponse response,
+ FilterChain chain) throws IOException, ServletException
+ ...
+
+ if (httpRequest.isRequestedSessionIdFromURL()) {
+ HttpSession session = httpRequest.getSession();
+ if (session != null) {
+ session.invalidate();
+ }
+ }
+ ...
+}
+```
+
+此外,对于每一次登录后的Session都重新生成ID, 并设置合理的失效期。
+
+```
+public JSONResult login(@RequestBody LoginRequestBody requestBody,
+ HttpServletRequest request)
+ ...
+ boolean loginResult = doLogin();
+ if(loginResult){
+ request.changeSessionId(); //重新生成Session ID
+ request.getSession().setMaxInactiveInterval(1800); //30分钟失效
+ }
+ ...
+}
+```
+
+## 9.3.7 隐私数据存储
+
+随着市面上发生一次次数据库被脱导致用户隐私数据被泄漏的事情,越来越多的人意识到了隐私的重要性,在选择一个应用的时候也越来越在意对自己隐私数据的保护。这里所说的隐私数据包括:手机号、实名、身份证号、家庭住址、单位地址、家庭情况、密码等等。那么在技术层面如何存储这些隐私数据来保障用户的隐私安全呢?
+
+1. 使用单向散列算法
+
+ 此种方式对明文进行加密后是无法恢复明文的,因此仅仅适用于密码这种不需要恢复明文只需要做验证的场景。
+
+1. 使用加密算法
+
+ 此种方式,在存储和使用用户数据的时候都进行加/解密运算,能够在一定程度上保护数据的安全性。但每次都要进行加解密使得代价有点高,而如果使用简单的算法则无法面对穷举或者字典攻击。并且加密的数据对于SQL等数据库查询语句优化是不友好的,操作都得通过程序进行。此外,算法所使用的密钥的安全也是一个问题,存储在哪里都有被拿到的机会。而如果进一步对于每个用户或者每条数据都使用不同的密钥,那么就会提高程序的逻辑复杂性。
+
+ 还得考虑到日志采集、数据分析等非具体业务场景,这些隐私数据最终还是要变为明文进行流通,无法从根本上保证隐私数据的安全。
+
+综上分析,可以采取以下这种方案:
+
+1. 每一个用户都有自己的密钥,对其手机号、身份证等隐私信息使用加密算法来混淆其中的几位。如:159efadsc2681。如此,在只是需要展示这些信息的地方无须解密,直接使用即可。只有诸如发送短信、用户信用验证时才需要解密。
+
+1. 密钥存储在另一个库中,由另外一个团队维护、独立管理,具有最高级别的访问权限,访问QPS也受严格控制。
+
+1. 如果给数据分析部门提供数据,则提供隐私数据转换后的数据。例如:对用户的归属地分析,那么可以提供身份证转化为地区归属地后的信息而不是直接提供身份证号。
+
+如此,即使脱库也无法解密所有数据。而且密钥库和业务库独立,单独脱一个库是没有意义的。密钥库的访问权限和访问频率也都受限制,即使是内部人员脱库都很容易被发现。
+
+总之,对诸如身份证号、通讯录、支付宝账号等隐私信息要注意加密或者散列存储,一定不要明文发送到客户端,展示也不要明文展示,只有当真正使用的时候再去获取明文。
+
+
+
diff --git a/img/alipay.png b/img/alipay.png
new file mode 100644
index 0000000..819e260
Binary files /dev/null and b/img/alipay.png differ
diff --git a/img/wechatpay.png b/img/wechatpay.png
new file mode 100644
index 0000000..b3188f9
Binary files /dev/null and b/img/wechatpay.png differ