应用开发者应该了解的架构知识

当前我们大量使用框架或中间件,从底层的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服务器。

  1. Web服务器在接收到客户的http请求后,根据特定的路由策略,如果是静态资源则直接返回,对于动态请求,会转发给应用服务器来处理。
  2. 应用服务器接收到请求后会进行消息解析,会为每个请求生成一个线程来处理此请求,此线程会调用对应的Servlet来进行实际业务处理。
  3. Servlet在处理过程中可能会访问数据库、创建自动任务,或是访问外部其他服务器(使用特定的接口规范)。
  4. Servlet处理完成后返回相应的Response对象给应用服务器。
  5. 应用服务器将此响应转化为http响应返回给web服务器。
  6. Web服务器将http响应返回给客户。(在特殊场景下web服务器可能会根据应用服务器的响应来做一些额外处理,比如返回静态文件)

对于Web服务器就不多做介绍了,因为它的功能比较简单。对于应用服务器澄清几件事:

  1. 应用服务器(Java环境)其实是一个Java程序,它运行在一个JVM虚拟机中。
  2. 应用服务器可以部署多个应用,每个应用之间彼此隔离,包括类加载路径。这是通过类加载器实现的。应用服务器会对每个应用使用单独的类加载器来加载。(热部署也与类加载器有关,有兴趣可以参考《深入理解Java虚拟机》)
  3. 应用服务器本身有一个线程池,用来处理http请求。
  4. 线程是JVM的概念,所有的线程都是归JVM统一调度的,包括应用服务器的应用自身启动的线程、Spring的线程池、JMS的线程池、Quarz的线程池等。
  5. 每个应用的每个请求是在1个线程内处理的。
  6. 同一个应用的不同线程共享一份类中的变量,不同应用之间相互隔离。
  7. ThreadLocal的全局变量比较特殊,是线程级别的,不是所有线程共享的

4.应用的生命周期知识

Web应用都会涉及到服务器的启停,这就会涉及到应用的生命周期,Spring框架也提供了生命周期的接口以及框架加载和销毁的接口。我们应该对这些生命周期有一定的了解,比如在启动时Spring框架会加载所有的Bean和其他资源。在停止时会对所有的Bean进行销毁,尝试关闭线程池。这里需要注意几点:

  1. Bean销毁本质上是删除Bean在Spring中注册的引用,如果某个类正在使用某个Bean还是可以正常使用的,但是如果你尝试通过全局方法根据Bean的Name来查找某个Bean,这时会报错。
  2. Web应用在关闭时会保证应用的联机交易能够执行完它的处理逻辑。
  3. 线程池关闭不是简单的kill,这和具体的线程池策略有关。为了防止由于某个任务执行时间过长而影响关闭问题,应该保证任务对中断异常进行处理,从而正常终止任务。(参考Java多线程发展简史了解为何应该使用interrupt、wait、notify)线程池关闭是一个复杂的问题,需要框架予以支持,后续我会专门进行介绍。
  4. 服务器可能在任意时刻停止,比如out of memory这时finally中的逻辑未必能被执行。只有数据库能够保证异常停止后状态的一致性。应用应该对服务器的异常停止有检测机制,至少能检查核心数据状态有无异常。在一致性要求高的场合最靠谱的就是使用事务保证一致性。
  5. 处理数据量较大、运行时间较长的任务在编写时应当对生命周期进行支持,当关闭时进行特殊的处理,例如实现spring的DisposableBean或是在通过注解、xml的方式进行配置;如果采用了线程池,线程池调起的任务要支持对interrupt做出响应,以便线程正常结束它自己。

上面提到的是关闭时应该注意的事,启动时也有生命周期的概念,对于启动时需要加载某些资源,或是某些Bean需要按照特定顺序启动或是销毁,这时可以借助框架提供的功能,Spring提供了对于生命周期的很好的支持,可以阅读Spring的Reference文档

5.其他一些常用组件:

1. Quartz:

它提供对于定时定频任务的支持,支持特定时间间隔调起某个任务。由于任务可能很多,为了防止过多消耗系统资源,它一般使用线程池的方式,由线程池来处理具体的任务。

2. Spring Batch:

它对于大批数据的处理提供的统一的框架,它对于失败重跑、重试以及事务控制等提供支持。另外它还可以控制多个任务的执行时序控制,并提供了并行处理功能(可以支持单机器线程池的并发、或是跨机器的并发处理)。在并发处理时,sping batch可以指定对应的线程池进行处理。

3.JMS:

JMS是java的消息处理机制,其本质是完成一个消息传输,JMS一般包括如下角色:

  1. 接收队列:用于接收具体的JMS消息,队列可以设置队列大小、存活时间等其他一些参数。
  2. 响应队列:其实本质也是一个接收队列。如果不需要响应就不需要此队列。
  3. JMS容器:负责管理这些队列并对接收的消息进行分发处理。其内部会有一个线程池,以支持多多个消息并发处理。
  4. JMS消息监听器:可以配置不同的监听规则,只监听某个队列或是某个消息。监听器会根据具体的消息来进行实际的业务处理。

JMS队列在集群中可以由单独的服务器担任,也可以和应用服务器部署在一起。从角色上看可以分为:(1)本地队列:即每个服务器都有一个对应的队列。这种队列只用于服务器各个线程之间通讯使用;(2)全局队列:整个集群只有1个队列,可以作为唯一点来做一些事项的处理。

6.全局变量、单例bean、线程:

  • 全局变量是应用级的,每个应用有一份,更严格的讲是classloader级别的。
  • 单例bean是框架级的,是由框架来保证唯一性的,它本质也是以一个普通的对象,只是整个应用只有1份。
  • 线程:线程是JVM级别的。应用的代码可以运行在不同的线程上。可以看出上面的全局变量、单例bean都需要保证是线程安全的,因为他们可能同时运行在多个线程上。
  • ThreadLocal变量:每个线程对于某个对象只有1份,对于整个应用来讲是多份。上面这些变量都是应用隔离的。

有了上面的概念再理解一些并发问题、事务处理机制、分布式事务就会轻松很多了。在进行不同软件、不同框架的选择上也可以从容很多,不会离了spring干不了活了,哈哈。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

线程安全简介

线程和进程都可以被操作系统直接调度,所不同的是多个线程之间会共享内存空间,但是多个进程之间的内存空间是相互隔离的。随着单核CPU的处理速度达到上限,多核时代正式到来,如何更好地利用多核资源成为了一个必须考虑的话题。在Java中倾向与使用多线程模型,Java针对多线程做了很多优化,包括提供了成熟的线程池1、线程安全工具类以及对锁的优化等。Java Web服务器也都是多线程的。但多线程编程一直以来都很困难且容易出错,虽然有很好的工具支撑,但编写代码时也需要格外注意,稍不小心就会掉进并发的陷阱。另一方面有些语言如Ruby则倾向于多进程的方式,由操作系统来实现并发且保证程序正确性,借助Linux的copy on write属性来节省内存。两种方式哪种更好已经超出了本文的范畴,这也是仁者见仁智者见智。值得一提的是Ruby on Rails 4开始正式支持多线程,Rails 5 更是使用puma作为默认的开发服务器,这也从另一角度预示着多线程的到来。

哪些情况是线程安全的?

下面主要讨论下多线程环境下的线程安全问题,线程安全主要是由于并发导致的,1.操作系统可能在任何时刻挂起某个线程;2.多核情况下两个线程可能处于并向执行的情况。其中第一点可以看出在单核情况下也会有线程安全问题。下面介绍哪些情况是线程安全的:

  1. 单线程环境中,包括多进程单线程、基于事件的程序(服务器)中,不存在多个线程同时修改或访问同一个变量的情况,这种情况下肯定线程安全的。为什么把这个放在第一位呢,因为如果你可以选择一个多进程单线程的Web服务器或是基于事件的服务器你就从根本上解决了线程安全的问题(当然可能有其他问题,比如内存消耗大、状态信息如session不一致等);
  2. 局部变量:即方法中声明的局部变量,这些是线程安全的,它们会随着方法调用结束而被回收,且只会对本线程可见。
  3. ThreadLocal全局变量:这些变量是线程级的,线程安全的,java会保证每个线程只有一份,每个线程只能访问属于自己的ThreadLocal变量。
  4. 线程安全的类:java中很多类会明确声明是线程安全的,例如concurrent包下类。这些类在访问是自身会进行加锁、原子化等操作来保证线程安全。
  5. 线程安全的静态类方法:这些类是单例的,其内部会保证线程安全,spring中很多类都属于此类。
  6. 无状态的bean。如没有状态的控制器bean。spring中的所有pojo类都可以是bean,且是单例的,这种类如果包括实例变量,不管共有私有都不一定是线程安全的。
  7. 不可变对象。例如java中的string类,以及scala中很多不可变的类。不可变类有很多优点,对于并发,可重复执行很有用。 上面提到的都是多个线程可以同时访问的变量,比如全局变量、类的静态变量、单例或是bean中的类实例变量(包括共有、私有)。

线程安全的优化方法

如果程序中确实需要包括这些共享信息,如何保证线程安全呢?可以采用如下措施:

  1. 将公共信息保存在数据库中,或是其他公共的位置。这样除了保证线程安全还可以保证一致性,但会牺牲效率。
  2. 对变量进行ThreadLocal包装,作为线程局部变量,做到线程间独立。
  3. 使用线程安全的类保存和处理这些共享信息(变量),如使用java的concurrent包的常用工具类
  4. 对共享信息的存取方法进行封装,并进行同步处理(加锁)。这里要注意不能只对set方法加锁,一方面是因为get方法不加锁不能保证取得的数据是最新的数据(参考java的happen before2),另一方面是除非你认真考虑了源码的各个分支情况,包括其底层调用的各个细节,否则读取方法也可能很危险,因为读取方法并没有规定不能进行其他处理,只是很多情况下它只是简单返回而已。SimpleDateFormat这个类在java中就不是线程安全的,我们有次想当然的把它作为静态变量,结果在日期格式化的时候出了问题,返回日期出现了错乱。
    [注]:Ruby中没有happen before的概念,取而代之的是GIL解释器锁3,这个锁会保证所有的单个native c方法的原子性。这样就导致在同一个进程内同时只有1个线程在使用CPU,如果是单进程单线程模式下甚至都不能充分使用1个CPU。如果要真正使用多核只能采用多进程。GIL的存在并不是必须的,只是对多线程实现的一个妥协。
  5. 如果多个状态不是独立的,这时即使每个状态是线程安全的,但整个对象也不是线程安全的,仍然需要同步处理(这样来看的话其实内部状态是不是线程安全就没什么意义了)。这时也可以使用下面要提的不变对象来处理。
  6. 对变量进行不变处理,使用不变对象保存状态。当更新状态时新建一个对象。在赋值时需要注意进行同步处理或是使用java的volatile标识。不变对象特别适合与同时保存多个关联的状态的情况,这样可以把多个状态当做1个来处理。
  7. 除非一个第三方类声明为线程安全,或是你认真读过它的代码,否则都可能有线程问题。这时需要进行同步处理或是进行充分测试。

如何保证整个程序的线程安全?4

  1. 框架自身是线程安全的
  2. 我们自己的应用代码是线程安全的
  3. 我们依赖的第三方代码是线程安全的

线程安全编程是很困难的,也是bug事故的多发区,除非性能要求否则尽量不要贸然使用多线程。现在很多web服务本身就是多线程的,进而我们的应用本身就是多线程的,在开发时也要小心,尽量不使用有状态的类。

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

文件字符编码概述

作为一个程序员,不可避免要经常与字符编码打交道。代码乱码、数据乱码、报文乱码也是时常遇到。近期生产上也出现了乱码、非法字符问题,趁着这个机会对字符编码进行下总结。

1.文件内容格式概述

首先大家应该理解:文件其实是一系列的二进制流,界面展示(或程序执行)相当于对这些二进制流的一种转义。理解这一点再考虑后面的问题就简单多了。

对于文件来说里面可以存放任何内容,例如文件可以存放文本、图片、音乐、视频甚至是程序二进制可执行文件。不同的文件需要不同的软件才能进行查看,例如一般的文本可以用记事本、UltraEdit、vim等来查看;图片可以用系统自带的看图工具打开,但编辑则需要Photoshop等专业工具;音乐和视频文件也类似。为了进一步标识哪些文件可以用什么软件打开或编辑,就有了文件拓展名(后缀),例如.txt、.pdf、.mp3等等,文件拓展名只是一个标识,并不影响具体的文件内容和展示。

下面再说下二进制与文本文件的区别:文本文件是指你能够直接用文本编辑器打开,能直接读得懂。二进制文件则需要特定的软件来读取。

2.文本文件编码问题

文本文件是我们最常遇到的文件格式,它们的后缀有很多种比如txt,log,rb,java,h,cpp等等,当然也不局限于这些后缀。刚也说过了后缀并不影响文件的内容和展示,一个没有后缀的文本文件也可以正常打开和查看。文本文件其实也是一系列的二进制流,但它是遵循某个编码标准的(另外还有BOM等特殊规定),常见的编码标准有ASCII、GBK、UTF-8等等。这些编码定义了二级制和字符之间的映射关系,例如汉字“中”的UTF-8编码为E4 B8 AD,它的GBK编码为D6 D0。编码可以参考这些博文1 2

假设文件是用GBK编码写的,但是当成UTF-8格式来读,得到的结果肯定是不对的,可能会有乱码或者天书文字。当然了,现在编辑器还是挺智能的,它们一般能识别出文件使用的编码,所以看上去没什么问题。遇到乱码问题需要做两件事:1.确定文件使用的正确编码格式;2.利用iconv等编码转换工具将文件转换为其他你指定的编码;或者指定让编辑器采用和文件一致的格式来读取。

大家可能还遇到过一种事情,我的编码选对了,大部分字符都能正确显示,但仍然有部分是?、[]或是其他乱码,这种一种可能是文件本身有问题,另一种是字符集的问题。比如Unicode定义了U+00000-U+10FFFF所有的字符集(其中U+D800-U+DFFF为UTF-16的代理对字段),但是一般的字体、软件仅支持U+0000-U+FFFF中的常用字符。对于这之外的即使是合法的字符,也不支持显示。

3.不同程序对于字符编码的支持

不同程序对于字符编码的支持各异,下面分别以java和ruby为例进行简单说明:

Java编码

在Java内部支持unicode编码,严格地说它采用的是UTF-16编码,对于BMP字符严格支持,但对于增补字符U+100000-U+10FFFF支持不佳,在调用函数时需小心。具体参见3 4

在java中常见的中文string,length方法和substring方法都能得到正确的结果,但是对于增补字符会有问题,可能出现半个字的问题。Java中的length对于U+0000-U+FFFF的字符会认为是长度为1,但对于增补字符会认为是2。

String str = "今天天气很好" ;
Assert. assertEquals(6, str.length());
Assert. assertEquals("天气", str.substring(2, 4));
Assert. assertEquals(2, "\uDD1E\uD834".length());//增补特殊字符
Assert. assertEquals("\uD834", "\uDD1E\uD834".substring(1, 2));

Ruby编码

在ruby中1.9+采用原始文件的编码,可以在文件、io等进行指定,字符串长度等方法对于字符集可以很好的支持,对于增补字符也能得到正确的结果。具体参见5

"今天天气很好".length   =>  6
"\u{1DD1E}".length  => 1

4.其他的编码规范

URL编码:

在URI规范中存在一些保留字符,如:/?&=@%等,这些字符如果要作为参数使用而不是作为特殊字符使用,必须在%字符后以十六进制数值表示,例如%3A表示:;另外由于URL只能使用ASCII编码,所以对于中文等需要按照网页的编码格式进行编码,例如“中”应表示为%E4%B8%AD;另外URL 不能包含空格,通常使用 + 来替换空格。

XML编码:

在xml中对于特殊字符如 &<>’" 也需要进行转义。例如使用&lt;来表示<。在报文或是文件中需要对于这些特殊字符进行处理,如转义或是加上<![CDATE[ ]]>。否则在读取或保存是会出错。

注:严格地讲,在 XML 中仅有字符 “<”和”&” 是非法的。省略号、引号和大于号是合法的,但是把它们替换为实体引用是个好的习惯。 在XML的头部可以指定文件的编码方式,包括gbk,utf-8等,这些字符并不需要特殊转义,只要和声明的编码一致即可。

HTML编码:

和xml一样,对于&<>’”也需要进行转义处理。另外html对于其他一些不易输入的特殊字符可以使用特殊编码6 7。对于文件中的字符只要和文件头中声明的字符集一致即可。

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》