Я веб-разработчик и не так давно (подрабатывая на стороне от основной работы) мне пришлось решать довольно нестандартную в наших кругах задачу: разработать фронтенд на Yii2 к сайту, весь бэкенд которого написан на древнегреческом ASP VBScript (простите, я уже забыл, как правильно это писать: просто ASP, или просто VBScript?).

Сразу оговорюсь, что весь проект заказчиков в данный момент состоит из ~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% совпадения данных в кеше и в базе. Но право на жизнь данный подход (в некритичных к этому проектах), наверное, имеет.

Благодарю за внимание!

К критике отношусь очень положительно.
Поделиться с друзьями
-->

Комментарии (13)


  1. kovalevsky
    04.08.2016 13:27
    -3

    Я впервые вижу, как люди пишут фронтенд на PHP.


    1. eastywest
      04.08.2016 14:09
      +1

      Некоторые используют PHP просто как шаблонизатор, при этом вся основная логика реализована в других сервисах.


      1. kovalevsky
        04.08.2016 14:13

        Не вижу явных преимуществ, по-сравнению с обычным API по HTTP и нормальным клиентом. А так, целый фреймворк ради AR и шаблонизатора, странное решение


        1. SamDark
          04.08.2016 16:16

          А что такое нормальный клиент?


          1. kovalevsky
            04.08.2016 16:18
            -1

            Ну что там сейчас модно, Angular/React, например. У меня складывается ощущение, что у нас понятия «фронтенд» воспринимается по-разному :)


            1. SamDark
              04.08.2016 16:43

              Да. Тут под фронтом имеется ввиду то, что наружу торчит, а не клиентсайд в понимании браузерное что-то.


              Тут проблема в том, что надо как-то отдать Angular-у JSON из API. А API нету. Есть legacy-хреновина в которую лезть страшно, не то что припилить к ней API. То есть так и так делать какой-то серверный прокси, который общается с этим legacy. Сам он рендерит HTML-ки или JSON отдаёт в контексте статьи не важно.


              1. vladnevlad
                06.08.2016 02:11

                господа, можно этот вечный холивар хабра больше не продолжать? надоело уже, честно. есть миллион постов, где эти трэды длятся и длятся.


            1. vladnevlad
              06.08.2016 02:18

              конечно по-разному. в Yii2 (если вы знаете что это) есть две такие папочки: frontend и backend. Тут речь, собственно о них, а не о вашем эго.


  1. oxidmod
    04.08.2016 13:48
    +3

    >> К сожалению, я не очень понял, можно ли пройти рефлекшеном по определенным папкам и вытянуть из файлов классы, поэтому сделал, возможно, лишний метод _getInvalidateModels(), в котором нужно повторять инвалидируемые модели. Если так можно — буду очень благодарен за подсказку.

    если вы придерживаетесь PSR4, то можно обычным директори итератором пройтись по папке с моделями и получить список моделей. Автолоад все подхватит.


  1. SamDark
    04.08.2016 14:07
    +2

    Вполне обычный костыль для legacy. Два вопроса:


    1. Точно ли нужно пихать логику инвалидации в модели AR?
    2. Зачем вам рефлексия, если есть instanceof? Да и объект у вас тоже есть...


    1. vladnevlad
      06.08.2016 02:14

      1. На самом деле логика туда запихана для простоты. Естественно, лучше это выносить в настройки. Или вы имеете ввиду саму имплементацию методов?

      2. Сейчас смотрю — вы правы. С instanceof было бы куда краше. Просто мысль в тот момент зацепилась за рефлексию. Не правильно, согласен.


      1. SamDark
        08.08.2016 13:00

        1. Саму имплементацию.


  1. Torrion
    05.08.2016 20:55
    +1

    Если классы не по пср, то можно использовать библиотеки наподобии https://github.com/hanneskod/classtools