来源:ImportNew - 陈晓舜

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

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

怎么确定应用是否是“数据库依赖”

确认一个应用是是否是数据库依赖,首先通过在一些开发环境中做基本的运行,可以使用VisualVM来进行监控。VisualVM是一个和JDK一起发布的Java性能调优器,可以通过命令行jvisualvm运行。

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

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

  2. 选择抽样器(Sampler)

  3. 点击设置复选框

  4. 选择只调优包,并且限定如下的包类型:

  • 你的应用程序包

  • 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框架在第一时间避免。

Spring / Hibernate 应用性能调优的更多相关文章

  1. JVM内存模型与性能调优

    堆内存(Heap) 堆是由Java虚拟机(JVM,下文提到的JVM特指Sun hotspot JVM)用来存放Java类.对象和静态成员的内存空间,Java程序中创建的所有对象都在堆中分配空间,堆只用 ...

  2. Spring/Hibernate 应用性能优化的7种方法

    对于大多数典型的 Spring/Hibernate 企业应用而言,其性能表现几乎完全依赖于持久层的性能.此篇文章中将介绍如何确认应用是否受数据库约束,同时介绍七种常用的提高应用性能的速成法.本文系 O ...

  3. 程序员必须掌握的性能调优 X Y Z

    热评博文:<如何设计出优美的Web API?>,现阅读量超 2500,小伙伴们不要错过哦! 2003 ~ 2008 年,这五年老兵哥我在通信行业做实习生和开发岗,主要用 C / C++ / ...

  4. JVM 性能调优实战之:使用阿里开源工具 TProfiler 在海量业务代码中精确定位性能代码

    本文是<JVM 性能调优实战之:一次系统性能瓶颈的寻找过程> 的后续篇,该篇介绍了如何使用 JDK 自身提供的工具进行 JVM 调优将 TPS 由 2.5 提升到 20 (提升了 7 倍) ...

  5. spring-petclinic性能调优实战(转)

    1.spring-petclinic介绍 spring-petclinic是spring官方做的一个宠物商店,结合了spring和其他一些框架的最佳实践. 架构如下: 1)前端 Thymeleaf做H ...

  6. Redis基础、高级特性与性能调优

    本文将从Redis的基本特性入手,通过讲述Redis的数据结构和主要命令对Redis的基本能力进行直观介绍.之后概览Redis提供的高级能力,并在部署.维护.性能调优等多个方面进行更深入的介绍和指导. ...

  7. Redis 基础、高级特性与性能调优

    本文将从Redis的基本特性入手,通过讲述Redis的数据结构和主要命令对Redis的基本能力进行直观介绍.之后概览Redis提供的高级能力,并在部署.维护.性能调优等多个方面进行更深入的介绍和指导. ...

  8. MySQL性能调优与架构设计——第 14 章 可扩展性设计之数据切分

    第 14 章 可扩展性设计之数据切分 前言 通过 MySQL Replication 功能所实现的扩展总是会受到数据库大小的限制,一旦数据库过于庞大,尤其是当写入过于频繁,很难由一台主机支撑的时候,我 ...

  9. hbase性能调优(1)

    hbase性能调优 标签: hbase 性能调优 | 发表时间:2014-05-17 15:10 | 作者:无尘道长 分享到: 出处:http://www.iteye.com 一.服务端调优 1.参数 ...

随机推荐

  1. java打包成jar文件

    JAR包是Java中所特有一种压缩文档,其实大家就可以把它理解为.zip包.当然也是有区别的,JAR包中有一个META-INF\MANIFEST.MF文件,当你找成JAR包时,它会自动生成.JAR包是 ...

  2. pulltorefresh 设置刷新文字提示颜色

     xmlns:ptr="http://schemas.android.com/apk/res-auto" 赵泽民 2016/7/12 15:48:58 ptr:ptrHeaderS ...

  3. RESTClient

    RESTClient是Mozilla Firefox一个用于测试http请求插件. 1.打开火狐扩展搜索RESTClient进行安装并重启浏览器. 2.重启后可以在Mozilla Firefox地址栏 ...

  4. OkHttp自定义重试次数

    本文主要应用了OkHttp的Interceptor来实现自定义重试次数 虽然OkHttp自带retryOnConnectionFailure(true)方法可以实现重试,但是不支持自定义重试次数,所以 ...

  5. css盒子居中定位问题

    在HTML中,div盒子的居中要通过外边距margin和width来控制,首先确定盒子的宽度,然后确定盒子方位并将其平移便可使盒子移到固定位置. <div id="divpic&quo ...

  6. 【转】shell学习笔记(一)——学习目的性、特殊字符、运算符等

    1 学习shell的目的性 写之前我们先来搞清楚为什么要学shell,学习要有目的性 shell简单.灵活.高效,特别适合处理一些系统管理方面的小问题 shell可以实现自动化管理,让系统管理员的工作 ...

  7. CSS中的选择器之html选择器和伪类选择器

    1.html选择器(标签选择器) 基本语法: html标签名称{ 属性名:属性值; 属性名:属性值; } 继续在上面的代码中做修改,实例代码: <!DOCTYPE html> <ht ...

  8. eclipse open call hierarchy无效

    问题: Eclipse中选中一个方法,右击选中open call hierarchy,不显示哪些地方调用了这个方法,却显示了这个方法里面调用了那些方法.前阵子还是好的,现在不知道为什么了. 解决: s ...

  9. SpringMVC源码情操陶冶-HandlerAdapter适配器简析

    springmvc中对业务的具体处理是通过HandlerAdapter适配器操作的 HandlerAdapter接口方法 列表如下 /** * Given a handler instance, re ...

  10. Codeforces Round #410 (Div. 2)

    Codeforces Round #410 (Div. 2) A B略..A没判本来就是回文WA了一次gg C.Mike and gcd problem 题意:一个序列每次可以把\(a_i, a_{i ...