Как кэшировать информацию в DAO потокобезопасным способом

Мне часто нужно внедрять DAO для некоторых справочных данных, которые меняются не очень часто. Я иногда кеширую это в поле сбора данных в DAO - так, чтобы оно загружалось только один раз и явно обновлялось при необходимости.

Однако это приводит ко многим проблемам параллелизма - что если другой поток попытается получить доступ к данным во время загрузки или обновления.

Очевидно, что это можно сделать, синхронизировав как получатели, так и установщики данных, но для большого веб-приложения это довольно непроизводительно.

Я включил тривиальный некорректный пример того, что мне нужно как соломенный. Пожалуйста, предложите альтернативные способы реализации этого.

public class LocationDAOImpl implements LocationDAO {

private List<Location> locations = null;

public List<Location> getAllLocations() {
    if(locations == null) {
        loadAllLocations();
    }
    return locations;
}

Для получения дополнительной информации я использую Hibernate и Spring, но это требование будет применяться ко многим технологиям.

Некоторые дальнейшие мысли:

Разве это не должно быть обработано в коде вообще - вместо этого пусть ehcache или подобное обрабатывает это? Есть ли общий шаблон для этого, что я скучаю? Очевидно, что этого можно достичь многими способами, но я никогда не находил шаблон, который был бы простым и понятным.

Заранее спасибо!

6 ответов

Решение

Если вы просто хотите быстро внедрить собственное решение для кэширования, взгляните на эту статью о JavaSpecialist, которая является рецензией на книгу Брайана Гетца " Практический параллелизм Java".

В нем рассказывается о реализации базового потокового кеша с использованием FutureTask и ConcurrentHashMap.

То, как это делается, гарантирует, что только один параллельный поток инициирует длительные вычисления (в вашем случае ваша база данных вызывает в вашей DAO).

Вам нужно изменить это решение, чтобы добавить срок действия кэша, если вам это нужно.

Другая мысль о кешировании - это сборка мусора. Без использования WeakHashMap для вашего кэша, GC не сможет освободить память, используемую кэшем, если это необходимо. Если вы кэшируете редко используемые данные (но данные, которые все еще стоили кэшировать, поскольку их трудно вычислить), вы можете помочь сборщику мусора при нехватке памяти с помощью WeakHashMap.

Самый простой и безопасный способ - включить библиотеку ehcache в ваш проект и использовать ее для настройки кэша. Эти люди решили все проблемы, с которыми вы можете столкнуться, и сделали библиотеку максимально быстрой.

В ситуациях, когда я свернул свой собственный кэш справочных данных, я обычно использовал ReadWriteLock чтобы уменьшить конфликт нитей. Каждый из моих методов доступа принимает форму:

public PersistedUser getUser(String userName) throws MissingReferenceDataException {
    PersistedUser ret;

    rwLock.readLock().lock();
    try {
        ret = usersByName.get(userName);

        if (ret == null) {
            throw new MissingReferenceDataException(String.format("Invalid user name: %s.", userName));
        }
    } finally {
        rwLock.readLock().unlock();
    }

    return ret;
}

Единственный способ снять блокировку записи refresh(), который я обычно выставляю через MBean:

public void refresh() {
    logger.info("Refreshing reference data.");
    rwLock.writeLock().lock();
    try {
        usersById.clear();
        usersByName.clear();

        // Refresh data from underlying data source.

    } finally {
        rwLock.writeLock().unlock();
    }
}

Кстати, я решил реализовать свой собственный кеш, потому что:

  • Мои коллекции справочных данных небольшие, поэтому я всегда могу сохранить их все в памяти.
  • Мое приложение должно быть простым / быстрым; Я хочу как можно меньше зависимостей от внешних библиотек.
  • Данные редко обновляются, и когда это происходит, вызов refresh() выполняется довольно быстро. Поэтому я с энтузиазмом инициализирую свои кеши (в отличие от вашего примера с соломенным человеком), что означает, что средства доступа никогда не должны снимать блокировку записи.

Если ваши справочные данные неизменны, кэш второго уровня hibernate может быть разумным решением.

Очевидно, что это можно сделать, синхронизировав как получатели, так и установщики данных, но для большого веб-приложения это довольно непроизводительно.

Я включил тривиальный некорректный пример того, что мне нужно как соломенный. Пожалуйста, предложите альтернативные способы реализации этого.

Хотя это может быть несколько верно, вы должны принять к сведению, что приведенный вами пример кода, безусловно, должен быть синхронизирован, чтобы избежать проблем параллелизма при отложенной загрузке locations, Если этот метод доступа не синхронизирован, то у вас будет:

  • Несколько потоков обращаются к loadAllLocations() метод в то же время
  • Некоторые темы могут войти loadAllLocations() даже после того, как другой поток завершил метод и присвоил результат locations - в модели памяти Java нет гарантии, что другие потоки увидят изменения в переменной без синхронизации.

Будьте осторожны при использовании отложенной загрузки / инициализации, это похоже на простое повышение производительности, но может вызвать множество неприятных проблем с многопоточностью.

Я думаю, что лучше не делать это самостоятельно, потому что правильно понять это очень сложно. Использование EhCache или OSCache с Hibernate и Spring - гораздо лучшая идея.

Кроме того, это делает ваши DAO полными, что может быть проблематично. У вас не должно быть никакого состояния, кроме объектов подключения, фабрики или шаблона, которыми управляет Spring.

ОБНОВЛЕНИЕ: Если ваши справочные данные не слишком велики и действительно никогда не меняются, возможно, альтернативным вариантом будет создание перечислений и полное исключение из базы данных. Нет кеша, нет спящего режима, нет проблем. Возможно, стоит рассмотреть точку зрения oxbow_lakes: возможно, это может быть очень простая система.

Другие вопросы по тегам