并发与线程安全
串行运行时正确的程序在并发运行时可能会出错,这是由于并发运行的多个任务(进程或线程)之间共享了变量。在Java EE环境下很多的组件最终都是在多线程环境下并发运行的,应该牢记住这一点,避免发生诡异的错误。在上个项目中,我们的程序就出现过诡异的错误,在并发量小的测试环境下,它很少出现,但是随着并发量的增大,它出现的次数增多。经查,发现如下类似代码:
public class SomeServlet extends HttpServlet {
private SomeType someVariable;
public void doPost(HttpServletRequest req, HttpServletRespons rsp) {
method1();
method2();
}
void method1() {
...
someVariable = someValue;
...
}
void method2() {
...
someVariable = someValue;
...
}
}
method1() 和method2()都访问到了someVariable,而且程序员的本意是想在method2()使用由method1()产生的 someVariable,但是Servlet可能被多个线程共享,上面的代码不能正常工作。当Servlet容器用thread1和thread2两个线程来服务两个请求request1和request2,而thread1和thread2使用了同一个SomeServlet对象,如果JVM在 SomeServlet在执行到method1()后和method2()前时发生线程间的切换,那么就很可能导致出错,比如:thread1.method1()->thread2.method1()->thread1.method2()->thread2.method2()。实例变量和类变量都是在线程间共享的,而方法内的局部变量和参数是不会被共享的,所以要处理这个问题,最简单的方法是通过方法参数来传递 someVariable,而不是通过实例变量。所以这段程序改成这样就可以了:
public class SomeServlet extends HttpServlet {
public void doPost(HttpServletRequest req, HttpServletRespons rsp) {
SomeType someVariable = method1();
method2(someVariable );
}
SomeType someVariablemethod1() {
...
return someValue;
}
void method2(SomeType someVariable) {
...
someVariable = someValue;
...
}
}
通过synchronized等并发访问控制机制也可以解决这个问题,但是我觉得上面的方式是最直观的。也许有人会说后面的代码比前面的看起来要难看,不够面向对象。这里不讨论怎样才是面向对象,或者面向对象好不好。总之后者是正确的,前者是错误的,没有正确性的程序是没有意义的。
被过线程共享而不会出错的对象,被称之为线程安全的。显然Servlet不是线程安全的,而且还有很多组件也不是线程安全的,比如HttpSession。在上个项目中,我们使用JAXB来处理XML,每次使用时都创建一个JAXBContext对象,后来发现创建JAXBContext的代价是相当的高,于是我们想把它缓存起来在整个应用程序范围内使用,幸好我们使用的JAXBContext的实现是线程安全的,可以放心的被多个线程共享。
有状态与无状态
有状态组件是这样一种组件,组件调用者的返回结果会依赖于这个组件之前或正在受到的调用。无状态的组件则相反,调用者的返回结果只和本次调用相关,所有调用者的调用过程不会影响到其他调用者。举例来说,someComponent组件的getLatestCaller()返回上一次的调用者,这个行为的返回值总会和上一次的调用者相关,这样的组件就是有状态的。假如有个组件math有个方法为add(x,y),返回x+y的值,那么无论之前被哪个调用者调用过,math.add()的返回值只和本次调用的参数相关,这样的组件是无状态的。无状态的组件是线程安全的,因为被别的调用者调用并不会影响到本次的调用结果。
除非有必要,否则应当尽量把你的程序组件实现成无状态的。在spring, seam, ejb里都有无状态的组件。不同的框架在如何实现无状态方面各不相同。最简单的实现方法就是为无状态组件做一个包装,每次调用组件的方法时,都由包装类生成一个新的对象来处理,这样实际上就不再任何的调用者之间共享被调用者的实例,实现了无状态。比如:
public interface SomeInterface {
public SomeType doMyBusinness(SomeArg arg);
}
public class SomeStatlessComponent implements SomeInterface {
public SomeType doMyBusinness(SomeArg arg) {
...
}
}
public class SomeStatlessComponentWrapper implements SomeInterface {
public SomeType doMyBusinness(SomeArg arg) {
return new SomeStatlessComponent().doMyBusinness(arg);
}
}
如果实例化组件的代价是昂贵的,用一个对象池缓存组件实例,并在每次从池中取对象时清空对象的状态可能是个更好的办法。其实只要遵循简单的原则,就很容易实现无状态,就是不使用实例变量或类变量,或者只使用只读的实例变量或只读类变量。这样的组件用单例就可以实先无状态。
由于实现上的差别,有些框架根本就不会做过多的努力来保证组件的无状态性,相反他们把责任交给了程序员。作为组件的创作者,如果你确定你要的是无状态的组件,那么只使用只读的实例变量或类变量,是个明智的选择。
September 26th, 2008数据迁移经验
做关系型数据库数据迁移的时候,为了加快速度,要注意以下几点。
充分利用数据库的能力
关系型数据库被设计成善于做查找,比较,排序等操作。在大数据量的时候,重新写个程序去对数据做查找排序等,效率往往比不过数据库。所以在数据迁移的时 候,应该尽量将此类操作交给数据库来完成。另外数据库往往都有自己的一些工具,比如导入导出工具,如果你的迁移过程要做类似的工作,也应当交给数据库来完 成。假如你要取出数据库里的一些数据,将格式做些转换,再插入到另一个数据库。你可以写个程序读一条,转换一条,写一条,但是这样很慢,还不如把数据导出 成csv等文件,然后再对文本处理生成新的另一个数据库的导入文件,然后再用另一个数据库的导入工具导入。我曾经做过这样的工作,将数据从sql server 2000迁移到 mysql,要对数据做些简单的格式转换,前一种方案用时40分钟,而后一种只要一分钟。
减少IO操作
现代计算机的CPU,总线和内存的速度都远超过各种IO的速度,所以减少IO操作就可以大幅提升数据迁移时的速度。使用连接池重复使用连接,减少新建连接 的次数可以减少IO操作。使用缓存将可能会被多次查询的数据缓存起来,减少查询数据库次数,可以显著减少IO操作。将中间结果缓存到内存或临时表中,非常 有用。最近在工作中遇到的数据迁移,通过大量的应用内存缓存,成功的将数据迁移时间有原来的近一周缩短到一个小时。
注意索引
也许你在做数据迁移时的很多查询操作是平时用不到的,所以并没有适合于做迁移查询的索引,那么你一定要记得在迁移前加上它。同样,需要被插入数据的表的索引还是先删掉吧,迁移完后再加上。
经常维护数据
不完整的数据,在迁移过程中是最让人头痛的。平时经常清理数据中的垃圾,如果垃圾成山了,数据迁移将是恶梦。
当你用jsf的<h:selectOneMenu />之类的控件选择实体时,小心你的实体的equals方法,否则你可能就会遇到”value is not valid”的验证错误。比如,如果你的页面里有这样一段:
<h:selectOneMenu value="#{fooAction.foo}">
<s:selectItems value="#{fooList.resultList}" var="foo" label="#{foo.name}"/>
<s:convertEntity />
</h:selectOneMenu>
你的Foo实体类的equals方法又是这样写的:
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Foo other = (Area) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
return true;
}
很好,你很可能就有机会遭遇seam下这个神奇的value is not valid验证问题了。到底原因何在?原因就在于seam不仅使用了jsf还使用了hibernate,而你的equals方法没有考虑到被对比的双方可 能一个是实体类,另一个可能是被hibernate动态增强过的类。JSF在客户端提交表单后,会验证客户端选中的元素是否是当初服务端给他的那些元素。 所以,JSF会拿经converter得到的实体与当初的集合里的实体一一调用equals方法对比,如果找不到一个在集合中的实体与提交来的一样,就报 错。如果集合里的对象是hibernate 延迟加载时的一个stub,那正常的对象和这个stub对比时就可能出错。对于上面的例子,getClass() != obj.getClass() 会为真,而 other.id != null 也可能为真。怎么办?改写equals方法,如下:
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!getClass().isAssignableFrom(obj.getClass()))
return false;
final Area other = (Area) obj;
if (id == null) {
if (other.getId() != null)
return false;
} else if (!id.equals(other.getId()))
return false;
return true;
}
isAssignableFrom方法对子类调用时一样为真。other.getId()时,则会初始化stub,取出id的值。
还有一种更简单的方法,那就是不要让用户选择实体,而是选择实体的id,那就没这个问题了。
InfoQ是一个很不错的技术网站,内容不是很多,但是都很精。前两天头看到InfoQ的Qclub要在杭州和支付宝联合搞一次活动。主讲人是支付宝的首席架构师程立。抱着讨教经验的想法,我们决定参加这次活动。
活动在7月26日下午在杭州支付宝的一个会议室内举行。来得人不多,只有五十来人,但是气氛非常好。最重头的,也是此行的目的就是要听程立介绍关于支付宝的架构和如何实现SOA。听了之后觉得不虚此行,非常有借鉴意义。
从程立的表述来看,SOA不是空洞的哲学,而是实在的准则。支付宝的面向服务不仅体现在企业信息系统内部,也体现在每一个企业应用。他把一个支付宝的企业系统的发展分为三个阶段。
第一个阶段他们以少数的人,从做应用的角度来做支付宝。早先他们选用spring来做中间的业务层,将业务层再细分为很多层次,这些层次的功能都由 spring bean来提供。后来,spring beans的数量太多,达到数千个,不仅难以管理,也难以使用。尽管开发者可以完全遵循分层的规则把beans分成一组组的,但是由于系统上并没有提供杜 绝开发人员自由调用bean的能力,还是会发生成有些开发人员把层次搞乱的事情。所以在应用级别上,他们提出把beans组成组件,让组件提供服务,把不 能被直接调用的beans隐藏起来。在应用级别上,这也是SOA的一种思想。他们最终采用了OSGI来做为组件基础。
第二个阶段,就是做企业级的平台。一个企业可能有很多的业务模块,一个业务流程可能就要很多的业务模块参与进去。运用SOA的方式,就是把各个相对独立的 业务模块以服务的方式暴露出去,再用ESB来把所有的服务集成起来供上层的企业应用使用,在一个企业之内实现各个业务模块之间的SOA。
第三个阶段,是做开放的面向行业的平台。为了和其他的企业和作,为了给培养整个行业生态圈,最终支付宝走向了开放。这个时候,支付宝以REST或WS方式将支付宝的服务暴露出去。
如果以这三个阶段来评估我现在公司的状况的话,我们正处在第一个阶段,还有很长的路要走。
支付宝的SOA实践并非是死板的照抄SOA标准做法,其中最引人注目的就是他们对事务的处理。如果完全按照WS-Transaction的方式来做,性能 达不到需求,他们就根据业务需要自定义了一套事务管理机制。一个业务流程里的事务原则上来说要满足ACID原则,但是实际中可以根据业务功能的重要度,把 某些功能做成不满足ACID只要满足BaSE就可以。这里的BASE的英文描述我记不得了,意思就是在满足基本业务可用的条件下,在某些业务流程上放宽对 隔离性和一致性的要求,实现最终一致性。如果把满足ACID的叫刚性事务,那么BaSE就是柔性事务。支付宝做到可以通过配置指定一个流程中哪些服务时要 在刚性事务中的,哪些是柔性事务的。估计这个要深入研究事务,才能实现这样的功能。
讨论的时候,还有人提到支付宝如何处理有状态服务在分布式环境下一致性问题,结果程立给出的答案是,支付宝的服务没有状态。看来REST里的服务端无状态原则在SOA中也非常有用,可以简化服务。
这次活动很有收获,看到支付宝在SOA上面的成功,给了我们信心。
如果说软件开发有什么真理的话,我想,那一定就是SOC(Separation of concerns,中文翻译叫关注点分离)。SOC原则,就是在软件开发中,通过各种手段,将问题的各个关注点分开。我已经记不得第一次在什么时候什么地方听说这一原则的,当时看一下就忘掉了。可是随着对软件开发的体会的加深,SOC这个概念不断的在我的脑海中浮现,以致于我把它当作软件开发的真理!
问题太过于复杂,要解决问题需要关注的点太多,而程序员的能力是有限的,不能同时关注于问题的各个方面。正如程序员的记忆力相对于计算机知识来说那么有限一样,程序员解决问题的能力相对于要解决的问题的复杂性也是一样的非常有限。计算机知识太多而且不断进化,计算机程序也太复杂,而且还在不断的复杂化。记得有一本记算机图书中,用这样一个例子来对比程序和传统行业复杂性的差别有多大,说一架精密的波音客机,所有的部件加在一起,总共只有2万个,而GCC编译器,光全局变量就有40万之多,可见程序有多复杂。另一方面,人类的注意力所能同时关注的点又是相当的少的。举个简单的例子,一般人很难同时左右手分别划圆划方,就是因为我们的大脑很难同时关注于两只手,让两只手分别做自己的事。因此,做为普通人的程序员面对这种复杂性时,必须采用某些方法把问题分解成若干部分,这样程序员可以同一时刻只关注于问题的某些方面或部分,如果分解后的部分还是太复杂,那就再划分下去,直到使复杂问题变成一个个的简单问题。这就是关注点分离原则。这一准则,并不是计算机界的发明,只不过在计算机界显得更加的重要。
实现关注点分离的方法主要有两种,一种是标准化,另一种是抽象与包装。
标准化就是制定一套标准,让使用者都遵守它,将人们的行为统一起来,这样使用标准的人就不用担心别人会有很多种不同的实现,使自己的程序不能和别人的配合。Java EE就是一个标准的大集合。如果所有的应用服务器的开发者和应用的开发者都按照标准来做,那么应用开发者就不用关心不同的应用服务器有什么差别,服务器的开发者也不用担心应用开发者开发的应用有什么差别。每个开发都只需要关注于标准本身和他所在做的事情就行了。就像是开发镙丝钉的人只专注于开发镙丝钉就行了,而不用关注镙帽是怎么生产的,反正镙帽和镙丝钉按标来就一定能合得上。也就是因为标准具有这样的威力,所有计算机界有很多标准。
不断地把程序的某些部分抽像差包装起来,也是实现关注点分离的好方法。一旦一个函数被抽像出来并实现了,那么使用函数的人就不用关心这个函数是如何实现的,同样的,一旦一个类被抽像并实现了,类的使用者也不用再关注于这个类的内部是如何实现的。诸如组件,分层,面向服务,等等这些概念都是在不同的层次上做抽像和包装,以使得使用者不用关心它的内部实现细节。
每一个程序员都应当理解SOC,并在实践中遵循这一真理。当你在编程的过程中,一时搞不清程序的每一个细节,你可以只关注于主干,把程序的主干写出来,再逐一关注没个分支。这是一种自上而下的方法。如果主干也搞不清,那可以先关注于分支,写出分支,再不断地组合这些分支成为主干,这是一种自下而上的方法。时刻考虑着,是不是要把这一段程序抽像成一个方法,或者类,是不是那样会更好。如果不这样去想去做,要么面对问题无从下手,写不出什么来,要么只会写出面条似的拖沓冗长的代码。不会用SOC,摆在面前的永远是复杂得不能解决的问题。