Spring / Hibernate 应用性能调优

来源:ImportNew - 陈晓舜spring

 

对大部分典型的Spring/Hibernate企业应用来讲,应用的性能大部分由持久层的性能决定。sql

 

这篇文章会重温一下怎么去确认咱们的应用是不是”数据库依赖(data-bound)”(译者注:即很是依赖数据库,大量时间花在数据库操做上),而后会大概过一下7个经常使用的提高应用性能的速效方案。数据库

 

怎么肯定应用是不是“数据库依赖”缓存

 

确认一个应用是是不是数据库依赖,首先经过在一些开发环境中作基本的运行,可使用VisualVM来进行监控。VisualVM是一个和JDK一块儿发布的Java性能调优器,能够经过命令行jvisualvm运行。安全

 

执行Visual VM后,尝试下面的步骤:bash

 

  1. 双击你正在运行的应用网络

  2. 选择抽样器(Sampler)session

  3. 点击设置复选框oracle

  4. 选择只调优包,而且限定以下的包类型:app

  • 你的应用程序包

  • org.hibernate.*

  • org.springframework.*

  • 你的数据库jar包名,如oracle.*

  • 点击抽样(Sample) CPU

 

CPU抽样一个典型的“数据库依赖”应用将会获得相似下面的结果:

 

 

 

咱们能够看到Java客户端进程花费了56%的时间在等待数据库从网络中返回结果。

 

这是一个很好的标志,表示正是数据库查询形成了应用的缓慢。Hibernate反射调用占了32.7%是正常,并且咱们对此也无能为力。

 

性能调优第一步 —— 获得基准运行值(baseline run)

 

性能调优的第一步是为程序定义一个基准运行值。咱们须要一系列可使程序运行的有效输入数据,它必须跟在生产环境运行相似。

 

最主要的区别是基准运行须要在更短的时间内运行完成,比较理想的指导值是执行时间为5-10分钟。

 

什么是好的基准(baseline)?

 

一个好的基准须要有下面的特性:

 

  • 保证功能正确

  • 输入数据在可变性上和生产环境相似

  • 在短期内能够完成

  • 在基准运行中作的优化能够直接影响到完整运行

 

取一个好的基准能够解决一大半的问题。

 

什么是很差的基准

 

例如,在一个批处理运行的执行电话数据记录的电信系统中,取得前10000条记录会是一个错误的作法。

 

缘由是:前10000条有可能大部分是语音电话,但未知的性能问题倒是在处理短信通道(SMS traffic)。在一个大批量执行的过程当中获取前面的一些记录不是一个好的基准,有可能会获得错误的结论。

 

收集SQL日志和查询时间

 

SQL查询和执行时间可使用如log4jdbc来进行收集。能够看这篇博客关于如何使用log4jdbc来收集SQL查询 —— 经过log4jdbc来改进Spring/Hibernate的SQL日志(http://blog.jhades.org/logging-the-actualreal-sql-queries-of-a-springhibernate-application/).

 

查询执行时间是在Java客户端进行计算的,它包含了到数据库的网络往返请求耗时。SQL查询日志看起来就像这样:

 

16 avr. 2014 11:13:48 | SQL_QUERY /* insert your.package.YourEntity */ insert into YOUR_TABLE (...) values (...) {executed in 13 msec}

 

Prepared statements本身也是很好的信息源——它容许识别常常执行的查询类型。根据这篇博客,能够很简单地记录——Hibernate在哪里,为何作这个SQL查询(http://blog.jhades.org/how-to-find-out-why-hibernate-is-doing-a-certain-sql-query/)。

 

SQL日志能够获得什么数据

 

SQL日志能够回答这些问题:

 

  • 最慢的查询是什么?

  • 最频繁的查询是什么?

  • 生成主键花了多少时间?

  • 是否有数据能够经过缓存受益?

 

怎么转换SQL日志

 

也许对于大日志文件最可行的方案就是使用命令行工具。这个方法的优势是比较灵活。

 

只须要耗费点时间写一小段脚本或命令,咱们能够抽取大部分任何须要的数据。任何命令行均可以按你喜欢的方式去使用。

 

若是你使用Unix命令行,bash会是一个很好的选择。Bash也能够在Windows工做站中使用,使用例如Cygwin或Git这些包含bash命令行的工具。

 

经常使用的速效方案

 

下面的速效方案能够识别Spring/Hibnerate应用中的常见性能问题和对应的解决方案。

 

速效方案1 —— 减小主键提早生成

 

在一些插入密集(intert-intensive)的处理中,主键生成策略的选择有很大的影响。一个常见的生成ID的方法是使用数据库的序列(sequences),一般每一个表一个,以免插入不一样表时的冲突。

 

问题在于,若是插入50条记录,咱们但愿能够避免50次经过数据库获取50个ID的网络往返,而不使Java进程在大部分时间内等待。

 

Hibernate一般是怎么处理这个的?

 

Hibernate提供了新优化的ID生成器能够避免这个问题。对于sequences,会默认使用一个HiLo id生成器。HiLo序列生成器的工做过程以下:

 

  • 调用一次sequence返回1000(最大值)

  • 以下计算50个ID:

  • 1000 * 50 + 0 = 50000

  • 1000 * 50 + 1 = 50001

  • 1000 * 50 + 49 = 50049, 达到小值 (50)

  • 调用sequence获取更大的值1001 …依此类推…

 

因此从第一次sequence调用时,就已经生成了50个key了,减小了大量的网络往返耗时。

 

这些新优化的主键生成器在Hibernate4中是默认开启的,在须要时,能够经过设置hibernate.id.new_generator_mappings为false进行关闭。

 

为何主键生成仍然是个问题?

 

问题在于,若是你定义主键生成策略为AUTO,优化生成器仍然是关闭的,你的应用仍然仍是会进行很大数量的sequence调用。

 

为了保证新的优化生成器被启用,确保使用SEQUENCE策略而不是AUTO:

 

@Id

@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "your_key_generator")

private Long id;

 

有了这个小改变,’插入密集’的应用会有10%-20%的提高,而并不须要作其余的代码修改。

 

速效方案2 —— 使用JDBC批量插入/修改

 

对于批量的程序,JDBC驱动一般会提供称之为’JDBC批量插入/修改’的优化方案用于减小网络往返消耗。在使用它们时,插入/修改在发送到数据库前会在驱动层排队(译者注:达到必定的数量后会一次性发送多条SQL进行执行)。

 

当指定的阀值达到后,队列中的批量语句将会被一次性发送到数据库。这防止了驱动一个接一个的发送请求,浪费多个网络请求。

 

下面是用于启用批量插入/更新的entity manager factory的配置:

 

<prop key="hibernate.jdbc.batch_size">100</prop>

<prop key="hibernate.order_inserts">true</prop>

<prop key="hibernate.order_updates">true</prop>

 

只是设置JDBC batch size不会生效。这是由于JDBC驱动只有在具体某个相同的表接收到插入/更新时才会把插入当成批量处理。

 

若是接收到对一个新表的插入命令,JDBC驱动会在执行新表的批量语句前先送出上一个表的批量的语句。

 

使用Spring Batch时也有隐晦地使用到一个相似的功能。这个优化能够很简单地为你的“插入密集”应用节省30%到40%的时间,而不须要修改一行代码。

 

速效方案3 —— 按期刷新和清空Hibernate session

 

当添加/修改数据库数据时,为了防止它们在session关闭后被从新修改,Hibnerate会在session中保持已经持久化的实体的版本。

 

但不少时候,在插入数据库完成后,咱们能够安全地丢弃实体。这能够在Java客户端释放内存,防止因为长时间运行Hibernate session形成的性能问题。

 

这种长时间运行的session应该被尽可能避免,但若是因为某些缘由确实须要使用,下面的代码展现了怎么继续保存内存引用:

 

entityManager.flush();

entityManager.clear();

 

这个flush会触发发送操做,新实体的插入操做会被马上发送到数据库。clear会从session中释放新实体。

 

速效方案4 —— 减小Hibernate提早的dirty-check

 

Hibernate使用称之为dirty-checking的内部的机制来跟踪修改的实体。这个机制并不基于实体的equals和hashcode方法。

 

Hibnerate竭尽所能使dirty-checking的性能损耗降到最小,只有在须要的时候才进行dirty-check,但这个机制依然是会有损耗的。在有大量字段的表时尤为须要注意。

 

在执行任何优化前,最重要的就是使用VisualVM计算一下dirty-check的损耗。

 

怎么避免dirty-check

 

在Spring中,咱们所知的业务方法是只读的,dirty-check能够经过下面的方法进行关闭:

 

@Transactional(readOnly=true)

public void someBusinessMethod() {

....

}

 

另一个可选的避免dirty-check的方法就是使用Hibnerate无状态Session(Stateless Session),在文档中有详细描述。

 

速效方案5 —— 查找“坏”查询方案

 

检查一下在最慢查询列表中的查询,看看它们是否有好的查询方案。最多见的“坏”查询方案是:

 

  • 全表查询(Full table scans):它发生在当表因为缺失索引或过时的表数据而被全量扫描。

     

  • 彻底笛卡尔链接(Full cartesian joins):这意味着多个表计算彻底笛卡尔积。检查一下是否缺乏链接条件,或是否能够经过分割语句来避免。

 

速效方案6 —— 检查错误的提交间隔

 

若是你正在作批量处理,提交的间隔在性能结果中能够形成巨大的差异,能够达到10-100倍。

 

确认一个提交的间隔是所指望的(Spring Batch通常是100-1000)。它一般是由于这个参数没有正确配置。

 

速效方案7 —— 使用二级和查询缓存

 

若是发现某些数据很适合缓存,那么看一下这篇文章怎么去配置Hibernate缓存:Hibernate二级/查询缓存的陷阱(http://blog.jhades.org/setup-and-gotchas-of-the-hibernate-second-level-and-query-caches/)。

 

结论

 

要解决应用的性能问题,要作的最重要的就是收集一些能够找到当前瓶颈所在的数据。没有一些数据,基本上不可能在有效的时间内猜到问题在哪里。而且,虽然不是全部,但不少的典型的“数据库依赖”的应用性能陷阱均可以经过使用Spring Batch框架在第一时间避免。