Кэширование результатов методов желудей ОСени аннотациями &Кэшируемый и &СбрасываетКэш: поставщики в памяти и в файлах, значения и потоки, общий бюджет памяти в байтах, настройки каждого кэша по имени, вытеснение SLRU, срок жизни записей
Пакет лежит в основном пуле хаба, а opm знает этот хаб по умолчанию: короткой формы достаточно.
opm install autumn-cacheСкачанный файл ставится командой opm install -f <файл> — сеть для этого уже не нужна.
Описание
autumn-cache
Кэширование результатов методов желудей ОСени
аннотациями — аналог @Cacheable / @CacheEvict из Spring. Кэши в памяти и в файлах,
значения и потоки, настройки у каждого кэша свои, общий бюджет памяти в байтах, срок жизни
записей, счётчики.
Установка
opm install autumn-cache
Быстрый старт
Желудь, чьи чтения кэшируются:
// Классы/СправочникКурсов.os
Перем Источник;
&Кэшируемый("курсы")
Функция Курс(&КлючКэша Валюта, &КлючКэша НаДату) Экспорт
// Выполняется только на промахе; повтор с теми же Валюта и НаДату берёт значение из кэша.
Возврат Источник.ЗагрузитьКурс(Валюта, НаДату);
КонецФункции
&СбрасываетКэш("курсы")
Процедура ЗаписатьКурс(&КлючКэша Валюта, &КлючКэша НаДату, Курс) Экспорт
// После успешного вызова запись с ключом (Валюта, НаДату) удаляется из кэша.
Источник.ЗаписатьКурс(Валюта, НаДату, Курс);
КонецПроцедуры
&Желудь
Процедура ПриСозданииОбъекта(&Пластилин ИсточникКурсов)
Источник = ИсточникКурсов;
КонецПроцедуры
Приложение подключает библиотеку рядом с ОСенью, настройки — в autumn-properties.json:
#Использовать autumn
#Использовать autumn-cache
#Использовать "."
Поделка = Новый Поделка();
Поделка.ЗапуститьПриложение();
{
"cache": {
"курсы": { "Предел": 5000, "СрокЖизни": 300 }
}
}
Как организовать кэши
Что кэшировать
Кэшируйте чистые чтения: результат зависит только от параметров и меняется редко —
настройки, справочники, права, данные из базы или внешней службы. Аннотации работают на
экспортных методах желудей, которые создаёт ОСень, и перехватывают только вызов снаружи:
вызов метода из того же желудя (напрямую или через ЭтотОбъект) идёт мимо кэша. Поэтому
кэш ставится на границе — на публичных методах службы или хранилища, которые зовут другие
желуди.
Процедуры не кэшируются: &Кэшируемый на процедуре и аннотации кэша на неэкспортном методе —
ошибка при сборке желудя.
Ключ кэша
Ключ записи — значения параметров, помеченных &КлючКэша, в порядке объявления. Если в методе
нет ни одного &КлючКэша, ключом становятся все параметры.
Помечайте только то, от чего зависит ответ. Контекст запроса, журнал, службы, флаги отладки в ключ не входят — иначе одинаковые чтения разойдутся по разным записям, а объект в ключе вообще недопустим:
Перем ХранилищеПрав;
&Кэшируемый("права")
Функция Права(&КлючКэша Пользователь, &КлючКэша Раздел, Контекст) Экспорт
Возврат ХранилищеПрав.Права(Пользователь, Раздел);
КонецФункции
В ключ входят только значения типов Строка, Число, Дата, Булево,
УникальныйИдентификатор и Неопределено. Структура, Массив, сущность и любой другой
объект в ключе — исключение при вызове, метод при этом не выполняется. Если нужен объект —
передайте в ключ его идентификатор.
Как сравниваются ключи:
| Значения | Ключи |
|---|---|
1, 1.0, 2/2 |
один ключ: число без хвостовых нулей |
"1" и 1 |
разные: тип входит в ключ |
"Иванов", "иванов", " Иванов " |
разные: строка берётся как есть |
"" и Неопределено |
разные |
("ab", "c") и ("a", "bc") |
разные: части ключа не склеиваются |
Регистр и пробелы нормализуйте до вызова — в желуде, который зовёт кэшируемый метод:
Перем СлужбаПрав;
Функция ПраваНаОтчёты(Логин, Контекст)
Возврат СлужбаПрав.Права(НРег(СокрЛП(Логин)), "отчёты", Контекст);
КонецФункции
Метод без параметров — одна запись на весь кэш. Необязательный параметр со значением
Неопределено — полноправная часть ключа.
Одно имя кэша — одно пространство ключей. Имя метода в ключ не входит: два метода с одним именем кэша и одинаковыми значениями ключа читают одну запись. На этом держится сброс из другого метода, но это и ловушка:
Перем Пользователи;
// У каждого способа поиска свой кэш. В одном кэше идентификатор "42" и логин "42" дали бы
// один ключ, и методы прочли бы ответы друг друга.
&Кэшируемый("пользователи")
Функция ПоИдентификатору(&КлючКэша Идентификатор) Экспорт
Возврат Пользователи.ПоИдентификатору(Идентификатор);
КонецФункции
&Кэшируемый("пользователи-по-логину")
Функция ПоЛогину(&КлючКэша Логин) Экспорт
Возврат Пользователи.ПоЛогину(Логин);
КонецФункции
Делите одно имя кэша только между методами, которые возвращают одно и то же по одинаковому ключу: читателем и писателем одной записи.
Сброс
&СбрасываетКэш("<имя>") удаляет запись после успешного вызова метода: если метод бросил
исключение, кэш не трогается. Ключ сброса собирается по тем же правилам, что у читателя, и должен
совпасть с ним по составу, типам и порядку частей:
Перем ХранилищеПрав;
// Читатель
&Кэшируемый("права")
Функция Права(&КлючКэша Пользователь, &КлючКэша Раздел, Контекст) Экспорт
Возврат ХранилищеПрав.Права(Пользователь, Раздел);
КонецФункции
// Писатель: те же ключевые параметры в том же порядке — сбрасывается ровно эта запись.
&СбрасываетКэш("права")
Процедура ВыдатьПраво(&КлючКэша Пользователь, &КлючКэша Раздел, Право) Экспорт
ХранилищеПрав.Записать(Пользователь, Раздел, Право);
КонецПроцедуры
// Писатель без ключа читателя: сбросить кэш целиком.
&СбрасываетКэш(Значение = "права", ВсеЗаписи = Истина)
Процедура ЗагрузитьРоли(Роли) Экспорт
ХранилищеПрав.ЗагрузитьРоли(Роли);
КонецПроцедуры
Писатель, который принимает сущность, без &КлючКэша соберёт ключ из всех параметров, получит
объект в ключе и упадёт, не выполнившись. Пометьте параметр с ключом, сбросьте весь кэш через
ВсеЗаписи или удалите запись программно после сохранения:
Перем Хранилище;
Перем КэшПользователей;
Процедура Сохранить(Сущность) Экспорт
Хранилище.Сохранить(Сущность);
КэшПользователей.Удалить(Сущность.Идентификатор);
КонецПроцедуры
&Желудь
Процедура ПриСозданииОбъекта(&Пластилин("ХранилищеСущностейПользователь") ХранилищеПользователей,
&Пластилин МенеджерКэшей)
Хранилище = ХранилищеПользователей;
КэшПользователей = МенеджерКэшей.Кэш("пользователи");
КонецПроцедуры
На одном методе может стоять несколько &СбрасываетКэш — по одному на кэш.
Сброс действует только в своём процессе. Если приложение запущено в нескольких процессах, другие
процессы отдают старое значение, пока запись не истечёт: задавайте таким кэшам СрокЖизни.
Что кладётся в кэш
Исключения, Неопределено и NULL не кэшируются — следующий вызов снова выполнит метод.
Этим удобно не запоминать временную неудачу: вернули Неопределено — повторите в следующий раз.
Если же «записи нет» — тоже ответ, который стоит помнить, верните для него значение, например
Ложь.
Кэш в памяти с общим бюджетом (cache.ПределПамяти, см. ниже) принимает только неизменяемые
значения: Строка, Число, Дата, Булево, УникальныйИдентификатор, ДвоичныеДанные и
ФиксированнаяСтруктура, ФиксированноеСоответствие, ФиксированныйМассив из них. Он отдаёт
вызывающему ту же ссылку, что хранит, и изменяемое значение испортил бы первый же получатель.
Сущность, Структура, Массив и ТаблицаЗначений не кладутся: метод работает без кэша и пишет
ошибку в журнал. Поток кладётся — кэш вычитывает его в ДвоичныеДанные, см. Потоки.
Из базы возвращайте снимок для чтения:
Перем Хранилище;
&Кэшируемый("пользователи")
Функция Пользователь(&КлючКэша Идентификатор) Экспорт
Сущность = Хранилище.ПолучитьОдно(Идентификатор);
Если Сущность = Неопределено Тогда
Возврат Ложь;
КонецЕсли;
Возврат Новый ФиксированнаяСтруктура("Идентификатор, Логин, Имя",
Сущность.Идентификатор, Сущность.Логин, Сущность.Имя);
КонецФункции
&Желудь
Процедура ПриСозданииОбъекта(&Пластилин("ХранилищеСущностейПользователь") ХранилищеПользователей)
Хранилище = ХранилищеПользователей;
КонецПроцедуры
Файловый кэш хранит только ДвоичныеДанные и потоки. Метод, который отдаёт файл или большой
ответ, может вернуть поток — см. Потоки.
Настройки
Каждый кэш настраивается по своему имени ключами cache.<имя>.*:
| Ключ | Значение | По умолчанию |
|---|---|---|
Поставщик |
память или файлы |
память |
Предел |
записей для памяти, байт для файлов | общий бюджет, если задан cache.ПределПамяти, иначе 1000 записей; для файлов обязателен |
СрокЖизни |
секунды от записи, дробные допустимы | бессрочно |
Каталог |
каталог файлового кэша | для файлов обязателен |
{
"cache": {
"ПределПамяти": 150000000,
"права": { "СрокЖизни": 60 },
"курсы": { "Предел": 5000, "СрокЖизни": 300 },
"отчёты": { "Поставщик": "файлы", "Каталог": "./cache/reports", "Предел": 2147483648 }
}
}
Здесь «права» делят общий бюджет 150 МБ со всеми кэшами в памяти без своего предела, «курсы» держат до 5000 записей любого вида, «отчёты» — до 2 ГБ файлов на диске. Кэш без настроек живёт в памяти: в общем бюджете, если он задан, иначе с пределом 1000 записей. Фактические настройки каждого кэша пишутся в журнал при его создании — опечатка в ключе видна там. Имя кэша «ПределПамяти» занято.
Вытесняются давно не читанные записи; записи, которые читали повторно, защищены от потока разовых ключей. В общем бюджете и в файловом кэше одна запись не может занять больше десятой доли предела.
Общий бюджет памяти
cache.ПределПамяти — байты на все кэши в памяти, у которых не задан свой Предел. Память
достаётся записям, которые читают чаще, в каком бы кэше они ни были.
Вес записи — оценка памяти под неё целиком:
- 1750 байт — сама запись, её место в индексе и в очереди;
- ключ — 56 байт плюс 2 байта на символ текста ключа;
- значение — число 32 байта, дата 40, строка 56 + 2 на символ, фиксированная структура 700 + 100 на поле плюс ключи и значения полей.
Прикиньте число горячих записей и умножьте на вес одной; после запуска Статистика().Занято
покажет фактическое. Бюджет меньше ~18 КБ не примет ни одной записи: наибольшая запись — десятая
доля бюджета, а одни накладные весят 1,75 КБ. Оценка сходится с реальным приростом памяти
процесса в пределах ±15 %; к бюджету прибавляются ~80 МБ самого процесса и запас сборщика мусора —
ставьте с запасом.
Файловый кэш
Хранит ДвоичныеДанные, потоки и отданные ему файлы, предел — байты на диске. При создании кэша из
каталога удаляются файлы *.cache прошлого запуска, поэтому каталог принадлежит одному кэшу одного
процесса. Попадание на ДвоичныеДанные читает файл в память целиком, на поток — открывает
ФайловыйПоток над файлом.
Потоки
Кэшируемый метод может вернуть поток — Поток, ФайловыйПоток или ПотокВПамяти: например, отдать
файл, не читая его в память. Запись помнит, положили в неё поток или значение, поэтому под одним
именем кэша уживаются методы обоих видов.
Перем Хранилище;
&Кэшируемый("артефакты")
Функция Артефакт(&КлючКэша Имя, &КлючКэша Версия) Экспорт
Возврат Хранилище.ОткрытьПоток(Имя, Версия);
КонецФункции
Вызывающий получает то же, что вернул бы метод: значение — значением, поток — потоком с теми же байтами от позиции, на которой метод его вернул, до конца.
| Метод вернул | Промах | Попадание в файловом кэше | Попадание в кэше в памяти |
|---|---|---|---|
| значение | само значение | ДвоичныеДанные, прочитанные из файла в память |
само значение |
| поток | новый поток над записью | ФайловыйПоток над файлом записи, без чтения в память |
Поток на чтение над ДвоичныеДанные записи |
Каждое попадание открывает новый поток: запись отдаётся сколько угодно раз. Полученный поток закрывает вызывающий.
Кто закрывает поток метода. Кэш вычитывает положенный поток до конца, закрывает его и отдаёт вместо него поток над записью. Поток, который не положен, отдаётся вызывающему нетронутым — как без кэша: он тяжелее десятой доли предела, кэш сбросили во время вызова или он не помещается рядом с записями, которые пишутся прямо сейчас. Место под поток занимается до копирования, по остатку потока от текущей позиции до конца, поэтому бюджет не превышается и на время записи.
Файловый кэш копирует поток в файл записи порциями, не поднимая его в память; вес записи —
размер файла, в Статистика().Занято он входит байтами. Читатели одного файла друг другу не мешают.
Кэш в памяти вычитывает поток в ДвоичныеДанные: запись занимает память размером с поток, иначе
второй раз отдать нечего. Попадание открывает поток на чтение над этими данными без копии. В общем
бюджете поток весит как ДвоичныеДанные того же размера. Больше 2 ГБ в память не поместится — это
предел ПотокВПамяти. Большие потоки держите в файловом кэше.
Сбой записи. Если поток записать не удалось — кончилось место на диске, поток не поместился в
память, поток над записью не открылся, — данные записи удаляются, место в бюджете освобождается, а
поток метода возвращается на прежнюю позицию: Положить вызывает исключение, напильник пишет ошибку
в журнал и отдаёт поток как есть. Поток без изменения позиции вернуть нельзя — тогда исключение
вызывает и напильник, чтобы вызывающий не получил данные с середины.
Поток без изменения позиции (ДоступноИзменениеПозиции — Ложь, например сетевой) не знает
своего размера. Кэш в памяти с пределом в записях его кладёт, а файловый кэш и общий бюджет — нет:
Положить вызывает исключение «хранит только потоки с изменением позиции», напильник пишет его в
журнал и отдаёт поток нетронутым.
Готовый файл отдают кэшу ПоложитьФайл(Ключ, Путь) — например, когда объект уже скачан во
временный файл. Запись получается потоковой, как если бы метод вернул поток над этим файлом.
Файловый кэш забирает файл перемещением в свой каталог, без копии и без чтения в память; на macOS и
Linux читатели, открывшие файл раньше, дочитывают его. Если временный каталог и каталог кэша на
разных томах — частый случай в контейнере, — перемещение копирует файл и удаляет источник. Кэш в
памяти вычитывает файл в ДвоичныеДанные и удаляет его.
Истина — файл теперь принадлежит кэшу; если кэш сбросили, пока файл переносился, запись не
появится, а файл кэш удалит. Ложь — файла нет, он тяжелее десятой доли предела, не помещается
рядом с пишущимися записями или кэш сбросили до переноса; тогда файл остаётся у вызывающего
нетронутым. Сбой переноса — исключение, файл тоже остаётся на месте; на Windows так бывает, если
файл кто-то держит открытым. Выбор простой: поток кэш копирует, файл — забирает.
Запись убирают, пока её читают. Вытеснение, истечение срока, Удалить и Очистить снимают
запись с кэша сразу. На macOS и Linux файл записи тут же удаляется, а открытый поток дочитывается до
конца. На Windows файл, открытый на чтение, удалить нельзя: он остаётся на диске вне учёта предела,
и кэш повторяет удаление при каждом следующем удалении записи — вытеснении, сбросе, очистке. Не
убранное до остановки удаляется при следующем запуске вместе с остальными файлами *.cache.
Закрывайте поток, как только он отдан.
Журнал
Журнал oscript.lib.autumn-cache на уровне Информация пишет настройки каждого созданного кэша, на
уровне Предупреждение — файлы кэша, которые не удалились с первого раза, на уровне Ошибка —
значения, которые не удалось положить. На уровне Отладка виден каждый шаг:
Кэш "права": промах, ключ С4:ivanС6:отчёты
Кэш "права": положено, ключ С4:ivanС6:отчёты, вес 1906, занято бюджета 1906 из 20000
Кэш "права": попадание, ключ С4:ivanС6:отчёты, вес 1906, сегмент испытательный
Кэш "права": вытеснение, ключ С5:user1С6:отчёты, вес 1908, сегмент испытательный, давление 20988 из 20000
Кэш "права": сброс, ключ С4:ivanС6:отчёты, удалено записей 1, поколение 1
Ключ в строке — текст ключа: у каждой части метка типа и длина (С4:ivan — строка из четырёх
символов). Строки «не положено» называют причину: Неопределено, вес больше допустимого, сброс
во время вызова, изменяемое значение.
Включить отладку — переменной среды или файлом logos.cfg рядом со стартовым сценарием:
LOGOS_CONFIG="logger.oscript.lib.autumn-cache=DEBUG" oscript main.os
Ключи кэша попадают в журнал — не делайте ключом секреты.
Напильник кэша стоит на &Порядок(400), внутри напильников телеметрии (500 и выше): попадания
видны телеметрии как вызовы метода, а напильники с меньшим порядком выполняются только на промахе.
Статистика и программный доступ
Кэш по имени выдаёт желудь МенеджерКэшей — тот же объект, с которым работают аннотации:
Перем КэшПрав;
Функция ДоляПопаданий() Экспорт
Статистика = КэшПрав.Статистика();
Обращений = Статистика.Попадания + Статистика.Промахи;
Если Обращений = 0 Тогда
Возврат 0;
КонецЕсли;
Возврат Статистика.Попадания / Обращений;
КонецФункции
Процедура ЗабытьПрава(Пользователь, Раздел) Экспорт
КэшПрав.Удалить(Новый КлючКэша().Добавить(Пользователь).Добавить(Раздел));
КонецПроцедуры
&Желудь
Процедура ПриСозданииОбъекта(&Пластилин МенеджерКэшей)
КэшПрав = МенеджерКэшей.Кэш("права");
КонецПроцедуры
| Класс | Метод | Что делает |
|---|---|---|
МенеджерКэшей |
Кэш(Имя) |
кэш по имени |
Кэш |
Получить(Ключ) |
значение, новый поток над потоковой записью или Неопределено |
Положить(Ключ, Значение) |
кладёт значение или поток и возвращает, что отдать: поток над записью или само значение; недопустимое — исключение | |
ПоложитьФайл(Ключ, Путь) |
отдаёт кэшу готовый файл как потоковую запись; Истина — файл теперь у кэша, Ложь — остался у вызывающего нетронутым |
|
Удалить(Ключ), Очистить() |
удаляет запись или все записи | |
Статистика() |
Структура("Попадания, Промахи, Вытеснения, Занято") |
|
КлючКэша |
Добавить(Значение) |
ключ из нескольких частей |
Поток программно кладут так же, как это делает напильник, — отдают то, что вернул Положить:
Поток = КэшАртефактов.Получить(Ключ);
Если Поток = Неопределено Тогда
Поток = КэшАртефактов.Положить(Ключ, Хранилище.ОткрытьПоток(Имя, Версия));
КонецЕсли;
Ключ из одного значения можно передать самим значением: Кэш.Удалить(Идентификатор) — то же, что
Кэш.Удалить(Новый КлючКэша().Добавить(Идентификатор)). Занято — вес записей этого кэша: число
записей или байты. Вытеснения считают записи, убранные по пределу и по сроку жизни.
Программный ключ попадает в запись аннотации, если собран из тех же значений тех же типов в том же
порядке, что параметры с &КлючКэша (или все параметры, если помеченных нет). Текст ключа — части
подряд, каждая записана как метка типа, длина текста, двоеточие и сам текст:
| Значение | Текст части | Пример |
|---|---|---|
Строка |
как есть: регистр и пробелы не меняются | "Ab12" → С4:Ab12 |
Число |
без хвостовых нулей дробной части | 1.50 → Ч3:1.5, 100 → Ч3:100 |
Дата |
XMLСтрока |
Д19:2026-09-25T12:00:00 |
Булево |
XMLСтрока |
Б4:true |
УникальныйИдентификатор |
XMLСтрока |
У36:11111111-2222-3333-4444-555555555555 |
Неопределено |
пусто | Н0: |
Поэтому КэшАртефактов.ПоложитьФайл(Sha256, Путь) кладёт ровно ту запись, которую потом прочтёт
&Кэшируемый("артефакты")-метод с единственным &КлючКэша Sha256, если строка суммы та же, вплоть
до регистра. Метод с двумя ключевыми параметрами читает ключ
Новый КлючКэша().Добавить(Первый).Добавить(Второй).
Ограничения
- Параллельные промахи по одному ключу вызывают метод каждый.
- Сброс, пришедший, пока метод выполнялся, отбрасывает его результат — и любые другие значения этого кэша, которые кладутся в тот же момент.
- Кэш в памяти с пределом в записях хранит ссылки: изменение полученного объекта меняет и кэш.
- Срок жизни проверяется при чтении: истёкшая запись занимает место до обращения или вытеснения.
- На Windows файл, который читают в момент вытеснения, не удаляется и остаётся вне учёта до следующего удаления записи или перезапуска.
Лицензия
MIT