目的
PhalconとRedisとMySQLを上手く組み合わせて使いたい。
完成品 GitHub
開発環境
WEBサーバー:Nginx [EC2]
APPサーバー:php-fpm [EC2 + Auto Scaling]
Cacheサーバー:redis [ElastiCache]
DBサーバー:MySQL [RDS]
マスタDB:Master, Slava
ユーザDB:[Master, Slave], [Master, Slave]...
Framework:Phalcon 2.x
準備するもの
- phpredis
- YAML
phpredis
これがないとそもそもphpでredisが使えない。
sudo yum install php-pecl-redis
YAML
開発環境、STG環境、本番環境と環境の管理が楽。
sudo yum install libyaml libyaml-devel
sudo pecl install YAML
sudo vim /etc/php.d/yaml.ini
extension=yaml.so
app/config/config.php
app/config内のymlファイルを自動的に読み込む実装。
<?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);
}
$application = array(
'application' => array(
'controllersDir' => $dir . 'controllers/',
'modelsDir' => $dir . 'models/',
'viewsDir' => $dir . 'views/',
'pluginsDir' => $dir . 'plugins/',
'libraryDir' => $dir . 'library/',
'cacheDir' => $dir . 'cache/',
)
);
return new \Phalcon\Config(array_merge($application, $configYml));
これでapp/config内のymlは自動的に読み込まれるようになった。
準備完了☆彡
課題
- MySQLのINDEXにあわせてクエリを発行
- PhalconでSharding対応する為にコネクションの切り替えが必要
- DBから一回取得したデータは更新があるまでキャッシュしておきたい
[課題] MySQLのINDEXにあわせてクエリを発行
発行されるクエリがINDEXにマッチしない事があるので
INDEXにあわせてクエリを並び換える。
metadataを拡張。
ついでにSCHEMAもキャッシュしてしまう。
さらに、そのついでにINDEXを取得してキャッシュ。
...省略
$di->set('modelsMetadata', function () { return new \RedisPlugin\MetaData(); });
namespace RedisPlugin;
class MetaData extends \Phalcon\Mvc\Model\MetaData
{
...省略
public function write($key, $data)
{
// ここでSCHEMAをキャッシュ
$this->getRedis()->hSet(self::CACHE_KEY, $key, $data);
if (!$this->getRedis()->isTimeout(self::CACHE_KEY)) {
$options = $this->getOptions();
$this->getRedis()->setTimeout(self::CACHE_KEY, $options['lifetime']);
}
$this->_cache[$key] = $data;
// ついでにINDEXを取得してキャッシュ
$this->writeIndexes($key);
}
/**
* @param string $key
*/
public function writeIndexes($key)
{
if (!$key)
return;
$keys = explode('-', $key);
if (3 > count($keys))
return;
$source = array_pop($keys);
$class = '';
foreach (explode("_", $source) as $value) {
$class .= ucfirst($value);
}
/** @var \Phalcon\Mvc\Model $model */
$model = new $class;
$indexes = $model->getReadConnection()->describeIndexes($source);
$cacheKey = $this->getIndexesKey($source);
$this->getRedis()->hSet(self::CACHE_KEY, $cacheKey, $indexes);
$this->_cache[$cacheKey] = $indexes;
}
/**
* @param string $source
* @return null
*/
public function readIndexes($source)
{
...省略
}
}
~~~
INDEXの一覧はreadIndexesから取得が可能
~~~php
$indexes = $model->getReadConnection()->describeIndexes($source);
~~~
ここの取得の方法、もっといい方法ないかなぁ・・・
後はわたってきたクエリをINDEXにあわせて加工
```php:\RedisPlugin\RedisDb.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);
[課題] PhalconでSharding対応する為にコネクションの切り替えが必要
まずは使うDBの情報をymlに設定
prd:
stg:
dev:
database:
dbMaster:
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 # master
dbSlave:
adapter: Mysql
host: 127.0.0.1
port: 3311
username: root
password: XXXXX
dbname: XXXXX
charset: utf8
options:
20: false
transaction: false
dbCommonMaster:
adapter: Mysql
host: 127.0.0.1
port: 3301
username: root
password: XXXXX
dbname: XXXXX
charset: utf8
options:
20: false
transaction: false
dbCommonSlave:
adapter: Mysql
host: 127.0.0.1
port: 3311
username: root
password: XXXXX
dbname: XXXXX
charset: utf8
options:
20: false
transaction: false
dbMember1Master:
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:
adapter: Mysql
host: 127.0.0.1
port: 3316
username: root
password: XXXXX
dbname: XXXXX
charset: utf8
options:
20: false
transaction: false
dbMember2Master:
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:
adapter: Mysql
host: 127.0.0.1
port: 3317
username: root
password: XXXXX
dbname: XXXXX
charset: utf8
options:
20: false
transaction: false
redis.ymlもDBの数だけ設定
参照先は可変可能。
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
次にservices.phpに登録。
<?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;
});
}
}
find | findFirst 簡易版
use \RedisPlugin\RedisDb;
class Robot extends \Phalcon\Mvc\Model
{
/**
* @param int $id
* @param string $type
* @return Robot
*/
public static function findFirst($id, $type)
{
return RedisDb::findFirst(array(
'query' => array(
'id' => $id,
'type' => $type
)
), new self);
}
/**
* @param int $id
* @param string $type
* @return Robot[]
*/
public static function find($id, $type)
{
return RedisDb::find(array(
'query' => array(
'id' => $id,
'type' => $type
)
), new self);
}
// cache => falseで個別にキャッシュをコントロール
public static function no_cache($id, $type)
{
return RedisDb::find(array(
'query' => array(
'id' => $id,
'type' => $type
),
'cache' => false
), new self);
}
// autoIndex => falseで個別にキャッシュをコントロール
public static function no_autoIndex($id, $type)
{
return RedisDb::find(array(
'query' => array(
'id' => array(
'operator' => Criteria::IN,
'value' => array(1,6,10)
),
'type' => $type
),
'autoIndex' => false
), new self);
}
public static function order($id, $type)
{
return RedisDb::find(array(
'query' => array(
'id' => $id,
'type' => $type
),
'order' => 'id DESC'
), new self);
}
public static function group($id, $type)
{
return RedisDb::find(array(
'query' => array(
'id' => $id,
'type' => $type
),
'group' => 'id'
), new self);
}
public static function limit($id, $type)
{
return RedisDb::find(array(
'query' => array(
'id' => $id,
'type' => $type
),
'limit' => 10 // array('number' => 10, 'offset' => 5)
), new self);
}
}
find | findFirst 比較演算子
use \RedisPlugin\RedisDb;
use \RedisPlugin\Criteria;
class Robot extends \Phalcon\Mvc\Model
{
// 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 not_equal($id, $type)
{
return RedisDb::findFirst(array(
'query' => array(
'id' => array(
'operator' => Criteria::NOT_EQUAL,
'value' => $id
),
'type' => $type
)
), new self);
}
public static function in($id, $type)
{
return RedisDb::findFirst(array(
'query' => array(
'id' => array(
'operator' => Criteria::IN,
'value' => array(1,6,10)
),
'type' => $type
)
), new self);
}
public static function not_in($id, $type)
{
return RedisDb::find(array(
'query' => array(
'id' => array(
'operator' => Criteria::NOT_IN,
'value' => array(1,6,10)
),
'type' => $type
)
), new self);
}
public static function between($start, $end)
{
return RedisDb::findFirst(array(
'query' => array(
'id' => array(
'operator' => Criteria::BETWEEN,
'value' => array($start, $end)
),
'type' => $type
)
), new self);
}
}
Criteria
use \RedisPlugin\RedisDb;
use \RedisPlugin\Criteria;
class Robot extends \Phalcon\Mvc\Model
{
public static function findFirst($id, $type)
{
$criteria = new Criteria(new self);
return $criteria
->add('id', $id)
->add('type', $type, Criteria::NOT_EQUAL)
->group('type')
->findFirst();
}
public static function find($id, $start, $end)
{
$criteria = new Criteria(new self);
return $criteria
->add('id', array($id), Criteria::IN)
->add('type', array($start, $end), Criteria::BETWEEN)
->limit(10, 30)
->order('type DESC')
->find();
}
// ->cache($boolean)でキャッシュをコントロール
public static function no_cache($id, $start, $end)
{
$criteria = new Criteria(new self);
return $criteria
->add('id', array($id), Criteria::IN)
->add('type', array($start, $end), Criteria::BETWEEN)
->limit(10, 30)
->order('type DESC')
->cache(false)
->find();
}
// ->autoIndex($boolean)でautoIndexをコントロール
public static function no_autoIndex($id, $start, $end)
{
$criteria = new Criteria(new self);
return $criteria
->add('id', array($id), Criteria::IN)
->add('type', array($start, $end), Criteria::BETWEEN)
->limit(10, 30)
->order('type DESC')
->autoIndex(false)
->find();
}
}
transaction, save
▼transaction
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);
}
[課題] 取得したデータは更新があるまでキャッシュしておきたい
find|findFirstで取得したデータをprefixで設定されたcolumnでキーを生成してキャッシュ。
更新があるまでredisにデータをキャッシュしてDBの負荷を減らしたい。
saveを経由したモデルを格納しておき、commit後にキャッシュを消す
<?php
namespace RedisPlugin;
class RedisDb
{
...省略
/**
* 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();
}
こんな感じで実装のベースは完成。
まだまだ改善の余地ありだな。。。
いけてない所があれば、どしどしご指摘ください。
完成品 GitHub
View関連もソシャゲ用に拡張だな。