本文基于Hibernate用户指南中最新添加的章节。性能调优与最佳实践章节旨在帮助应用开发者充分利用Hibernate持久层。
每个企业系统都是独一无二的。然而,对许多企业应用程序来说,拥有一个高效的数据访问层是一个共同的需求。Hibernate提供了一系列功能,可以帮助您调整数据访问层。
日志记录
每当您使用为您生成SQL语句的框架时,您必须确保生成的语句是您最初打算使用的。
有多种记录语句的替代方案。您可以通过配置底层日志框架来记录语句。对于Log4j,您可以使用以下附加器
### 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
但是,还有一些其他替代方案,例如使用数据源代理或p6spy。使用JDBC Driver
或DataSource
代理的优势是您可以超越简单的SQL记录
-
语句执行时间
-
JDBC批处理日志记录
使用DataSource
代理的另一个优点是,您可以在测试时断言执行的语句数量。这样,当检测到N+1查询问题时,可以自动使集成测试失败(参考链接)。
虽然简单的语句记录是可以的,但使用datasource-proxy或p6spy会更好。 |
JDBC批处理
JDBC允许我们批量处理多个SQL语句,并将它们作为一个请求发送到数据库服务器。这样可以节省数据库往返,从而显著减少响应时间(参考链接)。
不仅INSERT
和UPDATE
语句可以批处理,DELETE
语句也可以。对于INSERT
和UPDATE
语句,确保您已设置所有正确的配置属性,例如排序插入和更新以及激活版本数据的批处理。有关此主题的更多详细信息,请参阅此文章。
对于DELETE
语句,没有对父级和子级语句进行排序的选项,因此级联可能会干扰JDBC批处理过程。
与不自动生成SQL语句的其他框架不同,Hibernate使我们能够轻松激活JDBC级别的批处理,如在我们的用户指南的批处理章节中所示。
映射
选择正确的映射对于高性能数据访问层非常重要。从标识符生成器到关联,有许多选项可以选择,但并非所有选择在性能方面都相等。
标识符
在标识符方面,您可以选择自然标识符或合成键。
对于自然标识符,分配标识符生成器是正确选择。
对于合成键,应用开发者可以选择随机生成固定大小的序列(例如UUID)或自然标识符。自然标识符非常实用,比它们的UUID对应物更紧凑,因此有多个生成器可供选择
-
IDENTITY
-
SEQUENCE
-
TABLE
尽管TABLE
生成器解决了可移植性问题,但实际上它性能不佳,因为它需要使用单独的事务和行级锁来模拟数据库序列。因此,通常在IDENTITY
和SEQUENCE
之间进行选择。
如果底层数据库支持序列,您应该始终为Hibernate实体标识符使用它们。 只有在关系数据库不支持序列(例如MySQL 5.7)的情况下,您才应使用 |
如果您正在使用SEQUENCE
生成器,那么您应该使用由Hibernate 5默认启用的增强型标识符生成器。如池化和池化-lo优化器非常有用,可以减少在数据库事务中写入多个实体时的数据库往返次数。
关联
JPA提供了四种实体关联类型
-
@ManyToOne
-
@OneToOne
-
@OneToMany
-
@ManyToMany
以及用于嵌入可嵌入集合的@ElementCollection
。
由于对象关联可以是双向的,因此存在许多可能的关联组合。但是,并非所有可能的关联类型在数据库方面都是高效的。
关联映射与底层数据库关系越接近,其性能越好。 另一方面,关联映射越复杂,其低效的可能性就越大。 |
因此,使用@ManyToOne
和@OneToOne
子侧关联来表示FOREIGN KEY
关系最佳。
父侧的@OneToOne
关联需要字节码增强,以便关联可以延迟加载。否则,即使关联被标记为FetchType.LAZY
,父侧也总是会被检索。
因此,最好使用@MapsId
映射@OneToOne
关联,以便在子实体和父实体之间共享PRIMARY KEY
。当使用@MapsId
时,父侧变得多余,因为可以使用父实体标识符轻松检索子实体。
对于集合,关联可以是
-
单向的
-
双向的
对于单向集合,Sets
是最佳选择,因为它们生成最有效的SQL语句。与@ManyToOne
关联相比,Unidirectional Lists
效率较低。
双向关联通常是更好的选择,因为@ManyToOne
侧控制着关联。
可嵌入集合(@ElementCollection
)是单向关联,因此Sets
是最有效的,其次是有序的Lists
,而集合(无序的Lists
)效率最低。
@ManyToMany
注解很少是好的选择,因为它将两侧都视为单向关联。
因此,最好将链接表映射如双向多对多与链接实体生命周期用户指南部分所示。每个FOREIGN KEY
列都将映射为@ManyToOne
关联。在每个父侧,双向的@OneToMany
关联将映射到链接实体中的上述@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投影不需要管理。
获取关联数据
关于关联数据,有两种主要的获取策略:
-
立即
-
延迟
在JPA出现之前,Hibernate默认将所有关联数据都设置为
|
因此,应该避免使用立即
获取。出于这个原因,最好默认将所有关联数据标记为延迟
。
然而,延迟
关联在访问之前必须进行初始化。否则,会抛出LazyInitializationException
。处理LazyInitializationException
有好的和坏的方法。
处理LazyInitializationException
的最佳方法是,在关闭持久性上下文之前获取所有所需关联数据。对于@ManyToOne
和OneToOne
关联,以及最多一个集合(例如@OneToMany
或@ManyToMany
),JOIN FETCH
指令非常好。如果您需要获取多个集合,为了避免笛卡尔积,您应该使用由导航延迟
关联或调用Hibernate#initialize(proxy)
方法触发的二级查询。
缓存
Hibernate有两个缓存层:
-
二级缓存,与应用级缓存不同,它不存储实体聚合,而是存储归一化的脱水实体条目。
一级缓存不是一个真正的缓存解决方案,它更有助于确保REPEATABLE READ(s)
,即使在使用READ COMMITTED
隔离级别的情况下。
虽然一级缓存的生命周期很短,当底层的EntityManager
关闭时会被清除,但二级缓存与EntityManagerFactory
相关联。一些二级缓存提供程序支持集群。因此,一个节点只需要存储整个缓存数据的一个子集。
虽然二级缓存可以通过从缓存而不是数据库中检索实体来减少事务响应时间,但还有其他选项可以实现相同的目标,在跳转到二级缓存层之前,您应该考虑这些替代方案。
-
调整底层数据库缓存,使工作集适合内存,从而减少磁盘I/O流量。
-
通过JDBC批处理、语句缓存、索引优化数据库语句可以减少平均响应时间,从而提高吞吐量。
-
数据库复制也是增加只读事务吞吐量的非常有价值的选择
在适当调整数据库后,为了进一步减少平均响应时间并提高系统吞吐量,应用级缓存变得不可避免。
在主题上,像Memcached或Redis这样的键值应用级缓存是存储数据聚合的常见选择。如果您可以在键值存储中复制所有数据,您有选择在不完全丢失可用性的情况下关闭数据库系统进行维护的选项,因为只读流量仍可以从缓存中提供。
使用应用级缓存的主要挑战之一是确保实体聚合之间的数据一致性。这就是二级缓存发挥作用的地方。与Hibernate紧密结合,二级缓存可以提供更好的数据一致性,因为条目以规范化的方式缓存,就像在关系数据库中一样。更改父实体只需要更新单个条目缓存,而不是像键值存储中那样级联无效化缓存条目。
二级缓存提供了四种缓存并发策略
读写
是一个非常好的默认并发策略,因为它提供了强大的一致性保证,同时不会降低吞吐量。《事务性》并发策略使用JTA。因此,当实体经常被修改时,它更适合。
《读写》和《事务性》都使用写后缓存,而《非严格读写》是一种读后缓存策略。因此,如果实体经常更改,《非严格读写》并不非常合适。
在集群中使用时,二级缓存条目会分布在多个节点上。当使用Infinispan分布式缓存时,只有《读写》和《非严格读写》可用于读写缓存。请注意,《非严格读写》提供的致性保证较弱,因为可能存在过时的更新。
有关Hibernate性能调整的更多信息,请查看Devoxx France的《High-Performance Hibernate》演示High-Performance Hibernate。 |