15
15

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.

AWSAdvent Calendar 2014

Day 16

既存のコードをかえずにAdapterをかませることでMySQLからDynamoDBに移行したときの話

Last updated at Posted at 2014-12-16

はじめまして、こんにちわ。
AWS Advent Calendar 12月16日分を担当させていただきます。
mehosoです。

さて、いきなりですが、DynamoDBって便利ですよね!

  • 安い
  • 速い
  • 何と言っても負荷を気にしなくてもいい ←これに尽きます

そんなこんなで
既存のコードをかえずにAdapterをかませることでDynamoDBに移行したときの話
をしたいと思います。

経緯

あるところに、データベース負荷がボトルネックのPHPxMySQLのプロジェクトがありました。
そしてその横には「負荷対策をしろ」と突然言い渡された一人の男がおりました。

男はせっせとデータベースのチューニングをしてみたり、処理をキューイングにしてみたり、
しまいにはスケールアップをするべきだ!と嘆いてみたり...

当然MySQLは泣き止みません。

そこで男は DynamoDB化 を試みます。
男はちょっと横着で、既存のコードは変えたくない!と思いました。

下準備

①まずは AWS SDK for PHP をとりにいく
http://aws.amazon.com/jp/sdkforphp/

②適当なDynamoDBWrapperを拝借させていただく(ありがたやですね)
https://github.com/masayuki0812/dynamodb-php-wrapper

DynamoDBの準備

テーブル名:user_counts
KEY NAME TYPE
HASH user_id Number
- a_count Number
- b_count Number
- c_count Number
- d_count Number
- e_count Number
- f_count Number
- g_count Number
- last_update String

すみません今回至って簡単すぎる構成で、
プライマリーキーにuser_idを持つ各項目のカウントに利用するようなものを想定します。

既存コードの確認

user_counts_model.php

class User_counts_model extends CI_Model
{
    public function set($user_id, $data)
    {
        $this->db->insert('user_counts', $data)
    }

    public function get($user_id)
    {
        $this->db
            ->where('user_id', $user_id)
            ->get('user_counts')
        ->row();
    }

    public function countUp($column, $data)
    {
        $this->db
            ->set($column, $column.' + 1', false)
            ->where('user_id', $user_id)
            ->update('user_counts');
    }

    // 受け取ったカラムの値をリセットする
    public function reset_count($user_id, $columns)
    {
        foreach($columns as $column){
            if($column == 'last_update'){
                $this->db->set($column, date('Y-m-d'));
            }else{
                $this->db->set($column, 0);
            }
            $this->db
                ->where('user_id', $user_id)
                ->update('user_counts');
        }
    }

    // 複数のユーザーのカラムを一度にカウントアップする
    public function countUpUsers($column, $users){
        $this->db->set($column, $column.' + 1', false)
            ->where_in('user_id', $users)
            ->update('user_counts');
    }
}

かなり簡易的にまとめましたがざっくりこんな感じのプログラムがあったとします。
ここで思ったんです。
フレームワークのデータベースライブラリの内部処理を
MySQLからDynamoDBへスイッチしてあげればいいんだと...
(果たしてこれが良い判断だったのかどうか...笑)

DynamoDB用DAOを作成

dynamodb_dao.php

class Dynamodb_dao extends DynamoDBWrapper
{
    // テーブル名
    protected $tableName = 'user_count';
    // テーブルスキーマ
    protected $tableData = array(
        'HASH' => 'user_id::N',
        'COLUMNS' => array(
            'a_count::N',
            'b_count::N',
            'c_count::N',
            'd_count::N',
            'e_count::N',
            'f_count::N',
            'g_count::N',
            'last_update::S',
        )
    );
    private $updateData;
    private $userIds;

    /**
     * データをDynamoDBに格納する
     * @param object $data
     */
    public function putItem($data)
    {
        $params[$this->tableData['HASH']] = (int)$data->user_id;
        foreach($this->tableData['COLUMNS'] as $val){
            $column = $this->convertColumn($val);
            $name = $column[0];
            if(!empty($data->{$name})){
                $params[$val] = $this->_cast($column[1], $data->{$name});
            }
        }
        parent::putItem($params);
    }

    /**
     * DynamoDBからデータを取得する
     * (データがある場合はカラムを全てセットする)
     * @return array notification_count DATA
     */
    public function getItem()
    {
        $res = $this->getByKey($this->userIds[0], null, 1);
        if(empty($res[0])){
            return array();
        }
        $data = $res[0];
        foreach($this->tableData['COLUMNS'] as $val){
            $column = $this->convertColumn($val);
            $name = $column[0];
            if(!isset($data[$name])){
                $data[$name] = 0;
            }
        }
        return $data;
    }

    // updateする項目をセットする
    public function setUpdateData($key, $value)
    {
        $v = explode(' ', $value);
        if(count($v) == 3){
            $type = 'N';
            $action = 'ADD';
            $value = (int)$v[2];
        }else{
            $type = ($key == 'last_update') ? 'S' : 'N';
            $action = 'PUT';
            $value = $this->_cast($type, $value);
        }
        $this->updateData[$key.'::'.$type] = array($action, $value);
    }

    /**
     * user_idをセットする
     * @param array $ids (user_id)
     */
    public function setUserIds(array $ids)
    {
        $this->userIds = $ids;
    }

    /**
     * 事前にセットした条件でレコードのアップデートを実行する
     */
    public function updateItem()
    {
        $userIds = array_unique($this->userIds);
        foreach($userIds as $id){
            $key = array($this->tableData['HASH'] => (int)$id);
            parent::updateItem($key, $this->updateData);
        }

        $this->_reset();
    }

    private function _cast($type, $data)
    {
        return ($type == 'S') ? (string)$data : (int)$data;
    }

    private function _reset()
    {
        $this->updateData = array();
        $this->userIds = array();
    }
}

はじめにテーブル定義を書いといてあげることで、
次他のやつをDynamoDB化したいとなったときに汎用的に使えると思います。
またsetUpdateDataという関数ではMySQLのカラムの直接加算or減算などをうまく考慮してDynamoDBの仕様にあてはめています。

Adapterをかませる!

お待たせしました、これを書きたかったのですが、上のコードを変に改変しすぎて、かなり質素なものになってしまった予感です。

user_counts_adapter.php

class user_counts_adapter extends CI_Model
{
    public $ddb;

    public function __construct()
    {
        parent::__construct();
        $this->ddb = new Notification_Count_DDB();
    }

    public function insert($table, $data)
    {
        $this->ddb->putItem($data);
    }

    public function set($key, $value)
    {
        $this->ddb->setUpdateData($key, $value);
        return $this->_chain();
    }

    public function where($key, $value)
    {
        $this->ddb->setUserIds(array($value));
        return $this->_chain();
    }

    public function where_in($key,array $values)
    {
        $this->ddb->setUserIds($values);
        return $this->_chain();
    }

    public function update($table = null)
    {
        $this->ddb->updateItem();
    }

    public function row()
    {
        $res = $this->ddb->getItem();
        return empty($res) ? array() : (object)$res;
    }

    public function row_array()
    {
        return $this->ddb->getItem();
    }

    private function _chain()
    {
        return $this;
    }

    public function select($column = null)
    {
        return $this->_chain();
    }

    public function get($table = null)
    {
        return $this->_chain();
    }
}

これだけで既存のコードを変えずにMySQLに無駄に入れていたデータがなくなり、
無駄に走っていたUPDATEもなくなり、負荷もなくなり、やっと一息つけました。

Joinなどの処理がないMySQLのテーブルはほとんどこのパターンでDynamoDBに移行しちゃいました。

まとめ

本来なら設計の段階でDynamoDBの特性を活かしたベストなアーキテクトを組むべきなのですが、
ちょっと無理矢理にでも工数かけずに、MySQLネックなプログラムを改善したい。
そんな時には今回のようなやり方も良いのかもしれませんね!

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?