从我的二级 ehcache 中检索项目后出现 "org.hibernate.LazyInitializationException" 异常

Getting "org.hibernate.LazyInitializationException" exceptions after retrieving items from my second-level ehcache

我正在使用 Hibernate 5.1.0.Final 与 ehcache 和 Spring 3.2.11.RELEASE。我在我的 DAO 之一中设置了以下 @Cacheable 注释:

@Override
@Cacheable(value = "main")
public Item findItemById(String id)
{
    return entityManager.find(Item.class, id);
}

返回的项目有多个关联,其中一些是惰性的。因此,例如,它(最终)引用了字段:

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "product_category", joinColumns = { @JoinColumn(name = "PRODUCT_ID") }, inverseJoinColumns = { @JoinColumn(name = "CATEGORY_ID") })
private List<Category> categories;

我注意到,在我标记为 @Transactional 的方法之一中,当从二级缓存中检索上述方法时,在尝试遍历类别字段时出现以下异常:

@Transactional(readOnly=true)
public UserContentDto getContent(String itemId, String pageNumber) throws IOException
{
    Item Item = contentDao.findItemById(ItemId);
   …
   // Below line causes a “LazyInitializationException” exception
   for (Category category : item.getParent().getProduct().getCategories())
    {

堆栈跟踪是:

16:29:42,557 INFO  [org.directwebremoting.log.accessLog] (ajp-/127.0.0.1:8009-18) Method execution failed: : org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: org.mainco.subco.ecom.domain.Product.standardCategories, could not initialize proxy - no Session
    at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:579) [hibernate-myproject-5.1.0.Final.jar:5.1.0.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:203) [hibernate-myproject-5.1.0.Final.jar:5.1.0.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:558) [hibernate-myproject-5.1.0.Final.jar:5.1.0.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:131) [hibernate-myproject-5.1.0.Final.jar:5.1.0.Final]
    at org.hibernate.collection.internal.PersistentBag.iterator(PersistentBag.java:277) [hibernate-myproject-5.1.0.Final.jar:5.1.0.Final]
    at org.mainco.subco.ebook.service.ContentServiceImpl.getCorrelationsByItem(ContentServiceImpl.java:957) [myproject-90.0.0-SNAPSHOT.jar:]
    at org.mainco.subco.ebook.service.ContentServiceImpl.getContent(ContentServiceImpl.java:501) [myproject-90.0.0-SNAPSHOT.jar:]
    at sun.reflect.GeneratedMethodAccessor819.invoke(Unknown Source) [:1.6.0_65]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) [rt.jar:1.6.0_65]
    at java.lang.reflect.Method.invoke(Method.java:597) [rt.jar:1.6.0_65]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.proceedWithInvocation(TransactionInterceptor.java:96) [spring-tx-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:260) [spring-tx-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94) [spring-tx-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:91) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at com.sun.proxy.$Proxy126.getContent(Unknown Source)

我明白 Hibernate 会话关闭的原因——我不关心为什么会这样。此外,使上述关联变得急切(而不是懒惰)也不是一个选项。鉴于此,我该如何解决这个问题?

编辑: 这是我的 ehccahe.xml 的配置方式……

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd" updateCheck="false">
    <!-- This is a default configuration for 256Mb of cached data using the JVM's heap, but it must be adjusted
         according to specific requirement and heap sizes -->
    <defaultCache maxElementsInMemory="10000"
         eternal="false"
         timeToIdleSeconds="86400"
         timeToLiveSeconds="86400"
         overflowToDisk="false"
         memoryStoreEvictionPolicy="LRU">
    </defaultCache> 
    <cache name="main" maxElementsInMemory="10000" />   
     <cacheManagerPeerProviderFactory
         class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
         properties="peerDiscovery=automatic, multicastGroupAddress=230.0.0.1,
         multicastGroupPort=4446, timeToLive=32"/>
    <cacheManagerPeerListenerFactory
        class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
        properties="hostName=localhost, port=40001,
        socketTimeoutMillis=2000"/>    
</ehcache>

下面是我将其插入我的 Spring 上下文的方式……

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="packagesToScan" value="org.mainco.subco" />
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
    </property>
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaPropertyMap" ref="jpaPropertyMap" />
</bean>

<cache:annotation-driven key-generator="cacheKeyGenerator" />

<bean id="cacheKeyGenerator" class="org.mainco.subco.myproject.util.CacheKeyGenerator" />

<bean id="cacheManager"
        class="org.springframework.cache.ehcache.EhCacheCacheManager"
        p:cacheManager-ref="ehcache"/>

<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
        p:configLocation="classpath:ehcache.xml"
        p:shared="true" />

<util:map id="jpaPropertyMap">
    <entry key="hibernate.show_sql" value="false" />
    <entry key="hibernate.hbm2ddl.auto" value="validate"/>
        <entry key="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
        <entry key="hibernate.transaction.manager_lookup_class" value="org.hibernate.transaction.JBossTransactionManagerLookup" />
        <entry key="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
        <entry key="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider"/>
        <entry key="hibernate.cache.use_second_level_cache" value="true" />
        <entry key="hibernate.cache.use_query_cache" value="false" />
        <entry key="hibernate.generate_statistics" value="false" />
</util:map>

<bean id="entityManager" class="org.springframework.orm.jpa.support.SharedEntityManagerBean">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>

看看。基本上,您的缓存不是 Hibernate 二级缓存。您正在访问分离实体实例上的延迟未初始化关联,因此预计会抛出 LazyInitializationException

你可以试试 hibernate.enable_lazy_load_no_trans, but the recommended approach is to configure Hibernate second level cache 这样:

  • 缓存的实体会自动附加到加载它们的后续会话。
  • 缓存的数据在更改时会自动 refreshed/invalidated 在缓存中。
  • 考虑到事务语义,对缓存实例的更改是同步的。更改对其他 sessions/transactions 可见,所需级别为 cache/db consistency guarantees
  • 当从与它们有关联的其他实体导航到缓存实例时,缓存实例会自动从缓存中获取。

编辑

如果您仍然想为此目的使用 Spring 缓存,或者您的要求是这是一个足够的解决方案,请记住 Hibernate 托管实体不是线程安全的,因此您将必须存储和 return 分离实体 to/from 自定义缓存。此外,在分离之前,您需要初始化您希望在实体分离时访问的所有惰性关联。

要做到这一点,您可以:

  1. 使用 EntityManager.detach 显式分离托管实体。您还需要对关联实体进行分离或级联分离操作,并确保正确处理对来自其他托管实体的分离实体的引用。
  2. 或者,您可以在单独的事务中执行此操作,以确保所有内容都已分离,并且您不会从当前持久性上下文中的托管实体中引用分离的实体:

    @Override
    @Cacheable(value = "main")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Item findItemById(String id) {
        Item result = entityManager.find(Item.class, id);
        Hibernate.initialize(result.getAssociation1());
        Hibernate.initialize(result.getAssociation2());
        return result;
    }
    

    因为可能会发生Spring事务代理(拦截器)先于缓存代理执行(两者默认order值相同:transaction; cache),那么你会始终启动嵌套事务,无论是真正获取实体,还是仅 return 缓存实例。

    虽然我们可以得出结论,启动不需要的嵌套事务的性能损失很小,但这里的问题是当缓存中存在托管实例时,您会留下一小段时间 window。

    为避免这种情况,您可以更改默认订单值:

    <tx:annotation-driven order="200"/>
    <cache:annotation-driven order="100"/>
    

    这样缓存拦截器总是放在事务前。

    或者,为了避免对配置更改进行排序,您可以简单地将调用从 @Cacheable 方法委托给另一个 bean 上的 @Transactional(propagation = Propagation.REQUIRES_NEW) 方法。

问题是您正在缓存对延迟加载的对象的引用。在对象全部加载后缓存对象,或者根本不使用缓存。

以下是在缓存类别之前手动加载类别的方法:

Item item = entityManager.find(Item.class, id);
item.getParent().getProduct().getCategories();
return item;

还有一个更好的缓存策略是在应用程序的服务级别而不是 DAO 级别进行缓存,或者根本没有缓存。

您的问题是由以下事件引起的:

正在检索没有类别的项目,然后将其放入事务 1 的缓存中。在事务 2 中,您调用相同的方法并检索项目并尝试读取其类别。在那一刻,hibernate 尝试从与 Item 对象关联的事务 1 中读取类别,但事务 1 已经完成,因此它失败了。

您在代码片段中实现的是基于spring-缓存的自定义 缓存。对于您的实现,您需要处理缓存逐出,确保在您的对象图被缓存时它们被正确加载等。一旦它们被缓存并且加载它们的原始休眠会话关闭,它们就会变得超然,你再也无法驾驭不相关的懒惰联想。此外,您的自定义缓存解决方案在其当前状态下会缓存 实体图 ,这可能不是您想要的,因为该图的任何部分都可能在给定时间发生变化,而您的缓存解决方案需要观察该图表所有部分的变化才能正确处理驱逐。

您在问题中post的配置不是 Hibernate 二级缓存

管理缓存是一项复杂的工作,我不建议您自己动手,除非您完全确定自己在做什么(但这样您就不会问这个问题了)关于 Whosebug 的问题)。

让我解释一下当你得到 LazyInitializationException 时发生了什么:你用 @org.springframework.cache.annotation.Cacheable 标记了你的一个 dao 方法。在这种情况下会发生以下情况:

  1. Spring 将拦截器附加到您的托管 bean。拦截器会拦截 dao 方法调用,它会根据拦截器方法和实际方法参数(可以自定义)创建缓存键,并查找缓存以查看缓存中是否有该键的条目。如果有一个条目,它将 return 该条目而不实际调用您的方法。如果该键没有缓存条目,它将调用您的方法,序列化 return 值并将其存储在缓存中。
  2. 对于键没有缓存条目的情况,将调用您的方法。您的方法使用 spring 提供的单例代理到线程绑定 EntityManager,这是在 Spring 遇到第一个 @Transactional 方法调用时分配的。在您的例子中,这是另一个 spring 服务 bean 的 getContent(...) 方法。因此,您的方法使用 EntityManager.find() 加载实体。这将为您提供一个部分加载的实体图,其中包含尚未由持久性上下文加载的其他关联实体的未初始化代理和集合。
  3. 您的方法 returns 与部分加载的实体图和 spring 将立即为您序列化它并将其存储在缓存中。请注意,序列化部分加载的实体图将反序列化为部分加载的实体图。
  4. 第二次调用相同参数的@Cacheable标记的dao方法时,Spring会发现缓存中确实有该key对应的条目,会加载并反序列化入口。您的 dao 方法将不会被调用,因为它使用缓存的条目。现在你遇到了问题:当你存储在缓存中时,你的反序列化缓存实体图只被部分加载,一旦你触摸图的任何未初始化部分,你就会得到 LazyInitializationException。反序列化的实体将始终分离,因此即使原始 EntityManager 仍处于打开状态(不是),您仍然会遇到相同的异常。

现在的问题是:你能做些什么来避免 LazyInitializationException。好吧,我的建议是您忘记实现自定义缓存,只需配置 Hibernate 来为您执行缓存。我将在稍后讨论如何做到这一点。如果您想坚持使用您尝试实现的自定义缓存,您需要执行以下操作:

遍历您的整个代码库并找到您的 @Cacheable dao 方法的所有调用。遵循加载的实体图传递的所有可能代码路径,并标记客户端代码接触过的实体图的所有部分。现在回到您的 @Cacheable 方法并修改它,以便它加载和初始化实体图的所有可能被触及的部分。因为一旦你 return 它被序列化,然后反序列化,它总是处于分离状态,所以最好确保所有可能的图形路径都被正确加载。您应该已经感觉到这最终会变得多么不切实际。如果这仍然不能说服你不遵循这个方向,这里有另一个论点。

由于您加载了一个可能很大的数据库块,您将在给定时间实际加载和缓存该部分数据库时获得该部分的快照。现在,无论何时您使用这个大块数据库的缓存版本,都存在使用该数据的陈旧版本的风险。为了避免这种情况,您需要观察您刚刚缓存的大块数据库的当前版本中的任何更改,并将整个实体图从缓存中逐出。因此,您非常需要考虑哪些实体是实体图的一部分,并在这些实体发生更改时设置一些事件侦听器并逐出整个图。 None 这些问题存在于 Hibernate 二级缓存中。

现在回到我的建议:设置Hibernate二级缓存

Hibernate 二级缓存由 Hibernate 管理,您可以从 Hibernate 自动获得逐出管理。如果您启用了 Hibernate 二级缓存,Hibernate 将缓存重建您的实体所需的数据,并且,如果 - 当试图从数据库加载实体时 - 它发现它有一个您的实体的有效缓存条目,它将跳过访问数据库并从其缓存中重建您的实体。 (在您的自定义缓存解决方案中标记缓存实体图及其可能未获取的关联和未初始化的代理的区别)。当您更新实体时,它还将替换陈旧的缓存条目。它会做各种与管理缓存相关的事情,因此您不必担心它。

以下是启用 Hibernate 二级缓存的方法:除了您的配置外,还执行以下操作:

  1. 除了你已有的二级管理的hibernate属性,即

    <entry key="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
    <entry key="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider"/>
    <entry key="hibernate.cache.use_second_level_cache" value="true" />
    

    添加以下条目:

    <entry key="javax.persistence.sharedCache.mode" value="ENABLE_SELECTIVE" />
    

    或者,您可以向 persistence.xml 添加一个 shared-cache-mode 配置选项(因为您没有 post 它,我假设您不使用它,因此使用了以前的替代方案;但首选以下方案):

    <persistence-unit name="default">
        <!-- other configuration lines stripped -->
    
        <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
    
        <!-- other configuration lines stripped -->
    </persistence-unit>
    
  2. javax.persistence.@Cacheable 注释添加到您的 @Entity 类 您希望可缓存。
  3. 如果你想为 Hibernate 默认不缓存的集合值关联添加缓存,你可以为每个添加一个 @org.hibernate.annotations.Cache 注释(使用适当的缓存并发策略选择)此类合集:

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "product_category", joinColumns = { @JoinColumn(name = "PRODUCT_ID")
               }, inverseJoinColumns = { @JoinColumn(name = "CATEGORY_ID") })
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private List<Category> categories;
    

有关详细信息,请参阅 Hibernate 参考文档中的 Improving performance/The Second Level Cache

这是一篇关于该主题的内容丰富的文章:Pitfalls of the Hibernate Second-Level / Query Caches

我根据您的 posted 代码片段整理了一个 small project,您可以查看这些代码片段以了解 Hibernate 二级缓存的运行情况。

我在这个配置中使用了简单类型缓存,如下所示:

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

spring.jpa.open-in-view=true

spring.cache.type=simple