Symfony2でのXATransaction

この記事は、PHP Advent Calendar 2014 - Qiitaの記事です。

1年に1度くらい、「あー二相コミットしてーなー」って時、PHPエンジニアだとあるかもしれません。正直僕にはありません。
とはいえ、時々「二相コミット」って言葉聞きますが、どういうものかイマイチ分からないので、非常に上辺だけですが、実装をしてみました。

そもそも二相コミットとは?

  • Two Phase commitでぐぐるといろいろ出てきます
  • すごく単純にいうと複数にデータベースを分割している場合に通常のトランザクションでは保証できない部分を保証できる仕組みです。

通常の二相コミットを使わず複数にデータベースを分けている場合のトランザクション
以下の様な感じになると思います。

$first = $doctrine->getConnection('first');
$second = $doctrine->getConnection('second');
$first->beginTransaction();
$second->beginTransaction();
try {

    // なんらかのクエリ

    $first->commit(); // (1)
    $second->commit(); //(2)
} catch (\Exception $e) {
    $first->rollback();
    $second->rollback();
}

この場合、(2)の時点でなんらかの例外が発生した場合には(1)のcommitが有効になるため、'first'のロールバックは有効になりません。

二相コミットの場合は、(1)と(2)のコミット前にが「ちゃんとコミットできるよね?」と確認してから、実際のコミットを行います。

この二相コミットはMySQLでは、XA Transactionという名前で実装されています。
http://dev.mysql.com/doc/refman/5.6/en/xa.html

XAトランザクションのクエリの流れをざっくりと書きますと、以下のようになります。(例は1つのDBでのトランザクションです)

XA START xid

//ここでなんらかのSQLを発行

XA END xid
XA PREPARE xid -- ここで COMMITできるかの確認
XA COMMIT xid -- 実際のCOMMIT

実装

namespaceは適当なんですがこういう感じの実装をしてみました。

TwoPhaseConnection.php

https://gist.github.com/d82b933f88c17aed28d8

DoctrineのConnectionクラスをXA対応にしたものです。
Connetionクラス自体は結構拡張ポイントがあって
``Doctrine\DBAL\Connections\MasterSlaveConnection``あたりの実装を見ると結構参考になります。
app/config/config.ymlのDBの項目に以下の様な指定をすることにより利用可能になります。

    wrapper_class: XaTestBundle\Doctrine\Connections\TwoPhaseConnection
TransactionManager.php

https://gist.github.com/45e336db353cd0b77fe8
複数のトランザクションをまとめて管理するためのクラスです。
こっちは、複数のトランザクションをまとめて処理するためのクラスです。

実際の利用イメージは以下の様になります。

    $first = $doctrine->getConnection('first');
    $second = $doctrine->getConnection('second');
    $transactionSuccess = new TransactionManager([
        $first,
        $second,
    ]);

    $transactionSuccess->beginTransaction();
    try {

        // なんらかのクエリ

        $transactionSuccess->commit();
    } catch (\Exception $e) {
        $transactionSuccess->rollback();
    }

$transactionSuccess->commit();の中の実装をもう少し見て行きたいと思います。

public function commit()
{
    $successConnections = [];
    try {
        foreach ($this->connections as $connection) {
            $connection->prepareTwopahce();
            $successConnections[] = $connection;
        }
    } catch (Exception $e) {
        $this->rollback();
    }

    foreach ($this->connections as $connection) {
        $connection->commit();
    }
}

$connection->prepareTwopahce();をすることで、すべてのconnectionがcommitできることを確認していることがわかります。

テスト用に簡単な、コマンドを実装してみました。
https://gist.github.com/ede09c3ddb612daa0c38

実行してみるとSQLのログは、以下のようになります。

doctrine.DEBUG: "START TWO PHACE TRANSACTION"  
doctrine.DEBUG: XA START '433713564548829db945bd'  
doctrine.DEBUG: "START TWO PHACE TRANSACTION"  
doctrine.DEBUG: XA START '810041109548829db9461a'  
doctrine.DEBUG: INSERT INTO transaction1 (data) VALUES (?)
["433713564548829db945bd"] 
doctrine.DEBUG: INSERT INTO transaction2 (data) VALUES (?)
["810041109548829db9461a"] 
doctrine.DEBUG: "END TWO PHACE TRANSACTION"  
doctrine.DEBUG: XA END '433713564548829db945bd'  
doctrine.DEBUG: "PREPARE TWO PHACE TRANSACTION"  
doctrine.DEBUG: XA PREPARE '433713564548829db945bd'  
doctrine.DEBUG: "END TWO PHACE TRANSACTION"  
doctrine.DEBUG: XA END '810041109548829db9461a'  
doctrine.DEBUG: "PREPARE TWO PHACE TRANSACTION"  
doctrine.DEBUG: XA PREPARE '810041109548829db9461a'  
doctrine.DEBUG: "COMMIT TWO PHACE TRANSACTION"  
doctrine.DEBUG: XA COMMIT '433713564548829db945bd'  
doctrine.DEBUG: "COMMIT TWO PHACE TRANSACTION"  
doctrine.DEBUG: XA COMMIT '810041109548829db9461a'  

doctrine.DEBUG: "START TWO PHACE TRANSACTION"  
doctrine.DEBUG: XA START '1804794638548829db9a041'  
doctrine.DEBUG: "START TWO PHACE TRANSACTION"  
doctrine.DEBUG: XA START '1858734663548829db9a0a2'  
doctrine.DEBUG: INSERT INTO transaction1 (data) VALUES (?)
["1804794638548829db9a041"] 
doctrine.DEBUG: INSERT INTO transaction2 (data) VALUES (?)
["1858734663548829db9a0a2"] 
doctrine.DEBUG: "END TWO PHACE TRANSACTION"  
doctrine.DEBUG: XA END '1804794638548829db9a041'  
doctrine.DEBUG: XA ROLLBACK '1804794638548829db9a041'  
doctrine.DEBUG: "END TWO PHACE TRANSACTION"  
doctrine.DEBUG: XA END '1858734663548829db9a0a2'  
doctrine.DEBUG: XA ROLLBACK '1858734663548829db9a0a2'  

それぞれのDBに2回ずつ系4回INSERT文を発行していますが、2回目のクエリはROLLBACKしているため、DBには1件ずつのデータしか入ってないことが確認できます。

mysql> select * from xatest_first.transaction1;
+------------------------+
| data                   |
+------------------------+
| 433713564548829db945bd |
+------------------------+
1 row in set (0.00 sec)

select * from xatest_second.transaction2;
+------------------------+
| data                   |
+------------------------+
| 810041109548829db9461a |
+------------------------+
1 row in set (0.00 sec)

なんとなく動いてますね!!

最後に言い訳

だいたい実装概要は「あ、これで動きそう」というところまでは来たのですが、
正直まだまだ、課題があります。

  • 実案件に使う余地がないので、テストしてない
  • ライブラリに切り出すべき
  • トランザクションリカバー的なことをしていない see: http://www.slideshare.net/takezoe/ss-35337478
  • MySQLべったりなコード
  • 二相コミットといえどPREPAREのあとのCOMMITで1つ目をコミットして2つめのトランザクションをコミットしようとする間にサーバ障害などが発生すると、結局トランザクション的不整合を残すことになる
  • パフォーマンス的に良くないという記述を見かける
  • 実は↑考えると無理に二相コミットするメリットってあんまりないんじゃないかと思えてくる

これだけマイナスポイント言っておけば、使おうという人はいないと思いますが・・・。
もし、「あー将来的に使うかも」という人がいたら、お声がけいただければ、ゆるゆる実装を進めたいと思います。

とはいえ、基本概念などは他のORMでも使えるものだと思いますので、PHPで二相コミットをやってみる一助になれば幸いでございます。

最後に、ブログの更新がおくれて申し訳ありませんでした。