企业级应用系统体系架构十状态管理

企业级应用系统体系架构十状态管理

ID:21499868

大小:717.50 KB

页数:107页

时间:2018-10-19

上传者:U-13048
企业级应用系统体系架构十状态管理_第1页
企业级应用系统体系架构十状态管理_第2页
企业级应用系统体系架构十状态管理_第3页
企业级应用系统体系架构十状态管理_第4页
企业级应用系统体系架构十状态管理_第5页
资源描述:

《企业级应用系统体系架构十状态管理》由会员上传分享,免费在线阅读,更多相关内容在教育资源-天天文库

企业级应用系统体系架构(十)状态管理ChenHaopengThursday,July08,2021References:Ted Neward:EffectiveEnterpriseJava1 2状态管理对于寻求真理的人而言,有些准则是必须遵守的,真理并非教条或无知,而是通过推理、调查、检验、与探究得来的。无论其意图有多好,信仰都必须构建在事实而非幻想之上,幻想之上的信仰是最糟糕的虚假希望。——ThomasEdison 3状态管理在企业级系统中,大部分工作都涉及数据处理。事实上,可以论证,企业级系统只做了一件事,那就是数据处理。在两层架构的客户/服务器系统时代,这还不太明显,那时企业级程序员需要关心两种状态:瞬时状态(transientstate)与持久状态(durablestate)。瞬时状态并不算企业级数据所覆盖的正式部分,持久状态则是无论发生什么都需要被跟踪的部分。 4状态管理瞬时状态是那些企业并不关心,也不会为之流泪的数据,因为在系统崩溃的时候,真正重要的东西决不会丢失。电子商务中的购物车是瞬时状态典型的例子。在厚客户端或富客户端的应用中,瞬时状态很容易处理:相当于客户端进程中存储在局部变量中的数据,它们没有被保存在持久的存储介质中。当客户端进程结束的时候,瞬时状态也随之消亡,无需为其生命周期的处理而多费心思。 5状态管理不过在瘦客户端中,例如基于HTML浏览器的应用,瞬时状态则呈现出另一种尺度。因为HTTP本就是无状态的协议,自身并不具备保存每一个客户端状态的能力。所以要由程序员在底层协议之上自己实现瞬时状态机制。另一方面则是持久状态,在谈到它时人们自然就会想到“持久数据(persistentdata)”,即需要长久保存的数据。正规的说法是,如果定义了某个持久状态,那么它就绝对会被保存下来,即使遇到JVM终止甚至崩溃的情况也是如此。 6状态管理持久状态通常具有隐含的法律性或经济意义。在讨论状态管理时,二者的区别至关重要。因为对瞬时状态有效的机制对持久状态不一定有效,反之亦然。 7状态处理第1项:节省地使用HttpSession第2项:使用对象优先的持久化来保存你的领域模型第3项:使用关系优先的持久化来显示关系模型的威力第4项:使用过程优先的持久化来创建一个封装层第5项:识别对象-层次结构的阻抗失配(impedancemismatch) 8节省地使用HttpSession在基于HTML/HTTP的应用中,为维护代表客户端的瞬时状态,servlet容器提供了一种称为会话空间的设施,被表示为HttpSession接口。遗憾的是,这种机制并非完全免费的。首先,在服务器端为每个客户端存储数据将会减少该服务器上的可用资源,这意味着服务器的最大负载能力会成比例地下降。这个算式很简单:在会话空间中保存越多的数据,机器能够处理的会话就越少。由此推导出,为了令给定的机器能够支持尽可能多的客户端,必须将会话的存储量保持在最小。实际上,对于真正具备可扩展性的系统而言,无论何时都应该避免使用会话。如果在服务器端可以不产生任何为每个客户端进行处理的开销,那么机器的负载能力(在理论上)可以到达无限,能够支持任意多连接到它的客户端。 9节省地使用HttpSession避免使用会话的建议不单单是考虑到系统的可扩展性。对于在Web集群内运行的servlet容器而言,这也是必须的。会话是驻留内存的结构。因为内存是局限于特定的机器,除非Web集群有某种机制,能够令给定客户端的每一次请求都被传送给同一个服务器,否则对应先前的某个请求,其后续处理可能会找不到之前存储的会话对象。 10节省地使用HttpSession有一种可能的机制可以对此提供支持:在服务器集群中,指派一个单独的节点作为会话状态服务器。对每一个请求,无论哪个节点正在处理它,该节点都向会话状态服务器查询此客户端的会话状态,然后将会话状态通过网络传给处理此请求的节点。然而,这种机制有两种副作用:(1)每个请求都增加了一次与会话状态服务器之间的往返访问,这增加了客户端请求的等待时间。但更重要的是,(2)所有的会话状态都被存储在集中的服务器上,这使得集群中产生了一个单一故障点(singlepointoffailure)。 11节省地使用HttpSession另一种可能的机制是采用P2P(peer-to-peer)方式。当一个请求进入某节点时,此节点发出一个集群广播信号,询问其它节点是否拥有此客户端最近的会话状态。拥有此客户端最近状态的节点对此做出回答,并将该会话状态传递给当前正处理请求的节点。 12节省地使用HttpSession与会话相关的另一个需要注意的问题是会话的意外使用。JSP的规范清楚地陈述道,对于给定的JSP网页,“打开”会话的指示(带有session属性的@page指示性标记),缺省地被设置为true,这意味着下面的JSP网页将为其建立一个会话,即使该网页从未使用会话:<%@pageimport=”java.util.*”>Hello,world,I’mastatelessJSPpage.Itisnow<%=newDate()%>. 13节省地使用HttpSession更糟的是,在Web应用的任何角落,只要有一个这样的JSP页面,在该客户端使用此Web应用的整个过程中,该会话(即使没有在会话中保存对象,也会带来相关的系统开销。)将一直存在。所以,除非你需要使用会话,否则你应该确保你的JSP页面都关闭会话。我希望对于给定的Web应用,有某种方法能够令session=false成为缺省设置,然而到目前为止它并非如此。希望你不要误解,我并不是鼓吹完全不使用会话,实际上,如果建议HTTP不提供任何合理的机制以提供每个用户的状态的话,将是很滑稽的。如果小心地使用,那么HttpSession就可以提供必要而强大的机制去提供在Web应用中的每个用户的状态,而这通常是关键而且必需的东西。(否则怎么才能确定用户是否成功地通过了身份认证,或者在应用中跟踪用户的进度呢?)危险在于,不是必需使用会话的时候,过度地使用或滥用此机制,那将给servlet容器带来额外的开销。所以,如非必要,尽量不要使用会话,如果非用不可,为了尽可能少地消耗运行servlet容器的机器上的资源,请保持会话精简而有意义。 14状态处理第1项:节省地使用HttpSession第2项:使用对象优先的持久化来保存你的领域模型第3项:使用关系优先的持久化来显示关系模型的威力第4项:使用过程优先的持久化来创建一个封装层第5项:识别对象-层次结构的阻抗失配(impedancemismatch) 15使用对象优先的持久化 来保存你的领域模型使用对象优先的持久化方式时,我们力求在持久化的过程中保持对象的视角。这意味着无需我们的任何提示,对象就知道如何默默地持久化自己,或者它们能够提供某种以对象为中心的(object-centric)API进行持久化和读取操作。 16使用对象优先的持久化 来保存你的领域模型那么在理想世界中,编写像下面这样的代码将自动在数据库中创建一个包含了代表25岁的StuHalloway的项:Personp=newPerson(“Stu”,“Halloway”,25);System.out.println(p);//Prints“StuHalloway,age25”而下面的代码将自动更新在第一段代码中所创建的行,将Stu的年龄从25改成30:Personp=Person.find(“Stu”,“Halloway”);System.out.println(p);//Prints“StuHalloway,age25”p.setAge(30);System.out.println(p);//Prints“StuHalloway,age30”注意到了吗,对象优先持久化方法的一个重要的优点:没有丑陋的SQL语句,不用为是该INSERT还是该UPDATE这样的问题而烦心。我们所能看到的只是对象,这正是我们喜欢的方式。 17使用对象优先的持久化 来保存你的领域模型然而,在读取对象时,对象优先的方法往往很快就不起作用了。一般说来,对象优先的方法可能会采取以下两种方式:要么以纯面向对象的形式,通过创建包含查询规则的对象,进行对象查询;要么使用某种特定的“查询语言”进行对象查询。在纯粹的对象优先的环境中,除了对象,我们不希望看到任何东西,所以我们创建了查询对象(QueryObject),它包含了我们所关心的约束查询的规则。遗憾的是,如果要创建一个复杂的查询,若它的查询规则不是对象的主键(有时称为对象标识符objectidentifier,简称OID),从OODBMS的角度来看,这样做通常是复杂而且笨拙的:QueryObjectq=newQueryObject(Person.class);q.add(Criteria.and(Criteria.greaterThan(“dependents”,2)),Criteria.lessThan(“income”,80000)));q.add(Criteria.and(Criteria.greaterThan(“dependents”,0)),Criteria.lessThan(“income”,60000))); 18使用对象优先的持久化 来保存你的领域模型我们此处所做的等价于下面几行语句:SELECT*FROMpersonpWHERE((p.dependents>2ANDp.income<80000)OR(p.dependents>0ANDp.income<60000))哪一种更易于阅读?如果我们开始在查询中执行深层嵌套的布尔逻辑,例如查询“收入少于$80,000而且有超过2个子女的人,或者收入少于$60,000而且无子女的人”,此时事情将以指数级地恶化。事实上并不难发现,比起通用目的的查询语言,例如SQL,纯对象优先的查询方法对于能查询什么,有着过于严格的限制。 19使用对象优先的持久化 来保存你的领域模型这启发我们找寻第二条路,即创建某种“查询语言”,使它能够更简明地表达查询,而无需使用过于复杂的代码。Java中所有的对象优先技术最终都回到了这一点:EJB引入了EJBQL,一种为实体bean编写查询方法(finder)的查询语言;JDO引入了JDOQL,它为JDO增强的持久类做相同的工作;而OODBMS回过头来采用OQL(ObjectQueryLanguage)对象查询语言。这些语言相互之间有着微妙的区别,但有着一个明确的共同点:它们都很像SQL,而SQL正是我们起初试图摆脱的。 20使用对象优先的持久化 来保存你的领域模型使用对象优先的方法还有另一个副作用,即不可视的往返访问。例如,当像下面这样使用实体bean时,引发了多少次数据库访问呢?PersonHomeph=(PersonHome)ctx.lookup("java:comp/env/PersonHome");CollectionpersonCollection=ph.findByLastName("Halloway");for(Iteratori=personCollection.iterator();i.hasNext();){Personp=(Person)i.hasNext();System.out.println("Found"+p.getFirstName()+""+p.getLastName());}虽然看起来似乎只访问了一次数据库(读取每个姓Halloway的Person对象,并将其组装到PersonBean池中的实体bean上),但实际上,这正是EJB中的N+1次查询问题,查找方法调用只查找符合查询条件的行的主键,然后用只知道主键的实体bean的存根组装Collection中,并在必要的时候才将数据惰性加载到(lazy-load)实体bean中。 21使用对象优先的持久化 来保存你的领域模型开发一个实体bean的实现,对于查询的结果,它不是简单地取回实体的OID/主键,而是取得保存在实体中的整个数据集,这也是可行的。实质上这就是采用积极加载(eager-load),而不是更为常用的惰性加载。遗憾的是,这引发了一个相反的问题,现在我们抱怨的是取回的数据太多,而不是太少。这里问题的关键是,在对象优先的持久化场景中,数据读取的原子单位是对象本身,从面向对象的观点来看,返回那些比对象小的东西根本没有意义,就像在SQL查询中,如果返回的是比行还小的单位,结果同样没有意义。 22使用对象优先的持久化 来保存你的领域模型我可以写出某些类似于下面的语句:SELECTp.FirstName,p.LastNameFROMPersonpWHEREp.LastName=‘Halloway’;可是,这到底返回了什么?通常而言,一次对象查询的返回值是一个已定义类型的对象(如上例中的Person实例)。可此处我们得到的是什么?对于只返回“部分”对象,并没有普遍都可接受的方式,所以典型的结果是采用像ResultSet或者JavaMap一类的东西(或者是Map实例的一个List)。即使我们理清了这些问题,对象优先的查询还是有其它的问题:对象到对象的引用。在此种情况下,困难并不经常发生,因为我们还没有很好的建模技巧去管理在关系型数据库中的一对多、多对多、或多对一的关系(顺带一提,这并非微不足道)。但是问题仍然存在,当一个对象被读取的时候,问题就来了,是否应该将所有与它相关联的对象都读取出来呢。还有,我们应该如何解决这个问题:两次独立的查询通过该间接引用取回了两个相同的对象? 23使用对象优先的持久化 来保存你的领域模型例如,考虑此场景,我们的系统中有四个Person对象:StuHalloway娶了JoannaHalloway,他们有两个孩子HattieHalloway和HarperHalloway。从任何良好的对象观点来看,这意味着好的Person模型应该有一个配偶属性spouse,它是Person类型的(或者更确切一点,是指向Person的引用),同样还有一个孩子属性children,它是某种集合类型,包含指向Person的引用。现在,如果我们执行前面的查询,以取得第一个对象(让我们假设是Stu),那么通过网络取回Stu对象时是否也应该通过网络去取回Joanna、Hattie和Harper呢?问题又来了,我们此处是采用积极加载数据,还是惰性加载呢,记住,这些对象是被Stu对象实例的域所引用的。当我们从查询结果中取得下一个对象Joanna时,她也引用着Stu,此时在客户端的进程空间中,我们是有一个Stu对象还是两个?如果我们做两次独立的查询,第一次只读取Stu对象,第二次读取Joanna,会发生什么状况呢?对象同一性(identity)的概念很重要,因为在Java中,对象的同一性是通过this指针(对象的位置)来确立的,而在数据库中它是通过主键来表现的,令二者相匹配困难重重,特别是当我们将事务处理置于二者之间时。不过这也并非是不可解决的问题,同一性映射(IdentityMap)就是一个典型的解决方案,但是作为一个对象程序员,你对此必须警惕,以防你采用的对象优先持久化机制没有考虑到此问题。 24使用对象优先的持久化 来保存你的领域模型此处最终的结论是,如果你想采用某种对象优先的持久化方法,可不能仅仅因为“更容易使用”就选择它。在很多情况下,只使用对象,其性能与吸引力就已经足够弥补你别处的损失了,并且还具备很多优点。 25状态处理第1项:节省地使用HttpSession第2项:使用对象优先的持久化来保存你的领域模型第3项:使用关系优先的持久化来显示关系模型的威力第4项:使用过程优先的持久化来创建一个封装层第5项:识别对象-层次结构的阻抗失配(impedancemismatch) 26使用关系优先的持久化来显示关系模型的威力对象和关系相处得并不好。在这两种技术之间取得良好的映射很困难,这种困难甚至有自己的名字:对象与关系的阻抗失配(impedancemismatch)。面向对象语言与关系型数据访问技术(如JDBC)协同工作时的问题,很大一部分只是因为这两种技术的基础在看待世界时所采用的方式非常不同。面向对象语言希望使用对象,它具有属性(域)和行为(方法)。而关系技术将世界看成元组,即被群组为某种逻辑“事物”的数据项集合。 27使用关系优先的持久化来显示关系模型的威力尽管针对对象-关系映射层所固有的问题有大量的论文,且此问题也超出了本书的讨论范围,但是简要地看看其中的一个问题,将有助于我们理解为什么对象-关系映射问题在J2EE系统中如此普遍。请思考下面这个简单的领域对象模型:publicclassPerson{privateStringfirstName;privateStringlastName;privateintage;//...}publicclassEmployeeextendsPerson{privatelongemployeeID;privatefloatmonthlySalary;}这可能是世界上最简单的领域模型了,但我们应该如何将它持久化到一个关系型数据库中呢? 28使用关系优先的持久化来显示关系模型的威力一种方法是创建两个表:PERSON和EMPLOYEE。使用外键(foreign-key)关系将二者的行彼此关联起来。每次我们想得到一个Employee时就需要对两个表做一次连接(join)操作,而每次查询和修改数据时,数据库还需做更多的工作。我们也可以将Person和Employee数据存入单一的EMPLOYEE表中,但如果我们又创建了Student(继承自Person),并想找到所有姓Smith的Person对象时,我们不得不搜索STUDENT和EMPLOYEE两个表,而二者在关系层面上并不相关。如果这种继承层次继续变得更深,则问题几乎呈指数级地混杂起来。更何况,企业应用的开发人员通常没有对数据库模式(schema)的控制权,因为遗留系统或其它J2EE系统已经在使用它了,或者是由其他开发团队负责数据库模式。所以,即使我们想建立一个表的结构,使它优雅地映射到我们的对象模型,我们也不能随心所欲地改变数据库模式的定义。 29使用关系优先的持久化来显示关系模型的威力从另一个完全不同的角度来看,也许有其它更为实际的原因致使我们放弃对象优先的方式。由于这些(以及更多的)原因,我们通常更易于采用关系的观点来看待并操纵数据,而不是将访问关系的操作隐藏在其它某种封装技术之后,例如面向对象、面向过程、或面向层次结构。如果要理解我对于采用关系优先方式的看法,我们需要后退一步,重新看看关系型方式到底是什么。ChrisDate与E.F.Codd一同被看作是关系模型之父,按ChrisDate的说法,“关系系统建立在形式化的基础或理论之上,被称为数据的关系模型”。对数学家而言,关系模型是建立在集合论和谓词逻辑的基础上。然而对我们而言,用最简单的话来说,数据的关系模型就是表而已。访问数据得到的只是表,而操作那些数据的运算符(SQL)也是由表再产生表。 30使用关系优先的持久化来显示关系模型的威力关系型数据库的核心就是表,表就是关系模型中“关系”,就是它使得关系模型如此强大。由此关系数据访问做到了闭包性(closure):一次访问的结果可以作为另一次访问的输入。这使得我们能够写出嵌套的表达式:表达式中的操作数由一般的表达式来代表,而不是直接使用表名。SQL之所以那么强大,很大部分原因就是因为支持嵌套,虽然我们并不想过多地使用嵌套为什么闭包性很重要?对于关系型数据库而言,SQL是一种强大的数据访问语言,而SQL查询得到的结果就是表,这真是值得庆幸,因为这样我们只需要一个API,就能取得任何一次查询的返回结果,无论结果数据是很多,还是很少。我们也没有“比对象小”的问题,因为取得的结果总是表,即使是只有一列的表。我们必须面对的问题是,关系模型经常不能与程序员使用的对象模型相匹配,不过关于这一点我们可说的有很多。还是让我们先来看看怎样令关系访问本身更容易吧。 31使用关系优先的持久化来显示关系模型的威力在你一想到你余下的职业生涯都要跟讨厌的底层的JDBC访问打交道,因而在恐惧中萎缩之前,请先做个深呼吸,采用关系优先的方法并不意味着放弃任何比JDBC层次高的方法。实际上,远远不是这样。Java允许我们可以采用多种更为简单的机制进行关系的存取访问,而不仅仅是原始的JDBC(在许多情况下JDBC仍然是一种可选的方案,尽管它具有相对比较低层的特性)。首先,JDBC可不仅仅只是Connection、Statement、和ResultSet对象。RowSet和Sun公司独特的CachedRowSet,通过将查询行为与获得的结果进行封装,令JDBC更便于使用。 32使用关系优先的持久化来显示关系模型的威力因此,假设你没有JDBCDataSource,也可以很容易地像下面这样进行查询:RowSetrs=newWebRowSet();//OruseanotherRowSetimplementation//ProvideRowSetwithenoughinformationtoobtaina//Connectionrs.setUrl("jdbc:dburl://dbserver/PEOPLE");rs.setUsername("user");rs.setPassword("password");rs.setCommand("SELECTfirst_name,last_nameFROMperson"+"WHERElast_name=?");rs.setString(1,"Halloway");rs.execute();//rsnowholdstheresultsofthequery 33使用关系优先的持久化来显示关系模型的威力大多数对RowSet的调用都可以被隐藏在对象工厂接口之后,所以客户端代码还可以再少一些,如下所示:RowSetrs=MyRowSetFactory.getRowSet();rs.setCommand(...);rs.setString(1,“Halloway”);rs.execute();这与直接使用SQL进行访问几乎一样简单。此处使用的工厂也并不难以想象:publicclassMyRowSetFactory{publicstaticgetRowSet(){RowSetrs=newWebRowSet();rs.setDataSourceName("java:comp/env/jdbc/PEOPLE_DS");returnrs;}} 34使用关系优先的持久化来显示关系模型的威力一种方法是保持数据库面向表的观点,然后通过表数据网关(TableDataGateway)在你与数据访问技术之间构架更多的渠道。其实质上就是将每个表变成一个类,然后用这些类依次作为该表中任意行的访问点。publicclassPersonGateway{privatePersonGateway(){/*Singleton—can'tcreate*/}publicstaticPerson[]findAll(){ArrayListal=newArrayList();RowSetrs=MyRowSetFactory.getRowSet();rs.setCommand("SELECTfirst_name,last_name,age"+"FROMperson");rs.execute();while(rs.next())al.add(newPerson(rs.getString(1),rs.getString(2),rs.getInt(3));return(Person[])al.toArray(newPerson[0]);}publicstaticvoidupdate(Personp){//Andsoon,andsoon,andsoon}} 35使用关系优先的持久化来显示关系模型的威力表数据网关可以扩展出新的变体,叫做查询数据网关(QueryDataGateway),它不是对单个表进行包装,而是对一个查询进行包装。publicclassChildrenGateway{privateChildrenGateway(){}publicstaticPerson[]findKidsForPerson(Personp){ArrayListal=newArrayList();RowSetrs=MyRowSetFactory.getRowSet();rs.setCommand("SELECTfirst_name,last_name,age"+"FROMpersonp,parent_linkpp"+"WHEREp.id=pp.child_id"+"ANDp.last_name=?");rs.setInt(1,p.getPersonID());rs.execute();while(rs.next())al.add(newPerson(rs.getString(1),rs.getString(2),rs.getInt(3));return(Person[])al.toArray(newPerson[0]);}} 36状态处理第1项:节省地使用HttpSession第2项:使用对象优先的持久化来保存你的领域模型第3项:使用关系优先的持久化来显示关系模型的威力第4项:使用过程优先的持久化来创建一个封装层第5项:识别对象-层次结构的阻抗失配(impedancemismatch) 37使用过程优先的持久化来创建一个封装层使用存储过程作为持久化模型最大的优点是,我们能够将存储过程的实现交给负责数据库的同事,将整个数据模型都交给他们设计、实现,以及在必要时进行调整。只要存储过程的定义不变(否则我们就会得到运行期的SQLExecption异常),以及它的实现确实如我们所希望的(存储或取回数据),我们就可以完全忽略底层的数据模型。如果你在关系的设计与执行方面与我一样“优秀”的话(即根本不擅长),这确实是件好事。 38状态处理第1项:节省地使用HttpSession第2项:使用对象优先的持久化来保存你的领域模型第3项:使用关系优先的持久化来显示关系模型的威力第4项:使用过程优先的持久化来创建一个封装层第5项:识别对象-层次结构的阻抗失配(impedancemismatch) 39识别对象-层次结构的阻抗失配XML无所不在,包括在你的持久化方案中。问题在于XML在表示数据时使用的是固有的层次结构的方式,看看XMLInfoset规范,它要求数据是格式良好的(well-formed),即XML文档中的元素必须形成一棵良好的元素树(每个元素都可以有子元素嵌套其中,每个元素都有一个唯一的父结点,唯一的例外是“根”结点,它囊括了整个文档)。这意味着XML善于表现层级结构的数据(所以作为这一项的题目),假设你的对象形成一个整洁的层次结构,则XML是表达该对象最自然的方式(因此自然地假设了XML和对象是手牵着手)。 40识别对象-层次结构的阻抗失配但是当对象没办法形成整洁、自然的树时会发生什么呢?层次模型带来的问题是,在其中查找数据很困难。用户必须亲自在树的元素中搜寻,这迫使用户必须知道“如何查询”以获取数据,而不是仅仅关注“查询什么”感兴趣的数据即可。随着XML的出现(以及不断增长的对“XML数据库”的兴趣,尽管这个词具有二义性),似乎层次数据模型也再次流行起来。虽然对层次数据模型的全面讨论超出了本书的范围,但在此仍有必要弄清两件事:我们何时会在J2EE中使用层次数据模型,而它对Java程序员又有什么影响。 41识别对象-层次结构的阻抗失配将对象映射到XML(今天最常见的层次结构存储模型)并不是件简单的事情,这使得我们想知道是否存在对象-层次结构的阻抗失配(impedancemismatch),换言之,形式自由的对象模型与XMLInfoset严格的层次结构模型是否难以匹配。将对象映射到层次模型的问题,很大程度上与发生在将对象映射到关系模型时发生的问题一样:保证对象的同一性。为了说清楚我的意思,让我们重新看看前面曾经用过的Person对象:publicclassPerson{publicStringfirstName;publicStringlastName;publicintage;publicPerson(Stringfn,Stringln,inta){firstName=fn;lastName=ln;age=a;}} 42识别对象-层次结构的阻抗失配同样地简单而直接,也不难想象该对象的XML表示会是什么样子:RonReynolds30到目前为止一切都好。但是,现在让我们来添加一些东西,它们对面向对象模型而言绝对合理,却将层次结构完全打破了,这就是循环引用:publicclassPerson{publicStringfirstName;publicStringlastName;publicintage;publicPersonspouse;publicPerson(Stringfn,Stringln,inta){firstName=fn;lastName=ln;age=a;}} 43识别对象-层次结构的阻抗失配你应该如何表现下面的对象集?Personron=newPerson("Ron","Reynolds",31);Personlisa=newPerson("Lisa","Reynolds",25);ron.spouse=lisa;lisa.spouse=ron;下面的将ron序列化输出到XML的方法并非不合理,它简单地遍历属性成员,递归处理每个的对象并依次遍历其属性成员,以此类推。然而这样做很快就会出问题,如下所示:RonReynolds31LisaReynolds25RonReynolds31 44识别对象-层次结构的阻抗失配正如你看到的,因为两个对象循环引用对方,此处产生了无限递归。我们可以使用与Java对象序列化所采用的相同的方法来处理这个问题,即对哪些项被序列化了而哪些项没有被序列化都保持跟踪,但是这样的话我们就陷入了更大的问题:即使在给定的XML层次结构中我们保持对对象同一性的跟踪,那么我们怎么在多个层次结构之间作到这一点呢?Stringparam1=ron.toXML();//SerializetoXMLStringparam2=lisa.toXML();//SerializetoXMLsendXMLMessage(""+param1+param2+"");/*Produces:param1= 45识别对象-层次结构的阻抗失配RonReynolds31LisaReynolds25 46识别对象-层次结构的阻抗失配param2=LisaReynolds25RonReynolds25 47状态管理对于寻求真理的人而言,有些准则是必须遵守的,真理并非教条或无知,而是通过推理、调查、检验、与探究得来的。无论其意图有多好,信仰都必须构建在事实而非幻想之上,幻想之上的信仰是最糟糕的虚假希望。——ThomasEdison 48状态处理第1项:使用进程内(in-process)或本地存储以避开网络第2项:不要假设拥有数据或数据库第3项:惰性加载不频繁使用的数据第4项:积极载入频繁使用的数据第5项:批处理SQL的工作以避免往返访问第6项:了解你的JDBC供应商第7项:调整你的SQL语句 49使用进程内或 本地存储以避开网络大多数时候,当J2EE开发人员开始设计或划分项目的架构层次时,数据存储问题已经形成结论了:将采用关系型数据库,运行在数据中心或操作中心某处的机器上。为什么?并没有什么神秘或神奇的答案,仅仅只是为了可访问性。我们希望该系统的任何潜在客户都能够访问到数据。过去,在n层架构流行之前,客户端直接连接到服务器上,为了让所有客户端都能够访问到全部数据,数据需要直接存储在服务器端。那时网络还不普及,使网络更简单的无线访问技术还没出现。将机器连接到网络上是日常主要的杂务,结果是,P2P通讯的基础概念总是在讨论最底层的网络堆栈问题(例如IP协议)。 50使用进程内或 本地存储以避开网络我们渐渐认识到,将所有数据存放在一台中心服务器上有许多优点,即主要的负荷都置于该服务器端,而不是客户端。因为服务器是单独的机器,(所以我们相信)升级或替换它变得很划算,特别是比起替换所有连接到它的客户端来,更是如此。所以不久之后我们就建立起了数据库中心化的思想,我们开始将数据库放在能找到的最重的铁盒子中,用容量最大的RAM和巨大的千兆字节(后来是万亿字节)的驱动器装满其中。这些关于历史的题外话的重点是,中心化的远程数据库服务器的存在只是为了提供单一的数据聚合点,而不是因为数据库“必须”运行在服务器上,那可是花了5、6、或7位数的开销的。我们将数据存放到服务器上,因为(a)它是个很方便的地方;(b)易于将所有为客户端进行的处理放在一个集中的地方,且无需向客户端推出更新(零部署);(c)这是一种令数据与处理靠近的方法。 51使用进程内或 本地存储以避开网络通过线路传送所有数据并不便宜,而且也有其内在固有的问题。在可扩展性与执行性能两方面都有所牺牲(因为每有一个byte的带宽用于在网络上传送数据时,就有一个byte的带宽不能用于其它目的),而重组来回传递的数据所消耗的时间也并非微不足道。考虑到我们将数据放在中心的数据库中是为了使其它客户也可以使用它们,而这样的数据传送开销又不低,所以除非必须,否则不要将数据放在远程数据库中。也就是说,不要将任何数据放到远程数据库中,除非确实需要与其它客户端共享它们。 52使用进程内或 本地存储以避开网络在这种情况下,在与客户端应用相同的进程中运行关系型数据库(或其它数据存储技术,这里我们可以考虑使用对象数据库,甚至选择XML数据库),不仅可以令网络往返访问回合最少,而且可以将数据完全保存在本地机器上。虽然在我们的servlet容器内运行Oracle恐怕一段时间内还不可行,但是运行一个全Java的RDBMS实现却并非遥不可及。例如,Cloudscape就提供了这项功能,以及PointBase和HSQLDB(在Sourceforge上的一个开放源代码的实现),实质上是通过伪装成JDBC驱动程序而成为数据库的。或者,如果你喜欢基于对象的方法,另一个开源项目Prevayler,它以传统的对象优先持久化方式存储任意的Java对象。如果你希望看到层次结构形式的数据,Xindice是ApacheGroup下面的一个开源的XML数据库。 53使用进程内或 本地存储以避开网络RowSet本身就是一个简单的进程内数据存储技术。因为RowSet是完全与数据库断开连接的,我们可以用查询的结果创建一个RowSet,并在客户进程的整个生命期内都保持着此RowSet,而无需担心对数据库可扩展性的影响。因为RowSet是可序列化的,所以我们可以将它存进任何OutputStream而不会对其造成改动,例如一个文件或一个Preferences节点。事实上,如果RowSet是围绕着配置数据而被包装的,那么与以某些方式将其存储在本地文件中相比,将它保存在Preferences节点中更有意义。它不会扩展到数千行,也不强制要求关系的完整性,但是如果你需要存储很多数据,或是将关系约束施加于本地数据,那么你就应该使用“真正的”数据库,例如HSQLDB或支持“嵌入”到Java进程中的商业产品。 54使用进程内或 本地存储以避开网络然而这个故事还有另一面,一个显而易见的事实,远程数据库需要能够触及它的网络。尽管这似乎没有必要说出来,但是还是先思考一下这个问题,然后我们来看看大型制造业公司中普遍存在的连接到清单数据库的销售-订单应用。我们将此应用安装在销售员的便携式电脑上,然后此销售员去拜访一个客户公司的副总裁(VP)。在饮过红酒,吃过午餐,以及打完漂亮的私人高尔夫球场的第18洞之后,副总裁终于准备签署这个百万美元的订单了。销售员启动便携电脑,开始签单,令他恐惧的是发生了错误:“databasenotfound.(数据库未找到)”坐在高尔夫俱乐部豪华的餐厅,我们勇敢的销售员忽然面色苍白,转向副总裁,说到:“嗯,呃,我们可以回您的办公室吗,让我借用一下您们的网络?” 55使用进程内或 本地存储以避开网络假设副总裁确实让我们无畏的销售员使用了他的网络,通过VPN连接到销售员屋里的网络;假设销售员知道如何在他的便携电脑中设置VPN;假设本公司的IT员工打开了公司网络的VPN;无论VPN连接的速度怎样,假设应用确实在工作,此销售员的信誉仍然受到了沉重的打击。而在这之前,该副总裁有各种理由可以拒绝销售员使用其网络,毕竟,让外人的机器进入防火墙之后的网络是有安全风险的。而本公司的IT员工也有各种理由将数据库尽可能地与“外面的世界”隔离开,无论是VPN或不是VPN。当销售员回到公司开始保存订单的时候(潦草地记录在豪华餐厅的餐巾上),对方的副总裁可能已经改变了主意,或者销售员忘记在餐巾上记下某些重要的细节了等等。(所以销售员被训练为一旦顾客同意就要尽量快地下单,这是有道理的。) 56使用进程内或 本地存储以避开网络在某些圈子里,这称为旅行推销员问题(travelingsalesmanproblem)(不要与最便宜的旅行路途cheapest-way-to-travel问题相混淆了,那通常是在人工智能的书中讨论的问题)。此问题的核心很简单:你不能假设网络总是可用的。很多情况下,你想设计一个应用,并确保它可以在无法访问网络的时候以非连接模式运行。这与当路由器或hub不通的时候,提供格式良好,易于处理的SQLException是不同的。这是将应用设计为在没有网络的地方,也可以使用独立的便携电脑处理销售员的订单。当为了这个目标而进行设计时,问问你自己,如果用户在37,000英尺的空中,他应该怎样使用该应用把一些小工具卖给飞机上刚认识的某个人。 57使用进程内或 本地存储以避开网络要适应此场景,最简单的方法是运行一个本地的数据库,将中心数据库完整的数据转储在客户端机器上(顺便说一句,这可能是一个肥客户端应用,因为没有连接到HTTP服务器的网络连接)。当机器通过网络连接到远程数据库时,就通过中心数据库使用最新最完整的数据更新本地机器。你要知道,虽然没有必要是完整的数据库模式,但仍要保证在没有远程数据库的情况下有足够的数据保证运作。例如,在假想的销售应用中,只需要订单的清单、详细信息、和价格可能就足够了,而完整的销售、历史、以及运送信息的数据副本可能就不需要在本地存储。然后,当下了新订单之后,应用可以更新运行在同一台机器上的本地表。 58使用进程内或 本地存储以避开网络有些人无疑会很憎恶整个建议。“不连接到远程数据库?不可想象!我们如何避免数据完整性错误?那正是起初我们将数据库中心化的原因!说到底,如果只剩下100个小工具,而Susan和Steve将这最后100个工具都通过他们的便携系统卖给了不同的客户,那么当我们将他们俩的数据与中心数据库同步的时候,就肯定会遇到并发问题。这是每个系统架构师都知道的!”先做个深呼吸。然后问问自己,你会如何处理这个场景,因为无论它是发生在销售员下订单的时候,还是在订单进入数据库的时候,同样的问题总会发生。 59使用进程内或 本地存储以避开网络一般而言,在连接到服务器数据库的系统中,库存清单的检查发生在销售员下订单的时候,如果对小工具的需求数量库存不足,我们就需要使用某种红色标记去标注此订单,要么就不签此订单,要么必须通知销售员,令他知晓此刻库存不足。在此应用可连接的版本中,这个红色标记一般是指消息对话框或者警告窗口,这与稍候再作有何不同呢,即销售员与中心数据库同步数据的时候才通知他?事实上,后一种方式可能会更好,因为警告窗口很可能立刻就毁了这场生意。如果对方的副总裁看到了这条消息,她可能会重新考虑订单的事情:“嗯,我打赌你的竞争者有现货,而我们立刻就需要这些工具,”即使“立刻”指的是“我一周只需50个。”于此相反,如果红色标记回到办公室才出现的话,在打电话给客户告诉他新的消息之前,销售员完全可以做些研究(如果要想坐在对方副总裁的办公室里做这些研究,那么即便不是不可能,也将会很困难)。(“好的,现在我们的仓库里只有50个工具,但是Dayton那里还有50个,而我会让他们快递给你,不另收费。”) 60使用进程内或 本地存储以避开网络远程数据库对于企业系统有明显的优势,毕竟,我们对企业系统的定义,部分是因为它管理着对必须共享的资源的访问,所以才推出了中心化,但是对于某些类型的数据或特定的应用而言,远程数据库不是必然的最佳仓库(例如每个用户的设置、配置选择、或非共享数据),特别是那些需要在无网络连接方式下运行的应用。 61状态处理第1项:使用进程内(in-process)或本地存储以避开网络第2项:不要假设拥有数据或数据库第3项:惰性加载不频繁使用的数据第4项:积极载入频繁使用的数据第5项:批处理SQL的工作以避免往返访问第6项:了解你的JDBC供应商第7项:调整你的SQL语句 62不要假设拥有数据或数据库请记住,关于企业级软件我们有一个基本的假设,即企业级软件系统的部分或全部资源都是共享的。事实上,企业系统的核心——数据,是最频繁共享的资源。大多数时候,我们假设共享的范围是发生在企业系统各种不同的用户中。然而,它不止如此。许多企业数据库将其它企业系统也视作其用户的一部分,而且很遗憾,那些“用户”比普通用户有更加严格的需求。 63不要假设拥有数据或数据库如果多系统或代理共享一个资源,则这些系统或代理中的任何一个都不可能随心所欲地改变该资源的格式或模式,而不会引起其它系统的连锁反应。对模式的改动、过度的数据库锁操作,以及数据库实例的重定位,这些改变似乎可以只局限在你的代码中,但是必将引发其它系统的问题。事实上,正是这个原因令遗留系统一直存活着。对给定公司的资源或仓库(特别是大公司),追踪其每个客户端极为困难,远比弄清一个单一系统的一个给定用户的所有数据依赖关系要困难。因此,与其使用某种新系统来替换遗留系统,还不如为其创建适配器、翻译器和代理,然后将遗留系统保持在原处显得简单。 64不要假设拥有数据或数据库对开发者而言,它暗示的东西即使没有令你心寒也足够让你清醒的:你不拥有你的数据库,也不拥有其中的数据。即使在项目中你完全是从头开始建造数据库,即使该项目似乎只是塞在整个业务的一个小角落里,没有人对它有兴趣,即便如此你也不拥有此数据库。只是时间问题,总会有公司中的某个人听到了关于你的小系统,并希望获得对那些数据的访问,所以你开始设置后台数据源,直接连接等等。或者更糟的,你团队中的某人为他们建立了连接而没有告诉你。不久,你改动了某个schema,然后你从没听说过也没见过的人,就咆哮着给你打电话,要求知道你为什么破坏了他们的应用。 65不要假设拥有数据或数据库现在结论很清楚了:一旦完成了你的数据库模式的设计,与你的代码相比,它就处于了危险得多的被锁定状态之中。这也是为什么用户与数据库之间的封装层变得如此重要的原因,它强制客户(任何客户,包括你自己的代码)必须通过某种封装的障碍才能获得对数据的访问,例如存储过程层或Web服务端点层(endpointlayer),实质上,你是在为未来以及如下事实作计划:当你修改底层的数据库模式以及数据描述的定义时,其他人仍将需要访问这些数据。 66不要假设拥有数据或数据库这也意味着数据库模式不应该为J2EE应用进行优化:建立自己的模式时要当心,别将它与你的对象模型绑得太紧了,以二进制大型对象BLOB(BinaryLargeObject)存储序列化的Java对象时更要极为当心,因为那样做意味着此数据不能在SQL查询中使用(除非你的数据库支持,在那种情况下的查询将执行得非常慢)。 67不要假设拥有数据或数据库事实上,因为你的数据库可能很快就会变成一个共享的数据库,并且因为J2EE并不是肯定能统治整个世界(.NET和PHP程序员对认为它能够做到的人可能有话要说),所以你最好尽量将约束施加在数据库上,而不要依赖于J2EE代码来施加限制。举个例子,对于一个要存储的字符串,在存进数据库之前检查它的长度是否少于40个字符,可以直接依赖于数据库对这张表的完整性约束来检查字符串的长度,而不是在J2EE中作检查,并把列的长度设得比它大。既然数据库总会进行尺寸约束的检查,为什么要做两次呢?同样地,通过使用外键约束来确保约束的双方实际上都存在于数据库内,来直接在数据库内为该数据库中的表/实体之间的任何类型的关系进行建模。 68不要假设拥有数据或数据库乍一看,这似乎很容易,继续下去并在你的对象模型或领域逻辑中开始直接按此逻辑进行编码,Java可能是你的首选语言,毕竟这是人类的自然倾向,希望使用我们用得最舒服的工具来解决问题。应该抵制这种倾向。数据库提供了大量功能,但在你的J2EE代码中并不容易(或普遍)使用。 69状态处理第1项:使用进程内(in-process)或本地存储以避开网络第2项:不要假设拥有数据或数据库第3项:惰性加载不频繁使用的数据第4项:积极载入频繁使用的数据第5项:批处理SQL的工作以避免往返访问第6项:了解你的JDBC供应商第7项:调整你的SQL语句 70惰性加载不频繁使用的数据考虑到在网络上对远程数据库一次往返访问的开销,我们其实并不希望通过网络读取所有的数据,除非我们需要它。这看起来是个很简单的思想,但它确实引人注目,在系统中经常使用,它将数据库对程序员隐藏起来,例如,幼稚的对象-关系映射机制经常以一对一的形式将对象映射为表,于是对于从数据库取回某个特定对象的请求,就意味着将一个特定行的每一列全部取出构造出要求的对象。 71惰性加载不频繁使用的数据很多情况下,这显然是个糟糕的方式:考虑典型的深入检查(drill-down)场景,我希望列出数据项的清单,用户可以从中选择一个(或多个)进行更完整的检查。为了用户对选择有一个总体的映像必须列出大量的数据项,例如有10,000人符合初始查询条件,取回其中的每一个人意味着10,000xN个字节的数据必须通过网络传输,其中N是表中单个行的完整尺寸。如果这些行有外键关系,它们也必须作为对象的一部分而被取回(Person也许有0..*个地址实例与之相连),那么最终的数量就很容易膨胀到无法管理的程度。由于这些原因,许多对象-关系映射机制选择不为特定的行读取所有的数据,那是标准的读取操作的一部分,它们选择惰性加载数据,宁愿多做几次数据库的往返访问,以保持总的需求下降到可管理的级别。事实上,这种场景很常见,许多地方都对它引证,最引人注意的是惰性加载模式(LazyLoadPattern)。 72惰性加载不频繁使用的数据请注意:关键在于惰性加载的是不频繁使用的数据,而不是还没有被使用到的数据。在这里,EJB实体bean部分似乎要遇到灾难了,就像典型的N+1个查询问题。很多EJB实体bean的实现就决定了,对于不是立刻就需要使用的数据,我们希望避免全部都取回,通过基于Home的查找方法(findermethod)取得一个实体bean时,实际从数据存储(关系型数据库)中取回的只是行的主键,它们被存储在EJB容器里的实体bean中。然后,当客户端访问实体bean的属性方法(get或set)时,就会发出单独的查询以取得或更新特定bean的特定列的值。 73惰性加载不频繁使用的数据危险之处在于,如果你带着实体bean的帽子,却在客户端代码中写出如下的操作:PersonHomepersonHome=getHomeFromSomewhere();Personperson=personHome.findByPrimaryKey(1234);StringfirstName=person.getFirstName();StringlastName=person.getLastName();intage=person.getAge(); 74惰性加载不频繁使用的数据使用这样的代码,你其实是提出了如下这些要求:SELECTprimary_keyFROMPersonWHEREprimary_key=1234;SELECTfirst_nameFROMPersonWHEREprimary_key=1234;SELECTlast_nameFROMPersonWHEREprimary_key=1234;SELECTageFROMPersonWHEREprimary_key=1234; 75惰性加载不频繁使用的数据这除了显著的浪费CPU周期(解析每次查询,规划查询计划,将返回值编组成关系元组等等),同样还令cache淹没于不相关的数据中,同时你也承担着巨大的风险,即在bean方法调用之间数据不能改变。请记住,每次调用代表着一个独立的EJB驱动的事务,然而你现在却打开了这种可能性,在方法调用之间不同客户能够改变相同的行。 76惰性加载不频繁使用的数据然而,这个问题与惰性加载并没有必然联系。关键是要了解什么数据需要惰性加载。在容器管理的实体bean实现的例子中,与SQL相关的所有细节对于EJB开发者来说都被隐藏了,EJB容器没有办法知道哪些数据元素比其它的元素更可能被使用,所以它采取了更严厉的假设,这些数据都不一定会被使用。现在回想起来,这可能是个糟糕的决定,仅仅比留给容器实现者可选择的另一种方法稍好一点,那就是假设所有的列都会被使用。对于实体bean实现者最理想的环境是,允许你提供某种关于使用的提示,作为初始化bean状态的一部分,指出什么字段需要读取,而哪些字段最好是惰性加载。但这种事情是依赖于供应商的,会在相当大的程度上减小你的可移植性 77惰性加载不频繁使用的数据如果你在编写自己的关系访问代码,例如编写BMP实体bean或者就是直接写JDBC或SQL/J代码,那么对于读取什么数据的决定权就在落在了你的身上。在这里,就像任何SQL访问一样,你应该精确地指明你想要的数据:一点也不多,一点也不少。这意味着需要避免如下这种SQL查询:SELECT*FROMPersonWHERE...虽然在你写这个查询的时候,它似乎没那么重要,但当代码执行的时候,你会取回Person表中的每一列。如果Person表一开始的定义正好与你想要的数据列一致,那么使用通配符*来取得所有列似乎更方便更简单,但是请记住,你并不拥有数据库。在很多情况下,数据库的定义可能会改变,如果你在代码中使用通配符的话,就会出现问题。 78惰性加载不频繁使用的数据有几种方法可以控制SQL查询读取多少数据。一种方法是在JDBC级别通过设置setFetchSize,将需要读取的项的数量设为等于你呈现给用户的显示窗:如果你只显示读取的前10条数据,就在通过next读取第一个条之前,对ResultSet使用setFetchSize(10)。使用这种方法,你可以确定只读取指定数量的行,而且你知道你是通过一次往返访问以获取这些你需要使用的数据的。另一种方法是,对于驱动/数据库的组合,你可以让驱动程序设置它认为最佳的获取数量,通过调用setMaxRows语句来限定返回行的绝对数量;任何超过了传给setMaxRows的数量的多余行都会被悄悄地丢掉,不会被传送回来。 79惰性加载不频繁使用的数据有些数据库还支持另一种方法,使用TOP限定符作为查询的一部分,例如在SELECTTOP5first_name,last_nameFROMPersonWHERE...中,只有符合该谓词表达式的前5个行才会从数据库中返回。虽然这是非标准的扩展,但它仍然很有用,而且许多数据库供应商都支持。TOP是SQLServer的语法;其它数据库使用FIRST(Informix)、LIMIT(MySQL)、或SAMPLE(Oracle),原理都是相似的。再说一次,该思想很简单:只取回当前需要的数据,避免取回不会用到的过多数量的数据。然而请当心,当你开始考虑惰性加载的时候,你就站在滑梯的斜坡上了,如果你不当心的话,很容易就会发现自己陷入了过多地通过网络惰性加载数据元素的境地,Fowler称之为“波纹式载入(ripple-loading)”问题,或者更有名的N+1次查询问题。在那种情形下,为避免网络拥塞有时最好是采用积极加载数据。 80状态处理第1项:使用进程内(in-process)或本地存储以避开网络第2项:不要假设拥有数据或数据库第3项:惰性加载不频繁使用的数据第4项:积极载入频繁使用的数据第5项:批处理SQL的工作以避免往返访问第6项:了解你的JDBC供应商第7项:调整你的SQL语句 81积极载入频繁使用的数据积极加载与惰性加载正好相反:不再是通过增加的网络往返访问,在后来需要的时候才取回数据,因为考虑到我们终将会需要这些数据,所以我们决定现在就通过网络读取额外的数据,然后将它保存在本地数据库中。此思想隐含了一些条件。首先,对于最终得到的结果,即实际访问的额外数据,必须证明它带来的开销的合理性,即对通过网络从服务器传送到客户端的数据进行编组的开销。 82积极载入频繁使用的数据其次,我们也隐含假设了,在网络上移动数据的实际开销并不过分。例如,如果我们正在讨论取得一个巨大的BLOB列,请确定该列确实是必须的,因为大多数数据库对于获取与传送BLOB数据并不做特别的优化。关于积极加载数据,你也许听到的不多,因为对大多数开发人员而言,它让人回忆起对早期的对象数据库的恐怖印象,以及当某些“根”对象被请求时,他们对积极加载对象的癖好。例如,如果一个Company包含0个或多个Department对象,每个Department包含0个或多个Employee对象,而每个Employee对象又包含0个或多个PerformanceReview对象……好了,想象一下,为得到数据库中当前存储的Company对象的列表,有多少对象会被读取。如果我们感兴趣的只是Company名字的列表,将所有的Department、Employee、和PerformanceReview对象都通过网络进行传送,显然只是对时间和精力的巨大浪费。 83积极载入频繁使用的数据在获取对象状态时,底层并不知道什么数据需要被取回。所以,在采用积极加载的系统例子中,我们偏向于更少的网络往返,并将数据都取回来。除了显然的带宽损耗,积极加载还是有许多惰性加载不具备的优点的。 84积极载入频繁使用的数据首先,积极加载你所需要的完整的数据集将非常有助于并发的情形。请记住,在惰性加载的场景中,客户端有可能会看到语义上被损坏的数据,因为每次实体bean访问都会导致在独立的事务下的一次单独的SQL查询。这意味着,在事务与事务之间,在我们不知道的情况下,其它客户端就能够对行作出修改,这会导致所有已经取得的数据都变得无效。当积极加载数据时,作为一个单独的事务的一部分,我们取得完整的行的副本,这就排除了对数据库多次访问的需要,也消除了会破坏数据语义的可能性。因为不再需要会返回被改变了的数据的“第二次查询”。本质上,积极加载采用传值的方式,与惰性加载的传引用方法正相反。 85积极载入频繁使用的数据这对于锁窗口也有一些非常有用的隐含意义。在会话bean的整个事务期间,如果通过EJB进行访问,或是通过JTATransaction或JDBCConnection进行显式的事务维护,那么容器不必持有对应行(或页、或表,这依赖于你的数据库的缺省的锁机制)上的锁。缩短锁窗口意味着更低的竞争,也即更好的可扩展性。当然,积极加载所有数据也意味着,对于其它客户修改数据打开了一个机会的窗口,因为你不再持有其上的事务锁,但是这可以通过各种方法解决,包括显式的事务锁、悲观并发模型(pessimisticconcurrencymodels)、和乐观并发模型(optimisticconcurrencymodels)。 86积极载入频繁使用的数据其次,正如已经推导出的,对于给定的数据集,如果那些数据可以安全地假设为是确实需要的,那么积极加载数据能够大大降低花在通过网络获取数据上的总时间。积极加载并不仅仅是为了获取一行中的所有列。与惰性加载一样,你可以将积极加载原则应用于超过单独的行的范围。例如,我们可以将积极加载原则应用于表和数据依赖,这样的话,如果一个用户请求了一个Person对象,我们将加载与之相关联的所有Address,PerformanceReview,以及其它与此Person相关联的对象,尽管这可能意味着以某种批处理形式的单次往返访问执行了多个查询。 87积极载入频繁使用的数据最后,积极加载数据与惰性加载数据一样,都是可行且有益的优化方式,尽管其名声并不好。实际上,很多情况下它是一种比惰性加载的情形更可接受的折衷方案,比较起我们当前生存环境中的昂贵而缓慢的网络访问,它对内存的额外需求相对来说开销更低。和平常一样,在做惰性加载或积极加载的优化之前,应该先进行性能测量,不过如果积极加载能够为你节省很多网络访问,那么第一次访问时占用的额外带宽,以及积极加载的数据所占用的额外内存,通常都是值得的。 88状态处理第1项:使用进程内(in-process)或本地存储以避开网络第2项:不要假设拥有数据或数据库第3项:惰性加载不频繁使用的数据第4项:积极载入频繁使用的数据第5项:批处理SQL的工作以避免往返访问第6项:了解你的JDBC供应商第7项:调整你的SQL语句 89批处理SQL的工作 以避免往返访问考虑到在网络上移动的代价,我们必须将通过网络去连接其它机器的次数最小化。遗憾的是,JDBC驱动缺省的行为模式是有一个语句,就进行一种往返访问:每次运行execute(或者是它的某种变体,如executeUpdate或executeQuery),驱动就会编组请求并将其发送给数据库,在数据库中解析、执行并返回。在每一次往返访问中,这个冗长的过程都要消耗大量的CPU资源。如果批处理这些语句,在一次往返传递中就将多个SQL语句传递给数据库,那么就可以避免一些开销了。请铭记于心,尽管这主要是一个状态管理问题,但是通过减少使用网络的次数,以提高系统性能,它也同样可以运用于事务处理。在某种情况下,我们很可能持有开启的事务锁,因此我们应该使得锁打开的时间最小化。 90批处理SQL的工作 以避免往返访问然而还有其它的原因,有时,多个语句需要在一个群组里执行以获得合乎逻辑的结果。通常情况下,我们希望在一个事务里完成这些语句(因此,这需要你对正在使用的Connection设定setAutoCommit(false)),但是这并不意味着驱动程序会将它们在一个往返调用中执行。驱动程序完全可能分别地传送每一条语句,并在此期间保持事务锁为打开。因此,既然这些都发生在一个单一的事务中,那么将它们作为一个群组来执行就要好得多。毕竟,事务本身就是一个要么全部都做,要么全部都不做的过程。 91状态处理第1项:使用进程内(in-process)或本地存储以避开网络第2项:不要假设拥有数据或数据库第3项:惰性加载不频繁使用的数据第4项:积极载入频繁使用的数据第5项:批处理SQL的工作以避免往返访问第6项:了解你的JDBC供应商第7项:调整你的SQL语句 92了解你的JDBC供应商尽管经过了这么多年的修改,JDBC规范仍然没有刻意去制订大量的细节。例如,当新建一个ResultSet时,它通常只持有前N行,N是JDBC供应商认为比较合适的某个值。对于一些主要的供应商,N的值就是1,我可不是在开玩笑。幸运的是,这个值不仅可以看到,而且能够通过getFetchSize和setFetchSize这两个ResultSet的API进行配置。但是问题仍然存在,你知道它的缺省值是什么吗?如何判断它是否合适呢?一定要检查setFetchSize的布尔返回值,这样才能确保你所请求的抓取尺寸没有超出驱动程序或者数据库的支持范围。JDBC的许多特性都是不可用的,它们依赖于驱动程序的能力,同时这也会对你如何利用JDBC驱动进行编程有着很大影响。例如,当从ResultSet中获取数据时,可以通过ResultSet中的顺序位置或者通过列名来获取数据,这两种方法看起来完全等价。除了程序员个人的编程习惯之外,这两种方法难道真的就没有任何不同吗? 93了解你的JDBC供应商事实表明两者之间确实存在不同,这完全依赖于你所使用的JDBC驱动是怎么实现的。最早发布的JDBC规范仅要求JDBC驱动提供对消防水龙式的游标(firehosecursor)的支持。也就是说,一旦某一行或一列通过游标被取出,就无法再次获得了。这意味着当你从ResultSet中读取数据时,如果你没有按照在SQL语句中指定的顺序去读取某一列数据时,你就会跳过前面所有列的数据,而这些数据将永远不能再被读取了:ResultSetrs=stmt.executeQuery(“SELECTid,first_name,last_name“+“FROMperson”);while(rs.next()){StringfirstName=rs.getString(“first_name”);StringlastName=rs.getString(“last_name”);intid=rs.getInt(“id”);//ERROR!} 94了解你的JDBC供应商现在,如果你关注怎样写出可移植的J2EE代码,你必须确保这种特殊情况不再出现,以彻底解决这种1.0版驱动程序的可能性。因此,你必须严格按其顺序来获取数据,最简单的方法就是使用序号的形式来调用方法:ResultSetrs=Stmt.executeQuery(“SELECTid,first_name,last_name“+“FROMperson”);while(rs.next()){intage=rs.getInt(1);StringfirstName=rs.getString(2);StringlastName=rs.getString(3);} 95了解你的JDBC供应商这样的代码虽然不是很难书写,但是它们确实存在一些缺点。第一,如果一个程序员曾经修改过SQL语句,循环中对应的数据检索的代码也要更新以反映列顺序的更改,否则SQL语句又将抛出异常。(顺便讲一下,用序号的形式调用方法确实比用列名的形式速度要快一些,但是应该避免仅仅因为性能原因就将代码改为使用序号的调用形式,因为它的优化能力不值一提,尤其是,那就失去了使用列名带来的文本固有的提示信息。如果你确实希望优化代码,还有许多其它优化方法也许能带来更好的结果,所以不要采用这种方式了。) 96了解你的JDBC供应商我们讲这个并不仅仅是为了弄清楚列的顺序(顺便提一下,关于SQL语句中使用*替代所有列名曾经存在过争论)。你需要知道你所使用的驱动是否支持批处理语句和隔离级别,它可能支持哪个数量级的功能(scalarfunctions)等等。许多信息能够通过DatabaseMetaData类获得,而DatabaseMetaData的实例可以通过Connection取得;又或者可以通过ResultSetMetaData类获得稍少一些的信息,可以通过给定的ResultSet得到ResultSetMetaData的实例。 97了解你的JDBC供应商JDBC程序员感兴趣的另一个地方是线程安全:多线程调用驱动是否安全?在多线程中访问Connection、Statement、或ResultSet对象时,需不需要控制同步?例如,JDBC-ODBC驱动不是线程安全的,这样就需要由你确保,在同一时刻驱动不会被多于一个线程进行访问,否则在Java代码中调用此ODBC驱动就有可能导致非常糟糕的事情发生。(这里又一次强调,在产品代码中使用JDBC-ODBC驱动绝对不是一个好主意。)在许多JDBC和J2EE的书中,为了最佳的执行性能与可扩展性,它们提出了很多建议,其中一个思想是PreparedStatement比Statement好:应该使用PreparedStatement取代常规的Statement。道理很简单:因为每次使用数据库都需要一定量的工作(解析SQL,创建查询计划,进行优化等等),如果能够假设将会发生同样的调用的话,最好是能够分摊这种开销,于是可以将上一次的这些预备工作保持到下一次。一个PreparedStatement只“准备”一次SQL调用(除去你想要传进参数,因为还不知道这些参数是什么呢,所以无法准备那些参数),这样在后续的调用中你就能够获得更好的性能。 98了解你的JDBC供应商从安全的角度来看,使用PreparedStatement是必须的但是从性能和(或)可扩展性的角度来看就不一定了。例如,JDBC规范指出,当一个Connection被关闭时,它所对应的Statement对象也随之关闭。这样当你将从连接池中取出的Connection返回给连接池的时候,从该Connection中获得的PreparedStatement是否也随之关闭了呢?又或者底层的物理连接仍然打开,这样使得PreparedStatement依然处于活动状态准备接受其它的请求呢?从PreparedStatement继承而来的CallableStatement是否也遵从同样的约定呢?遗憾的是,当我在挥手并声明“很明显,实际的驱动程序会在连接池中保存Statement,只有傻子和笨蛋才不这样做”的时候,现实的情况是,有时你不得不使用傻子和笨蛋编写的软件,只有通过测试才能确信你的数据库或者数据库驱动是否属于这一类。 99了解你的JDBC供应商顺便说一下,某些数据库供应商(最显著的是PostgreSQL)在一个事务结束的时候(COMMIT)会将PreparedStatement变成“无准备的”。当讨论这个问题的时候,关于PreparedStatement的讨论向左来了个急转弯。也就是说,下面的代码不会按我们预期的工作,尽管JDBC规范坚持它应该按我们预期的工作:PreparedStatementpstmt=connection.prepareStatement(“...”);booleanautocommit=connection.getAutoCommit();//Let’sassumeit’struepstmt.executeUpdate();//COMMIThappensautomatically,sinceautocommit==truepstmt.executeUpdate();//FAILS!PreparedStatementpstmtisnolongerprepared 100了解你的JDBC供应商如果这还不能使你对“你有必要去了解你所使用的JDBC驱动程序到底是怎么工作的”这一思想信服,那么我只能祝你好运了。要知道你的驱动程序到底做了什么,最简单的方式就是使用数据库执行查询的性能测量工具,并且看一看在JDBC查询执行的过程中到底发生了什么。 101状态处理第1项:使用进程内(in-process)或本地存储以避开网络第2项:不要假设拥有数据或数据库第3项:惰性加载不频繁使用的数据第4项:积极载入频繁使用的数据第5项:批处理SQL的工作以避免往返访问第6项:了解你的JDBC供应商第7项:调整你的SQL语句 102调整你的SQL语句小测验时间:什么时候两个逻辑上等价的SQL语句会不等价?让我们来看看下面的两个SQL语句:SELECT*FROMTableWHEREcolumn1=’A’ANDcolumn2=’B’SELECT*FROMTableWHEREcolumn2=’B’ANDcolumn1=’A’你认为哪一个执行速度会快些?答案是:你不知道,大多数数据库的优化器也不知道。但是,如果你恰好知道对于特定数据库中的特定用户或者特定查询,column2的值为B的数据要少得多,这样的话第二个语句将会比第一个执行得快,尽管逻辑上它们是等价的(顺便提一下,对于Oracle数据库,当使用基于开销的优化器时,一定不要这样做,它肯定会出错)。 103调整你的SQL语句或者我们再看另一个例子,下面这两个语句将会怎样?SELECT*FROMTableWHEREcolumn1=5ANDNOT(column3=7ORcolumn1=column2)SELECT*FROMTableWHEREcolumn1=5ANDcolumn3<>7ANDcolumn2<>5答案是:对于前面所提到的八个数据库中的五个来说,第二个语句执行得会快些。同样的,在逻辑上它们是完全一样的语句,所以它们的执行结果将完全相同。只是数据库优化器对待第二条语句的方式不同于第一条,所以获得了更佳的响应时间。 104调整你的SQL语句当使用关系型数据库时,调整SQL语句仍然是你能够做到的最佳优化。不管JDO和实体bean的供应商在过去五年的努力中得到了什么,我们仍然处于SQL很重要的时代。是的,供应商确实使他们的对象优先持久化模型进步了很多,不像最初那么糟糕。但是,如果数据库本身不能区分这两种语句的不同之处,那么你的对象-关系层也就不可能区分得出来。这就是为什么对于一个对象-关系系统而言,它必须要向你提供某种类型的挂钩点是如此重要,通过它你才能够向其中传入原始的SQL语句,并对那些最经常执行的查询进行调整和优化。 105调整你的SQL语句顺便说一下,你应该铭记于心,对某些数据库而言以下两个语句是完全不同的,因此需要重新解析、重新计划、并重新执行:SELECTcolumn1*4FROMTable1WHERECOLUMN1=COLUMN2+7SELECTColumn1*4FROMTable1WHEREcolumn1=(column2+7)这就是说,即使这两个语句的行为完全是一模一样,但由于它们所使用的大小写与空白符不同,数据库将它们当作独立而无关的语句分别处理。当使用工具产生你的查询语句时,我们希望它们能够使用统一风格,但是你能确信这一点吗? 106调整你的SQL语句在许多情况下,你必须知道数据库中SQL语句实际是如何执行的,然后才去考虑如何调整它。从你的立场上好好看看将要执行的SQL语句,对SQL语句进行优化,往往比你在别的事情上花费时间精力能够获得更大的效益。即使你的CMP实体bean层有某种适应SQL生成的模式,你还是需要了解它,并确保你的数据库管理员所进行那部分工作不会在其上不能运转。 EndTobecontinued107

当前文档最多预览五页,下载文档查看全文

此文档下载收益归作者所有

当前文档最多预览五页,下载文档查看全文
温馨提示:
1. 部分包含数学公式或PPT动画的文件,查看预览时可能会显示错乱或异常,文件下载后无此问题,请放心下载。
2. 本文档由用户上传,版权归属用户,天天文库负责整理代发布。如果您对本文档版权有争议请及时联系客服。
3. 下载前请仔细阅读文档内容,确认文档内容符合您的需求后进行下载,若出现内容与标题不符可向本站投诉处理。
4. 下载文档时可能由于网络波动等原因无法下载或下载错误,付费完成后未能成功下载的用户请联系客服处理。
大家都在看
近期热门
关闭