text-media / shm-cache
Shared Memory ReadOnly Cache
Requires
- php: >=7.0
Requires (Dev)
- ext-memcached: *
- jamm/memory: ^1.0
- phpunit/phpunit: ^7.1
- squizlabs/php_codesniffer: ^3.1
This package is auto-updated.
Last update: 2025-10-10 20:36:16 UTC
README
Кэш типа "ключ -> значение" на основе разделяемой памяти.
Предназначен для работы большого количества одновременно запущенных скриптов с большим блоком статичных неизменяемых данных:
- один скрипт по расписанию или какому-то событию "прогревает" кэш, т.е. читает откуда-то данные, приводит их к нужному виду и сохраняет в памяти;
- множество других скриптов читают данные из памяти, т.е. не тратят ни ресурсы процессора на чтение и приведение к нужному виду, ни память на хранение одних и тех же общих для всех них данных.
Настройка
Для настройки кэша необходимо создать класс-потомок абстракции \TextMedia\ShmCache\Behavior.
В нем необходимо обязательно переопределить следующие константы:
| Константа | Описание |
|---|---|
PROJECT_ID | Идентификатор проекта. Это должен быть один символ (см. http://php.net/manual/ru/function.ftok.php). |
CACHE_SIZE | Размер блока разделяемой памяти. Может быть задан целым числом байт или строкой типа: 1m, 100k и т.п. Должен быть не менее 1 Кб. |
VALUE_SIZE | Определяет, сколько нужно байт для хранения длины упакованных данных. Может меняться в пределах от 1 до 4. |
VALUE_SIZE должен подбираться из расчета длины хранимых значений. Если это просто число или небольшой текст
(не более 255 ASCII-символов) - достаточно указать 1 байт.
CACHE_SIZE следует выбирать исходя из суммы следующих значений:
- число записей в кэше, умноженное на 1 - столько байт потребуется для хранения длин ключей;
- суммарная длина строковых представлений всех ключей;
- число записей, умноженное на
VALUE_SIZE- столько нужно для хранения длин значений; - суммарная длина строковых представлений всех значений.
Строковые представления значений не должны быть представлены в виде результата выполнения var_export и т.п.,
т.к. такие данные имеют избыточную информацию, в частности - имена ключей.
Например, при сохранении массива вида ['x' => число1, 'y' => число2] достаточно сохранить только числа в бинарном виде,
а при чтении из памяти приводить их к нужному типу и формировать массив нужного вида.
Для этого в классе необходимо переопределить следующие методы:
| Метод | Описание |
|---|---|
string packData(string $key, $data) | Упаковка элемента данных в строку для записи. |
mixed unpackData(string $key, string $packed) | Распаковка прочитанных из памяти данных в исходную структуру. |
Значение $key передается в оба метода, т.к. оно может повлиять на алгоритм упаковки/распаковки.
Например, если хранимые данные имеют вид массива со следующими полями:
user_id: число, на запись которого нужно не более 2 байт;name: строка, длиной не более 255 символов;desc: строка.
use TextMedia\ShmCache\Behavior;
class MyCacheBehavior extends Behavior
{
public function packData(string $key, $data): string
{
return (self::packNumber($data['user_id'], 2)
. self::packNumber(strlen($data['name'], 1))
. $data['name']
. $data['desc']);
}
public function unpackData(string $key, string $packed)
{
$nameLength = self::unpackNumber(substr($packed, 2, 1));
return [
'user_id' => self::unpackNumber(substr($packed, 0, 2)),
'name' => substr($packed, 3, $nameLength),
'desc' => substr($packed, 3 + $nameLength),
];
}
}
Если метод packData() должен прости привести к строке, лучше использовать parent::packData(),
т.к. он проверяет, можно ли значение привести к строки, и выбрасывает исключение, если нельзя.
Это позволит избежать фатальных ошибок.
Метод unpackData() при распаковке данных может проверятьо их валидность, соответствие каким-то своим шаблонам,
и в случае ошибки - выбрасывать исключение типа \TextMedia\ShmCache\Exception (см. раздел "Ошибки"),
которое будет перехвачено основным объектом, в следствии чего будут выполнены следующие действия:
- кэшу будет выставлен статус "поврежден";
- вызовется метод
onCorrupt- по умолчанию он ничего не делает, его можно переопределить; - исключение будет проброшено выше, т.е. работа скрипта будет прекращена.
Так же этот класс обязательно должен определять метод getData(), необходимый для "прогрева" кэша.
Этот метод должен возвращать ArrayObject
для сохранения в кэше в виде "ключ-значение". Например:
use ArrayObject;
use TextMedia\ShmCache\Behavior;
class MyCacheBehavior extends Behavior
{
public function getData(): ArrayObject
{
return new ArrayObject($this->database->getQueryResult('SELECT user_id, name, desc FROM users', 'user_id'));
}
}
Класс \TextMedia\ShmCache\Behavior содержит следующие статичные методы,
которые можно использовать для упаковки/распаковки данных:
| Метод | Описание |
|---|---|
string packNumber(int $number, int $size) | Упаковка числа в последовательность символов ($size - число байт). |
int unpackNumber(string $string) | Распаковка последовательности символов в число. |
Обработчики событий
Класс-потомок \TextMedia\ShmCache\Behavior может переопределять поведение при возникновении некоторых событий
(все три метода необязательны для переопределения и по умолчанию ничего не делают):
| Метод | Событие |
|---|---|
onCorrupt | В процессе обработки данных, прочитанных из кэша, произошла ошибка. |
onEmpty | При попытке чтения данных из кэша выяснилось, что он (кэш) пуст. |
onWarmed | Завершен "прогрев" кэша. |
onIndexed | Завершено формирование таблицы индексов (смещений). |
Первым аргументом для каждого метода является объект-кэш класса \TextMedia\ShmCache\Cache,
при работе с которым произошло данное событие.
Для обработчика onCorrupt дополнительными аргументами являются:
| Аргумент | Тип | Описание |
|---|---|---|
$key | string | Ключ, данные по которому не удалось обработать. |
$value | string | Данные, прочитанные из памяти. |
Обработчики onWarmed и onIndexed имеет дополнительно два аргумента $onStart и $onReady;
оба - являются обычными объектами со следующими полями:
| Поле | Тип | Описание |
|---|---|---|
time | float | Когда был запущен/завершен процесс (microtime(true)). |
memory | integer | Используемая память на момент запуска/завершения (memory_get_peak_usage(true)). |
records | integer | Число записанных/прочитанных записей (на момент старта = 0). |
size | integer | Сколько байт было записано/прочитано (на момент старта = 0); без учета размера заголовка кэша. |
Обработчик onEmpty дополнительных аргументов не имеет и по умолчанию вызывается обработчиком onCorrupt (если он не переопределен никак иначе),
т.к. очевидно, что итоговое поведение в обоих случаях должно быть одно - исправить содержимое кэша, "прогреть" его заново.
Использование
Для работы с кэшем необходимо создать экземпляр класса \TextMedia\ShmCache\Cache,
передав в его конструктор объект-наследник \TextMedia\ShmCache\Behavior.
Для "прогрева" кэша используется метод warmup(), имеющий один необязательный параметр,
указывающий на то, нужно ли сперва полностью вычистить все данные из памяти, т.е. забить их 0-ми.
По умолчанию этот параметр имеет значение TRUE.
Для чтение данных из разделяемой памяти доступны следующие методы:
| Метод | Описание |
|---|---|
mixed getItem(string $key) | Чтение по одному ключу ключу. |
array getItems(array $keys) | Чтение по множеству ключей. На выходе массив вида "ключ --> значение". |
Вторым аргументом в метод getItems() можно передать bool $ignoreMissing (по умолчанию = TRUE),
который указывает, нужно ли игнорировать отсутствующие в таблице индексов ключи или все же выбрасывать исключения.
Если этот аргумент равен TRUE и было выброшено исключение с кодом FAILED_SEARCH_KEY (см. раздел "Ошибки"),
то значение не попадет в результирующий массив; в остальных случаях - исключение будет проброшено вверх.
Ошибки
Все вышеперечисленные классы в случае ошибок выбрасывают исключения типа \TextMedia\ShmCache\Exception.
Каждому типу ошибок назначен свой код - константа данного класса:
| Константа | Значение | Описание ошибки |
|---|---|---|
INVALID_PROJECT_ID | 1 | Неправильное значение идентификатора проекта. |
FAILED_GET_SHM_ID | 2 | Не удалось определить идентификатор блока разделяемой памяти. |
INVALID_CACHE_SIZE | 3 | Неправильное значение размера блока разделяемой памяти. |
INVALID_VALUE_SIZE | 4 | Неправильное значение размера длины строкового представления данных. |
FAILED_OPEN_SHMOP | 5 | Не удалось открыть блок разделяемой памяти. |
FAILED_DELETE_SHMOP | 6 | Не удалось удалить блок разделяемой памяти. |
INVALID_STATUS_VALUE | 7 | Неправильное значение статуса. |
FAILED_READ_SHMOP | 8 | Ошибка чтения из разделяемой памяти. |
FAILED_WRITE_SHMOP | 9 | Ошибка записи в разделяемую память. |
FAILED_SEARCH_KEY | 10 | Указанный ключ отсутствует в таблице. |
CACHE_NOT_READY | 11 | Кэш не готов к чтению. |
FAILED_PACK_VALUE | 12 | Не удалось привести значение к строке. |
FAILED_UNPACK_VALUE | 13 | Не удалось распаковать значение из строки. |
Отладка
Для включения режима отладки необходимо вызвать метод Cache::setDebugMode() с аргументом TRUE;
для отключения - его же, но с аргументом FALSE.
Режим отладки включает сохранение данных о времени выполнения операций и используемой для этого памяти. Отслеживаются следующие операции:
| Название | Описание |
|---|---|
CACHE CLEAN | Очистка кэша. |
CACHE WARMUP | "Прогрев" кэша. |
TABLE CREATION | Формирование таблицы индексов. |
OFFSET SEARCH | Поиск по таблице индексов. |
Данные отладки могут быть получены методом Cache::getDebugData(), который возврашает массив,
список которого являются массивы со следующими полями:
| Поле | Описание |
|---|---|
action | Выполненное действие (см. таблицу выше). |
time | Затраченное время (секунды, до 8 знаков после запятой). |
memory | Сколько в итоге было максимально использовано памяти (в байтах). |
Тесты
cd /path/to/package
vendor/bin/phpunit
Кроме проверки записи/чтения в разделяюмую память, тесты включают в себя сравнение производительности по сравнению с Memcached:
- запускается отдельный скрипт, который:
- очищает и разделяемую память, и
memcached; - запускает 300 процессов (поровну для всех типов кэша); запущенные процессу "висят" в памяти, ожидая "прогрева" кэша;
- выполняет "прогрев" - все типы кэшей заполняются 200 одинаковыми элементами;
- очищает и разделяемую память, и
- запущенные скрипты выполняют 10000 запросов к кэшу к рандомным элемнтам;
- по завершении работы всех скриптов выводится:
- среднеее время работы отдельного процесса;
- максимальное;
- минимальное;
- время "прогрева" кэша.
Тест производительности активен по умолчанию, но может быть отключен следующим образом:
export PHP_UNIT='--no-performance' && vendor/bin/phpunit
Если тест производительности не был отключен и либо "завис", либо прерван, перед запуском следующего теста необоходимо "убить" запущенные ранее процессы:
pkill -f "TestPerformance.php"