
暂无个人介绍
能力说明:
掌握封装、继承和多态设计Java类的方法,能够设计较复杂的Java类结构;能够使用泛型与集合的概念与方法,创建泛型类,使用ArrayList,TreeSet,TreeMap等对象掌握Java I/O原理从控制台读取和写入数据,能够使用BufferedReader,BufferedWriter文件创建输出、输入对象。
暂时未有相关云产品技术能力~
阿里云技能认证
详细说明服务演进概述单体应用-> SOA ->微服务持续集成,持续部署,持续交付。集成:是指软件个人研发的部分向软件整体部分集成,以便尽早发现个人开发部分的问题;部署: 是指代码尽可能快的向可运行的开发/测试节点交付,以便尽早测试;交付: 是指研发尽可能快的向客户交付,以便尽早发现生产环境中存在的问题。如果说等到所有东西都完成了才向下个环节交付,导致所有的问题只能在最后才爆发出来,解决成本巨大甚至无法解决。而所谓的持续,就是说每完成一个完整的部分,就向下个环节交付,发现问题可以马上调整。使问题不会放大到其他部分和后面的环节。这种做法的核心思想在于:既然事实上难以做到事先完全了解完整的、正确的需求,那么就干脆一小块一小块的做,并且加快交付的速度和频率,使得交付物尽早在下个环节得到验证。早发现问题早返工。持续集成工具:Jenkins(https://jenkins.io/doc/book/pipeline/)单体应用概念:所有功能全部打包在一起。应用大部分是一个 war 包或 jar 包。随着业务发展,功能增多,项目会越来越臃肿。好处:容易开发、测试、部署,适合项目初期试错。坏处:随着项目越来越复杂,团队不断扩大。坏处就显现出来了。复杂性高:代码多,十万行,百万行级别。加一个小功能,会带来其他功能的隐患,因为它们聚合在一起。技术债务:人员流动,不坏不修,因为不敢修。持续部署困难:由于是全量应用,改一个小功能,全部部署,会导致无关的功能暂停使用。编译部署上线耗时长,不敢随便部署,导致部署频率低,进而又导致两次部署之间功能修改多,从而更不敢部署,恶性循环。可靠性差:某个小问题,比如小功能出现 OOM,会导致整个应用崩溃。扩展受限:只能整体扩展,无法按需扩展, 不能根据计算密集型(电商系统)和 IO 密集型(文件服务) 进行合适的区分。阻碍创新:单体应用是以一种技术解决所有问题,不容易引入新技术。但在高速的互联网发展过程中,适应的潮流是:用合适的语言做合适的事情。比如在单体应用中,一个项目用 Spring MVC,想换成 Spring Boot,切换成本很高,因为有可能10万,百万行代码都要改,而微服务可以轻松切换,因为每个服务,功能简单,代码少。SOA对单体应用的改进:引入 SOA(Service-Oriented Architecture)面向服务架构,拆分系统,用服务的流程化来实现业务的灵活性。服务间需要某些方法进行连接,面向接口等,它是一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。一个服务通常以独立的形式存在于操作系统进程中。各个服务之间通过网络调用。但是还是需要用些方法来进行服务组合,有可能还是个单体应用。所以引入微服务,是 SOA 思想的一种具体实践。微服务架构 = 80%的 SOA 服务架构思想 + 100%的组件化架构思想。微服务微服务概况无严格定义。微服务是一种架构风格,将单体应用划分为小型的服务单元。微服务架构是一种使用一系列粒度较小的服务来开发单个应用的方式;每个服务运行在自己的进程中;服务间采用轻量级的方式进行通信(通常是 HTTP API);这些服务是基于业务逻辑和范围,通过自动化部署的机制来独立部署的,并且服务的集中管理应该是最低限度的,即每个服务可以采用不同的编程语言编写,使用不同的数据存储技术。英文定义:http://www.martinfowler.com/articles/microservices.html小类比合久必分。分开后通信,独立部署,独立存储。服从服务管理各自完成各自的一块业务服务间互相调用分担流量压力微服务特性独立运行在自己进程中一系列独立服务共同构建起整个系统一个服务只关注自己的独立业务轻量的通信机制 RESTful API使用不同语言开发使用不同的数据存储技术全自动部署机制微服务组件介绍不局限于具体的微服务实现技术。服务注册与发现:服务提供方将己方调用地址注册到服务注册中心,让服务调用方能够方便地找到自己;服务调用方从服务注册中心找到自己需要调用的服务的地址发起调用。负载均衡:服务提供方一般以多实例的形式提供服务,负载均衡功能能够让服务调用方连接到合适的服务节点。并且,服务节点选择的过程对服务调用方来说是透明的。服务网关:服务网关是服务调用的唯一入口,可以在这个组件中实现用户鉴权、动态路由、灰度发布、A/B测试、负载限流等功能。 灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行 A/B 测试,即让一部分用户继续用产品特性 A,一部分用户开始用产品特性 B,如果用户对 B 没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到 B 上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。配置中心:将本地化的配置信息(Properties、XML、YAML 等形式)注册到配置中心,实现程序包在开发、测试、生产环境中的无差别性,方便程序包的迁移,也是无状态特性。集成框架:微服务组件都以职责单一的程序包对外提供服务,集成框架以配置的形式将所有微服务组件(特别是管理端组件)集成到统一的界面框架下,让用户能够在统一的界面中使用系统。Spring Cloud 就是一个集成框架。调用链监控:记录完成一次请求的先后衔接和调用关系,并将这种串行或并行的调用关系展示出来。在系统出错时,可以方便地找到出错点。支撑平台:系统微服务化后,各个业务模块经过拆分变得更加细化,系统的部署、运维、监控等都比单体应用架构更加复杂,这就需要将大部分的工作自动化。现在,Docker 等工具可以给微服务架构的部署带来较多的便利,例如持续集成、蓝绿发布、健康检查、性能监控等等。如果没有合适的支撑平台或工具,微服务架构就无法发挥它最大的功效。1. 蓝绿部署是不停老版本,部署新版本然后进行测试,确认 OK,将流量切到新版本,然后老版本同时也升级到新版本。2. 灰度是选择部分部署新版本,将部分流量引入到新版本,新老版本同时提供服务。等待灰度的版本 OK,可全量覆盖老版本。灰度是不同版本共存,蓝绿是新旧版本切换,两种模式的出发点不一样。微服务优点独立部署。不依赖其他服务,耦合性低,不用管其他服务的部署对自身的影响。服务聚焦,聚焦于业务。松耦合。易于开发和维护:关注特定业务,所以业务清晰,代码量少,模块变的易开发、易理解、易维护。启动快:功能少,代码少,所以启动快,有需要停机维护的服务,不会长时间暂停服务。局部修改容易:只需要部署相应的服务即可,适合敏捷开发。技术栈不受限:java,node.js 等。按需伸缩:某个服务受限,可以按需增加内存,cpu 等。职责专一。专门团队负责专门业务,有利于团队分工。代码复用。不需要重复写。底层实现通过接口方式提供。便于团队协作:每个团队只需要提供 API 就行,定义好 API 后,可以并行开发。微服务缺点分布式固有的复杂性:容错(某个服务宕机),网络延时,调用关系、分布式事务等,都会带来复杂。分布式事务的挑战:每个服务有自己的数据库,优点在于不同服务可以选择适合自身业务的数据库。订单用 MySQL,评论用 MongoDB 等。目前最理想的解决方案是:柔性事务的最终一致性。刚性事务:遵循 ACID 原则,强一致性。柔性事务:遵循 BASE 理论,最终一致性;与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。BASE 是 Basically Available(基本可用)、Soft State(软状态)和 Eventually Consistent (最终一致性)三个短语的缩写。BASE 理论是对 CAP 中 AP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障时允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足 BASE 理论的事务,我们称之为“柔性事务”。接口调整成本高:改一个接口,调用方都要改。增加了系统间通信成本。测试难度提升:一个接口改变,所有调用方都得测。自动化测试就变的重要了。API 文档的管理也尤为重要。推荐:yapi。运维要求高:需要维护几十上百个服务。监控变的复杂。并且还要关注多个集群,不像原来单体,一个应用正常运行即可。重复工作:比如 java 的工具类可以在共享 common.jar 中,但在多语言下行不通,C++ 无法直接用 java 的 jar 包。设计原则单一职责原则:关注整个系统功能中单独,有界限的一部分。服务自治原则:可以独立开发,测试,构建,部署,运行,与其他服务解耦。轻量级通信原则:轻,跨平台,跨语言。REST,AMQP 等。粒度把控:与自己实际相结合。 不要追求完美,随业务进化而调整。(推荐书籍:《淘宝技术这10年》)技术选型Spring Cloud 和 Dubbo 组件比较Dubbo:Zookeeper+Dubbo+SpringMVC/SpringBoot通信方式:RPC注册中心:Zookeeper,Nacos配置中心:Diamond(淘宝开发)Spring Cloud:Spring+Netflix通信方式:Http RESTful注册中心:Eureka,Consul,Nacos配置中心:Config断路器:Hystrix网关:Zuul,Gateway分布式追踪系统:Sleuth+Zipkin差别背景国内影响大国外影响大平手社区活跃度中等高spring cloud 胜出架构完整度不完善(dubbo 有些不提供,需要用第三方组件,它只关注服务治理)比较完善,微服务组件应有尽有。spring cloud 胜出学习成本dubbo 需要配套学习无缝 springspring cloud 胜出性能高。(基于 Netty)低。(基于 http,每次都要创建)。 此性能的损耗对大部分应用是可以接受的。用小的性能损耗换来了 http 风格的 api 的方便性。dubbo 胜出Spring Cloud 简介Spring Cloud 是实现微服务架构的一系列框架的有机集合。是在 Spring Boot 基础上构建的,用于简化分布式系统构建的工具集。是拥有众多子项目的项目集合。利用 Spring Boot 的开发便利性,巧妙地简化了分布式系统基础设施(服务注册与发现、熔断机制、网关路由、配置中心、消息总线、负载均衡、链路追踪等)的开发。版本演进版本过程:版本名.版本号。版本名:伦敦地铁字母顺序。版本号M(milestone):里程碑SR(Service Releases):稳定版RC(Release Candidate):稳定版的候选版,也就是稳定版的最后一个版本可以在 Spring Cloud 官网:https://spring.io/projects/spring-cloud 查看各个版本,目前的最新版本为 2021.0.0。Spring Cloud 自 2016 年 1 月发布第一个 Angel.SR5 版本,到 2020 年 7 月发布 Hoxton.SR7 版本,已经历经了 4 年时间。这 4 年时间里,Spring Cloud 一共发布了 46 个版本,支持的组件数从 5 个增加到 21 个。Spring Cloud 在 2019 年 12 月对外宣布后续 RoadMap:下一个版本 Ilford 版本是一个大版本。这个版本基于 Spring Framework 5.3 & Spring Boot 2.4,会在 2020 Q4 左右发布;Ilford 版本会删除处于维护模式的项目。目前处于维护模式的 Netflix 大部分项目都会被删除(spring-cloud-netflix Github 项目已经删除了这些维护模式的项目);简化 Spring Cloud 发布列车。后续 IaaS 厂商对应的 Spring Cloud 项目会移出 Spring Cloud 组织,各自单独维护(spring-cloud-azure 一直都是单独维护,spring-cloud-alibaba 孵化在 Spring Cloud 组织,毕业后单独维护);API 重构,会带来重大的改变(Spring Cloud Hoxton 版本新增了 Spring Cloud Circuit Breaker 用于统一熔断操作的编程模型和 Spring Cloud LoadBalanacer 用于处理客户端负载均衡并代替 Netflix Ribbon)。这个 RoadMap 可以说是对 Spring Cloud 有着非常大的变化。组件服务注册与发现组件:Eureka,Zookeeper,Consul,Nacos 等。Eureka 基于 REST 风格的。服务调用组件:Hystrix (熔断降级,在出现依赖服务失效的情况下,通过隔离系统依赖服务的方式,防止服务级联失败,同时提供失败回滚机制,使系统能够更快地从异常中恢复),Ribbon(客户端负载均衡,用于提供客户端的软件负载均衡算法,提供了一系列完善的配置项:连接超时、重试等),OpenFeign(优雅的封装 Ribbon,是一个声明式RESTful 网络请求客户端,它使编写 Web 服务客户端变得更加方便和快捷)。网关:路由和过滤。Zuul,Gateway。配置中心:Spring Cloud Config,提供了配置集中管理,动态刷新配置的功能;配置通过 Git 或者其他方式来存储。消息组件:Spring Cloud Stream(对分布式消息进行抽象,包括发布订阅、分组消费等功能,实现了微服务之间的异步通信)和 Spring Cloud Bus(主要提供服务间的事件通信,如刷新配置)。安全控制组件:Spring Cloud Security 基于 OAuth2.0 开放网络的安全标准,提供了单点登录、资源授权和令牌管理等功能。链路追踪组件:Spring Cloud Sleuth(收集调用链路上的数据),Zipkin(对 Sleuth 收集的信息,进行存储,统计,展示)。SpringCloud替代实现SpringCloud AlibabaSentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。Dubbo:Apache Dubbo 是一款高性能 Java RPC 框架。Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。Alibaba Cloud ACM:一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心产品。Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
架构概论Reliable Scalable Distributed(可靠的、可扩展的、分布式的),高并发,可靠性,性能Distributed Middleware(分布式中间件)Micro Service(微服务)DevOps(开发运维一体化),Docker,K8s,CI/CDService Mesh,解决性能问题,每一个物理节点上有一个代理节点,节点与节点之间通信,代理节点完成末端的统一接口,这样就可以让我们的集群网络通信、开发成本降低AIOps(人工智能运维)ChatOps(聊天室运维)Serviceless(无服务化)顶层设计按需、预期未来,规划企业架构业务全局出发,制定可落地的架构方案技术选型、难题解决规划方案与代码,广度与深度技术+管理(人,资源,技术)核心需求分布式、高并发、高性能、高可用、可扩展、松耦合、高内聚、可复用、边界、安全、成本、规模等等服务、缓存、消息、搜索、调度、任务、数据、监控、配置、网关等等Paxos、CAP、BASE、ACID、raft、rpc、Reactor、SLA、SLB等等关键词汇缓冲 Buffer:解决上下游速度不匹配的问题,消息队列、批处理都属于 Buffer缓存 Cache:解决 I/O 性能问题,在内存中存储热点数据,提升慢速磁盘 I/O 和程序使用的过程,PageCache、内存到 CPU 中的三级缓存、Redis、本地缓存等都属于 Cache复用 Pool:降低复杂度,提升性能,连接池、线程池、内存池、对象池都属于 Pool分治 Sharding:分而治之,单机、集群都适用,降低时间复杂度亲密(粘性)sticky:让某一个线程亲密到某一颗 CPU上,将相同 id 的商品路由到一台机器上,或者是将相同 ip 地址/相同 session 在负载均衡时负载到一台机器上,这样可以减少资源的重复分配,ThreadLocal、数据路由等都属于 sticky权衡 Balance or trade-off:技术选型,在技术之间做一个均衡喝啤酒理论:假设你住在24层,在1层有12瓶啤酒,现在你想喝啤酒,但是有一个要求,你必须把啤酒从1层带到24层去喝,你可以有两种选择:一种是每次拿1瓶回去喝,另一种是拿个箱子把12瓶酒全部装回去喝。很显然,第二种方式是更优的,它相比第一种方式,减少了来回跑动所付出的代价。这种理论实际上就体现了缓冲的概念。技术词汇:QPS:Queries Per Second(2/8 定理:0.8并发量/0.2天秒)TPS:Transactions Per SecondRT:Response-timePV:Page ViewUV:Unique Visitor并发数:同时访问服务器站点的连接数线程数:((挂起时间+运行时间)/ 运行时间 )* CPU 2倍(状态(挂起,运行)->时间)各种中间件的性能:https://help.aliyun.com/书籍推荐1、大型网站技术架构:核心原理与案例分析2、分布式服务框架原理与实践3、互联网创业核心技术:构建可伸缩的web应用4、高扩展性网站的50条原则5、架构即未来:现代企业可扩展的Web架构、流程和组织6、系统架构:复杂系统的产品设计与开发7、Java性能优化权威指南8、大规模分布式存储系统:原理解析与架构实战9、大规模分布式系统架构与设计实战10、企业IT架构转型之道:阿里巴巴中台战略思想与架构实战11、尽在双11:阿里巴巴技术演进与超越12、大型网站系统与Java中间件实践13、架构探险—从零开始写Java Web框架14、架构探险:从零开始写分布式服务框架15、软件架构师的12项修炼16、Web信息架构设计大型网站17、深入分析Java Web技术内幕(修订版)18、实用负载均衡技术:网站性能优化攻略19、ZeroC Ice权威指南20、架构之美从过去到未来从web server到web containerServlet从NIO到WebSocket同步到异步AFK拆分原则前后端分离原则服务无状态原则通信无状态原则
1. 基本概念1.1. 进程进程即运行中的程序,比如当你双击QQ.exe这个程序时,操作系统就会启动一个进程。一个程序可以启动多个进程。(比如你可以运行多个QQ.exe程序,相当于启动了多个进程)1.2. 线程线程是进程中最小的执行单元。下面我们用一个程序来了解什么是线程:package basic_concepts; import java.util.concurrent.TimeUnit; public class WhatIsThread { private static class Thread1 extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { try { TimeUnit.MICROSECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread1"); } } } public static void main(String[] args) { // 普通方法调用 new Thread1().run(); // 启动一个线程,并执行它的 run 方法 new Thread1().start(); for (int i = 0; i < 10; i++) { try { TimeUnit.MICROSECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("main"); } } }在上面的程序中,线程的 run 方法相当于普通方法的调用,它会以程序流的顺序执行,而 start 方法相当于启动了一个线程,这个线程会和 main线程交替执行,因此我们可以在控制台上看到 “Thread1”和“main”交替进行打印,这也说明了不同的线程是进程中不同的执行路径,Thread1 线程和 main 线程代表了上述程序中两条不同的执行路径。在 Java 中创建线程有三种方式,一种是继承 Thread 类,一种是实现 Runnable 接口,还有一种是使用线程池来创建线程:package basic_concepts; public class HowToCreateThread { static class MyThread extends Thread { @Override public void run() { System.out.println("Hello MyThread!"); } } static class MyRun implements Runnable { @Override public void run() { System.out.println("Hello MyRun!"); } } public static void main(String[] args) { // 通过继承 Thread 类来创建线程 new MyThread().start(); // 通过实现 Runnable 接口来创建线程 new Thread(new MyRun()).start(); // 使用 Runnable 的 lambda 表达式来创建线程 new Thread(() -> { System.out.println("Hello Lambda!"); }).start(); // 使用线程池来创建线程 ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(new MyRun()); executorService.shutdown(); } }Thread 对象的常用方法有 sleep 、yield 和 join :package basic_concepts; public class SleepYieldJoin { public static void main(String[] args) { // testSleep(); // testYield(); testJoin(); } static void testSleep() { new Thread(() -> { for (int i = 0; i < 100; i++) { System.out.println("A" + i); try { // 使线程休眠500毫秒 Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } static void testYield() { new Thread(() -> { for (int i = 0; i < 100; i++) { System.out.println("A" + i); if (i % 10 == 0) { Thread.yield(); } } }).start(); new Thread(() -> { for (int i = 0; i < 100; i++) { System.out.println("B" + i); if (i % 10 == 0) { Thread.yield(); } } }).start(); } static void testJoin() { Thread t1 = new Thread(() -> { for (int i = 0; i < 100; i++) { System.out.println("A" + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t2 = new Thread(() -> { try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0; i < 100; i++) { System.out.println("B" + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); t2.start(); } }其中 sleep 会使当前线程休眠,进入阻塞状态,如果线程在睡眠状态被中断,将会抛出 InterruptedException 中断异常,yield 会使当前线程暂停并允许其他线程执行,当一个线程执行了yield()方法之后,就会进入就绪状态,CPU 此时就会从就绪状态线程队列中选择与该线程优先级相同或者更高优先级的线程去执行(因此执行了yield()方法之后的线程仍有可能继续执行,如果此时没有和它优先级相同或者比它优先级更高的线程时),join 方法让一个线程等待另外一个线程完成才继续执行,比如在线程 A 执行体中调用 B 线程的join()方法,则 A 线程将会被阻塞,直到 B 线程执行完为止,A 才能得以继续执行。线程状态package basic_concepts; public class ThreadState { static class MyThread extends Thread { @Override public void run() { System.out.println(this.getState()); for (int i = 0; i < 10; i++) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(i); } } } public static void main(String[] args) { Thread t = new MyThread(); // 通过 getState 方法获取线程状态 System.out.println(t.getState()); t.start(); try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(t.getState()); } }线程的上下文切换用户态 - 内核态int 0x80 - 128sysenter cpu支持保存用户态现场寄存器压栈进行syscall内核态返回 eax恢复用户态现场用户程序继续执行1.3. 纤程/协程CPU - Ring0 - 1 2 - Ring3Ring0 -> 内核态 Ring3 -> 用户态内核调用/系统调用 线程的操作用户态启动线程进入到内核态 - 保存用户态的线程用户态不经过内核态的线程 - 纤程 golang的go程2. synchronized关键字synchronized关键字用于对某个对象加锁,加锁的对象对资源是独占的。它用于解决多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。下面的代码展示了 synchronized 常见的应用场景:package synchronize; /** * synchronized关键字 * 对某个对象加锁 */ public class Synchronize { private static int count = 10; private Object object = new Object(); public void lockObject() { /** * 不能用String常量 Integer Long */ synchronized (object) { count --; System.out.println(Thread.currentThread().getName() + " count = " + count); } } public void lockThis() { synchronized (this) { count --; System.out.println(Thread.currentThread().getName() + " count = " + count); } } /** * 等同于在方法的代码执行时要synchronized (this) */ public synchronized void synchronizedMethod() { count --; System.out.println(Thread.currentThread().getName() + " count = " + count); } /** * 等同于synchronized(Synchronize.class) */ public synchronized static void staticSynchronizedMethod() { count --; System.out.println(Thread.currentThread().getName() + " count = " + count); } public static void lockClass() { synchronized (Synchronize.class) { count --; System.out.println(Thread.currentThread().getName() + " count = " + count); } } static class MyRun implements Runnable { @Override public void run() { Synchronize synchronize = new Synchronize(); synchronize.lockObject(); } } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(new MyRun()).start(); } } }synchronized 关键字既能保证可见性,又能保证可见性,因此对于下面这个程序,它无论运行多少次,结果都会是 0 :package synchronize; public class ThreadSafe implements Runnable { private int count = 100; @Override public synchronized void run() { count --; System.out.println(Thread.currentThread().getName() + " count = " + count); } public static void main(String[] args) { ThreadSafe threadSafe = new ThreadSafe(); for (int i = 0; i < 100; i++) { new Thread(threadSafe, "Thread" + i).start(); } } }同步方法和非同步方法是可以同时调用的,在下面的程序中,非同步方法会穿插在同步方法中执行:package synchronize; /** * 同时调用同步和非同步方法 */ public class SynchronizeCallNonSynchronize { public synchronized void synchronizedMethod() { System.out.println(Thread.currentThread().getName() + " synchronized method start..."); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " synchronized method end"); } public void nonSynchronizedMethod() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " non-synchronized method call"); } public static void main(String[] args) { SynchronizeCallNonSynchronize synchronizeCallNonSynchronize = new SynchronizeCallNonSynchronize(); /*new Thread(() -> synchronizeCallNonSynchronize.synchronizedMethod(), "t1").start(); new Thread(() -> synchronizeCallNonSynchronize.nonSynchronizedMethod(), "t2").start();*/ new Thread(synchronizeCallNonSynchronize::synchronizedMethod, "t1").start(); new Thread(synchronizeCallNonSynchronize::nonSynchronizedMethod, "t2").start(); } }让我们来看一下 synchronized 另一个应用场景,有这样一个面试题:需要你模拟银行账户的存款取款操作,下面的程序模拟了这一过程,需要注意的是,对 getBalance 方法也需要使用 synchronized 关键字来修饰,否则会产生脏读问题:package synchronize; import java.util.concurrent.TimeUnit; /** * 模拟银行账户 */ public class ThreadSafeAccount { private String name; private double balance; public synchronized void set(String name, double balance) { this.name = name; try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } this.balance = balance; } /** * 如果对业务写方法加锁, * 而对业务读方法不加锁, * 会产生脏读问题(dirtyRead) * @param name * @return */ public synchronized double getBalance(String name) { return this.balance; } public static void main(String[] args) { ThreadSafeAccount account = new ThreadSafeAccount(); new Thread(() -> account.set("zhangsan", 100.0)).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(account.getBalance("zhangsan")); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(account.getBalance("zhangsan")); } }synchronized 是可重入的锁,它允许同一线程重复获得锁。下面的程序验证了 synchronized 是可重入的,如果 synchronized 不可重入,那么下面的程序将会产生死锁,但是事实证明,下面的程序能正常执行完,因此 synchronized 是可重入的:package synchronize; import java.util.concurrent.TimeUnit; /** * 一个同步方法可以调用另一个同步方法, * 一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁, * 也就是说synchronized获得的锁是可重入的 */ public class ReentrantSynchronized { synchronized void m() { System.out.println("m start"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m end"); } synchronized void m1() { System.out.println("m1 start"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } m2(); System.out.println("m1 end"); } synchronized void m2() { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m2"); } static class Child extends ReentrantSynchronized { @Override synchronized void m() { System.out.println("child m start"); super.m(); System.out.println("child m end"); } } public static void main(String[] args) { new ReentrantSynchronized().m1(); new Child().m(); } }要特别注意的是,在同步业务逻辑中,要非常小心的处理异常,因为一旦产生异常,默认情况下锁会被释放。在下面的程序中,当 count = 5 时,程序产生的异常会导致线程 t1 的锁被释放了,从而让 t2 线程得到锁执行:package synchronize; import java.util.concurrent.TimeUnit; /** * 程序在执行过程中,如果出现异常,默认情况锁会被释放 * 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。 * 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适, * 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。 * 因此要非常小心的处理同步业务逻辑中的异常。 */ public class SynchronizeCatchException { int count = 0; synchronized void m() { System.out.println(Thread.currentThread().getName() + " start"); while (true) { count ++; System.out.println(Thread.currentThread().getName() + " count = " + count); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } if (count == 5) { // 此处抛出异常,锁将被释放,要想不释放锁,可以在这里进行catch,然后让循环继续 int i = 1 / 0; System.out.println(i); } } } public static void main(String[] args) { SynchronizeCatchException synchronizeCatchException = new SynchronizeCatchException(); new Thread(synchronizeCatchException::m, "t1").start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(synchronizeCatchException::m, "t2").start(); } }HotSpot虚拟机在对象头(64位)上拿出两位(mark word,这两位记录了锁的类型)来记录对象是否被锁定。JDK早期的时候,synchronized的实现是重量级的(OS级,找操作系统申请锁),这样造成早期的时候synchronized的效率非常低。后来jdk针对synchronized做了一些改进,引入了锁升级的概念。锁升级:只有一个线程访问的时候,先在对象头上的markword中记录这个线程的ID(偏向锁)如果有线程争用,升级为自旋锁如果线程自旋多次(默认10次)以后还是无法获取到锁,则会升级为重量级锁(OS)锁只能升级不能降级执行时间短(加锁代码),线程数少,用自旋。执行时间长,线程数多,用系统锁。
1 前言Java 编程语言允许线程访问共享变量。作为规则,为了确保共享变量被一致并可靠地更新,线程应该确保独占地使用这种变量,其惯用的方式是通过获取锁来实现,即强制线程互斥地使用这些变量。Java 编程语言还提供了第二种机制,即 volatile,volatile 的意思是可见的,常用来修饰某个共享变量,意思是当共享变量的值被修改后,会及时通知其它线程,其它线程就能知道当前共享变量的值已经被修改了。在某些方面,它比加锁机制要方便。字段可以被声明为 volatile ,此时 Java 内存模型会确保所有线程看到的都是该变量的一致的值。如果 final 变量同时也被声明为 volatile ,那么就会产生一个编译时错误。2 volatile 域对于下面的例子:public class NoSync { static int i = 0, j = 0; static void one() { i++; j++; } static void two() { System.out.println("i=" + i + " j=" + j); } public static void main(String[] args) { new Thread(() -> { for (int k = 0; k < 10000; k++) { one(); } }).start(); new Thread(() -> { for (int k = 0; k < 10000; k++) { two(); } }).start(); } }如果一个线程重复地调用方法 one(但是总共不超过 Integer.MAX_VALUE 次),而另一个线程重复地调用方法 two,那么方法 two 打印出的 j 的值偶尔会比 i 的值要大:i=3315 j=3418 i=8350 j=8405 i=9107 j=9152 i=9715 j=9878 i=10000 j=10000 i=10000 j=10000 i=10000 j=10000因为这个示例没有包含任何同步机制,共享变量 i 和 j 可能会被乱序更新。—种可以防止这种乱序行为的方式是将方法 one 和 two 都声明为 synchronized 。public class Sync { static int i = 0, j = 0; static synchronized void one() { i++; j++; } static synchronized void two() { System.out.println("i=" + i + " j=" + j); } public static void main(String[] args) { new Thread(() -> { for (int k = 0; k < 10000; k++) { one(); } }).start(); new Thread(() -> { for (int k = 0; k < 10000; k++) { two(); } }).start(); } }这可以阻止方法 one 和方法 two 被并发地执行,并且可以确保共享变量 i 和 j 都会在方法 one 返回之前被更新。因此,方法 tow 永远都不会看到 j 的值大于 i 的值。实际上,它总是看到 i 和 j 有相同的值。i=3738 j=3738 i=3738 j=3738 i=3738 j=3738 i=3738 j=3738 i=3738 j=3738 i=3919 j=3919 i=3922 j=3922 i=3922 j=3922 i=3922 j=3922 i=3922 j=3922 i=3922 j=3922 i=3922 j=3922另一种方法是将 i 和 j 声明为 volatile:public class Volatile { static volatile int i = 0, j = 0; static void one() { i++; j++; } static void two() { System.out.println("i=" + i + " j=" + j); } public static void main(String[] args) { new Thread(() -> { for (int k = 0; k < 10000; k++) { one(); } }).start(); new Thread(() -> { for (int k = 0; k < 10000; k++) { two(); } }).start(); } }这使得方法 one 和方法 two 可以并发地执行,但是可以确保对共享变量 i 和 j 的访问发生的次数,与所有线程执行这段程序文本时这些访问出现的次数精确相等,并且以完全相同的顺序发生。因此,j 的共享值永远都不会大于 i 的共享值,因为每次对 i 的更新必须在 j 被更新之前反映到 i 的共享值中。但是,有可能会发现,任意给定的对方法 two 的调用都会观察到 j 的值比观察到的 i 的值大许多,因为方法 one 可能会在方法 two 抓取 i 的值的时刻与抓取 j 的值的时刻之间执行了许多次。i=9260 j=9474 i=10000 j=10000 i=10000 j=10000 i=10000 j=10000 i=10000 j=10000 i=10000 j=10000 i=10000 j=100003 volatile的内存语义在了解 volatile 实现原理之前,让我们先来了解一下与其实现原理相关的 CPU 相关的知识。现代计算机中,为了提高处理速度,处理器不直接和内存进行通信,而是会先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。这时候会产生这样一个问题,CPU 缓存中的值和内存中的值可能并不是时刻都同步的,导致线程获取到的用于计算的值可能不是最新的,共享变量的值有可能已经被其它线程所修改了,但此时修改的是机器内存的值,CPU 缓存的还是原来没有更新的值,从而就会导致计算出现问题。那么 volatile 是如何来保证可见性的呢?我们可以在 X86 处理器下通过工具获取 JIT 编译器生成的汇编指令来了解其背后的原理。对于如下的 Java 代码:instance = new Singleton(); // instance 是 volatile 变量转变成的汇编代码如下:0x01a3de1d: movb $0 * 0,0 * 1104800(%esi); 0x01a3de24: lock add1 $0 * 0,(%esp);有 volatile 修饰的共享变量在进行写操作的时候会多出第二行汇编代码,通过查Intel IA-32 架构软件开发者手册的多处理器管理章节可知,Lock 前缀的指令在多核处理器下会引发两件事情:将当前处理器缓存行的数据写回到系统内存。这个写回内存的操作会使在其他 CPU 里缓存里该内存地址的数据无效。缓存行:CPU 高速缓存中可以分配的最小存储单位(通常为64个字节)。处理器填写缓存行时会加载整个缓存行,现代 CPU 需要执行几百次 CPU 指令。如果对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令, 将这个变量所在的缓存行的数据写回到系统内存。但是,就算写回到内存,也不能保证其他处理器会立刻去内存中读取最新的值,这个时候处理器中缓存的值还是原来的旧值,线程在获取这个值执行计算操作时就会有问题。因此,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查字节缓存的值是不是过期了,如果发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。volatile 的两条实现原则是:Lock 前缀指令会引起处理器缓存写回到内存。 Lock 指令首先会尝试锁缓存,如果锁缓存无法保证独占共享内存,则会锁定整个总线。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。CPU每个缓存行标记四种状态(额外两位)Modified:CPU修改了缓存中的数据(缓存中的数据与内存相比有更改过)Exclusive:该缓存数据只由当前CPU使用(缓存中的数据是独享的)Shared:缓存中的数据除了该CPU在读取,其他CPU也在读取Invalid:缓存中的数据在读取时被其他CPU修改过有些无法被缓存的数据(比较大的数据),或者跨越多个缓存行的数据依然必须使用总线锁。参考资料《Java语言规范 基于 Java SE 8》Java Language Specification 8《Java并发编程的艺术》
1 前言这三个关键字常用于捕捉异常的一整套流程,try 用来确定需要捕获异常的代码的执行范围,catch 捕捉可能会发生的异常,finally 用来执行一定要执行的代码块。除此之外,我们还需要清楚,每个语句块如果发生异常会怎么办,让我们来看下面这个例子:public class TryCatchFinallyDemo { private static Logger log = Logger.getLogger("TryCatchFinallyDemo"); public static void testCatchFinally() { try { log.info("try is run"); if (true) { throw new RuntimeException("try exception"); } } catch (Exception e) { log.info("catch is run"); if (true) { throw new RuntimeException("catch exception"); } } finally { log.info("finally is run"); } } public static void main(String[] args) { testCatchFinally(); } }这个代码演示了在 try、catch 中都遇到了异常的情况,从输出结果可以看出来:代码的执行顺序为:try -> catch -> finally。六月 27, 2020 2:30:04 下午 com.l1fe1.exception.TryCatchFinallyDemo testCatchFinally 信息: try is run 六月 27, 2020 2:30:04 下午 com.l1fe1.exception.TryCatchFinallyDemo testCatchFinally 信息: catch is run 六月 27, 2020 2:30:04 下午 com.l1fe1.exception.TryCatchFinallyDemo testCatchFinally 信息: finally is run Exception in thread "main" java.lang.RuntimeException: catch exception at com.l1fe1.exception.TryCatchFinallyDemo.testCatchFinally(TryCatchFinallyDemo.java:17) at com.l1fe1.exception.TryCatchFinallyDemo.main(TryCatchFinallyDemo.java:25)此外,我们还可以看出两点:finally 先执行后,再抛出 catch 的异常;最终捕获的异常是 catch 的异常,try 抛出来的异常已经被 catch 吃掉了,所以当我们遇见 catch 也有可能会抛出异常时,我们可以先打印出 try 的异常,这样 try 的异常在日志中就会有所体现。2. try语句在 try 关键字之后紧跟着的 Block 被称为 try 语句 的 try 块。在 finally 关键字之后紧跟着的 Block 被称为 try 语句 的 finally 块。try 语句可以有 catch 子句,这些子句也被称为异常处理器。catch 子句有且只有一个参数,这个参数被称为异常参数。异常参数可以将它的类型表示成单一的类类型(uni-catch子句):try { // try block } catch (Exception e) { // uni-catch block }也可以表示成两个或者更多类类型(multi-catch子句)的联合体(这些类型称为可选择项)。联合体中的可选择项在语法上用 | 隔开:try { // try block } catch (ArithmeticException | ArrayIndexOutOfBoundsException e) { // multi-catch block }用来表示异常参数的每个类类型都必须是 Throwable 类 或 Throwable 的子类,否则就会产生编译错误。如果类型变量被用来表示异常参数的类型,会产生编译错误。如果类型联合体包含两个可选项Di和Dj(i ≠ j),其中Di是Dj的子类型,那么就会产生编译错误。例如如下语句:try { // try block } catch (ArithmeticException | Exception e) { // multi-catch block }会产生 Types in multi-catch must be disjoint: 'java.lang.ArithmeticException' is a subclass of 'java.lang.Exception' 的编译错误。multi-catch子句的异常参数如果没有被显式声明为 final,那么就会被隐式声明为 final。如果显式或隐式声明为 final 的异常参数在 catch 子句体内被赋值,那么就会产生编译错误。try { // try block } catch (ArithmeticException | ArrayIndexOutOfBoundsException e) { // Cannot assign a value to final variable 'e' 编译错误 e = new ArithmeticException(); }uni-catch子句的异常参数从来都不会被隐式声明为 final,但是它可以被显式声明为 final 或是有效的 final(未被显式声明为 final 但未对它重新赋过值)。隐式 final 的异常参数是因其声明的特性而是 final 的,而有效的 final 的异常参数是因其被使用方式的特性而是 final 的。multi-catch子句的异常参数隐式的声明为 final,因此永远不会作为赋值操作的左操作数而出现,但是它不会被认为是有效的 final。如果uni-catch子句的异常参数被显式声明为 final 的,那么移除 final 修饰符会引入编译时错误。这是因为这样的异常参数尽管仍旧是有效的 final,但是再也不能被像 catch 子句体中声明的匿名类和局部类这样的类引用了。另一方面,如果没有任何编译时错误,那么可以在将来变更程序,使得异常参数被重新赋值,这时它就不再是有效的 final 了。异常处理器会按照从左到右的顺序被考虑是否合适:最靠前的可以接受异常的 catch 子句将被抛出的异常对象当作其引用参数而接收。multi-catch 子句可以被看作是uni-catch 子句序列。即异常参数类型表示为联合体D1 | D2 | ... | Dn的catch子句等价于 n 个异常类型分别是D1,D2,...,Dn的 catch 子句序列。在这 n 个 catch 子句的每个 Block 中,异常参数的声明类型都是lub(D1,D2,...,Dn)。例如,下面的代码:try { ... throws ReflectiveOperationException ... } catch (ClassNotFoundException | IllegalAccessException ex) { // ... body ... }在语义上等价于下面的代码:try { ... throws ReflectiveOperationException ... } catch (final ClassNotFoundException ex1) { final ReflectiveOperationException ex = ex1; // ... body ... } catch (final IllegalAccessException ex2) { final ReflectiveOperationException ex = ex2; // ... body ... }其中,具有两个可选项的 multi-catch 子句已经被转译成两个分离的 catch 子句,每个对应一个选项。Java 编译器既不要求也不推荐以这种方式通过重复代码来编译 multi-catch 子句,因为在 class 文件中无需重复就可以表示 multi-catch 子句。2.1 try-catch 的执行不带 finally 块的 try 语句是由先执行 try 块而开始的。然后有以下选择:如果 try 块的执行正常结束,那么就不会有更进一步的动作。如果 try 块的执行因为一个值为 v 的 throw 对象而结束,那么会有以下选择:如果 v 的运行时类型与 try 语句的任何 catch 子句的可捕获异常类是赋值兼容的,那么第一个(最左边)的 catch 子句将被选中执行,值 v 被赋值给这个 catch 子句的参数,然后有以下两种情况:如果该块正常结束,那么该 try 语句正常结束。如果该块因某个原因而异常结束,那么该 try 语句也会以同样的原因而异常结束。如果 v 的运行时类型与 try 语句的任何 catch 子句的可捕获异常类都不是赋值兼容的,那么该 try 语句就会因为一个 v 值的 throw 对象而异常结束。如果 try 块的执行因某个原因而猝然结束,那么该 try 语句也会以同样的原因而猝然结束。class BlewIt extends Exception { BlewIt() { } BlewIt(String s) { super(s); } } public class TestTryCatch { static void blowUp() throws BlewIt { throw new BlewIt(); } public static void main(String[] args) { try { blowUp(); } catch (RuntimeException r) { System.out.println("Caught RuntimeException"); } catch (BlewIt b) { System.out.println("Caught BlewIt"); } } }在这里,BlewIt 异常是 blowUp 方法抛出的。在 main 方法体中的 try-catch 语句有两个 catch 子句。异常的运行时类型是 BlewIt,它对 RuntimeException 类型的变量是不可赋值的,但是它对 BlewIt 类型的变量是可赋值的,因此这个示例的输出为:Caught BlewIt2.2 try-finally 和 try-catch-finally 的执行带 finally 块的 try 语句也是由先执行 try 块而开始的。然后有以下选择:如果 try 块的执行正常结束,那么 finally 块就会被执行:如果 finally 块正常结束,那么 try 语句正常结束。如果 finally 块因某个原因而猝然结束,那么 try 语句会因同样的原因而猝然结束。如果 try 块的执行因为一个值为 v 的 throw 对象而猝然结束:如果 v 的运行时类型与 try 语句的任何 catch 子句的可捕获异常类是赋值兼容的,那么第一个(最左边)的 catch 子句将被选中执行,值 v 被赋值给这个 catch 子句的参数,然后有以下选择:如果该 catch 块正常结束,那么 finally 块就会被执行。然后有以下两种情况:如果该 finally 块正常结束,那么 try 语句正常结束。如果该 finally 块因某个原因而猝然结束,那么 try 语句会因同样的原因而猝然结束。如果该 catch 块因为某个原因 R 而猝然结束,那么 finally 块就会被执行。然后有以下两种情况:如果 finally 块正常结束,那么该 try 语句就会因为 R 而猝然结束。如果 finally 块因为某个原因 S 而猝然结束,那么该 try 语句就会因为 S 而猝然结束(并且原因 R 会被丢弃)。如果 v 的运行时类型与 try 语句的任何 catch 子句的可捕获异常类都不是赋值兼容的,那么 finally 块就会被执行。然后有以下选择:如果 finally 块正常结束,那么 try 语句就会因为一个值为 v 的 throw 对象而猝然结束。如果 finally 块因为某个原因 S 而猝然结束,那么该 try 语句就会因为 S 而猝然结束(并且值为 V 的 throw 对象会被丢弃和忘记)。如果 try 的执行因为任何其他原因 R 而猝然结束,那么 finally 块就会被执行:如果 finally 块正常结束,那么该 try 语句就会因为 R 而猝然结束。如果 finally 块因为某个原因 S 而猝然结束,那么该 try 语句就会因为 S 而猝然结束(并且原因 R会被丢弃)。public class TestTryCatchFinally { static void blowUp() throws BlewIt { throw new NullPointerException(); } public static void main(String[] args) { try { blowUp(); } catch (BlewIt b) { System.out.println("Caught BlewIt"); } finally { System.out.println("Uncaught Exception"); } } }这个程序会产生以下输出:Uncaught Exception Exception in thread "main" java.lang.NullPointerException at com.l1fe1.exception.TestTryCatchFinally.blowUp(TestTryCatchFinally.java:5) at com.l1fe1.exception.TestTryCatchFinally.main(TestTryCatchFinally.java:9)blowup 方法抛出的 NullPointerException(RuntimeException的一种)没有被 main 中的任何 catch 语句捕获,因为 NullPointerException 对象对于 BlewIt 类型的变量来说,不是赋值兼容的。finally 子句会被执行,之后执行 main 的线程,也就是该测试程序的唯一线程,将会因为未捕获的异常而终止,这通常会导致在控制台打印异常名和简单的回溯追踪的情况。2.3 try-with-resources带资源的 try 语句是用变量(被称为资源)来参数化的,这些资源在 try 块执行之前被初始化,并且会在 try 块执行之后,自动地以与初始化相反地顺序被关闭。当资源会被自动化关闭时,catch 子句 和 finally 子句通常就不是必需的了。TryWithResourcesStatement: try ResourceSpecification Block [Catches] [Finally] ResourceSpecification: ( ResourceList [;] ) ResourceList: Resource {; Resource} Resource: {VariableModifeier} UnannType VariableDeclaratorId = ExpressionResourceSpecification 用初始化器表达式声明了一个或多个局部变量作为 try 语句中的 Resource 。对于ResourceSpecification 来说,声明两个具有相同名字的变量会产生编译错误。如果 final 作为修饰符在每一个在 ResourceSpecification 中声明的变量中出现了多次,那么就是一个编译时错误。如果没有被显式地声明为 final ,那么在 ResourceSpecification 中声明的资源会被隐式地声明为final。在 ResourceSpecification 中声明的变量的类型必须是 AutoCloseable 的子类型,否则就会产生编译时错误。资源是按照从左到右的顺序初始化的。如果某个资源初始化失败了(即,其初始化器表达式抛出了异常),那么所有已经被带资源的 try 语句初始化的资源都将被关闭。如果所有资源都成功初始化了,那么 try 块会正常执行,然后该带资源的 try 语句的所有非空资源都将被关闭。资源将以与它们被初始化的顺序相反的顺序被关闭。资源只有在其被初始化为非空值时才会被关闭。在关闭资源时抛出的异常不会阻止其他资源的关闭。如果之前在某个初始化器、try 块或资源关闭中抛出过异常,那么这种异常会被压制。带有声明了多种资源的 ResourceSpecification 子句的带资源 try 语句会被当作多个带资源的 try 语句对待,其中每个都有一个声明了单一资源的 ResourceSpecification 子句。当带有 n (n > 1) 个资源的带资源 try 语句被转译时,其结果是带有 n - 1 个资源的带资源 try 语句。在 n 次这样的转译之后,就会产生 n 个嵌套的 try-catch-finally 语句,至此所有的转译就结束了。2.3.1 基本的带资源的 try 语句不带任何 catch 子句或 finally 子句的带资源的 try 语句被称为基本的带资源的 try语句。基本的带资源的 try 语句:try ({VariableModifier} R Identifier = Expression ...) Block其含义是由下面转译成的局部变量声明和 try-catch-finally 语句给出的:{ final {VariableModifierNoFinal} R Identifier = Expression; Throwable #primaryExc = null; try ResourceSpecification_tail Block catch (Throwable #t) { #primaryExc = #t; throw #t; } finally { if (Identifier != null) { if (#primaryExc != null) { try { Identifier.close(); } catch (Throwable #suppressedExc) { #primaryExc.addSuppressed(#suppressedExc); } } else { Identifier.close(); } } } }{VariableModifierNoFinal}是作为不带 final 的{VariableModifier}而定义的(如果它存在的话)。#t、#primaryExc 和 #suppresedExc 是自动生成的标识符,它们有别于在带资源的 try 语句出现之处位于其作用域中的其他任何标识符(无论是自动生成的还是其他)。如果 ResourceSpecification 声明了一个资源,那么 ResourceSpecification_tail 就是空的(并且该 try-catch-flnally 语句自身并不是一个带资源的 try 语句)。如果 ResourceSpecification 声明了 n > 1 个资源,那么在 ResourceSpecification_tail 中就以同样的顺序包含了在 ResourceSpecification 中的第2个、第3个、…、第 n 个资源(并且该 try-catch-finally 语句自身也是一个带资源的 try 语句)。用于基本的带资源 try 语句的可达性和明确赋值规则由上面的转译隐式地进行了说明。在只管理单一资源的基本的带资源 try 语句中:如果资源初始化因为一个 V 值的 throw 对象而猝然结束,那么该带资源的 try 语句也会因 V 值的 throw 对象而猝然结束。如果资源初始化正常结束,并且 try 块因为一个 V 值的 throw 对象而猝然结束,那么:如果所有成功初始化过的资源(可能是0个)的自动化关闭都正常结束,那么该带资源的 try 语句就会因 V 值的throw对象而猝然结束。如果所有成功初始化过的资源(可能是0个)的自动化关闭因V1...Vn值的 throw 对象而猝然结束,那么该带资源的 try 语句就会因 V 值的 throw 对象而猝然结束,剩余的V1...Vn值将被添加到被 V 压制的异常列表中。如果所有资源的初始化都正常结束,并且 try 块因一个 V 值的 throw 对象而猝然结束,那么:如果所有初始化过的资源的自动关闭正常结束,那么该带资源的 try 语句就会因 V 值的 throw 对象而猝然结束。如果一个或多个初始化过的资源的自动关闭因V1...Vn值的 throw 对象而猝然结束,那么该带资源的 try 语句就会因 V 值的 throw 对象而猝然结束,剩余的V1...Vn值将被添加到被 V 压制的异常列表中。如果每个资源的初始化都正常结束,并且 try 块正常结束,那么:如果某个初始化过的资源的某次自动关闭因一个 V 值的 throw 对象而猝然结束,并且其他所有初始化过的资源的自动关闭都正常结束,那么该带资源的 try 语句就会因 V 值的 throw 对象而猝然结束。如果某个初始化过的资源的超过一次的自动关闭因V1...Vn值的 throw 对象而猝然结束,那么该带资源的 try 语句就会因V1而猝然结束,剩余的V2...Vn值将被添加到被 V1 压制的异常列表中(其中V1是从最右边的关闭失败的资源中抛出的异常,而 Vn 是从最左边的关闭失败的资源中抛出的异常)。2.3.2 扩展的带资源的 try 语句带有至少一个 catch 子句或 finally 子句的带资源的 try 语句被称为扩展的带资源的 try 语句。扩展的带资源的 try 语句:try ResourceSpecification Block {Catches} {Finally}其含义是由下面转译成的嵌套在try-catch、try-finally 或 try-catch-flnally 语句中的基本的带资源的 try 语句给出的:try { try ResourceSpecification Block } {Catches} {Finally}这种转译的效果就像是将 ResourceSpecification 放置到 try 语句的“内部” 一样。这使得扩展的带资源的 try 语句的 catch 子句可以捕获异常,因为任何资源都会自动地初始化和关闭。更进一步,所有资源在 finally 块被执行的时刻都已经被关闭(或尝试被关闭),这与 finally 关键词的意图也保持了一致。3. 面试题catch 中发生了未知异常,finally 还会执行么?会的,catch 无论是否发生异常,finally 总会执行,并且 catch 中的异常是在 finally 执行完成之后,才会抛出的。不过 catch 会吃掉 try 中抛出的异常,为了避免这种情况,在一些可以预见 catch 中会发生异常的地方,先把 try 抛出的异常打印出来,这样从日志中就可以看到完整的异常了。参考资料《Java语言规范 基于 Java SE 8》Java Language Specification 8
1 HDFS初识考虑这样一个问题:文件切成很多小文件块散列存储在集群中时,是如何知道每个小文件块存储的位置的呢?让我们举个例子来解释一下,假设现在有100台机器,如果有10个人拿着10批数据过来存储,那么他们可能会找到不同的人把他们的数据各自存储在不同的机器上,过了10天之后,这些人想要取回他们的数据,但是他们忘了自己的数据存储在谁那里了,那么这些数据就取不回来了。那么该如何解决这个问题呢?我们可以单独拿出一台机器用于记录,那么每当有人要存储数据的时候,都先找到这台机器,然后这台机器会告诉他将数据存储在哪,这个询问的过程是很快的,每个人问完之后就会拿着数据去存储,然后下一个人过来接着问...如果这个例子你能听懂,那么恭喜你,HDFS你已经学会了。上述的过程其实就是指的HDFS中的NameNode和DataNode各司其职的过程,DataNode用于存储数据,而NameNode用于存储文件的元数据描述信息。让我们来进一步思考一下,如果把上述例子中的文件数据换成一张一张的一百块钱,比如有个人过来要存100块钱,我说你去张三那存吧,这时候我应该立刻写下谁谁谁的100块钱存在张三那了吗?这是不可能的,因为有可能我记完之后这个人没去张三那存,如果我记下了这笔帐,那个人回来找我取回那100块钱,我让他去张三那,那张三就有苦说不出了,这就是所谓的数据一致性问题。数据的一致性问题一定是要非常小心的,尤其是在分布式、多节点多人协作的情况下。因此在上面的例子中,一定是要张三给我发送一个确认的回报之后,我才能把这个100块钱给记录下来的。HDFS就是这样一个用多个节点散列未来要存储的数据,然后用一个主节点进行记账的分布式文件系统。此外,记账这件事情,一个人可以做,两个人也可以做,但是两个人记账中面临的数据一致性问题会更加复杂(两个人之间同步信息),因此HDFS采用了一主的架构。分布式文件系统那么多,为什么hadoop项目中还要开发一个hdfs文件系统?我们知道,在hadoop项目里面除了hdfs文件系统模块之外,还有另外一个很重要的用于计算的模块,因此我们可以推测出,hdfs一定会具备一个特征:它能更好的支持分布式计算。2 HDFS理论知识点存储模型架构设计角色功能元数据持久化安全模式副本放置策略读写流程安全策略2.1 存储模型文件线性按字节切割成块(block),具有offset,idoffset就是指偏移量,文件的第一个block块偏移量为0,后续block块的偏移量可以通过block块的大小来计算,只要有了offset,就能把所有的block块拼成一个完整的文件。id标识了每个block块的名字,用于映射文件与文件的block大小可以不一样一个文件除最后一个block,其他block大小一致比如block大小为4个字节,而最后一个block块的大小可能不够4个字节block的大小依据硬件的I/O特性调整block被分散存放在集群的节点中,具有location(block块所在节点的位置)Block具有副本(replication),没有主从概念,副本不能出现在同一个节点中如果副本放在一个节点中,那么如果这个节点挂掉了,所有副本都读取不到了副本是满足可靠性和性能的关键文件上传可以指定block大小和副本数,上传后只能修改副本数一次写入多次读取,不支持修改比如在文件中增加一些内容,这会导致修改位置所在的block大小变大,这时候会将这个block块中多出来的内容转移到下一个block块中,而这又会导致下一个block块大小变大...这是一个泛洪的操作,这会导致集群中很多的节点,它们的CPU、内存、网卡会参与到因为一个修改的事情而造成的资源的高度使用,网络一直被疯狂的传输数据,因此hdfs设计者做了一个折中的方案,就是hdfs作为一个文件系统,它可以存,可以读,可以批量计算,它可以支持很多程序进行计算,它可以让每个程序都跑得很快,但是它不支持修改。支持追加数据追加数据只是在文件的最后一个block块中添加内容,或者是在文件的最后添加block块,因此是可以支持的。无论是什么文件,在计算机中存储的都是二进制,二进制在计算机中就是一个个二进制位,但是在计算机中,一般很少用二进制位去描述文件,而是会用字节去描述,我们看到的文件大小一般都是用字节作为单位的,文件本质上都是字节数组。2.2 架构设计HDFS是一个主从(Master/Slaves)架构由一个NameNode(主)和一些DataNode(从)组成面向文件包含:文件数据(data)和文件元数据(metadata)NameNode负责存储和管理文件元数据,并维护了一个层次型的文件目录树DataNode负责存储文件数据(block块),并提供block的读写DataNode与NameNode维持心跳,并汇报自己持有的block信息Client和NameNode交互文件元数据、和DataNode交互文件block数据Windows和Linux文件系统的差异。Windows和Linux上都会有硬盘,而硬盘上都有分区,在windows上分区对应的是盘符(c盘、d盘),这个时候如果你想要存一些目录,需要自主去找一个分区,在分区下去存一级目录、二级目录,而linux中虽然也有两个分区,但是它的分区会挂载到它内存的虚拟目录树结构上,它是由一个虚拟的根起,根下的A目录可能是你的第一个分区,B目录可能是你的第二个分区。因此在你使用linux系统的时候,你好像感觉不到底层到底分了几个分区,而在使用windows的时候,容易混乱。比如说,如果你在windows中写了一个软件,这个软件必须从G盘加载一个文件(conf、xml),那么这个软件在其他人的电脑上面可能就跑不起来了,因为有的电脑上根本就没有G盘,而如果你使用的是linux系统,linux系统除了它的根目录结构之外,还有它的mount挂载(/g -> disk:G分区、/b -> disk:B分区),上述软件如果是在linux系统上面开发的,假如它的文件加载路径还是写死的,比如从/g目录下加载文件,这时候其他电脑可能也没有/g目录,但是这时它可以创建一个/g目录(可以挂载在不同的分区上,它的映射关系可以随便换),因此linux相对于windows来说使得软件具备了移动性。2.3 角色功能NameNode完全基于内存存储文件元数据、目录结构、文件block的映射需要持久化方案保证数据可靠性提供副本放置策略DataNode基于本地磁盘存储block(文件的形式)并保存block的校验和数据保证block的可靠性与NameNode保持心跳,汇报block列表状态角色即进程。HDFS并没有真正存储数据,只是管理映射。2.4 元数据持久化任何对文件系统元数据产生修改的操作,Namenode都会使用一种称为EditsLog的事务日志记录下来使用FsImage存储内存所有的元数据状态使用本地磁盘保存EditsLog和FsImageEditsLog具有完整性,数据丢失少,但恢复速度慢,并有体积膨胀风险FsImage具有恢复速度快,体积与内存数据相当,但不能实时保存,数据丢失多NameNode使用了FsImage+EditsLog整合的方案:滚动将增量的EditsLog更新到FsImage,以保证更近时点的FsImage和更小的EditsLog体积数据持久化方式日志文件(文本文件):记录(append)实时发生的增删改操作(mkdir /abc),通过读取日志文件重放每一行指令来恢复数据优点:完整性比较好缺点:加载恢复数据慢、占空间镜像、快照、dump、db(二进制文件):间隔的(小时,天,10分钟,1分钟,5秒钟),内存全量数据基于某一时间点做的向磁盘的溢写优点:恢复速度快过日志文件缺点:因为是间隔的,容易丢失一部分数据HDFS:EditsLog:日志文件体积小,记录少:必然有优势FsImage:镜像、快照如果能更快的滚动更新时点,必然有优势HDFS采用的是最近时点的FsImage + 增量的EditsLog的持久化方案。比如现在10点,HDFS中记录了9点的FsImage + 9点到10点的增量的EditsLog,那么通过以下步骤就能得到关机前的全量数据:加载FsImage加载EditsLogFsImage时点是怎么滚动更新的?由NameNode8点溢写一次,9点溢写一次(每一小时溢写一次,产生大量I/O,耗费资源)NameNode第一次开机的时候只写一次FsImage(假设8点),到9点的时候,EditsLog记录的是8~9点的日志,只需要将8~9点的日志的记录,更新到8点的FsImage中(使用另外一台机器:Secondary NameNode来做合并),FsImage的数据时点就变成了9点。2.5 安全模式HDFS搭建时会格式化,格式化操作会产生一个空的FsImage当NameNode启动时,它从硬盘中读取EditsLog和FsImage将所有EditsLog中的事务作用在内存中的FsImage上并将这个新版本的FsImage从内存中保存到本地磁盘上然后删除旧的EditsLog,因为这个旧的EditsLog的事务都已经作用在FsImage上了NameNode启动后会进入一个称为安全模式的特殊状态。处于安全模式的NameNode是不会进行数据块的复制的。NameNode从所有的 DatNnode接收心跳信号和块状态报告。每当NameNode检测确认某个数据块的副本数目达到这个最小值,那么该数据块就会被认为是副本安全(safely replicated)的。在一定百分比(这个参数可配置)的数据块被NameNode检测确认是安全之后(加上一个额外的30秒等待时间),NameNode将退出安全模式状态。接下来它会确定还有哪些数据块的副本没有达到指定数目,并将这些数据块复制到其他DataNode上。NameNode存的元数据主要有两种:文件属性、每个块存在哪个DataNode上。在持久化的时候,文件属性会持久化,但是文件的每一个块存在哪不会持久化。恢复的时候,NameNode会丢失块的位置信息。那么为什么NameNode不持久化块的位置信息呢?这就又回到了分布式时代数据一致性的问题了,假如NameNode持久化了块的位置信息,但是由于集群启动时DataNode挂掉了,那么这些块就取不回来了,这就会产生数据不一致的问题,因此NameNode宁可不存块的位置信息,而是等DataNode和NameNode建立心跳,然后向它汇报块的信息,这个过程就叫安全模式。2.6 HDFS中的SNNSecondary NameNode(SNN)在非Ha模式下,SNN一般是独立的节点,周期完成对NN的EditsLog向FsImage合并,减少EditsLog大小,减少NN启动时间根据配置文件设置的时间间隔fs.checkpoint.period 默认3600秒根据配置文件设置edits log大小 fs.checkpoint.size 规定edits文件的最大值默认是64MBSNN存在的意义就是让EditsLog很小、恢复很快。2.7 Block的副本放置策略第一个副本:放置在上传文件的DN;如果是集群外提交,则随机挑选一台磁盘不太满,CPU不太忙的节点。第二个副本:放置在与第一个副本不同的机架的节点上。第三个副本:与第二个副本相同机架的节点。更多副本:随机节点。在早期的HDFS中,第一个副本和第二个副本会放在同机架,第三个副本才会出机架,这样当副本数设定为2的时候,如果副本所在机架挂了,会导致这个块丢失,因此在2.x的时候修正了这个问题,第二个副本放在与第一个副本不同的机架上。第二个副本和第三个副本放在同一机架是为了减少跨交换机的成本。2.8 HDFS读写流程2.8.1 HDFS写流程Client和NN连接创建文件元数据NN判定元数据是否有效NN触发副本放置策略,返回一个有序的DN列表Client和DN建立Pipeline连接Client将块切分成packet(64KB),并使用chunk(512B)+chucksum(4B)填充Client将packet放入发送队列dataqueue中,并向第一个DN发送第一个DN收到packet后本地保存并发送给第二个DN第二个DN收到packet后本地保存并发送给第三个DN这一个过程中,上游节点同时发送下一个packet生活中类比工厂的流水线:结论:流式其实也是变种的并行计算Hdfs使用这种传输方式,副本数对于client是透明的当block传输完成,DN们各自向NN汇报,同时client继续传输下一个block所以,client的传输和block的汇报也是并行的Client在与NameNode进行交互的时候,会触发副本放置策略,NameNode会根据副本放置策略,在返回DataNode信息时做一个排序(根据距离),Client本机上的DataNode会排在第一位。然后Client会和第一个DataNode建立tcp连接,第一个DataNode和第二个DataNode建立tcp连接,第二个DataNode和第三个DataNode建立tcp连接,这些连接链路被称为pipline。DataNode如果挂掉会怎么办?如果是第三个DataNode挂掉了,影响最小,因为前面两个DataNode的pipline连接没有断,继续传输packet即可。如果是第二个DataNode挂掉了,那么第一个DataNode直接与第三个DataNode建立连接,然后根据第三个DataNode已经接收的packet来向它传输下一个packet即可。如果是第一个DataNode挂掉了,那么Client直接与第二个DataNode建立连接,然后把剩余的packet传输给第二个DataNode即可。DataNode会向NameNode汇报状态,因此在1个DataNode挂掉之后,由于汇报时的DataNode少了一个,这时NameNode会让某个DataNode从自身再复制一个DataNode出来。2.8.2 HDFS读流程为了降低整体的带宽消耗和读取延时,HDFS会尽量让读取程序读取离它最近的副本。如果在读取程序的同一个机架上有一个副本,那么就读取该副本。如果一个HDFS集群跨越多个数据中心,那么客户端也将首先读本地数据中心的副本。语义:下载一个文件:Client和NN交互文件元数据获取fileBlockLocationNN会按距离策略排序返回Client尝试下载block并校验数据完整性语义:下载一个文件其实是获取文件的所有的block元数据,那么子集获取某些block也应该成立Hdfs支持client给出文件的offset自定义连接哪些block的DN,自定义获取数据这个是支持计算层的分治、并行计算的核心
1 大数据启蒙1.1 分治思想在认识分治思想之前,让我们先来看这样一个需求:我有一万个元素(比如数字或单词)需要存储,如果查找某一个元素,最简单的遍历方式复杂度是多少呢?更进一步,如果我期望的复杂度是O(4)呢?对于第一个需求,我们很容易就能想到可以用数组或者是链表来存储,这样查找某一个元素的时间复杂度分别是O(logn)和O(n):那么对于第二个需求,我们应该如何实现呢?如果你有学过数据结构,那你一定很快就能想到可以用散列表这种结构来实现这个需求,使用2500个长度为4(抛开数据分布不平衡的因素)的链表来存储这10000个元素,在查找的时候,首先通过hash值与2500取模定位到所在的链表数组,然后就能以O(4)的复杂度来查找这个元素了,如下图所示:上述案例中的散列表就体现出了一种分治思想,分而治之的思想非常重要,它被运用在了很多场景中,比如:Redis集群ElasticSearchHbaseHaDoop生态1.2 单机处理大数据问题继续来看这样一个需求:有一个非常大的文本文件,里面有很多很多的行,只有两行一样,它们出现在未知的位置,需要查找到它们。给定的硬件只有一台单机,而且可用的内存很少,也就几十兆。现在我们来分析一下这个问题,假设这个文件有1T大小,I/O速度是500MB每秒,那么1T文件读取一遍需要约30分钟,循环遍历(逐行两两比对)需要N次I/O时间(总共需要30 * N 分钟),使用分治思想可以使时间为2次I/O(也就是1个小时左右)。那么如何利用分治思想呢?我这里提供一种思路:可以计算每一行的hash值,然后对一个基数(比如2000)取模,将文件散列成2000个小文件块(这些文件块小到可以放到内存里),这样会花费一次I/O,也就是30分钟,我们可以知道的是,哈希运算和取模运算都是稳定的运算(两个相同的值无论经过多少次运算,得到的结果总是一样的),因此相同的两行,它们的计算结果一定会相同,也就是说,它们一定会被散列到同一个文件块中,这样再遍历每一个文件块(1次I/O)就能找到某个文件块中重复的两行。tips:内存寻址比I/O寻址快10万倍。现在让我们来考虑另一个问题:如果1T的文件中放的都是数值且都是乱序的,现在我们希望把这些数值做一次全排序,我们应该如何来实现呢?我们都知道哈希这种运算是无序的,因此在这个需求中就不再适用了,这时我们可以使用另一种方式:读取一行中的数值,如果它在某个选定的范围内(假设是1~100),就将它发送到0号小文件块中,如果它在101~200之内,就将它发送到1号小文件块中...依此类推,直到文件中的所有数值都被发送到一个个小的文件块中。这样耗费一次I/O时间可以使得这些数值做到外部(文件块之间)有序,内部(某个文件块中)无序。这时如果我们切分的文件块足够小,我们就可以将它放到内存中做一次排序,这样就能使得数值在每个文件块中也是有序的,从而实现了使用两次I/O完成了所有数值的全排序。我们也可以先读取一个小文件块大小(比如50M)的数值进行排序,这样我们就能得到n多个内部有序,外部无序的小文件块,学过算法的同学都知道有一种算法可以用于外部排序——对,那就是归并排序,因此我们可以使用归并排序对这n多个小文件块进行排序,这样也能使用两次I/O完成数值的全排序。如果你理解了上面的东西,让我们再来做进一步思考:如果把时间变成分钟、秒级,应该如何做到呢?很显然在单机、硬件条件有限的情况下是绝无可能做到的,这也是单机在面临大数据问题的瓶颈所在(内存大小、I/O速度),这个时候集群分布式的优势就体现出来了。1.3 集群分布式处理大数据的辩证思考2000台机器真的比一台机器速度快吗?如果考虑分发上传文件的时间呢?如果考虑每天都有1T数据的产生呢?如果增量了一年,最后一天计算数据呢?让我们对上述问题进行逐一解答,首先2000台机器不一定比一台机器速度快,如果只有1T的数据需要处理,那么由于集群分布式需要先将文件分发上传到2000台机器中,这个过程消耗的网卡I/O(网络I/O比磁盘I/O速度还要慢)传输时间是非常多的,因此这种情况下并不能体现出集群分布式的优势。但是如果考虑每天都会有1T的数据产生,这些数据增量了一年,这个时候集群分布式在处理这些数据的计算时的长处就能充分发挥出来了。在处理增量数据时,集群分布式处理数据的时长并不会随着数据量的增长而有着明显的增长,但是单机在数据量逐渐增长的情况下处理数据所耗费的时间会越来越多。数据量越大,集群分布式的优势就会越明显。1.4 结论总而言之,从上述的分析我们可以得知,大数据技术关注的重心主要有下面几点:分而治之并行计算计算向数据移动数据本地化读取2 Hadoop初识2.1 Hadoop 之父 Doug CuttingDoug Cutting是一位软件设计师,也是开源搜索技术的倡导者和创造者。他创建了Lucene,并与Mike Cafarella一起创建了Nutch,这两个都是开源搜索技术领域的项目,这些项目现在由Apache Software Foundation管理。Cutting和Cafarella也是Apache Hadoop的共同创始人。 -- 摘自维基百科Hadoop的发音是 [hædu:p]Cutting儿子对玩具小象的昵称NutchLuceneAvroHadoop2.2 Hadoop的时间简史《The Google File System 》 2003年《MapReduce: Simplified Data Processing on Large Clusters》 2004年《Bigtable: A Distributed Storage System for Structured Data》 2006年Hadoop由 Apache Software Foundation 于 2005 年秋天作为Lucene的子项目Nutch的一部分正式引入。2006 年 3 月份,Map/Reduce 和 Nutch Distributed File System (NDFS) 分别被纳入称为 Hadoop 的项目中。Cloudera公司在2008年开始提供基于Hadoop的软件和服务。2016年10月hadoop-2.6.52017年12月hadoop-3.0.0hadoop.apache.org2.3 Hadoop项目/生态Hadoop项目包含了这些模块:Hadoop Common:给其他Hadoop模块提供支撑的基础功能类库。Hadoop Distributed File System (HDFS™):提供对应用程序数据的高吞吐量访问的分布式文件系统。Hadoop YARN:作业调度和集群资源管理框架。Hadoop MapReduce:基于YARN的大型数据集并行处理系统。Hadoop Ozone: Hadoop的对象存储。Apache中其他与Hadoop相关的项目包括:Ambari™:一个基于Web的工具,用于配置,管理和监控Hadoop集群,其中包括对Hadoop HDFS,Hadoop MapReduce,Hive,HCatalog,HBase,ZooKeeper,Oozie,Pig和Sqoop的支持。Avro™:数据序列化系统。Cassandra™:可扩展的多主数据库,没有单点故障。Chukwa™:用于管理大型分布式系统的数据收集系统。HBase™:可扩展的分布式数据库,支持大型表的结构化数据存储。Hive™:提供数据汇总和即席查询的数据仓库基础设施。Mahout™: 可扩展的机器学习和数据挖掘库。Pig™:用于并行计算的高级数据流语言和执行框架。Spark™:用于Hadoop数据的快速通用计算引擎。Spark提供了一种简单而富有表现力的编程模型,该模型支持广泛的应用程序,包括ETL,机器学习,流处理和图形计算。Tez™:一个基于Hadoop YARN的通用数据流编程框架,该框架提供了强大而灵活的引擎来执行任意DAG任务,以处理批处理和交互用例的数据。Hadoop生态系统中的Hive™,Pig™和其他框架以及其他商业软件(例如ETL工具)都采用了Tez,以取代Hadoop™MapReduce作为基础执行引擎。ZooKeeper™:面向分布式应用程序的高性能协调服务。2.4 大数据生态Cloudera官网:https://www.cloudera.com/Cloudera’s Distribution Including Apache Hadoop(CDH)是Apache Hadoop和相关项目中最完整的,经过测试的和最受欢迎的发行版。hadoop-2.6.0+cdh5.16.1hbase-1.2.0+cdh5.16.1hive-1.1.0+cdh5.16.1spark-1.6.0+cdh5.16.1
1 前言final 的意思是不变的,一般来说可以用于以下三种场景:被 final 修饰的类,表明该类是无法继承的;被 final 修饰的方法,表明该方法是无法覆写的;被 final 修饰的变量,说明该变量在声明的时候,就必须初始化完成,而且以后也不能修改其内存地址。注意:对于 List、Map 这些集合类来说,被 final 修饰后,是可以修改其内部值的,但却无法修改其初始化时的内存地址。2 修饰的对象final 可以用来修饰变量、字段、方法和类。2.1 变量可以将变量声明为 final 。final 变量的值一旦分配了就无法再修改了。在一次赋值之前,final 变量必须是明确未赋值的,否则会产生编译错误。如果 final 变量包含对对象的引用,则对象的状态可能会通过对对象的操作进行更改,但变量将始终引用相同的对象。这同样适用于数组,因为数组也是对象;如果一个 final 变量持有一个数组的对象,则可以通过对数组的操作修改数组中的元素,但是该变量始终会引用相同的数组。注意,如果 final 用于局部变量,那么它可以不用初始化,而如果用于类中的某个字段,那么它必须进行初始化,否则会产生编译错误。空 final 是指其声明缺少初始化器的 final 变量。常数变量是一个用常量表达式初始化的基本类型或String类型的 final 变量。 变量是否为常数变量与类的初始化,二进制兼容性和明确赋值有关。有三种变量会被隐式声明为 final :接口中的字段,try-with-resources 语句中的局部变量以及multi-catch子句中的异常参数。 一个单一的 catch 子句中的 exception 参数永远不会隐式声明为 final ,但是它可以被认为效果等同于 final 。某些未被声明为 final 变量实际上可以认为效果等同于 final 变量:声明带有初始化器且满足下列所有条件的局部变量:永远不会作为赋值操作符的左操作数出现永远不会作为前缀或后缀递增或递减操作符的操作数出现声明缺少初始化器且满足下列所有条件的局部变量:无论何时它作为赋值操作符的左操作数出现,都是明确未赋过值的,并且在本次赋值操作前未被明确赋值。即它是明确未赋过值的,并且在本次赋值操作的右操作数之后未被明确赋值永远不会作为前缀或后缀递增或递减操作符的操作数出现方法、构造器、lambda 和异常参数在明确其是否可以认定为等同于 final 时可以被认为是声明带有初始化器的局部变量如果变量在效果上等同于 final ,那么对其声明添加 final 修饰符并不会引入任何编译错误。相反地,在合法程序中被声明为 final 的局部变量和参数,如果将它们的 final 修饰符移除,那么它们在效果上会等同于 final 。2.2 类类可以被声明为 final ,如果一个 final 类的名字出现在另一个类声明的 extends 子句中,会产生编译错误,这意味着 final 类不能有任何子类。如果一个类同时被声明为 final 和 abstract ,也会产生编译错误,因为这样的类是永远无法实现的。因为一个 final 类不能有任何子类,所以 final 类的方法永远都不会被覆写。如果将一个没有被声明的 final 类声明为 final ,那么当这个类的已有子类的二进制文件被加载时,就会抛出一个 VerifyError ,因为 final 类不能有任何子类。因此不推荐在广泛分布的类中进行这种变更。反之,将一个声明为 final 的类不再声明为 final ,不会破坏与已有二进制文件的兼容性。2.3 方法方法可以被声明为 final 以阻止子类覆写或者隐藏它。如果试图覆写或者隐藏 final 方法,会产生编译错误。private 方法和所有在 final 类中声明的方法表现就如 final方法一样,因为它们是不可能被覆写的。在运行时,机器码生成器或优化器可以“内联” final 方法的方法体,将方法的调用替换为其方法体中的代码。内联处理必须保留方法调用的语义。特别是,如果实例方法调用的目标为null,那么即使该方法是内联的,也必须抛出空指针异常。一个Java编译器必须确保异常在正确的位置上被抛出,这样在方法调用之前,传递给方法的实际参数将被按正确的顺序计算过。2.4 字段字段可以被声明为 final 。类和实例变量(static 和 非 static 字段)都可以被声明为 final。空 final 类变量必须在声明它的类的静态初始化器中赋过值,否则会产生编译错误。空 final 实例变量必须在声明它的类的每一个构造器的末尾赋过值,否则会产生编译错误。参考资料《Java语言规范 基于 Java SE 8》Java Language Specification 8
1 前言static意思是静态的、全局的,在java中一旦被static修饰,说明被修饰的东西在一定范围内是共享的,谁都可以访问,这时候需要注意并发读写时的线程安全问题。被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问,这使得我们可以很方便的在没有创建对象的情况下来进行方法/变量的调用/访问。2 修饰的对象static 可以用来修饰变量、方法、内部类和代码块。2.1 静态变量使用 static 修饰的变量是属于类的,而不是属于对象的,静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。注意,static 关键字并不能修改变量的访问权限,也就是说 private 变量即使使用 static 关键字修饰,依然只能由本类及本类的对象访问,而用 static 关键字修饰的 public 变量可以被任何类直接访问,而无需初始化类,直接使用 类名.static变量 这种形式访问即可。在使用多个线程对静态变量进行读写时,需要注意它的线程安全问题,比如对于public static List<String> list = new ArrayList();这样的共享变量在并发环境下进行读写时就会产生线程安全问题,这时我们可以使用线程安全的 CopyOnWriteArrayList 或者在读写时手动进行加锁来保证多线程读写下的线程安全。静态变量在字节码层面是使用访问标志位来进行标识的,如下图所示:也就是说,在类加载时,JVM会通过class文件中的访问标志位来判断某个变量是否为静态变量。2.2 静态方法和静态变量一样,静态方法也可以不依赖于任何对象就直接进行访问,因此对于静态方法来说,是没有 this 的,因为它不依附于任何对象,而 this 本身就是个对象。由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用/访问的,而反过来,非静态成员方法是可以调用/访问静态成员方法/变量的。static 方法内部的变量在执行时是没有线程安全问题的。方法执行时,数据运行在栈里面,栈里面的数据每个线程都是隔离开的,所以不会有线程安全的问题。static 方法在字节码层面是使用一个ACC_STATIC的标志位来进行标识的,如下图所示:2.3 静态内部类普通类是不允许声明为静态的,只有内部类才可以,使用 static 修饰的内部类可以直接由外部类来创建和访问,而没有用 static 修饰的内部类则必须要先实例化一个外部类的对象,再通过外部类的对象来创建内部类的实例。2.4 静态代码块static 关键字还可以修饰代码块用于在类启动之前,初始化一些值。static 块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。在静态代码块中只能调用同样被 static 修饰的变量,并且 static 的变量需要写在静态块的前面,不然编译会报错。和 static 方法一样,静态代码块在字节码层面使用一个ACC_STATIC的标志位来进行标识。3 初始化时机我们通过一个演示程序来测试一下被 static 修饰的类变量、代码块和方法的初始化时机:public class Parent { private static List<String> parentList = new ArrayList(){{ System.out.println("父类静态变量初始化"); }}; static { System.out.println("父类静态代码块初始化"); } public Parent() { System.out.println("父类构造器初始化"); } public static void testStatic() { System.out.println("父类静态方法被调用"); } }public class Child extends Parent { static { System.out.println("子类静态代码块初始化"); } private static List<String> childList = new ArrayList(){{ System.out.println("子类静态变量初始化"); }}; public Child() { System.out.println("子类构造器初始化"); } public static void main(String[] args) { System.out.println(" main 方法执行"); new Child(); } }运行程序,发现打印的结果是:父类静态变量初始化 父类静态代码块初始化 子类静态代码块初始化 子类静态变量初始化 main 方法执行 父类构造器初始化 子类构造器初始化因此,我们可以得出以下结论:父类的静态变量和静态代码块比子类优先初始化;静态变量和静态块比类构造器优先初始化;静态变量和静态代码块按照定义的顺序依次进行初始化;被 static 修饰的方法,在类初始化的时候并不会执行,只有当自己被调用时,才会被执行。4 面试题如何证明 static 静态变量和类无关?不需要初始化类就可直接使用静态变量;在类中写个 main 方法运行,即便不写初始化类的代码,静态变量都会自动初始化;静态变量只会初始化一次,初始化完成之后,不管再 new 多少个类出来,静态变量都不会再初始化了。
1 前言我们都知道,java提供了8种基本数据类型供我们使用,此外,java还提供了这些基本数据类型所对应的包装类,而且通过自动装箱和自动拆箱机制能够轻松地完成基本数据类型和包装类之间的转换。自动装箱:自动将基本数据类型转换为包装类。自动拆箱:自动将包装类转换为基本数据类型。2 Integer2.1 总体结构首先,我们来看一下Integer类的总体结构,如下图所示:Integer类图结构Integer继承了Number类,并重写了Number类intValue()、longValue()、floatValue()等方法来完成对一些基本数据类型的转换Integer类实现了Comparable接口,这使得我们可以重写compareTo方法来自定义Integer对象之间的比较操作2.2 注释从Integer类的注释中我们可以获取到以下信息:Integer类将原始类型int的值包装在一个对象中,Integer类的对象使用一个int类型的字段value来表示对象的值此外,Integer类还提供了一系列的方法来完成int到String,int到其他基本数据类型,String到int的转换和String类一样,Integer类也是不可变类,Integer类使用final进行修饰,而且用于表示Integer对象值的字段value也使用了final进行修饰,Java中的所有包装类都是不可变类。2.3 自动装箱、拆箱原理通过以下一段简单的代码我们就能了解到自动装箱、拆箱的过程:public class IntegerDemo { public static void main(String[] args) { // 自动装箱 Integer numberI = 66; // 自动拆箱 int number = numberI; } }那么包装类的自动装箱、拆箱究竟是如何实现的呢?我们可以通过javap反编译或者通过idea中的插件jclasslib来了解这一过程,如下图所示:可以看到,Integer类是通过调用自身内部的valueOf()方法来实现自动装箱的,而自动拆箱则是通过调用继承自Number类的intValue()方法来实现的。2.4 缓存在《阿里巴巴 Java 开发手册》中有一段关于包装对象之间值的比较问题的规约:就是所有整型包装类对象之间值的比较,全部使用 equals 方法比较。而手册中对这条规约的说明就是:对于 Integer var = ? 在 - 128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache() 方法中产生的,会复用已有对象,这个区间内的 Integer 值可以直接使用 == 进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,因此推荐使用 equals 方法进行值的判断。让我们来看一下这样一段代码:Integer a = 100, b = 100, c = 150, d = 150; System.out.println(a == b); System.out.println(c == d);有了上文的分析,我们很轻松地就能知道答案:true,false。这是因为对于 - 128 至 127 范围内的值,Integer对象会使用缓存,因此b会复用a对象,而超出了缓存区间的 150 则不会使用缓存,因此会创建新的对象。从上文对自动装箱的分析我们可以得知,每次对Integer对象进行赋值都会调用其valueOf()方法,接下来我们从源码来分析Integer类的缓存机制:/** * Returns an {@code Integer} instance representing the specified * {@code int} value. If a new {@code Integer} instance is not * required, this method should generally be used in preference to * the constructor {@link #Integer(int)}, as this method is likely * to yield significantly better space and time performance by * caching frequently requested values. * * This method will always cache values in the range -128 to 127, * inclusive, and may cache other values outside of this range. * * @param i an {@code int} value. * @return an {@code Integer} instance representing {@code i}. * @since 1.5 */ public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }可以得知,Integer的缓存机制是通过它内部的一个静态类IntegerCache来实现的,在IntegerCache.low和IntegerCache.high之间的值将直接使用IntegerCache的cache数组中缓存的值,否则会创建一个新的对象。同时,从源码的注释中我们还可以得知:如果不要求必须新建一个整型对象,缓存最常用的值(提前构造缓存范围内的整型对象),会更省空间,速度也更快。因此Integer的缓存是为了减少内存占用,提高程序运行的效率而设计的。进一步了解IntegerCache的源码,我们还可以知道缓存的区间其实是可以设置的:/** * Cache to support the object identity semantics of autoboxing for values between * -128 and 127 (inclusive) as required by JLS. * * The cache is initialized on first usage. The size of the cache * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option. * During VM initialization, java.lang.Integer.IntegerCache.high property * may be set and saved in the private system properties in the * sun.misc.VM class. */ private static class IntegerCache { // 最小值固定为-128 static final int low = -128; static final int high; static final Integer cache[]; // 初始化缓存数组 static { // 最大值可以通过属性来配置 int h = 127; // 获取系统配置里设置的值 String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // 最大的缓存数组大小为Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // 如果属性值不能转换为int,就忽略它. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; // 为缓存数组赋值 for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} }从注释中我们可以得知:缓存是为了支持自动装箱对象的标识语义,在自动装箱时可以复用这些缓存的对象,而 -128 到 127 的范围是基于JLS的要求而设定的首次使用时会初始化缓存。缓存的大小可以通过虚拟机参数 -XX:AutoBoxCacheMax=<size>} 来控制,在VM初始化期间,java.lang.Integer.IntegerCache.high可以设置并保存在sun.misc.VM类中的私有系统属性中(-Djava.lang.Integer.IntegerCache.high=<value>),未指定则为 127更多细节可以参考 JLS 的Boxing Conversion 部分的相关描述。3 Long3.1 源码分析有了上文对于Integer类的分析,Long内部的自动装箱、拆箱以及缓存机制也就大同小异了:/** * Returns a {@code Long} instance representing the specified * {@code long} value. * If a new {@code Long} instance is not required, this method * should generally be used in preference to the constructor * {@link #Long(long)}, as this method is likely to yield * significantly better space and time performance by caching * frequently requested values. * * Note that unlike the {@linkplain Integer#valueOf(int) * corresponding method} in the {@code Integer} class, this method * is <em>not</em> required to cache values within a particular * range. * * @param l a long value. * @return a {@code Long} instance representing {@code l}. * @since 1.5 */ public static Long valueOf(long l) { final int offset = 128; if (l >= -128 && l <= 127) { // will cache return LongCache.cache[(int)l + offset]; } return new Long(l); }与Integer不同的是,Long的valueOf()方法并没有被要求缓存特定范围的值,而且通过LongCache我们可以得知Long的缓存范围被固定在了 -128 到 127 ,并不能通过参数来修改缓存的范围:private static class LongCache { private LongCache(){} // 缓存,范围从 -128 到 127,+1 是因为有个 0 static final Long cache[] = new Long[-(-128) + 127 + 1]; static { // 缓存 Long 值,注意这里是 i - 128 ,所以在拿的时候就需要 + 128 for(int i = 0; i < cache.length; i++) cache[i] = new Long(i - 128); } }3.2 面试题为什么使用 Long 时,大家推荐多使用 valueOf 方法,少使用 parseLong 方法?因为 Long 本身有缓存机制,缓存了 -128 到 127 范围内的 Long,valueOf 方法会从缓存中去拿值,如果命中缓存,会减少资源的开销,parseLong 方法就没有这个机制。4 总结通过本文,我们学习了Integer、Long的自动装箱、拆箱原理,还学习了它们内部的缓存机制,缓存是一种重要且常见的性能提升手段,在很多应用场景诸如Spring、数据库、Redis中都有广泛运用,因此了解它的机制对于我们的学习工作都有很大的帮助。本文分析了Integer、Long内部的一些巧妙设计,对于其他包装类来说也是互通的,本文就不再赘述了,有兴趣的读者可以自行去研究。注:以上分析皆基于JDK 1.8版本来进行。
前言字符串在我们的工作场景中应用广泛,不同于基本数据类型,Java中的字符串属于对象,Java中提供了 String 类来创建字符串,并提供了一系列的方法来对字符串进行替换、拆分和合并等操作。总体结构首先,我们来看一下String类的总体结构,如下图所示:String类实现了Serializable接口,这说明String的对象能够被序列化String类实现了Comparable接口,这使得我们可以重写compareTo方法来自定义String对象之间的比较操作String类实现了CharSequence接口,并重写了接口中的length()、charAt()等方法注释我们首先通过注释来了解一下String,在源码学习中,注释往往能给我们提供很多有用的信息,因此在阅读源码之前,从注释中获取一些信息是非常重要的。从String类的注释中我们可以获取以下信息:String类表示字符串。 所有Java程序中的字符串文字例如“ abc”都是作为此类的实例实现。String类是不可变类,它们的值在创建之后就不能改变了。而StringBuffer、StringBuilder提供了字符串的可变类实现。因为String是不可变类,因此它们的值是可以被共享的,比如:String str = "abc";等价于char data[] = {'a', 'b', 'c'};String str = new String(data);Java语言为String的连接操作提供了特殊的支持(通过“+”号来进行字符串的连接),而StringBuilder、StringBuffer内部通过append方法实现了字符串的连接。不变性上文提到了不可变类,而HashMap 的 key 通常建议使用不可变类,比如说 String 这种不可变类。这里说的不可变指的是一旦一个类的对象被创建出来,在其整个生命周期中,它的成员变量就不能被修改,如果被修改,将会生成新的对象。从源码中我们可以得知String是如何实现它的不可变性的:public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; }String 被 final 修饰,说明 String 类绝不可能被继承了,也就是说任何对 String 的操作方法,都不会被继承覆写;String 中保存数据的是一个 char 类型的数组 value。 value 也是被 final 修饰的,也就是说 value 一旦被赋值,内存地址是绝对无法修改的,而且 value 的访问权限是 private 的,外部绝对访问不到,String 也没有开放出可以对 value 进行赋值的方法,所以说 value 一旦产生,内存地址就根本无法被修改。以上两点就是 String 不变性的原因,充分利用了 final 关键字的特性,所以在自定义类时,如果你也希望类是不可变的,可以模仿 String 的这两点操作。字符串乱码在二进制转化操作时,并没有强制规定文件编码,而不同的环境默认的文件编码不一致就会导致字符串乱码。比如如下代码中创建的字符串s2会产生乱码:String str ="nihao 你好"; byte[] bytes = str.getBytes("ISO-8859-1"); String s2 = new String(bytes); System.out.println(s2);在打印的结果中可以看到字符串产生了乱码:nihao ??这时,即使使用String s2 = new String(bytes,"ISO-8859-1");来统一字符集也还是会产生乱码,这是由于 ISO-8859-1 这种编码本身就对中文的支持有限,从而导致中文会产生乱码。而当我们统一使用 UTF-8 这种编码的时候就不会有乱码产生了。首字母大小写需要首字母小写的场景:通过 applicationContext.getBean(className); 这种方式得到 SpringBean,这时 className 必须是要满足首字母小写的在反射场景下面,我们也经常要使类属性的首字母小写,这时候我们一般都会这么做:name.substring(0, 1).toLowerCase() + name.substring(1);上面的 substring 方法主要用于截取字符串连续的一部分,substring 有两个方法:public String substring(int beginIndex, int endIndex) beginIndex:开始位置,endIndex:结束位置;public String substring(int beginIndex)beginIndex:开始位置,结束位置为文本末尾。substring 方法的底层使用的是字符数组范围截取的方法 :Arrays.copyOfRange(字符数组, 开始位置, 结束位置); 从字符数组中进行一段范围的拷贝。相反的,如果要修改成首字母大写,只需要修改成 name.substring(0, 1).toUpperCase() + name.substring(1) 即可。相等判断判断相等有两种办法,equals 和 equalsIgnoreCase。后者判断相等时,会忽略大小写。String是通过其底层的结构来判断是否相等的,即挨个比较char数组value的每一个字符是否相等。public boolean equals(Object anObject) { // 判断内存地址是否相同 if (this == anObject) { return true; } // 待比较的对象是否是 String,如果不是 String,直接返回不相等 if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; // 两个字符串的长度是否相等,不等则直接返回不相等 if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; // 依次比较每个字符是否相等,若有一个不等,直接返回不相等 while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }替换、删除替换在很多场景中会使用到,有 replace 替换所有字符、replaceAll 批量替换字符串、replaceFirst 替换遇到的第一个字符串三种场景。其中在使用 replace 时需要注意,replace 有两个方法,一个入参是 char,一个入参是 String,前者表示替换所有字符,如:name.replace('a','b'),后者表示替换所有字符串,如:name.replace("a","b"),两者就是单引号和多引号的区别。需要注意的是, replace 并不只是替换一个,是替换所有匹配到的字符或字符串。以replace(char oldChar, char newChar)方法为例,从源码中可以看出来是通过逐个查找到需要替换的字符进行替换来实现的:public String replace(char oldChar, char newChar) { if (oldChar != newChar) { int len = value.length; int i = -1; /** * 在一个方法中需要大量引用实例域变量的时候, * 使用方法中的局部变量代替引用可以减少getfield操作的次数,提高性能 */ char[] val = value; /* avoid getfield opcode */ // 查找到需要替换的字符oldChar在value数组中第一次出现的位置 while (++i < len) { if (val[i] == oldChar) { break; } } if (i < len) { char buf[] = new char[len]; for (int j = 0; j < i; j++) { buf[j] = val[j]; } // 找到所有需要替换的字符,逐一进行替换 while (i < len) { char c = val[i]; buf[i] = (c == oldChar) ? newChar : c; i++; } // 返回一个新字符串 return new String(buf, true); } } // 替换前后字符相等,直接返回 return this; }当然我们想要删除某些字符,也可以使用 replace 方法,把想删除的字符替换成 "" 即可。拆分和合并拆分我们使用 split 方法,该方法有两个入参数。第一个参数是我们拆分的标准字符,第二个参数是一个 int 值,叫 limit,来限制我们需要拆分成几个元素。如果 limit 大于0并且比实际能拆分的个数小,按照 limit 的个数进行拆分。String s ="boo:and:foo"; s.split(":"); // 结果:["boo","and","foo"] s.split(":",2); // 结果:["boo","and:foo"] s.split(":",5); // 结果:["boo","and","foo"] s.split(":",-2); // 结果:["boo","and","foo"] s.split("o"); // 结果:["b","",":and:f"] s.split("o",2); // 结果:["b","o:and:foo"]limit 对拆分的结果,是具有限制作用的,还有就是拆分结果里面不会出现被拆分的字段。以源码中可以看出来,public String[] split(String regex, int limit)方法针对不同的分隔符提供两种路径来进行字符串的分割:char ch = 0; /** 当regex是以下两种情况时的快速分割路径: * (1)一个字符的字符串并且这个字符不是以下正则表达式的元字符".$|()[{^?*+\\"之一 * (2)两个字符的字符串并且第一个字符是反斜杠,第二个字符不是ASCII数字或ASCII字母。 */ if (((regex.value.length == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) || (regex.length() == 2 && regex.charAt(0) == '\\' && (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && ((ch-'a')|('z'-ch)) < 0 && ((ch-'A')|('Z'-ch)) < 0)) && (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE)) { // 用于记录每个拆分的子串起始位置的索引 int off = 0; // 用于记录每个分隔符在字符串中的索引值 int next = 0; boolean limited = limit > 0; ArrayList<String> list = new ArrayList<>(); // 从字符串开头开始将每个分隔符之前的子串保存在一个list中 while ((next = indexOf(ch, off)) != -1) { if (!limited || list.size() < limit - 1) { list.add(substring(off, next)); off = next + 1; } else { // 最后一个子串 // 当list的大小等于limit - 1,直接将剩下的字符串加入到list中 list.add(substring(off, value.length)); off = value.length; break; } } // 如果字符串中没有匹配的分隔符,直接返回原字符串 if (off == 0) return new String[]{this}; // 将剩下的子串加入到list中 if (!limited || list.size() < limit) list.add(substring(off, value.length)); // 构建结果 int resultSize = list.size(); if (limit == 0) { // 忽略掉最后的空串 while (resultSize > 0 && list.get(resultSize - 1).length() == 0) { resultSize--; } } // 将list中保存的字符串转换成字符串数组返回 String[] result = new String[resultSize]; return list.subList(0, resultSize).toArray(result); } // 第二条路径,以正则匹配的方式分割字符串 return Pattern.compile(regex).split(this, limit);如果字符串里面有一些空值,空值是拆分不掉的,仍然成为结果数组的一员,如果我们想删除空值,只能自己拿到结果后再做操作,但 Guava(Google 开源的技术工具) 提供了一些可靠的工具类(如Splitter),可以帮助我们快速去掉空值。String b =",a, , b c ,"; List<String> list = Splitter.on(',') .trimResults()// 去掉空格 .omitEmptyStrings()// 去掉空值 .splitToList(b); list.stream().forEach(System.out::println);合并我们使用 join 方法,此方法是静态的,我们可以直接使用。方法有两个入参,参数一是合并的分隔符,参数二是合并的数据源,数据源支持数组和 List。从源码中可以看出,字符串的合并操作实际上是借助StringBuilder的append方法来完成的。StringJoiner joiner = new StringJoiner(delimiter); for (CharSequence cs: elements) { // 通过add方法来合并字符串 joiner.add(cs); } return joiner.toString();public StringJoiner add(CharSequence newElement) { // 通过StringBuilder的append方法来合并字符串 prepareBuilder().append(newElement); return this; }// 初始化StringJoiner中的StringBuilder对象 private StringBuilder prepareBuilder() { if (value != null) { value.append(delimiter); } else { value = new StringBuilder().append(prefix); } return value; }join方法在使用的时候,有两个不太方便的地方:不支持依次 join 多个字符串,比如我们想依次 join 字符串 s 和 s1,如果你这么写的话 String.join(",",s).join(",",s1) 最后得到的是 s1 的值,第一次 join 的值被第二次 join 覆盖了;如果 join 的是一个 List,无法自动过滤掉 null 值。而 Guava 正好提供了 Joiner API,用于解决上述问题:// 依次 join 多个字符串 Joiner joiner = Joiner.on(",").skipNulls(); String result = joiner.join("hello",null,"china"); log.info("依次 join 多个字符串: " + result); List<String> list = Lists.newArrayList(new String[]{"hello","china",null}); log.info("自动删除 list 中空值: " + joiner.join(list));注:以上分析皆基于JDK 1.8版本来进行。
消息队列消息队列是利用了数据结构中先进先出的一种数据结构——队列来实现的,在当前大部分企业的系统架构中,作为中间件提供服务。消息中间件功能应用解耦AB应用不再互相依赖。流量削峰流量达到高峰的时候,通常使用限流算法来控制流量涌入系统,避免系统被击瘫,但是这种方式损失了一部分请求。此时可以使用消息中间件来缓冲大量的请求,匀速消费,当消息队列中堆积消息过多时,我们可以动态上线增加消费端,来保证不丢失重要请求。大数据处理消息中间件可以把各个模块中产生的管理员操作日志、用户行为、系统状态等数据文件作为消息收集到主题中。数据使用方可以订阅自己感兴趣的、互不影响的数据内容,进行消费。异构系统跨语言。基本概念1 RocketMQ 角色消息模型(Message Model)RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器,每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。BrokerBroker在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。Broker也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。Broker面向Producer和Consumer接收和发送消息向nameserver提交自己的信息是消息中间件的消息存储、转发服务器。每个Broker节点,在启动时,都会遍历NameServer列表,与每个NameServer建立长连接,注册自己的信息,之后定时上报。broker集群Broker高可用,可以配成Master/Slave结构,Master可写可读,Slave只可以读,Master将写入的数据同步给Slave。 一个Master可以对应多个Slave,但是一个Slave只能对应一个MasterMaster与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示SlaveMaster多机负载,可以部署多个broker 每个Broker与nameserver集群中的所有节点建立长连接,定时注册Topic信息到所有nameserver。Producer一个Producer会把业务应用系统里产生的消息发送到Broker服务器。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。消息的生产者与集群中的其中一个节点(随机选择)建立长连接,获得Topic的路由信息,包括Topic下面有哪些Queue,这些Queue分布在哪些Broker上等接下来向提供Topic服务的Master建立长连接,且定时向Master发送心跳生产者组(Producer Group)同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。注意:考虑到提供的生产者在发送消息方面足够强大,因此每个生产者组仅允许拥有一个实例,以避免不必要的生产者实例初始化。Consumer消息的消费者,通过NameServer集群获得Topic的路由信息,连接到对应的Broker上消费消息。注意,由于Master和Slave都可以读取消息,因此Consumer会与Master和Slave都建立连接。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。拉取式消费(Pull Consumer):应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。推动式消费(Push Consumer):封装消息拉取,消费进度并在内部维护其他工作,并将回调接口留给最终用户来实现,该接口将在消息到达时执行。该模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。消费者组(Consumer Group)同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。集群消费(Clustering):集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。广播消费(Broadcasting):广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。NameServerNameServer充当路由消息的提供者。生产者或消费者能够通过NameServer查找各主题相应的Broker IP列表。多个NameServer实例组成集群,但相互独立,没有信息交换,彼此是无状态的节点。底层由netty实现,提供了路由管理、服务注册、服务发现的功能。nameserver是服务发现者,集群中各个角色(producer、broker、consumer等)都需要定时向nameserver上报自己的状态,以便互相发现彼此,超时不上报的话,nameserver会把它从列表中剔除nameserver可以部署多个,当多个nameserver存在的时候,其他角色同时向他们上报信息,以保证高可用NameServer集群间互不通信,没有主备的概念nameserver内存式存储,nameserver中的broker、topic等信息默认不会持久化为什么不用zookeeper?rocketmq为了提高性能,CAP定理,客户端负载均衡2 RocketMQ消息模型根据上述模型,我们可以更深入地探讨有关消息系统设计的一些主题:消费者并发消费者热点消费者负载平衡消息路由连接复用金丝雀部署主题(Topic)表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。主题是生产者传递消息和消费者拉取消息的类别。主题与生产者和消费者之间的关系非常松散。具体来说,一个主题可能有零个,一个或多个向其发送消息的生产者。相反,生产者可以发送不同主题的消息。从消费者的角度来看,一个主题可以由零个,一个或多个消费者组订阅。 与此类似,消费者组可以订阅一个或多个主题,只要该组的实例保持其订阅一致即可。消息(Message)消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题,该主题可以理解为要发送给你的信件的地址。消息还可以具备可选的标签和额外的键值对。 例如,你可以为消息设置业务key,然后在broker上查找消息以在开发过程中诊断问题。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。消息队列(Message Queue)主题可以分为一个或多个子主题,即“message queue”。标签(Tag)为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。对比JSM中的Topic和Queue在RocketMQ中Topic是一个逻辑上的概念,实际上Message是在每个Broker上以Queue的形式记录。对应到JMS中的topic实现是由客户端来完成的。3 消息顺序使用DefaultMQPushConsumer时,你可以决定按顺序或并发地消费消息。顺序消息有序地消费消息意味着消息的消息顺序与生产者为每个消息队列发送消息的顺序相同。如果要处理必须强制执行全局顺序的情况,请确保你使用的主题只有一个消息队列。注意:如果指定了顺序消费,则消息消费的最大并发度是消费者组订阅的消息队列数。普通顺序消息(Normal Ordered Message)普通顺序消费模式下,消费者通过同一个消费队列收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。严格顺序消息(Strictly Ordered Message)严格顺序消息模式下,消费者收到的所有消息均是有顺序的。并发消息当并发的消费消息时,消息消费的最大并发度仅受为每个消费者客户端指定的线程池限制。注意:在此模式下不再保证消息顺序。参考资料RocketMQ 官网RocketMQ 官方文档
简介Apache RocketMQ 是阿里开源的一款高性能、高吞吐量的分布式消息中间件,在由阿里捐赠给Apache软件基金会之后孵化成了Apache的一个顶级项目(Top-Level Project,TLP)。RocketMQ 是基于阿里闭源的 MetaQ 内核实现的,它服务于阿里的各个体系,尤其是在电商领域提供了极高的并发支撑。RocketMQ 是使用 Java 语言开发的,因此相较于业界流行的其它消息中间件比如 RabbitMQ(使用 Erlang 开发)、Kafka(使用 Scala 开发),它对于 Java 程序员来说更加友好,更加利于开发者在其底层的基础上进行封装和扩展。RocketMQ 的官网是http://rocketmq.apache.org/,根据官网的介绍,Apache RocketMQ™ 是一个标准化的消息引擎,轻量级的数据处理平台。它具备以下优势:低延迟:在高并发压力下,超过99.6%的响应延迟在1毫秒之内面向金融的:系统支撑的追踪和审计功能具备高可用性行业可发展性:保证了万亿级别的消息容量厂商中立的:在最新的4.1版本开放了一个新的分布式消息和流标准对大数据友好的:具备通用集成功能的批量传输支撑了海量吞吐大量的积累:只要给与足够的磁盘空间,就可以累积消息而不会造成性能损失RocketMQ 诞生背景在早期,阿里曾基于 ActiveMQ 5.x(低于5.3)构建了分布式消息中间件。 并将其用于他们的跨国公司的异步通信,搜索,社交网络活动流,数据管道,甚至交易过程中。 随着贸易业务吞吐量的增长,来自消息集群的压力也就变成了亟待解决的问题。为什么设计 RocketMQ?随着队列的增长和虚拟主题的使用,ActiveMQ IO 模块遇到了瓶颈。 尽管阿里尽力通过限流,熔断或降级来解决此问题,但效果不佳。 因此,那时他们开始关注流行的消息传递解决方案Kafka。 不幸的是,Kafka不能满足阿里的要求,特别是在低延迟和高可靠性方面,请参阅此处以了解详细信息。在这种情况下,他们决定发明一个新的消息传递引擎来处理更广泛的用例集——从传统的发布/订阅方案到大批量实时零损失容忍的交易系统。RocketMQ vs. ActiveMQ vs. Kafka下表展示了RocketMQ,ActiveMQ和Kafka(依照awesome-java 来说,Apache RocketMQ是最流行的消息传递解决方案)之间的比较:消息中间件ActiveMQKafkaRocketMQ客户端SDKJava,.NET,C++等Java, Scala等Java, C++, Go协议和规范推模型,支持OpenWire, STOMP, AMQP, MQTT, JMS拉模型,支持TCP拉模型,支持TCP,JMS,OpenMessaging顺序消息独立的消费者或者队列可以确保顺序确保分区内消息的顺序能确保对消息进行严格排序,并可以很好地扩展定时消息支持不支持支持批量消息不支持支持,带有异步生产者支持,使用异步模式来避免消息丢失广播消息支持不支持支持消息过滤支持支持,可以使用Kafka流来过滤消息支持,基于SQL92的属性过滤表达式服务器触发的消息重新投递不支持不支持支持消息存储使用JDBC以及高性能日志(例如levelDB,kahaDB)来支持非常快速的持久化操作高性能文件存储高性能且低延迟的文件存储消息追溯性支持通过偏移量指示器来支持这一特性支持时间戳和偏移量两种指示器消息优先级支持不支持不支持高可用和故障转移支持,取决于存储,如果使用kahadb,则需要一个ZooKeeper服务器支持,需要一个ZooKeeper服务器支持,主从模式,不需要其他工具消息追踪不支持不支持支持配置默认配置为低级别,用户需要优化配置参数Kafka使用键值对格式进行配置。 这些值可以从文件或以编程方式提供开箱即用,用户只需要关注少量的配置管理和操作工具支持支持,使用终端命令展示核心指标支持,丰富的Web和终端命令可显示核心指标Quick Start本部分内容将会介绍如何在本地的机器上快速安装一个 RocketMQ 用于收发消息,更多的细节可以访问 https://github.com/apache/rocketmq/tree/master/docs/cn进行查看。前置要求在安装 RokcetMQ 之前需要先安装如下软件:64位操作系统,推荐使用 Linux/Unix/Mac 操作系统64位 JDK 1.8 及以上的版本Maven 3.2.xGit为 Broker server 至少预留 4G 的磁盘空间下载、安装及使用Linux1. 安装jdk首先在 oracle 官网下载 Linux jdk 8 的压缩包然后使用 ftp 工具将下载好的压缩包上传到 Linux 服务器上使用 tar -zxvf jdk-8u261-linux-x64.tar.gz 命令解压文件配置系统环境变量[root@node01 opt]# vi /etc/profile在文件末尾添加以下内容:# jdk放置目录 JAVA_HOME=/opt/jdk1.8.0_226 # jre放置目录 JRE_HOME=/opt/jdk1.8.0_226/jre # 配置 path 环境变量,以 : 分隔 PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin # 配置 classpath 环境变量,以 : 分隔 CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib # 设置环境变量 export JAVA_HOME JRE_HOME PATH CLASSPATH使配置文件的变更生效[root@node01 opt]# source /etc/profile查看 jdk 版本,至此,jdk 就已经安装完毕[root@node01 opt]# java -version java version "1.8.0_226" Java(TM) SE Runtime Environment (build 1.8.0_226-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.226-b11, mixed mode)2. 安装maven使用 wget 下载 maven[root@node01 opt]# wget https://mirrors.bfsu.edu.cn/apache/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz解压[root@node01 opt]# tar -zxvf apache-maven-3.6.3-bin.tar.gz添加阿里云镜像[root@node01 opt]# cd apache-maven-3.6.3/conf/ [root@node01 conf]# ll 总用量 20 drwxr-xr-x. 2 root root 4096 11月 7 2019 logging -rw-r--r--. 1 root root 10468 11月 7 2019 settings.xml -rw-r--r--. 1 root root 3747 11月 7 2019 toolchains.xml [root@node01 conf]# vi settings.xml在配置文件 settings.xml 中加入如下代码: <mirror> <id>aliyun-maven</id> <mirrorOf>*</mirrorOf> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/groups/public</url> </mirror>配置环境变量[root@node01 conf]# vi /etc/profile在文件中添加如下配置:# maven 安装目录 M2_HOME=/opt/apache-maven-3.6.3 PATH=$PATH:$M2_HOME/bin export M2_HOME[root@node01 opt]# source /etc/profile查看 maven 版本,至此,maven 就安装完毕了[root@node01 conf]# mvn -v Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f) Maven home: /opt/apache-maven-3.6.3 Java version: 1.8.0_226, vendor: Oracle Corporation, runtime: /opt/jdk1.8.0_226/jre Default locale: zh_CN, platform encoding: UTF-8 OS name: "linux", version: "3.10.0-1127.el7.x86_64", arch: "amd64", family: "unix"3. 安装rocketmq下载 rocketmq 源码包[root@node01 opt]# wget https://mirror.bit.edu.cn/apache/rocketmq/4.7.1/rocketmq-all-4.7.1-source-release.zip解压[root@node01 opt]# yum install -y unzip [root@node01 opt]# unzip rocketmq-all-4.7.1-source-release.zip去到解压后的文件目录中进行编译[root@node01 rocketmq-all-4.7.1-source-release]# mvn -Prelease-all -DskipTests clean install -U将编译好的文件移个位置[root@node01 rocketmq-all-4.7.1-source-release]# cd distribution/target/rocketmq-4.7.1 [root@node01 rocketmq-4.7.1]# ll 总用量 4 drwxr-xr-x. 6 root root 4096 8月 29 15:42 rocketmq-4.7.1 [root@node01 rocketmq-4.7.1]# mv rocketmq-4.7.1/ /opt启动 Name Server,如果在终端显示如下信息,则说明启动成功了:[root@node01 opt]# cd rocketmq-4.7.1/bin/ [root@node01 bin]# ls cachedog.sh dledger mqbroker mqbroker.numanode1 mqnamesrv mqshutdown.cmd play.sh runbroker.sh setcache.sh tools.sh cleancache.sh mqadmin mqbroker.cmd mqbroker.numanode2 mqnamesrv.cmd os.sh README.md runserver.cmd startfsrv.sh cleancache.v1.sh mqadmin.cmd mqbroker.numanode0 mqbroker.numanode3 mqshutdown play.cmd runbroker.cmd runserver.sh tools.cmd [root@node01 bin]# ./mqnamesrv Java HotSpot(TM) 64-Bit Server VM warning: Using the DefNew young collector with the CMS collector is deprecated and will likely be removed in a future release Java HotSpot(TM) 64-Bit Server VM warning: UseCMSCompactAtFullCollection is deprecated and will likely be removed in a future release. The Name Server boot success. serializeType=JSON启动 broker使用命令启动 broker,会发现报如下的错误:[root@node01 bin]# ./mqbroker -n localhost:9876 Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory(0x00000005c0000000, 8589934592, 0) failed; error='Cannot allocate memory' (errno=12) # # There is insufficient memory for the Java Runtime Environment to continue. # Native memory allocation (mmap) failed to map 8589934592 bytes for committing reserved memory. # An error report file with more information is saved as: # /opt/rocketmq-4.7.1/bin/hs_err_pid9514.log从错误信息我们可以得知原因是 jvm 启动初始化内存分配大于物理内存。因此,我们可以修改启动脚本中的 jvm 参数来解决这个问题,首先,让我们来修改nameserver的启动脚本:[root@node01 bin]# vi runserver.sh找到分配jvm内存的配置行:JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"将它修改为如下配置(具体配置数值根据实际生产场景进行调整):JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"然后修改broker的启动脚本:[root@node01 bin]# vi runbroker.sh同样是找到分配jvm内存的配置行:JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g"将它修改为如下配置(具体配置数值根据实际生产场景进行调整):JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn512m"修改完之后重启 nameserver 和 broker,当出现如下信息,说明 broker 已经启动成功:[root@node01 bin]# ./mqbroker -n localhost:9876 The broker[node01, 192.168.114.60:10911] boot success. serializeType=JSON and name server is localhost:9876执行测试程序测试消息发送和接收在发送/接收消息之前,我们需要告诉客户端 Name Server 的位置。RocketMQ提供了多种方法来实现这一目标。为了简单起见,我们通过设置环境变量 NAMESRV_ADDR 来实现,在 /etc/profile 中加入如下配置:export NAMESRV_ADDR=localhost:9876使用测试程序发送消息:./tools.sh org.apache.rocketmq.example.quickstart.Producer显示如下信息则表示消息发送成功:另起一个会话,然后用测试程序来接收消息:./tools.sh org.apache.rocketmq.example.quickstart.Consumer显示如下信息则表示消息接收成功:8. 关闭关闭 broker:[root@node01 bin]# ./mqshutdown broker The mqbroker(1502) is running... Send shutdown request to mqbroker(1502) OK关闭 name server:[root@node01 bin]# ./mqshutdown namesrv The mqnamesrv(1444) is running... Send shutdown request to mqnamesrv(1444) OKWindows本文主要讲述的是 windows 10 操作系统下 RocketMQ 的安装,请确保你的操作系统中已经安装了 PowerShell。和 Linux 一样,在 Windows 安装 RocketMQ 需要先安装 jdk 和 maven ,本文就不再细述如何安装 jdk 和 maven 了,请自行查阅资料安装。1. 安装 RocketMQ首先,在官网下载 RocketMQ 的二进制压缩包,然后选择一个本地目录进行解压缩配置环境变量,添加如下两个环境变量:打开 runbroker.cmd,修改如下配置行:set "JAVA_OPT=%JAVA_OPT% -cp %CLASSPATH%"修改后的配置行如下:rem set "JAVA_OPT=%JAVA_OPT% -cp "%CLASSPATH%"启动 name server:启动 broker:当控制台显示如下信息时,则说明启动成功了:使用测试程序发送/接收消息发送消息:接收消息:关闭直接关闭 cmd/powershell 即可(不要在生产环境这样做)安装启动时可能会遇到的问题1. 编译时包无法在mirror上找到 提示502错误原因:网络不好或maven仓库服务器出错重试即可,或者换一个镜像仓库2. 启动broker失败,报 Cannot allocate memory 错误此问题在上文中已经叙述过了,在此就不再赘述了3. 启动broker成功但提示:Failed to obtain the host name原因:无法解析当前的主机名在/etc/hosts里添加映射即可# 配置 ip 地址到主机名的映射 192.168.114.60 node-014. linux日期校准安装ntpdateyum install ntpdate ntpdate ntp1.aliyun.com控制台rocketmq-console编译安装1. 下载编译源码包下载地址:https://github.com/apache/rocketmq-externals中文指南https://github.com/apache/rocketmq-externals/blob/master/rocketmq-console/doc/1_0_0/UserGuide_CN.md2. 将源码包上传到服务器并解压缩[root@node01 opt]# unzip rocketmq-externals.zip3. 编译[root@node01 opt]# cd rocketmq-externals/rocketmq-console/ [root@node01 rocketmq-console]# mvn clean package -Dmaven.test.skip=true4. 启动编译成功后在rocketmq-console/target目录下执行rocketmq-console-ng-2.0.0.jar启动(需要确保你的 name server 已经启动了),直接动态添加nameserver地址即可,或者你也可以通过编辑rocketmq-console/src/main/resources目录下的application.properties配置文件添加rocketmq.config.namesrvAddr属性来配置nameserver地址。[root@node01 rocketmq-console]# ll 总用量 68 drwxr-xr-x. 3 root root 4096 8月 30 10:50 doc -rw-r--r--. 1 root root 30422 8月 30 10:50 LICENSE -rw-r--r--. 1 root root 180 8月 30 10:50 NOTICE -rw-r--r--. 1 root root 10593 8月 30 10:50 pom.xml -rw-r--r--. 1 root root 2390 8月 30 10:50 README.md drwxr-xr-x. 4 root root 4096 8月 30 10:50 src drwxr-xr-x. 3 root root 4096 8月 30 10:50 style drwxr-xr-x. 7 root root 4096 8月 30 10:59 target [root@node01 rocketmq-console]# cd target/ [root@node01 target]# java -jar rocketmq-console-ng-2.0.0.jar --rocketmq.config.namesrvAddr=localhost:9876当出现如下界面时就说明启动成功了:启动成功后访问服务器 8080 端口即可。界面组成RocketMQ 是面向集群而生的,这一点从 rocketmq-console 的界面上就能体现出来。运维NameSvrAddrList:配置 name server 的地址,它是个列表,因此可以配置多个 name server 的地址,这说明 name server 是可以集群部署的IsUseVIPChannel:是否使用VIPChannel驾驶舱Broker TOP 10:查看消息量最多的10个broker的消息量(总量)Broker 5min trend:查看broker消息量5分钟的趋势主题 TOP 10:查看消息量最多的10个单一主题的消息量(总量)主题 5min trend:查看主题消息量5分钟的趋势集群查看集群的分布情况 cluster与broker关系cluster中包含的broker查看broker具体信息/运行信息/状态信息查看broker配置信息主题展示所有的主题,可以通过主题名称进行过滤筛选 普通/重试/死信/系统 主题新增/更新主题 clusterName:集群名brokerName:主机名topicName:主题名writeQueueNums:写队列数量readQueueNums:读队列数量perm:2是写 4是读 6是读写状态 查询消息投递状态(投递到哪些broker/哪些queue/多少量等)路由 查看消息的路由(现在你发这个主题的消息会发往哪些broker,对应broker的queue信息)CONSUMER管理(这个topic都被哪些group订阅了、消费了,消费情况何如)topic配置(查看/变更当前的topic的配置)发送消息(向这个主题发送一个测试消息)重置消费位点(分为在线和离线两种情况,不过都需要检查重置是否成功)删除主题 (会删除掉所有broker以及namesrv上的主题配置和路由信息,在生产环境上请慎重进行这项操作)消费者展示所有的消费组,可以通过组名进行过滤刷新页面/每隔五秒定时刷新页面按照订阅组/数量/TPS/延迟 进行排序新增/更新消费组 clusterName :集群名brokerName:主机名groupName:消费组名字consumeEnable:是否可以消费,设置为FALSE的话将无法进行消费consumeBroadcastEnable:是否可以广播消费retryQueueNums:重试队列的大小brokerId:正常情况从哪消费whichBrokerWhenConsumeSlowly:出问题了从哪消费终端:在线的消费客户端查看,包括版本订阅信息和消费模式消费详情:对应消费组的消费明细查看,这个消费组订阅的所有Topic的消费情况,每个queue对应的消费client查看(包括Retry消息)配置:查看/变更消费组的配置删除:在指定的broker上删除消费组(谨慎操作)生产者展示在线的消息生产者客户端(主机、版本、地址等信息),可以通过组名进行过滤通过主题进行筛选消息根据Topic和时间区间查询(由于数据量大 最多只会展示2000条,多的会被忽略)根据Message Key和Topic进行查询 最多只会展示64条根据Message Id和Topic进行消息的查询消息详情:展示这条消息的详细信息,查看消息对应到具体消费组的消费情况(如果异常,可以查看具体的异常信息)。可以向指定的消费组重发消息。消息轨迹根据Message Key和Topic进行查询 最多只会展示64条根据Message Id和Topic进行消息的查询消息轨迹详情:展示展示这条消息的消息轨迹的详细信息参考资料RocketMQ 官网RocketMQ 官方文档
Docker是一个用于开发,交付和运行应用程序的开放平台。它让你能够将应用程序与基础架构分离,从而可以快速交付软件。借助Docker,你可以以与管理应用程序相同的方式来管理基础架构。正如它的logo所展示的,鲸鱼/货轮代表了操作系统,而集装箱就是那些需要一个个部署的应用。在一艘货轮上,集装箱的作用就是将各种货物进行标准化摆放,并且使它们相互之间不受到影响。有了集装箱,我们就不需要专门运输某种特定货品的船了。我们可以把各种货品通过集装箱打包,然后统一放到一艘船上运输。Docker 要做的就是把各种软件打包成一个集装箱(镜像),然后发布到任何流行的Linux机器或Windows 机器上(也可以实现虚拟化),并且其内部的沙箱机制可以确保软件在运行的时候可以相互隔离。安装 DockerDocker支持在当前各大主流平台上安装使用,包括CentOS、Debian、Fedora、Ubuntu等Linux平台以及macOS、Windows等非Linux平台。因为 Linux 是 Docker 的原生支持平台,所以推荐你在 Linux 上使用 Docker。由于在生产环境中 CentOS 使用的较多,下文主要介绍在 CentOS 平台下安装和使用 Docker。前置条件操作系统要求安装 Docker,需要 CentOS 7或8的维护版本。存档版本不支持或未经过测试。必须启用 centos-extras 存储库。 该存储库默认情况下处于启用状态,但是如果你禁用了它,则需要重新启用。建议使用 overlay2 存储驱动程序。卸载旧版本如果你已经安装过旧版的 Docker,可以先执行以下命令卸载旧版 Docker 以及它的依赖项。sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine如果yum报告没有安装这些软件包,那就可以了。/var/lib/docker/中的内容(包括镜像,容器,卷和网络)将被保留。Docker Engine 软件包现在名为 docker-ce。安装方法Docker 官方提供了三种安装方法:设置 Docker 仓库并从中进行安装,以简化安装和升级过程。这是官方推荐的安装方法。下载RPM软件包手动进行安装,并完全手动管理升级。 这在无法访问互联网的空白系统上安装Docker的情况下很有用。在测试和开发环境中,你还可以选择使用自动化脚本来安装Docker。本文介绍通过设置 Docker 仓库来进行安装的方法。设置仓库使用以下命令安装yum-utils软件包(提供yum-config-manager程序)并设置稳定的仓库: sudo yum install -y yum-utils sudo yum-config-manager \ --add-repo \ https://download.docker.com/linux/centos/docker-ce.repo如果在下面的安装过程中遇到”没有可用软件包“错误,可以将仓库地址设置为阿里云的镜像仓库:sudo yum-config-manager \ --add-repo \ https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo安装 Docker Engine安装最新版本的 Docker Engine 和容器:sudo yum install docker-ce docker-ce-cli containerd.io在安装过程中如果提示你接受GPG密钥,需要验证指纹是否与060A 61C5 1B55 8A7F 742B 77AA C52F EB6B 621E 9F35匹配,如果是,选择接受。如果你想要安装指定版本的 Docker,可以使用以下命令列出当前仓库中所以可用的版本,然后选择一个版本进行安装:yum list docker-ce --showduplicates | sort -r 已加载插件:fastestmirror 可安装的软件包 * updates: mirrors.ustc.edu.cn Loading mirror speeds from cached hostfile * extras: mirrors.ustc.edu.cn * epel: d2lzkl7pfhq30w.cloudfront.net docker-ce.x86_64 3:20.10.6-3.el7 docker-ce-stable docker-ce.x86_64 3:20.10.5-3.el7 docker-ce-stable docker-ce.x86_64 3:20.10.4-3.el7 docker-ce-stable docker-ce.x86_64 3:20.10.3-3.el7 docker-ce-stable docker-ce.x86_64 3:20.10.2-3.el7 docker-ce-stable docker-ce.x86_64 3:20.10.1-3.el7 docker-ce-stable docker-ce.x86_64 3:20.10.0-3.el7 docker-ce-stable docker-ce.x86_64 3:19.03.9-3.el7 docker-ce-stable docker-ce.x86_64 3:19.03.8-3.el7 docker-ce-stable docker-ce.x86_64 3:19.03.7-3.el7 docker-ce-stable通过一个完全限定的软件包名称来(即软件包名称(docker-ce)加上版本字符串(第2列),从第一个冒号(:)开始,一直到第一个连字符(-)结束)安装特定版本。例如,docker-ce-20.10.5。sudo yum install docker-ce-<VERSION_STRING> docker-ce-cli-<VERSION_STRING> containerd.ioDocker 安装完成后不会自动启动。此时 docker 的组已经创建了,但是还没有用户添加到组中去。启动 Docker使用如下命令启动 Dockersudo systemctl start docker通过运行 hello world 镜像来验证 Docker 是否正确:sudo docker run hello-world这个命令会下载一个测试镜像并在容器中运行。当容器运行时,它会打印如下消息然后退出:Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world b8dfde127a29: Pull complete Digest: sha256:f2266cbfc127c960fd30e76b7c792dc23b588c0db76233517e1891a4e357d519 Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://hub.docker.com/ For more examples and ideas, visit: https://docs.docker.com/get-started/安装完成后 docker 命令默认只能以 root 用户执行,如果想允许普通用户执行 docker 命令,需要执行以下命令 sudo groupadd docker && sudo gpasswd -a ${USER} docker && sudo systemctl restart docker ,执行完命令后,退出当前命令行窗口并打开新的窗口即可。卸载 Docker使用以下命令卸载 Docker:sudo yum remove docker-ce docker-ce-cli containerd.io上述卸载命令不会删除主机上的镜像、容器、卷或自定义配置文件。要删除所有镜像、容器和卷,可以执行以下操作:sudo rm -rf /var/lib/docker sudo rm -rf /var/lib/containerd容器技术的雏形—chrootchroot 是最早的容器雏形,它意味着切换根目录,有了 chroot 就意味着我们可以把任何目录更改为当前进程的根目录,这与容器非常相似,那么什么是 chroot 呢?下面是维基百科中对 chroot 的定义:chroot 是在 Unix 和 Linux 系统的一个操作,针对正在运作的软件进程和它的子进程,改变它外显的根目录。一个运行在这个环境下,经由 chroot 设置根目录的程序,它不能够对这个指定根目录之外的文件进行访问动作,包括读取,以及更改它的内容。简单来说 ,chroot 就是可以改变某进程的根目录,使这个程序不能访问目录之外的其他目录,仿佛置身于一个独立的文件系统中一样。下面我们通过一个实例来演示一下 chroot。首先我们创建一个 rootfs 目录:[root@node01 local]# mkdir rootfs然后使用 busybox 镜像来创建一个系统,下面的操作你可以理解为在 rootfs 下创建了一些目录和放置了一些二进制文件。[root@node01 local]# cd rootfs [root@node01 rootfs]# docker export $(docker create busybox) -o busybox.tar [root@node01 rootfs]# tar -xf busybox.tar执行完上面的命令后,在 rootfs 目录下,我们会得到以下目录和文件:[root@node01 rootfs]# ls bin busybox.tar dev etc home proc root sys tmp usr var接下来我们通过 chroot 命令启动一个 sh 进程,并且把 /usr/local/rootfs 作为 sh 进程的根目录。[root@node01 rootfs]# chroot /usr/local/rootfs/ /bin/sh / #此时,我们的命令行窗口已经处于上述命令启动的 sh 进程中了。在当前 sh 命令行窗口下,我们使用 ls 命令查看一下当前进程根目录下的内容:/ # /bin/ls / bin busybox.tar dev etc home proc root sys tmp usr var可以看到当前进程的根目录已经变成了主机上的 /usr/local/rootfs 目录。这样就实现了当前进程与主机的隔离。至此,一个目录隔离的容器就完成了。但是,此时还不能称之为一个容器,为什么呢?你可以执行以下命令,查看一下路由信息:/ # /bin/ip route default via 192.168.188.2 dev ens33 metric 100 172.17.0.0/16 dev docker0 scope link src 172.17.0.1 192.168.188.0/24 dev ens33 scope link src 192.168.188.130 metric 100然后新打开一个命令行,使用如下命令查看一下主机的路由信息:[root@node01 ~]# ip route default via 192.168.188.2 dev ens33 proto static metric 100 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 192.168.188.0/24 dev ens33 proto kernel scope link src 192.168.188.130 metric 100可以看到网络信息并没有隔离,实际上进程等信息此时也并未隔离。
2022年01月
2021年11月
2021年10月
2021年09月
2021年08月