unoh.github.com

MemcacheとMySQLのデータ不整合をPropelPDOで解決してみる

2010-10-25 08:26:55 +0000

こんにちは市丸です。

Zynga Japanでは単純なPrimaryKeyをキャッシュする際、symfonyのPeerをオーバーライドし自動的にキャッシュ&クリアしています。

UserPeer.php

class UserPeer extends BaseUserPeer
{
    public static function retrieveByPK($pk, PropelPDO $con = null)
    {
        if (!$data = /* Cacheからとるよ */) {
            $data = parent::retrieveByPK($pk, $con);
            /* "User_$pk"みたいなキーでCacheするよ */
        }
        return $data;
    }
    public static function doInsert($values, PropelPDO $con = null)
    {
        /* Cacheクリアするよ */
        return parent::doInsert($values, $con);
    }

    public static function doUpdate($values, PropelPDO $con = null)
    {
        /* Cacheクリアするよ */
        return parent::doUpdate($values, $con);
    }

    public static function doDelete($values, PropelPDO $con = null)
    {
        /* Cacheクリアするよ */
        return parent::doDelete($values, $con);
    }
}

ただし、ソーシャルゲームのように、2人以上のユーザーが同じテーブルを参照する場合、MemcacheとMySQLのデータ不整合に気をつけないといけません。

bad.png

このように、トランザクションのcommit前に別のユーザーからアクセスされると、CacheとMySQLのデータに不整合が起こります。

good.png

正しくは、このようにcommit後にキャッシュを削除しなければなりません。

そこで、PropelPDOを改変しcommit後にキャッシュ削除しましょう。

MemPropelPDO.php

class MemPropelPDO extends PropelPDO {
    protected $memcacheKeys = array();

    public function addMemcacheKey($key) {
        $this->memcacheKeys[] = $key;
    }

    public function commit(){
        parent::commit();
        foreach($this->memcacheKeys as $key){
            // よしなにCache削除してくだされ。
        }
    }    
}

database.yml

all:
 db1:
   class:        sfPropelDatabase
   param:
     classname:  MemPropelPDO
     (略)

User.php

class User extends BaseUser
{
    public function save(PropelPDO $con = null)
    {
        if (!is_null($con)) {
            $con->addMemcacheKey("User_".$this->getPrimaryKey())
        }
        return parent::save($con);
    }
}

【仕上げ】トランザクションを使う場合はsaveメソッドにMemPropelPDOを渡します

        $userModel = UserPeer::retrieveByPK(1);
        $userModel->setStatus(2); //statusを1->2にしてみる

        $con = Propel::getConnection(); // database.yml で指定したMemPropelPDO
        $con->beginTransaction();
        try {
            $userModel->save($con);
            $con->commit();
        } catch (Exception $e) {
            $con->rollback();
            throw $e;
        }

まあ、忘れずにcommit後にキャッシュクリアすればいいんだけどね!(爆)