SONICMOOV Googleページ

PhalconとRedisとMySQLを上手に組み合わせて使う方法

PhalconとRedisとMySQLを上手に組み合わせて使う方法

  • このエントリーをはてなブックマークに追加

どうも!トーマスです。

今回は「PhalconでMySQLとRedisを上手く組み合わせて使いたい」をコンセプトに書きます!

 

課題

  • MySQLのINDEXにあわせてクエリを発行したい
  • コネクションの切り替えをしたい
  • MySQLから一回取得したデータは更新があるまでキャッシュしておきたい

 

完成品

ienaga/RedisPlugin(GitHub)
 

Composer

{
    "require": {
        "ienaga/phalcon-redis-plugin": "*"
    }
}

 

準備するもの

  • phpredis
  • YAML

 

phpredis

sudo yum install php-pecl-redis

 

YAML

sudo yum install libyaml libyaml-devel
sudo pecl install YAML
sudo vim /etc/php.d/yaml.ini
extension=yaml.so

 

これで準備OK!

 

簡単な使い方の説明


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;
}

 

では課題を一個一個、解消していきたいと思います。

 

MySQLのINDEXにあわせてクエリを発行したい

開発と設計が並行して行われているような場合

プログラムの実装者とMySQL設計で想定されるクエリに差異が生じる事があります。

なので、Phalconのmetadata取得時にMySQLのINDEX情報も一緒に取得します。

続きを読む

 

app/config/services.phpに追記

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

 

テストテーブル

* TABLE
    user_quest
* COLUMNS
    id: int
    user_id: int,
    quest_id: int
    quest_status: tiny_int
* INDEXES
    (user_id, quest_id)

 

テストクラス


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;
    }
}

 

実行

$userQuest = UserQuest::getLatestByQuestId(1, 100);

 

発行されるクエリ

SELECT * FROM `user_quest` WHERE `quest_id` = 100 AND `user_id` = 1 DESC LIMIT 1;

 

orz…

 

もちろん、INDEXはききません。

 

発行したいクエリ

SELECT * FROM `user_quest` WHERE `user_id` = 1 AND `quest_id` = 100 DESC LIMIT 1;

 

となるように機能を拡張します。

 


...省略


/** @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);

 

これで、INDEXにマッチしたクエリを発行できる!

 

コネクションの切り替えをしたい

app/config/config.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);
}

...省略

 

ymlでコネクション管理をする。


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

 

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

 

app/config/services.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;
        });

    }
}

 

MySQLから一回取得したデータは更新があるまでキャッシュしておきたい

find|findFirstで取得したデータをprefixで設定されたcolumnでキーを生成してキャッシュ。

更新があるまでredisにデータをキャッシュしてDBの負荷を減らしたい。

saveを経由したモデルを格納しておき、commit後にキャッシュを消す。

 

transaction, save


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);

}

 

auto clear


    /**
     * 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();
    }

 

まだまだ改善の余地ありだけど。

まずまずな仕上がりかなぁっと思います。

その後・・・

redisの公式サイトでも掲載してもらえました(^o^)

(ページの一番下だけど。。。)

http://redis.io/clients

 

 

  • このエントリーをはてなブックマークに追加

記事作成者の紹介

thomas(システムエンジニア)

この仕事を始める前、僕はコックさんをしてました。毎日四苦八苦しながらもゲーム作りを楽しんでます☆彡

システムエンジニア募集中!

×

SNSでも情報配信中!ぜひご登録ください。

×

SNSでも
情報配信中!
SONICMOOV Facebookページ SONICMOOV Twitter SONICMOOV Googleページ
システムエンジニア募集中!

新着の記事

mautic is open source marketing automation