PhalconとRedisとMySQLを上手に組み合わせて使う方法
どうも!トーマスです。
今回は「PhalconでMySQLとRedisを上手く組み合わせて使いたい」をコンセプトに書きます!
課題
- MySQLのINDEXにあわせてクエリを発行したい
- コネクションの切り替えをしたい
- MySQLから一回取得したデータは更新があるまでキャッシュしておきたい
完成品
ienaga/RedisPlugin(GitHub)
Composer
1 2 3 4 5 6 7 |
[sourcecode lang="php"] { "require": { "ienaga/phalcon-redis-plugin": "*" } } [/sourcecode] |
準備するもの
- phpredis
- YAML
phpredis
1 2 3 |
[sourcecode lang="php"] sudo yum install php-pecl-redis [/sourcecode] |
YAML
1 2 3 4 5 6 |
[sourcecode lang="php"] sudo yum install libyaml libyaml-devel sudo pecl install YAML sudo vim /etc/php.d/yaml.ini extension=yaml.so [/sourcecode] |
これで準備OK!
簡単な使い方の説明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
[sourcecode lang="php"] use \RedisPlugin\Criteria; // LIST // Criteria::EQUAL = "="; // Criteria::NOT_EQUAL = "<>;"; // Criteria::GREATER_THAN = ">"; // Criteria::LESS_THAN = "<"; // Criteria::GREATER_EQUAL = ">="; // Criteria::LESS_EQUAL = "<="; // Criteria::IS_NULL = "IS NULL"; // Criteria::IS_NOT_NULL = "IS NOT NULL"; // Criteria::LIKE = "LIKE"; // Criteria::I_LIKE = "ILIKE"; // Criteria::IN = "IN"; // Criteria::NOT_IN = "NOT IN"; // Criteria::BETWEEN = "BETWEEN"; public static function findFirst($userId, $typeId) { $criteria = new Criteria(new self); return $criteria ->add("user_id", $userId) ->add("type_id", $typeId, Criteria::NOT_EQUAL) ->findFirst(); // SELECT * FROM `table_name` WHERE `user_id` = $userId AND `type_id` = $typeId LIMIT 1; } public static function find($userId, $typeId) { $criteria = new Criteria(new self); return $criteria ->add("user_id", $userId) ->add("type_id", $typeId, Criteria::LESS_THAN) ->find(); // SELECT * FROM `table_name` WHERE `user_id` = $userId AND `type_id` = $typeId; } [/sourcecode] |
では課題を一個一個、解消していきたいと思います。
MySQLのINDEXにあわせてクエリを発行したい
開発と設計が並行して行われているような場合
プログラムの実装者とMySQL設計で想定されるクエリに差異が生じる事があります。
なので、Phalconのmetadata取得時にMySQLのINDEX情報も一緒に取得します。
続きを読む
目次
app/config/services.phpに追記
1 2 3 |
[sourcecode lang="php"] $di->set("modelsMetadata", function () { return new \RedisPlugin\MetaData(); }); [/sourcecode] |
テストテーブル
1 2 3 4 5 6 7 8 9 10 11 |
[sourcecode lang="php"] * TABLE user_quest * COLUMNS id: int user_id: int, quest_id: int quest_status: tiny_int * INDEXES (user_id, quest_id) [/sourcecode] |
テストクラス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
[sourcecode lang="php"] use \RedisPlugin\Criteria; class UserQuest { ...省略 /** * 最新のクエスト情報 * @param int $userId * @param int $questId * @param \RedisPlugin\Criteria $criteria * @return UserQuest */ public static function getLatestByQuestId($userId, $questId, $criteria = null) { if ($criteria === null) $criteria = new Criteria(new self); $criteria ->limit(1) ->add("quest_id", $questId); return self::getsAvailableWithMemberId($userId, $criteria)->findFirst(); } /** * @param int $userId * @param \RedisPlugin\Criteria $criteria * @return \RedisPlugin\Criteria */ public static function getsAvailableWithUserId($userId, $criteria = null) { if ($criteria === null) $criteria = new Criteria(new self); $criteria ->add("user_id", $userId) return $criteria; } } [/sourcecode] |
実行
1 2 3 |
[sourcecode lang="php"] $userQuest = UserQuest::getLatestByQuestId(1, 100); [/sourcecode] |
発行されるクエリ
1 2 3 |
[sourcecode lang="php"] SELECT * FROM `user_quest` WHERE `quest_id` = 100 AND `user_id` = 1 DESC LIMIT 1; [/sourcecode] |
orz…
もちろん、INDEXはききません。
発行したいクエリ
1 2 3 |
[sourcecode lang="php"] SELECT * FROM `user_quest` WHERE `user_id` = 1 AND `quest_id` = 100 DESC LIMIT 1; [/sourcecode] |
となるように機能を拡張します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
[sourcecode lang="php"] ...省略 /** @var \Phalcon\Db\Index[] $indexes */ $indexes = $model->getModelsMetaData()->readIndexes($model->getSource()); $indexQuery = array(); if ($indexes) { foreach ($indexes as $key => $index) { $columns = $index->getColumns(); if (!isset($query[$columns[0]])) continue; $chkQuery = array(); foreach ($columns as $column) { if (!isset($query[$column])) break; $chkQuery[$column] = $query[$column]; } if (count($chkQuery) > count($indexQuery)) { $indexQuery = $chkQuery; } // PRIMARY優先 if ($key === 0) break; } } $query = array_merge($indexQuery, $query); [/sourcecode] |
これで、INDEXにマッチしたクエリを発行できる!
コネクションの切り替えをしたい
app/config/config.phpに追記
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
[sourcecode lang="php"] $dir = __DIR__ .'/../../app/'; $env = getenv('ENV'); // [Nginx] fastcgi_param ENV XXXX; $ignore_file = array('routing'); // 無視したいymlがあれば設定 $configYml = array(); if ($configDir = opendir($dir.'config')) { while (($file = readdir($configDir)) !== false) { $exts = explode('.', $file); if ($exts[1] !== 'yml') continue; $file_name = $exts[0]; if ($ignore_file && in_array($file_name, $ignore_file)) continue; $yml = yaml_parse_file($dir . "config/{$file_name}.yml"); $configYml = array_merge($configYml, $yml[$env]); if (isset($yml['all'])) { $configYml = array_merge($configYml, $yml['all']); } } closedir($configDir); } ...省略 [/sourcecode] |
ymlでコネクション管理をする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
[sourcecode lang="php"] prd: stg: dev: database: dbMaster: # master adapter: Mysql host: 127.0.0.1 port: 3301 username: root password: XXXXX dbname: XXXXX charset: utf8 options: 20: false transaction: true transaction_name: XXXXX dbSlave: # slave adapter: Mysql host: 127.0.0.1 port: 3311 username: root password: XXXXX dbname: XXXXX charset: utf8 options: 20: false transaction: false dbCommonMaster: # 共通DBのmaster adapter: Mysql host: 127.0.0.1 port: 3301 username: root password: XXXXX dbname: XXXXX charset: utf8 options: 20: false transaction: false dbCommonSlave: # 共通DBのsalve adapter: Mysql host: 127.0.0.1 port: 3311 username: root password: XXXXX dbname: XXXXX charset: utf8 options: 20: false transaction: false dbMember1Master: # ユーザDB1のmaster adapter: Mysql host: 127.0.0.1 port: 3306 username: root password: XXXXX dbname: XXXXX charset: utf8 options: 20: false transaction: true transaction_name: XXXXX # member1 dbMember1Slave: # ユーザDB1のslave adapter: Mysql host: 127.0.0.1 port: 3316 username: root password: XXXXX dbname: XXXXX charset: utf8 options: 20: false transaction: false dbMember2Master: # ユーザDB2のmaster adapter: Mysql host: 127.0.0.1 port: 3307 username: root password: XXXXX dbname: XXXXX charset: utf8 options: 20: false transaction: true transaction_name: XXXXX # member2 dbMember2Slave: # ユーザDB2のslave adapter: Mysql host: 127.0.0.1 port: 3317 username: root password: XXXXX dbname: XXXXX charset: utf8 options: 20: false transaction: false [/sourcecode] |
redis.ymlもDBの数だけ設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
[sourcecode lang="php"] prd: stg: dev: redis: enabled: true # false => cache off default: name: db expire: 3600 autoIndex: true prefix: # 対象のカラムがModelに存在したら使用。左から順に優先。存在が確認できた時点でbreak columns: column, column, column # e.g. user_id, id, social_id # 共通のマスタがあれば登録「table_」と共有部分だけの記載はtable_*と同義 # common common: dbs: table, table, table... # e.g. master_, access_log admin: # ユーザマスタ # e.g. # CREATE TABLE IF NOT EXISTS `admin_user` ( # `id` int(10) unsigned NOT NULL AUTO_INCREMENT, # `social_id` varchar(255) NOT NULL COMMENT 'ソーシャルID', # `admin_config_db_id` tinyint(3) unsigned NOT NULL COMMENT 'AdminConfigDb.ID', # `admin_flag` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '0=一般、1=管理者', # `status_number` tinyint(3) unsigned NOT NULL DEFAULT '0', # `created_at` datetime NOT NULL, # `updated_at` datetime NOT NULL, # PRIMARY KEY (`id`), # UNIQUE KEY `social_id` (`social_id`) # ) ENGINE=InnoDB DEFAULT CHARSET=utf8; model: XXXXX # AdminUser column: XXXXX # admin_config_db_id # ユーザマスタの登録「table_」と共有部分だけの記載はtable_*と同義 dbs: table, table, table... # e.g. admin_, user_ranking shard: enabled: true # Shardingを使用しないばあいはfalse # Shardingをコントロールするテーブルとカラム # # e.g. # CREATE TABLE IF NOT EXISTS `admin_config_db` ( # `id` int(10) unsigned NOT NULL AUTO_INCREMENT, # `name` varchar(50) NOT NULL COMMENT 'DBコンフィグ名', # `gravity` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '重み(振り分け用)', # `status_number` tinyint(3) unsigned NOT NULL DEFAULT '0', # PRIMARY KEY (`id`) # ) ENGINE=InnoDB DEFAULT CHARSET=utf8; # INSERT INTO `admin_config_db` (`id`, `name`, `gravity`, `status_number`) VALUES # (1, 'dbMember1', 50, 0), # (2, 'dbMember2', 50, 0); # shard config master control: model: XXXXX # AdminConfigDb column: XXXXX # name metadata: host: XXXXX port: 6379 select: 0 server: dbMaster: host: XXXXX port: 6379 select: 1 # redis select [データベースインデックス] dbSlave: host: XXXXX port: 6379 select: 1 dbCommonMaster: host: XXXXX port: 6379 select: 0 dbCommonSlave: host: XXXXX port: 6379 select: 0 dbMember1Master: host: XXXXX port: 6379 select: 2 dbMember1Slave: host: XXXXX port: 6379 select: 2 dbMember2Master: host: XXXXX port: 6379 select: 3 dbMember2Slave: host: XXXXX port: 6379 select: 3 [/sourcecode] |
app/config/services.phpに追記
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
[sourcecode lang="php"] use Phalcon\Db\Adapter\Pdo\Mysql as DbAdapter; use Phalcon\Mvc\Model\Transaction\Manager; ...省略 foreach ($config->get('database') as $db => $arguments) { $di->setShared($db, function () use ($arguments) { // 存在するDBすべて登録 return new DbAdapter($arguments->toArray()); }); // Master分トランザクションを登録 if (isset($arguments['transaction']) && $arguments['transaction']) { $di->setShared($arguments['transaction_name'], function() use ($db) { $manager = new Manager(); if ($db !== null) $manager->setDbService($db); return $manager; }); } } [/sourcecode] |
MySQLから一回取得したデータは更新があるまでキャッシュしておきたい
find|findFirstで取得したデータをprefixで設定されたcolumnでキーを生成してキャッシュ。
更新があるまでredisにデータをキャッシュしてDBの負荷を減らしたい。
saveを経由したモデルを格納しておき、commit後にキャッシュを消す。
transaction, save
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
[sourcecode lang="php"] try { RedisDb::beginTransaction(); $robot= new Robot(); $robot->setId($id); $robot->setType($type); RedisDb::save($robot); $user= new User(); $user->setId($id); $user->setName($name); RedisDb::save($user); RedisDb::commit(); } catch (Exception $e) { RedisDb::rollback($e); } [/sourcecode] |
auto clear
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
[sourcecode lang="php"] /** * autoClear */ public static function autoClear() { foreach (self::getModels() as $model) { self::setPrefix($model); self::connect($model, self::getPrefix()); $redis = self::getRedis($model); $redis->delete(self::getHashKey($model)); } self::$models = array(); } [/sourcecode] |
まだまだ改善の余地ありだけど。
まずまずな仕上がりかなぁっと思います。
その後・・・
redisの公式サイトでも掲載してもらえました(^o^)
(ページの一番下だけど。。。)