避免同一缓存区域的多次重新填充(由于并发)

che*_*vim 13 java concurrency caching hibernate ehcache

我有一个高流量的网站,我使用休眠.我还使用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));
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

Vla*_*hev 5

我不太确定,但是:

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

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

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

  1. Hib获取对象的请求
  2. Hib检查对象是否在缓存中 - cache.get()
  3. 没有?Hib从DB加载对象并放入缓存 - cache.put()

因此,如果对象不在缓存中(未通过某些先前的更新操作放置在那里),Hib将永远等待1).

我认为你需要一个缓存变体,其中线程只在短时间内等待一个对象.例如100ms.如果对象未到达,则线程应该为null(因此Hibernate将从DB加载对象并放入缓存).

实际上,更好的逻辑是:

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

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

如果BlockingCache不支持此行为,则需要自己实现缓存.我在过去做过,这并不难 - 主要的方法是get()和put()(尽管API显然已经增长了).

UPDATE

实际上,我刚刚阅读了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);
    }
}
Run Code Online (Sandbox Code Playgroud)

所以它有点奇怪它不适合你.告诉我一些事情:在你的代码中这个地方:

Ehcache cache = manager.getEhcache("foo");
Run Code Online (Sandbox Code Playgroud)

它同步了吗?如果多个请求同时出现,那么只有一个缓存实例吗?


che*_*vim 1

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

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