Система кешей
В ERP нет единого распределённого кеша. Основной механизм для редко изменяемых справочников — классы из пакета ru.bgcrm.cache: снимок данных хранится в памяти одного процесса JVM и лениво перестраивается через CacheHolder. Кроме него существуют локальный кеш вызовов Interactor, кеш HTTP-сессий и специализированные кеши отдельных модулей.
|
Кеши |
Виды кешей
| Механизм | Область жизни | Назначение |
|---|---|---|
|
Один процесс JVM, обычно до конца текущих суток или явного |
Справочники, пользователи, права, типы и очереди процессов, параметры и другие общие данные. |
|
Один proxy-объект Interactor, без TTL |
Повторное использование результата метода внутри короткой прикладной операции. Сбрасывается целиком методом с |
Менеджер HTTP-сессий |
Локально либо в Redis, в зависимости от конфигурации |
Состояние пользовательских сессий. Это отдельная подсистема, описанная в документации менеджера сессий. |
Специализированный кеш модуля |
Определяется реализацией |
Например, данные 1С, значения отдельных параметров, результаты внешнего API или поисковый индекс. |
ConcurrentHashMap, registry singleton или коллекция внутри ServerContext сами по себе не относятся к общей системе кешей. В частности, ServiceProvider и RepositoryProvider сохраняют созданные экземпляры только на время жизни контекста, а не кешируют бизнес-данные между запросами.
CacheHolder: стандартный жизненный цикл
Кеш наследуется от Cache<C> и реализует newInstance(). Статический CacheHolder<C> хранит фабрику и текущий снимок:
private static final CacheHolder<ExampleCache> holder =
new CacheHolder<>(new ExampleCache());
public static Example get(int id) {
return holder.getInstance().items.get(id);
}
public static void flush(Connection connection) {
holder.flush(connection);
}
При getInstance():
-
если снимка ещё нет или срок действия истёк, первый поток входит в синхронизированный блок;
-
newInstance()строит новый объект кеша; -
срок действия устанавливается на конец текущих суток по времени JVM;
-
готовый объект атомарно заменяет прежнюю ссылку и регистрируется в
CacheHolderRegistryпо полному имени класса.
Проверка срока выполняется только при обращении. Фонового обновления в CacheHolder нет: после полуночи снимок останется в памяти до следующего вызова getInstance().
Поле lastAccess исторически названо неточно: оно обновляется после возврата из newInstance(), а не при каждом чтении. Поэтому getLastAccess() показывает время последней завершившейся перестройки, а не активность пользователей кеша и не гарантированно успешную загрузку данных.
Во время перестроения читатели, уже получившие старый снимок, продолжают работать с ним. Новый снимок публикуется только после возврата из newInstance(). Для большого кеша это означает временное одновременное присутствие в heap старого и нового объектов; особенно это заметно при обновлении UserCache.
Явный сброс и транзакция
Обычный путь обновления — статический метод конкретного кеша, делегирующий в CacheHolder.flush(connection). Текущая реализация выполняет операции в таком порядке:
-
немедленно вызывает
commitдля переданного JDBC-соединения; -
синхронно строит и публикует новый снимок;
-
создаёт локальный
CacheResetEvent, если событие не отключено флагомneedSendEvent.
|
|
Сброс также находится на критическом пути запроса: вызывающий поток ждёт полной загрузки данных и выполнения обработчиков CacheResetEvent. Для тяжёлого кеша учитывайте длительность запроса, нагрузку на БД и пик памяти.
Некоторые старые реализации newInstance() перехватывают ошибку загрузки, только записывают её в журнал и возвращают частично заполненный объект. Поэтому отсутствие исключения из flush ещё не гарантирует полноту нового снимка. При изменении такого кеша предпочтительно сначала собрать все данные во временные локальные коллекции, а публиковать объект только после успешной загрузки.
Событие сброса и несколько инстансов
CacheResetEvent содержит полное имя класса кеша и обрабатывается обычным EventProcessor. Для декларативного listener доступна аннотация @OnCacheResetEvent. Событие означает, что конкретный holder уже перестроен в текущей JVM.
Это локальное событие:
-
для него нет встроенного
MessageBuilderшины сообщений; -
CacheHolderRegistryтолько хранит найденные holder и сам ничего не сбрасывает; -
стандартный
CacheHolderне подписывается на Redis, ActiveMQ или другую общую шину.
Проектный код может организовать доставку отдельно. Например, изменения типов процессов и параметров после локального сброса отправляют предметные сообщения в ActiveMQ. Получатель и гарантия обновления других инстансов не являются частью CacheHolder. В некоторых установках динамический обработчик подписан на общий топик сброса кешей; это конфигурация конкретного стенда, а не гарантия ядра.
Если данные должны быть согласованы на нескольких инстансах, явно определите:
-
сообщение и его получателя на каждом инстансе;
-
момент публикации относительно commit;
-
поведение при недоступном брокере и повторной доставке;
-
допустимое окно устаревания и резервный периодический refresh.
UserNewsCache показывает отдельный компромисс: настройка flush.news.everySeconds разрешает периодический полный сброс при чтении, чтобы несколько серверов на общей БД со временем увидели новые новости. Значение -1 отключает этот механизм. Точечный flushCache(connection, userIds) удаляет записи только в текущей JVM и тоже предварительно выполняет commit.
Подробнее о локальных событиях, MessageBus и транзакционных границах см. в документации системы событий.
Основные кеши ядра
| Кеш | Что хранит |
|---|---|
|
Пользователей, группы, наборы и дерево прав, членство, объединённые конфигурации. Настройка |
|
Типы процессов, дерево типов и статусы. |
|
Очереди и раскрытые наборы типов процессов; при построении зависит от |
|
Метаданные параметров, группы, списковые и древовидные значения. Справочники из произвольных таблиц имеют отдельный режим обновления, описанный ниже. |
|
Группы контрагентов. |
|
Виды назначений. |
|
Типы работ для табеля и графиков. |
|
Роли групп из настройки |
|
Рассчитанную информацию о непрочитанных и всплывающих новостях по пользователям. |
ObjectTypeCache в текущем исходном коде полностью закомментирован и не является работающим кешем.
Особенности ParameterCache
Обычные списковые значения загружаются вместе со снимком ParameterCache. Для параметра, чей справочник читается из отдельной таблицы, значения загружаются лениво при первом обращении и сохраняются в ConcurrentHashMap.
После первой загрузки TableChangeMonitor раз в 60 секунд выполняет SELECT COUNT(*) для таблицы и перечитывает значения, если число строк изменилось. У механизма есть ограничения:
-
изменение существующей строки без изменения количества строк не обнаруживается;
-
проверка и callback выполняются в одном legacy-потоке;
-
необработанная ошибка завершает поток монитора;
-
при полном сбросе
ParameterCacheудаляются все подписки монитора и специализированные param-cache.
Таблицы из настройки parameter-cache.directory.ignore не кешируются: значения читаются из БД при каждом обращении. Используйте это как осознанный выбор между актуальностью и нагрузкой, а не как универсальное исправление инвалидации.
Специализированные holder
OneCCacheHolder и ParamCacheHolder переопределяют получение просроченного снимка. Они пытаются захватить ReentrantLock, запускают загрузку через CompletableFuture и ограничивают ожидание:
| Настройка | По умолчанию | Назначение |
|---|---|---|
|
|
Ожидание блокировки при обновлении кеша 1С. |
|
|
Максимальное ожидание загрузки кеша 1С. |
|
|
Ожидание блокировки специализированного кеша параметров. |
|
|
Максимальное ожидание его загрузки. |
При нулевом времени ожидания параллельный запрос не ждёт освобождения блокировки и получает ошибку с предложением повторить запрос. Timeout прерывает ожидание результата, но не гарантирует остановку уже запущенной фоновой загрузки.
Кеширование в Interactor
CacheProxyHandler хранит результаты методов, помеченных @EnableCache. Ключ строится из метода и аргументов; методы, возвращающие void, не кешируются. Перед вызовом метода с @DropCache очищается весь кеш этого proxy.
У механизма нет TTL, ограничения размера и межинстансной инвалидации. Используйте его только для повторных чтений в рамках короткой операции. Каждый метод, изменяющий данные, от которых зависят кешированные getters, должен иметь @DropCache. Примеры и ограничения наследования аннотаций приведены в быстром старте Interactor и рекомендациях.
Потокобезопасность и изменяемые значения
volatile и синхронизация в CacheHolder обеспечивают публикацию ссылки на снимок, но не делают его содержимое неизменяемым. Многие legacy-getters возвращают исходные Map, List, Set и сами доменные объекты.
-
Считайте полученные из общего кеша данные read-only.
-
Не добавляйте элементы в возвращённую коллекцию и не меняйте поля объекта.
-
Для локальной сортировки или фильтрации сначала создайте копию.
-
Внутренний lazy-кеш, который изменяется после публикации снимка, храните в конкурентной структуре и отдельно проверяйте атомарность вычисления.
Иначе один запрос может незаметно изменить данные для всех потоков текущего инстанса.
Добавление или изменение кеша
Перед созданием глобального кеша проверьте, нельзя ли ограничить данные временем жизни ServerContext или одного Interactor. Для нового process-wide кеша:
-
Определите источник истины, допустимый срок устаревания, размер и максимальное время загрузки.
-
Опишите все операции изменения и точку инвалидации для каждой из них.
-
Отдельно спроектируйте синхронизацию нескольких инстансов; локального
CacheResetEventнедостаточно. -
Загружайте новый снимок целиком и не публикуйте частичное состояние при ошибке.
-
Не меняйте переданное вызывающим кодом соединение неожиданно. Учитывайте, что наследование текущего
CacheHolder.flushозначает немедленный commit. -
Не возвращайте изменяемые внутренние коллекции из нового API.
-
Для нового backend-кода используйте Kotlin и размещайте класс с суффиксом
Cacheв пакете..cache...
Минимальные тесты должны проверять:
-
холодную первую загрузку и повторное чтение без запроса к источнику;
-
одновременную первую загрузку несколькими потоками;
-
явную инвалидацию и отсутствие старых данных после неё;
-
ошибку загрузки без публикации неполного снимка;
-
границы срока действия;
-
изменение данных на другом инстансе либо явно зафиксированное допустимое окно рассогласования;
-
пик памяти при перестроении большого снимка.
Диагностика
У базового CacheHolder нет общего MBean, hit/miss-счётчиков и стандартной метрики длительности загрузки. CacheHolderRegistry.get(полноеИмяКласса) возвращает уже зарегистрированный holder, но сам registry не предоставляет список и не управляет кешами.
При расследовании проверяйте:
-
время и ошибку последней загрузки в журнале конкретного кеша;
-
был ли
flushвыполнен на том же инстансе, который обслуживает проблемный запрос; -
не завершился ли
TableChangeMonitor, если устарели directory-значения параметров; -
длительность SQL и внешний источник во время холодной загрузки;
-
heap до, во время и после сброса тяжёлого кеша;
-
настройки и потребителей брокера, если заявлена межинстансная инвалидация.
Специализированные кеши и индексы могут иметь собственные метрики. Например, MBean ElasticCache описан отдельно в каталоге MBean; он не показывает состояние ru.bgcrm.cache.CacheHolder.