Spring/Hibernate应用性能调优
对于大多数典型的Spring/Hibernate 企业应用来说,应用程序的性能几乎完全取决于它的持久层的性能。
这篇文章将会对如何确认在“数据库约束”的应用前,使用7种“快速见效”的技巧来帮助我们提升应用性能。
如何确认一个应用受到“数据库约束”
为了验证一个应用程序是否受到“数据库约束”,首先在一些开发环境中做一些普遍的行为,即使用 VisualVM来监控。 VisualVM是一个搭载JDK的Java解析器,它通过调用 jvisualvm
来进行命令行登陆。
登陆Visual VM后按照这样做:
- 运行你的应用程序
- 选择 Sampler
- 点击
Settings
复选框 - 选择
Profile only packages
,同时引入下面的包:-
your.application.packages.*
-
org.hibernate.*
-
org.springframework.*
-
your.database.driver.package
, for exampleoracle.*
- Click
Sample CPU
-
一个典型“数据库约束”应用的CPU性能分析应该像这样:
我们可以看到 Java进程的客户端花费了 56%
的时间用来等待数据库通过网络返回结果。
这是个好的标志,它显示了是什么让数据库查询应用变慢的。 32.7%
的Hibernate反射调用可以正常运行所以没什么可以改进的。
第一步优化:获得运行基线
做优化的第一步是定义一个基线运行的程序。我们需要确定一组有效的输入数据使程序通过一个类似于生产输出的典型执行类。
主要的区别在于,基线运行应该在更短的时间内运行,作为一个指导方针的执行时间,5到10分钟是一个理想的时间。
如何得到一个好的基线?
一个好的基线应该有以下特点:
- 功能正确
- 在类型方面输入数据类似于输出数据
- 能在很短的时间内完成
- 基线运行的优化可以进行推广
良好的基线能事半功倍。
怎样会得到一个糟糕的基线?
例如,在批处理调用一个通信系统的数据记录中,获取前 10 000 条记录是 错误的方法。
原因是这前10 000个数据可能是语音通话,而这些未知的错误可能是由于以短信的方式来处理导致的。大量采用这些记录会导致产生一个错误的基线,由此错误的结论就产生了。
手机SQL日志和查询计时
SQL查询的执行时间可以收集用于 log4jdbc。看这篇博客是如何使用log4jdbc收集SQL查询 – Spring/Hibernate improved SQL logging with log4jdbc。
查询执行时间衡量的是Java客户端,它包括往返到数据库的网络。SQL查询日志是像这样的:
16 avr. 2014 11:13:48 | SQL_QUERY /* insert your.package.YourEntity */ insert into YOUR_TABLE (...) values (...) {executed in 13 msec}
好的语句本身也是一个不错的信息来源 –他们允许轻松地识别频繁的查询 类型。他们记录在以下这篇博客日志 – 为什么 Hibernate 做这样的 SQL 查询?
SQL日志可以找出哪些指标
SQL 日志可以回答下列问题:
- 执行最慢的查询是什么?
- 最常见的查询是什么?
- 生成主键的时间量是多少?
- 有数据可以受益于缓存吗?
如何解析SQL日志
可能对于大量日志唯一可行的选择是使用命令行工具。这种方法的优点是非常灵活的。
写一个脚本或命令后我们可以提取主要指标。只要你觉得合适任何命令行工具都是可以用的。
如果你是使用Unix命令行,bash可能是一个不错的选择。Bash也可以在Windows工作站上使用,例如使用 Cygwin,或者包括bash命令行的 Git 。
频繁应用 Quick-Wins
Quick-wins 能识别Spring/Hibernate应用中的常见问题并找到相应的解决方案。
Quick-win 技巧 1:减少主键生成开销
在‘insert-intensive’的进程中,主键生成策略的选择非常重要。一种常见的生成id的方法是’s用数据库序列,通常每个表进行插入数据的时候避免争用一个同一资源。
问题是如果插入50条记录,我们想避免为了获得50条记录的id而造成的网络往返对数据库的消耗,这会导致大部分时间保持Java进程挂起。
Hibernate通常是如何处理的呢?
Hibernate提供了新的优化后的ID生成器来避免这个问题。这就是序列,一个 HiLo
id 生成器是在默认情况下使用的。这是HiLo序列发生器如何工作的:
- 一旦调用一个序列和1000个(高值的)
- 像这样来计算 50 个id的序列:
- 1000 * 50 + 0 = 50000
- 1000 * 50 + 1 = 50001
- …
- 1000 * 50 + 49 = 50049, 最低值 (50)
- 调用更高的序列值 1001 … 等等 …
从一个序列的调用中可以看出,生产50个键可以减少很多网络传输所造成的开销。
这些新的主键优化的产生默认是基于Hibernate 4的,同时也可以在必要时将 hibernate.id.new_generator_mappings
设置为false来关掉它。
为什么生成主键依旧是个问题?
问题就是,如果你宣布主键生成策略是 AUTO
,优化后 仍然是关闭的状态, 这样的话您的应用程序最终会调用大量的序列。
为了确保新的优化生成器处于运行状态,保证使用 SEQUENCE
策略而不是 AUTO
:
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "your_key_generator") private Long id;
通过这个简单的改变,有 10%-20%
的范围可以改善,这可以在基本上没有修改代码的情况下在‘insert-intensive’应用程序中采用。
Quick-win 技巧 2:使用JDBC批处理插入/更新
对于批处理程序,JDBC驱动程序通常提供优化来减少网络流量,这就是‘JDBC 批量插入/更新’。当使用这些的时候,驱动级别的插入/更新在发送到数据库前被排入队列。
当达到阈值后整个批处理队列语句一次性发送到数据库。这可以防止驱动逐条发送语句,这可以进行多网络传输。
这是工厂配置的实体管理来激活插入/更新的批处理:
<prop key="hibernate.jdbc.batch_size">100</prop> <prop key="hibernate.order_inserts">true</prop> <prop key="hibernate.order_updates">true</prop>
只配置JDBC批处理的大小是 不能正常工作的。这是因为JDBC驱动程序只有当接收到插入/更新完全相同的表时才会进行批处理插入操作。
如果新表收到一条插入语句,那么JDBC驱动会在开始对新表进行批处理操作前首先刷新前一张表的批处理语句。
如果使用Spring批处理的话,一个类似的功能是隐式地进行使用。这种优化可以很容易地完成 30%
到 40%
的‘insert intensive’程序,而无需改动一行代码。
Quick-win 技巧 3:定期刷新和清理Hibernate会话
当添加/修改数据库的数据时,Hibernate保持了一个已经存在的实体版本的会话,以防在会话关闭之前进行修改。
但是很多时候,一定在数据库中有匹配的插入时我们就可以安全地丢弃实体。这在Java客户机进程中释放了内存,防止长时间运行Hibernate会话所导致的性能问题。
像这样长时间运行的会话 应该尽可能被阻止,但是由于某种原因需要它们的话就应该包含内存是如何消耗的:
entityManager.flush(); entityManager.clear();
flush
将触发插入新实体从而发送到数据库。 clear
则从会话释放新的实体。
Quick-win 技巧 4 :减少Hibernate过多的dirty-checking
Hibernate使用内部的一种机制来保持记录修改的实体的方式就叫做 dirty-checking。这种机制 不是基于实体的equals和hashcode方法的类。
Hibernate能让dirty-checking的性能成本降至最低,dirty-check只会在需要的时候出现,但是这种机制也是有代价的,它有更多的表和列。
在应用做任何优化前,最重要的是测量使用VisualVM所耗费的dirty-checking的成本。
如何避免dirty-checking?
我们所知道的Spring事务方法是只读的,dirty-checking可以像这样来关闭:
@Transactional(readOnly=true) public void someBusinessMethod() { .... }
另一种避免dirty-checking的方式是使用Hibernate无状态会话,这在 documentation有详细描述。
Quick-win 技巧 5:搜寻 “差的” 查询计划
在最慢的查询列表里进行检查来看他们是否有良好的查询计划。最常见的“差劲的”查询计划是:
- 全表扫描:表完全地被扫描是因为经常缺少索引或者过时的表统计。
- 笛卡尔连接:这意味着几张表进行笛卡儿积后的结果正在进行计算。检查正在丢失的连接条件,或者可以通过将一个步骤分为几步来完成可以避免发生这个问题。
Quick-win 技巧 6:检查错误的提交时间间隔
如果你是正在做批处理,那么提交间隔会在性能结果上产生很大的影响, 能达到10-100倍甚至更多。
确认提交间隔是所预期的(通常是Spring批处理作业的100-1000倍)。参数配置错误的情况时有发生。
Quick-win 技巧 7:使用二级查询缓存
如果某些数据被确定为合格缓存,那么看看这篇博客如何设置Hibernate缓存的: Pitfalls of the Hibernate Second-Level / Query Caches
总结
为了解决应用程序性能问题,最重要的操作是收集一些指标来找到当前的瓶颈是什么。
不给定指标的话几乎不可能在有意义的时间内发现是什么导致问题发生的。
同时,许多但并非所有 ‘数据库-驱动’ 的表现缺陷可以避免在最先使用的Spring Batch框架的应用中发生。