diff --git a/ActiveQueryCacheHelper.php b/ActiveQueryCacheHelper.php index 37bb1d4..62fb0d4 100644 --- a/ActiveQueryCacheHelper.php +++ b/ActiveQueryCacheHelper.php @@ -3,8 +3,8 @@ namespace sitkoru\cache\ar; use yii\db\ActiveRecord; +use yii\db\Exception; use yii\db\Query; -use yii\redis\Connection; /** * Class ActiveQueryCacheHelper @@ -14,55 +14,32 @@ */ class ActiveQueryCacheHelper extends CacheHelper { - private static $logClass; - private static $connection = null; + private static $inited = false; + public static $shaCache = '86bda7598e8952af3ca5aa2f23eedc54a5a11414'; + public static $shaInvalidate = '8cc3d1f5ba2ec9b0ceee2925dcdf516d67e18d70'; + + private static $jsonOptions = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR; /** - * @return Connection + * */ - public static function getRedis() + public static function initialize() { - if (!self::$connection) { - self::$connection = \Yii::$app->cache->redis; - } - return self::$connection; - } + if (!self::$inited) { - public static function setConnection(Connection $connection) - { - self::$connection = $connection; - } + if (!ActiveQueryCacheHelper::scriptExists(self::$shaCache)) { + $path = __DIR__ . DIRECTORY_SEPARATOR . 'lua' . DIRECTORY_SEPARATOR . 'cache.lua'; + ActiveQueryCacheHelper::loadScript($path); + } + if (!ActiveQueryCacheHelper::scriptExists(self::$shaInvalidate)) { + $path = __DIR__ . DIRECTORY_SEPARATOR . 'lua' . DIRECTORY_SEPARATOR . 'invalidate.lua'; + ActiveQueryCacheHelper::loadScript($path); + } - public static function log($message) - { - if (self::$logClass) { - $logger = self::$logClass; - $logger::log($message); + self::$inited = true; } } - const PROFILE_RESULT_HIT_ONE = 0; - const PROFILE_RESULT_HIT_ALL = 1; - const PROFILE_RESULT_MISS_ONE = 2; - const PROFILE_RESULT_MISS_ALL = 3; - const PROFILE_RESULT_DROP_PK = 4; - const PROFILE_RESULT_DROP_DEPENDENCY = 5; - const PROFILE_RESULT_NO_CACHE = 6; - const PROFILE_RESULT_EMPTY_ONE = 7; - const PROFILE_RESULT_EMPTY_ALL = 8; - - public static $types = [ - self::PROFILE_RESULT_HIT_ONE => 'HIT ONE', - self::PROFILE_RESULT_HIT_ALL => 'HIT ALL', - self::PROFILE_RESULT_MISS_ONE => 'MISS ONE', - self::PROFILE_RESULT_MISS_ALL => 'MISS ALL', - self::PROFILE_RESULT_DROP_PK => 'DROP PK', - self::PROFILE_RESULT_DROP_DEPENDENCY => 'DROP DEPENDENCY', - self::PROFILE_RESULT_NO_CACHE => 'NO CACHE', - self::PROFILE_RESULT_EMPTY_ONE => 'EMPTY ONE', - self::PROFILE_RESULT_EMPTY_ALL => 'EMPTY ALL' - ]; - private static $cacheTTL = 7200; //two hours by default /** @@ -70,7 +47,7 @@ public static function log($message) */ public static function setTTL($ttl) { - self::$cacheTTL = intval($ttl); + self::$cacheTTL = (int)$ttl; } /** @@ -81,22 +58,6 @@ public static function getTTL() return self::$cacheTTL; } - /** - * @param $className - */ - public static function setLogClass($className) - { - self::$logClass = $className; - } - - /** - * @return string|null - */ - public static function getLogClass() - { - return self::$logClass; - } - /** * @param $className * @param $condition @@ -115,9 +76,9 @@ public static function dropCachesForCondition($className, $condition, $params) } /** - * @param $className - * @param $condition - * @param $params + * @param ActiveRecord $className + * @param $condition + * @param $params * * @return array */ @@ -126,324 +87,55 @@ protected static function getModelsToDelete($className, $condition, $params) /** * @var ActiveRecord $className */ - $pks = $className::primaryKey(true); + $pks = $className::primaryKey(); $pkName = reset($pks); - $query = new Query(); - $results = $query->select($pkName)->from($className::tableName())->where( + $query = (new Query())->select($pkName)->from($className::tableName())->where( $condition, $params - )->createCommand()->queryAll(); - return [$pkName, $results]; - } - - /** - * @param ActiveRecord $model - * @param array $changedAttributes - * @param bool $withEvents - */ - public static function dropCaches($model, $changedAttributes = [], $withEvents = true) - { - self::log( - "LD " . $model::className() . " " . json_encode($model->attributes) ); - $depended = self::getDependedCaches($model, $changedAttributes, $withEvents); - if (count($depended)) { - foreach ($depended as $cacheKey) { - self::log("D " . $cacheKey['key']); - self::profile(self::PROFILE_RESULT_DROP_DEPENDENCY, $cacheKey['key']); - \Yii::$app->cache->delete($cacheKey['key']); - self::removeFromSet($cacheKey['setKey'], $cacheKey['key']); - } + try { + $results = $query->createCommand()->queryAll(); + } catch (Exception $ex) { + $results = []; } + return [$pkName, $results]; } /** * @param ActiveRecord $model - * * @param array $changedAttributes - * @param bool $withEvents - * - * @return array - */ - public static function getDependedCaches(ActiveRecord $model, $changedAttributes, $withEvents) - { - $keys = []; - - $tableName = $model->tableName(); - $pks = $model->getPrimaryKey(true); - $pk = reset($pks); - - $setKey = $tableName . "_" . $pk; - $setKeys = self::getSetMembers($setKey); - if ($setKeys) { - foreach ($setKeys as $member) { - $keys[] = [ - 'setKey' => $setKey, - 'key' => $member, - ]; - } - } - - if ($withEvents) { - $keys = self::getEventsKeys($model, $changedAttributes, $keys); - } - - return $keys; - } - - /** - * @param ActiveRecord $singleModel - * @param $changedAttributes - * @param $keys - * - * @return array - */ - public static function getEventsKeys($singleModel, $changedAttributes, $keys) - { - //if ($singleModel->insert) { - $keys = self::getKeysForCreateEvent($singleModel, $keys); - //} else { - $keys = self::getKeysForUpdateEvent($singleModel, $changedAttributes, $keys); - // } - - return $keys; - } - - /** - * @param $singleModel - * @param $keys - * - * @return array - */ - public static function getKeysForCreateEvent(ActiveRecord $singleModel, $keys) - { - $keys = self::getEvents($singleModel::tableName(), 'create', $keys); - foreach ($singleModel->attributes as $attr => $value) { - - if (is_array($singleModel->$attr)) { - continue; //skip array fields - } - $type = 'create_' . $attr . '_' . $singleModel->$attr; - $keys = self::getEvents($singleModel::tableName(), $type, $keys); - } - - return $keys; - } - - /** - * @param $tableName - * @param $type - * @param array $keys - * - * @return array - */ - private static function getEvents($tableName, $type, $keys) - { - $setName = $tableName . "_" . $type; - $setMembers = self::getSetMembers($setName); - foreach ($setMembers as $member) { - $keys[] = [ - 'setKey' => $setName, - 'key' => $member, - ]; - } - return $keys; - } - - /** - * @param ActiveRecord $singleModel - * @param $changedAttributes - * @param array $keys - * - * @return array - */ - public static function getKeysForUpdateEvent($singleModel, $changedAttributes, $keys) - { - $keys = self::getEvents($singleModel::tableName(), 'update', $keys); - foreach ($changedAttributes as $changedAttr => $oldValue) { - $setKeyType = 'update_' . $changedAttr; - $keys = self::getEvents($singleModel::tableName(), $setKeyType, $keys); - foreach ($singleModel->attributes as $attr => $value) { - if (is_array($singleModel->$attr)) { - continue; //skip array fields - } - $type = $setKeyType . '_' . $attr . '_' . $singleModel->$attr; - $keys = self::getEvents($singleModel::tableName(), $type, $keys); - } - } - - return $keys; - } - - /*** - * @param $result - * @param $key - * @param bool $query - */ - public static function profile($result, $key, $query = false) - { - if (defined('ENABLE_CACHE_PROFILE') && ENABLE_CACHE_PROFILE) { - $entry = json_encode( - [ - 'date' => time(), - 'result' => $result, - 'key' => $key, - 'query' => $query - ] - ); - self::increment('cacheResult' . $result); - self::addToList("cacheLog", $entry); - } - } - - /** - * @param $key - * @param $data - * @param $indexes - * @param $dropConditions */ - public static function insertInCache($key, $data, $indexes, $dropConditions) + public static function dropCaches($model, array $changedAttributes = []) { - self::log("I " . $key); - $result = \Yii::$app->cache->set($key, $data, self::$cacheTTL); - - if ($result) { - foreach ($indexes as $modelName => $keys) { - foreach ($keys as $pk) { - self::addToSet($modelName . "_" . $pk, $key); - } - foreach ($dropConditions as $event) { - - $setKey = $modelName . '_' . $event['type']; - switch ($event['type']) { - case 'create': - if ($event['param'] && $event['value']) { - $setKey .= "_" . $event['param'] . "_" . $event['value']; - } - self::addToSet($setKey, $key); - self::log("ID " . $setKey . ' ' . $key); - break; - case 'update': - $setKey .= '_' . $event['param']; - if ($event['conditions']) { - foreach ($event['conditions'] as $param => $value) { - if (!is_array($value)) { - $value = [$value]; - } - foreach ($value as $val) { - $paramSetKey = $setKey . "_" . $param . "_" . $val; - self::addToSet($paramSetKey, $key); - self::log("ID " . $paramSetKey . ' ' . $key); - } - } - } else { - self::addToSet($setKey, $key); - self::log("ID " . $setKey . ' ' . $key); - } - break; - default: - continue; - break; - } + self::initialize(); + $attrs = $model->getAttributes(); + $changed = []; + if ($changedAttributes) { + $attrNames = array_keys($changedAttributes); + foreach ($attrNames as $attrName) { + if (array_key_exists($attrName, $attrs)) { + $changed[$attrName] = $attrs[$attrName]; } } } - } - - /** - * @param ActiveRecord $model - * @param $key - */ - public static function insertKeyForPK(ActiveRecord $model, $key) - { - /*$keys = $model->getPrimaryKey(true); - $pk = reset($keys); - ActiveQueryCacheHelper::log( - "RK " . $key - ); - CacheHelper::addToSet($model->tableName() . "_" . $pk, $key);*/ - } - - /** - * @param int $count - * @param int $page - * - * @return array - */ - public static function getProfileRecords($count = 100, $page = 1) - { - $records = []; - $end = $count * $page; - $start = ($count * ($page - 1)); - $jsonEntries = self::getListMembers("cacheLog", $start, $end); - foreach ($jsonEntries as $entry) { - $records[] = json_decode($entry, true); - } - return $records; - } + $args = [ + $model->tableName(), - /** - * @return array - */ - public static function getProfileStats() - { - $stats = [ - 'get' => 0, - 'hit' => 0, - 'miss' => 0, - 'empty' => 0, + json_encode($attrs, self::$jsonOptions), + json_encode($changed, self::$jsonOptions) ]; - foreach (self::$types as $key => $typeName) { - $stats[$key] = self::getRedis()->get('cacheResult' . $key); - if ($key == self::PROFILE_RESULT_HIT_ALL || $key == self::PROFILE_RESULT_HIT_ONE) { - $stats['get'] += $stats[$key]; - $stats['hit'] += $stats[$key]; - } - if ($key == self::PROFILE_RESULT_MISS_ALL || $key == self::PROFILE_RESULT_MISS_ONE) { - $stats['get'] += $stats[$key]; - $stats['miss'] += $stats[$key]; - } - if ($key == self::PROFILE_RESULT_EMPTY_ALL || $key == self::PROFILE_RESULT_EMPTY_ONE) { - $stats['empty'] += $stats[$key]; - $stats['miss'] += $stats[$key]; - } - } - return $stats; + CacheHelper::evalSHA(self::$shaInvalidate, $args, 0); } - /** - * @return integer - */ - public static function getProfileRecordsCount() - { - return self::getListLength('cacheLog'); - } - /** - * @param ActiveRecord $className - * @param string|null $param - * @param string|null $value - */ - public static function dropCachesForCreateEvent($className, $param = null, $value = null) + public static function dropCachesForCreateEvent($model, $param = null, $value = null) { - $type = 'create'; - $keys = []; - if (!$param) { - $keys = self::getEvents($className::tableName(), $type, $keys); + if ($param) { + $model->$param = $value; + self::dropCaches($model, [$param => $value]); } else { - if (!is_array($value)) { - $value = [$value]; - } - foreach ($value as $val) { - $keys = self::getEvents($className::tableName(), $type . "_" . $param . '_' . $val, $keys); - } - } - - foreach ($keys as $key) { - self::profile(self::PROFILE_RESULT_DROP_DEPENDENCY, $key['key']); - \Yii::$app->cache->delete($key['key']); - self::removeFromSet($key['setKey'], $key['key']); + self::dropCaches($model); } } } diff --git a/ActiveRecordTrait.php b/ActiveRecordTrait.php index 6ce3d39..3175cc3 100644 --- a/ActiveRecordTrait.php +++ b/ActiveRecordTrait.php @@ -2,6 +2,8 @@ namespace sitkoru\cache\ar; +use yii\db\ActiveRecord; + /** * Class ActiveRecordTrait * @@ -19,10 +21,9 @@ public static function find() public function afterSave($insert, $changedAttributes) { - \Yii::info( - ($insert ? "Insert" : "Update") . " " . get_called_class() . ": " . json_encode($this->attributes), - 'cache' - ); + /** + * @var $this ActiveRecord + */ parent::afterSave($insert, $changedAttributes); $this->insert = $insert; ActiveQueryCacheHelper::dropCaches($this, $changedAttributes); @@ -30,12 +31,18 @@ public function afterSave($insert, $changedAttributes) public function afterDelete() { + /** + * @var $this ActiveRecord + */ parent::afterDelete(); ActiveQueryCacheHelper::dropCaches($this); } public function refresh() { + /** + * @var $this ActiveRecord + */ ActiveQueryCacheHelper::dropCaches($this); return parent::refresh(); @@ -75,6 +82,7 @@ private function applyDropConditions(CacheActiveQuery $query) $query->dropCacheOnCreate($param, $this->$value); } } + return $query; } diff --git a/CacheActiveQuery.php b/CacheActiveQuery.php index f8972f3..eb2b556 100644 --- a/CacheActiveQuery.php +++ b/CacheActiveQuery.php @@ -4,6 +4,7 @@ use yii\db\ActiveQuery; use yii\db\ActiveRecord; +use yii\helpers\ArrayHelper; /** * Class CacheActiveQuery @@ -12,68 +13,156 @@ */ class CacheActiveQuery extends ActiveQuery { + + private $dropConditions = []; - private $noCache = false; + private $disableCache = false; + /** * @inheritdoc */ public function all($db = null) { - $command = $this->createCommand($db); - $rawSql = $command->rawSql; - $key = $this->generateCacheKey($rawSql, 'all'); - /** - * @var ActiveRecord[] $fromCache - */ - ActiveQueryCacheHelper::log( - 'LA ' . $key - ); - $fromCache = \Yii::$app->cache->get($key); - if (!$this->noCache && $fromCache) { - - $resultFromCache = []; - if ($fromCache == ['null']) { - ActiveQueryCacheHelper::profile(ActiveQueryCacheHelper::PROFILE_RESULT_EMPTY_ALL, $key, $rawSql); - ActiveQueryCacheHelper::log( - 'SEA ' . $key - ); - } else { - ActiveQueryCacheHelper::profile(ActiveQueryCacheHelper::PROFILE_RESULT_HIT_ALL, $key, $rawSql); - ActiveQueryCacheHelper::log( - 'SA ' . $key - ); + ActiveQueryCacheHelper::initialize(); + + if (!$this->disableCache) { + $command = $this->createCommand($db); + $key = $this->generateCacheKey($command->rawSql, 'all'); + + /** + * @var ActiveRecord[] $fromCache + */ + $fromCache = CacheHelper::get($key); + if ($fromCache) { + + $resultFromCache = []; foreach ($fromCache as $i => $model) { - $key = $i; + $index = $i; if ($model instanceof ActiveRecord) { - //restore key - ActiveQueryCacheHelper::insertKeyForPK($model, $key); $model->afterFind(); } + //index by if (is_string($this->indexBy)) { - $key = $model instanceof ActiveRecord ? $model->{$this->indexBy} : $model[$this->indexBy]; + $index = $model instanceof ActiveRecord ? $model->{$this->indexBy} : $model[$this->indexBy]; } - $resultFromCache[$key] = $model; + $resultFromCache[$index] = $model; } - } - return $resultFromCache; + return $resultFromCache; + } else { + $models = parent::all($db); + if ($models) { + $this->insertInCacheAll($key, $models); + } + + return $models; + } } else { - ActiveQueryCacheHelper::log( - 'MA ' . $key - ); - ActiveQueryCacheHelper::profile( - $this->noCache ? ActiveQueryCacheHelper::PROFILE_RESULT_NO_CACHE : ActiveQueryCacheHelper::PROFILE_RESULT_MISS_ALL, - $key, - $rawSql - ); - $models = parent::all($db); - if (!$this->noCache) { - $this->insertInCacheAll($key, $models); + return parent::all($db); + } + + } + + /** + * @inheritdoc + */ + public function one($db = null) + { + ActiveQueryCacheHelper::initialize(); + if (!$this->disableCache) { + $command = $this->createCommand($db); + $key = $this->generateCacheKey($command->rawSql, 'one'); + /** + * @var ActiveRecord $fromCache + */ + $fromCache = CacheHelper::get($key); + if ($fromCache) { + if (is_string($fromCache) && $fromCache === 'null') { + $fromCache = null; + } else { + if ($fromCache instanceof ActiveRecord) { + $fromCache->afterFind(); + } + } + + return $fromCache; + } else { + $model = parent::one(); + if ($model) { + $this->insertInCacheOne($key, $model); + } + if ($model && $model instanceof ActiveRecord) { + return $model; + } else { + return null; + } } + } else { + return parent::one(); + } + } + + /** + * @param bool $value + * @return static + */ + public function asArray($value = true) + { + if ($value) { + $this->disableCache = true; + } + + return parent::asArray($value); + } - return $models; + /** + * @param $key + * @param ActiveRecord[] $models + * + * @return bool + */ + private function insertInCacheAll($key, $models) + { + $toCache = []; + if ($models) { + array_map(function ($model) use $toCache { + $copy = clone $model; + $copy->fromCache = true; + $toCache[$k] = $copy; + }, $models) } + $this->insertInCache($key, $toCache); + + return true; + } + + /** + * @param $key + * @param ActiveRecord $model + * + * @return bool + */ + private function insertInCacheOne($key, $model) + { + /** @var $class ActiveRecord */ + $copy = clone $model; + $copy->fromCache = true; + $this->insertInCache($key, $copy); + + return true; + } + + private function insertInCache($key, $toCache) + { + $conditions = $this->getDropConditions(); + $args = [ + $key, + zlib_encode(serialize($toCache), ZLIB_ENCODING_DEFLATE), + json_encode($conditions), + ActiveQueryCacheHelper::getTTL() + ]; + CacheHelper::evalSHA(ActiveQueryCacheHelper::$shaCache, $args, 1); } /** @@ -85,9 +174,7 @@ public function all($db = null) */ private function generateCacheKey($sql, $mode) { - $key = $mode; - $key .= strtolower($this->modelClass); - $key .= $sql; + $key = $mode . strtolower($this->modelClass) . $sql;; if (count($this->where) === 0 && count($this->dropConditions) === 0) { $this->dropCacheOnCreate(); } @@ -98,11 +185,8 @@ private function generateCacheKey($sql, $mode) if ($this->offset > 0) { $key .= 'offset' . $this->offset; } - ActiveQueryCacheHelper::log( - 'G ' . $sql . ': ' . md5($key) - ); - return md5($key); + return 'q:' . md5($key); } /** @@ -113,231 +197,171 @@ private function generateCacheKey($sql, $mode) */ public function dropCacheOnCreate($param = null, $value = null) { - if (!is_array($value)) { - $value = [$value]; + /** + * @var ActiveRecord $className + */ + $className = $this->modelClass; + $tableName = $className::tableName(); + if (!array_key_exists($tableName, $this->dropConditions)) { + $this->dropConditions[$tableName] = []; } - - foreach ($value as $val) { - $event = [ - 'type' => 'create', - 'param' => $param, - 'value' => $val - ]; - $this->dropConditions[] = $event; + if ($param) { + if (!array_key_exists($param, $this->dropConditions[$tableName])) { + $this->dropConditions[$tableName][$param] = []; + } + $this->dropConditions[$tableName][$param][] = [$param, $value]; + } else { + $this->dropConditions[$tableName]['create'] = true; } + return $this; } /** - * @param $key - * @param ActiveRecord[] $models + * @param string $param + * @param null|array $condition * - * @return bool + * @return self */ - private function insertInCacheAll($key, $models) + public function dropCacheOnUpdate($param, $conditions = null) { - /** @var $class ActiveRecord */ - $class = $this->modelClass; - $indexes = [ - $class::tableName() => [ - ] - ]; - if ($models) { - $toCache = $models; - foreach ($toCache as $index => $model) { - $mToCache = clone $model; - $mToCache->fromCache = true; - $toCache[$index] = $mToCache; - $pks = $mToCache->getPrimaryKey(true); - $indexes[$class::tableName()][] = reset($pks); - } - } else { - $toCache = ['null']; - $indexes[$class::tableName()][] = null; - $this->generateDropConditionsForEmptyResult(); + /** + * @var ActiveRecord $className + */ + $className = $this->modelClass; + $tableName = $className::tableName(); + if (!isset($this->dropConditions[$tableName][$param])) { + $this->dropConditions[$tableName][$param] = []; } + $cond = '*'; + if ($conditions) { + $cond = ['conditions' = $conditions]; + } + $this->dropConditions[$tableName][$param][] = $cond; - ActiveQueryCacheHelper::insertInCache($key, $toCache, $indexes, $this->dropConditions); - - return true; + return $this; } /** + * @return array */ - private function generateDropConditionsForEmptyResult() + private function getDropConditions() { - $conditions = 0; - if (count($this->where) !== 0) { - $where = $this->getParsedWhere(); - foreach ($where as $condition) { - $column = $condition[0]; - $operator = $condition[1]; - $value = $condition[2]; - if (in_array( - $operator, - [ - 'NOT IN', - '!=', - '>', - '<', - '>=', - '<=' - ], - true - )) { - continue; + $this->fillDropConditions(); + + $conditions = []; + foreach ($this->dropConditions as $tableName => $entries) { + $table = [$tableName]; + $tableConditions = []; + foreach ($entries as $column => $values) { + if ($column === 'create' && $values === true) { + $tableConditions[] = []; + } else { + foreach ($values as $value) { + if (is_array($value)) { + if (array_key_exists('conditions', $value)) { + $arr = []; + foreach ($value as $key => $val) { + if ($key === 'conditions') { + foreach ($val as $dep => $cond) { + if (is_array($cond)) { + foreach ($cond as $condValue) { + $arr[] = [$dep, $condValue]; + } + } else { + $arr[] = [$dep, $cond]; + } + + } + } else { + $arr[] = [$column, $val]; + } + } + $tableConditions[] = $arr; + } else { + if (array_key_exists(1, $value) && is_array($value[1])) { + foreach ($value[1] as $val) { + $tableConditions[] = [[$value[0], $val]]; + } + } else { + $tableConditions[] = [$value]; + } + } + } else { + $tableConditions[] = [[$column, $value]]; + } + } } - $this->dropCacheOnCreate($column, $value); - $conditions++; } + $table[] = $tableConditions; + $conditions[] = $table; } - if ($conditions === 0) { - $this->dropCacheOnCreate(); - } - } - - protected function getParsedWhere() - { - $parser = new WhereParser(\Yii::$app->db); - $data = $parser->parse($this->where, $this->params); - return $data; + return $conditions; } /** - * @inheritdoc + * @return array */ - public function one($db = null) + private function fillDropConditions() { - $command = $this->createCommand($db); - $rawSql = $command->rawSql; - $key = $this->generateCacheKey($command->rawSql, 'one'); - /** - * @var ActiveRecord $fromCache - */ - ActiveQueryCacheHelper::log( - 'LO ' . $key - ); - $fromCache = \Yii::$app->cache->get($key); - if (!$this->noCache && $fromCache) { - if (is_string($fromCache) && $fromCache === 'null') { - ActiveQueryCacheHelper::profile(ActiveQueryCacheHelper::PROFILE_RESULT_EMPTY_ONE, $key, $rawSql); - ActiveQueryCacheHelper::log( - 'SEO ' . $key - ); - $fromCache = null; - } else { - ActiveQueryCacheHelper::profile(ActiveQueryCacheHelper::PROFILE_RESULT_HIT_ONE, $key, $rawSql); - ActiveQueryCacheHelper::log( - 'SO ' . $key - ); - if ($fromCache instanceof ActiveRecord) { - //restore key - ActiveQueryCacheHelper::insertKeyForPK($fromCache, $key); - $fromCache->afterFind(); - } + foreach ($this->from as $tableName) { + if (!array_key_exists($tableName, $this->dropConditions)) { + $this->dropConditions[$tableName] = []; } + if (count($this->where) !== 0) { + $where = $this->getParsedWhere(); + foreach ($where as $condition) { + list($column, $operator, $value) = $condition; + if (in_array( + $operator, + [ + 'NOT IN', + '!=', + '>', + '<', + '>=', + '<=' + ], + true + )) { + continue; + } + $this->dropConditions[$tableName][$column][] = [$column, $value]; - return $fromCache; - } else { - ActiveQueryCacheHelper::profile( - $this->noCache ? ActiveQueryCacheHelper::PROFILE_RESULT_NO_CACHE : ActiveQueryCacheHelper::PROFILE_RESULT_MISS_ONE, - $key, - $rawSql - ); - ActiveQueryCacheHelper::log( - 'MO ' . $key - ); - $model = parent::one(); - if (!$this->noCache) { - $this->insertInCacheOne($key, $model); - } - if ($model && $model instanceof ActiveRecord) { - return $model; - } else { - return null; + } + } elseif (!$this->dropConditions[$tableName]) { + $this->dropConditions[$tableName]['create'] = true; } } - } - /** - * @param $key - * @param ActiveRecord $model - * - * @return bool - */ - private function insertInCacheOne($key, $model) - { - /** @var $class ActiveRecord */ - $class = $this->modelClass; - if ($model) { - $keys = $model->getPrimaryKey(true); - $pk = reset($keys); - $indexes = [ - $class::tableName() => [ - $pk - ] - ]; - $toCache = clone $model; - $toCache->fromCache = true; - } else { - $toCache = 'null'; - $indexes[$class::tableName()] = ['null']; - $this->generateDropConditionsForEmptyResult(); - } - ActiveQueryCacheHelper::insertInCache($key, $toCache, $indexes, $this->dropConditions); - - return true; + return $this->dropConditions; } /** - * @param string $param - * @param null|array $condition - * - * @return self + * @return array */ - public function dropCacheOnUpdate($param, $condition = null) + protected function getParsedWhere() { - $event = [ - 'type' => 'update', - 'param' => $param, - 'conditions' => [] - ]; - if ($condition) { - foreach ($condition as $param => $value) { - $event['conditions'] = [$param => $value]; - } - } - $this->dropConditions[] = $event; + $parser = new WhereParser(\Yii::$app->db); + $data = $parser->parse($this->where, $this->params); - return $this; + return $data; } + /** * @return static */ public function noCache() { - $this->noCache = true; + $this->disableCache = true; return $this; } - /** - * @param bool $value - * @return static - */ - public function asArray($value = true) - { - if ($value) { - $this->noCache = true; - } - - return parent::asArray($value); - } - /** * @return int */ @@ -351,4 +375,17 @@ public function deleteAll() return $class::deleteAll($this->where, $params); } + + /** + * @return ActiveRecord|null + */ + public function any() + { + $query = clone $this; + $result = $query->limit(1)->noCache()->one(); + unset($query); + + return $result; + } + } diff --git a/CacheHelper.php b/CacheHelper.php index 38d635c..6ebc9b4 100644 --- a/CacheHelper.php +++ b/CacheHelper.php @@ -19,43 +19,27 @@ public static function getRedis() return \Yii::$app->cache->redis; } - public static function addToSet($setKey, $member) + public static function loadScript($path) { - return static::getRedis()->executeCommand("SADD", [$setKey, $member]); - } - - public static function getSetMembers($setKey) - { - return static::getRedis()->executeCommand("SMEMBERS", [$setKey]); - } - - public static function removeFromSet($setKey, $member) - { - return static::getRedis()->executeCommand("SREM", [$setKey, $member]); - } + $script = file_get_contents($path); - public static function addToList($listKey, $member) - { - return static::getRedis()->executeCommand("LPUSH", [$listKey, $member]); + return static::getRedis()->executeCommand('SCRIPT', ['load', $script]); } - public static function getListMembers($listKey, $start = 0, $length = -1) + public static function scriptExists($sha) { - return static::getRedis()->executeCommand("LRANGE", [$listKey, $start, $length]); + return reset(static::getRedis()->executeCommand('SCRIPT', ['exists', $sha])); } - public static function getListLength($listKey) + public static function evalSHA($sha, $args, $numKeys) { - return static::getRedis()->executeCommand("LLEN", [$listKey]); + return static::getRedis()->executeCommand('EVALSHA', [$sha, $args, $numKeys]); } - public static function deleteList($listKey) + public static function get($key) { - return static::getRedis()->executeCommand("DEL", [$listKey]); - } + $res = static::getRedis()->executeCommand('get', [$key]); - public static function increment($key) - { - return static::getRedis()->executeCommand("INCR", [$key]); + return $res !== false ? unserialize(zlib_decode($res)) : $res; } } diff --git a/lua/cache.lua b/lua/cache.lua new file mode 100644 index 0000000..7e496e1 --- /dev/null +++ b/lua/cache.lua @@ -0,0 +1,55 @@ +local key = KEYS[1] +local data = ARGV[1] +local dnfs = cjson.decode(ARGV[2]) +local timeout = tonumber(ARGV[3]) + + +-- Write data to cache +redis.call('setex', key, timeout, data) + + +-- A pair of funcs +local conj_schema = function (conj) + local parts = {} + for _, eq in ipairs(conj) do + table.insert(parts, eq[1]) + end + + return table.concat(parts, ',') +end + +local conj_cache_key = function (db_table, conj) + local parts = {} + for _, eq in ipairs(conj) do + table.insert(parts, eq[1] .. '=' .. tostring(eq[2])) + end + + return 'conj:' .. db_table .. ':' .. table.concat(parts, '&') +end + + +-- Update schemes and invalidators +for _, disj_pair in ipairs(dnfs) do + local db_table = disj_pair[1] + local disj = disj_pair[2] + for _, conj in ipairs(disj) do + -- Ensure scheme is known + redis.call('sadd', 'schemes:' .. db_table, conj_schema(conj)) + + -- Add new cache_key to list of dependencies + local conj_key = conj_cache_key(db_table, conj) + redis.call('sadd', conj_key, key) + -- NOTE: an invalidator should live longer than any key it references. + -- So we update its ttl on every key if needed. + -- NOTE: if CACHEOPS_LRU is True when invalidators should be left persistent, + -- so we strip next section from this script. + -- TOSTRIP + local conj_ttl = redis.call('ttl', conj_key) + if conj_ttl < timeout then + -- We set conj_key life with a margin over key life to call expire rarer + -- And add few extra seconds to be extra safe + redis.call('expire', conj_key, timeout * 2 + 10) + end + -- /TOSTRIP + end +end \ No newline at end of file diff --git a/lua/invalidate.lua b/lua/invalidate.lua new file mode 100644 index 0000000..e0bd4a3 --- /dev/null +++ b/lua/invalidate.lua @@ -0,0 +1,62 @@ +local db_table = ARGV[1] +local obj = cjson.decode(ARGV[2]) +local objChanged = cjson.decode(ARGV[3]) + + +-- Utility functions +local conj_cache_key = function(db_table, scheme, obj) + local parts = {} + for field in string.gmatch(scheme, "[^,]+") do + if (obj[field] ~= nil) then + table.insert(parts, field .. '=' .. tostring(obj[field])) + end + end + + return 'conj:' .. db_table .. ':' .. table.concat(parts, '&') +end + +local conj_cache_key_wild = function(db_table, scheme, obj, objChanged) + local parts = {} + local first = true + for field in string.gmatch(scheme, "[^,]+") do + if (objChanged[field] ~= nil and first) then + table.insert(parts, field .. '=*') + end + if (obj[field] ~= nil and first == false) then + table.insert(parts, field .. '=' .. tostring(obj[field])) + end + first = false + end + + return 'conj:' .. db_table .. ':' .. table.concat(parts, '&') +end + +local call_in_chunks = function(command, args) + local step = 1000 + for i = 1, #args, step do + redis.call(command, unpack(args, i, math.min(i + step - 1, #args))) + end +end + + +-- Calculate conj keys +local conj_keys = {} +local schemes = redis.call('smembers', 'schemes:' .. db_table) +for _, scheme in ipairs(schemes) do + table.insert(conj_keys, conj_cache_key(db_table, scheme, obj)) + table.insert(conj_keys, conj_cache_key_wild(db_table, scheme, obj, objChanged)) +end + + +-- Delete cache keys and refering conj keys +if next(conj_keys) ~= nil then + local cache_keys = redis.call('sunion', unpack(conj_keys)) + -- we delete cache keys since they are invalid + -- and conj keys as they will refer only deleted keys + redis.call('del', unpack(conj_keys)) + if next(cache_keys) ~= nil then + -- NOTE: can't just do redis.call('del', unpack(...)) cause there is limit on number + -- of return values in lua. + call_in_chunks('del', cache_keys) + end +end \ No newline at end of file