1
ソーシャルゲーム案件における
DB 分割の PHP 実装
~とにかく分割ですよ。 10 回じゃ足りない。 20 回くらい分割。~
株式会社インフィニットループ
佐々木 亨基
自己紹介
・佐々木 亨基
・ゆきこ yukicon
・ Twitter:@yukiconEx
・株式会社インフィニットループ所属
・札幌 MySQL 勉強会代表
・ PHP 歴は 4 年くらい
・現在仕事では PHP オンリー
・いい加減な人間...
インフィニットループについて
・北海道札幌市にあるシステム開発会社
 約 90 名(契約スタッフ・アルバイト含む)で活動中
 社長も含め、ほぼ全員がエンジニア
・主な開発実績(主にサーバサイドを担当)
 ブラウザ三国志 (2009)
 英雄クエ...
お題目
■ はじめに
・ DB 分割とは
・どうして DB 分割なんかするの?
・ユーザ単位による水平分割
・ DB 分割のデメリット
お題目
■ 実装のお話
・クラス設計
・使用例
・エイリアス
・水平分割
・水平分割された DB への問い合わせ
・トランザクション
・トランザクションの開始
・コミット
・まとめ
DB 分割とは
分割していない DB
A テーブル
B テーブル
C テーブル
D テーブル
E テーブル
F テーブル
DB 分割とは
分割した DB
A テーブル
B テーブル
E テーブル
F テーブル
E テーブル
F テーブル
E テーブル
F テーブル …
垂直分割
( テーブル単位での分割 )
水平分割
( 同テーブルの ID 単位による分割 )
C...
DB 分割とは
更にそれぞれがマスタスレーブ構成を取る
Master
Master
…
垂直分割
( テーブル単位での分割 )
水平分割
( 同テーブルの ID 単位による分割 )
Slave Slave Slave
Master
Slave ...
どうして DB 分割なんかするの?
■ 要件
数万~十数万の同時接続数に耐えられるシステム
■ 案件の特徴
ソーシャルゲーム案件の規模は予測が難しい
突然跳ねる事もあり、正確な規模が見積もれない
どうして DB 分割なんかするの?
・同時接続数万という要求は高い
・さらに予想を上回る可能性もある
・しかしその実さっぱり流行らない可能性もある
・スモールスタート可能
・かつ困った時にサーバ追加で解決できる構成
つまり
スケールアウト可能な...
どうして DB 分割なんかするの?
Web サーバは単純なサーバ追加で対応可能
しかし DB は簡単にはいかない
・・・ ×nWeb Web Web Web
DB × ?
どうして DB 分割なんかするの?
DB のスケールアウトと言えばマスタスレーブ構成
Master
Slave Slave Slave ・・・ ×n
SELECT
UPDATE
SELECT SELECT
どうして DB 分割なんかするの?
しかしマスタスレーブ構成のマスタサーバは 1 台
マスタサーバの更新性能がネックとなり
いずれ限界が来る
Master
Slave Slave Slave ・・・ ×n
SELECT
UPDATE
SELEC...
どうして DB 分割なんかするの?
マスタ 1 台で数万の同時接続数を捌くのは不可能
要件的にマスタスレーブ構成では破綻する
どうして DB 分割なんかするの?
ではどうする?
・ Fusion-io のような超高速ストレージを使う?
 →用意できるとは限らない
 →導入しても解決しなかった場合に詰む
・ MySQL Cluster を使う?
 →まだ枯れていない技術...
どうして DB 分割なんかするの?
マスタスレーブのセットを増やそう !
       |
   \   __   /
    _ (m) _ピコーン
       | ミ |
    /  ` ´   \
      ('A`)
     ...
ユーザ単位による DB 分割
ユーザ数が増えたなら UserDB を追加
Global が苦しくなったら更に垂直分割をする事で
スケールアウト可能
User1 User2 User3 …
水平分割された UserDB
ユーザに紐付くデータを一定...
DB 分割のデメリット
■ 設計でカバーする
・ DB 間を跨いだ JOIN ができない
→ 冗長なデータの持ち方をしてしまう
→ マスタデータは全 DB に持つなどして対策
・水平分割すればするほどパフォーマンスが下がる
 ※とにかく分割とか...
DB 分割のデメリット
■ できるだけライブラリ側で吸収する
・水平分割された DB の串刺し検索が大変
→ ユーザ一覧を持ってくるのですら一苦労
・複数 DB のトランザクション管理が煩雑
ここからは実装のお話
クラス設計
3 つのクラスから成っている
・ DatabaseAccess
 全 DB 、トランザクションを統括するクラス
・ DatabaseAccessNode
 単一 DB にアクセスするクラス
 マスタスレーブの切り替えも行う
 分割な...
クラス設計
図にすると
Master
Master
…
DatabaseAccess
Slave Slave Slave
Master
Slave Slave Slave
Slave Slave Slave
Master
Slave Slave...
使用例
// singleton なインスタンスを取得
$dba = DatabaseAccess::getInstance();
// グローバル DB から SELECT
$dba->getDBAN('global')->select('u...
使用例
// singleton なインスタンスを取得
$dba = DatabaseAccess::getInstance();
// グローバル DB から SELECT
$dba->getDBAN('global')->select('u...
エイリアス
エイリアスをつくって抽象化
class DatabaseAccess
{
function gb()
{
// global の DatabaseAccessNode オブジェクトを返す
return $this->getDBAN(...
エイリアス
// singleton なインスタンスを取得
$dba = DatabaseAccess::getInstance();
// グローバル DB から SELECT
$dba->gb()->select('user_id_tbl'...
エイリアス
// singleton なインスタンスを取得
$dba = DatabaseAccess::getInstance();
// グローバル DB から SELECT
$dba->gb()->select('user_id_tbl'...
水平分割
ID とサーバ ID をマッピングするテーブルを
グローバル DB に作成して管理
CREATE TABLE `id_partition_tbl` (
`id` bigint(20) unsigned NOT NULL AUTO_IN...
水平分割
ID の発行 ( 仮に server_id を 1 で指定 )
INSERT INTO id_partition_tbl (server_id) VALUE (1);
SELECT LAST_INSERT_ID();
 ↓
分割ルール...
水平分割
分割ルール
・テーブルによる管理
必ずテーブルを参照する必要がある
server_id をキャッシュするなどの工夫が必要
登録数の少ないサーバに振り分け
各サーバに重みをつけて振り分け
既存データを意図通りに再配置
など柔軟な対応が可...
水平分割
分割ルール
・剰余やハッシュによる振り分け
均等にバランシングされる
サーバ追加時に既存データの再配置が必要
$server_id = ($id % $server_cnt) + 1;
水平分割
分割ルール
・範囲による振り分け
ある程度意図を持ってバランシングできる
既存データに手をいれずサーバ追加が可能
ただし小回りは効かない
foreach ($range_arr as $range) {
if ($range['min...
水平分割された DB への問い合わせ
DatabaseAccessMultiNode クラスにより実現
複数 DB に同じ SQL を投げ、結果をマージ
使用者は複数 DB への問い合わせである事を意識
せず、単一 DB を扱うのと同様に記述す...
水平分割された DB への問い合わせ
__call と call_user_func_array によって実装
DatabaseAccessNode クラスに単一 DB へ問い合
わせる処理を追加すると、
DatabaseAccessMulti...
水平分割された DB への問い合わせ
レスポンスは型によって処理を振り分ける
$tmp_data = reset($tmp_data_arr);
if (is_numeric($tmp_data)) {
// 数値の場合は和を返す
$sum =...
水平分割された DB への問い合わせ
user_id をキーにした場合
UPDATE は対象レコードが存在しないため問題無いが
User1
(1000-1999)
User2
(2000-2999)
User3
(3000-3999)
UPDA...
水平分割された DB への問い合わせ
INSERT は気をつける必要がある
User1
(1000-1999)
User2
(2000-2999)
User3
(3000-3999)
INSERT INTO t (user_id, a) VAL...
水平分割された DB への問い合わせ
INSERT は気をつける必要がある
User1
(1000-1999)
User2
(2000-2999)
User3
(3000-3999)
INSERT INTO t (user_id, a) VAL...
水平分割された DB への問い合わせ
__call による実装は、あくまでも全 DB に同じ
クエリを投げているだけ
レスポンスも型によって機械的に対応している
INSERT のようにそれでは問題がある場合は、
専用メソッドを立てて対応する
トランザクション
トランザクションは DB 単位でかかるため
管理に気を使わなくてはいけない
// 対象のユーザ DB にトランザクション開始
$dba->beginTransactionToUser($user_id);
// この更新はグロ...
トランザクションの開始
複数 DB へのトランザクション開始方法は 2 通り
・最初にまとめて開始してしまう
・必要になった時点で開始する
トランザクションが必要な事がわかりきっている場合は最
初にまとめてしまう方が管理が楽かつ簡単
どれか 1...
トランザクションの開始
■ 最初にまとめて開始してしまう場合
トランザクションは短い方が良い
コネクションのコストによって無用にトランザクションが
長くならないようにマスタサーバへのコネクションを行
なってからまとめてトランザクションを開始する...
トランザクションの開始
■ 必要になった時点で開始する場合
ある DB に対してトランザクションをかけるが、
ある DB に対しては 10 回に 1 回くらいしかトランザクショ
ンが必要が無い処理の場合、必要になった時にトランザク
ションを開始...
コミット
各 DB に対してバラバラのタイミングでコミットを行うとつ
くりが複雑になり、データ不整合となるバグを引き起こす
可能性が高くなる
悪い例
// グローバル DB をアップデート
$dba->gb()->update();
// グロ...
コミット
コミットは必ず処理の最後にまとめて行うようにする
処理の順番によるデータ不整合に気を配る必要が無くなり
コーディングの難易度も下がる
// グローバル DB をアップデート
$dba->gb()->update();
// ユーザ D...
コミット
まとめてコミットと言っても順番にコミットするだけ
いわゆる 2 相コミットではないため、一部がコミットされ
てしまうと全体のロールバックは不可能
やはり途中でエラーとなった場合はデータ不整合となる
$commited_arr = ar...
コミット
途中でコミットがエラーとなった場合は、どの DB がコミッ
トされ、どの DB がコミットされていないのかをログに残す
} catch (Exception $e) {
if (0 < count($commited_arr)) {
...
コミット
ログを頼りに手で対応する事になってしまうが
実際データ不整合はほとんど起こらず
1 年で 1 回や 2 回という低い頻度のため
ログによる対応で問題となった事はない
まとめ
・エイリアスを用意
・複数 DB を束ねて管理するクラスを用意
抽象化は重要
抽象化する事で経験の浅いエンジニアでも扱える
・分割ルールは設計段階で破綻の無いように決めておく
必要であれば独自のルールによる振り分けを実装する
・トランザ...
求人募集
インフィニットループでは、エンジニアを募集しています
・社長も含めほぼ全員がプログラマで技術者に優しい環境
・勤務地は北海道札幌市、他社出向などは無し
・おいしい食べ物、自然いっぱい、花粉少ない、涼しい
・短い通勤時間、徒歩や自転車で...
ご清聴ありがとうございました
Upcoming SlideShare
Loading in...5
×

ソーシャルゲーム案件におけるDB分割のPHP実装

22,886

Published on

ソーシャルゲーム案件におけるDB分割のPHP実装
~とにかく分割ですよ。10回じゃ足りない。20回くらい分割。~
株式会社インフィニットループ 佐々木 亨基

2013/7/15にPHPMatsuri2013内で発表された講演のスライド

Published in: Technology
2 Comments
58 Likes
Statistics
Notes
  • a「18. DB 分割のデメリット ■ 設計でカバーする ・ DB 間を跨いだ JOIN ができない」 ⇒JOINは出来ますが、インデックスが効かないようです。 結果的に、パフォーマンスを下げるのでJOINはしてはいけない ⇒ できないになるかもですが。
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • a
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
No Downloads
Views
Total Views
22,886
On Slideshare
0
From Embeds
0
Number of Embeds
19
Actions
Shares
0
Downloads
112
Comments
2
Likes
58
Embeds 0
No embeds

No notes for slide

ソーシャルゲーム案件におけるDB分割のPHP実装

  1. 1. 1 ソーシャルゲーム案件における DB 分割の PHP 実装 ~とにかく分割ですよ。 10 回じゃ足りない。 20 回くらい分割。~ 株式会社インフィニットループ 佐々木 亨基
  2. 2. 自己紹介 ・佐々木 亨基 ・ゆきこ yukicon ・ Twitter:@yukiconEx ・株式会社インフィニットループ所属 ・札幌 MySQL 勉強会代表 ・ PHP 歴は 4 年くらい ・現在仕事では PHP オンリー ・いい加減な人間なので PHP の緩さは好き
  3. 3. インフィニットループについて ・北海道札幌市にあるシステム開発会社  約 90 名(契約スタッフ・アルバイト含む)で活動中  社長も含め、ほぼ全員がエンジニア ・主な開発実績(主にサーバサイドを担当)  ブラウザ三国志 (2009)  英雄クエスト (2010)   Lord of Knights(2012)  フォトバトラー (2012)   Vim 検定 (2012)   PHP 検定 (2013)  その他いろいろ
  4. 4. お題目 ■ はじめに ・ DB 分割とは ・どうして DB 分割なんかするの? ・ユーザ単位による水平分割 ・ DB 分割のデメリット
  5. 5. お題目 ■ 実装のお話 ・クラス設計 ・使用例 ・エイリアス ・水平分割 ・水平分割された DB への問い合わせ ・トランザクション ・トランザクションの開始 ・コミット ・まとめ
  6. 6. DB 分割とは 分割していない DB A テーブル B テーブル C テーブル D テーブル E テーブル F テーブル
  7. 7. DB 分割とは 分割した DB A テーブル B テーブル E テーブル F テーブル E テーブル F テーブル E テーブル F テーブル … 垂直分割 ( テーブル単位での分割 ) 水平分割 ( 同テーブルの ID 単位による分割 ) C テーブル D テーブル
  8. 8. DB 分割とは 更にそれぞれがマスタスレーブ構成を取る Master Master … 垂直分割 ( テーブル単位での分割 ) 水平分割 ( 同テーブルの ID 単位による分割 ) Slave Slave Slave Master Slave Slave Slave Slave Slave Slave Master Slave Slave Slave Master Slave Slave Slave
  9. 9. どうして DB 分割なんかするの? ■ 要件 数万~十数万の同時接続数に耐えられるシステム ■ 案件の特徴 ソーシャルゲーム案件の規模は予測が難しい 突然跳ねる事もあり、正確な規模が見積もれない
  10. 10. どうして DB 分割なんかするの? ・同時接続数万という要求は高い ・さらに予想を上回る可能性もある ・しかしその実さっぱり流行らない可能性もある ・スモールスタート可能 ・かつ困った時にサーバ追加で解決できる構成 つまり スケールアウト可能なシステム構成
  11. 11. どうして DB 分割なんかするの? Web サーバは単純なサーバ追加で対応可能 しかし DB は簡単にはいかない ・・・ ×nWeb Web Web Web DB × ?
  12. 12. どうして DB 分割なんかするの? DB のスケールアウトと言えばマスタスレーブ構成 Master Slave Slave Slave ・・・ ×n SELECT UPDATE SELECT SELECT
  13. 13. どうして DB 分割なんかするの? しかしマスタスレーブ構成のマスタサーバは 1 台 マスタサーバの更新性能がネックとなり いずれ限界が来る Master Slave Slave Slave ・・・ ×n SELECT UPDATE SELECT SELECT
  14. 14. どうして DB 分割なんかするの? マスタ 1 台で数万の同時接続数を捌くのは不可能 要件的にマスタスレーブ構成では破綻する
  15. 15. どうして DB 分割なんかするの? ではどうする? ・ Fusion-io のような超高速ストレージを使う?  →用意できるとは限らない  →導入しても解決しなかった場合に詰む ・ MySQL Cluster を使う?  →まだ枯れていない技術という印象  →制約も多く、導入は怖い
  16. 16. どうして DB 分割なんかするの? マスタスレーブのセットを増やそう !        |    \   __   /     _ (m) _ピコーン        | ミ |     /  ` ´   \       ('A`)      ノヽノヽ        くく
  17. 17. ユーザ単位による DB 分割 ユーザ数が増えたなら UserDB を追加 Global が苦しくなったら更に垂直分割をする事で スケールアウト可能 User1 User2 User3 … 水平分割された UserDB ユーザに紐付くデータを一定のルールで振り分けて格納 Global GlobalDB ユーザに紐付かない共通のデータを格納
  18. 18. DB 分割のデメリット ■ 設計でカバーする ・ DB 間を跨いだ JOIN ができない → 冗長なデータの持ち方をしてしまう → マスタデータは全 DB に持つなどして対策 ・水平分割すればするほどパフォーマンスが下がる  ※とにかく分割とかいうタイトルになってますが   あんまり分割しちゃダメです、最低限にしましょう
  19. 19. DB 分割のデメリット ■ できるだけライブラリ側で吸収する ・水平分割された DB の串刺し検索が大変 → ユーザ一覧を持ってくるのですら一苦労 ・複数 DB のトランザクション管理が煩雑
  20. 20. ここからは実装のお話
  21. 21. クラス設計 3 つのクラスから成っている ・ DatabaseAccess  全 DB 、トランザクションを統括するクラス ・ DatabaseAccessNode  単一 DB にアクセスするクラス  マスタスレーブの切り替えも行う  分割なしならこのクラスのみで完結 ・ DatabaseAccessMultiNode  水平分割された DB にまとめてアクセスするクラス
  22. 22. クラス設計 図にすると Master Master … DatabaseAccess Slave Slave Slave Master Slave Slave Slave Slave Slave Slave Master Slave Slave Slave Master Slave Slave Slave DatabaseAccessNode DatabaseAccessMultiNode
  23. 23. 使用例 // singleton なインスタンスを取得 $dba = DatabaseAccess::getInstance(); // グローバル DB から SELECT $dba->getDBAN('global')->select('user_id_tbl'); // 対象ユーザ ID のデータがある DB から SELECT $dba->getDBAN('user', $user_id)->select('user_tbl'); // 複数の対象ユーザ DB から SELECT $dban_arr = $dba->getDBANArr('user', $user_id_arr); $dbam = new DatabaseAccessMultiNode($dban_arr); $dbam->select('user_tbl');
  24. 24. 使用例 // singleton なインスタンスを取得 $dba = DatabaseAccess::getInstance(); // グローバル DB から SELECT $dba->getDBAN('global')->select('user_id_tbl'); // 対象ユーザ ID のデータがある DB から SELECT $dba->getDBAN('user', $user_id)->select('user_tbl'); // 複数の対象ユーザ DB から SELECT $dban_arr = $dba->getDBANArr('user', $user_id_arr); $dbam = new DatabaseAccessMultiNode($dban_arr); $dbam->select('user_tbl'); なんか難しい!
  25. 25. エイリアス エイリアスをつくって抽象化 class DatabaseAccess { function gb() { // global の DatabaseAccessNode オブジェクトを返す return $this->getDBAN('global'); } function user($user_id) { // user の DatabaseAccessNode オブジェクトを返す return $this->getDBAN('user', $user_id); } : :
  26. 26. エイリアス // singleton なインスタンスを取得 $dba = DatabaseAccess::getInstance(); // グローバル DB から SELECT $dba->gb()->select('user_id_tbl'); // 対象ユーザ ID のデータがある DB から SELECT $dba->user($user_id)->select('user_tbl'); // 複数のユーザ DB から SELECT $dba->user_multi($user_id_arr)->select('user_tbl'); だいぶすっきり
  27. 27. エイリアス // singleton なインスタンスを取得 $dba = DatabaseAccess::getInstance(); // グローバル DB から SELECT $dba->gb()->select('user_id_tbl'); // 対象ユーザ ID のデータがある DB から SELECT $dba->user($user_id)->select('user_tbl'); // 複数のユーザ DB から SELECT $dba->user_multi($user_id_arr)->select('user_tbl'); // 全ユーザ DB から SELECT $dba->user_all()->select('user_tbl'); 全ユーザ DB 用のエイリアスも用意すると便利
  28. 28. 水平分割 ID とサーバ ID をマッピングするテーブルを グローバル DB に作成して管理 CREATE TABLE `id_partition_tbl` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `server_id` tinyint(4) unsigned NOT NULL, PRIMARY KEY (`id`), KEY `server_id` (`id`,`server_id`) ) id server_id 1000 1 1001 2 1002 3
  29. 29. 水平分割 ID の発行 ( 仮に server_id を 1 で指定 ) INSERT INTO id_partition_tbl (server_id) VALUE (1); SELECT LAST_INSERT_ID();  ↓ 分割ルールに従ってサーバの割り当て  ↓ テーブルへの登録 UPDATE id_partition_tbl SET server_id = 3 WHERE id = 1000;
  30. 30. 水平分割 分割ルール ・テーブルによる管理 必ずテーブルを参照する必要がある server_id をキャッシュするなどの工夫が必要 登録数の少ないサーバに振り分け 各サーバに重みをつけて振り分け 既存データを意図通りに再配置 など柔軟な対応が可能 $server_id = getServerId('id_partition_tbl', $id);
  31. 31. 水平分割 分割ルール ・剰余やハッシュによる振り分け 均等にバランシングされる サーバ追加時に既存データの再配置が必要 $server_id = ($id % $server_cnt) + 1;
  32. 32. 水平分割 分割ルール ・範囲による振り分け ある程度意図を持ってバランシングできる 既存データに手をいれずサーバ追加が可能 ただし小回りは効かない foreach ($range_arr as $range) { if ($range['min'] <= $id and $id <= $range_info['max']) { $server_id = $range['server_id']; break; } }
  33. 33. 水平分割された DB への問い合わせ DatabaseAccessMultiNode クラスにより実現 複数 DB に同じ SQL を投げ、結果をマージ 使用者は複数 DB への問い合わせである事を意識 せず、単一 DB を扱うのと同様に記述する事がで きる // 単一 DB への問い合わせ $dba->user($user_id)->select('user_tbl'); $dba->user($user_id)->update('user_tbl'); // 複数 DB への問い合わせ $dba->user_multi($user_id_arr)->select('user_tbl'); $dba->user_multi($user_id_arr)->update('user_tbl');
  34. 34. 水平分割された DB への問い合わせ __call と call_user_func_array によって実装 DatabaseAccessNode クラスに単一 DB へ問い合 わせる処理を追加すると、 DatabaseAccessMultiNode クラスを経由して複 数 DB に問い合わせもする事ができる class DatabaseAccessMultiNode { function __call($func_name, $args = array()) { // 各 DB に対して実行 foreach ($this->dban_arr as $key => $dban) { $tmp_data_arr[] = call_user_func_array(array($dban, $func_name), $args); }
  35. 35. 水平分割された DB への問い合わせ レスポンスは型によって処理を振り分ける $tmp_data = reset($tmp_data_arr); if (is_numeric($tmp_data)) { // 数値の場合は和を返す $sum = 0; foreach ($tmp_data_arr as $tmp_data) { $sum += $tmp_data; } return $sum; } else if (is_array($tmp_data)) { // 配列の場合はマージして返す $data = array(); foreach ($tmp_data_arr as $tmp_data) { $data = array_merge($data, $tmp_data); } return $data; : :
  36. 36. 水平分割された DB への問い合わせ user_id をキーにした場合 UPDATE は対象レコードが存在しないため問題無いが User1 (1000-1999) User2 (2000-2999) User3 (3000-3999) UPDATE t SET a = a + 100 WHERE user_id IN (1000, 2000, 3000); UPDATE t SET a = a + 100 WHERE user_id IN (1000, 2000, 3000); UPDATE t SET a = a + 100 WHERE user_id IN (1000, 2000, 3000);
  37. 37. 水平分割された DB への問い合わせ INSERT は気をつける必要がある User1 (1000-1999) User2 (2000-2999) User3 (3000-3999) INSERT INTO t (user_id, a) VALUES (1000, 0), (2000, 0), (3000, 0); INSERT INTO t (user_id, a) VALUES (1000, 0), (2000, 0), (3000, 0); INSERT INTO t (user_id, a) VALUES (1000, 0), (2000, 0), (3000, 0);
  38. 38. 水平分割された DB への問い合わせ INSERT は気をつける必要がある User1 (1000-1999) User2 (2000-2999) User3 (3000-3999) INSERT INTO t (user_id, a) VALUES (1000, 0), (2000, 0), (3000, 0); INSERT INTO t (user_id, a) VALUES (1000, 0), (2000, 0), (3000, 0); INSERT INTO t (user_id, a) VALUES (1000, 0), (2000, 0), (3000, 0); 不要なレコードまで INSERT されてしまう
  39. 39. 水平分割された DB への問い合わせ __call による実装は、あくまでも全 DB に同じ クエリを投げているだけ レスポンスも型によって機械的に対応している INSERT のようにそれでは問題がある場合は、 専用メソッドを立てて対応する
  40. 40. トランザクション トランザクションは DB 単位でかかるため 管理に気を使わなくてはいけない // 対象のユーザ DB にトランザクション開始 $dba->beginTransactionToUser($user_id); // この更新はグローバル DB への更新のため // トランザクションの対象とならない $dba->gb()->update(); XA トランザクション… うっ…頭が… ( 分離レベルが SERIALIZABLE に限られる、  挙動が怪しいという事で、ミドルウェアに頼らず  アプリによる実装としました )
  41. 41. トランザクションの開始 複数 DB へのトランザクション開始方法は 2 通り ・最初にまとめて開始してしまう ・必要になった時点で開始する トランザクションが必要な事がわかりきっている場合は最 初にまとめてしまう方が管理が楽かつ簡単 どれか 1 つでもトランザクションがかかっていれば他の DB も更新処理時に自動でトランザクション状態となるオート モードも用意したが、管理できなくなる懸念があったため 使っていない
  42. 42. トランザクションの開始 ■ 最初にまとめて開始してしまう場合 トランザクションは短い方が良い コネクションのコストによって無用にトランザクションが 長くならないようにマスタサーバへのコネクションを行 なってからまとめてトランザクションを開始する // 対象の DB をマスタに接続 $dba->gb()->useMaster(); $dba->user($user_id)->useMaster(); // マスタに接続した DB を一斉にトランザクション開始 $dba->myBeginTransactionToConnectionMaster(); Global User コネクション BEGIN コネクション BEGIN Global User コネクション コネクション BEGIN BEGIN
  43. 43. トランザクションの開始 ■ 必要になった時点で開始する場合 ある DB に対してトランザクションをかけるが、 ある DB に対しては 10 回に 1 回くらいしかトランザクショ ンが必要が無い処理の場合、必要になった時にトランザク ションを開始する 別々のユーザ ID が対象になった際に両方のユーザ ID が同 じ DB に所属している場合など、既にトランザクションが開 始されている事もある // 対象のユーザ DB にトランザクション開始 $dba->beginTransactionToUser($user_id); // たまにしかここに来ないので、ここでトランザクション開始 // 既にトランザクション開始されているならスルーする If ($dba->user($other_user_id)->isTransaction()) { // 同じ DB の場合はここにはこない $dba->beginTransactionToUser($other_user_id); }
  44. 44. コミット 各 DB に対してバラバラのタイミングでコミットを行うとつ くりが複雑になり、データ不整合となるバグを引き起こす 可能性が高くなる 悪い例 // グローバル DB をアップデート $dba->gb()->update(); // グローバル DB をコミット $dba->commitToGlobal(); // ユーザ DB をアップデート $dba->user($user_id)->update(); // ユーザ DB をコミット $dba->commitToUser($user_id); ここでエラーが起こるとユーザ DB のみ更新されず データ不整合状態となる Global User UPDATE COMMIT UPDATE COMMIT
  45. 45. コミット コミットは必ず処理の最後にまとめて行うようにする 処理の順番によるデータ不整合に気を配る必要が無くなり コーディングの難易度も下がる // グローバル DB をアップデート $dba->gb()->update(); // ユーザ DB をアップデート $dba->user($user_id)->update(); // まとめてコミット $dba->allCommit(); Global User UPDATE UPDATE COMMIT COMMIT
  46. 46. コミット まとめてコミットと言っても順番にコミットするだけ いわゆる 2 相コミットではないため、一部がコミットされ てしまうと全体のロールバックは不可能 やはり途中でエラーとなった場合はデータ不整合となる $commited_arr = array(); foreach ($dban_arr as $dban) { $dban->commit(); $commited_arr[] = $dban->database_name; }
  47. 47. コミット 途中でコミットがエラーとなった場合は、どの DB がコミッ トされ、どの DB がコミットされていないのかをログに残す } catch (Exception $e) { if (0 < count($commited_arr)) { // 1 度以上コミットしたということはデータ不整合 $uncommited_arr = array(); foreach ($dban_arr as $dban) { if ($dban->isTransaction()) { // トランザクション中なら配列に含める $uncommited_arr[] = $dban->database_name; } } // エラーとなった DB 情報のログを残す logging(sprintf('commit error commited[%s] uncommited[%s]', implode(',', $commited_arr), implode(',', $uncommited_arr))); } }
  48. 48. コミット ログを頼りに手で対応する事になってしまうが 実際データ不整合はほとんど起こらず 1 年で 1 回や 2 回という低い頻度のため ログによる対応で問題となった事はない
  49. 49. まとめ ・エイリアスを用意 ・複数 DB を束ねて管理するクラスを用意 抽象化は重要 抽象化する事で経験の浅いエンジニアでも扱える ・分割ルールは設計段階で破綻の無いように決めておく 必要であれば独自のルールによる振り分けを実装する ・トランザクションの管理もできるだけ簡単にする ・特にコミットはデータ不整合の起こりやすいポイント ・ 2 相コミットではない ・一部コミットされると全体ロールバックは不可 ・データ不整合はコミット失敗のログを残す事で対応する DB 分割意外となんとかなる でも気をつけるところはちゃんと気をつけないとダメ
  50. 50. 求人募集 インフィニットループでは、エンジニアを募集しています ・社長も含めほぼ全員がプログラマで技術者に優しい環境 ・勤務地は北海道札幌市、他社出向などは無し ・おいしい食べ物、自然いっぱい、花粉少ない、涼しい ・短い通勤時間、徒歩や自転車で通勤が可能 ・ U ターン、 I ターン大歓迎 ・ PHP 開発エンジニア ・スマホ開発エンジニア ・ MySQL エンジニア ・インフラエンジニア
  51. 51. ご清聴ありがとうございました
  1. A particular slide catching your eye?

    Clipping is a handy way to collect important slides you want to go back to later.

×