13

我有一个高流量的网站,我使用休眠。我还使用 ehcache 来缓存生成页面所需的一些实体和查询。

问题是“并行缓存未命中”,长的解​​释是,当应用程序启动并且缓存区域很冷时,每个缓存区域被不同的线程多次填充(而不是一次),因为该站点被许多用户访问同时。此外,当某些缓存区域失效时,由于相同的原因,它会被多次重新填充。我怎样才能避免这种情况?

通过向 hibernate.cache.provider_class 提供我自己的实现,我设法将 1 个实体和 1 个查询缓存转换为 BlockingCache,但 BlockingCache 的语义似乎不起作用。更糟糕的是,有时 BlockingCache 死锁(块)并且应用程序完全挂起。线程转储显示在执行 get 操作时 BlockingCache 的互斥体上的处理被阻塞。

那么,问题来了,Hibernate 支持这种使用方式吗?

如果没有,您如何在生产中解决这个问题?

编辑hibernate.cache.provider_class指向我的自定义缓存提供程序,它是来自SingletonEhCacheProvider的复制粘贴,并且在 start() 方法的末尾(在第 136 行之后)我这样做:

Ehcache cache = manager.getEhcache("foo");
if (!(cache instanceof BlockingCache)) {
    manager.replaceCacheWithDecoratedCache(cache, new BlockingCache(cache));
}

这样在初始化时,在其他人接触名为“foo”的缓存之前,我用 BlockingCache 装饰它。“foo”是查询缓存,“bar”(相同的代码但省略)是 pojo 的实体缓存。

编辑2:“似乎不起作用”意味着最初的问题仍然存在。由于并发性,缓存“foo”仍然被多次重新填充相同的数据。我通过使用具有 10 个线程的 JMeter 向站点施加压力来验证这一点。我希望这 9 个线程阻塞,直到第一个从“foo”请求数据以完成它的工作(执行查询,将数据存储在缓存中),然后直接从缓存中获取数据。

编辑 3:这个问题的另一种解释可以在https://forum.hibernate.org/viewtopic.php?f=1&t=964391&start=0但没有明确的答案。

4

2 回答 2

5

我不太确定,但是:

它允许对缓存中已经存在的元素进行并发读取访问。如果元素为空,则其他读取将阻塞,直到将具有相同键的元素放入缓存。

这是否意味着 Hibernate 会等到其他线程将对象放入缓存中?这就是你观察到的,对吧?

Hib 和缓存的工作方式如下:

  1. Hib 收到对对象的请求
  2. Hib 检查对象是否在缓存中——cache.get()
  3. 不?Hib 从数据库加载对象并放入缓存——cache.put()

因此,如果对象不在缓存中(之前的一些更新操作未将其放置在那里),Hib 将永远等待 1)。

我认为您需要一个缓存变体,其中线程仅在短时间内等待对象。例如 100 毫秒。如果对象没有到达,线程应该为 null(因此 Hibernate 将从 DB 加载对象并放入缓存中)。

实际上,更好的逻辑是:

  1. 检查另一个线程是否在请求同一个对象
  2. 如果为真,请等待很长时间(500 毫秒)让对象到达
  3. 如果不为真,立即返回 null

(我们不能永远等待 2,因为线程可能无法将对象放入缓存 - 由于异常)。

如果 BlockingCache 不支持这种行为,你需要自己实现一个缓存。我过去做过,这并不难——主要方法是 get() 和 put()(尽管 API 显然从那以后有所增长)。

更新

实际上,我只是阅读了 BlockingCache 的来源。它正如我所说的那样——锁定并等待超时。因此,您无需执行任何操作,只需使用它...

public Element get(final Object key) throws RuntimeException, LockTimeoutException {
    Sync lock = getLockForKey(key);
    Element element;
        acquiredLockForKey(key, lock, LockType.WRITE);
        element = cache.get(key);
        if (element != null) {
            lock.unlock(LockType.WRITE);
        }
    return element;
}

public void put(Element element) {
    if (element == null) {
        return;
    }
    Object key = element.getObjectKey();
    Object value = element.getObjectValue();

    getLockForKey(key).lock(LockType.WRITE);
    try {
        if (value != null) {
            cache.put(element);
        } else {
            cache.remove(key);
        }
    } finally {
        getLockForKey(key).unlock(LockType.WRITE);
    }
}

所以这有点奇怪,它对你不起作用。告诉我一些事情:在你的代码中这个地方:

Ehcache cache = manager.getEhcache("foo");

是同步的吗?如果多个请求同时到来,会不会只有一个缓存实例?

于 2009-10-05T17:31:27.433 回答
1

在这个问题上最大的改进是 ehcache 现在(从 2.1 开始)支持transactional休眠缓存策略。这极大地缓解了本期中描述的问题。

为了更进一步(在访问同一查询缓存区域时锁定线程),需要实现QueryTranslatorFactory以返回自定义(扩展)QueryTranslatorImpl实例,该实例将检查查询和参数并在列表方法中根据需要进行阻塞。这当然涉及使用 hql 获取许多实体的查询缓存的特定用例。

于 2011-03-27T09:35:16.367 回答