Я веб-разработчик и не так давно (подрабатывая на стороне от основной работы) мне пришлось решать довольно нестандартную в наших кругах задачу: разработать фронтенд на Yii2 к сайту, весь бэкенд которого написан на древнегреческом ASP VBScript (простите, я уже забыл, как правильно это писать: просто ASP, или просто VBScript?).
Сразу оговорюсь, что весь проект заказчиков в данный момент состоит из ~500мб скриптов (ребята до сих пор пишут на нем, года так с 97-го).
Конечно, далеко не ко всему этому функционалу нужен был фронт, что очень радовало. Не радовали две вещи: БД Oracle (но это другая история) и невозможность инвалидировать кеш. А кеш данная команда не использует вообще ни в каком виде, и не будет этого делать ни при каких обстоятельствах: то ли в силу инертности мышления, то ли из-за трудностей VBScript+Memcache, а скорее просто из-за тех 500мб (о которых я написал выше).
В общем, должен был получится такой забавный пирог: Backend на ASP, Frontend на Yii2.
Это было вместо вступления.
Проблема, которая встала сразу: т.к. нужно использовать кеширование — то как узнать когда и какие данные изменились в базе? Никакой инвалидации от бекенда не будет, удаления ключа не будет. Ничего такого не светило и в помине. Хоть проект и не очень высоконагруженный, но хотелось сделать как-то гибко и оптимально.
Предлагаю на суд песочницы мое, возможно, не самое элегантное, но решение. Может быть, кто-то столкнется с этим и этот пост ему поможет, как мне помогли многие отличные решения на этом сайте.
Итак, какова главная идея: раз у нас нет информации о том, что изменилось в базе, то мы будем собирать ее сами.
Первым делом создаем интерфейс:
Обычно я прилежно заполняю phpDoc — поэтому там, где он более-менее полон — особенно комментировать не буду.
Каждая модель, реализующая данный интерфейс, рассказывает будущей процедуре инвалидации, что она готова инвалидировать свой кеш:
Тут стоит заметить, что если у таблицы не�� поля, за которое можно зацепиться, то, например, для Oracle можно использовать банальный ROWNUM, так как (если архитекторы БД не совсем нехорошие люди) данные в такой таблице никогда не изменяются, а только добавляются.
Дальше дело за малым. Пишем стандартную консольную команду Yii2, которую будем вызывать кроном раз в какое-то вменяемое время (у меня было 5 минут). И в ней делаем как-то так:
К сожалению, я не очень понял, можно ли пройти рефлекшеном по определенным папкам и вытянуть из файлов классы, поэтому сделал, возможно, лишний метод _getInvalidateModels(), в котором нужно повторять инвалидируемые модели. Если так можно — буду очень благодарен за подсказку.
Безусловно, используя этот метод, мы будем иметь временной лаг, и это никогда не будет гарантировать 100% совпадения данных в кеше и в базе. Но право на жизнь данный подход (в некритичных к этому проектах), наверное, имеет.
Благодарю за внимание!
К критике отношусь очень положительно.
Сразу оговорюсь, что весь проект заказчиков в данный момент состоит из ~500мб скриптов (ребята до сих пор пишут на нем, года так с 97-го).
Конечно, далеко не ко всему этому функционалу нужен был фронт, что очень радовало. Не радовали две вещи: БД Oracle (но это другая история) и невозможность инвалидировать кеш. А кеш данная команда не использует вообще ни в каком виде, и не будет этого делать ни при каких обстоятельствах: то ли в силу инертности мышления, то ли из-за трудностей VBScript+Memcache, а скорее просто из-за тех 500мб (о которых я написал выше).
В общем, должен был получится такой забавный пирог: Backend на ASP, Frontend на Yii2.
Это было вместо вступления.
Проблема, которая встала сразу: т.к. нужно использовать кеширование — то как узнать когда и какие данные изменились в базе? Никакой инвалидации от бекенда не будет, удаления ключа не будет. Ничего такого не светило и в помине. Хоть проект и не очень высоконагруженный, но хотелось сделать как-то гибко и оптимально.
Предлагаю на суд песочницы мое, возможно, не самое элегантное, но решение. Может быть, кто-то столкнется с этим и этот пост ему поможет, как мне помогли многие отличные решения на этом сайте.
Итак, какова главная идея: раз у нас нет информации о том, что изменилось в базе, то мы будем собирать ее сами.
Первым делом создаем интерфейс:
<?php /** * Интерфейс необходим для исключения ошибок в процедуре инвалидации кеша. * В данном случае он выступает гарантом того, что кеш модели, которая его реализует * может быть инвалидирован процедурой инвалидации т.к. она реализует необходимые для этого методы. */ interface InvalidateModels { public function getInvalidateTime(); public function getInvalidateField(); }
Обычно я прилежно заполняю phpDoc — поэтому там, где он более-менее полон — особенно комментировать не буду.
Каждая модель, реализующая данный интерфейс, рассказывает будущей процедуре инвалидации, что она готова инвалидировать свой кеш:
class Airport extends ActiveRecord implements InvalidateModels { /** * Периодичность проверки актуальности кеша * @return int */ public function getInvalidateTime() { return 60 * 60 * 24; } /** * Поле в таблице по которому проверяем актуальность * @return string */ public function getInvalidateField() { return 'update_stamp'; }
Тут стоит заметить, что если у таблицы не�� поля, за которое можно зацепиться, то, например, для Oracle можно использовать банальный ROWNUM, так как (если архитекторы БД не совсем нехорошие люди) данные в такой таблице никогда не изменяются, а только добавляются.
Дальше дело за малым. Пишем стандартную консольную команду Yii2, которую будем вызывать кроном раз в какое-то вменяемое время (у меня было 5 минут). И в ней делаем как-то так:
/** * Контроллер консольных команд для работы с кешем */ class InvalidateCacheController extends \yii\console\Controller { /** * Модели, которые необходимо инвалидировать по расписанию * Эти модели должны обязательно реализовывать интерфейс \common\interfaces\InvalidateModels * ,иначе ничего не произойдёт * * @return array */ private function _getInvalidateModels() { return [ Airport::class, ]; } /** * Action инвалидации кэша таблиц * 1. Берем данные из классов как надо проверять изменения в таблицах * 2. Если пришло время проверить изменения то сверяем максимальную дату изменения записей в таблице * с датой когда был установлен для неё кеш. Если дата кеша меньше то инвалидируем кеш, * чтобы он актуализировался при следующем запросе пользователя. */ public function actionInvalidateCache() { $models = $this->_getInvalidateModels(); $reflectionObjects = []; foreach ($models as $modelName) { $reflectionObjects[] = new \ReflectionClass($modelName); } /** @var \ReflectionClass $refObject */ foreach ($reflectionObjects as $refObject) { //Проверим реализует ли наш класс интерфейс InvalidateModels if (!$refObject->implementsInterface('\common\interfaces\InvalidateModels')) { continue; } $modelName = $refObject->getName(); /** @var \common\interfaces\InvalidateModels $model */ $model = new $modelName; $invalidateTime = $model->getInvalidateTime(); $cacheKey = 'LastInvalidateTime-' . $refObject->getName(); $lastInvalidateTime = \Yii::$app->memcache->get($cacheKey); if (false === $lastInvalidateTime) { $this->_invalidateCache($refObject); \Yii::$app->memcache->set($cacheKey, 1, $invalidateTime); } } } /** * Инвалидация просроченного кеша * * @param \ReflectionClass $refObject */ private function _invalidateCache(\ReflectionClass $refObject) { $modelName = $refObject->getName(); /** @var \common\interfaces\InvalidateModels $model */ $model = new $modelName; $invalidateField = $model->getInvalidateField(); /** @var \yii\db\ActiveRecord $model */ $lastChangedTime = $model::find()->max($invalidateField); $lastDataInCache = \Yii::$app->memcache->get('last-set-time.' . $model::tableName()); //Если дата установки тега в кеш меньше чем дата обновления последней записи в таблице то инвалидируем данные с этим тегом if ($lastDataInCache < strtotime($lastChangedTime)) { \Yii::$app->memcache->invalidateTags([$model::tableName()]); } } }
К сожалению, я не очень понял, можно ли пройти рефлекшеном по определенным папкам и вытянуть из файлов классы, поэтому сделал, возможно, лишний метод _getInvalidateModels(), в котором нужно повторять инвалидируемые модели. Если так можно — буду очень благодарен за подсказку.
Заключение
Безусловно, используя этот метод, мы будем иметь временной лаг, и это никогда не будет гарантировать 100% совпадения данных в кеше и в базе. Но право на жизнь данный подход (в некритичных к этому проектах), наверное, имеет.
Благодарю за внимание!
К критике отношусь очень положительно.
