多个并发读取和一个写入导致了由于ehcache而引起的ObjectNotFoundException问题

19

我使用 Hibernate 3.6.8,ehcache 2.4.5(也尝试了最新的 2.8.0 版本),jvm 1.6.0_22 在一个高流量站点上,有时我会遇到

ObjectNotFoundException: No row with the given identifier exists: [com.example.Foo#123]`

当通过最简单的代码创建一个新的 Foo(在本例中为 id 123)时。

Foo foo = new Foo();
session.save(foo);
这个高流量网站的所有页面中,我获取到的所有Foo都是这样的原因:

在该高流量网站的所有页面中,我以以下方式获取所有的Foo:

session.createQuery("from Foo").setCacheable(true).list();

存储Foo的表格包含1000行,实体存储在ehcache缓存中:

<class-cache class="com.example.Foo" usage="read-write" />

我Hibernate配置中可能相关的其他部分包括:

<property name="connection.url">jdbc:mysql://localhost:3306/example?characterEncoding=UTF-8</property>
<property name="connection.driver_class">com.mysql.jdbc.Driver</property>

<property name="connection.provider_class">org.hibernate.connection.C3P0ConnectionProvider</property>
<property name="hibernate.c3p0.acquire_increment">1</property>
<property name="hibernate.c3p0.idle_test_period">60</property>
<property name="hibernate.c3p0.min_size">10</property>
<property name="hibernate.c3p0.max_size">20</property>
<property name="hibernate.c3p0.max_statements">0</property>
<property name="hibernate.c3p0.timeout">0</property>
<property name="hibernate.c3p0.acquireRetryAttempts">1</property>
<property name="hibernate.c3p0.acquireRetryDelay">1</property>

<property name="hibernate.show_sql">true</property>
<property name="hibernate.use_sql_comments">true</property>

<property name="hibernate.transaction.factory_class">org.hibernate.transaction.JDBCTransactionFactory</property>
<property name="hibernate.current_session_context_class">thread</property>
<property name="hibernate.jdbc.use_scrollable_resultset">true</property>

<property name="hibernate.cache.provider_class">net.sf.ehcache.hibernate.SingletonEhCacheProvider</property>
<property name="net.sf.ehcache.configurationResourceName">/ehcache.xml</property>
<property name="hibernate.cache.use_query_cache">true</property>

这个错误会出现一次,然后就消失了。我怀疑ehcache查询缓存已经使用新实体ID(123)更新,但实体缓存还没有更新该实体的内容。我使用JMeter在本地很容易复制这个问题。

有任何解决这个问题的想法吗?

在创建Foo时,会抛出 ObjectNotFoundException 错误一次。另外,如果我删除一个Foo实例,则每次执行.list()时都会不断地(永久)获得ObjectNotFoundException。可以在http://pastebin.com/raw.php?i=dp3HBgDB看到堆栈跟踪。


请问您能否发布您的ehcache.xml文件? - Ashish Jagtap
@AshishJagtap:http://pastebin.com/raw.php?i=LdgWiLE0 - cherouvim
你已经确认在访问缓存而非数据库时实际上抛出了异常吗?可能是通过读取缓存间接触发的? - Ralf
Foo有任何外键关系吗? - Durandal
@MagicMan:是的,Foo表中有外键,但不一定与我创建或删除以重现问题的条目相关。 - cherouvim
显示剩余2条评论
3个回答

12

读写策略不能保证数据库和缓存之间的事务性,因此我认为当写操作发生时会出现以下情况:

  • 新对象Foo被附加到写入的Hibernate会话中。

  • Hibernate会话插入了一个延迟加载代理到二级缓存中。

  • 该同一会话将新的Foo插入到数据库中,但插入需要一定的时间用于构建、刷新和提交。

  • 同时,另一个请求命中了缓存中的代理以加载所有Foo。它在缓存中找到了延迟加载代理(见堆栈跟踪DefaultLoadEventListener.proxyOrLoad()),并决定加载对象(DefaultLoadEventListener.load())。

  • 这将触发一个Hibernate load(),用于加载由写线程尚未插入到数据库中的Foo。

  • 在数据库中找不到具有该Id的Foo,因此抛出ObjectNotFoundException异常。

要确认这一点,请在IDE上设置异常断点,以查看在抛出异常的时刻对象尚未插入到数据库中。解决这个问题的一种方法是使用事务性策略。


1
我曾经使用过transactional ehcache,但很快就回退了,因为出现了各种不一致的情况。经过一些调查,发现原因是我的数据库事务隔离级别是REPEATABLE_READ,而应该是READ_COMMITTED才能正常工作。 - cherouvim
你的解释很有道理。 - cherouvim
4
解释看起来很有可能,但我有一个反对意见。由于我们已经有了一个ID,因此Foo已经被插入到数据库中(并且可能已经提交)。然而,实体缓存尚未更新,导致另一个线程执行按id查询。该查询不返回任何内容,因为数据库隔离级别为REPEATABLE_READ。因此,即使另一个线程实际上已经提交了插入操作,它也不会得到任何结果。 - Argyro Kazaki

3
为了防止实体被删除后list()完全无法工作,我在更高的级别上捕获了ObjectNotFoundException。当这种情况发生时,我会执行以下操作:
session.getSessionFactory().getCache().evictCollectionRegions();
session.getSessionFactory().getCache().evictDefaultQueryRegion();
session.getSessionFactory().getCache().evictQueryRegions();  

清除第二级缓存可以使网站重新工作。当然,这并不能防止问题再次发生,但它解决了整个网站停机的问题。

-1

摘自缓存配置文档:

The following attributes and elements are optional.

timeToIdleSeconds:
Sets the time to idle for an element before it expires.
i.e. The maximum amount of time between accesses before an element expires
Is only used if the element is not eternal.
Optional attribute. A value of 0 means that an Element can idle for infinity.
The default value is 0.

timeToLiveSeconds:
Sets the time to live for an element before it expires.
i.e. The maximum time between creation time and when an element expires.
Is only used if the element is not eternal.
Optional attribute. A value of 0 means that and Element can live for infinity.
The default value is 0.

或者您也可以选择其他选项:

数据新鲜度与过期


我的问题不在于缓存过期,而在于并非所有的缓存区域(查询缓存和实体缓存)都能迅速更新有关某些内容是否已放置或移除的信息。 - cherouvim

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