英文原文:http://in.relation.to/2016/09/28/performance-tuning-and-best-practices/
本文基于已添加到Hibernate用户指南的最新章节。性能调优和最佳实践一章旨在帮助应用程序开发人员充分利用其Hibernate持久层。
每个企业系统都是独特的。但是,拥有非常高效的数据访问层是许多企业应用程序的常见要求。 Hibernate提供了多种功能,可以帮助您调整数据访问层。
Schema管理
虽然Hibernate为hibernate.hbm2ddl.auto配置属性提供了更新选项,但此功能不适用于生产环境。
自动架构迁移工具(例如Flyway,Liquibase)允许您使用任何特定于数据库的DDL功能(例如规则,触发器,分区表)。每次迁移都应该有一个关联的脚本,该脚本与应用程序源代码一起存储在版本控制系统中。
当应用程序部署在类似生产的QA环境上,并且部署按预期工作时,将部署推送到生产环境应该是直截了当的,因为最新的模式迁移已经过测试。
您应始终使用自动架构迁移工具,并将所有迁移脚本存储在版本控制系统中。
日志记录
每当您使用代表您生成SQL语句的框架时,您必须确保生成的语句首先是您想要的语句。
记录语句有几种替代方法。 您可以通过配置基础日志记录框架来记录语句。 对于Log4j,您可以使用以下appender:
### log just the SQL
log4j.logger.org.hibernate.SQL=debug
### log JDBC bind parameters ###
log4j.logger.org.hibernate.type=trace
log4j.logger.org.hibernate.type.descriptor.sql=trace
但是,还有一些其他选择,例如使用datasource-proxy或p6spy。 使用JDBC驱动程序或数据源代理的优点是您可以超越简单的SQL日志记录:
- 声明执行时间
- JDBC批处理日志记录
- 数据库连接监控
使用DataSource代理的另一个好处是可以在测试时断言执行语句的数量。 这样,当自动检测到N + 1查询问题时,您可以使集成测试失败。
虽然简单的语句记录很好,但使用datasource-proxy或p6spy更好。
JDBC批处理
JDBC允许我们批量处理多个SQL语句并将它们发送到数据库服务器中。这样可以节省数据库往返次数,因此可以显着缩短响应时间。
不仅INSERT和UPDATE语句,甚至DELETE语句也可以批处理。对于INSERT和UPDATE语句,请确保您具有所有正确的配置属性,例如订购插入和更新以及激活版本化数据的批处理。有关此主题的更多详细信息,请查看此文章。
对于DELETE语句,没有选项来排序父语句和子语句,因此级联可能会干扰JDBC批处理过程。
与不自动生成SQL语句的任何其他框架不同,Hibernate使得激活JDBC级批处理变得非常容易,如我们的用户指南中的批处理章节所示。
Mapping映射
选择正确的映射对于高性能数据访问层非常重要。从标识符生成器到关联,有许多选项可供选择,但并非所有选择在性能方面都是相同的。
身份标识
说到标识符,您可以选择自然ID或合成键。
对于自然标识符,分配的标识符生成器是正确的选择。
对于合成密钥,应用程序开发人员可以选择随机生成固定大小的序列(例如UUID)或自然标识符。自然标识符非常实用,比UUID对应物更紧凑,因此有多个生成器可供选择:
- IDENTITY
- 序列SEQUENCE
- 表TABLE
尽管TABLE生成器解决了可移植性问题,但实际上它的性能很差,因为它需要使用单独的事务和行级锁来模拟数据库序列。因此,选择通常在IDENTITY和SEQUENCE之间。
如果底层数据库支持序列,则应始终将它们用于Hibernate实体标识符。
仅当关系数据库不支持序列(例如MySQL 5.7)时,才应使用IDENTITY生成器。 但是,您应该记住,IDENTITY生成器禁用INSERT语句的JDBC批处理。
如果您正在使用SEQUENCE生成器,那么您应该使用Hibernate 5中默认启用的增强型标识符生成器。池化和池化优化器对于减少每个写入多个实体时数据库往返次数非常有用 数据库事务。
关联关系
JPA提供四种实体关联类型:
- @ManyToOne
- @OneToOne
- @OneToMany
- @ManyToMany
以及用于嵌入式集合的@ElementCollection。
因为对象关联可以是双向的,所以存在许多可能的关联组合。 但是,从数据库的角度来看,并非每种可能的关联类型都是有效
关联映射越接近底层数据库关系,它就越好。
另一方面,关联映射越奇特,效率低的可能性就越大。
因此,@ ManyToOne和@OneToOne子端关联最好表示FOREIGN KEY关系。
父端@OneToOne关联需要字节码增强,以便可以懒惰地加载关联。否则,即使关联标记为FetchType.LAZY,也始终提取父端。
因此,最好使用@MapsId映射@OneToOne关联,以便在子实体和父实体之间共享PRIMARY KEY。使用@MapsId时,父端变为冗余,因为可以使用父实体标识符轻松获取子实体。
对于集合,关联可以是:
- 单向
- 双向
对于单向集合,集合是最佳选择,因为它们生成最有效的SQL语句。单向列表的效率低于@ManyToOne关联。
双向关联通常是更好的选择,因为@ManyToOne端控制关联。
可嵌入集合(`@ElementCollection)是单向关联,因此集合是最有效的,其次是有序列表,而行李(无序列表)效率最低。
@ManyToMany注释很少是一个很好的选择,因为它将双方视为单向关联。
出于这个原因,映射链接表要好得多,如双向多对多链接实体生命周期用户指南部分所示。每个FOREIGN KEY列将映射为@ManyToOne关联。在每个父节点上,双向@OneToMany关联将映射到链接实体中的上述@ManyToOne关系。
仅仅因为您支持集合,并不意味着您必须将任何一对多数据库关系转换为集合。
有时,@ ManyToOne关联就足够了,集合可以简单地替换为更容易分页或过滤的实体查询。
继承
JPA提供了SINGLE_TABLE,JOINED和TABLE_PER_CLASS来处理继承映射,并且这些策略中的每一个都有优点和缺点。
SINGLE_TABLE在执行的SQL语句方面表现最佳。但是,您不能在列级别上使用NOT NULL约束。您仍然可以使用触发器和规则来强制执行此类约束,但这并不是那么简单。
JOINED解决了数据完整性问题,因为每个子类都与不同的表相关联。多态查询或`@OneToMany基类关联在此策略中表现不佳。但是,多态@ ManyToOne`关联很好,它们可以提供很多价值。
应避免使用TABLE_PER_CLASS,因为它不会生成有效的SQL语句。
获取
获取太多数据是绝大多数JPA应用程序的首要性能问题。
Hibernate支持实体查询(JPQL / HQL和Criteria API)和本机SQL语句。仅当您需要修改获取的实体时,实体查询才有用,因此受益于自动脏检查机制。
对于只读事务,您应该获取DTO预测,因为它们允许您选择满足特定业务用例所需的列数。这有许多好处,例如减少当前运行的持久性上下文的负载,因为不需要管理DTO预测。
获取关联
与关联相关,有两种主要的提取策略:
- 急于EAGER
- 懒LAZY
在JPA之前,Hibernate默认将所有关联都设置为LAZY。但是,当JPA 1.0规范出现时,人们认为并非所有提供商都会使用Proxies。因此,默认情况下,@ ManyToOne和@OneToOne关联现在是EAGER。
EAGER获取策略不能在每个查询的基础上覆盖,因此即使您不需要,也始终会检索关联。此外,如果您忘记在JPQL查询中JOIN FETCH一个EAGER关联,Hibernate将使用辅助语句初始化它,这反过来可能导致N + 1查询问题。
因此,应避免使用EAGER。因此,默认情况下,所有关联都标记为LAZY会更好。
但是,LAZY关联必须在被访问之前初始化。否则,抛出LazyInitializationException。有很多好方法可以处理LazyInitializationException。
处理LazyInitializationException的最佳方法是在关闭持久性上下文之前获取所有必需的关联。 JOIN FETCH指令适用于@ManyToOne和OneToOne关联,最多可用于一个集合(例如@OneToMany或@ManyToMany)。如果您需要获取多个集合,以避免使用笛卡尔积,则应使用通过导航LAZY关联或通过调用Hibernate#initialize(proxy)方法触发的辅助查询。
缓存
Hibernate有两个缓存层:
第一级缓存本身不是缓存解决方案,即使在使用READ COMMITTED隔离级别时,对于确保REPEATABLE READ也更有用。
虽然第一级缓存是短暂的,当底层EntityManager关闭时被清除,但二级缓存与EntityManagerFactory相关联。一些二级缓存提供程序提供对群集的支持。因此,节点只需要存储整个缓存数据的子集。
虽然二级缓存可以减少事务响应时间,因为从缓存而不是从数据库中检索实体,还有其他选项可以实现相同的目标,您应该在跳转到二级缓存层之前考虑这些选择:
- 调整底层数据库缓存,以便工作集适合内存,从而减少磁盘I / O流量。
- 通过JDBC批处理,语句缓存,索引优化数据库语句可以减少平均响应时间,从而提高吞吐量。
- 数据库复制也是增加只读事务吞吐量的非常有价值的选择
在正确调整数据库之后,为了进一步缩短平均响应时间并提高系统吞吐量,应用程序级缓存变得不可避免。
局部地,像Memcached或Redis这样的键值应用程序级缓存是存储数据聚合的常见选择。如果您可以复制键值存储中的所有数据,则可以选择在不完全失去可用性的情况下关闭数据库系统进行维护,因为仍可以从缓存中提供只读流量。
使用应用程序级缓存的主要挑战之一是确保跨实体聚合的数据一致性。这就是二级缓存拯救的地方。与Hibernate紧密集成,二级缓存可以提供更好的数据一致性,因为条目以标准化方式缓存,就像在关系数据库中一样。更改父实体仅需要单个条目缓存更新,而不是键值存储中的缓存条目无效级联。
二级缓存提供四种缓存并发策略:
- READ_ONLY
- NONSTRICT_READ_WRITE
- READ_WRITE
- TRANSACTIONAL
READ_WRITE是一种非常好的默认并发策略,因为它提供了强大的一致性保证,而不会影响吞吐量。 TRANSACTIONAL并发策略使用JTA。因此,当经常修改实体时,它更合适。
READ_WRITE和TRANSACTIONAL都使用直写缓存,而NONSTRICT_READ_WRITE是一种直读缓存策略。因此,如果经常更改实体,则NONSTRICT_READ_WRITE不太合适。
使用群集时,二级缓存条目分布在多个节点上。使用Infinispan分布式缓存时,只有READ_WRITE和NONSTRICT_READ_WRITE可用于读写缓存。请记住,NONSTRICT_READ_WRITE提供较弱的一致性保证,因为可以进行过时更新。
有关Hibernate性能调优的更多信息,请查看Devoxx France的高性能Hibernate演示文稿。