加速你的Hibernate引擎(上)
只需要一张表,一条多态查询生成的SQL大概是这样的:
select id, payment_type, amount, currency, rtn, credit_card_type from payment
针对具体子类(例如CashPayment)的查询生成的SQL是这样的:
select id, amount, currency from payment where payment_type=’CASH’
这样做的优点包括只有一张表、查询简单以及容易与其他表进行关联。第二个查询中不需要包含其他子类中的属性。所有这些特性让该策略的性能调优要比其他策略容易得多。这种方法通常比较适合数据仓库系统,因为所有数据都在一张表里,不需要做表连接。
主要的缺点整个类层次中的所有属性都挤在一张大表里,如果有很多子类特有的属性,数据库中就会有太多字段的取值为null,这为当前基于行的数据库(使用基于列的DBMS的数据仓库处理这个会更好些)的SQL调优增加了难度。除非进行分区,否则唯一的数据表会成为热点,OLTP系统通常在这方面都不太好。
需要4张表,多态查询生成的SQL如下:
select id, payment_type, amount, currency, rtn, credit_card type,????????case when c.payment_id is not null then 1 ?????when ck.payment_id is not null then 2 ?????when cc.payment_id is not null then 3 ?????when p.id is not null then 0 end as clazzfrom payment p left join cash_payment c on p.id=c.payment_id left join???cheque_payment ck on p.id=ck.payment_id left join ???credit_payment cc on p.id=cc.payment_id;
针对具体子类(例如CashPayment)的查询生成的SQL是这样的:
select id, payment_type, amount, currencyfrom payment p left join cash_payment c on p.id=c.payment_id;
优点包括数据表比较紧凑(没有不需要的可空字段),数据跨三个子类的表进行分区,容易使用超类的表与其他表进行关联。紧凑的数据表可以针对基于行的数据库做存储块优化,让SQL执行得更好。数据分区增加了数据修改的并发性(除了超类,没有热点),OLTP系统通常会更好些。
同样的,第二个查询不需要包含其他子类的属性。
缺点是在所有策略中它使用的表和表连接最多,SQL语句稍显复杂(看看Hibernate动态鉴别器的长CASE子句)。相比单张表,数据库要花更多时间调优数据表连接,数据仓库在使用该策略时通常不太理想。
因为不能跨超类和子类的字段来建立复合索引,如果需要按这些列进行查询,性能会受影响。任何子类数据的修改都涉及两张表:超类的表和子类的表。
涉及三张或更多的表,多态查询生成的SQL是这样的:
select p.id, p.amount, p.currency, p.rtn, p. credit_card_type, p.clazzfrom (select id, amount, currency, null as rtn,null as credit_card type, 1 as clazz from cash_payment union all select id, amount, null as currency, rtn,null as credit_card type, 2 as clazz from cheque_payment union all select id, amount, null as currency, null as rtn,credit_card type, 3 as clazz from credit_payment) p;
针对具体子类(例如CashPayment)的查询生成的SQL是这样的:
select id, payment_type, amount, currency from cash_payment;
优点和上面的“每个子类一张表”策略相似。因为超类通常是抽象的,所以具体的三张表是必须的[开头处说的3张或更多的表是必须的],任何子类的数据修改只涉及一张表,运行起来更快。
缺点是SQL(from子句和union all子查询)太复杂。但是大多数数据库对此类SQL的调优都很好。
如果一个类想和Payment超类关联,数据库无法使用引用完整性(referential integrity)来实现它;必须使用触发器来实现它。这对数据库性能有些影响。
只需要三张表。对于Payment的多态查询生成三条独立的SQL语句,每个对应一个子类。Hibernate引擎通过Java反射找出Payment的所有三个子类。
具体子类的查询只生成该子类的SQL。这些SQL语句都很简单,这里就不再阐述了。
它的优点和上节类似:紧凑数据表、跨三个具体子类的数据分区以及对子类任意数据的修改都只涉及一张表。
缺点是用三条独立的SQL语句代替了一条联合SQL,这会带来更多网络IO。Java反射也需要时间。假设如果你有一大堆领域对象,你从最上层的Object类进行隐式选择查询,那该需要多长时间啊!
根据你的映射策略制定合理的选择查询并非易事;这需要你仔细调优业务需求,基于特定的数据场景制定合理的设计决策。
以下是一些建议:
范例4
下面是一个交易描述应用程序的部分领域类图:
开始时,项目只有GasDeal和少数用户,它使用“每个类层次一张表”。
OilDeal和ElectricityDeal是后期产生更多业务需求后加入的。没有改变映射策略。但是ElectricityDeal有太多自己的属性,因此有很多电相关的可空字段加入了Deal表。因为用户量也在增长,数据修改变得越来越慢。
重新设计时我们使用了两张单独的表,分别针对气/油和电相关的属性。新的映射混合了“每个类层次一张表”和“每个子类一张表”。我们还重新设计了查询,以便允许针对具体交易子类进行选择,消除不必要的列和表连接。
基于4.1节中对业务规则和设计的调优,你得到了一个用POJO来表示的领域对象的类图。我们建议:
范例5
我们有一个名为ElectricityDeals的核心POJO用于描述电的交易。从业务角度来看,它有很多many-to-one关联,例如和Portfolio、Strategy和Trader等的关联。因为引用数据十分稳定,它们被缓存在前端,能基于其ID属性快速定位到它们。
为了有好的加载性能,ElectricityDeal只映射元数据,即那些引用POJO的值类型ID属性,因为在需要时,可以在前端通过portfolioKey从缓存中快速查找Portfolio:
<property name="portfolioKey" column="PORTFOLIO_ID" type="integer"/>这种隐式关联避免了数据库表连接和额外的字段选择,降低了数据传输的大小。
由于创建物理数据库连接非常耗时,你应该始终使用连接池,而且应该始终使用生产级连接池而非Hibernate内置的基本连接池算法。
通常会为Hibernate提供一个有连接池功能的数据源。Apache DBCP的BasicDataSource[13]是一个流行的开源生产级数据源。大多数数据库厂商也实现了自己的兼容JDBC 3.0的连接池。举例来说,你也可以使用Oracle ReaApplication Cluster?[15]提供的JDBC连接池[14]以获得连接的负载均衡和失败转移。
不用多说,你在网上能找到很多关于连接池调优的技术,因此我们只讨论那些大多数连接池所共有的通用调优参数:
短数据库事务对任何高性能、高可扩展性的应用程序来说都是必不可少的。你使用表示对话请求的会话来处理单个工作单元,以此来处理事务。
考虑到工作单元的范围和事务边界的划分,有3中模式:
你还应该注意以下几点。?
范例6
我们的应用程序有多个在大多数情况下只和数据库“A”打交道的服务层方法;它们偶尔也会从数据库“B”中获取只读数据。因为数据库“B”只提供只读数据,我们对这些方法在这两个数据库上仍然使用本地事务。
服务层上有一个方法设计在两个数据库上执行数据变更。以下是伪代码:
//Make sure a local transaction on database A exists@Transactional (readOnly=false, propagation=Propagation.REQUIRED)public void saveIsoBids() { //it participates in the above annotated local transaction insertBidsInDatabaseA(); //it runs in its own local transaction on database B insertBidRequestsInDatabaseB(); //must be the last operation因为insertBidRequestsInDatabaseB()是saveIsoBids ()中的最后一个方法,所以只有下面的场景会造成数据不一致:
在saveIsoBids()执行返回时,数据库“A”的本地事务提交失败。
但是,就算saveIsoBids()使用JTA,在两阶段提交(2PC)的第二个提交阶段失败的时候,你还是会碰到数据不一致。因此如果你能处理好上述的数据不一致性,而且不想为了一个或少数几个方法引入JTA的复杂性,你应该使用本地事务。
(未完待续)
关于作者
Yongjun Jiao是SunGard Consulting Services的技术主管。过去10年中他一直是专业软件开发者,他的专长包括Java SE、Java EE、Oracle和应用程序调优。他最近的关注点是高性能计算,包括内存数据网格、并行计算和网格计算。
Stewart Clark是SunGard Consulting Services的负责人。过去15年中他一直是专业软件开发者和项目经理,他的专长包括Java核心编程、Oracle和能源交易。
[译注:由于原文较长,中译版分两次发布]
查看英文原文:Revving Up Your Hibernate Engine