33
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Phalconでソシャゲを作る[Redis + MySQL-Sharding編]

Last updated at Posted at 2015-06-01

目的

PhalconとRedisとMySQLを上手く組み合わせて使いたい。

完成品 GitHub

ienaga/RedisPlugin

開発環境

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ファイルを自動的に読み込む実装。

app/config/config.php

<?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を取得してキャッシュ。

app/config/services.php

...省略

$di->set('modelsMetadata', function () { return new \RedisPlugin\MetaData(); });

\RedisPlugin\MetaData.php

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に設定

app/config/database.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の数だけ設定
参照先は可変可能。

app/config/redis.yml

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に登録。

app/config/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後にキャッシュを消す

\RedisPlugin\RedisDb.php

<?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

ienaga/RedisPlugin

View関連もソシャゲ用に拡張だな。

33
35
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?