当前我们大量使用框架或中间件,从底层的API到软件框架、web服务器、集群软件,在享受各种便利的同时也使得我们对这些外部软件的理解更模糊,也剥夺了我们了解这些轮子设计的权利。当我们遇到新需求时也很难知道应该如何选择这些轮子,哪些特性才是应用真正要关心的,哪些特性是不需要的。(正是因为需求场景不同,才导致世界上每天都有新的轮子产生)
下面我站在应用开发者的角度介绍下我们应该了解的架构知识:
1.单台应用服务器内部介绍:
一个应用服务器(Servlet容器、也叫Web容器)本身是一个java虚拟机,其中运行这我们整个系统(或是多套系统),对外Servlet提供的功能是根据输入信息进行计算和处理,最终返回客户需要的结果,即对http请求进行处理,并返回响应。Servlet服务器天生就是多线程的,对于每个http请求都会由单独的线程处理。由于线程的创建和销毁开销很大,一般会采用线程池来处理,每个线程处理结束会重新回到线程池。java官方提供的线程池很强大,可以支持各种配置,具体可以参考java自带线程池和队列详细讲解。
我们应用的代码也是由这些servlet线程调起的,由于servlet服务器天生是多线程的,我们的代码当然也是多线程的,即使你没有显式地声明线程。
[注]:不同的应用服务器的实现方式不一样,例如Ruby的Thin是基于事件驱动的,单线程的;Passenger普通版也是单线程的(可以多进程),但是Puma是多线程的。所以作为应用开发者需要了解这些基础的运行环境知识,了解这些环境下可能会遇到的并发或执行问题是什么。
2.Spring框架的作用:
现在普遍使用Spring框架进行后台应用的开发。Spring会负责整个Java应用的启动、加载和销毁的全生命周期,对于可以重用资源(Bean)会保证整个服务器只会加载一次,对于某些资源比如数据库连接等全局资源会保证其线程安全。如果一个Servlet容器只启动一个Spring容器,则Spring框架在整个虚拟机只会装载一份。
刚刚提到Spring的Bean一般是单例的(Spring允许一个类创建两个实例,每个实例有不同的名称,每个实例是独立的),这些单例在web环境下肯定会被多线程访问,也就是说它们一定要支持多线程,且线程安全。为了很简单地支持多线程,一些公司(包括我们公司)会要求在类中不要包含类变量,包括私有的和公有的,这是为了防止大家写错而导致并发问题。这些单例类如果必须有类变量,这就需要应用开发者自己来保证它的线程安全性。如何设计线程安全的类,可以参考“线程安全简介”
3.我们的代码如何被调起来处理web请求:
常见的Web服务器有Apache、Nginx,这些服务器擅长处理http解析,静态资源,如html、图片、js、css等文件;在处理具体的应用逻辑时会委托给相应的应用服务器(Java中也称为Servlet容器)来处理,常见的应用服务器有Tomcat、Jetty、Weblogic、Jboss。Web服务器和应用服务器一般部署在一起,内部通信一般通过http或是其他自有协议。现在应用服务器集成的功能越来越多,很多应用服务器也具有web服务器的功能,但还是不建议直接使用应用服务器来代替web服务器。
- Web服务器在接收到客户的http请求后,根据特定的路由策略,如果是静态资源则直接返回,对于动态请求,会转发给应用服务器来处理。
- 应用服务器接收到请求后会进行消息解析,会为每个请求生成一个线程来处理此请求,此线程会调用对应的Servlet来进行实际业务处理。
- Servlet在处理过程中可能会访问数据库、创建自动任务,或是访问外部其他服务器(使用特定的接口规范)。
- Servlet处理完成后返回相应的Response对象给应用服务器。
- 应用服务器将此响应转化为http响应返回给web服务器。
- Web服务器将http响应返回给客户。(在特殊场景下web服务器可能会根据应用服务器的响应来做一些额外处理,比如返回静态文件)
对于Web服务器就不多做介绍了,因为它的功能比较简单。对于应用服务器澄清几件事:
- 应用服务器(Java环境)其实是一个Java程序,它运行在一个JVM虚拟机中。
- 应用服务器可以部署多个应用,每个应用之间彼此隔离,包括类加载路径。这是通过类加载器实现的。应用服务器会对每个应用使用单独的类加载器来加载。(热部署也与类加载器有关,有兴趣可以参考《深入理解Java虚拟机》)
- 应用服务器本身有一个线程池,用来处理http请求。
- 线程是JVM的概念,所有的线程都是归JVM统一调度的,包括应用服务器的应用自身启动的线程、Spring的线程池、JMS的线程池、Quarz的线程池等。
- 每个应用的每个请求是在1个线程内处理的。
- 同一个应用的不同线程共享一份类中的变量,不同应用之间相互隔离。
- ThreadLocal的全局变量比较特殊,是线程级别的,不是所有线程共享的
4.应用的生命周期知识
Web应用都会涉及到服务器的启停,这就会涉及到应用的生命周期,Spring框架也提供了生命周期的接口以及框架加载和销毁的接口。我们应该对这些生命周期有一定的了解,比如在启动时Spring框架会加载所有的Bean和其他资源。在停止时会对所有的Bean进行销毁,尝试关闭线程池。这里需要注意几点:
- Bean销毁本质上是删除Bean在Spring中注册的引用,如果某个类正在使用某个Bean还是可以正常使用的,但是如果你尝试通过全局方法根据Bean的Name来查找某个Bean,这时会报错。
- Web应用在关闭时会保证应用的联机交易能够执行完它的处理逻辑。
- 线程池关闭不是简单的kill,这和具体的线程池策略有关。为了防止由于某个任务执行时间过长而影响关闭问题,应该保证任务对中断异常进行处理,从而正常终止任务。(参考Java多线程发展简史了解为何应该使用interrupt、wait、notify)线程池关闭是一个复杂的问题,需要框架予以支持,后续我会专门进行介绍。
- 服务器可能在任意时刻停止,比如out of memory这时finally中的逻辑未必能被执行。只有数据库能够保证异常停止后状态的一致性。应用应该对服务器的异常停止有检测机制,至少能检查核心数据状态有无异常。在一致性要求高的场合最靠谱的就是使用事务保证一致性。
- 处理数据量较大、运行时间较长的任务在编写时应当对生命周期进行支持,当关闭时进行特殊的处理,例如实现spring的DisposableBean或是在通过注解、xml的方式进行配置;如果采用了线程池,线程池调起的任务要支持对interrupt做出响应,以便线程正常结束它自己。
上面提到的是关闭时应该注意的事,启动时也有生命周期的概念,对于启动时需要加载某些资源,或是某些Bean需要按照特定顺序启动或是销毁,这时可以借助框架提供的功能,Spring提供了对于生命周期的很好的支持,可以阅读Spring的Reference文档。
5.其他一些常用组件:
1. Quartz:
它提供对于定时定频任务的支持,支持特定时间间隔调起某个任务。由于任务可能很多,为了防止过多消耗系统资源,它一般使用线程池的方式,由线程池来处理具体的任务。
2. Spring Batch:
它对于大批数据的处理提供的统一的框架,它对于失败重跑、重试以及事务控制等提供支持。另外它还可以控制多个任务的执行时序控制,并提供了并行处理功能(可以支持单机器线程池的并发、或是跨机器的并发处理)。在并发处理时,sping batch可以指定对应的线程池进行处理。
3.JMS:
JMS是java的消息处理机制,其本质是完成一个消息传输,JMS一般包括如下角色:
- 接收队列:用于接收具体的JMS消息,队列可以设置队列大小、存活时间等其他一些参数。
- 响应队列:其实本质也是一个接收队列。如果不需要响应就不需要此队列。
- JMS容器:负责管理这些队列并对接收的消息进行分发处理。其内部会有一个线程池,以支持多多个消息并发处理。
- JMS消息监听器:可以配置不同的监听规则,只监听某个队列或是某个消息。监听器会根据具体的消息来进行实际的业务处理。
JMS队列在集群中可以由单独的服务器担任,也可以和应用服务器部署在一起。从角色上看可以分为:(1)本地队列:即每个服务器都有一个对应的队列。这种队列只用于服务器各个线程之间通讯使用;(2)全局队列:整个集群只有1个队列,可以作为唯一点来做一些事项的处理。
6.全局变量、单例bean、线程:
- 全局变量是应用级的,每个应用有一份,更严格的讲是classloader级别的。
- 单例bean是框架级的,是由框架来保证唯一性的,它本质也是以一个普通的对象,只是整个应用只有1份。
- 线程:线程是JVM级别的。应用的代码可以运行在不同的线程上。可以看出上面的全局变量、单例bean都需要保证是线程安全的,因为他们可能同时运行在多个线程上。
- ThreadLocal变量:每个线程对于某个对象只有1份,对于整个应用来讲是多份。上面这些变量都是应用隔离的。
有了上面的概念再理解一些并发问题、事务处理机制、分布式事务就会轻松很多了。在进行不同软件、不同框架的选择上也可以从容很多,不会离了spring干不了活了,哈哈。