Избегание множественных репопуляций одной и той же области кэша (из-за concurrency)
У меня есть сайт с высоким трафиком, и я использую спящий режим. Я также использую ehcache для кэширования некоторых объектов и запросов, необходимых для создания страниц.
Проблема заключается в "промахах параллельного кэша", и длинное объяснение заключается в том, что при загрузке приложения и регионах кеша каждый регион кеша заполняется много раз (а не только один раз) разными потоками, потому что сайт попадает многими пользователями одновременно. Кроме того, когда некоторая область кеша делает недействительными, она многократно заселяется по той же причине.
Как я могу избежать этого?
Мне удалось конвертировать 1 сущность и 1 кэш запросов в BlockingCache, предоставив мою собственную реализацию hibernate.cache.provider_class, но семантика BlockingCache не кажется Работа. Даже худшие иногда блокировки BlockingCache (блоки) и приложение зависают полностью. Дамп потока показывает, что обработка блокируется в мьютексе 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" по-прежнему многократно заполняется теми же данными из-за concurrency. Я подтверждаю это, подчеркивая сайт с помощью JMeter с 10 потоками. Я бы ожидал, что 9 потоков будут блокироваться до первого, который запросил данные из "foo", чтобы завершить задание (выполнить запросы, сохранить данные в кеше), а затем получить данные непосредственно из кеша.
Изменить 3. Еще одно объяснение этой проблемы можно увидеть на https://forum.hibernate.org/viewtopic.php?f=1&t=964391&start=0, но без определенного ответа.
Ответы
Ответ 1
Самым большим улучшением этой проблемы является то, что ehcache (начиная с версии 2.1) поддерживает политику кэширования transactional
. Это значительно смягчает проблемы, описанные в этой проблеме.
Чтобы сделать еще один шаг (блокировать потоки при доступе к одному и тому же регистру кеша запросов), нужно будет реализовать QueryTranslatorFactory для возврата пользовательских (расширенных) QueryTranslatorImpl экземпляров, которые будут проверять запрос и параметры и блокировать по мере необходимости в методе списка. Это, конечно, относится к конкретному варианту использования кеша запросов с использованием hql, который извлекает многие объекты.
Ответ 2
Я не совсем уверен, но:
Позволяет одновременно просматривать доступ к элементы уже находятся в кеше. Если элемент имеет значение null, другие блокировать до тех пор, пока элемент с тем же ключ помещается в кеш.
Не означает ли это, что Hibernate будет ждать, пока какой-либо другой поток помещает объект в кеш? Что вы наблюдаете, правильно?
Hib и кеш работают следующим образом:
- Hib получает запрос для объекта
- Hib проверяет, находится ли объект в кеше - cache.get()
- Нет? Hib загружает объект из базы данных и помещает в кеш - cache.put()
Итак, если объект не находится в кеше (не помещается в него в предыдущую операцию обновления), Hib будет ждать 1) навсегда.
Я думаю, вам нужен вариант кеша, где поток ожидает только объект на короткое время. Например. 100мс. Если объект не прибыл, поток должен получить нуль (и, следовательно, Hibernate будет загружать объект из базы данных и помещать его в кеш).
Собственно, лучшей логикой было бы следующее:
- Убедитесь, что другой поток запрашивает один и тот же объект
- Если true, подождите долго (500 мс), чтобы объект прибыл
- Если не верно, немедленно возвращайте 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);
}
}
Так странно, что это не сработает для вас. Скажите мне что-нибудь: в вашем коде это место:
Ehcache cache = manager.getEhcache("foo");
синхронизируется? Если одновременно возникает несколько запросов, будет ли только один экземпляр кеша?