使用JPA将整个表格及其关联加载到内存中

9

我需要处理分布在20个表中的大量数据(总计约500万条记录),并且我需要高效地加载它们。

我正在使用Wildfly 14和JPA / Hibernate。

由于最终,每个单独的记录都将被业务逻辑(在同一事务中)使用,因此我决定通过以下方式将所需表的整个内容预加载到内存中:

em.createQuery("SELECT e FROM Entity e").size();

之后,每个对象都应在事务中可用,因此可以通过以下方式访问:

em.find(Entity.class, id);

但是,这种方法并不起作用,仍然有很多调用数据库的操作,特别是涉及到关系的时候。
如何高效地加载所需表格的全部内容,包括关系,并确保我已经获取了所有数据/不会再进行其他数据库调用呢?
我已经尝试过的方法:
- FetchMode.EAGER:仍然存在太多的单个查询/对象图太复杂 - EntityGraphs:与FetchMode.EAGER相同 - Join fetch语句:目前为止最好的结果,因为它同时填充了对所引用实体的关系 - 二级/查询缓存:无效,可能与em.find的问题相同
需要注意的一点是,数据是不可变的(至少在特定时间内),也可以在其他事务中使用。
编辑:
我的计划是在一个@Singleton bean中加载和管理整个数据。但我想确保以最有效的方式加载它,并确保已经加载了整个数据。当业务逻辑使用数据时,不应该再有其他查询。在特定时间之后(ejb定时器),我将丢弃整个数据,并重新从数据库加载当前状态(始终是整个表格)。

这是一个减少查询数量的可怕计划。你想解决什么问题?无论我怎么看,你都会缓存整个数据库,这不是事情应该运作的方式。而且,如果你有多台服务器...为什么不制作一个镜像数据库?你可以把所有东西都复制到那里,并将查询发送到镜像。这将有效地完成相同的工作,而不会出现复制问题。 - Andrii Plotnikov
我想从几个一致的表(而不是“整个数据库”)中加载静态数据到内存中,在短时间内在服务器上的几个时间关键型进程中完全需要。在这台内存实际上没有限制的服务器上,我看不出这有什么“不可行”或“可怕”的地方。如果没有事务/并发需要,为什么我需要镜像数据库及其开销呢?此外,为了有效,镜像数据库必须在内存中,那么与我的方法相比,有何区别/好处? - Meini
3个回答

6
请注意,您可能需要使用64位JVM和大量的内存。请参阅Hibernate 2级缓存。由于我们没有您的代码,以下是一些要检查的事项:
  1. @Cacheable注释将为Hibernate提供线索,以便实体可缓存
  2. 配置第二级缓存以使用类似于ehcache的东西,并将最大内存元素设置为足够大以适合您的工作集
  3. 确保您的代码中没有意外使用多个会话。

如果您需要以这种方式处理事物,则可能希望考虑更改设计,以不依赖于在内存中拥有所有内容,不使用Hibernate / JPA或不使用应用程序服务器。这将使您更加控制事务的执行方式。这甚至可能更适合像Hadoop这样的东西。没有更多信息,很难说哪个方向对您最好。


我配置了二级缓存/可缓存,但据我所知,只有em.find()会使用二级缓存。当我访问OneToMany集合时,仍然会有很多查询到数据库,即使它们被定义为FetchType.EAGER。我还必须/应该坚持使用应用服务器,因为它给了我很多我需要的基础设施。服务器运行在一台拥有144GB内存的机器上,这应该足够了。 - Meini
@Meini,你的JVM内存设置是多少?你安装了64位版本吗?即使你的服务器有144GB,你的JVM也必须配置好才能处理它。 - Jason Armstrong
我已经安装了Java 8的64位版本,并且将内存限制为-Xmx120g,以便留出其他应用程序的空间。我的测试数据只有几GB(约5个)。 - Meini
好吧,第二级缓存对我也没有帮助,因为我仍然需要加载数据,这就是问题所在。第二级缓存可以是一种将数据保留在内存中的方法,但是数据仅由主键索引,这意味着查询不会使用它,只有像em.find()这样的东西才能工作。 - Meini

5

基本上,使用一条查询语句加载整个表并链接对象应该是一个非常简单的任务,但是JPA的工作方式与此不同,如下例所示。

最大的问题在于@OneToMany/@ManyToMany关系:

@Entity
public class Employee {
    @Id
    @Column(name="EMP_ID")
    private long id;
    ...
    @OneToMany(mappedBy="owner")
    private List<Phone> phones;
    ...
}
@Entity
public class Phone {
    @Id
    private long id;    
    ...
    @ManyToOne
    @JoinColumn(name="OWNER_ID")
    private Employee owner;
    ...
}

FetchType.EAGER

如果定义为FetchType.EAGER,并且查询SELECT e FROM Employee e,Hibernate将生成SQL语句SELECT * FROM EMPLOYEE,紧接着是SELECT * FROM PHONE WHERE OWNER_ID=?,对于每个加载的Employee,通常称为1 + n问题

我可以通过使用JPQL查询SELECT e FROM Employee e JOIN FETCH e.phones来避免n + 1问题,这将导致类似于SELECT * FROM EMPLOYEE LEFT OUTER JOIN PHONE ON EMP_ID = OWNER_ID的结果。

问题在于,这对于涉及约20个表的复杂数据模型不起作用。

FetchType.LAZY

如果定义为FetchType.LAZY,查询SELECT e FROM Employee e将仅将所有员工作为代理加载,在访问phones时才加载相关电话,最终也会导致1 + n问题。

要避免这种情况,很明显只需将所有电话加载到同一个会话中SELECT p FROM Phone p。但是当访问phones时,Hibernate仍将执行SELECT * FROM PHONE WHERE OWNER_ID=?,因为Hibernate不知道当前会话中已经有所有电话。

即使使用第二级缓存,语句也将在数据库上执行,因为Phone在第二级缓存中是按其主键索引的,而不是按OWNER_ID索引的。

结论

Hibernate中没有像“只加载所有数据”这样的机制。

似乎没有其他方法,只能保持关系瞬态并手动连接它们,甚至只使用普通的JDBC。

编辑:

我刚刚找到了一个非常好的解决方案。我将所有相关的@ManyToMany@OneToMany定义为FetchType.EAGER,并与@Fetch(FetchMode.SUBSELECT)组合,将所有@ManyToOne@Fetch(FetchMode.JOIN)组合,这导致了可以接受的加载时间。除了将javax.persistence.Cacheable(true)添加到所有实体之外,我还将org.hibernate.annotations.Cache添加到每个相关集合中,这启用了第二级缓存中的集合缓存。我禁用了第二级缓存超时驱逐,并通过@Singleton EJB与@Startup在服务器启动/部署时“预热”第二级缓存。现在我可以100%控制缓存,直到我手动清除它,不会有进一步的数据库调用。


2
在Hibernate中,仍然存在FetchMode.SUBSELECT。这是非常重要的一种模式,因为它可以减少从数据库传输到应用服务器的数据量。请查看关于此问题的以下链接:https://dev59.com/B1wY5IYBdhLWcg3wH0xC - michaeak
谢谢您的评论。是的,我知道这个问题,并已经尝试使用良好放置的FetchMode.JOINFetchMode.SUBSELECT来优化查询,我确实可以明显减少查询次数,但它并没有给我带来我所希望的结果,也许我应该再试一次。 - Meini

5

我明白你的问题,但是JPA / Hibernate不会想要为您缓存那么多数据,或者至少我不会指望它保证。考虑到您描述了500万条记录,每条记录的平均长度是多少呢?如果每条记录100字节,就需要500MB的内存,这将导致未经调整的JVM崩溃。平均值可能更接近5000字节,这将需要25GB的内存,因此您需要考虑一下自己在寻求什么。

如果您想要缓存,您应该自己处理,或者更好的方法是在拥有结果时再使用它们。如果您想要基于内存的数据访问,您应该寻找专门用于此目的的技术。看起来http://www.ehcache.org/很受欢迎,但这取决于您自己,您应该确保先了解您的用例。

如果您想要数据库效率,则应该仔细设计和测试,并了解自己在做什么。


"100字节提供500兆字节的PermGen内存" - 为什么是PermGen?请注意,它在Java 8及以后的版本中甚至不存在。但即使在旧的JVM中,PermGen也只用于某些特定的事情。普通字符串数据不是其中之一。 - Stephen C
1
好的。也许在这个例子中堆会更好。我会把具体细节拿出来。在使用JPA进行连接加载时,它可能会使用大量的元空间,这可能会导致问题。当在像容器这样的限制环境中运行时,您需要注意这一点。即使最终的内存需求可能仅为25 GB,但加载比字符串更复杂的东西将需要超过25 GB的系统内存。 - K.Nicholas
我现在没有参考资料,但我还没有认真查找。为了将一些10-30K行读入实体集并保存在内存中,我不得不将MAX_METASPACE设置为192m,而默认值我认为是128m或64m,这是基于docker/wildfly的openshift项目。系统需要720m才能启动,但在运行时,只使用大约380堆/非堆总量,当实体加载到内存中时。最大堆/非堆最大值约为300/600。我仔细追踪了JPA提取代码,并特别设置了Metaspace以解决问题。 - K.Nicholas
在你的例子中,元空间很可能是由于(过度)生成动态代理类而不是涉及的数据量导致的。元空间用于保存代码和与类相关的描述符。元空间不保存数据。因此,我认为你从应用于特定问题的修复中得出了错误的结论。 - Stephen C
此外,根据 https://blogs.oracle.com/poonam/about-g1-garbage-collector,-permanent-generation-and-metaspace,元空间默认仅限于可用内存/地址空间的数量。`java` 命令的手动条目也是如此。你看到的“默认值”必须是应用程序特定包装器/启动器设置的。 - Stephen C
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接