
微服务架构现在是谈到企业应用架构时必聊的话题,微服务之所以火热也是因为相对之前的应用开发方式有很多优点,如更灵活、更能适应现在需求快速变更的大环境。 但说起微服务,不少人还是有这样的困惑:“作为一个开发,微服务架构是不是和我关系不大?那不都是架构师的事吗?” 关于这个问题,我来谈谈自己的看法。 微服务是当下最火热的后端架构之一。不管你是一个什么级别的程序员,也不论你在一个什么体量的公司,服务化都是你迟早会遇到的难题。实践微服务的过程本身也是一个升级打怪的过程,这中间你会遇到基本上所有后端架构的问题。解决了这些问题,你自然也就理解了那些高深的概念,也就成为了一名架构师,成长和能力提升都是这个过程的附属品。 并且,你了解微服务架构之后,能知道领导为什么让你这么做,也更容易站在系统角度思考公司技术的进程,这对于你的大局观构建来说非常有帮助。 本文将介绍微服务架构的演进、优缺点和微服务应用的设计原则,然后着重介绍作为一个“微服务应用平台”需要提供哪些能力、解决哪些问题才能更好的支撑企业应用架构。 微服务平台也是我目前正在参与的,还在研发过程中的平台产品,平台是以SpringCloud为基础,结合了普元多年来对企业应用的理解和产品的设计经验,逐步孵化的一个微服务应用平台。 image 目录: 一、微服务架构演进过程 二、微服务架构的好处 三、微服务该掌握哪些技术 四、总结展望 一、微服务架构演进过程 image 近年来我们大家都体会到了互联网、移动互联带来的好处,作为IT从业者,在生活中时刻感受互联网好处的同时,在工作中可能感受的却是来自自互联网的一些压力,那就是我们传统企业的IT建设也是迫切需要转型,需要面向外部客户,我们也需要应对外部环境的快速变化、需要快速创新,那么我们的IT架构也需要向互联网企业学习作出相应的改进,来支撑企业的数字化转型。 我们再看一下应用架构的演进过程,回忆一下微服务架构是如何一步一步进化产生的,最早是应用是单块架构,后来为了具备一定的扩展和可靠性,就有了垂直架构,也就是加了个负载均衡,接下来是前几年比较火的SOA,主要讲了应用系统之间如何集成和互通,而到现在的微服务架构则是进一步在探讨一个应用系统该如何设计才能够更好的开发、管理更加灵活高效。 微服务架构的基本思想就是“围绕业务领域组件来创建应用,让应用可以独立的开发、管理和加速”。 二、微服务架构的好处 image 我们总结了四个方面的优点,分别如下: 是每个微服务组件都是简单灵活的,能够独立部署。不再像以前一样,应用需要一个庞大的应用服务器来支撑。 可以由一个小团队负责更专注专业,相应的也就更高效可靠。 微服务之间是松耦合的,微服务内部是高内聚的,每个微服务很容易按需扩展。 微服务架构与语言工具无关,自由选择合适的语言和工具,高效的完成业务目标即可。 看到这里,大家会觉得微服务架构挺不错,然而还会有一些疑问,什么样的应用算是一个微服务架构的应用?该怎样设计一个微服务架构的应用?那我们来一起看看我们推荐的微服务应用的设计原则。 三、微服务该掌握哪些技术? 这是我整理的一些微服务需要掌握的知识技术点,分享给大家: 需要思维导图格式的可以加群:810589193免费获取。 1.1. Dubbo应用及源码解读 Dubbo简介及初入门 Dubbo管理中心及监控系统安装部署 领域驱动之如何正确划分Dubbo分布式服务 通讯协议TCP、UDP、HTTP分析 Dubbo负载均衡机制探析 如何完成Dubbo服务只订阅及只注册模式 架构师必备技术之如何设计Dubbo服务接口 Dubbo设计原理及源码分析 Dubbo容错机制及高扩展性分析 Dubbo服务与Docker虚拟化技术集成实战 1.2. SpringBoot SpringBoot与微服务的区别与联系 快速构建SpringBoot工程 SpringBoot核心组件start、actuator等剖析 快速集成Mybatis实战 快速集成Dubbo及案例实战 快速集成redis及案例实战 构建Swagger插件实现API管理及接口测试体系 1.3. SpringCloud应用及源码解读 Zuul路由网关详解及源码探析 Ribbon客户端负载均衡原理与算法详解,与服务端负载均衡区别 Feign声明式服务调用方式实现 Hystrix服务熔断及服务降级实战 Eureka注册中心构件及源码分析 Config配置服务中心与svn、git快速集成 Sleuth调用链路跟踪 BUS消息总线技术 Stream 消息驱动的微服务 1.4. Docker虚拟化技术 Docker介绍、安装与使用 Docker组成 Docker Compose部署脚本 Docker Service 服务编排 Docker Redis分布式集群部署 Docker File构建 通过Maven插件打包Docker镜像 Docker部署及运行应用程序 Kubernetes编配 基于Docker构建Mysql集群实战 高可用SpringCloud微服务与Docker集成实现动态扩容实战 针对以上的技术点,有十余年Java经验系统架构师录制了一些视频,用来回答这些技术。 现在加群:810589193可以免费获取这些视频以及Java工程化、高性能及分布式、高性能、高架构、性能调优、Spring、MyBatis、Netty源码分析等多个知识点高级进阶干货的相关视频资料,还有spring和虚拟机等书籍扫描版,还有更多面试题等你来拿 分享给喜欢Java,喜欢编程,有梦想成为架构师的程序员们,希望能够帮助到你们。 四、总结展望 我们再来回顾一下,三大基础环境的关系。微服务应用平台负责应用开发、运行以及管理;DevOps负责项目管理、计划管理、CI、CD和团队沟通协作等;容器云平台则负责基础设置管理,屏蔽环境的复杂度。 这三大基础环境的建设情况,直接反应出了企业IT能力水平。这三大基础环境是技术人员和企业都希望拥有的,是企业赢得竞争、驱动业务创新的基础,是企业加速数字化转型的必由之路。 最后,我们一起看一下普元正在研发的新一代The Platform平台。 image 上图红框中的内容是与我们今天分享的微服务应用平台相关的部分。整个The Platform平台是我们站在企业整体架构规划的角度,从多个维度入手,目标是为企业搭建一个持续发展的IT生态环境,加速企业的数字化型。
为什么很多公司不招大龄码农,这个问题最近很沸腾,初看这个问题时扎心了,终有一天,我们都会成为大龄码农,那么首先定义下大龄码农,一般认为是35岁以上。按现在推算是1983年(含1983)前出身的。有很多还在一线开发,有些已经是中高层管理人员了。我接触到的这类朋友较多,有同事,有朋友,也有一些读者。并不是他们现在没有工作,而是他们在跳槽去下家时,是个较长的过程,不想一些3-5年的小伙子,放到市面上,大把公司招人,坑位也多,大龄码农,得有合适匹配的岗位才行。大龄码农身价一般较高,不是一般公司给报酬给予的起,这类公司要么是BAT,要么是独角兽公司。有持续业务收入和市场中的江湖地位。大龄码农分两类,一类是专家能力,如音视频专家、编解码专家,另一类是管理能力,如技术总监、高级经理等。公司对于两者要求也不一样。前者可能是希望参与解决重大问题,疑难杂症等。后者涉及搭建团队,打造团队,培养输出技术人才。 image.png 这是个现实问题,相比年轻人来说,坑位少,和更高的要求。如果你的工作年限很长,无论是在管理或是专家能力上,和你的年限不成匹配,就会面对这个问题,不好找工作。并不是找不到工作,你期待的待遇和自身能力的一旦不匹配,就会造成失衡状态。我想着这就是大家常说的中年男人的压力吧。 永远都有新生的、更好用的劳动力。如韭菜一样一茬茬的被割。(甚至可以说1%)的顶层从业者实力强大到可以承包90%的技术性工作,剩下的90~99%从业者基本都是从事业务性工作; 渐渐的,行业前1%的人会把90%的基础工作代码封装完分享出去,只有前10%的人在高标准大公司有竞争力,剩下的90%码农的水平轻重都不太影响工程质量了,老板可以随便挑; image.png 我们很多时候常常会带着幸存者偏差,就像那些不读书的坏同学最后都去当老板去了,认为读书没有用,也总有大龄码农说,我就出去找工作随便找哇。这类就是幸存者偏差,人们总是喜欢把那些留下来的人,当成最终整体的结果。即便如此,只要我们提前做好准备,大龄码农一定要在技术路上或是管理路上走下去么?也不见得。说说我认识的大龄码农都去干什么去了? image.png 看了很多文章,程序员大龄的出路无非也就几条:转管理,创业,继续做程序,我的观点是比较倾向于做管理方面的工作的。 1、依旧在一线做技术专家和技术管理。 其实30岁开始大多数程序员就已经慢慢开始不适合做程序员了,众所周知程序员一般加班较多,要求对新技术要敏感,自学能力要强,而人过30岁,琐事慢慢也就开始多了起来,结了婚生了子,家里老人年龄越来越大,需要人去照顾等等,这些都是会分散一个人的精力。 转管理的话,在现有资源基础上可能承担的风险,还有就是学习成本上应该会相对会少一些,尤其是做技术管理岗可能会更好一些,自己以往所积累的技术经验都可以继续派上用场。 创业也是条出路,而更多的时候是要考虑风险,因为年龄渐渐大了起来人所要承担的家庭责任也会越来越大,但是随着年龄增长,人的阅历和人脉也会慢慢增长,创业相对来说可能会容易一些,所以这条路也是要谨慎选择。 2、创业。带知名公司背景,创业有优势,容易聚拢一批优秀的人。 甚至有连带效应,原来的老部下出来了,直接来之前老领导的公司。这种非常多见。大家彼此非常熟悉,共事起来,沟通交流也会少很多障碍,效率也会提高,那些独家兽公司很多都有类似发展过程。 3、自由职业。 写书、做培训讲师,导师咨询等。这种也非常多见,就是做了很多年后,无论是在经验上、还是技术指导上都有丰富经历。可以自由做类似工作。现在整个大环境对付费都是很认可的,只要你对别人有价值,自然也会获得不错的回报。 4、转做投资,天使投资人。 这点之前没有注意到,但是最近遇到个,就是在腾讯做了12年,出来后在某资本公司,做创业投资合伙人,对有价值的商业项目,进行天使投资。 其实每一步都是有因有果,当然还有变化的机遇和机会,天时、地利、人和,塑造大龄码农更加成为中年不惑的标志。当风暴还没来临时,要备好足够的防御装置。 一个终生学习的人,一个与时俱进的人,是不会被生活“清理”,不会被时代淘汰。 我有这样的信心,希望你也有。 我整理的一些需要掌握的知识技术点,分享给大家,话不多说,提高你的编程技能,认真 + 严肃,走起! 我在这里分享 “6” 个专项来帮助你顺利提高你的编程技能。 一:架构师框架 学习Java技术体系,设计模式,流行的框架与组件,常见的设计模式,编码必备,Spring5,做应用必不可少的最新框架,MyBatis,玩数据库必不可少的组件...... 开源框架.png 二:工程化(团队协作) 工欲善其事必先利其器,选择好的工具,提升开发效率和团队协作效率,是必不可少的:Maven,项目管理,Jenkins,持续集成,Sonar,代码质量管理,Git,版本管理,敏捷开发... 团队协作.png 三:架构师必备技术栈(高性能架构) 高并发,高可用,海量数据,没有分布式的架构知识肯定是玩不转的,要了解分布式中的,分布式架构原理,分布式架构策略,分布式中间件,分布式架构实战等等内容 高性能架构.png 四:架构技术(微服务架构) 业务越来越复杂,服务分层,微服务架构是架构升级的必由之路。比如:微服务框架,Spring Cloud,Docker与虚拟化,微服务架构 微服务专题.png 五:性能优化 任何脱离细节的ppt架构师都是耍流氓,向上能运筹帷幄,向下能解决一线性能问题,比如:性能指标体系,JVM调优,Web调优,DB调优等等.... 架构师筑基.png 六:架构必备(商城实战) 从架构设计,到应用层调优,再深入了解底层原理,扎实的Java基本功才能让自己变为扫地神僧:内存模型,并发模式,线程模型,锁细节等等 B2C商城实战.png 最后给大家一个惊喜,针对以上技术图,我也录制了一些视频资料提供给大家~ 有高清架构脑图、Java架构资料、Java面试资料等... 现在加群:810589193可以获取Java工程化、高性能及分布式、高性能、高架构、性能调优、Spring、MyBatis、Netty源码分析等多个知识点高级进阶干货的相关视频资料,还有spring和虚拟机等书籍扫描版,还有更多面试题等你来拿 分享给喜欢Java,喜欢编程,有梦想成为架构师的程序员们,希望能够帮助到你们。
一.jvm内存布局 程序计数器:当前线程正在执行的字节码的行号指示器,线程私有,唯一一个没有规定任何内存溢出错误的情况的区域。 Java虚拟机栈:线程私有,描述Java方法执行的内存模型,每个方法运行时都会创建一个栈帧,存放局部变量表、操作数栈、动态链接、方法出口等信息,每个方法的运行到结束对应一个栈帧的入栈和出栈。会有StackOverFlowError异常(申请的栈深度大于虚拟机所允许深度)和OutOfMemoryError异常(线程无法申请到足够内存)。 本地方法栈:功能与Java虚拟机栈相同,不过是为Native方法服务。 java堆:线程共享,存放实例对象和数组对象,申请空间不足抛出OutOfMemoryError异常。 方法区:线程共享,存储已被虚拟机加载的类的类信息、常量、静态变量、编译后的代码;运行时常量池存放class文件中描述的符号引用和直接引用,具有动态性。方法空间不足时抛出OutOfMemoryError异常。 直接内存:JVM规范之外的,NIO类引入了一种基于通道和缓冲区的I/O方式,可使用Native函数库直接分配内存,通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了在Java堆和Native堆中来回复制数据。 二.垃圾回收算法与垃圾回收器 垃圾收集算法: 标记-清除算法:将所有需要回收的对象先进行标记,标记结束后对标记的对象进行回收,效率低,会造成大量碎片。 复制算法:将内存分为两块大小相等的空间,每次只用其中一块,若一块内存用完了,就将这块内存中活着的对象复制到另一快内存中,将已使用的进行清除。不会产生碎片,但是会浪费一定的内存空间。堆的年轻代使用此算法,因为年轻代对象多为生存周期比较短的对象。年轻代将空间分为一个Eden和两个survivor,每次只使用Eden加一个survivor,回收时,将Eden和survivor中存活的对象复制到另一个survivor上,最后清理Eden和survivor。当Eden与survivor存活对象大于另一个survivor空间大小则需要老年代来担保。 标记-整理算法:标记阶段与标记-清除算法相同,标记完成后将所有存活对象向一端移动,然后清除掉端边界外对象。 分代收集算法:根据对象存活周期分为将内存分为新生代与老年代,新生代采取复制算法,老年代采用标记清除或标记整理算法。 垃圾回收器: Serial收集器:单线程,垃圾回收时需要停下所有的线程工作。 ParNew收集器:Serial的多线程版本。 Parallel Scavenge收集器:年轻代,多线程并行收集。设计目标是实现一个可控的吞吐量(cpu运行代码时间/cpu消耗的总时间)。 Serial Old收集器:Serial老年代版本。 CMS:目标是获得最短回收停顿时间,基于标记清除算法,整个过程四个步骤:初始标记(标记GCRoot直接关联对象,速度很快)、并发标记(从GCRoot向下标记)、重新标记(并发标记过程中发生变化的对象)、并发清除(清除老年代垃圾)。初始标记和重新标记需要停顿所有用户线程。缺点:无法处理浮动垃圾、有空间碎片的产生、对CPU敏感。 G1收集器:唯一一个可同时用于老年代与新生代的收集器。采用标记整理算法,将堆分为不同大小星等的Region,G1追踪每个region的垃圾堆积的价值大小,然后有一个优先列表,优先回收价值最大的region,避免在整个堆中进行安全区域的垃圾收集,能建立可预测的停顿时间模型。整个过程四个步骤:初始标记、并发标记、最终标记(并发标记阶段发生变化的对象的变化记录写入线程remembered set log,同时与remembered set合并)、筛选回收(对每个region回收价值和成本拍寻,得到一个最好的回收方案并回收)。 三.垃圾回收对象时程序的逻辑是否可以继续执行 不同回收器不同:Serial、ParNew会暂停用户所有线程工作;CMS、G1会在某一阶段暂停用户线程。 内存分配策略 对象优先在Eden分配:若Eden无空间,Java虚拟机发起一次Minor GC。 大对象直接进入老年代:大对象指需要大量连续内存空间的对象(如长数组、长字符串) 长期存活的对象进入老年代:每个对象有一个对象年龄计数器,age=15晋升为老年代。age+1的两个情况:对象在Eden出生并经过一次Minor GC存活且被survivor容纳;在survivor区经历过一次minor GC。 四.空间分配担保 在Minor GC之前,先检查老年代最大可用连续空间是否大于新生代所有空间总和,成立则此次GC安全 不成立,查看是否允许担保失败设置为true,不允许则进行Full GC 允许,看老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,不成立则Full GC 成立,则进行Minor GC 五.Java中的引用 强引用:new这类引用,只要强引用在,对象永远不会被回收。 软引用:描述有用但非必需的对象,在内存溢出之前,会把这些对象列入回收范围内进行第二次垃圾回收。 弱引用:描述非必需对象,只存活到下一次垃圾回收前。 虚引用:不会对生存时间造成影响,不能通过虚引用获得对象实例,只是在被虚引用的对象被回收时受到一个系统通知。 六.简述minor gc和full gc Minor GC:从新生代回收内存,关键是Eden区内存不足,造成不足的原因是Java对象大部分是朝生夕死(java局部对象),而死掉的对象就需要在合适的时机被JVM回收 Major GC:从老年代回收内存,一般比Minor GC慢10倍以上。 Full GC:对整个堆来说的,出现Full GC通常伴随至少一次Minor GC,但非绝对。Full GC被触发的时候:老年代内存不足;持久代内存不足;统计得到的Minor GC晋升到老年代平均大小大于老年代空间。 七.java虚拟机new一个对象的创建过程 在常量池中查看是否有new的参数对应的类的符号引用,并检查这个符号引用对应的类是否被加载、解析、初始化 加载后,为新对象分配内存空间,对象多需要的内存大小在类被加载之后就被确定(堆内分配内存:指针碰撞、空闲列表)。 将分配的空间初始化为零值。 对对象头进行必要设置(实例是哪个类的实例、类的元信息数据、GC分代年龄等)。 执行方法,按照程序的值初始化。 八.java中的类加载机制 Java虚拟机中类加载过程:加载、验证、准备、解析、初始化。 加载:通过一个类的全限名来获取定义此类的二进制字节流;将这个字节流代表的静态存储结构转换为方法区的的动态存储结构;在内存中生成一个代表此类的java.lang.Class对象,作为方法区中这个类的访问入口。 验证:验证class文件中的字节流是否符合Java虚拟机规范,包括文件格式、元数据等。 准备:为类变量分配内存并设置类变量初始值,分配内存在方法区。 4.解析:将常量池中符号引用替换为直接引用的过程;符号引用与虚拟机实现的内存布局无关,是使用一组符号来描述所引用的目标。class文件中不会保存各个方法的最终布局信息,所以这些符号引用不经过转化是无法得到真正的内存入口地址;直接引用与虚拟机实现的内存布局有关,可以是直接指向目标的指针,偏移量或指向目标的句柄。此过程主要是静态链接,方法主要为静态方法和私有方法。 5.初始化:真正执行类中定义的Java代码。初始化执行类的方法,该方法由编译器自动收集类中所有类变量的赋值动作和静态语句块的语句合并产生,且保证子类的clinit调用之前会先执行父类的clinit方法,clinit可以不存在(如没有类变量和静态语句块)。 九.双亲委派模型 java中类加载器主要用于实现类的加载,Java中的类和类加载器一起唯一确定类在JVM中的一致性。 系统提供的类加载器:启动类加载器、扩展类加载器、应用程序类加载器。 启动类加载器:用C++实现,是JVM的一部分,其他加载器使用Java实现,独立于JVM。主要负责加载<JAVA_HOME>\lib目录下的类库或被-Xbootclasspath参数指定的路径中的类库,应用程序不能使用该类加载器。 扩展类加载器:负责加载<JAVA_HOME>/lib/ext目录下或者类系统变量java.ext.dirs指定路径下的类库,开发者课直接使用。 应用程序类加载器:主要负责加载classpath下的类库,若应用程序没有自定义类加载器,默认使用此加载器 双亲委派模型要求除了启动类加载器,其他类加载器都有自己的父类加载器,使用组合关系来实现复用父类加载器。过程:若一个类加载器收到类加载请求,会把此请求委派给父类加载器去完成,每层都是如此,因此所有的加载请求最后都会传到启动类加载器;只有当父类加载器反馈不能加载,才会把此请求交给子类完成。 好处:使得java类伴随他的类加载器有了优先级;保证Java程序运行的稳定性 十.简述分派 包括静态分派与动态分派 静态分派:发生在编译时期,所有依赖静态类型来定位方法执行版本的分派称为静态分派,典型应用为方法重载。 动态分派:在运行期根据实际类型确定方法执行版本的分派过程。典型应用为方法重写,实现是在方法去中建立方法表,若子类中没有重写父类方法,则子类虚方法表中该方法的入口地址与父类指向相同,否则子类方法表中地址会替换为指向子类重写的方法的入口地址。 十一.对象的内存布局 对象内存布局分为三部分:对象头、实例数据、对齐填充。 对象头包含两部分: 存储对象自身运行时数据:哈希码、分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等 对象指向它的类元数据指针–类型指针 实例数据:程序代码中所定义的各种类型的字段内容 对齐填充:不是必然存在,仅起到占位符作用(对象大小必须是8子节整数倍) 十二.虚拟机栈中的各个部分 局部变量表:存放方法参数和方法内部定义的局部变量,以变量槽Slot为基本单位,一个Slot可以存放32位以内的数据类型,可重用。 操作数栈:先入后出,32位数据类型所占栈容量为1,64为数据类型所占栈容量为2 动态链接:常量池中符号引用有一部分在每次运行期间转换为直接引用,这部分称为动态链接。(一部分在类加载阶段或第一次使用时转换为直接引用—静态解析) 方法返回地址:方法执行后退出的两种方式:正常完成出口(执行引擎遇到任意一个返回的字节码指令)和异常完成出口(在方法执行过程中遇到异常且此异常未被处理)。两种方式都需要返回到方法被调用的位置程序才能继续执行(正常退出时调用者的PC计数器的值可以作为返回地址且栈帧中很可能保存这个计数器值;异常退出返回地址要通过异常处理器表来确定,栈帧中一般不会保存)。 十三.Java内存模型的happen before原则 如果两个操作存在happens-before关系,那么前一个操作的结果就会对后面一个操作可见,是定义的两个操作之间的偏序关系,常见的规则: 程序顺序规则:一个线程中每个操作,happens-before于该线程中的任意后续操作 监视器锁规则:对一个锁的解锁,happens-before于随后这个锁的加锁 volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个域的读 传递性:若A happens-before B,B happens-before C,则A happens-before C start()规则:如果线程A执行ThreadB.start(),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。 join()规则:若线程A 执行ThreadB.join()并成功返回,则线程B的任意操作happens-before于线程A从ThreadB.jion()操作返回成功。 十四.java中方法区存放哪些东西?jvm如何控制方法区的大小以及内存溢出的原因和解决 方法区大小不是固定的,jvm可根据需要动态调整。方法区主要存放类信息、常量、静态变量、编译后的代码。 控制方法区大小:减少程序中class数量、尽量使用较少的静态变量 修改:-XX:MaxPerSize调大 StackOverflowError异常:线程的方法嵌套调用层次太多,随着Java栈中桢的增多,最终会由于该线程Java栈中所有栈帧总和大于-Xss设置的值而产生此异常。 十五.jvm OutMemory的种类 堆溢出:被缓存的实例对象,大的map,list引用大的对象等 栈溢出:栈帧太多 方法区溢出:加载很多类会有可能出现,GC不会在主程序运行期对此区域进行清理,可通过设置jvm启动参数解决:-XX:MaxPermSize=256m 十六.jvm如何判断对象是否失效?可达性分析是否可以解决循环引用 引用计数器算法:给对象添加一个引用计数器,当被引用时给计数器加1,引用失效减1,当为0时对象失效。实现简单,判定效率高,无法解决循环引用问题。 可达性分析算法:将一系列GC Root作为起始点,从这些节点开始向下搜索,所走过路径称为引用链,若一个对象无引用链,则判断是否执行finalize()方法,若finalize()被覆盖并且没被JVM调用过,则执行此方法,执行后若还无引用链,则对象失效。 可以作为GC Root的对象: 虚拟机栈中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 本地方法栈中Native方法引用的对象 除了了解以上的16到JVM面试题,我们还需要掌握JVM的相关技术点。 现在互联网公司面试的时候都会问到JVM,但是仅仅掌握JVM是不够的,我们需要掌握更多的基础知识,这是我整理的一些需要掌握的知识技术点,分享给大家: 需要思维导图格式的可以私信我“架构” 架构师筑基知识点 1.1. JVM性能调优 性能优化如何理解 JVM内存管理机制 JVM执行子系统 程序编译与代码优化 实战调优案例与解决方法 1.2. Java程序性能优化 优雅的创建对象 注意对象的通用方法 类的设计陷阱 泛型需要注意的问题 Java方法的那些坑 程序设计的通用规则 1.3. Tomcat Tomcat线程模型分析 Tomcat生产环境配置 Tomcat运行机制及框架 Tomcat针对并发优化 Tomcat针对内存优化 手写Tomcat实战 1.4. 并发编程进阶 线程基础 原子操作类和CAS Lock、Condition和显示锁 AbstractQueuedSynchronizer分析 并发工具类和并发容器 线程池和Executor框架 实现原理和Java内存模型 线程安全 并发项目实战 1.5. Mysql 探析BTree机制 执行计划深入分析 Mysql索引优化详解 慢查询分析与SQL优化 1.6. 高性能Netty框架 Netty简介 I/O 演进之路及NIO 入门 Netty 开发环境搭建安装 TCP 粘包/拆包问题的解决之道 分隔符和定长解码器的应用 Netty 多协议开发和应用 WebSocket 协议开发 Netty源码分析 1.7. Linux基础与进阶 Linux入门安装 Linux注意事项 Linux基础指令 Linux Jdk1.8环境安装及操作指令 Linux Tomcat安装与停启 Linux下Docker进阶讲解 Linux下Docker与Tomcat集成实战 针对以上的技术点,有十余年Java经验的我有自己的一些心得,也录制了一些视频,解析这些技术。一.jvm内存布局 程序计数器:当前线程正在执行的字节码的行号指示器,线程私有,唯一一个没有规定任何内存溢出错误的情况的区域。 Java虚拟机栈:线程私有,描述Java方法执行的内存模型,每个方法运行时都会创建一个栈帧,存放局部变量表、操作数栈、动态链接、方法出口等信息,每个方法的运行到结束对应一个栈帧的入栈和出栈。会有StackOverFlowError异常(申请的栈深度大于虚拟机所允许深度)和OutOfMemoryError异常(线程无法申请到足够内存)。 本地方法栈:功能与Java虚拟机栈相同,不过是为Native方法服务。 java堆:线程共享,存放实例对象和数组对象,申请空间不足抛出OutOfMemoryError异常。 方法区:线程共享,存储已被虚拟机加载的类的类信息、常量、静态变量、编译后的代码;运行时常量池存放class文件中描述的符号引用和直接引用,具有动态性。方法空间不足时抛出OutOfMemoryError异常。 直接内存:JVM规范之外的,NIO类引入了一种基于通道和缓冲区的I/O方式,可使用Native函数库直接分配内存,通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了在Java堆和Native堆中来回复制数据。 二.垃圾回收算法与垃圾回收器 垃圾收集算法: 标记-清除算法:将所有需要回收的对象先进行标记,标记结束后对标记的对象进行回收,效率低,会造成大量碎片。 复制算法:将内存分为两块大小相等的空间,每次只用其中一块,若一块内存用完了,就将这块内存中活着的对象复制到另一快内存中,将已使用的进行清除。不会产生碎片,但是会浪费一定的内存空间。堆的年轻代使用此算法,因为年轻代对象多为生存周期比较短的对象。年轻代将空间分为一个Eden和两个survivor,每次只使用Eden加一个survivor,回收时,将Eden和survivor中存活的对象复制到另一个survivor上,最后清理Eden和survivor。当Eden与survivor存活对象大于另一个survivor空间大小则需要老年代来担保。 标记-整理算法:标记阶段与标记-清除算法相同,标记完成后将所有存活对象向一端移动,然后清除掉端边界外对象。 分代收集算法:根据对象存活周期分为将内存分为新生代与老年代,新生代采取复制算法,老年代采用标记清除或标记整理算法。 垃圾回收器: Serial收集器:单线程,垃圾回收时需要停下所有的线程工作。 ParNew收集器:Serial的多线程版本。 Parallel Scavenge收集器:年轻代,多线程并行收集。设计目标是实现一个可控的吞吐量(cpu运行代码时间/cpu消耗的总时间)。 Serial Old收集器:Serial老年代版本。 CMS:目标是获得最短回收停顿时间,基于标记清除算法,整个过程四个步骤:初始标记(标记GCRoot直接关联对象,速度很快)、并发标记(从GCRoot向下标记)、重新标记(并发标记过程中发生变化的对象)、并发清除(清除老年代垃圾)。初始标记和重新标记需要停顿所有用户线程。缺点:无法处理浮动垃圾、有空间碎片的产生、对CPU敏感。 G1收集器:唯一一个可同时用于老年代与新生代的收集器。采用标记整理算法,将堆分为不同大小星等的Region,G1追踪每个region的垃圾堆积的价值大小,然后有一个优先列表,优先回收价值最大的region,避免在整个堆中进行安全区域的垃圾收集,能建立可预测的停顿时间模型。整个过程四个步骤:初始标记、并发标记、最终标记(并发标记阶段发生变化的对象的变化记录写入线程remembered set log,同时与remembered set合并)、筛选回收(对每个region回收价值和成本拍寻,得到一个最好的回收方案并回收)。 三.垃圾回收对象时程序的逻辑是否可以继续执行 不同回收器不同:Serial、ParNew会暂停用户所有线程工作;CMS、G1会在某一阶段暂停用户线程。 内存分配策略 对象优先在Eden分配:若Eden无空间,Java虚拟机发起一次Minor GC。 大对象直接进入老年代:大对象指需要大量连续内存空间的对象(如长数组、长字符串) 长期存活的对象进入老年代:每个对象有一个对象年龄计数器,age=15晋升为老年代。age+1的两个情况:对象在Eden出生并经过一次Minor GC存活且被survivor容纳;在survivor区经历过一次minor GC。 四.空间分配担保 在Minor GC之前,先检查老年代最大可用连续空间是否大于新生代所有空间总和,成立则此次GC安全 不成立,查看是否允许担保失败设置为true,不允许则进行Full GC 允许,看老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,不成立则Full GC 成立,则进行Minor GC 五.Java中的引用 强引用:new这类引用,只要强引用在,对象永远不会被回收。 软引用:描述有用但非必需的对象,在内存溢出之前,会把这些对象列入回收范围内进行第二次垃圾回收。 弱引用:描述非必需对象,只存活到下一次垃圾回收前。 虚引用:不会对生存时间造成影响,不能通过虚引用获得对象实例,只是在被虚引用的对象被回收时受到一个系统通知。 六.简述minor gc和full gc Minor GC:从新生代回收内存,关键是Eden区内存不足,造成不足的原因是Java对象大部分是朝生夕死(java局部对象),而死掉的对象就需要在合适的时机被JVM回收 Major GC:从老年代回收内存,一般比Minor GC慢10倍以上。 Full GC:对整个堆来说的,出现Full GC通常伴随至少一次Minor GC,但非绝对。Full GC被触发的时候:老年代内存不足;持久代内存不足;统计得到的Minor GC晋升到老年代平均大小大于老年代空间。 七.java虚拟机new一个对象的创建过程 在常量池中查看是否有new的参数对应的类的符号引用,并检查这个符号引用对应的类是否被加载、解析、初始化 加载后,为新对象分配内存空间,对象多需要的内存大小在类被加载之后就被确定(堆内分配内存:指针碰撞、空闲列表)。 将分配的空间初始化为零值。 对对象头进行必要设置(实例是哪个类的实例、类的元信息数据、GC分代年龄等)。 执行方法,按照程序的值初始化。 八.java中的类加载机制 Java虚拟机中类加载过程:加载、验证、准备、解析、初始化。 加载:通过一个类的全限名来获取定义此类的二进制字节流;将这个字节流代表的静态存储结构转换为方法区的的动态存储结构;在内存中生成一个代表此类的java.lang.Class对象,作为方法区中这个类的访问入口。 验证:验证class文件中的字节流是否符合Java虚拟机规范,包括文件格式、元数据等。 准备:为类变量分配内存并设置类变量初始值,分配内存在方法区。 4.解析:将常量池中符号引用替换为直接引用的过程;符号引用与虚拟机实现的内存布局无关,是使用一组符号来描述所引用的目标。class文件中不会保存各个方法的最终布局信息,所以这些符号引用不经过转化是无法得到真正的内存入口地址;直接引用与虚拟机实现的内存布局有关,可以是直接指向目标的指针,偏移量或指向目标的句柄。此过程主要是静态链接,方法主要为静态方法和私有方法。 5.初始化:真正执行类中定义的Java代码。初始化执行类的方法,该方法由编译器自动收集类中所有类变量的赋值动作和静态语句块的语句合并产生,且保证子类的clinit调用之前会先执行父类的clinit方法,clinit可以不存在(如没有类变量和静态语句块)。 九.双亲委派模型 java中类加载器主要用于实现类的加载,Java中的类和类加载器一起唯一确定类在JVM中的一致性。 系统提供的类加载器:启动类加载器、扩展类加载器、应用程序类加载器。 启动类加载器:用C++实现,是JVM的一部分,其他加载器使用Java实现,独立于JVM。主要负责加载<JAVA_HOME>\lib目录下的类库或被-Xbootclasspath参数指定的路径中的类库,应用程序不能使用该类加载器。 扩展类加载器:负责加载<JAVA_HOME>/lib/ext目录下或者类系统变量java.ext.dirs指定路径下的类库,开发者课直接使用。 应用程序类加载器:主要负责加载classpath下的类库,若应用程序没有自定义类加载器,默认使用此加载器 双亲委派模型要求除了启动类加载器,其他类加载器都有自己的父类加载器,使用组合关系来实现复用父类加载器。过程:若一个类加载器收到类加载请求,会把此请求委派给父类加载器去完成,每层都是如此,因此所有的加载请求最后都会传到启动类加载器;只有当父类加载器反馈不能加载,才会把此请求交给子类完成。 好处:使得java类伴随他的类加载器有了优先级;保证Java程序运行的稳定性 十.简述分派 包括静态分派与动态分派 静态分派:发生在编译时期,所有依赖静态类型来定位方法执行版本的分派称为静态分派,典型应用为方法重载。 动态分派:在运行期根据实际类型确定方法执行版本的分派过程。典型应用为方法重写,实现是在方法去中建立方法表,若子类中没有重写父类方法,则子类虚方法表中该方法的入口地址与父类指向相同,否则子类方法表中地址会替换为指向子类重写的方法的入口地址。 十一.对象的内存布局 对象内存布局分为三部分:对象头、实例数据、对齐填充。 对象头包含两部分: 存储对象自身运行时数据:哈希码、分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等 对象指向它的类元数据指针–类型指针 实例数据:程序代码中所定义的各种类型的字段内容 对齐填充:不是必然存在,仅起到占位符作用(对象大小必须是8子节整数倍) 十二.虚拟机栈中的各个部分 局部变量表:存放方法参数和方法内部定义的局部变量,以变量槽Slot为基本单位,一个Slot可以存放32位以内的数据类型,可重用。 操作数栈:先入后出,32位数据类型所占栈容量为1,64为数据类型所占栈容量为2 动态链接:常量池中符号引用有一部分在每次运行期间转换为直接引用,这部分称为动态链接。(一部分在类加载阶段或第一次使用时转换为直接引用—静态解析) 方法返回地址:方法执行后退出的两种方式:正常完成出口(执行引擎遇到任意一个返回的字节码指令)和异常完成出口(在方法执行过程中遇到异常且此异常未被处理)。两种方式都需要返回到方法被调用的位置程序才能继续执行(正常退出时调用者的PC计数器的值可以作为返回地址且栈帧中很可能保存这个计数器值;异常退出返回地址要通过异常处理器表来确定,栈帧中一般不会保存)。 十三.Java内存模型的happen before原则 如果两个操作存在happens-before关系,那么前一个操作的结果就会对后面一个操作可见,是定义的两个操作之间的偏序关系,常见的规则: 程序顺序规则:一个线程中每个操作,happens-before于该线程中的任意后续操作 监视器锁规则:对一个锁的解锁,happens-before于随后这个锁的加锁 volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个域的读 传递性:若A happens-before B,B happens-before C,则A happens-before C start()规则:如果线程A执行ThreadB.start(),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。 join()规则:若线程A 执行ThreadB.join()并成功返回,则线程B的任意操作happens-before于线程A从ThreadB.jion()操作返回成功。 十四.java中方法区存放哪些东西?jvm如何控制方法区的大小以及内存溢出的原因和解决 方法区大小不是固定的,jvm可根据需要动态调整。方法区主要存放类信息、常量、静态变量、编译后的代码。 控制方法区大小:减少程序中class数量、尽量使用较少的静态变量 修改:-XX:MaxPerSize调大 StackOverflowError异常:线程的方法嵌套调用层次太多,随着Java栈中桢的增多,最终会由于该线程Java栈中所有栈帧总和大于-Xss设置的值而产生此异常。 十五.jvm OutMemory的种类 堆溢出:被缓存的实例对象,大的map,list引用大的对象等 栈溢出:栈帧太多 方法区溢出:加载很多类会有可能出现,GC不会在主程序运行期对此区域进行清理,可通过设置jvm启动参数解决:-XX:MaxPermSize=256m 十六.jvm如何判断对象是否失效?可达性分析是否可以解决循环引用 引用计数器算法:给对象添加一个引用计数器,当被引用时给计数器加1,引用失效减1,当为0时对象失效。实现简单,判定效率高,无法解决循环引用问题。 可达性分析算法:将一系列GC Root作为起始点,从这些节点开始向下搜索,所走过路径称为引用链,若一个对象无引用链,则判断是否执行finalize()方法,若finalize()被覆盖并且没被JVM调用过,则执行此方法,执行后若还无引用链,则对象失效。 可以作为GC Root的对象: 虚拟机栈中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 本地方法栈中Native方法引用的对象 除了了解以上的16到JVM面试题,我们还需要掌握JVM的相关技术点。 现在互联网公司面试的时候都会问到JVM,但是仅仅掌握JVM是不够的,我们需要掌握更多的基础知识,这是我整理的一些需要掌握的知识技术点,分享给大家: 需要思维导图格式的可以私信我“架构” 架构师筑基知识点 1.1. JVM性能调优 性能优化如何理解 JVM内存管理机制 JVM执行子系统 程序编译与代码优化 实战调优案例与解决方法 1.2. Java程序性能优化 优雅的创建对象 注意对象的通用方法 类的设计陷阱 泛型需要注意的问题 Java方法的那些坑 程序设计的通用规则 1.3. Tomcat Tomcat线程模型分析 Tomcat生产环境配置 Tomcat运行机制及框架 Tomcat针对并发优化 Tomcat针对内存优化 手写Tomcat实战 1.4. 并发编程进阶 线程基础 原子操作类和CAS Lock、Condition和显示锁 AbstractQueuedSynchronizer分析 并发工具类和并发容器 线程池和Executor框架 实现原理和Java内存模型 线程安全 并发项目实战 1.5. Mysql 探析BTree机制 执行计划深入分析 Mysql索引优化详解 慢查询分析与SQL优化 1.6. 高性能Netty框架 Netty简介 I/O 演进之路及NIO 入门 Netty 开发环境搭建安装 TCP 粘包/拆包问题的解决之道 分隔符和定长解码器的应用 Netty 多协议开发和应用 WebSocket 协议开发 Netty源码分析 1.7. Linux基础与进阶 Linux入门安装 Linux注意事项 Linux基础指令 Linux Jdk1.8环境安装及操作指令 Linux Tomcat安装与停启 Linux下Docker进阶讲解 Linux下Docker与Tomcat集成实战 针对以上的技术点,有十余年Java经验的我有自己的一些心得,也录制了一些视频,解析这些技术。 分享给喜欢Java,喜欢编程,有梦想成为架构师的程序员们,希望能够帮助到你们。
目前流行的开发技术、常见的面试问题以及问题的答案都已经写的特别清楚了,今天我在之前的基础上,再基于面个人的经验继续精选一些面试题给大家阅读参考。 image 1,Java的反射 Java 反射机制是在运行状态中,对于任意一个类,都能够获得这个类的所有属性和方法,对于任意一个对象都能够调用它的任意一个属性和方法。这种在运行时动态的获取信息以及动态调用对象的方法的功能称为Java 的反射机制。反射也就是动态加载对象,并对对象进行剖析。Class 类与java.lang.reflect 类库一起对反射的概念进行了支持,该类库包含了Field,Method,Constructor类(每个类都实现了Member 接口)。 image 反射的作用: 1),在运行时判断任意一个对象所属的类 2),在运行时构造任意一个类的对象 3),在运行时判断任意一个类所具有的成员变量和方法 4),在运行时调用任意一个对象的方法 优点:可以动态的创建对象和编译,最大限度发挥了java的灵活性。 缺点:对性能有影响。使用反射基本上一种解释操作,告诉JVM我们要做什么并且满足我们的要求,这类操作总是慢于直接执行java代码。 如何使用java的反射? a. 通过一个全限类名创建一个对象 1) Class.forName("全限类名"); 2) 类名.class; 获取Class<?> clazz 对象 3) 对象.getClass(); b. 获取构造器对象,通过构造器new出一个对象 1) Clazz.getConstructor([String.class]); 2) Con.newInstance([参数]); c. 通过class对象创建一个实例对象(就相当与new类名()无参构造器) 1) Clazz.newInstance(); d. 通过class对象获得一个属性对象 1) Field c=clazz.getFields(); 获得某个类的所有的公共(public)的字段,包括父类中的字段。 2) Field c=clazz.getDeclaredFields(); 获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的申明字段。 e、通过class对象获得一个方法对象 1)Clazz.getMethod("方法名",class…..parameaType);(只能获取公共的) 2)Clazz.getDeclareMethod("方法名"); (获取任意修饰方法,不能执行私有) M.setAccessible(true);(让私有的方法可以执行) f. 让方法执行 Method.invoke(obj实例对象,obj可变参数); 2,Spring的源码分析及实现 Spring是一个开源的、轻量级的Java 开发框架。Spring使用的是基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅仅限于服务器端的开发。从简单性、可测试性和松耦合性角度而言,绝大部分Java应用都可以从Spring中受益。Spring的核心是控制反转(IOC)和面向切面(AOP)。 什么是IOC? 控制反转(IOC)就是把原先我们代码里面需要实现的对象创建、依赖的代码,反转给容器来帮忙实现。那么必然的我们需要创建一个容器,同时需要一种描述来让容器知道需要创建的对象与对象的关系。这个描述最具体表现就是我们可配置的文件。IOC容器最主要是完成了对象的创建和依赖的管理注入等。 Spring IOC体系结构: Spring Bean的创建是典型的工厂模式,这一系列的Bean工厂,也即IOC容器为开发者管理对象间的依赖关系提供了很多便利和基础服务,在Spring中有许多的IOC容器的实现供用户选择和使用,其相互关系如下: image BeanFactory作为最顶层的一个接口类,它定义了IOC容器的基本功能规范,BeanFactory 有三个子类:ListableBeanFactory、HierarchicalBeanFactory 和AutowireCapableBeanFactory。IOC容器接口BeanFactory: public interface BeanFactory { 在BeanFactory里只对IOC容器的基本行为作了定义,根本不关心你的bean是如何定义怎样加载的。正如我们只关心工厂里得到什么的产品对象,至于工厂是怎么生产这些对象的,这个基本的接口不关心。 而要知道工厂如何产生对象,就需要看具体的IOC容器实现,spring提供了许多IOC容器的实现。如XmlBeanFactory,ClasspathXmlApplicationContext等。其中XmlBeanFactory就是针对最基本的ioc容器的实现,这个IOC容器可以读取XML文件定义的BeanDefinition(XML文件中对bean的描述),如果说XmlBeanFactory是容器中的屌丝,ApplicationContext应该算容器中的高富帅了。 ApplicationContext是Spring提供的一个高级的IoC容器,它除了能够提供IoC容器的基本功能外,还为用户提供了以下的附加服务。从ApplicationContext接口的实现,我们看出其特点: 1, 支持信息源,可以实现国际化。(实现MessageSource接口); 2, 访问资源。(实现ResourcePatternResolver接口,这个后面要讲); 3, 支持应用事件。(实现ApplicationEventPublisher接口); Spring ****AOP AOP即面向切面编程。可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。AOP技术利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。 AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。 Spring对AOP的支持 Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。Spring创建代理的规则为: 1)、默认使用Java动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了; 2)、当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB; AOP编程其实是很简单的事情,纵观AOP编程,程序员只需要参与三个部分: 1)、定义普通业务组件 2)、定义切入点,一个切入点可能横切多个业务组件 3)、定义增强处理,增强处理就是AOP框架为普通业务组件织入的处理动作 所以进行AOP编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法=增强处理+被代理对象的方法。 下面给出一个Spring AOP的.xml文件模板,名字叫做aop.xml,之后的内容都在aop.xml上进行扩展: <?xml version="1.0" encoding="UTF-8"?> 3,****动态代理****(cglib 与 JDK) JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。 1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP; 2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP; 3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换; 如何强制使用CGLIB实现AOP? 1) 添加CGLIB库,SPRING_HOME/cglib/*.jar 2)在spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/> JDK动态代理和CGLIB字节码生成的区别? 1) JDK动态代理只能对实现了接口的类生成代理,而不能针对类; 2) CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法最好不要声明成final; 4,分布式锁的实现方案 分布式锁一般有三种实现方式: 1. 数据库锁; 2. 基于Redis的分布式锁; 3. 基于ZooKeeper的分布式锁。 基于数据库锁实现 方案1: CREATE TABLE `methodLock` ( 当我们想要锁住某个方法时,执行以下SQL: insert into methodLock(method_name,desc) values (`method_name`,`desc`); :想要释放锁的话,需要执行以下Sql: delete from methodLock where method_name ='method_name'; 方案2: CREATE TABLE `method_lock` ( 获取锁: select id, method_name, state,version from method_lock where state=1 and method_name='methodName'; 占有锁: update t_resoure set state=2, version=2, update_time=now() where method_name='methodName' and state=1 and version=2; 缺点: 1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。 2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。 3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。 4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。 解决方案: 1、数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。 2、没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。 3、非阻塞的?搞一个while循环,直到insert成功再返回成功。 4、非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。 基于redis的分布式锁 实现方案: try{ 缺点: 在这种场景(主从结构)中存在明显的竞态: 客户端A从master获取到锁, 在master将锁同步到slave之前,master宕掉了。 slave节点被晋级为master节点, 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效! 基于zookeeper实现 方案:可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。 image Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。 缺点: 性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。 三种方案的比较 上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。 从理解的难易程度角度(从低到高):数据库 > 缓存 > Zookeeper 从实现的复杂性角度(从低到高):Zookeeper >= 缓存 > 数据库 从性能角度(从高到低):缓存 > Zookeeper >= 数据库 从可靠性角度(从高到低):Zookeeper > 缓存 > 数据库 5,Java设计模式部分搞笑解读 1、工厂模式, 搞笑解读:—追MM少不了请吃饭了,麦当劳的鸡翅和肯德基的鸡翅都是MM爱吃的东西,虽然口味有所不同,但不管你带MM去麦当劳或肯德基,只管向服务员说“来四个鸡翅”就行了。麦当劳和肯德基就是生产鸡翅的Factory。 工厂模式:客户类和工厂类分开。消费者任何时候需要某种产品,只需向工厂请求即可。消费者无须修改就可以接纳新产品。缺点是当产品修改时,工厂类也要做相应的修改。如:如何创建及如何向客户端提供。 2,单例模式 搞笑解读:俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公Sigleton,她们只要说道“老公”,都是指的同一个人,那就是我(白日美梦)。 单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例单例模式。单例模式只应在有真正的“单一实例”的需求时才可使用。 3,适配器模式 搞笑解读:在一次聚会碰到了一个很漂亮的乌克兰MM,可不会说乌克兰语,她也不会说普通话,只好求助于会乌克兰语的朋友,他作为我们之间的Adapter,让我们可以相互交谈了(也不知道他会不会耍我)。适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端。 4,代理模式 搞笑解读:跟MM在网上聊天,一开头总是“hi,你好”,“你从哪儿来呀?”“你多大了?”“身高多少呀?”这些话,真烦人,写个程序做为我的Proxy吧,凡是接收到这些话都设置好了自动的回答,接收到其他的话时再通知我回答,酷吧。 代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个人或一个机构代表另一个人或者一个机构采取行动。某些 情况下,客户不想或者不能够直接引用一个对象,代理对象可以在客户和目标对象直接起到中介的作用。客户端分辨不出代理主题对象与真实主题对象。代理模式可 以并不知道真正的被代理对象,而仅仅持有一个被代理对象的接口,这时候代理对象不能够创建被代理对象,被代理对象必须有系统的其他角色代为创建并传入。 免费获取Java工程化、高性能及分布式、高性能、高架构、性能调优、Spring、MyBatis、Netty源码分析等多个知识点高级进阶干货的直播免费学习权限及相关视频资料,还有spring和虚拟机等书籍扫描版
既然励志在java路上走的更远,那就必须了解java的路径。先看图 image.png更加细化的细节如下 一: 编程基础不管是C还是C++,不管是Java还是PHP,想成为一名合格的程序员,基本的数据结构和算法基础还是要有的。下面几篇文章从思想到实现,为你梳理出常用的数据结构和经典算法。 1-1 常用数据结构 数组、链表、堆、栈、队列、Hash表、二叉树等 1-2 算法思想 算法时间复杂度和空间复杂度的分析计算 算法思想:递推、递归、穷举、贪心、分治、动态规划、迭代、分枝界限 1-3 经典算法 经典排序:插入排序、冒泡排序、快排(分划交换排序)、直接选择排序、堆排序、合并排序 经典查找:顺序查找、二分查找、二叉排序树查找 1-4 高级数据结构 B+/B-数、红黑树、图等 1-5 高级算法 图的深度优先搜索、图的广度优先搜索、拓扑排序、Dijkstra算法(单源最短路径)、霍夫曼编码、辗转相除法、最小生成树等 二:Java语言基础诞生不过二十余年的Java语言凭借其跨平台、面向对象、适合于分布式计算的特性,广泛应用于Web网站、移动设备、桌面应用中,并且已经连续多年稳居TOBIE编程语言排行榜前列,最近更是登上冠军宝座。Java有哪些优秀而又与众不同的地方首先一定要清楚。 2-1 基础语法 Java语法格式,常量和变量,变量的作用域,方法和方法的重载,运算符,程序流程控制,各种基本数据类型及包装类 2-2 重要:集合类 Collection以及各种List、Set、Queue、Map的实现以及集成关系,实现原理 Collections和Arrays 2-3 其他JavaAPI String和StringBuffer,System和Runtime类,Date和DateFomat类 java.lang包 java.util包(集合类体系、规则表达式、zip,以及时间、随机数、属性、资源和Timer等) java.math包 java.net包 java.text包(各种格式化类等) java.security包 2-4 面向对象、面向接口 对象的三大特性:封装、继承和多态,优缺点 如何设计类,类的设计原则 this关键字,final关键字,static关键字 对象的实例化过程 方法的重写和重载;方法和方法的参数传递过程 构造函数 内部类,抽象类,接口 对象的多态性(子类和父类之间的转换、父类纸箱子类的引用),抽象类和接口在多态中的应用 2-5 JVM内存模型、垃圾回收 2-6 关于异常 Throwable/Error/Exception,Checked Exception vs. Unchecked Exception,异常的捕捉和抛出,异常捕捉的原则,finally的使用 2-7 多线程 线程和进程的概念 如何在程序中创建多线程,线程安全问题,线程之间的通讯 线程的同步 死锁问题的剖析 线程池 2-8 IO java.io包,理解IO体系的基于管道模型的设计思路以及常用IO类的特性和使用场合。 File及相关类,字节流InputStream和OutputStream,字符流Reader和Writer,以及相应缓冲流和管道流,字节和字符的转化流,包装流,以及常用包装类使用 分析IO性能 2-9XML 熟悉SAX、DOM以及JDOM的优缺点并且能够使用其中的一种完成XML的解析及内容处理;这几种解析方法的原理 2-10 一些高级特性 反射、代理、泛型、枚举、Java正则表达式 2-11 网络编程 网络通信协议原理及适用场景,Socket编程,WEB服务器的工作原理 2-11 JDK1.5、JDK1.6、JDK1.7、JDK1.8每个版本都比前面一个版本添加了哪些新特性,进行了哪些提升 三:数据库相关前面说到了数据结构,数据库简单来说就像是电子化的档案柜,是按照一定的数据结构来组织、存储和管理数据的仓库。 3-1理论基础 数据库设计原则和范式 事务(ACID、工作原理、事务的隔离级别、锁、事务的传播机制) 3-2 各种数据库优缺点、使用场景分析 MySQL/SQLServer/Oracle以及各种NoSQL(Redis、MongoDB、Memcached、HBase、CouchDB等) 3-2 SQL语句 数据库创建,权限分配,表的创建,增删改查,连接,子查询 触发器、存储过程、事务控制 3-3 优化 索引原理及适用,大表查询优化,多表连接查询优化,子查询优化等 3-4 分库、分表、备份、迁移 导入、导出,分库、分表,冷备热备,主从备份、双机热备、纵向扩展、横向扩展 3-5 JDBC JDBC Connection、Statement、PreparedStatement、CallableStatement、ResultSet等不同类的使用 连接池(配置使用、实现原理) ORM,DAO 四:JavaWeb核心技术(包括部分前端)Html5/Css/JS原生/jQuery Ajax(跨域等) JSP/JavaBean/Servlet/EL/JSTL/TabLib JSF JSON EJB 序列化和反序列化 规则引擎 搜索引擎 模板引擎 缓存 身份认证 测试 集群 持久化 生成静态页技术 高性能 安全 事务JTA 其他需要了解的,如:管理JMX、安全JCCA/JAAS、集成JCA、通信JNDI/JMS/JavaMain/JAF、SSI技术 五、主流框架及工具Struts1/Struts2 Spring(IoC、AOP等),SpringMVC 持久化:Hibernate/MyBatis 日志:Log4j 单元测试:JUnit 消息队列:ActiveMQ、RabbitMQ等 负载均衡:Nginx/HaProxy Web服务器:Tomcat、JBoss、Jetty、Resin、WebLogic、WebSphere等 通信:WebService(cxf的soap、restful协议) 缓存:Redis、Memcached 工作流:Activity、JBPM 搜索引擎:lucene,基于lucene封装的solr 模板引擎:Velocity、FreeMaker 大数据:Hadoop(HDFS和MapReduce) 构建工具:Ant/Maven 六、JavaWeb系统设计与架构Java设计模式 JAVA与UML建模 面向服务架构:SOA/SCA/ESB/OSGI/EAI,微服务 面向资源架构:ROA/REST 面向云架构:COA/Saas/云计算 大型网站负载均衡、系统调优等 七、More排错能力: 应该可以根据异常信息比较快速的定位问题的原因和大致位置 优化能力 代码规范、代码管理: 有自己的代码规范体系,代码可读性好 知识面广: 懂各种网络产品及特性,懂各种中间件,能够知道坑在哪儿,深谙各种技术方案的优缺点,懂整合各种资源并达到最优....了解各种技术及应用场景,有足够的工作经验解决集成中遇到的各种奇葩问题 技术管理/技术总监: 产品管理、项目管理、团队建设、团队提升 CTO: 发展战略 总结:目前的我和我的目标还有很大的差距,希望每当过一段时间就来看看这些东西,看看自己是否又前进了一步。java大神任而道远,路在脚下,加油。
之前实习的时候就想着写一篇面经,后来忙就给忘了,现在找完工作了,也是该静下心总结一下走过的路程了,我全盘托出,奉上这篇诚意之作,希望能给未来找工作的人一点指引和总结, 也希望能使大家少走点弯路 , 如果能耐心读完,相信对你会找到你需要的东西。一、心态 心态很重要! 心态很重要! 心态很重要! 重要的事情说三遍,这一点我觉得是必须放到前面来讲。找工作之前,有一点你必须清楚,就是找工作是一件看缘分的事情,不是你很牛逼,你就一定能进你想进的公司,都是有一个概率在那。如果你基础好,项目经验足,同时准备充分,那么你拿到offer的概率就会比较高;相反,如果你准备不充分,基础也不好,那么你拿到offer的概率就会比较低,但是你可以多投几家公司,这样拿到offer的几率就要大一点,因为你总有运气好的时候。所以,不要惧怕面试,刚开始失败了没什么的,多投多尝试,面多了你就自然能成面霸了。得失心也不要太重,最后每个人都会有offer的。还有一个对待工作的心态,有些人可能觉得自己没有动力去找一个好工作。其实你需要明白一件事情,你读了十几二十年的书,为的是什么,最后不就是为了找到一个好工作么。现在到了关键时刻,你为何不努力一把呢,为什么不给自己一个好的未来呢,去一个自己不满意的公司工作,你甘心吗?想清楚这一点,我相信大多数人都会有一股干劲了,因为LZ刚刚准备开始找实习的时候,BAT这种公司想都不敢想,觉得能进个二线公司就很不错了,后来发现自己不逼自己一把,你真不知道自己有多大能耐,所以请对找工作保持积极与十二分的热情,也请认真对待每一次笔试面试。二、基础基础这东西,各个公司都很看重,尤其是BAT这种大公司,他们看中人的潜力,他们舍得花精力去培养,所以基础是重中之重。之前很多人问我,项目经历少怎么办,那就去打牢基础,当你的基础好的发指的时候,你的其他东西都不重要了。基础无外乎几部分:语言(C/C++或java),操作系统,TCP/IP,数据结构与算法,再加上你所熟悉的领域。这里面其实有很多东西,各大面试宝典都有列举。J2SE基础1. 九种基本数据类型的大小,以及他们的封装类。2. Switch能否用string做参数?3. equals与==的区别。4. Object有哪些公用方法?5. Java的四种引用,强弱软虚,用到的场景。6. Hashcode的作用。7. ArrayList、LinkedList、Vector的区别。8. String、StringBuffer与StringBuilder的区别。9. Map、Set、List、Queue、Stack的特点与用法。10. HashMap和HashTable的区别。11. HashMap和ConcurrentHashMap的区别,HashMap的底层源码。12. TreeMap、HashMap、LindedHashMap的区别。13. Collection包结构,与Collections的区别。14. try catch finally,try里有return,finally还执行么?15. Excption与Error包结构。OOM你遇到过哪些情况,SOF你遇到过哪些情况。16. Java面向对象的三个特征与含义。17. Override和Overload的含义去区别。18. Interface与abstract类的区别。19. Static class 与non static class的区别。20. java多态的实现原理。21. 实现多线程的两种方法:Thread与Runable。22. 线程同步的方法:sychronized、lock、reentrantLock等。23. 锁的等级:方法锁、对象锁、类锁。24. 写出生产者消费者模式。25. ThreadLocal的设计理念与作用。26. ThreadPool用法与优势。27. Concurrent包里的其他东西:ArrayBlockingQueue、CountDownLatch等等。28. wait()和sleep()的区别。29. foreach与正常for循环效率对比。30. Java IO与NIO。31. 反射的作用于原理。32. 泛型常用特点,List能否转为List。33. 解析XML的几种方式的原理与特点:DOM、SAX、PULL。34. Java与C++对比。35. Java1.7与1.8新特性。36. 设计模式:单例、工厂、适配器、责任链、观察者等等。37. JNI的使用。Java里有很多很杂的东西,有时候需要你阅读源码,大多数可能书里面讲的不是太清楚,需要你在网上寻找答案。推荐书籍:《java核心技术卷I》《Thinking in java》《java并发编程》《effictive java》《大话设计模式》JVM1. 内存模型以及分区,需要详细到每个区放什么。2. 堆里面的分区:Eden,survival from to,老年代,各自的特点。3. 对象创建方法,对象的内存分配,对象的访问定位。4. GC的两种判定方法:引用计数与引用链。5. GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?6. GC收集器有哪些?CMS收集器与G1收集器的特点。7. Minor GC与Full GC分别在什么时候发生?8. 几种常用的内存调试工具:jmap、jstack、jconsole。9. 类加载的五个过程:加载、验证、准备、解析、初始化。10. 双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。11. 分派:静态分派与动态分派。JVM过去过来就问了这么些问题,没怎么变,内存模型和GC算法这块问得比较多,可以在网上多找几篇博客来看看。推荐书籍:《深入理解java虚拟机》操作系统1. 进程和线程的区别。2. 死锁的必要条件,怎么处理死锁。3. Window内存管理方式:段存储,页存储,段页存储。 4. 进程的几种状态。5. IPC几种通信方式。6. 什么是虚拟内存。7. 虚拟地址、逻辑地址、线性地址、物理地址的区别。因为是做android的这一块问得比较少一点,还有可能上我简历上没有写操作系统的原因。推荐书籍:《深入理解现代操作系统》三、 项目关于项目,这部分每个人的所做的项目不同,所以不能具体的讲。项目不再与好与不好,在于你会不会包装,有时候一个很low的项目也能包装成比较高大上的项目,多用一些专业名词,突出关键字,能使面试官能比较容易抓住重点。在聊项目的过程中,其实你的整个介绍应该是有一个大体的逻辑,这个时候是在考验你的表达与叙述能力,所以好好准备很重要。面试官喜欢问的问题无非就几个点:1. XXX(某个比较重要的点)是怎么实现的?2. 你在项目中遇到的最大的困难是什么,怎么解决的?3. 项目某个部分考虑的不够全面,如果XXXX,你怎么优化?4. XXX(一个新功能)需要实现,你有什么思路?其实你应该能够预料到面试官要问的地方,请提前准备好,如果被问到没有准备到的地方,也不要紧张,一定要说出自己的想法,对不对都不是关键,主要是有自己的想法,另外,你应该对你的项目整体框架和你做的部分足够熟悉。四、 其他你应该问的问题面试里,最后面完之后一般面试官都会问你,你有没有什么要问他的。其实这个问题是有考究的,问好了其实是有加分的,一般不要问薪资,主要应该是:关于公司的、技术和自身成长的。以下是我常问的几个问题,如果需要可以参考:1. 贵公司一向以XXX著称,能不能说明一下公司这方面的特点?2. 贵公司XXX业务发展很好,这是公司发展的重点么?3. 对技术和业务怎么看?4. 贵公司一般的团队是多大,几个人负责一个产品或者业务?5. 贵公司的开发中是否会使用到一些最新技术?6. 对新人有没有什么培训,会不会安排导师?7. 对Full Stack怎么看?8. 你觉得我有哪些需要提高的地方?知识面除了基础外,你还应该对其他领域的知识有多少有所涉猎。对于你所熟悉的领域,你需要多了解一点新技术与科技前沿,你才能和面试官谈笑风生。软实力什么是软实力,就是你的人际交往、灵活应变能力,在面试过程中,良好的礼节、流畅的表达、积极的交流其实都是非常重要的。很多公司可能不光看你的技术水平怎么样,而更看重的是你这个人怎么样的。所以在面试过程中,请保持诚信、积极、乐观、幽默,这样更容易得到公司青睐。很多时候我们都会遇到一个情况,就是面试官的问题我不会,这时候大多数情况下不要马上说我不会,要懂得牵引,例如面试官问我C++的多态原理,我不懂,但我知道java的,哪我可以向面试官解释说我知道java的,类似的这种可以往相关的地方迁移(但是需要注意的是一定不要不懂装懂,被拆穿了是很尴尬的),意思就是你要尽可能的展示自己,表现出你的主动性,向面试官推销自己。还有就是遇到智力题的时候,不要什么都不说,面试官其实不是在看你的答案,而是在看你的逻辑思维,你只要说出你自己的见解,有一定的思考过程就行。五、 面经面经其实说来话长,包括实习的话面过的公司有:腾讯、阿里、百度、网易、蘑菇街、小米。最早得追溯到到今年3月份,那时候刚过完年,然后阿里的实习内推就开始了,我基本都没什么准备,就突如其来的接到了人生中第一个面试电话。阿里实习内推一面:电话面试, 由于是第一次面试,所以非常紧张,项目都没怎么说清楚。然后面试官就开始问项目细节了,这里我关于一个项目细节和面试官有不同的看法,面试官说我这样做有问题,然后我说我们确实是这样做的,并没有出什么错,差点和面试官吵起来,最后我还是妥协了。然后问了我一个怎么对传输的数据加密,我答的很挫,然后面试官就开始鄙视我:你这个基础不好,那个基础不好,那你说说你还有其他什么优势没?Blabla紧张的说了一些…………只面了30分钟不到,然后妥妥的就挂了。经过这次面试突然感觉人生的艰辛,几天后我们教研室的其他同学陆续开始了面试,他们都很顺利,其中我的室友(单程车票)很顺利的拿到了offer,他是个大神,然后我就压力无比的大。制定了整套复习计划,从早上9点看书看到晚上10点。腾讯实习1面:50分钟左右, 面试的时候还是有些紧张的,但是运气好,遇到了一个学校的师兄,他一直叫我不要紧张。几个比较关键的问题:死锁的必要条件,怎么解决,java和c++比有什么优势,java同步方法,activity生命周期,中间让我设计了个银行排队系统,我说了一堆。然后让我写了一个计算一个int里面二进制有几个1,然后我用最高效的方法(n=n&n-1)写出来之后,面试官有点意外,还说没见过这么写的,让我跟他解释一下。后面就是拉拉家常,问我对工作地点怎么看,让我对比qq和微信,一面出来之后,面试官让我留意通知,心想是过了,其实发挥的不怎么好。就在会学校的路上,都要到学校了,收到了腾讯二面的通知,下午3点。然后我又跑回去二面。2面: 二面是一个很严肃的人,看上去就比较资深那种,一直都不笑,后面才知道是手机管家T4的专家。一开始就问我项目里,心跳包是怎么设计的,我项目里并没有用心跳,然后只能跟他说没做,问我用json传输数据有什么不好(我只知道用哪想过有什么不好)。又问了http和socket的区别,两个协议哪个更高效一点,遇到过java内存泄露没有,用过哪些调试java内存工具,java四种引用。多数都是项目上的东西,基础的东西没问太多,然后感觉自己答的不是很好,很多都不知道,而且还答错了。其实我感觉我应该是过不了的,但是最后我问问题的时候,我让他评价下我的表现,他说不好评价,我自己说了一堆,说在学校里确实见识到的东西比较少,很多东西没考虑全面,然后他表示赞同,和我探讨了一番,我觉得最后这个问题给我加了不少分。二面也面了50分钟左右。回来后发现我的状态一直没变,而他们二面完了的都到了HR面了,我以为我已经挂定了,后来在一天晚上12点的时候,惊喜的收到了第二天HR面的短信,当晚上几乎高兴得一晚上没睡着觉。3面(HR): 就是hr面,也就面了十几分钟,聊聊天,问问哪的人,未来什么打算的等等,基本不怎么挂人就不详细写了。就这样拿到了人生中第一个实习offer。后面找实习的心就放松了,没有复习了。然后到了5月5号,阿里来了。对阿里也只是想去面一面的心态了,因为已经有腾讯的offer了,就没想太多。阿里实习1面: 面过腾讯之后发现自己已经比较淡定了,面试得时候能够比较好的交谈了。这一面也遇到一个比较好的面试官,能很轻松的和他交流。主要的问题是android的:activity的生命周期、activity的四种启动模式(当时忘了一些没答全)、线性布局和相对布局、多线程请求,java GC算法与GC方法,内存模型,有一个比较特别的问题是问我微信的朋友圈怎么设计,然后我把思路跟他说了,其他的就是问了项目相关的了。还问了我一个觉得技术深度重要还是技术宽度重要,一面感觉还是比较基础的。实习2面: 这一面就比较虐心,碰到一个阿里云的CTO,一上去项目看都不看,直接问我写过多少行代码,我说至少3、4万行,然后他让我写了两个题:一个找素数,一个递归求阶层,对我也算手下留情(他后来让我同学写AVL树的插入算法,想想也是醉了)。后面就各种基础了,java的基础挨个问了一遍,比较关键多线程实现,锁的几种等级等,反射的用法,wait()和sleep()(讨论这个的时候他把我说晕了),Java还好,多数能应付,然后他就开始问c++的了。虽然是基础,但是lz忘了差不多了,什么指针数组和数组指针,虚函数,多态实现(这个我扯到java上了)等等,问了很多,很多都没答上来,然后他说我基础不太好(我想说我简历上写的了解C++,为什么要追着我问TT)。就这样出来了,本来以为挂了,后面被通知过了。同学都只有2面技术面,我居然多了一面,叫交叉面试,心想这下肯定完了。实习3面: 这一面遇到了后面我去实习时候的部门boss,人非常好,来的时候走的时候都要和我握手,非常的平易近人。这一面还是问项目上的一些东西居多,基础就问了个java多线程,各个排序的时间复杂度、思想。技术问了半个小时,后面半个小时就开始各种聊人生了(@_@),我家是哪的,父母干嘛的,中学怎么样,大学怎么样,等等,完全就不像是技术面嘛(后来才知道,我一个同学一开始来就和他聊人生,还聊过了。再次感叹找工作是看缘分呐)。实习4面(HR) :阿里hr比腾讯hr面专业,面了一个小时,把我的生活经历趴了一遍,(问了类似你的优缺点,最让你高兴的一件事,最让你伤心的一件事,你的职业规划,你的理想等等,这种,现在想不起来了)也没什么特别好说的。面完后第二天去圆桌签offer,就这样又拿到了阿里的实习offer。LZ后面衡量了杭州阿里B2B和广州腾讯MIG,最后选择去了阿里,因为在总部,感觉大boss人比较好,发展前途可能不错,而且留下来的几率比较大,而腾讯是一个分部门,感觉可能不是很有前景(但是后来了解到其实广州腾讯MIG发展前景非常好,环境也非常和谐,我同学去实习的都留下来了。哎,只能感叹选择是个大问题)。在阿里实习的两个月时间也挺愉快的,学到了不少东西,也认识了很好的师兄和主管,只因最后被拥抱了变化没有拿到正式offer。实习面经就已经写完了,后面是正式找工作的经历,主要是内推比较多:腾讯、网易、蘑菇街、小米,校招就面了家百度。在阿里实习的时候,面了网易和蘑菇街。网易内推1面: 电话面,一天在里中午休息的时候面的。这一面我面得很烂,由于在阿里实习,面试官恰好也在阿里呆过,问了我在阿里学到了哪些东西,看过哪些框架,看过源码没有,我支支吾吾说了一些,面试官不太满意(我表示我都说不全啊,在阿里就来了不久,哪那么多时间看源码)。项目各种细节问一通之后,开始问基础,Http报文结构,Handler、Looper模型,ThreadLocal(这个LZ当时没答上来),怎么使service不被杀死,android内存优化,自己实现线程队列模型,问我怎么设计(这个当时被前面的问题问蒙了,直接说不知道了),面了20+分钟,感觉答得都不怎么好,然后面试官问我说还有没有什么比较擅长的他没有问道的,我就把android Framework里zygote的启动和Binder通信说了一遍(这里强行装了一次逼)。面完之后本以为挂定了,然后师姐跟我说居然过了,也是够神奇,我觉得是我后面补充的内容救了我。内推2面: 二面是现场面,就在阿里滨江区的隔壁。时间是一天中午,吃了饭就到了隔壁。面试官是个比较年轻人,可能大不了我几岁,也是非常好说话,开始也是聊项目,我把在阿里做的app和自己写的小框架拿出来,他就指着上面各种问,这里怎么实现,会有什么问题,你怎么解决,然后他描述了一个场景说,两个activity,前面的是个dialog activity,怎么在dialog activity存在的情况下改变后面的activity(lz答的用广播)。android怎么解决缓存,要是内存超了怎么办?然后扯到了JVM,GC判定算法与方法,哪个区域用什么GC算法,怎么改进复制算法。然后是基础,也像一面一样问了一些,hashmap和concurrntHashmap的区别、泛型能否强制转换。然后是算法,问了快排和归并的平均时间复杂度与最差时间复杂度,出了个算法题:怎么找到一个随机数组的前50大数、中间50大数,(这个用最小堆和partition函数),复杂度是多少。面完之后其实感觉还不错,基本都打答上来了,顺利进入三面。内推3面(HR): hr面也是现场,也聊了很多,问我为什么要从阿里来网易,有什么打算,你看中网易的什么(主要是针对我是在阿里实习来问的,我就讲了一堆网易的优势),让来杭州工作愿不愿意。还跟我说了,这次内推是优中选优,有名额限制,如果没有通过,请继续关注网易校招。后面让师姐查了下状态,状态显示是三面已通过。但是最后没有收到offer,还是有点小失望。蘑菇街内推1面: 电话面,也是在一个中午面的。18分钟,问了一些项目,主要是问基础、问得非常基础:Arraylist与LinkedList区别,String与StringBuffer用法,HashMap与HashTable区别,Synchronized用法等等等等(非常基础),这不一一列举了,然后很顺利的就过了。2面是在20天后了,也不知道蘑菇街出了什么岔子。内推2面 :也是电话面,CTO面试,就整体聊了项目,我在项目中学到了什么,遇到什么困难怎么解决的,在阿里实习学到了哪些东西,有看过源码么,我的优缺点,我为什么选择蘑菇街,我了解蘑菇街哪些东西。最后答完感觉自己答得还行但是也没有过,不知道为什么。小米内推1面: 电话面,大概40分钟,面试的时候那边很吵,不过幸好面试官语速慢,而且我答完一个问题后,面试官会和我交流哪里没有答好。没有问项目,就问了基础,问题也不多:HashMap删除元素的方法,for each和正常for的用在不同数据结构(ArrayList、set、hashmap)上的效率区别(LZ表示没有看过源码,不知道),static class和non-static class的区别,一个大文件几个GB,怎么实现复制(这个也没有答好)。然后问了两个算法:之前一个出现过,另一个是在git里面,如果有n个分支,m次commit怎么找到任意两个节点共同的那个父节点(这个当时我想错了,想到二叉树上去了,没有答好)。然后让两个算法用代码实现,1个小时内写好email给他。小米面了以后也杳无音信,估计也是要求很高,毕竟解决北京户口。其实在阿里实习的时候很早就开始投简历了,因为出去实习一段时间后,感觉还是很想留在成都(因为lz是四川人)。腾讯我没有参加校招面试,直接走的内推流程。腾讯1面: 电话面,7月20+号,很水,就问了项目,聊了可能有十多分钟,然后面试官说,内推没有什么作用,还是要走校招面试(我觉得他可能是有其他事情,想节省时间),你在实习不能回来,还是要现场面一次才行,然后就留了个电话让我校招联系他,这样就完了。2面是在我回学校后了。2面: 9月6号我回学校之后,下午3点接到电话,让我晚上7点去腾讯现场面的(我在想为何是在晚上,lz学校到腾讯要2个小时,还让不让人回来了),当时紧张得要死,因为刚从阿里回来不久,都没怎么好好准备基础,在地铁上看了两本基础书,亚历山大。面试是在腾讯里面,微信部门,面试官是个中年人(现在是LZ的主管),看起来还是比较沉稳的那种。也没问基础技术问题,就聊项目细节和一些可优化的地方,然后把lz的简历看了翻了一遍,问了一遍,然后就是问我在阿里学到了什么,为什么当时选择了阿里(这时候肯定要各种跪舔啊)。然后后来他说他是做ios的,我在想难怪不问我基础。面完了说一周之内通知我结果,也没报太大希望,感觉并不太对口,因为搞不懂为什么是做ios的来面我。两天之后,在阿里HRG电话通知我拥抱变化之后,几乎同一时间,腾讯电话通知我拿到了成都offer,我只能感叹太巧了(大概这大半辈子的运气都花光了)。 后来校招开始后,只面了百度一家公司,百度确实比较重视基础与算法,看中技术。百度1面: 大概1个小时,又是个做ios的师兄面试我,自然就只能聊项目了,我给他展示了我做的app后,也问了些技术问题,缓存怎么做的,内存溢出怎么处理。然后两个算法题:把一个数组中奇数放前面,偶数放后面,这个要求写出来。另一个是3亿条IP中,怎么找到次数出现最多的5000条IP。最后问了是否愿意去北京,对于技术的看法。2面: 50分钟,写个4个程序题:反转链表、冒泡排序、生产者消费者,这三个都还好写,很快的写出来了,还有一个题是在一组排序数中,给定一个数,返回最接近且不大于这个数的位置,要求时间在O(logn)(这个想了一会,用二分查找,然后特殊处理了一下),最后他看不懂,要我一步一步解释。花了好一整子,最后问了个java反射,就让我走了。百度果然是重视算法。3面: 这一面应该是个技术高层,笼统的问了我一下项目的问题,然后问了几个基础:java反射机制;android动画有哪些,什么特点?TCP/IP层次架构,每层的作用与协议;TCP拥塞控制;滑动窗口是怎么设计的,有什么好处;android的布局都有哪些。问完这些之后,然后就是有点类似于HR的聊天了:如果这次面试过了你觉得是因为什么原因,没过呢?你觉得百度怎么样?你对技术路线什么打算?有些和前面重复的就不写了。然后他让我问他问题,我就连续问了5、6个问题,最后愉快的走了。百度这两天给结果。六、 写在最后关于选择LZ当时实习的时候,杭州阿里和广州腾讯选择去了阿里,但是却因为拥抱变化没有留下来,相反这边在腾讯实习的同学却很顺利。但是也是因为没有去广州腾讯,最后我能留在成都腾讯。选择是一件非常重要的事情,它决定着你的未来,但是也有一点你得知道:塞翁失马焉知非福,现在看起来不太好的选择,不一定将来就好,未来有太多未知数。心怀感恩其实一路走来,我也是在成长,从最初的不自信,到了最后面试一切都比较冷静与沉着。我一直相信,机会是留给有准备的人,所以,请提早准备,越早越好。我很感激能有那么多人帮助我和肯定我,没有最初腾讯的肯定,我肯定不会走的这么顺利,所以我很感恩哪些让我通过的人,也感谢我们实验室的兄弟姐妹,给了我良好的学习成长环境,心怀感恩才能好运常在。找工作其实就像是一场战役, 前面我们经历了高考或者考研, 现在是找工作,你不在这个时候搏一搏,怎么对得起你之前的努力。不要担心找不到好工作,你要相信:天道酬勤! 目标已经有了,下面就看行动了!记住:学习永远是自己的事情,你不学时间也不会多,你学了有时候却能够使用自己学到的知识换得更多自由自在的美好时光!时间是生命的基本组成部分,也是万物存在的根本尺度,我们的时间在那里我们的生活就在那里!我们价值也将在那里提升或消弭!Java程序员,加油吧
把redis作为缓存使用已经是司空见惯,但是使用redis后也可能会碰到一系列的问题,尤其是数据量很大的时候,经典的几个问题如下: (一)缓存和数据库间数据一致性问题 分布式环境下(单机就不用说了)非常容易出现缓存和数据库间的数据一致性问题,针对这一点的话,只能说,如果你的项目对缓存的要求是强一致性的,那么请不要使用缓存。我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。合适的策略包括 合适的缓存更新策略,更新数据库后要及时更新缓存、缓存失败时增加重试机制,例如MQ模式的消息队列。 (二)缓存击穿问题 缓存击穿表示恶意用户模拟请求很多缓存中不存在的数据,由于缓存中都没有,导致这些请求短时间内直接落在了数据库上,导致数据库异常。这个我们在实际项目就遇到了,有些抢购活动、秒杀活动的接口API被大量的恶意用户刷,导致短时间内数据库c超时了,好在数据库是读写分离,同时也有进行接口限流,hold住了。 解决方案的话: 方案1、使用互斥锁排队 业界比价普遍的一种做法,即根据key获取value值为空时,锁上,从数据库中load数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用分布式锁,单机的话用普通的锁(synchronized、Lock)就够了。 public String getWithLock(String key, Jedis jedis, String lockKey, String uniqueId, long expireTime) { // 通过key获取value String value = redisService.get(key); if (StringUtil.isEmpty(value)) { // 分布式锁,详细可以参考https://blog.csdn.net/fanrenxiang/article/details/79803037 //封装的tryDistributedLock包括setnx和expire两个功能,在低版本的redis中不支持 try { boolean locked = redisService.tryDistributedLock(jedis, lockKey, uniqueId, expireTime); if (locked) { value = userService.getById(key); redisService.set(key, value); redisService.del(lockKey); return value; } else { // 其它线程进来了没获取到锁便等待50ms后重试 Thread.sleep(50); getWithLock(key, jedis, lockKey, uniqueId, expireTime); } } catch (Exception e) { log.error("getWithLock exception=" + e); return value; } finally { redisService.releaseDistributedLock(jedis, lockKey, uniqueId); } } return value; } 这样做思路比较清晰,也从一定程度上减轻数据库压力,但是锁机制使得逻辑的复杂度增加,吞吐量也降低了,有点治标不治本。 方案2、接口限流与熔断、降级 重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些服务不可用时候,进行熔断,失败快速返回机制。 方案3、布隆过滤器 bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,下面先来简单的实现下看看效果,我这里用guava实现的布隆过滤器: <dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency> </dependencies> public class BloomFilterTest { private static final int capacity = 1000000; private static final int key = 999998; private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity); static { for (int i = 0; i < capacity; i++) { bloomFilter.put(i); } } public static void main(String[] args) { /*返回计算机最精确的时间,单位微妙*/ long start = System.nanoTime(); if (bloomFilter.mightContain(key)) { System.out.println("成功过滤到" + key); } long end = System.nanoTime(); System.out.println("布隆过滤器消耗时间:" + (end - start)); int sum = 0; for (int i = capacity + 20000; i < capacity + 30000; i++) { if (bloomFilter.mightContain(i)) { sum = sum + 1; } } System.out.println("错判率为:" + sum); } } 成功过滤到999998 布隆过滤器消耗时间:215518 错判率为:318 可以看到,100w个数据中只消耗了约0.2毫秒就匹配到了key,速度足够快。然后模拟了1w个不存在于布隆过滤器中的key,匹配错误率为318/10000,也就是说,出错率大概为3%,跟踪下BloomFilter的源码发现默认的容错率就是0.03: public static <T> BloomFilter<T> create(Funnel<T> funnel, int expectedInsertions /* n */) { return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions } 我们可调用BloomFilter的这个方法显式的指定误判率: image.png private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity,0.01); 我们断点跟踪下,误判率为0.02和默认的0.03时候的区别: image.png image.png 对比两个出错率可以发现,误判率为0.02时数组大小为8142363,0.03时为7298440,误判率降低了0.01,BloomFilter维护的数组大小也减少了843923,可见BloomFilter默认的误判率0.03是设计者权衡系统性能后得出的值。要注意的是,布隆过滤器不支持删除操作。用在这边解决缓存穿透问题就是: public String getByKey(String key) { // 通过key获取value String value = redisService.get(key); if (StringUtil.isEmpty(value)) { if (bloomFilter.mightContain(key)) { value = userService.getById(key); redisService.set(key, value); return value; } else { return null; } } return value; } (三)缓存雪崩问题 缓存在同一时间内大量键过期(失效),接着来的一大波请求瞬间都落在了数据库中导致连接异常。 解决方案: 方案1、也是像解决缓存穿透一样加锁排队,实现同上; 方案2、建立备份缓存,缓存A和缓存B,A设置超时时间,B不设值超时时间,先从A读缓存,A没有读B,并且更新A缓存和B缓存; 方案3、设置缓存超时时间的时候加上一个随机的时间长度,比如这个缓存key的超时时间是固定的5分钟加上随机的2分钟,酱紫可从一定程度上避免雪崩问题; public String getByKey(String keyA,String keyB) { String value = redisService.get(keyA); if (StringUtil.isEmpty(value)) { value = redisService.get(keyB); String newValue = getFromDbById(); redisService.set(keyA,newValue,31, TimeUnit.DAYS); redisService.set(keyB,newValue); } return value; } (四)缓存并发问题 这里的并发指的是多个redis的client同时set key引起的并发问题。其实redis自身就是单线程操作,多个client并发操作,按照先到先执行的原则,先到的先执行,其余的阻塞。当然,另外的解决方案是把redis.set操作放在队列中使其串行化,必须的一个一个执行,具体的代码就不上了,当然加锁也是可以的,至于为什么不用redis中的事务,留给各位看官自己思考探究。 免费获取Java工程化、高性能及分布式、高性能、高架构、性能调优、Spring、MyBatis、Netty源码分析等多个知识点高级进阶干货的直播免费学习权限及相关视频资料,还有spring和虚拟机等书籍扫描版
公司用到的很多技术,自己之前都没学过(尬),于是只能慢慢补了。这次给大家写写我学习消息队列的笔记,希望对大家有帮助。 一、什么是消息队列? 消息队列不知道大家看到这个词的时候,会不会觉得它是一个比较高端的技术,反正我是觉得它好像是挺牛逼的。 消息队列,一般我们会简称它为MQ(Message Queue),嗯,就是很直白的简写。 我们先不管消息(Message)这个词,来看看队列(Queue)。这一看,队列大家应该都熟悉吧。 队列是一种先进先出的数据结构。 什么是消息队列? 在Java里边,已经实现了不少的队列了: 什么是消息队列? 那为什么还需要消息队列(MQ)这种中间件呢???其实这个问题,跟之前我学Redis的时候很像。Redis是一个以key-value形式存储的内存数据库,明明我们可以使用类似HashMap这种实现类就可以达到类似的效果了,那还为什么要Redis?《Redis合集》 到这里,大家可以先猜猜为什么要用消息队列(MQ)这种中间件,下面会继续补充。 消息队列可以简单理解为:把要传输的数据放在队列中。 什么是消息队列? ) 科普: 把数据放到消息队列叫做生产者 从消息队列里边取数据叫做消费者 二、为什么要用消息队列? 为什么要用消息队列,也就是在问:用了消息队列有什么好处。我们看看以下的场景 2.1 解耦 现在我有一个系统A,系统A可以产生一个userId 什么是消息队列? 然后,现在有系统B和系统C都需要这个userId去做相关的操作 什么是消息队列? 写成伪代码可能是这样的: public class SystemA { // 系统B和系统C的依赖 SystemB systemB = new SystemB(); SystemC systemC = new SystemC(); // 系统A独有的数据userId private String userId = "Java3y"; public void doSomething() { // 系统B和系统C都需要拿着系统A的userId去操作其他的事 systemB.SystemBNeed2do(userId); systemC.SystemCNeed2do(userId); } } 结构图如下: 什么是消息队列? ok,一切平安无事度过了几个天。 某一天,系统B的负责人告诉系统A的负责人,现在系统B的SystemBNeed2do(String userId)这个接口不再使用了,让系统A别去调它了。 于是,系统A的负责人说"好的,那我就不调用你了。",于是就把调用系统B接口的代码给删掉了: public void doSomething() { // 系统A不再调用系统B的接口了 //systemB.SystemBNeed2do(userId); systemC.SystemCNeed2do(userId); } 又过了几天,系统D的负责人接了个需求,也需要用到系统A的userId,于是就跑去跟系统A的负责人说:"老哥,我要用到你的userId,你调一下我的接口吧" 于是系统A说:"没问题的,这就搞" 什么是消息队列? 然后,系统A的代码如下: public class SystemA { // 已经不再需要系统B的依赖了 // SystemB systemB = new SystemB(); // 系统C和系统D的依赖 SystemC systemC = new SystemC(); SystemD systemD = new SystemD(); // 系统A独有的数据 private String userId = "Java3y"; public void doSomething() { // 已经不再需要系统B的依赖了 //systemB.SystemBNeed2do(userId); // 系统C和系统D都需要拿着系统A的userId去操作其他的事 systemC.SystemCNeed2do(userId); systemD.SystemDNeed2do(userId); } } 时间飞逝: 又过了几天,系统E的负责人过来了,告诉系统A,需要userId。 又过了几天,系统B的负责人过来了,告诉系统A,还是重新掉那个接口吧。 又过了几天,系统F的负责人过来了,告诉系统A,需要userId。 …... 于是系统A的负责人,每天都被这给骚扰着,改来改去,改来改去....... 还有另外一个问题,调用系统C的时候,如果系统C挂了,系统A还得想办法处理。如果调用系统D时,由于网络延迟,请求超时了,那系统A是反馈fail还是重试?? 最后,系统A的负责人,觉得隔一段时间就改来改去,没意思,于是就跑路了。 然后,公司招来一个大佬,大佬经过几天熟悉,上来就说:将系统A的userId写到消息队列中,这样系统A就不用经常改动了。为什么呢?下面我们来一起看看: 什么是消息队列? 系统A将userId写到消息队列中,系统C和系统D从消息队列中拿数据。这样有什么好处? 系统A只负责把数据写到队列中,谁想要或不想要这个数据(消息),系统A一点都不关心。 即便现在系统D不想要userId这个数据了,系统B又突然想要userId这个数据了,都跟系统A无关,系统A一点代码都不用改。 系统D拿userId不再经过系统A,而是从消息队列里边拿。系统D即便挂了或者请求超时,都跟系统A无关,只跟消息队列有关。 这样一来,系统A与系统B、C、D都解耦了。 2.2 异步 我们再来看看下面这种情况:系统A还是直接调用系统B、C、D 什么是消息队列? 代码如下: public class SystemA { SystemB systemB = new SystemB(); SystemC systemC = new SystemC(); SystemD systemD = new SystemD(); // 系统A独有的数据 private String userId ; public void doOrder() { // 下订单 userId = this.order(); // 如果下单成功,则安排其他系统做一些事 systemB.SystemBNeed2do(userId); systemC.SystemCNeed2do(userId); systemD.SystemDNeed2do(userId); } } 假设系统A运算出userId具体的值需要50ms,调用系统B的接口需要300ms,调用系统C的接口需要300ms,调用系统D的接口需要300ms。那么这次请求就需要50+300+300+300=950ms 并且我们得知,系统A做的是主要的业务,而系统B、C、D是非主要的业务。比如系统A处理的是订单下单,而系统B是订单下单成功了,那发送一条短信告诉具体的用户此订单已成功,而系统C和系统D也是处理一些小事而已。 那么此时,为了提高用户体验和吞吐量,其实可以异步地调用系统B、C、D的接口。所以,我们可以弄成是这样的: 什么是消息队列? 系统A执行完了以后,将userId写到消息队列中,然后就直接返回了(至于其他的操作,则异步处理)。 本来整个请求需要用950ms(同步) 现在将调用其他系统接口异步化,只需要100ms(异步) (例子可能举得不太好,但我觉得说明到点子上就行了,见谅。) 2.3削峰/限流 我们再来一个场景,现在我们每个月要搞一次大促,大促期间的并发可能会很高的,比如每秒3000个请求。假设我们现在有两台机器处理请求,并且每台机器只能每次处理1000个请求。 什么是消息队列? 那多出来的1000个请求,可能就把我们整个系统给搞崩了...所以,有一种办法,我们可以写到消息队列中: 什么是消息队列? 系统B和系统C根据自己的能够处理的请求数去消息队列中拿数据,这样即便有每秒有8000个请求,那只是把请求放在消息队列中,去拿消息队列的消息由系统自己去控制,这样就不会把整个系统给搞崩。 三、使用消息队列有什么问题? 经过我们上面的场景,我们已经可以发现,消息队列能做的事其实还是蛮多的。 说到这里,我们先回到文章的开头,"明明JDK已经有不少的队列实现了,我们还需要消息队列中间件呢?"其实很简单,JDK实现的队列种类虽然有很多种,但是都是简单的内存队列。为什么我说JDK是简单的内存队列呢?下面我们来看看要实现消息队列(中间件)可能要考虑什么问题。 3.1高可用 无论是我们使用消息队列来做解耦、异步还是削峰,消息队列肯定不能是单机的。试着想一下,如果是单机的消息队列,万一这台机器挂了,那我们整个系统几乎就是不可用了。 什么是消息队列? 所以,当我们项目中使用消息队列,都是得集群/分布式的。要做集群/分布式就必然希望该消息队列能够提供现成的支持,而不是自己写代码手动去实现。 3.2 数据丢失问题 我们将数据写到消息队列上,系统B和C还没来得及取消息队列的数据,就挂掉了。如果没有做任何的措施,我们的数据就丢了。 什么是消息队列? 学过Redis的都知道,Redis可以将数据持久化磁盘上,万一Redis挂了,还能从磁盘从将数据恢复过来。同样地,消息队列中的数据也需要存在别的地方,这样才尽可能减少数据的丢失。 那存在哪呢? 磁盘? 数据库? Redis? 分布式文件系统? 同步存储还是异步存储? 3.3消费者怎么得到消息队列的数据? 消费者怎么从消息队列里边得到数据?有两种办法: 生产者将数据放到消息队列中,消息队列有数据了,主动叫消费者去拿(俗称push) 消费者不断去轮训消息队列,看看有没有新的数据,如果有就消费(俗称pull) 3.4其他 除了这些,我们在使用的时候还得考虑各种的问题: 消息重复消费了怎么办啊? 我想保证消息是绝对有顺序的怎么做? …….. 虽然消息队列给我们带来了那么多的好处,但同时我们发现引入消息队列也会提高系统的复杂性。市面上现在已经有不少消息队列轮子了,每种消息队列都有自己的特点,选取哪种MQ还得好好斟酌。 最后 本文主要讲解了什么是消息队列,消息队列可以为我们带来什么好处,以及一个消息队列可能会涉及到哪些问题。希望给大家带来一定的帮助。 同时需要更多java相关资料以及面试心得和视频资料的 免费获取Java工程化、高性能及分布式、高性能、高架构、性能调优、Spring、MyBatis、Netty源码分析等多个知识点高级进阶干货的直播免费学习权限及相关视频资料,还有spring和虚拟机等书籍扫描版
本文起源于之前去面试的一道面试题,面试题大致上是这样的:消费者去Kafka里拉去消息,但是目前Kafka中又没有新的消息可以提供,那么Kafka会如何处理? 如下图所示,两个follower副本都已经拉取到了leader副本的最新位置,此时又向leader副本发送拉取请求,而leader副本并没有新的消息写入,那么此时leader副本该如何处理呢?可以直接返回空的拉取结果给follower副本,不过在leader副本一直没有新消息写入的情况下,follower副本会一直发送拉取请求,并且总收到空的拉取结果,这样徒耗资源,显然不太合理。 这里就涉及到了Kafka延迟操作的概念。Kafka在处理拉取请求时,会先读取一次日志文件,如果收集不到足够多(fetchMinBytes,由参数fetch.min.bytes配置,默认值为1)的消息,那么就会创建一个延时拉取操作(DelayedFetch)以等待拉取到足够数量的消息。当延时拉取操作执行时,会再读取一次日志文件,然后将拉取结果返回给follower副本。 延迟操作不只是拉取消息时的特有操作,在Kafka中有多种延时操作,比如延时数据删除、延时生产等。 对于延时生产(消息)而言,如果在使用生产者客户端发送消息的时候将acks参数设置为-1,那么就意味着需要等待ISR集合中的所有副本都确认收到消息之后才能正确地收到响应的结果,或者捕获超时异常。 假设某个分区有3个副本:leader、follower1和follower2,它们都在分区的ISR集合中。为了简化说明,这里我们不考虑ISR集合伸缩的情况。Kafka在收到客户端的生产请求后,将消息3和消息4写入leader副本的本地日志文件,如上图所示。 由于客户端设置了acks为-1,那么需要等到follower1和follower2两个副本都收到消息3和消息4后才能告知客户端正确地接收了所发送的消息。如果在一定的时间内,follower1副本或follower2副本没能够完全拉取到消息3和消息4,那么就需要返回超时异常给客户端。生产请求的超时时间由参数request.timeout.ms配置,默认值为30000,即30s。 那么这里等待消息3和消息4写入follower1副本和follower2副本,并返回相应的响应结果给客户端的动作是由谁来执行的呢?在将消息写入leader副本的本地日志文件之后,Kafka会创建一个延时的生产操作(DelayedProduce),用来处理消息正常写入所有副本或超时的情况,以返回相应的响应结果给客户端。 延时操作需要延时返回响应的结果,首先它必须有一个超时时间(delayMs),如果在这个超时时间内没有完成既定的任务,那么就需要强制完成以返回响应结果给客户端。其次,延时操作不同于定时操作,定时操作是指在特定时间之后执行的操作,而延时操作可以在所设定的超时时间之前完成,所以延时操作能够支持外部事件的触发。 就延时生产操作而言,它的外部事件是所要写入消息的某个分区的HW(高水位)发生增长。也就是说,随着follower副本不断地与leader副本进行消息同步,进而促使HW进一步增长,HW每增长一次都会检测是否能够完成此次延时生产操作,如果可以就执行以此返回响应结果给客户端;如果在超时时间内始终无法完成,则强制执行。 回顾一下文中开头的延时拉取操作,它也同样如此,也是由超时触发或外部事件触发而被执行的。超时触发很好理解,就是等到超时时间之后触发第二次读取日志文件的操作。外部事件触发就稍复杂了一些,因为拉取请求不单单由follower副本发起,也可以由消费者客户端发起,两种情况所对应的外部事件也是不同的。如果是follower副本的延时拉取,它的外部事件就是消息追加到了leader副本的本地日志文件中;如果是消费者客户端的延时拉取,它的外部事件可以简单地理解为HW的增长。 人过留名,雁过留声,路过记得点个赞。 同时需要更多java相关资料以及面试心得和视频资料的,关注+转发+私信我“架构”免费获取Java工程化、高性能及分布式、高性能、高架构、性能调优、Spring、MyBatis、Netty源码分析等多个知识点高级进阶干货的直播免费学习权限及相关视频资料,还有spring和虚拟机等书籍扫描版 私信关键词 【架构】免费获取!
Introduction 有很多人问过我这么一类问题:RabbitMQ如何确保消息可靠?很多时候,笔者的回答都是:说来话长的事情何来长话短说。的确,要确保消息可靠不只是单单几句就能够叙述明白的,包括Kafka也是如此。可靠并不是一个绝对的概念,曾经有人也留言说过类似全部磁盘损毁也会导致消息丢失,笔者戏答:还有机房被炸了也会导致消息丢失。可靠性是一个相对的概念,在条件合理的范围内系统所能确保的多少个9的可靠性。一切尽可能的趋于完美而无法企及于完美。 我们可以尽可能的确保RabbitMQ的消息可靠。在详细论述RabbitMQ的消息可靠性之前,我们先来回顾下消息在RabbitMQ中的经由之路。 image.png 如图所示,从AMQP协议层面上来说: 消息先从生产者Producer出发到达交换器Exchange; 交换器Exchange根据路由规则将消息转发对应的队列Queue之上; 消息在队列Queue上进行存储; 消费者Consumer订阅队列Queue并进行消费。 我们对于消息可靠性的分析也从这四个阶段来一一探讨。(全文共6500+字,反正一时半会你也看不完,不如关注收藏一波先~~) Phase 1 消息从生产者发出到达交换器Exchange,在这个过程中可以发生各种情况,生产者客户端发送出去之后可以发生网络丢包、网络故障等造成消息丢失。一般情况下如果不采取措施,生产者无法感知消息是否已经正确无误的发送到交换器中。如果消息在传输到Exchange的过程中发生失败而可以让生产者感知的话,生产者可以进行进一步的处理动作,比如重新投递相关消息以确保消息的可靠性。 为此AMQP协议在建立之初就考虑到这种情况而提供了事务机制。RabbitMQ客户端中与事务机制相关的方法有三个:channel.txSelect、channel.txCommit以及channel.txRollback。channel.txSelect用于将当前的信道设置成事务模式,channel.txCommit用于提交事务,而channel.txRollback用于事务回滚。在通过channel.txSelect方法开启事务之后,我们便可以发布消息给RabbitMQ了,如果事务提交成功,则消息一定到达了RabbitMQ中,如果在事务提交执行之前由于RabbitMQ异常崩溃或者其他原因抛出异常,这个时候我们便可以将其捕获,进而通过执行channel.txRollback方法来实现事务回滚。注意这里的RabbitMQ中的事务机制与大多数数据库中的事务概念并不相同,需要注意区分。 事务确实能够解决消息发送方和RabbitMQ之间消息确认的问题,只有消息成功被RabbitMQ接收,事务才能提交成功,否则我们便可在捕获异常之后进行事务回滚,与此同时可以进行消息重发。但是使用事务机制的话会“吸干”RabbitMQ的性能,那么有没有更好的方法既能保证消息发送方确认消息已经正确送达,又能基本上不带来性能上的损失呢?从AMQP协议层面来看并没有更好的办法,但是RabbitMQ提供了一个改进方案,即发送方确认机制(publisher confirm)。 生产者将信道设置成confirm(确认)模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地了。RabbitMQ回传给生产者的确认消息中的deliveryTag包含了确认消息的序号,此外RabbitMQ也可以设置channel.basicAck方法中的multiple参数,表示到这个序号之前的所有消息都已经得到了处理。 image.png 事务机制在一条消息发送之后会使发送端阻塞,以等待RabbitMQ的回应,之后才能继续发送下一条消息。相比之下,发送方确认机制最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack(Basic.Nack)命令,生产者应用程序同样可以在回调方法中处理该nack命令。 生产者通过调用channel.confirmSelect方法(即Confirm.Select命令)将信道设置为confirm模式,之后RabbitMQ会返回 Confirm.Select-Ok命令表示同意生产者将当前信道设置为confirm模式。所有被发送的后续消息都被ack或者nack一次,不会出现一条消息即被ack又被nack的情况。并且RabbitMQ也并没有对消息被confirm的快慢做任何保证。 事务机制和publisher confirm机制两者是互斥的,不能共存。如果企图将已开启事务模式的信道再设置为publisher confirm模式,RabbitMQ会报错:{amqp_error, precondition_failed, "cannot switch from tx to confirm mode", 'confirm.select'},或者如果企图将已开启publisher confirm模式的信道在设置为事务模式的话,RabbitMQ也会报错:{amqp_error, precondition_failed, "cannot switch from confirm to tx mode", 'tx.select' }。 事务机制和publisher confirm机制确保的是消息能够正确的发送至RabbitMQ,这里的“发送至RabbitMQ”的含义是指消息被正确的发往至RabbitMQ的交换器,如果此交换器没有匹配的队列的话,那么消息也将会丢失。所以在使用这两种机制的时候要确保所涉及的交换器能够有匹配的队列。更进一步的讲,发送方要配合mandatory参数或者备份交换器一起使用来提高消息传输的可靠性。 Phase 2 mandatory和immediate是channel.basicPublish方法中的两个参数,它们都有当消息传递过程中不可达目的地时将消息返回给生产者的功能。而RabbitMQ提供的备份交换器(Alternate Exchange)可以将未能被交换器路由的消息(没有绑定队列或者没有匹配的绑定)存储起来,而不用返回给客户端。 RabbitMQ 3.0版本开始去掉了对于immediate参数的支持,对此RabbitMQ官方解释是:immediate参数会影响镜像队列的性能,增加代码复杂性,建议采用TTL和DLX的方法替代。所以本文只简单介绍mandatory和备份交换器。 当mandatory参数设为true时,交换器无法根据自身的类型和路由键找到一个符合条件的队列的话,那么RabbitMQ会调用Basic.Return命令将消息返回给生产者。当mandatory参数设置为false时,出现上述情形的话,消息直接被丢弃。 那么生产者如何获取到没有被正确路由到合适队列的消息呢?这时候可以通过调用channel.addReturnListener来添加ReturnListener监听器实现。使用mandatory参数的关键代码如下所示: channel.basicPublish(EXCHANGE_NAME, "", true, MessageProperties.PERSISTENT_TEXT_PLAIN, "mandatory test".getBytes()); channel.addReturnListener(new ReturnListener() { public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP .BasicProperties basicProperties, byte[] body) throws IOException { String message = new String(body); System.out.println("Basic.Return返回的结果是:" + message); } }); 上面代码中生产者没有成功的将消息路由到队列,此时RabbitMQ会通过Basic.Return返回“mandatory test”这条消息,之后生产者客户端通过ReturnListener监听到了这个事件,上面代码的最后输出应该是“Basic.Return返回的结果是:mandatory test”。 生产者可以通过ReturnListener中返回的消息来重新投递或者其它方案来提高消息的可靠性。 备份交换器,英文名称Alternate Exchange,简称AE,或者更直白的可以称之为“备胎交换器”。生产者在发送消息的时候如果不设置mandatory参数,那么消息在未被路由的情况下将会丢失,如果设置了mandatory参数,那么需要添加ReturnListener的编程逻辑,生产者的代码将变得复杂化。如果你不想复杂化生产者的编程逻辑,又不想消息丢失,那么可以使用备份交换器,这样可以将未被路由的消息存储在RabbitMQ中,再在需要的时候去处理这些消息。 可以通过在声明交换器(调用channel.exchangeDeclare方法)的时候添加alternate-exchange参数来实现,也可以通过策略的方式实现。如果两者同时使用的话,前者的优先级更高,会覆盖掉Policy的设置。 参考下图,如果此时我们发送一条消息到normalExchange上,当路由键等于“normalKey”的时候,消息能正确路由到normalQueue这个队列中。如果路由键设为其他值,比如“errorKey”,即消息不能被正确的路由到与normalExchange绑定的任何队列上,此时就会发送给myAe,进而发送到unroutedQueue这个队列。 image.png 备份交换器其实和普通的交换器没有太大的区别,为了方便使用,建议设置为fanout类型,如若读者想设置为direct或者topic的类型也没有什么不妥。需要注意的是消息被重新发送到备份交换器时的路由键和从生产者发出的路由键是一样的。备份交换器的实质就是原有交换器的一个“备胎”,所有无法正确路由的消息都发往这个备份交换器中,可以为所有的交换器设置同一个AE,不过这里需要提前确保的是AE已经正确的绑定了队列,最好类型也是fanout的。如果备份交换器和mandatory参数一起使用,那么mandatory参数无效。 Phase 3 mandatory或者AE可以让消息在路由到队列之前得到极大的可靠性保障,但是消息存入队列之后的可靠性又如何保证? 首先是持久化。持久化可以提高队列的可靠性,以防在异常情况(重启、关闭、宕机等)下的数据丢失。队列的持久化是通过在声明队列时将durable参数置为true实现的,如果队列不设置持久化,那么在RabbitMQ服务重启之后,相关队列的元数据将会丢失,此时数据也会丢失。正所谓“皮之不存,毛将焉附”,队列都没有了,消息又能存在哪里呢?队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是并不能保证内部所存储的消息不会丢失。要确保消息不会丢失,需要将其设置为持久化。通过将消息的投递模式(BasicProperties中的deliveryMode属性)设置为2即可实现消息的持久化。 设置了队列和消息的持久化,当RabbitMQ服务重启之后,消息依旧存在。单单只设置队列持久化,重启之后消息会丢失;单单只设置消息的持久化,重启之后队列消失,既而消息也丢失。单单设置消息持久化而不设置队列的持久化显得毫无意义。 在持久化的消息正确存入RabbitMQ之后,还需要有一段时间(虽然很短,但是不可忽视)才能存入磁盘之中。RabbitMQ并不会为每条消息都做同步存盘(调用内核的fsync6方法)的处理,可能仅仅保存到操作系统缓存之中而不是物理磁盘之中。如果在这段时间内RabbitMQ服务节点发生了宕机、重启等异常情况,消息保存还没来得及落盘,那么这些消息将会丢失。 如果在Phase1中采用了事务机制或者publisher confirm机制的话,服务端的返回是在消息落盘之后执行的,这样可以进一步的提高了消息的可靠性。但是即便如此也无法避免单机故障且无法修复(比如磁盘损毁)而引起的消息丢失,这里就需要引入镜像队列。镜像队列相当于配置了副本,绝大多数分布式的东西都有多副本的概念来确保HA。在镜像队列中,如果主节点(master)在此特殊时间内挂掉,可以自动切换到从节点(slave),这样有效的保证了高可用性,除非整个集群都挂掉。虽然这样也不能完全的保证RabbitMQ消息不丢失(比如机房被炸。。。),但是配置了镜像队列要比没有配置镜像队列的可靠性要高很多,在实际生产环境中的关键业务队列一般都会设置镜像队列。 Phase 4 进一步的从消费者的角度来说,如果在消费者接收到相关消息之后,还没来得及处理就宕机了,这样也算数据丢失。 为了保证消息从队列可靠地达到消费者,RabbitMQ提供了消息确认机制(message acknowledgement)。消费者在订阅队列时,可以指定autoAck参数,当autoAck等于false时,RabbitMQ会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移去消息(实质上是先打上删除标记,之后再删除)。当autoAck等于true时,RabbitMQ会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正的消费到了这些消息。 采用消息确认机制后,只要设置autoAck参数为false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直等待持有消息直到消费者显式调用Basic.Ack命令为止。 当autoAck参数置为false,对于RabbitMQ服务端而言,队列中的消息分成了两个部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者确认信号的消息。如果RabbitMQ一直没有收到消费者的确认信号,并且消费此消息的消费者已经断开连接,则RabbitMQ会安排该消息重新进入队列,等待投递给下一个消费者,当然也有可能还是原来的那个消费者。 RabbitMQ不会为未确认的消息设置过期时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开,这么设计的原因是RabbitMQ允许消费者消费一条消息的时间可以很久很久。 如果消息消费失败,也可以调用Basic.Reject或者Basic.Nack来拒绝当前消息而不是确认,如果只是简单的拒绝那么消息会丢失,需要将相应的requeue参数设置为true,那么RabbitMQ会重新将这条消息存入队列,以便可以发送给下一个订阅的消费者。如果requeue参数设置为false的话,RabbitMQ立即会把消息从队列中移除,而不会把它发送给新的消费者。 还有一种情况需要考虑:requeue的消息是存入队列头部的,即可以快速的又被发送给消费,如果此时消费者又不能正确的消费而又requeue的话就会进入一个无尽的循环之中。对于这种情况,笔者的建议是在出现无法正确消费的消息时不要采用requeue的方式来确保消息可靠性,而是重新投递到新的队列中,比如设定的死信队列中,以此可以避免前面所说的死循环而又可以确保相应的消息不丢失。对于死信队列中的消息可以用另外的方式来消费分析,以便找出问题的根本。
一、前言 消息队列中间件(简称消息中间件)是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下提供应用解耦、弹性伸缩、冗余存储、流量削峰、异步通信、数据同步等等功能,其作为分布式系统架构中的一个重要组件,有着举足轻重的地位。 目前开源的消息中间件可谓是琳琅满目,能让大家耳熟能详的就有很多,比如ActiveMQ、RabbitMQ、Kafka、RocketMQ、ZeroMQ等。不管选择其中的哪一款,都会有用的不趁手的地方,毕竟不是为你量身定制的。有些大厂在长期的使用过程中积累了一定的经验,其消息队列的使用场景也相对稳定固化,或者目前市面上的消息中间件无法满足自身需求,并且也具备足够的精力和人力而选择自研来为自己量身打造一款消息中间件。但是绝大多数公司还是不会选择重复造轮子,那么选择一款合适自己的消息中间件显得尤为重要。就算是前者,那么在自研出稳定且可靠的相关产品之前还是会经历这样一个选型过程。 在整体架构中引入消息中间件,势必要考虑很多因素,比如成本及收益问题,怎么样才能达到最优的性价比?虽然消息中间件种类繁多,但是各自都有各自的侧重点,选择合适自己、扬长避短无疑是最好的方式。如果你对此感到无所适从,本文或许可以参考一二。 二、各类消息队列简述 ActiveMQ是Apache出品的、采用Java语言编写的完全基于JMS1.1规范的面向消息的中间件,为应用程序提供高效的、可扩展的、稳定的和安全的企业级消息通信。不过由于历史原因包袱太重,目前市场份额没有后面三种消息中间件多,其最新架构被命名为Apollo,号称下一代ActiveMQ,有兴趣的同学可行了解。 RabbitMQ是采用Erlang语言实现的AMQP协议的消息中间件,最初起源于金融系统,用于在分布式系统中存储转发消息。RabbitMQ发展到今天,被越来越多的人认可,这和它在可靠性、可用性、扩展性、功能丰富等方面的卓越表现是分不开的。 Kafka起初是由LinkedIn公司采用Scala语言开发的一个分布式、多分区、多副本且基于zookeeper协调的分布式消息系统,现已捐献给Apache基金会。它是一种高吞吐量的分布式发布订阅消息系统,以可水平扩展和高吞吐率而被广泛使用。目前越来越多的开源分布式处理系统如Cloudera、Apache Storm、Spark、Flink等都支持与Kafka集成。 RocketMQ是阿里开源的消息中间件,目前已经捐献个Apache基金会,它是由Java语言开发的,具备高吞吐量、高可用性、适合大规模分布式系统应用等特点,经历过双11的洗礼,实力不容小觑。 ZeroMQ号称史上最快的消息队列,基于C语言开发。ZeroMQ是一个消息处理队列库,可在多线程、多内核和主机之间弹性伸缩,虽然大多数时候我们习惯将其归入消息队列家族之中,但是其和前面的几款有着本质的区别,ZeroMQ本身就不是一个消息队列服务器,更像是一组底层网络通讯库,对原有的Socket API上加上一层封装而已。 目前市面上的消息中间件还有很多,比如腾讯系的PhxQueue、CMQ、CKafka,又比如基于Go语言的NSQ,有时人们也把类似Redis的产品也看做消息中间件的一种,当然它们都很优秀,但是本文篇幅限制无法穷极所有,下面会针对性的挑选RabbitMQ和Kafka两款典型的消息中间件来做分析,力求站在一个公平公正的立场来阐述消息中间件选型中的各个要点。 三、选型要点概述 衡量一款消息中间件是否符合需求需要从多个维度进行考察,首要的就是功能维度,这个直接决定了你能否最大程度上的实现开箱即用,进而缩短项目周期、降低成本等。如果一款消息中间件的功能达不到想要的功能,那么就需要进行二次开发,这样会增加项目的技术难度、复杂度以及增大项目周期等。 1. 功能维度 功能维度又可以划分个多个子维度,大致可以分为以下这些: 优先级队列 优先级队列不同于先进先出队列,优先级高的消息具备优先被消费的特权,这样可以为下游提供不同消息级别的保证。不过这个优先级也是需要有一个前提的:如果消费者的消费速度大于生产者的速度,并且消息中间件服务器(一般简单的称之为Broker)中没有消息堆积,那么对于发送的消息设置优先级也就没有什么实质性的意义了,因为生产者刚发送完一条消息就被消费者消费了,那么就相当于Broker中至多只有一条消息,对于单条消息来说优先级是没有什么意义的。 延迟队列 当你在网上购物的时候是否会遇到这样的提示:“三十分钟之内未付款,订单自动取消”?这个是延迟队列的一种典型应用场景。延迟队列存储的是对应的延迟消息,所谓“延迟消息”是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。延迟队列一般分为两种:基于消息的延迟和基于队列的延迟。基于消息的延迟是指为每条消息设置不同的延迟时间,那么每当队列中有新消息进入的时候就会重新根据延迟时间排序,当然这也会对性能造成极大的影响。实际应用中大多采用基于队列的延迟,设置不同延迟级别的队列,比如5s、10s、30s、1min、5mins、10mins等,每个队列中消息的延迟时间都是相同的,这样免去了延迟排序所要承受的性能之苦,通过一定的扫描策略(比如定时)即可投递超时的消息。 死信队列 由于某些原因消息无法被正确的投递,为了确保消息不会被无故的丢弃,一般将其置于一个特殊角色的队列,这个队列一般称之为死信队列。与此对应的还有一个“回退队列”的概念,试想如果消费者在消费时发生了异常,那么就不会对这一次消费进行确认(Ack),进而发生回滚消息的操作之后消息始终会放在队列的顶部,然后不断被处理和回滚,导致队列陷入死循环。为了解决这个问题,可以为每个队列设置一个回退队列,它和死信队列都是为异常的处理提供的一种机制保障。实际情况下,回退队列的角色可以由死信队列和重试队列来扮演。 重试队列 重试队列其实可以看成是一种回退队列,具体指消费端消费消息失败时,为防止消息无故丢失而重新将消息回滚到Broker中。与回退队列不同的是重试队列一般分成多个重试等级,每个重试等级一般也会设置重新投递延时,重试次数越多投递延时就越大。举个例子:消息第一次消费失败入重试队列Q1,Q1的重新投递延迟为5s,在5s过后重新投递该消息;如果消息再次消费失败则入重试队列Q2,Q2的重新投递延迟为10s,在10s过后再次投递该消息。以此类推,重试越多次重新投递的时间就越久,为此需要设置一个上限,超过投递次数就入死信队列。重试队列与延迟队列有相同的地方,都是需要设置延迟级别,它们彼此的区别是:延迟队列动作由内部触发,重试队列动作由外部消费端触发;延迟队列作用一次,而重试队列的作用范围会向后传递。 消费模式 消费模式分为推(push)模式和拉(pull)模式。推模式是指由Broker主动推送消息至消费端,实时性较好,不过需要一定的流制机制来确保服务端推送过来的消息不会压垮消费端。而拉模式是指消费端主动向Broker端请求拉取(一般是定时或者定量)消息,实时性较推模式差,但是可以根据自身的处理能力而控制拉取的消息量。 广播消费 消息一般有两种传递模式:点对点(P2P,Point-to-Point)模式和发布/订阅(Pub/Sub)模式。对于点对点的模式而言,消息被消费以后,队列中不会再存储,所以消息消费者不可能消费到已经被消费的消息。虽然队列可以支持多个消费者,但是一条消息只会被一个消费者消费。发布订阅模式定义了如何向一个内容节点发布和订阅消息,这个内容节点称为主题(topic),主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者则从主题中订阅消息。主题使得消息的订阅者与消息的发布者互相保持独立,不需要进行接触即可保证消息的传递,发布/订阅模式在消息的一对多广播时采用。RabbitMQ是一种典型的点对点模式,而Kafka是一种典型的发布订阅模式。但是RabbitMQ中可以通过设置交换器类型来实现发布订阅模式而达到广播消费的效果,Kafka中也能以点对点的形式消费,你完全可以把其消费组(consumer group)的概念看成是队列的概念。不过对比来说,Kafka中因为有了消息回溯功能的存在,对于广播消费的力度支持比RabbitMQ的要强。 消息回溯 一般消息在消费完成之后就被处理了,之后再也不能消费到该条消息。消息回溯正好相反,是指消息在消费完成之后,还能消费到之前被消费掉的消息。对于消息而言,经常面临的问题是“消息丢失”,至于是真正由于消息中间件的缺陷丢失还是由于使用方的误用而丢失一般很难追查,如果消息中间件本身具备消息回溯功能的话,可以通过回溯消费复现“丢失的”消息进而查出问题的源头之所在。消息回溯的作用远不止与此,比如还有索引恢复、本地缓存重建,有些业务补偿方案也可以采用回溯的方式来实现。 消息堆积+持久化 流量削峰是消息中间件的一个非常重要的功能,而这个功能其实得益于其消息堆积能力。从某种意义上来讲,如果一个消息中间件不具备消息堆积的能力,那么就不能把它看做是一个合格的消息中间件。消息堆积分内存式堆积和磁盘式堆积。RabbitMQ是典型的内存式堆积,但这并非绝对,在某些条件触发后会有换页动作来将内存中的消息换页到磁盘(换页动作会影响吞吐),或者直接使用惰性队列来将消息直接持久化至磁盘中。Kafka是一种典型的磁盘式堆积,所有的消息都存储在磁盘中。一般来说,磁盘的容量会比内存的容量要大得多,对于磁盘式的堆积其堆积能力就是整个磁盘的大小。从另外一个角度讲,消息堆积也为消息中间件提供了冗余存储的功能。援引纽约时报的案例(https://www.confluent.io/blog/publishing-apache-kafka-new-york-times/),其直接将Kafka用作存储系统。 消息追踪 对于分布式架构系统中的链路追踪(trace)而言,大家一定不会陌生。对于消息中间件而言,消息的链路追踪(以下简称消息追踪)同样重要。对于消息追踪最通俗的理解就是要知道消息从哪来,存在哪里以及发往哪里去。基于此功能下,我们可以对发送或者消费完的消息进行链路追踪服务,进而可以进行问题的快速定位与排查。 消息过滤 消息过滤是指按照既定的过滤规则为下游用户提供指定类别的消息。就以kafka而言,完全可以将不同类别的消息发送至不同的topic中,由此可以实现某种意义的消息过滤,或者Kafka还可以根据分区对同一个topic中的消息进行分类。不过更加严格意义上的消息过滤应该是对既定的消息采取一定的方式按照一定的过滤规则进行过滤。同样以Kafka为例,可以通过客户端提供的ConsumerInterceptor接口或者Kafka Stream的filter功能进行消息过滤。 多租户 也可以称为多重租赁技术,是一种软件架构技术,主要用来实现多用户的环境下公用相同的系统或程序组件,并且仍可以确保各用户间数据的隔离性。RabbitMQ就能够支持多租户技术,每一个租户表示为一个vhost,其本质上是一个独立的小型RabbitMQ服务器,又有自己独立的队列、交换器及绑定关系等,并且它拥有自己独立的权限。vhost就像是物理机中的虚拟机一样,它们在各个实例间提供逻辑上的分离,为不同程序安全保密地允许数据,它既能将同一个RabbitMQ中的众多客户区分开,又可以避免队列和交换器等命名冲突。 多协议支持 消息是信息的载体,为了让生产者和消费者都能理解所承载的信息(生产者需要知道如何构造消息,消费者需要知道如何解析消息),它们就需要按照一种统一的格式描述消息,这种统一的格式称之为消息协议。有效的消息一定具有某种格式,而没有格式的消息是没有意义的。一般消息层面的协议有AMQP、MQTT、STOMP、XMPP等(消息领域中的JMS更多的是一个规范而不是一个协议),支持的协议越多其应用范围就会越广,通用性越强,比如RabbitMQ能够支持MQTT协议就让其在物联网应用中获得一席之地。还有的消息中间件是基于其本身的私有协议运转的,典型的如Kafka。 跨语言支持 对很多公司而言,其技术栈体系中会有多种编程语言,如C/C++、JAVA、Go、PHP等,消息中间件本身具备应用解耦的特性,如果能够进一步的支持多客户端语言,那么就可以将此特性的效能扩大。跨语言的支持力度也可以从侧面反映出一个消息中间件的流行程度。 流量控制 流量控制(flow control)针对的是发送方和接收方速度不匹配的问题,提供一种速度匹配服务抑制发送速率使接收方应用程序的读取速率与之相适应。通常的流控方法有Stop-and-wait、滑动窗口以及令牌桶等。 消息顺序性 顾名思义,消息顺序性是指保证消息有序。这个功能有个很常见的应用场景就是CDC(Change Data Chapture),以MySQL为例,如果其传输的binlog的顺序出错,比如原本是先对一条数据加1,然后再乘以2,发送错序之后就变成了先乘以2后加1了,造成了数据不一致。 安全机制 在Kafka 0.9版本之后就开始增加了身份认证和权限控制两种安全机制。身份认证是指客户端与服务端连接进行身份认证,包括客户端与Broker之间、Broker与Broker之间、Broker与ZooKeeper之间的连接认证,目前支持SSL、SASL等认证机制。权限控制是指对客户端的读写操作进行权限控制,包括对消息或Kafka集群操作权限控制。权限控制是可插拔的,并支持与外部的授权服务进行集成。对于RabbitMQ而言,其同样提供身份认证(TLS/SSL、SASL)和权限控制(读写操作)的安全机制。 消息幂等性 对于确保消息在生产者和消费者之间进行传输而言一般有三种传输保障(delivery guarantee):At most once,至多一次,消息可能丢失,但绝不会重复传输;At least once,至少一次,消息绝不会丢,但是可能会重复;Exactly once,精确一次,每条消息肯定会被传输一次且仅一次。对于大多数消息中间件而言,一般只提供At most once和At least once两种传输保障,对于第三种一般很难做到,由此消息幂等性也很难保证。 Kafka自0.11版本开始引入了幂等性和事务,Kafka的幂等性是指单个生产者对于单分区单会话的幂等,而事务可以保证原子性地写入到多个分区,即写入到多个分区的消息要么全部成功,要么全部回滚,这两个功能加起来可以让Kafka具备EOS(Exactly Once Semantic)的能力。 不过如果要考虑全局的幂等,还需要与从上下游方面综合考虑,即关联业务层面,幂等处理本身也是业务层面所需要考虑的重要议题。以下游消费者层面为例,有可能消费者消费完一条消息之后没有来得及确认消息就发生异常,等到恢复之后又得重新消费原来消费过的那条消息,那么这种类型的消息幂等是无法有消息中间件层面来保证的。如果要保证全局的幂等,需要引入更多的外部资源来保证,比如以订单号作为唯一性标识,并且在下游设置一个去重表。 事务性消息 事务本身是一个并不陌生的词汇,事务是由事务开始(Begin Transaction)和事务结束(End Transaction)之间执行的全体操作组成。支持事务的消息中间件并不在少数,Kafka和RabbitMQ都支持,不过此两者的事务是指生产者发生消息的事务,要么发送成功,要么发送失败。消息中间件可以作为用来实现分布式事务的一种手段,但其本身并不提供全局分布式事务的功能。 下表是对Kafka与RabbitMQ功能的总结性对比及补充说明: 功能项 Kafka(1.1.0版本) RabbitMQ(3.6.10版本) 优先级队列 不支持 支持。建议优先级大小设置在0-10之间。 延迟队列 不支持 支持 死信队列 不支持 支持 重试队列 不支持 不支持。RabbitMQ中可以参考延迟队列实现一个重试队列,二次封装比较简单。如果要在Kafka中实现重试队列,首先得实现延迟队列的功能,相对比较复杂。 消费模式 推模式 推模式+拉模式 广播消费 支持。Kafka对于广播消费的支持相对而言更加正统。 支持,但力度较Kafka弱。 消息回溯 支持。Kafka支持按照offset和timestamp两种维度进行消息回溯。 不支持。RabbitMQ中消息一旦被确认消费就会被标记删除。 消息堆积 支持 支持。一般情况下,内存堆积达到特定阈值时会影响其性能,但这不是绝对的。如果考虑到吞吐这因素,Kafka的堆积效率比RabbitMQ总体上要高很多。 持久化 支持 支持 消息追踪 不支持。消息追踪可以通过外部系统来支持,但是支持粒度没有内置的细腻。 支持。RabbitMQ中可以采用Firehose或者rabbitmq_tracing插件实现。不过开启rabbitmq_tracing插件件会大幅影响性能,不建议生产环境开启,反倒是可以使用Firehose与外部链路系统结合提供高细腻度的消息追踪支持。 消息过滤 客户端级别的支持 不支持。但是二次封装一下也非常简单。 多租户 不支持 支持 多协议支持 只支持定义协议,目前几个主流版本间存在兼容性问题。 RabbitMQ本身就是AMQP协议的实现,同时支持MQTT、STOMP等协议。 跨语言支持 采用Scala和Java编写,支持多种语言的客户端。 采用Erlang编写,支持多种语言的客户端。 流量控制 支持client和user级别,通过主动设置可将流控作用于生产者或消费者。 RabbitMQ的流控基于Credit-Based算法,是内部被动触发的保护机制,作用于生产者层面。 消息顺序性 支持单分区(partition)级别的顺序性。 顺序性的条件比较苛刻,需要单线程发送、单线程消费并且不采用延迟队列、优先级队列等一些高级功能,从某种意义上来说不算支持顺序性。 安全机制 (TLS/SSL、SASL)身份认证和(读写)权限控制 与Kafka相似 幂等性 支持单个生产者单分区单会话的幂等性。 不支持 事务性消息 支持 支持 2. 性能 功能维度是消息中间件选型中的一个重要的参考维度,但这并不是唯一的维度。有时候性能比功能还要重要,况且性能和功能很多时候是相悖的,鱼和熊掌不可兼得,Kafka在开启幂等、事务功能的时候会使其性能降低,RabbitMQ在开启rabbitmq_tracing插件的时候也会极大的影响其性能。消息中间件的性能一般是指其吞吐量,虽然从功能维度上来说,RabbitMQ的优势要大于Kafka,但是Kafka的吞吐量要比RabbitMQ高出1至2个数量级,一般RabbitMQ的单机QPS在万级别之内,而Kafka的单机QPS可以维持在十万级别,甚至可以达到百万级。 消息中间件的吞吐量始终会受到硬件层面的限制。就以网卡带宽为例,如果单机单网卡的带宽为1Gbps,如果要达到百万级的吞吐,那么消息体大小不得超过(1Gb/8)/100W,即约等于134B,换句话说如果消息体大小超过134B,那么就不可能达到百万级别的吞吐。这种计算方式同样可以适用于内存和磁盘。 时延作为性能维度的一个重要指标,却往往在消息中间件领域所被忽视,因为一般使用消息中间件的场景对时效性的要求并不是很高,如果要求时效性完全可以采用RPC的方式实现。消息中间件具备消息堆积的能力,消息堆积越大也就意味着端到端的时延也就越长,与此同时延时队列也是某些消息中间件的一大特色。那么为什么还要关注消息中间件的时延问题呢?消息中间件能够解耦系统,对于一个时延较低的消息中间件而言,它可以让上游生产者发送消息之后可以迅速的返回,也可以让消费者更加快速的获取到消息,在没有堆积的情况下可以让整体上下游的应用之间的级联动作更加高效,虽然不建议在时效性很高的场景下使用消息中间件,但是如果所使用的消息中间件的时延方面比较优秀,那么对于整体系统的性能将会是一个不小的提升。 3. 可靠性+可用性 消息丢失是使用消息中间件时所不得不面对的一个同点,其背后消息可靠性也是衡量消息中间件好坏的一个关键因素。尤其是在金融支付领域,消息可靠性尤为重要。然而说到可靠性必然要说到可用性,注意这两者之间的区别,消息中间件的可靠性是指对消息不丢失的保障程度;而消息中间件的可用性是指无故障运行的时间百分比,通常用几个9来衡量。 从狭义的角度来说,分布式系统架构是一致性协议理论的应用实现,对于消息可靠性和可用性而言也可以追溯到消息中间件背后的一致性协议。对于Kafka而言,其采用的是类似PacificA的一致性协议,通过ISR(In-Sync-Replica)来保证多副本之间的同步,并且支持强一致性语义(通过acks实现)。对应的RabbitMQ是通过镜像环形队列实现多副本及强一致性语义的。多副本可以保证在master节点宕机异常之后可以提升slave作为新的master而继续提供服务来保障可用性。Kafka设计之初是为日志处理而生,给人们留下了数据可靠性要求不要的不良印象,但是随着版本的升级优化,其可靠性得到极大的增强,详细可以参考KIP101。就目前而言,在金融支付领域使用RabbitMQ居多,而在日志处理、大数据等方面Kafka使用居多,随着RabbitMQ性能的不断提升和Kafka可靠性的进一步增强,相信彼此都能在以前不擅长的领域分得一杯羹。 同步刷盘是增强一个组件可靠性的有效方式,消息中间件也不例外,Kafka和RabbitMQ都可以支持同步刷盘,但是笔者对同步刷盘有一定的疑问:绝大多数情景下,一个组件的可靠性不应该由同步刷盘这种极其损耗性能的操作来保障,而是采用多副本的机制来保证。 这里还要提及的一个方面是扩展能力,这里我狭隘地将此归纳到可用性这一维度,消息中间件的扩展能力能够增强其用可用能力及范围,比如前面提到的RabbitMQ支持多种消息协议,这个就是基于其插件化的扩展实现。还有从集群部署上来讲,归功于Kafka的水平扩展能力,其基本上可以达到线性容量提升的水平,在LinkedIn实践介绍中就提及了有部署超过千台设备的Kafka集群。 5. 运维管理 在消息中间件的使用过程中难免会出现各式各样的异常情况,有客户端的,也有服务端的,那么怎样及时有效的进行监测及修复。业务线流量有峰值又低谷,尤其是电商领域,那么怎样前进行有效的容量评估,尤其是大促期间?脚踢电源、网线被挖等事件层出不穷,如何有效的做好异地多活?这些都离不开消息中间件的衍生产品——运维管理。 运维管理也可以进行进一步的细分,比如:申请、审核、监控、告警、管理、容灾、部署等。 申请、审核很好理解,在源头对资源进行管控,既可以进行有效校正应用方的使用规范,配和监控也可以做好流量统计与流量评估工作,一般申请、审核与公司内部系统交融性较大,不适合使用开源类的产品。 监控、告警也比较好理解,对消息中间件的使用进行全方位的监控,即可以为系统提供基准数据,也可以在检测到异常的情况配合告警,以便运维、开发人员的迅速介入。除了一般的监控项(比如硬件、GC等)之外,对于消息中间件还需要关注端到端时延、消息审计、消息堆积等方面。对于RabbitMQ而言,最正统的监控管理工具莫过于rabbitmq_management插件了,但是社区内还有AppDynamics, Collectd, DataDog, Ganglia, Munin, Nagios, New Relic, Prometheus, Zenoss等多种优秀的产品。Kafka在此方面也毫不逊色,比如:Kafka Manager, Kafka Monitor, Kafka Offset Monitor, Burrow, Chaperone, Confluent Control Center等产品,尤其是Cruise还可以提供自动化运维的功能。 不管是扩容、降级、版本升级、集群节点部署、还是故障处理都离不开管理工具的应用,一个配套完备的管理工具集可以在遇到变更时做到事半功倍。故障可大可小,一般是一些应用异常,也可以是机器掉电、网络异常、磁盘损坏等单机故障,这些故障单机房内的多副本足以应付。如果是机房故障就要涉及异地容灾了,关键点在于如何有效的进行数据复制,对于Kafka而言,可以参考MirrorMarker、uReplicator等产品,而RabbitMQ可以参考Federation和Shovel。 6. 社区力度及生态发展 对于目前流行的编程语言而言,如Java、Python,如果你在使用过程中遇到了一些异常,基本上可以通过搜索引擎的帮助来得到解决,因为一个产品用的人越多,踩过的坑也就越多,对应的解决方案也就越多。对于消息中间件也同样适用,如果你选择了一种“生僻”的消息中间件,可能在某些方面运用的得心应手,但是版本更新缓慢、遇到棘手问题也难以得到社区的支持而越陷越深;相反如果你选择了一种“流行”的消息中间件,其更新力度大,不仅可以迅速的弥补之前的不足,而且也能顺应技术的快速发展来变更一些新的功能,这样可以让你以“站在巨人的肩膀上”。在运维管理维度我们提及了Kafka和RabbitMQ都有一系列开源的监控管理产品,这些正是得益于其社区及生态的迅猛发展。 四、消息中间件选型误区探讨 在进行消息中间件选型之前可以先问自己一个问题:是否真的需要一个消息中间件?在搞清楚这个问题之后,还可以继续问自己一个问题:是否需要自己维护一套消息中间件?很多初创型公司为了节省成本会选择直接购买消息中间件有关的云服务,自己只需要关注收发消息即可,其余的都可以外包出去。 很多人面对消息中间件时会有一种自研的冲动,你完全可以对Java中的ArrayBlockingQueue做一个简单的封装,你也可以基于文件、数据库、Redis等底层存储封装而形成一个消息中间件。消息中间件做为一个基础组件并没有想象中的那么简单,其背后还需要配套的管理运维整个生态的产品集。自研还有会交接问题,如果文档不齐全、运作不规范将会带给新人噩梦般的体验。是否真的有自研的必要?如果不是KPI的压迫可以先考虑下这2个问题:1. 目前市面上的消息中间件是否都真的无法满足目前业务需求? 2. 团队是否有足够的能力、人力、财力、精力来支持自研? 很多人在做消息中间件选型时会参考网络上的很多对比类的文章,但是其专业性、严谨性、以及其政治立场问题都有待考证,需要带着怀疑的态度去审视这些文章。比如有些文章会在没有任何限定条件及场景的情况下直接定义某款消息中间件最好,还有些文章没有指明消息中间件版本及测试环境就来做功能和性能对比分析,诸如此类的文章都可以唾弃之。 消息中间件犹如小马过河,选择合适的才最重要,这需要贴合自身的业务需求,技术服务于业务,大体上可以根据上一节所提及的功能、性能等6个维度来一一进行筛选。更深层次的抉择在于你能否掌握其魂,笔者鄙见:RabbitMQ在于routing,而Kafka在于streaming,了解其根本对于自己能够对症下药选择到合适的消息中间件尤为重要。 消息中间件选型切忌一味的追求性能或者功能,性能可以优化,功能可以二次开发。如果要在功能和性能方面做一个抉择的话,那么首选性能,因为总体上来说性能优化的空间没有功能扩展的空间大。然而对于长期发展而言,生态又比性能以及功能都要重要。 很多时候,对于可靠性方面也容易存在一个误区:想要找到一个产品来保证消息的绝对可靠,很不幸的是这世界上没有绝对的东西,只能说尽量趋于完美。想要尽可能的保障消息的可靠性也并非单单只靠消息中间件本身,还要依赖于上下游,需要从生产端、服务端和消费端这3个维度去努力保证,《RabbitMQ消息可靠性分析》这篇文章就从这3个维度去分析了RabbitMQ的可靠性。 消息中间件选型还有一个考量标准就是尽量贴合团队自身的技术栈体系,虽然说没有蹩脚的消息中间件只有蹩脚的程序员,但是让一个C栈的团队去深挖PhxQueue总比去深挖Scala编写的Kafka要容易的多。 五、总结 消息中间件大道至简:一发一存一消费,没有最好的消息中间件,只有最合适的消息中间件。人过留名,雁过留声,路过记得点个赞。
1. 概述 多任务和高并发是衡量一台计算机处理器的能力重要指标之一。一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions Per Second,TPS)这个指标比较能说明问题,它代表着一秒内服务器平均能响应的请求数,而TPS值与程序的并发能力有着非常密切的关系。在讨论Java内存模型和线程之前,先简单介绍一下硬件的效率与一致性。 2.硬件的效率与一致性 由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中没这样处理器就无需等待缓慢的内存读写了。 基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主存,如下图所示:多个处理器运算任务都涉及同一块主存,需要一种协议可以保障数据的一致性,这类协议有MSI、MESI、MOSI及Dragon Protocol等。Java虚拟机内存模型中定义的内存访问操作与硬件的缓存访问操作是具有可比性的,后续将介绍Java内存模型。 image 除此之外,为了使得处理器内部的运算单元能竟可能被充分利用,处理器可能会对输入代码进行乱起执行(Out-Of-Order Execution)优化,处理器会在计算之后将对乱序执行的代码进行结果重组,保证结果准确性。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Recorder)优化。 3.Java内存模型 定义Java内存模型并不是一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并发操作不会产生歧义;但是,也必须得足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度。经过长时间的验证和修补,在JDK1.5发布后,Java内存模型就已经成熟和完善起来了。 3.1 主内存与工作内存 Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。 Java内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(可以与前面将的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,线程、主内存和工作内存的交互关系如下图所示,和上图很类似。 image 这里的主内存、工作内存与Java内存区域的Java堆、栈、方法区不是同一层次内存划分。 3.2 内存间交互操作 关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成: lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。 unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用 load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。 use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。 assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。 write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则: 不允许read和load、store和write操作之一单独出现 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。 3.3 重排序 在执行程序时为了提高性能,编译器和处理器经常会对指令进行重排序。重排序分成三种类型: 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 从Java源代码到最终实际执行的指令序列,会经过下面三种重排序: image 为了保证内存的可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。Java内存模型把内存屏障分为LoadLoad、LoadStore、StoreLoad和StoreStore四种: image 3.4 同步机制 介绍volatile、synchronized和final 3.5 原子性、可见性与有序性 介绍三个特性 欢迎工作一到五年的Java程序员朋友们加入Java架构交流:810589193 本群提供免费的学习指导 架构资料 以及免费的解答 不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导
2018年,我的经历 整个互联网行业都在裁员1/3,我也是亲历者。 我想弄清楚为什么?所谓的行业凛冬,具体是什么。 是真的来了?或是老板们在忽悠我们? 找到原因,我希望反思,2019年可以不会被裁员。 现在,我有了答案,和你分享。 烧脑准备哈~~~ 开始! 寒流:2014 - 被高估的创业公司 2014年,互联网行业蓬勃发展。 各大公司(例如:阿里巴巴)争先上市。 为了让企业上市的估值更高,大公司需要收购更多创业公司。 于是,创业公司的价值被炒了起来。 不挣钱,也估值千万的创业公司多如牛毛。 发酵:2015-2017 - 被高估的员工工资 互联网创业的热潮,一波接着一波。 今天的创意,恨不得明天就要实现。 专注,机制,口碑,快。成为行业的口号,尤其是快! 快就需要更多的人才,每一家公司都需要。 于是行业内员工的工资急速飙升。 我们还发现,涨工资最快的办法,不是提升能力, 而是跳槽。 这是什么情况:员工的工资被高估,能力和薪资不匹配。 当时出现了:1个员工2个月,360 - 阿里 - 腾讯 - 360 走一圈,工资涨了3倍。 梦碎:2018 - 没有最后一轮融资的恶性循环 2018年,美团,小米,阿里等公司股价持续走低。 惨的时候,上市的股价,比最后一轮融资还少一半。 于是投资者们发现:最后一轮投资是亏钱的。 于是创业者们发现:最后一轮每人投钱。 于是投资者们发现:由于最后一轮没人投,C 轮就是最后一轮。 于是创业者们发现:C 轮也没人投。 以此恶性循环,现在天使轮都不太好找了。 老板不忽悠:资本寒冬来了 因为投资人都非常的慎重,没有那么大方了,甚至非常小气。 那么,公司就要过冬了。 没有钱扩展业务,那就要做专注的业务。 原来过多的人力成本,现在就要抛弃,不然跑不下去。 于是,裁员来了。问题是:抛弃谁? 问题的根本问题是:员工的工资被高估,能力和薪资不匹配。 解决根本问题的办法是:抛弃能力和薪资不匹配的员工。 于是,就有了一个裁员剃刀法则: 有贡献(业绩)的 有实力(综合资源)的 ——下面可以被裁 —— 有能力的 有潜力的 于是,有人被裁掉了 有能力和有潜力的兄弟被裁掉了,可惜的不要不要的。 留下了有贡献和有实力的兄弟,吓的不要不要的。 这就是寒冬裁员的大背景。 你被裁了吗? 反思一下 其实,我们被裁的兄弟,我知道的几个,已经在很好的单位继续奋斗。有能力的人,在哪都不会吃亏。这是次对行业的调整也是一件好事。 但是,裁员剃刀法则,足以让我们反思一下。 为什么有能力,和有潜力的员工会被裁员? 如果不改变,再裁员,还会裁你吗? 2019年,你要成长到什么高度? 从事java十余年,现在把架构师必须具备的一些技术总结出来一套思维导图和录制了一些相关视频,分享给大家,供大家参考。 需要相关资料可以加群:810589193,点击链接加入群聊【Java架构学习交流群】:https://jq.qq.com/?_wv=1027&k=5deQUBl 开源框架 程序员每天都和代码打交道。经过数年的基础教育和职业培训,大部分程序员都会「写」代码,或者至少会抄代码和改代码。但是,会读代码的并不在多数,会读代码又真正读懂一些大项目的源码的,少之又少。这也造成了很多错误看源码的方式。 那要如何正确的分析源码呢? 高性能 随着我们的业务量越来越大和越重要,单体的架构模式已经无法对应大规模的应用场景,而且系统中决不能存在单点故障导致整体不可用,所以只有垂直或是水平拆分业务系统,使其形成一个分布式的架构,利用分布式架构来冗余系统消除单点的故障,从而提高整个系统的可用性。同时高性能系统的模块重用度更高,速度更快,扩展性更高是大型的项目必不可少的环节。 微服务 关于微服务架构的取舍 1、在合适的项目,合适的团队,采用微服务架构收益会大于成本。 2、微服务架构有很多吸引人的地方,但在拥抱微服务之前,也需要认清它所带来的挑战。 3、需要避免为了“微服务”而“微服务”。 4、微服务架构引入策略 – 对传统企业而言,开始时可以考虑引入部分合适的微服务架构原则对已有系统进行改造或新建微服务应用,逐步探索及积累微服务架构经验,而非全盘实施微服务架构。 架构师筑基 我们不仅仅对项目要运筹帷幄,还要能解决一切性能问题。只有深入学习JVM底层原理,mysql底层优化以及Tomcat调优,才能达到知其然,知其所以然的效果。除了性能优化之外,也能提供通用的常见思路以及方案选型的考虑点,帮助大家培养在方案选型时的意识、思维以及做各种权衡的能力。 并发编程 主要培养编程者深入了解最底层的运作原理,加强编程者逻辑思维,这样才能写出高效、安全、可靠的多线程并发程序。 团队协作 所谓团队合作能力,是指建立在团队的基础之上,发挥团队精神、互补互助以达到团队最大工作效率的能力。对于团队的成员来说,不仅要有个人能力,更需要有在不同的位置上各尽所能、与其他成员协调合作的能力。 项目实战 要想立足于互联网公司,且能在互联网浪潮中不被淹没,对于项目的开发实战演练是不必可少的技能,也是对自身能力的一个衡量,有多少的量对等于获得多少的回报。看似简单的一个项目需求图谱,其中的底层原理,实现原理又能知道多少?
我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。
Spring 简介 Spring 是一种用来简化企业级应用开发的开源框架,包括Spring Framework, Spring Data, Spring Security,Spring Boot,SpringMVC等。Spring 家族最核心的概念当属 AOP 和 IoC,详解见下节。其中 Spring 优点如下: 降低了组件之间的耦合性 ,实现了软件各层之间的解耦 可以使用便捷的众多服务,如事务管理,消息服务等 容器提供了AOP技术,利用它很容易实现如权限拦截,运行期监控等功能 Spring对于主流的应用框架提供了集成支持,如Hibernate、JPA等 Spring属于低侵入式设计,代码的污染极低 Spring的高度开放性,开发者可以自由选择Spring的部分或全部 AOP和IOC AOP(Aspect Oriented Programming,面向切面编程) AOP简单说就是在目标方法执行前后自定义一些操作,一般都是基于代理模式来实现的,Spring支持两种代理模式,JDK原生代理和CGLib代理。AOP给程序带来良好的扩展性和封装性,可以实现业务代码与非业务代码的隔离。比如可以在不改变目标代码的前提下实现目标方法的增强:埋点业务处理、方法执行时间监控,打印日志,权限控制等等。 JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。只能对实现了接口的类生成代理。 CGLib动态代理是利用ASM开源包,对代理对象类的Class文件加载进来,通过修改其字节码生成子类来处理。 切面(Aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象。 切点(Pointcut):对连接点进行拦截的定义。 连接点(Joinpoint):被拦截到的点,比如方法(Spring中一般是方法)、字段、构造器。 通知(Advice):指拦截到连接点后要执行的代码,通知分为前置、后置、异常、最终、环绕五类。 AOP:在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想 IoC(Inversion of Control,控制反转) IOC (Inversion of Control,控制反转):对象之间的依赖关系由容器来创建。本来对象之间的关系是由开发者自己创建和维护的,在使用Spring框架后,对象之间的关系由容器来创建和维护,将开发者做的事让容器做,这就是控制反转。BeanFactory接口是Spring Ioc容器的核心接口。 DI (Dependecy Injection,依赖注入):我们在使用Spring容器的时候,容器通过调用set方法或者是构造器来建立对象之间的依赖关系。注入方式有设值注入、构造注入、注解注入、接口注入(基本不用),设值注入直观,自然;构造注入可以在构造器中决定依赖关系的顺序。 控制反转是目标,依赖注入是我们实现控制反转的一种手段。 SpringMVC和Struts SpringMVC执行流程如下: 客户端向Spring容器发起一个HTTP请求。 发起的请求被前端控制器(DispatcherServlet)拦截。 查询处理器映射(HandlerMapping)得到执行链,并请求相应的处理器适配器(HandlerAdapter)。 执行处理器(Handler)并处理请求,以ModelAndView(属性值和返回页面)的形式返回。此处Handler即平时编写的Controller。 前端控制器查询视图解析器(ViewResolver),并返回View。 成功渲染视图则返回给客户端,否则抛异常。 比较点SpringMVCStruts核心控制器DispatcherServletFilterDispatcher配置文件量少(AOP)量大(Interceptor机制)RESTful API易实现(方法级别)实现费劲(类级别)处理Ajax请求@ResponseBody返回响应文本拦截器集成Ajax性能稍快稍慢 DispatcherServlet初始化流程如下: 加载配置文件 扫描所有的相关类 初始化所有相关类的Class实例,并将其保存到IoC容器 自动化的依赖注入(Autowired) 初始化HandlerMapping Spring事务 数据库事务是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。事务满足原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability) 四大特性。 事务隔离级别 数据读的三类问题: 问题解释脏读事务 A 读取了事务 B 未提交的数据(发现读到的数据是脏数据)不可重复读事务 A 读取了事务 B 已提交的更改数据(发现与前一次读的不一致)幻读事务 A 读取了事务 B 已提交的新增数据(发现之前没有这条数据的) 事务的四种隔离级别见下表,MySQL默认为REPEATABLE_READ: 隔离级别(简写)含义隐患READ_UNCOMMITTED允许读未提交脏读、不可重复读、幻读READ_COMMITTED允许读已提交不可重复读、幻读REPEATABLE_READ允许重复读幻读SERIALIZABLE序列化读– 事务管理 Spring事务管理器的接口是PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager、JpaTransactionManager)等都提供了对应的事务管理器。 事务的传播特性 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播,根据如下代码中方法 A 有无事务,Spring定义了7种传播行为(默认为REQUIRED): @Transactionalvoid A(){ }@Transactionalvoid B(){A(); } 传播行为(简写)含义REQUIRED如果没有,就开启一个事务;如果有,就加入当前事务(方法A加入到方法B)REQUIRES_NEW如果没有,就开启一个事务;如果有,就将当前事务挂起NESTED如果没有,就开启一个事务;如果有,就在当前事务中嵌套其他事务(主事务提交或回滚,子事务也会提交或回滚)SUPPORTS如果没有,就以非事务方式执行;如果有,就使用当前事务NOT_SUPPORTED如果没有,就以非事务方式执行;如果有,就将当前事务挂起NEVER如果没有,就以非事务方式执行;如果有,就抛出异常MANDATORY如果没有,就抛出异常;如果有,就使用当前事务 Spring Boot Spring Boot来自于 Spring 大家族,是一套全新的框架,它默认帮我们进行了很多配置,集成了大量常用的第三方库(例如 Jackson、JDBC、MongoDB、Redis、Mail 等),这些第三方库几乎都可以开箱即用。Spring Boot可以帮助我们快速搭建一个项目,从而让开发者能够更加专注于业务逻辑。 Spring扩展 实现BeanPostProcess接口在Bean生成前后进行操作。 实现BeanFactoryPostProcessor接口配置Bean元属性。 实现FactoryBean接口定制个性化的Bean。 MyBatis MyBatis是一款优秀的持久层框架,它几乎避免了所有的JDBC代码和手动设置参数以及获取结果集,它可以使用XML或注解来将接口和POJO映射成数据库中的记录。 MyBatis与Spring集成的时候Spring提供了全局唯一的SqlSessionTemplate,SqlSessionTemplate 实现了SqlSession接口,那么如何保证多个线程调用同一个dao时拿到的SqlSession不会错乱呢?这个时候就用到了ThreadLocal,SqlSessionUtils.getSqlSession()会首先查看当前线程资源map有无SqlSession,有则返回无则新建然后返回,这样就能保证一条业务始终用的是同一个数据库连接,也就能正确处理数据库事务。 MyBatis和Hibernate 比较点MyBatisHibernate特点半自动(手写SQL)全自动(根据映射生成SQL)SQL直接优化方便复杂数据库移植性弱强缓存机制欠缺更优日志系统欠缺完整 Statement和PreparedStatement的区别 /***PreparedStatement extends Statement***///Statement用法sql1 ="select * from tbl_user where username='"+ u +"' and password='"+ p +"'";statement = conn.createStatement(); result1 = statement.executeQuery(sql1); //PrepareStatement用法sql2 ="select * from tbl_user where username=? and password=?"; prepareStatement = conn.prepareStatement(sql2); pstmt.setString(1, u); pstmt.setString(2, p); result2 = prepareStatement.executeQuery(); 比较点StatementPreparedStatement用途执行静态SQL语句并返回结果执行已预编译SQL语句并返回结果可读性低(字符串拼接)高(Set方法设值)效率低(字符串拼接)高(占位符)安全性低(SQL注入)高 消息队列 消息队列中间件是分布式系统中重要的组件,主要主要解决应用耦合、异步消息、流量削锋等问题,具有异步性、可靠性(存储到本地硬盘)、松耦合、分布式的特性。 主要特点是异步处理 主要目的是减少请求响应时间、解耦 主要使用场景是将比较耗时且不需同步返回结果的操作当做消息存入队列 流量削峰的一种解决方案 MQ推送模式改为定时或者批量拉取模式 消息接收方实现批量处理等方式 RabbitMQ RabbitMQ 是一个由 ErLang 开发的AMQP的开源实现。 交换机(Exchange)的功能主要是接收消息并且转发到绑定的队列,交换机不存储消息,交换机本质是一张路由查询表。 Direct:绑定时设定一个路由键,消息的路由键匹配才会被投送到队列中。 Topic:根据模糊匹配转发消息(最灵活)。 Headers:设置头部参数类型的交换机。 Fanout:转发消息到所有绑定队列,消息广播的模式。 路由键(routing_key)在消息中,而绑定键(binding_key)作用于交换机和队列之间。当消息中的路由键和绑定键对应上的时候,交换机就知道将该消息存入哪个队列。 分布式 分布式:一个业务分拆多个子业务,部署在不同的服务器上(厨师和配菜师的关系) 集群:同一个业务,部署在多个服务器上(两个厨师的关系) 微服务 微服务架构风格是一种使用一套小服务来开发单个应用的方式,每个服务运行在自己的进程中,并使用轻量级机制通信(通常是HTTP API),这些服务能够通过自动化部署机制来独立部署、可以使用不同的编程语言实现、可以使用不同的数据存储技术,并保持最低限度的集中式管理。 时下热门的微服务开发框架有:Spring Cloud、Dubbo RESTful URL定位资源,HTTP动词描述操作 GET:读取资源 POST:创建资源 PUT:更新资源 DELETE:删除资源 使用PUT方式更新时,必须发送资源所有的属性 Nginx 反向代理 正向代理:隐藏真实的请求客户端,服务端不知道真实的客户端是谁,正向代理服务器会代替客户端向服务器发送请求。正向代理代理的对象是客户端。 反向代理:隐藏真实的响应服务端,客户端不知道真实的服务端是谁,反向代理服务器会把请求转发到真实的服务器。反向代理代理的对象是服务端。 10086总机就是一种反向代理,客户不知道真正提供服务人的是谁。 负载均衡 四层负载均衡:工作在OSI模型的传输层,它在接收到客户端的流量以后通过修改数据包的地址信息将流量转发到应用服务器,因此四层负载均衡的主要工作就是转发。 七层负载均衡:工作在OSI模型的应用层,七层负载均衡在接到客户端的流量以后,还需要一个完整的TCP/IP协议栈与客户端建立一条完整的连接,并将应用层的请求流量解析出来,再按照调度算法选择一个应用服务器,并与应用服务器建立另外一条连接将请求发送过去,因此七层负载均衡的主要工作就是代理。 设计模式 设计模式的六大原则 单一职责原则:一个类只负责一个功能领域中的相应职责。 开闭原则:一个软件实体应当对扩展开放,对修改关闭。 里氏替换原则:所有引用父类的地方必须能透明地使用其子类的对象。 依赖倒置原则:抽象不应该依赖于细节,细节应当依赖于抽象。(要针对接口编程,而不是针对实现编程)。 接口隔离原则:使用多个专门的接口,而不使用单一的总接口。 迪米特法则:一个软件实体应当尽可能少地与其他实体发生相互作用。 单例、工厂、观察者、适配器、责任链 单例模式:一个类负责创建自己的对象,同时确保只有单个对象被创建,并提供一种访问其唯一对象的方式。 工厂模式:在创建对象时不暴露创建逻辑,并通过使用一个共同的接口来指向新创建的对象。 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。 适配器模式:负责加入独立的或不兼容的接口功能的类,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 责任链模式:为请求创建了一个接收者对象的链,沿着这条链传递请求,直到有对象处理它为止,对请求的发送者和接收者进行解耦。 写出生产者消费者模式 生产者生产数据到缓冲区中,消费者从缓冲区中取数据。如果缓冲区已经满了,则生产者线程阻塞;如果缓冲区为空,那么消费者线程阻塞。 publicclassProducerAndConsumer{ staticBlockingQueueresourceQueue =newLinkedBlockingQueue(10); public static void main(String[] args) {Producerp =newProducer();//生产者Consumerc1 =newConsumer();//消费者1Consumerc2 =newConsumer();//消费者2Consumerc3 =newConsumer();//消费者3p.start(); c1.start(); c2.start(); c3.start(); }}/** * 资源 */classResource{ int id; publicResource(int id) {this.id = id; }}/** * 生产者 */classProducerextendsThread{ int p =1;@Overridepublic void run() {while(true) {try{Resourceresource =newResource(p++);System.out.println("生产资源"+ resource.id);ProducerAndConsumer.resourceQueue.put(resource); }catch(InterruptedExceptione) { e.printStackTrace(); } } }}/** * 消费者 */classConsumerextendsThread{@Overridepublic void run() {while(true) {try{System.out.println("消费资源"+ ((Resource)ProducerAndConsumer.resourceQueue.take()).id); }catch(InterruptedExceptione) { e.printStackTrace(); } } }} 高内聚、低耦合 耦合性:也称块间联系。指软件各模块之间相互联系紧密程度的一种度量。模块之间联系越紧密,则其耦合性就越强。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息。 内聚性:也称块内联系。指软件模块内部各元素彼此结合紧密程度的一种度量。模块内各元素(语名之间、程序段之间)联系越紧密,则其内聚性就越高。 高内聚、低耦合的系统具有更好的重用性,维护性,扩展性,可以更高效的完成系统的维护开发,持续的支持业务的发展,而不会成为业务发展的障碍 最后提供一个小福利,对于想要跳槽 换工作的可以加群:960439918,点击链接加入群聊【java高级架构交流群】:https://jq.qq.com/?_wv=1027&k=5fozFzF可以领取免费的架构师学习资料;了解最新的学习动态;了解最新的阿里、京东招聘资讯;获取更多的面试资料。 1、具有1-5工作经验的,面对目前流行的技术不知从何下手, 需要突破技术瓶颈的。2、在公司待久了,过得很安逸, 但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的。 3、如果没有工作经验,但基础非常扎实,对java工作机制, 常用设计思想,常用java开发框架掌握熟练的。 4、觉得自己很牛B,一般需求都能搞定。 但是所学的知识点没有系统化,很难在技术领域继续突破的。
Java面试笔试题大汇总一(最全+详细答案):https://www.jianshu.com/p/73b6b3d35676 Java面试笔试题大汇总二(最全+详细答案)https://www.jianshu.com/p/f5120f1b75be 101、常用的Web服务器有哪些? 102、JSP和Servlet是什么关系? 103、讲解JSP中的四种作用域。 104、如何实现JSP或Servlet的单线程模式? 105、实现会话跟踪的技术有哪些? 106、过滤器有哪些作用和用法? 107、监听器有哪些作用和用法? 108、web.xml文件中可以配置哪些内容? 109、你的项目中使用过哪些JSTL标签? 110、使用标签库有什么好处?如何自定义JSP标签? 111、说一下表达式语言(EL)的隐式对象及其作用。 112、表达式语言(EL)支持哪些运算符? 113、Java Web开发的Model 1和Model 114、Servlet 3中的异步处理指的是什么? 115、如何在基于Java的Web项目中实现文件上传和下载? 116、服务器收到用户提交的表单数据,到底是调用Servlet的doGet()还是doPost()方法? 117、JSP中的静态包含和动态包含有什么区别? 118、Servlet中如何获取用户提交的查询参数或表单数据? 119、Servlet中如何获取用户配置的初始化参数以及服务器上下文参数? 120、如何设置请求的编码以及响应内容的类型? 121、如何在Web项目中配置Spring的IoC容器? 122、如何在Web项目中配置Spring MVC? 123、Spring MVC的工作原理是怎样的? 124、如何配置配置事务增强? 125、选择使用Spring框架的原因(Spring框架为企业级开发带来的好处有哪些)? 126、SpringIoC容器配置Bean的方式? 127、阐述Spring框架中Bean的生命周期? 128、依赖注入时如何注入集合属性? 129、Spring中的自动装配有哪些限制? 130、在Web项目中如何获得Spring的IoC容器? 131. 大型网站在架构上应当考虑哪些问题? 132、你用过的网站前端优化的技术有哪些? 133、你使用过的应用服务器优化技术有哪些? 134、什么是XSS攻击?什么是SQL注入攻击?什么是CSRF攻击? 135. 什么是领域模型(domain model)?贫血模型(anaemic domain model)和充血模型(rich domain model)有什么区别? 136. 谈一谈测试驱动开发(TDD)的好处以及你的理解。 以上就是java面试笔试题,对于想要提升自己,对自己未来迷茫的Java工程师们,可以加群程序员交流群:960439918获取哦,点击链接加入群聊【java高级架构交流群】:https://jq.qq.com/?_wv=1027&k=5fozFzF涉及的知识点(Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术)。 推荐一套我把目前互联网公司用到的java核心技术总结出知识体系思维导图分享给大家!(学习是一个复杂的过程,当你拥有了学习的方向和学习的方法时,你缺的只是时间,时间是自己积累出来的,而不是我想学习时说“好像没空”这些借口。不要让今天的借口变成了明天的迷茫!) 需要了解更多可加群960439918获取哦,点击链接加入群聊【java高级架构交流群】:https://jq.qq.com/?_wv=1027&k=5fozFzF获取更详细资料。
RabbitMQ 在上一家公司已经接触过了, 但是懵懵懂懂的. 不是很清楚. 具体怎么个逻辑. 这次公司打算搭建新的系统. 领导要求研究一下MQ. 经过研究得出的结论是. MSMQ的设计理念不适合做系统的底层框架. 他不适合做分布式系统. 最主要的是. MSMQ如果没有消费者, 默认消息是一直存在的. 而RabbitMQ的设计理念是.只要有接收消息的队列. 邮件就会存放到队列里. 直到订阅人取走. . 如果没有可以接收这个消息的消息队列. 默认是抛弃这个消息的.. 下面就把我的研究结果写一下. 如何在新的系统中使用RabbitMQ. 系统设计的两个重大问题. 第一条要满足未来的业务需求的不断变化和增加. 也就是可扩展性. 第二条要满足性能的可伸缩性. 也就是可集群性…通过增加机器能处理更多的请求 第三条要解耦合. 如果不解耦合, 未来业务增加或变更的时候你还在修改3年前写的代码.试问你有多大的把握保证升级好系统不出问题? 如何可以写新的代码而不用修改老代码所带来的好处谁都知道… 第四条简单易懂. 以上4条在任何一个系统中都要遵循的原则. 以前是无法做到的. 自从有了MQ以后. 这些都可以同时做到了. 以前的设计理念是把系统看作一个人,按照工作的指令从上到下的执行. 现在要建立的概念是, 把系统的各个功能看作不同的人. 人与人之间的沟通通过消息进行交流传递信息… 有了MQ以后把一个人的事情分给了不同的人, 分工合作所带来的好处是专业化, 并行化. 当然也引入了一些麻烦,性能开销多一些, 工作任务的完整性不能立即得到反馈.幸好我们可以通过最终一致性.来解决这个麻烦的问题… 下面进入正题. 第一个问题RabbitMQ是如何支持可扩展性的. 如上图, 寄件人P是系统的一个功能模块. 用来发送消息. 一般是在某些重要的业务状态变更时发送消息. 例如: 新订单产生时, 订单已打包时, 订单已出库时, 订单已发出时. 那么当事件 新订单产生时, 我们需要把这个信息告诉谁呢? 给财务? 还是给仓库发货? 这个地方最大的重点是. 当事件产生时. 根本不关心. 该投递给谁. 我只要把我的重要的信息投到这个乱七八糟的MQ系统即可. 其它人你该干嘛干嘛. 反正我的任务完成了. (有没有甩手掌柜的感觉..) 我只要告诉系统,我的事件属于那一类. 例如: “某某省.某某市.某某公司.产生新订单” 那么这个地址就属于 投递地址.. 至于这个地址具体投到哪个邮箱那是邮局的事情. 当然还有一些具体的订单内容也属于要告诉系统的内容. 那么下一个问题来了, 邮局怎么知道 你的这个消息应该投递给谁? 参考我们现实世界中的邮寄系统.是默认的省市县这么投递的. 这是固定思维. 但是我们的MQ系统中不是这样的. 是先有收件人的邮箱. (队列Queue). MQ才能投递. 否则就丢弃这个信息… 所以MQ系统应该先有收件人的邮箱 Queue 也就是队列. 才能接收到信息. 再有邮局 再有发信息的人. RabbitMQ能实现系统扩展的一个重要功能在于, 可以两个邮箱收同一个地址的信. 翻译成专业的话 RabbitMQ 可以 两个队列Queue订阅同一个RoutingKey的信息.. RabbitMQ在投递的时候,会把一份信息,投递到多个队列邮箱中Queue… 这是系统可扩展性的基础. 第二个问题RabbitMQ如何满足性能的可伸缩性. 也就是可集群性 先上图 从上图, 可以看到. 性能扩展的关键点就在于 订阅人C1, 订阅人C2 轮流收到邮箱队列里面的信息, 订阅人C1和订阅人C2收到的信息内容不同, 但都属于同一类…. 所以. 订阅人C1和订阅人C2是干同一种工作的客户端.用来提高处理能力. 上面说完了,如何使用. 下面再分析一下几个关注点. 如果订阅人的down机了. 信息会丢失吗? 事实上是不会的. 只要有邮箱(队列Queue)存在.信息就一直存在, 除非订阅人去取走. 如果订阅人一直down机, 邮箱队列能存多少信息?会不会爆掉? 理论上和实际上都是有上限的不可能无限多. 具体多少看硬盘吧..我没测到过上限. 我这篇文章并不打算讲解邮局的4种投递模式. 有其它文章讲的很好. 我只打算使用topic这种模式. 因为它更灵活一些. 再说一下我的另外两个观点. 不要在业务程序中用代码定义创建 邮局 ExChange. 和邮箱Queue队列 这属于系统设计者要构架的事情. 要有专门独立的程序和规则去创建. 这样可以统一管理事件类型.避免过多的乱七八糟的RoutingKey混乱. 我的理解认为 消息系统的分布式可扩展的实现在于消息广播, 集群性的实现在于邮箱队列. RabbitMQ是先广播后队列的. Exchange: 就是邮局的概念等同于 中国邮政和顺丰快递、 routingkey: 就是邮件地址的概念. queue: 就是邮箱接收软件,但是可以接收多个地址的邮件,通过bind实现。 producer: 消息生产者,就是投递消息的程序。 consumer:消息消费者,就是接受消息的程序。 channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。 给大家推荐一个程序员学习交流群:960439918。点击链接加入群聊【java高级架构交流群】:https://jq.qq.com/?_wv=1027&k=5fozFzF群里有分享的视频,还有思维导图群公告有视频,都是干货的,你可以下载来看。主要分享分布式架构、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战学习架构师视频。
Java面试笔试题大汇总一(最全+详细答案)https://www.jianshu.com/p/73b6b3d35676Java面试笔试题大汇总三(最全+详细答案)https://www.jianshu.com/p/3e9a7073e60e 51、类ExampleA继承Exception,类ExampleB继承ExampleA。有如下代码片断: 52、List、Set、Map是否继承自Collection接口? 53、阐述ArrayList、Vector、LinkedList的存储性能和特性。 54、Collection和Collections的区别? 55、List、Map、Set三个接口存取元素时,各有什么特点? 56、TreeMap和TreeSet在排序时如何比较元素? 57、Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别? 58、线程的sleep()方法和yield()方法有什么区别? 59、当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B? 60、请说出与线程同步以及线程调度相关的方法。 61、编写多线程程序有几种实现方式? 62、synchronized关键字的用法? 63、举例说明同步和异步。 64、启动一个线程是调用run()还是start()方法? 65、什么是线程池(thread pool)? 67、简述synchronized 和java.util.concurrent.locks.Lock的异同? 68、Java中如何实现序列化,有什么意义? 69、Java中有几种类型的流? 70、写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数。 71、如何用Java代码列出一个目录下所有的文件? 72、用Java的套接字编程实现一个多线程的回显(echo)服务器。 73、XML文档定义有几种形式?它们之间有何本质区别?解析XML文档有哪几种方式? 74、你在项目中哪些地方用到了XML? 75、阐述JDBC操作数据库的步骤。 76、Statement和PreparedStatement有什么区别?哪个性能更好? 77、使用JDBC操作数据库时,如何提升读取数据的性能?如何提升更新数据的性能? 78、在进行数据库编程时,连接池有什么作用? 79、什么是DAO模式? 80、事务的ACID是指什么? 81、JDBC中如何进行事务处理? 82、JDBC能否处理Blob和Clob? 83、简述正则表达式及其用途。 84、Java中是如何支持正则表达式操作的? 85、获得一个类的类对象有哪些方式?答: 86、如何通过反射创建对象? 87、如何通过反射获取和设置对象私有字段的值? 88、如何通过反射调用对象的方法? 89、简述一下面向对象的"六原则一法则"。 90、简述一下你了解的设计模式。 91、用Java写一个单例类。 92、什么是UML? 93、UML中有哪些常用的图? 94、用Java写一个冒泡排序。 95、用Java写一个折半查找。 96、阐述Servlet和CGI的区别? 97、Servlet接口中有哪些方法? 98、转发(forward)和重定向(redirect)的区别? 99、JSP有哪些内置对象?作用分别是什么? 100、get和post请求的区别? 以上就是第二部分面试笔试题,小编会持续更新哒。对于想要提升自己,对自己未来迷茫的Java工程师们,可以加群程序员交流群:960439918获取哦,点击链接加入群聊【java高级架构交流群】:https://jq.qq.com/?_wv=1027&k=5fozFzF涉及的知识点(Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术)。
Java面试笔试题大汇总二(最全+详细答案)https://www.jianshu.com/p/f5120f1b75be Java面试笔试题大汇总三(最全+详细答案)https://www.jianshu.com/p/3e9a7073e60e 1、面向对象的特征有哪些方面? 2、访问修饰符public,private,protected,以及不写(默认)时的区别? 3、String 是最基本的数据类型吗? 4、float f=3.4;是否正确? 5、short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗? 6、Java有没有goto? 7、int和Integer有什么区别? 8、&和&&的区别? 9、解释内存中的栈(stack)、堆(heap)和静态区(static area)的用法。 10、Math.round(11.5) 等于多少?Math.round(-11.5)等于多少? 11、swtich 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上? 12、用最有效率的方法计算2乘以8? 13、数组有没有length()方法?String有没有length()方法? 14、在Java中,如何跳出当前的多重嵌套循环? 15、构造器(constructor)是否可被重写(override)? 16、两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对? 17、是否可以继承String类? 18、当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递? 19、String和StringBuilder、StringBuffer的区别? 20、重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分? 21、描述一下JVM加载class文件的原理机制? 22、char 型变量中能不能存贮一个中文汉字,为什么? 23、抽象类(abstract class)和接口(interface)有什么异同? 24、静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同? 25、Java 中会存在内存泄漏吗,请简单描述。 26、抽象的(abstract)方法是否可同时是静态的(static),是否可同时是本地方法(native),是否可同时被synchronized修饰? 27、阐述静态变量和实例变量的区别。 28、是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用? 29、如何实现对象克隆? 30、GC是什么?为什么要有GC? 32、接口是否可继承(extends)接口?抽象类是否可实现(implements)接口?抽象类是否可继承具体类(concrete class)? 33、一个".java"源文件中是否可以包含多个类(不是内部类)?有什么限制? 34、Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口? 35、内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制? 36、Java 中的final关键字有哪些用法? 37、指出下面程序的运行结果。 38、数据类型之间的转换: 39、如何实现字符串的反转及替换? 40、怎样将GB2312编码的字符串转换为ISO-8859-1编码的字符串? 41、日期和时间: - 如何取得年月日、小时分钟秒? - 如何取得从1970年1月1日0时0分0秒到现在的毫秒数? - 如何取得某月的最后一天? - 如何格式化日期? 42、打印昨天的当前时刻。 43、比较一下Java和JavaSciprt。 44、什么时候用断言(assert)? 45、Error和Exception有什么区别? 46、try{}里有一个return语句,那么紧跟在这个try后的finally{}里的代码会不会被执行,什么时候被执行,在return前还是后? 47、Java语言如何进行异常处理,关键字:throws、throw、try、catch、finally分别如何使用? 48、运行时异常与受检异常有何异同? 49、列出一些你常见的运行时异常? 50、阐述final、finally、finalize的区别。 以上就是一部分面试笔试题,小编会持续更新哒。对于想要提升自己,对自己未来迷茫的Java工程师们,可以加群程序员交流群:960439918获取哦,点击链接加入群聊【java高级架构交流群】:https://jq.qq.com/?_wv=1027&k=5fozFzF 涉及的知识点(Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术)。
1、servlet执行流程 客户端发出http请求,web服务器将请求转发到servlet容器,servlet容器解析url并根据web.xml找到相对应的servlet,并将request、response对象传递给找到的servlet,servlet根据request就可以知道是谁发出的请求,请求信息及其他信息,当servlet处理完业务逻辑后会将信息放入到response并响应到客户端。 2、springMVC的执行流程 springMVC是由dispatchservlet为核心的分层控制框架。首先客户端发出一个请求web服务器解析请求url并去匹配dispatchservlet的映射url,如果匹配上就将这个请求放入到dispatchservlet,dispatchservlet根据mapping映射配置去寻找相对应的handel,然后把处理权交给找到的handel,handel封装了处理业务逻辑的代码,当handel处理完后会返回一个逻辑视图modelandview给dispatchservlet,此时的modelandview是一个逻辑视图不是一个正式视图,所以dispatchservlet会通过viewresource视图资源去解析modelandview,然后将解析后的参数放到view中返回到客户端并展现。 3、给定一个txt文件,如何得到某字符串出现的次数 1 File file = new File("E://test.txt"); 2 InputStream is = new FileInputStream(file); 3 byte b[] = new byte[1024]; 4 int a = is.read(b); 5 String str[] = new String(b,0,a).split(""); 6 int count = 0; 7 for(int i = 0;i<str.length;i++){ 8 if("a".equals(str[i]))count++; 9 } 10 System.out.println(count); 4、Java设计模式思想(单列模式,工厂模式,策略模式,共23种设计模式) a) 单例模式:单例模式核心只需要new一个实例对象的模式,比如数据库连接,在线人数等,一些网站上看到的在线人数统计就是通过单例模式实现的,把一个计时器存放在数据库或者内存中,当有人登陆的时候取出来加一再放回去,有人退出登陆的时候取出来减一再放回去,但是当有两个人同时登陆的时候,会同时取出计数器,同时加一,同时放回去,这样的话数据就会错误,所以需要一个全局变量的对象给全部人使用,只需要new出一个实例对象,这就是单例模式的应用,并且单例模式节省资源,因为它控制了实例对象的个数,并有利于gc回收。 b) 策略模式:就是将几个类中公共的方法提取到一个新的类中,从而使扩展更容易,保证代码的可移植性,可维护性强。比如有个需求是写鸭子对象,鸭子有叫,飞,外形这三种方法,如果每个鸭子类都写这三个方法会出现代码的冗余,这时候我们可以把鸭子中的叫,飞,外形这三个方法提取出来,放到鸭父类中,让每个鸭子都继承这个鸭父类,重写这三个方法,这样封装的代码可移植性强,当用户提出新的需求比如鸭子会游泳,那么对于我们oo程序员来讲就非常简单了我们只需要在鸭父类中加一个游泳的方法,让会游泳的鸭子重写游泳方法就可以了。 c) 工厂模式:简单的工厂模式主要是统一提供实例对象的引用,通过工厂模式接口获取实例对象的引用。比如一个登陆功能,后端有三个类,controller类,interface类,实现接口的实现类。当客户端发出一个请求,当请求传到controller类中时,controller获取接口的引用对象,而实现接口的实现类中封装好了登陆的业务逻辑代码。当你需要加一个注册需求的时候只需要在接口类中加一个注册方法,实现类中实现方法,controller获取接口的引用对象即可,不需要改动原来的代码,这种做法是的可拓展性强。 5、冒泡排序、二分查找 a) 冒泡 ? 1 public static void mp(int a[]) { 2 int swap = 0; 3 for (int i = 0; i < a.length; i++) { 4 for (int j = i; j < a.length; j++) { 5 if (a[j] > a[i]) { 6 swap = a[i]; 7 a[i] = a[j]; 8 a[j] = swap; 9 } 10 } 11 } 12 System.out.println(Arrays.toString(a)); 13 } b)二分查找 ? 1 public static int ef(int a[], int tag) { 2 int first = 0; 3 int end = a.length; 4 for (int i = 0; i < a.length; i++) { 5 int middle = (first + end) / 2; 6 if (tag == a[middle]) { 7 return middle; 8 } 9 if (tag > a[middle]) { 10 first = middle + 1; 11 } 12 if (tag < a[middle]) { 13 end = middle - 1; 14 } 15 } 16 return 0; 17 } 6-8、对ajax的理解 a) Ajax为异步请求,即局部刷新技术,在传统的页面中,用户需要点击按钮或者事件触发请求,到刷新页面,而异步技术为不需要点击即可触发事件,这样使得用户体验感增强,比如商城购物车的异步加载,当你点击商品时无需请求后台而直接动态修改参数。 9、父类与子类之间的调用顺序(打印结果) a) 父类静态代码块 b) 子类静态代码块 c) 父类构造方法 d) 子类构造方法 e) 子类普通方法 f) 重写父类的方法,则打印重写后的方法 10、内部类与外部类的调用 a) 内部类可以直接调用外部类包括private的成员变量,使用外部类引用的this.关键字调用即可 b) 而外部类调用内部类需要建立内部类对象 11、多线程 a)一个进程是一个独立的运行环境,可以看做是一个程序,而线程可以看做是进程的一个任务,比如QQ是一个进程,而一个QQ窗口是一个线程。 b)在多线程程序中,多线程并发可以提高程序的效率,cpu不会因为某个线程等待资源而进入空闲状态,它会把资源让给其他的线程。 c)用户线程就是我们开发程序是创建的线程,而守护线程为系统线程,如JVM虚拟中的GC d)线程的优先级别:每一个线程都有优先级别,有限级别高的可以先获取CPU资源使该线程从就绪状态转为运行状态。也可以自定义线程的有限级别 e)死锁:至少两个以上线程争取两个以上cpu资源,避免死锁就避免使用嵌套锁,只需要在他们需要同步的地方加锁和避免无限等待 12、AOP与IOC的概念(即spring的核心) a) IOC:Spring是开源框架,使用框架可以使我们减少工作量,提高工作效率并且它是分层结构,即相对应的层处理对应的业务逻辑,减少代码的耦合度。而spring的核心是IOC控制反转和AOP面向切面编程。IOC控制反转主要强调的是程序之间的关系是由容器控制的,容器控制对象,控制了对外部资源的获取。而反转即为,在传统的编程中都是由我们创建对象获取依赖对象,而在IOC中是容器帮我们创建对象并注入依赖对象,正是容器帮我们查找和注入对象,对象是被获取,所以叫反转。 b) AOP:面向切面编程,主要是管理系统层的业务,比如日志,权限,事物等。AOP是将封装好的对象剖开,找出其中对多个对象产生影响的公共行为,并将其封装为一个可重用的模块,这个模块被命名为切面(aspect),切面将那些与业务逻辑无关,却被业务模块共同调用的逻辑提取并封装起来,减少了系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。 13、hibernate的核心思想 a) Hibernate的核心思想是ROM对象关系映射机制。它是将表与表之间的操作映射成对象与对象之间的操作。也就是从数据库中提取的信息会自动按照你设置的映射要求封装成特定的对象。所以hibernate就是通过将数据表实体类的映射,使得对对象的修改对应数据行的修改。 14、Struts1与Struts2的区别 15、最优删除谋字符串的某个字符 16-17、Arraylist与linkedlist的区别 a) 都是实现list接口的列表,arraylist是基于数组的数据结构,linkedlist是基于链表的数据结构,当获取特定元素时,ArrayList效率比较快,它通过数组下标即可获取,而linkedlist则需要移动指针。当存储元素与删除元素时linkedlist效率较快,只需要将指针移动指定位置增加或者删除即可,而arraylist需要移动数据。 18、数据库优化 a) 选择合适的字段,比如邮箱字段可以设为char(6),尽量把字段设置为notnull,这样查询的时候数据库就不需要比较null值 b) 使用关联查询( left join on)查询代替子查询 c) 使用union联合查询手动创建临时表 d) 开启事物,当数据库执行多条语句出现错误时,事物会回滚,可以维护数据库的完整性 e) 使用外键,事物可以维护数据的完整性但是它却不能保证数据的关联性,使用外键可以保证数据的关联性 f) 使用索引,索引是提高数据库性能的常用方法,它可以令数据库服务器以比没有索引快的多的速度检索特定的行,特别是对于max,min,order by查询时,效果更明显 g) 优化的查询语句,绝大多数情况下,使用索引可以提高查询的速度,但如果sql语句使用不恰当的话,索引无法发挥它的特性。 19、Tomcat服务器优化(内存,并发连接数,缓存) a) 内存优化:主要是对Tomcat启动参数进行优化,我们可以在Tomcat启动脚本中修改它的最大内存数等等。 b) 线程数优化:Tomcat的并发连接参数,主要在Tomcat配置文件中server.xml中配置,比如修改最小空闲连接线程数,用于提高系统处理性能等等。 c) 优化缓存:打开压缩功能,修改参数,比如压缩的输出内容大小默认为2KB,可以适当的修改。 20、HTTP协议 a) 常用的请求方法有get、post b) Get与post的区别:传送数据,get携带参数与访问地址传送,用户可以看见,这的话信息会不安全,导致信息泄露。而post则将字段与对应值封装在实体中传送,这个过程用户是不可见的。Get传递参数有限制,而post无限制。 21、TCP/UDP协议 22、Java集合类框架的基本接口有哪些 a) Collection集合接口,List、set实现Collection接口,arraylist、linkedlist,vector实现list接口,stack继承vector,Map接口,hashtable、hashmap实现map接口 23、类加载的过程 a) 遇到一个新的类时,首先会到方法区去找class文件,如果没有找到就会去硬盘中找class文件,找到后会返回,将class文件加载到方法区中,在类加载的时候,静态成员变量会被分配到方法区的静态区域,非静态成员变量分配到非静态区域,然后开始给静态成员变量初始化,赋默认值,赋完默认值后,会根据静态成员变量书写的位置赋显示值,然后执行静态代码。当所有的静态代码执行完,类加载才算完成。 24、对象的创建 a) 遇到一个新类时,会进行类的加载,定位到class文件 b) 对所有静态成员变量初始化,静态代码块也会执行,而且只在类加载的时候执行一次 c) New 对象时,jvm会在堆中分配一个足够大的存储空间 d) 存储空间清空,为所有的变量赋默认值,所有的对象引用赋值为null e) 根据书写的位置给字段一些初始化操作 f) 调用构造器方法(没有继承) 25、jvm的优化 a) 设置参数,设置jvm的最大内存数 b) 垃圾回收器的选择 26、高并发处理 a) 了解一点高并发性问题,比如一W人抢一张票时,如何保证票在没买走的情况下所有人都能看见这张票,显然是不能用同步机制,因为synchronize是锁同步一次只能一个人进行。这时候可以用到锁机制,采用乐观锁可以解决这个问题。乐观锁的简单意思是在不锁定表的情况下,利用业务的控制来解决并发问题,这样即保证数据的可读性,又保证保存数据的排他性,保证性能的同时解决了并发带来的脏读数据问题。 27、事物的理解 a) 事物具有原子性,一致性,持久性,隔离性 b) 原子性:是指在一个事物中,要么全部执行成功,要么全部失败回滚。 c) 一致性:事物执行之前和执行之后都处于一致性状态 d) 持久性:事物多数据的操作是永久性 e) 隔离性:当一个事物正在对数据进行操作时,另一个事物不可以对数据进行操作,也就是多个并发事物之间相互隔离。 28、Struts工作流程 a) 客户端发出一个请求到servlet容器 b) 请求经过一些列过滤被filterdispatcher调用,filterdispatch通过actionMapper去找相对应的action。 c) Actionmapper找到对应的action返回给filterdispatch,dispatch把处理权交给actionproxy d) Actionproxy通过配置文件找到对应的action类 e) Actionproxy创建一个actionIinvocation的实例处理业务逻辑 f) 一旦action处理完毕,actioninvocation负责根据stuts.xml的配置找到对应的返回结果。返回结果通常是jsp页面。 以上就是java技术面试题,小编个人觉得还是需要时刻提升自己的技术。给大家推荐一个程序员学习交流群:960439918。群里有分享的视频,还有思维导图主要分享分布式架构、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战学习架构师视频。
我们先来设想一个场景。 有一个 http 的接口 A,该接口内部实际上是由另外三个接口 B、C、D 返回结果的组合,这三个接口不存在相互依赖。我们一般的写法就是 B、C、D 同步顺序执行,依次拿到结果后组装在一起。那么假如这三个接口分别耗时 2 秒,那么 A 接口就要耗时 6 秒。如果可以让 B、C、D 同时执行的话,那么 A 接口理论上只要耗时 2 秒。 当然实际情况肯定复杂的多,如果一个接口内部存在不相互依赖的耗时调用的话,那么我们可以做这样的合并,响应时间上的减少还是非常明显的。整个接口的响应时间取决于最长的那个内部接口。 那么我们来看看在 Java 中有哪些方法可以达到这样的目的。认真思考下你会发现,如果要并行处理的话,在 Java 中只能用多线程来做。实际情况中每个线程处理完的时间肯定不一样,那么如何让线程先处理完的停下来等最后那个处理完的呢。如果经常用多线程的小伙伴肯定能想到 CountDownLatch 工具类。当然也有直接简单暴力的方法,在空循环里轮询每个线程是否执行完,但是这样做肯定不优雅。 那下面就直接上代码了: 假设有个学生服务提供查询学生名字,年龄和家庭信息,每个服务之间没有相互依赖。 我们就简单模拟下来获取学生信息的一个接口。 常规方法 @RequestMapping("/getStudentInfo") public Object getStudentInfo() { long start = System.currentTimeMillis(); Map resultMap = new HashMap<>(10); try { resultMap.put("studentName", studentService.getStudentName()); resultMap.put("studentAge", studentService.getSutdentAge()); resultMap.put("studentFamilyInfo", studentService.getSutdentFamilyInfo()); } catch (Exception e) { resultMap.put("errMsg", e.getMessage()); } resultMap.put("total cost", System.currentTimeMillis() - start); return resultMap; } 顺序同步执行,耗时 6 秒。 1. Future @RequestMapping("/getStudentInfoWithFuture") public Object testWhitCallable() { long start = System.currentTimeMillis(); Map resultMap = new HashMap<>(10); try { CountDownLatch countDownLatch = new CountDownLatch(3); Future futureStudentName = es.submit(() -> { Object studentName = studentService.getStudentName(); countDownLatch.countDown(); return studentName; }); Future futureStudentAge = es.submit(() -> { Object studentAge = studentService.getSutdentAge(); countDownLatch.countDown(); return studentAge; }); Future futureStudentFamilyInfo = es.submit(() -> { Object studentFamilyInfo = studentService.getSutdentFamilyInfo(); countDownLatch.countDown(); return studentFamilyInfo; }); //同步等待所有线程执行完之后再继续 countDownLatch.await(); resultMap.put("studentName", futureStudentName.get()); resultMap.put("studentAge", futureStudentAge.get()); resultMap.put("studentFamilyInfo", futureStudentFamilyInfo.get()); } catch (Exception e) { resultMap.put("errMsg", e.getMessage()); } resultMap.put("total cost", System.currentTimeMillis() - start); return resultMap; } 2.RxJava @RequestMapping("/getStudentInfoWithRxJava") public Object testWithRxJava() { long start = System.currentTimeMillis(); Map resultMap = new HashMap<>(10); try { CountDownLatch countDownLatch = new CountDownLatch(1); Observable studentNameObservable = Observable.create(observableEmitter -> { resultMap.put("studentName", studentService.getStudentName()); observableEmitter.onComplete(); }).subscribeOn(Schedulers.io()); Observable studentAgeObservable = Observable.create(observableEmitter -> { resultMap.put("studentAge", studentService.getSutdentAge()); observableEmitter.onComplete(); }).subscribeOn(Schedulers.io()); Observable familyInfoObservable = Observable.create(observableEmitter -> { resultMap.put("studentFamilyInfo", studentService.getSutdentFamilyInfo()); observableEmitter.onComplete(); }).subscribeOn(Schedulers.io()); //创建一个下游 Observer Observer observer = new Observer() { @Override public void onSubscribe(Disposable d) { } @Override public void onNext(Object o) { } @Override public void onError(Throwable e) { } @Override public void onComplete() { //因为后面用了 merge 操作符,所以会合并后发射,那么只要 countdown 一次就行了。 countDownLatch.countDown(); } }; //建立连接, Observable.merge(studentNameObservable, studentAgeObservable, familyInfoObservable).subscribe(observer); //等待异步线程完成 countDownLatch.await(); } catch (Exception e) { resultMap.put("errMsg", e.getMessage()); } resultMap.put("total cost", System.currentTimeMillis() - start); return resultMap; } 对于 RxJava 我不熟,我也是临时学习的,不知道这种写法是不是最佳的。 3.CompletableFutures @RequestMapping("/getStudentInfoWithCompletableFuture") public Object getStudentInfoWithCompletableFuture() { long start = System.currentTimeMillis(); Map resultMap = new HashMap<>(10); try { CompletableFuture completableFutureStudentName = CompletableFuture.supplyAsync(() -> { try { return studentService.getStudentName(); } catch (InterruptedException e) { e.printStackTrace(); } return null; }); CompletableFuture completableFutureSutdentAge = CompletableFuture.supplyAsync(() -> { try { return studentService.getSutdentAge(); } catch (InterruptedException e) { e.printStackTrace(); } return null; }); CompletableFuture completableFutureFamilyInfo = CompletableFuture.supplyAsync(() -> { try { return studentService.getSutdentFamilyInfo(); } catch (InterruptedException e) { e.printStackTrace(); } return null; }); CompletableFuture.allOf(completableFutureStudentName, completableFutureSutdentAge, completableFutureFamilyInfo).join(); resultMap.put("studentName", completableFutureStudentName.get()); resultMap.put("studentAge", completableFutureSutdentAge.get()); resultMap.put("studentFamilyInfo", completableFutureFamilyInfo.get()); } catch (Exception e) { resultMap.put("errMsg", e.getMessage()); } resultMap.put("total cost", System.currentTimeMillis() - start); return resultMap; } 自带最后的同步等待,不需要 CountDownLatch。CompletableFuture 还有很多其他好用的方法。 有兴趣的可以自己来实验下。 github 项目地址 reactive-programming-sample。 Java程序员如何学习才能快速入门并精通呢? 当真正开始学习的时候难免不知道从哪入手,导致效率低下影响继续学习的信心。 但最重要的是不知道哪些技术需要重点掌握,学习时频繁踩坑,最终浪费大量时间,所以有一套实用的视频课程用来跟着学习是非常有必要的。 为了让学习变得轻松、高效,今天给大家免费分享一套阿里架构师传授的一套教学资源。帮助大家在成为架构师的道路上披荆斩棘。这套视频课程详细讲解了(Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构)等这些成为架构师必备的内容!而且还把框架需要用到的各种程序进行了打包,根据基础视频可以让你轻松搭建分布式框架环境,像在企业生产环境一样进行学习和实践。 如果想提升自己的,看看上图大纲能知道你现在还处于什么阶段要向那些方面发展? 同时小编已将上图知识大纲里面的内容打包好了...... 想要资料的朋友,可以直接加群960439918获取免费架构资料(包括高可用,高并发,spring源码,mybatis源码,JVM,大数据,Netty等多个技术知识的架构视频资料和各种电子书籍阅读) 加入群聊【java高级架构交流群】