Сегодня я хочу вам рассказать, как была реализована работа с кэшем в социальное игре тайм менеджере. Можете считать эту статью продолжением вот этой.
Напомню, что в проекте используется php(Yii), mysql и memcached. В проекте достаточно много сущностей, для каждой из которой есть своя модель, которая наследуется от CActiveRecord.
Хранятся файлы моделей следующим образом. В папке models создаем папку base. Когда генерируем модель через Gii, то указываем, что ее нужно положить в папку models/base и к имени класса добавляем Base. Затем создаем в models аналогичный класс без Base, который наследуется от базового класса и имеет в себе лишь метод model().
Кстати заранее скажу, что базовые модели наследуем не от CActiveRecord, а от ExtActiveRecord — расширяем CActiveRecord под наши нужды. Но об этом позже. Пока что разницы никакой.
Пример:
models/base/BaseUser.php — стандартный класс, который генерируется через Gii
models/User.php — класс, который наследуется от BaseUser и имеет в себе метод model()/** * Returns the static model of the specified AR class. * @param string $className active record class name. * @return User the static model class */ public static function model($className=__CLASS__) { return parent::model($className); }
Данная схема используется для того, чтобы в случае повторной генерации файла модели не потерять свой код и просто не забивать пространство стандартными кодом от Yii.
Не забываем добавить в конфиге ‘application.models.base.*’.
Перейдем собственно к теме поста и поставим задачи, которые хотим решить:
- Уменьшить количество запросов в базу на обновление
- Уменьшить количество запросов в базу на выборку
Уменьшаем количество запросов в базу на обновление
Как вы помните по прошлой статье, у нас используется очередь для выполнения команд. И для какого-то конкретного пользователя может требоваться выполнение более 2х команд последовательно. К примеру у нас приходит пачка из 3х команд: увеличить опыт, купить здание и поменять имя игрока. Предположим, что опыт, деньги и имя хранится в одной таблице user.
Реализация обработки очереди такова, что команды ничего не знают друг о друге и в стандартной реализации каждая команда будет загружать модель пользователя, изменять ее и сохранять.
Мы сейчас сделаем так, что подгрузка пользователя произойдет при первом к нему обращении, а сохранение только после выполнения всех команд. Для этого мы сделаем реестр моделей.
Это такая штука, к которой мы будем обращаться, чтобы получить модель User, вместо того, чтобы писать User::model()->findByPk().
Реестр моделей будет являться компонентом и будет прописан в конфиге в components
'components' => array( // ... 'modelRegistry'=>array( 'class' => 'ModelRegistry' ) // ... )
Сам класс выглядит следующим образом
class ModelRegistry { protected $registries = array(); public function init() {} /** * Возвращает реестр выбранной модели * @param string $name * @param mixed $attr * @return ExtActiveRecord */ public function & registry($name, $attr = array()) { $key = $name . md5(serialize($attr)); if (!isset($this->registries[$key])) { $model = ucfirst($name); $obj = $model::model(); if (!is_array($attr)) $attr = array($attr); $this->registries[$key] = call_user_func_array(array(&$obj, 'registry'), $attr); } // будет возвращена ссылка на объект в массиве благодаря & в имени функции return $this->registries[$key]; } /** * Сохранение изменений */ public function saveAll() { foreach ($this->registries as $obj) { $obj->save(); } } }
У каждой нашей модели, которую мы хотим получить через реестр моделей будет метод registry, который будет возвращать объект. User в данном случае выглядит следующим образом
/** * @property integer $id * @property integer $exp * @property integer $money * @property integer $name */ class User extends BaseUser { /** * Returns the static model of the specified AR class. * @param string $className active record class name. * @return User the static model class */ public static function model($className=__CLASS__) { return parent::model($className); } /** * Получает профиль игрока * @param int $userID * @return User|bool */ public function registry($userID) { if ($obj = $this->findByPk($userID)) { $res = $obj; } else { $res = false; } return $res; } }
К чему это все привело. К примеру у нас есть контроллер, который вызывает два метода, изменяющие пользователя.
/** * @var ModelRegistry */ protected $reg; public function actionRun() { $userID = 1; $this->reg = &Yii::app()->modelRegistry; $this->firstChange($userID); $this->secondChange($userID); $this->reg->saveAll(); } public function firstChange($userID) { // здесь первой обращение. Реестр создаст модель и подгрузит данные из базы // & нужно, чтобы получить ссылку на объект из массива $user = &$this->reg->registry('user', $userID); $user->exp = 10; } public function secondChange($userID) { // здесь обращение к тому, что уже подгружено в реест. В базу обращения нет // & аналогично $user = &$this->reg->registry('user', $userID); $user->money = 20; }
Собственно будет одно обращение в базу на select и одно на update.
Здесь мы сталкиваемся с дополнительной задачей. Если вызвать этот код второй раз, то поля пользователя остануться теми же, но сохранение все равно будет произведено. Нам нужно сделать так, чтобы наш ActiveRecord сохранялся только в том случае, когда объект притерпел изменения.
Тут нам на помощь приходит наш ExtActiveRecord, который мы использовали для расширения CActiveRecord.
class ExtActiveRecord extends CActiveRecord { protected $_oldAttributes = array(); /** * Тот самый метод, который должен быть во всех моделях */ public function registry() {} /** * Запомнить текущее состояние модели */ public function memoryAttributes() { $this->_oldAttributes = $this->attributes; } /** * Поиск изменений в моделе. Возвращает список измененных полей * Если объект был только создан и его нужно будет сохранить полностью, то возвращает false * @return array|false */ protected function getChanges() { $res = array(); if (empty($this->_oldAttributes)) { $res = false; } else { foreach ($this->_oldAttributes as $key => $value) { if ($this->$key != $value) { $res[] = $key; } } } return $res; } /** * Сохраняем только изменения * @return bool */ public function save() { if (($attr = $this->getChanges()) === false) { $res = parent::save(); } elseif ($attr) { $res = $this->update($attr); } else { $res = false; } return $res; } }
И обновляем метод registry в модели User
public function registry($userID) { if ($obj = $this->findByPk($userID)) { $res = $obj; } else { $res = false; } if ($res) { // запоминаем текущее состояние модели $res->memoryAttributes(); } return $res; }
Собственно теперь будет производиться только insert или update измененных полей модели.
Оставляю вам пространство для творчества и позволяю самим разобраться в том, как создать нового пользователя и поместить его в реест моделей во время выполнения скрипта.
Я показал вам, как можно хранить в реестре какие-либо объекты. Но иногда возникают ситуации, когда нам нужно хранить там какой-то список. К примеру для каждого пользователя у нас есть 10 машин. И мы хотим, чтобы в реестре было не 10 машин, а один объект, содержащий все машины. Для этого используется класс ModelList, который хранит модели машин.
class ModelList { /** * @var array с данными ExtActiveRecord */ public $list = array(); /** * Создает новый список * @param array|bool $list массив с ExtActiveRecord * @return ModelList */ public static function make($list = array()) { if (!is_array($list) && empty($list)) { $list = array(); } $obj = new ModelList(); $obj->list = $list; return $obj; } /** * Добавить объект в список * @param ExtActiveRecord $obj */ public function pushObject($obj) { $this->list[] = $obj; } /** * Вызвать у всех моделей метод * @param string $name */ public function callMethod($name) { foreach ($this->list as &$obj) { $obj->$name(); } } /** * Сохранение всех объектов */ public function save() { $this->callMethod('save'); } }
А вот так выглядит модель машины
<?php /** * @property integer $id * @property integer $user_id * @property integer $car_id * @property integer $speed */ class Car extends BaseCar { /** * Returns the static model of the specified AR class. * @param string $className active record class name. * @return Car the static model class */ public static function model($className=__CLASS__) { return parent::model($className); } /** * Получает реестр всех машин пользователя * @param int $userID * @return ModelList */ public function registry($userID) { $list = $this->findAllByAttributes(array('user_id'=>$userID)); $res = ModelList::make($list); // у всех машин сохраняем состояние $res->callMethod('memoryAttributes'); return $res; } /** * Создаем, но не сохраняем. Используется, когда мы хотим положить в ModelList через pushObject * @param int $userID * @param int $carID * @return Car */ public static function make($userID, $carID) { $obj = new Car(); $obj->user_id = $userID; $obj->car_id = $dict->area_id; $obj->speed = 10; return $obj; } }
Собственно теперь, когда мы выполним следующий код,
$carList = &Yii::app()->modelRegistry->registry('car', 1);
то получим объект класса ModelList, который будет содержать в себе все машины игрока. Их так же можно изменять (не забывая обращаться по ссылке в $carList->list) и потом сохранять через реест моделей стандартным saveAll.
Так как статья выходит довольно большая, то про кэширование всего этого я расскажу в следующей статье.
Могу сказать, что в идеальных условиях с кэшированием данная реализация не обращается за одними и теми же данными два раза, даже если после первого раза было произведено их обновление.
Данный вариант работы с моделями удобно используется в нашем проекте, но возможно не подойдет вам.
Все что я хотел, так это просто показать какие бывают задачи и как их можно решать.
ссылка на оригинал статьи http://habrahabr.ru/company/alawar/blog/177181/
Добавить комментарий