なぜ「共有データの整合性」が重要なのか? ゲームにおけるサーバーサイド設計のいろは

正しいゲーミング Web サーバーの作り方 #2/2 >> 1はこちら

ゲーム本体のデータストア設計のコツ

幾田雅仁氏(以下、幾田):続いて「RDBMS+NoSQL」です。

先ほどと同じ流れですが、ゲーム本体のデータストアの設計のコツを1つ挙げると「共有データの整合性を担保する」ことです。

ゲームの本体以外のマイクロサービスって、裏側はNoSQLがメインデータでぜんぜんOKなんです。DynamoDBも使いますし、他にもCloud Searchとか、いろんなデータベースを使っている。でも実は、ゲームの本体だけは、なかなかRDBMSをやめられない。

まず、RDBMSを使いましょう。これが鉄板です。トランザクションで整合性や一貫性を守る。それから水平分割で負荷分散する。ここは正直難易度が高いところではあるんですが、仕方がないですね。

次に、垂直分割は可能な限り避けましょう。最後にNoSQLを組み合わせるべきではあるけれど、キャッシュ用途で頼りすぎないように気をつけてください。NoSQLはそれぞれ意味があって使うべきものであって、決して、せっかくのNoSQLをキャッシュで使わないように気を付けましょう。memchachedとかもあるんですけど(笑)。

垂直分割とキャッシュがだめというのはけっこう意外というか、こういうことを言っている人はあまりいないのかなと思っています。先ほどと同じように、失敗例から理由を説明していきます。

どんな失敗かというと、まず、ゲームのメインデータベースにNoSQLを使っちゃう。NoSQLをメインDBにしようという代表的な理由は、まずは速度ですね。ゲームは1回のリクエストで複数のテーブルの1レコードだけを更新するので、更新速度がほしい。

次に、DBサーバが落ちるとけっこう慌てますよね。基本的に単一障害点なので、バックアップから復旧するのか、レプリカを上に昇格するのかという話も出てくるんですけど、まぁ運用がつらい。

それから、SQLが難しいという話ですね。テーブル設計も勉強しなきゃいけない。トランザクション分離レベルも4つ言えますか? それから、デッドロックを回避するためにはどうしたらいいのか、そもそもデッドロックの検知はどうやればいいのか。みなさん、デッドロックのエラーログを読んで、しっかり理解してピンポイントで修正できますか? けっこう大変なんですよ。

結果、RDBMSを使っているのに、速度をもとめて整合性を犠牲にした設計をし始める。そして事故が起こるんです。これでRDBMSが嫌いになっちゃう人も多いです。それでNoSQLで結果整合性どうなのかな? とか。さらにはNoSQLで強い整合性を自作できないかと考えていくわけです。

なんでこんなことを言っているかというと、実際にお取引先でこういうことを考えている人達がいたんです。止めたんですけど、当時はポジションパワーがなくてだめでした。

CASとは何か?

結果整合性の例から見ていきます。「CAS」をご存知でしょうか? ご存知でなくても安心してください。ゆっくり説明していきます。

CASは、Compare And Swapの略です。整合性を守れる。CASで整合性を守れる範囲はドキュメントたった1つに限定されます。なので、関係性の高いデータを1つのドキュメントに閉じ込める必要があります。

まず、取得ですね。今回レースコンディションをCASで解決する説明なので、もう1個プロセスを置くんですけど、(スライドを指して)こんな感じでデータをP1が取得・P2が取得ってやります。

その後データを修正してあげて、書き戻すんですね。

その後、さらにもう1つのプロセスのデータも書き戻します。書き戻すんですが、前のドキュメントとバージョンが変わっていると失敗します。

そこでデータを再取得して、新しいデータをもとに更新を行ってデータを書き戻す。これで、レースコンディションが解決します。これがCASです。

もしCASの説明がわかりづらかったら、後でまた聞いてください。

ところが、整合性を守るためにドキュメントにどんどんデータを入れておくことによって、1個のドキュメントが超巨大になっていくんですよ。

そうすると、ネットワーク転送量がめっちゃ増えます。加えて、巨大なドキュメントをメモリに展開するので、メモリが溢れます。

最後に、これはあんまり問題にならないかもしれないですが、CASって、後ろから来る高速の処理に追い抜かれるんですよ。なので、めったにないですが、処理が無限に終わらない可能性があります。運が悪いと追い抜かれまくって、こんな短い処理なのにぜんぜん終わらない、ということがあります。

ちなみに、MongoDBがトランザクションを実装したことはみなさんご存知でしょうか?

結局、整合性というテーマに対してCASでは対応できなかったので、トランザクションという概念を持ち込まなきゃいけなかったんです。当たり前ですが、トランザクションという複雑なシステムを実装すると、ストアの処理速度は落ちちゃいます。

せっかく速さを売りにしてたのに、トランザクションを実装したらストアの速度が落ちるって当たり前の話です。さらにMongoの開発者が、なるべく単一ドキュメントでがんばって、CASでがんばってねって公言してるんですよ。速度が落ちちゃうから。

要はCASのトランザクションの意味とか特性を完全に理解して使いわけることを、開発者側が利用者に求めています。それが、今のMongoDBのトランザクションの実態です。ところで、XAトランザクションはいつ実装されるんでしょうね。

強整合性に挑戦したものの…

これらを解決するために、無謀にも強整合性に挑戦した例が身近でありました。何年か前に大取引先が開発して、gumi名義でパブリッシュしたゲームなんですが。

どういうことかというと、単一プロセスでがんばるという技があります。

こういうことです。リクエストがいっぱいきます。メッセージブローカーを間において、たくさんのリクエストに1個のプロセスでがんばる。1個のプロセスだから、処理が全部シーケンシャルに並ぶので、整合性が担保されるという作戦です。

当然のことなんですが、ゲームのリクエストはすごく多いから全くスケールしません。せめてリードだけでもということで、こういうかたちで書き込みだけメッセージブローカーを通して書き込んで、リードだけはみんな自由にやろうぜって組むこともあります。

ところが、ゲームは更新が多いのでやっぱりだめなんです。しかも中途半端な更新途中の状態が見れてしまいます。

だって、ドキュメント分断したらそうですよね。1個ドキュメントを更新しているときに、あっち見たりこっち見たりするんだから、更新途中のデータが見れちゃう。これではぜんぜん整合性、一貫性を担保できません。

「Mutex Lock」を使おう、という話もありました。片方はロックを獲得するので書き込める。片方はロック待ちになるから、なんと処理がシーケンシャルに並ぶ。これで整合性が取れるぞ、と思う。ところで、ロックの開放忘れをどうするんでしょうか。例えば、1回ロック取ったあとにプロセスが落ちました。ロック開放されません。どうしましょう。

それから、デッドロックをどうするか。複数のドキュメントをロックしまくって、そのうちデッドロックします。それから効率を求めて、共有ロックとか排他ロックとか実装し始める。「排他だけじゃどうもいかん。リードしかしないから共有ロックでいいよ」みたいな話もあるんです。はい、ロック沼へようこそ。

(会場笑)

ロックはめちゃくちゃ奥深いので、簡単には実装できないです。

応答速度を稼ぐための3つのポイント

結果整合性とか、強整合性みたいな話をしてきましたが、問題がたくさんあります。負荷が高くなっちゃいますし、スケールしないし、難易度が高い。

なので、せめてゲーム本体はRDBMSを使ったほうがいいです。「応答速度を稼ぎたい」「機能障害による停止を防ぎたい」「開発難易度を下げたい」そういう問題は、ほぼ誤解です。

速度を稼ぎたければ、正しい設計と運用をしてください。停止を防ぎたければ、マネージドなサービスを使ってください。開発速度を落としたくないという話もありましたが、さっきの事例を見ていると、NoSQLでやってもけっこうつらいでしょう。

ということで、応答速度を稼ぐにはどうしたらいいかというと、SQLのアンチパターンを徹底的に避ける。それから水平、垂直分割をする。NoSQLを補助として活用する。

まず「SQLのアンチパターンを避ける」について。

「ゲーム開発だから、アンチパターンは必要悪」という話をたまに聞きます。例えば、利便性と速度を得るために正規化を崩したり、柔軟性のためにEntity Attribute Valueパターンを適用したり。EAVはけっこう有名なので検索してみてください。

ものすごく端的に言うと、MySQLでKey-Valueストアをがんばって実装するという感じです。gumiにそれ用のライブラリがあって僕も見たことがありますが、それを駆逐するのに1年かかりました。アンチパターンが必要悪というのは誤認です。

アンチパターンはアンチパターンで問題が発生します。それでいろいろRDBMSを使ったにも関わらず、たくさん事故が起き、NoSQLを採用しようという話に流れていって、事故が起こる。

この本(『SQLアンチパターン』)は2013年ぐらいに出たんですけど、この辺をちゃんと勉強して、アンチパターンに陥らないように気を付けて設計していきましょう。

SQLアンチパターン

水平、垂直分割について

問題は、水平・垂直分割について。ここはけっこう難しいので説明していきます。

まず水平分割ですね。

ゲームというものは、ユーザがアクセスしてくるので、ユーザで負荷分散して、負荷分散の単位で水平分割をする。なのでユーザIDで負荷分散、分割キーはユーザIDになります。

それから、ホストの引き当ては計算で行います。

ユーザIDを投げ入れたらdb_hostを返してくる関数を1個作って、各プロセス、プログラムにばら撒いていきます。要は、同じ値を入れたら同じ結果が返る副作用のない純粋な関数を置いて、ユーザIDからdb_hostに変換するという計算処理をする。

もう1つ、こういうやり方があります。

MySQLでもMongoでもDynamoでもRedisでもいいんですが、外のストアにdb_hostとユーザIDの組み合わせを置いておいて「このユーザIDはこっちのホストだよ」というマッピング表を置くパターン。これ、保存場所変えたりしてうれしいよね。

実際、後から入社してきた社員から、gumiがあまりにも計算にこだわっているので「なんでこっちで作らないの?」という質問をよく受けます。

これは、(スライドを指して)こういうことなんです。

単一障害点なんですよ。これが止まったら全部止まります。「そんなわけあるか」と。単一障害点を作らないようにこっち側のシステムをZooKeeperで管理する方法もなくはないんですけど、ゲームのサーバを作るのにそんなものを持ち出してがんばりたいですか?

素直に計算できるなら、計算したほうがいいです。計算できないからZooKeeperを使ってるんです。計算できないパターンって私はあまり見かけたことがなくて、今のところは計算できています。ZooKeeperはマイクロサービスのサービスディスカバリーとかで使うのが一般的だと思います。

次。インデックスは、分割キーを含む複合キーを付けるのがポイントです。

InnoDB限定の話になるんですけど、レコードロックって実はインデックスで行うんです。1個もインデックスがないテーブルが存在したとして、そこに検索をかけるとテーブルロックがかかります。

最大ロックの粒度は、ユーザ単位にするべきなんです。1人のユーザが2個のスマホでやれたら問題ですけど、基本的にユーザは1人で操作するので、その人の単位でロックを掛けておけばほとんどぶつかりません。なので、最大ロック粒度はユーザにしておくべきです。

さらにもっと細かいレベルでロックの粒度を小さくしているのは、ユーザのロック粒度が大きくあって、その中でアイテムとかでロック粒度を小さくしていくのがポイントなんですね。なので、必ずユーザIDのインデックスを入れてください。

それからこの例でいくと、新しいアイテムを検索掛けたいと思ったときに、ユーザIDとアイテムを複合キーにしないとだめです。なぜかというと、アイテムIDで仮にインデックスを貼ってしまうと、そのアイテムID全部に対してロックがかかり始めるんです。

ものすごくわかりやすくいうと、Booleanのフィールドがあったとして、そこにインデックスを貼ったら、TRUEで検索かけたときにTRUEフィールドが全部ロックされます。ものすごい広域ロックだと思いません? ジャイアントロックもいいところです。

なので、必ずユーザIDを全てのインデックスに含めるのがポイントです。どちらか片方でいいです。

最低限、1つのテーブルにはユーザのインデックスが存在して、さらに何かインデックスを作る場合は、必ずユーザIDを混ぜてインデックスを作るのがポイントです。

垂直分割を避けるべき理由

分割キーの種別で垂直分割をする。さっき、垂直分割は避けるべきパターンだという話をしたんですけど、なぜかというと、垂直分割の場合は、ホスト引き当ての情報をテーブルに入れる必要があって、どうあがいても正規化が崩れちゃうんですよ。

「このプレイヤーが所属しているギルドはどこだ」というときに、プレイヤーで検索を掛けて、GUILD_IDを引っ張ってきますよね。だから、正規化が崩れちゃう。

そもそも「ギルドメンバー」ってテーブルがギルドのデータベースに存在して、そこに検索を掛ければいいんですが、データベースが分断しているからSQLを結合できない。だから、引っ張るためのキーを入れなきゃいけない。正規化が崩れる。これはもう仕方がない。いろいろ見てきましたけど、これだけは必要悪でした。

あとは、XAトランザクションを使う。

こういうことですね。垂直分割をすると、1回のリクエストでアクセスするホスト数が増えやすいんです。なので、XAトランザクションで整合性を担保する必要がある。分散型トランザクションです。

これは極論ですが、水平分割しかしてなかったら、実はXAトランザクションはいらないんですよ。なぜかというと、人間は1人なので、よその人とインストラクションが存在しなければ、1人遊びのゲームだったら1個のデータベースと自分のクライアントが通信さえしていれば、一切データベースのホストを跨がないのでXAトランザクションがいらない。ただのトランザクションでOKなんです。だけど垂直分割しちゃうから、XAトランザクションが必要になっちゃう。

垂直分割を使うということは、それだけ複雑なことを引き込まなきゃいけない。なので、垂直分割で負荷分散をするのは誤りです。負荷分散のために垂直分割をしているなら、今すぐやめて、水平分割をしてください。

垂直分割は、あくまでも1回のクエリで複数のユーザの一覧がほしいからやることで、負荷分散したいからやることではない。意味合いがぜんぜん違います。

実は、設計難易度も非常に高い。1回のリクエストでアクセスするホストの数が増えるから負荷分散しづらくて、難易度がすごく高いから設計にめちゃめちゃ気を使う。なので、垂直分割はなるべく避けるけど、使わざるを得ない時は使うというスタンスがいいです。

NoSQLを補助として活用する

最後に、NoSQLを補助として使うことについて。まず、レスポンスをキャッシュする例ですね。

まず、全てのリクエストにゲームのクライアントUUID4で生成したリクエストIDを付けてあげて、リクエストIDを常にリクエストデータに乗せてあげます。そして、リクエストIDを送ります。リクエストIDでロックを獲る。

ロックは非常に難しいです。とはいえ、Redisの公式サイトでDesign pattern who is SETNXというドキュメントを読めば、自力でちゃんとしたロックが実装できます。

リクエストIDでロックを獲ったあとに、リクエストIDで問い合わせをすると「まだデータがないよ」って返ってくるので、処理を行い、リクエストIDをキーにしてレスポンスデータをキャッシュしてあげて保存してあげて、リクエストIDを送ってロックを開放してレスポンスを返す。という一連の流れを、レスポンスキャッシュで行います。

レスポンスキャッシュをすると、Redisのメモリがすごく溢れやすいので、Redisを水平分割しておく必要があります。それから、なんでもかんでも全部乗せるとメモリが溢れるので、冪等性が必要なところだけキャッシングするように制御をかけます。

例えば電車に乗っていて、ガチャを引いている途中にトンネルに入ったら、通信が途切れちゃう。もう1回ガチャを引いたときに、ちゃんとAPIの冪等性が保証されていれば、大事なゲーム内通貨が重複して失われることもなくユニットが正しく手に入ります。

データ通信がここで失敗したときに、こうやってもう1回送ってあげると、ロック獲ってあげてデータ取ってあげてロック開放したときにレスポンスが返る。これで、冪等性の担保ができる。クライアントから見たときの結果整合性の担保ですね。

それから、ランキングのキャッシュ。

MySQL側に分断されたストアが存在していて、1ヵ所のRedisのSorted Setにまとめるパターンです。なぜこれを使っているかというと、例えば何らかの行動をして書き換えたあとに、こうやってSorted Setでランキング順が変動します。

例えば、1回のリクエストでデータベースへの更新が成功して、トランザクションのコミットが成功しました。コミットが完了して、キャッシュに書き込もうとしたときに通信障害で落ちました。

これがなぜ許容できるかというと、この後はどんどん新しい行動をするので、どんどん新しいスコアに書き変わるんです。

がんがん書き込まれるから、常に最新のデータが送られ続けるので、途中ちょいちょい失敗しても、最終的なランキング結果は集約していきます。それによって問題なく結果整合性が取れるという理由で、ランキングはキャッシュを使っても大丈夫です。

それから、データベースのレコードキャッシュ。

まぁこんな感じですよね。データベースがバラバラっとあって、まぁRDBMSで通信にいくと重たくなってしまうので、O/Rマッパーで言うところのモデルデータをキャッシュして、という話です。

例えば「このプレイヤーの情報をください」と。

読み込みました。ないですね。じゃあデータベースから読み込みます。キャッシュに書き込みます。キャッシュにデータを送りました。クライアントへ情報を返せました。

2回目のアクセスはキャッシュから取れるから、スピードアップ……と思うかもしれませんが、実はゲームの更新がめちゃめちゃ多いので、たくさん更新された結果、たくさんデータベースが更新され、たくさんキャッシュが更新されまくります。

そうすると、更新処理=データベースとキャッシュ両方の処理で二重処理になっていて、ただでさえ遅い更新処理が、キャッシュ更新という無駄な処理が入ることによって、二重に重くなります。

そうまでして頑張った結果どうなったかっていうと、Redisからゲットするのに1msぐらいかかります。なんとMySQLから1レコードだけゲットするのに、2msぐらいしかかからないんです。もちろん、何msかは使うサーバの強度やアクセスの状況によって変わってくるのであくまで仮定ですが。おおむね1msから2msぐらいですね。

更新を多くしたのに、得られた結果は、2msが1msになっただけ。そのために、キャッシュに書くとかいう複雑な処理がコードの中に入り込んでいるんですよ。

さらに、キャッシュサーバのメモリ対策で水平分割しますよね。そうすると、構成にかなりコストがかかって、管理も大変になります。

しかも、Redisはシングルスレッドなので台数がすごくたくさん必要です。なんとMySQLはマルチスレッドなんですよ。それからKVSは複数のクエリを1個のクエリにまとめるのすごく苦手なんですけど、SQLは複数のクエリを1個のクエリにまとめやすいんですよね。

それから、RDBMS自体にもキャッシュの機能がある。MySQLをチューニングするときに、ちゃんとテーブル全体がメモリに乗るように、テーブル1個が巨大にならないように時系列データを分割しておいたりいろいろ工夫するんですけど、基本はちゃんとメモリに乗るように設計しておけば、かなりの速度で動きます。

今回言いたいこととしては、DBのレコードをキャッシュにするのは難易度が高いので、やめたほうがいいです。水平分割の時点でもうスケールしているので、速度を上げるなら水平分割に舵を切って、データベースのレコードキャッシュなんて無駄なことはしないほうがいい。

何が言いたいかというと、RDBMSも日々進化しているんですよ。

これ、Amazonさんが配っているAuroraの論文です。「どうしてリードが鬼みたいに速いのか」ということがいろいろ書かれています。素のMySQLと裏側の仕組みはまったくの別世界になっていて、恐ろしい程にリード性能が出るのがAuroraなんですよ。

なので、小手先のテクニックでRedisとかmemchachedにデータベースのレコードをキャッシングして速度が上がった、というような話は嘘なので気を付けてください。素直にAuroraとかを使いましょう。

ということで、データベースの設計のコツとしては、RDBMSを使いましょう、水平分割を負荷分散で使って、垂直分割はなるべくやめましょう、NoSQLを用いたキャッシュに頼りすぎないように気を付けましょう。

言語とサービス選定のコツ

続いて、言語、サービス選定のコツ。「共有データの整合性を担保する」。

まず、クラウドサービスを選定するときに、マネージドのストアにどういうものがあるかを調べ尽くしたほうがいいです。例えば、そのストアの論文がちゃんと公開されているか。詳しい情報がものすごく丁寧に公開されているかどうかを基準にする。

それから、ストアを作った開発者とちゃんと会話ができるかどうかも気にしたほうがいいです。AWSさんやGoogleさんはちゃんとペーパーも配っていますし、開発者とお話しすることも可能です。

言語を選定するときは、どういうストアのライブラリが存在しているのかもちゃんと調べておいたほうがいいです。スキーマのマイグレーションがあるか、水平分割はサポートされているか、XAトランザクションはサポートされているか。

RDBMS関連だけではなく、例えばRedisと接続するときにコネクションがちゃんとプーリングに対応しているのか、水平分割に対応しているのかそういったことを見たほうがいいです。

これがないと選択しちゃいけないのかと言われそうですが、違うんです。作るものを探すために調べているんです。

gumiの場合はPythonやElixirを選択しているんですけど、PythonにはXAトランザクションがないので、自作しているんですよ。Redisをうまいこと操作するライブラリも、なかったので自作しました。Elixirでも、なんとスキーママイグレーションを自動生成するライブラリがなくて、当然水平分割もなかったので自作しています。

要は言語の選定において、どちらかというと作るものを探すために、データストアのライブラリに何があるかを調べるようにしています。

まとめです。さまざまなことを共有データを中心に考えていきましょう、という話でした。

それから、共有データの整合性を担保すれば、リリース後の運用がすごく楽になります。システムの応答速度も開発速度も、基本妨げません。むしろ、正しく設計すれば、整合性を守りながら応答速度も担保しつつ、開発と運用の難易度も下げてくれます。

以上です。ということで、質疑応答の時間で大丈夫ですか?

司会者:ありがとうございました。

(会場拍手)

Redisのキャッシュについて

司会者:それでは少し質疑応答の時間をお取りしようと思いますが、どなたかいらっしゃいますでしょうか。では、どうぞ。

質問者1:マイクロサービスに関する話で一番納得ができました。ありがとうございました。

幾田:ありがとうございました。

質問者1:2つ質問があるんですが、Redisをキャッシュする場合に……レスポンスをキャッシュする場合に、expireする基準がどうなっているのか。

あと、RDBMSを使うときに、結局デッドロックに関してがどうしても難しくて、どうしているんだろう、と。

SQLとかテーブル設計の難しいところは勉強すればいい。だけどデッドロックに関しては、どうアルゴリズムを勉強していけばいいかがよくわかっていないので、何かアドバイスがあればお願いします。

幾田:まず、Redisのキャッシュですね。レスポンスのexpire timeは、基本的に24時間ぐらいで設定してあるんですけど、各APIごとにあまりにも特性が違うので、24時間もいらないものが多いんです。なので、みなさんには「なるべく1時間ぐらいに設定してね」って言ってはいるんですけど、だいたい24時間に設定されることが多いです。

質問者1:メモリは溢れないですか?

幾田:水平分割しています。リクエストIDをハッシュキーにしてあげて、そのハッシュキーからホスト名を確定して書き込むかたちです。

やっぱり、過去には溢れたこともあって、どうしてかというと、最初は全APIのレスポンスをキャッシュしていたんです。クライアントがすべてのリクエストIDを送ってきちゃって、使えないキャッシュで溢れかえっちゃったので、今は本当に冪等性がほしいやつだけに絞るように変えました。

質問者1:溢れそうになったら古いやつから離すような仕組みとか、考えているんですか?

幾田:そこは、今はオフにしています。今、Redisを使っているんですが、古いキャッシュが圧迫していったとしても、勝手に押し出さないようにしています。

memcachedとかの押し出しって、実はデータのサイズによってグルーピングされているんです。データのサイズごとで押し出されるので、古いものから押し出されるわけじゃないんですよ。まず、そのアルゴリズムをちゃんと理解しておく。

そのうえで、レスポンスデータってけっこう大きくなるので、本当はちゃんと時間通りに消えてほしいのに、たまたま同じサイズのものが積み重なってしまって押し出されて、問題になることがある。

つまり、本来レスポンスキャッシュで守られるべき冪等性が、1秒も経たないうちに押し出されちゃってダメになる、ということがあるんです。なので、基本はオフにして、時間でちゃんとコントロールしています。

逆にそのRedis警察みたいな……Redisにexpireが付いていないデータを入れたら怒られるみたいな話もあります。

デッドロックしないための考え方

幾田:それから、デッドロックですね。MySQLを使ってて、デッドロックしないために簡単な方法がいくつかあります。

一番簡単なのは、まず、脳死状態でジャイアントロックをプレイヤーでとっちゃうんです。「select 〜 for 〜 update プレイヤーID」で、すべてのスタートのポイントでガンッてとっちゃう。そのあとは、ノーガード戦法でガンガンいっちゃう、という方法がまず1つ目、ジャイアントロックです。

これでほぼ問題ないんですけど、あともう1つやるとしたら、処理の順番ももちろんあるんですけど、テーブル名とプレイヤーIDを複数跨ぐ場合、プレイヤーIDやテーブル名でソートしてあげて、ソートした順番で処理するとデッドロックは起きにくいです。

質問者1:デッドロック用のテーブルを事前に作るのか、それとも既存のテーブルを再帰してジャイアントロックをしちゃうのか、どっちなのかが気になります。

幾田:既存のテーブルをプレイヤーIDでジャイアントロックをすることもありますし、実はgumiって、ユーザを、プレイヤーという名称で統一してるんですよ。プレイヤーテーブルが存在するので「select 〜 for 〜 update 〜」してあげて……。

質問者1:それは、APIのリクエストで最初にやる……。

幾田:そうです、その通りです。なので、ロック用のテーブルといえばプレイヤーテーブルですね。実際は、プレイヤーテーブルは一切修正を加えずに、他のテーブルをガンガン更新かけていたりはします。

あとは、ギルドのバトルで50対50の戦いがある時に、ギルドテーブルでロックをとらずに、RedisのMutexロックで、バトルIDでロックをとるということもやっていました。

それから、トランザクションの分離レベルを3から4に引き上げて処理をすることはあります。シーケンシャルに並べたり。そうすると、selectが「select 〜 for 〜 update 〜」と同じになるので。

質問者1:それは、スピードは出る……?

幾田:落ちます! でも、どうしても崩したくない。けっこうスピード重視でリピータブルリードのトランザクション分離レベルにしておいて処理していることがあるんですけど、たまにクエリの発行の仕方が悪くて古いデータを読み込んでしまい、古いデータをベースにして更新データを計算して上書きしちゃったということもあるんです。

質問者1:わかりました。ありがとうございました。

司会者:それではお時間になりました。ありがとうございました。

(会場拍手)