なぜ私は毎回Userレコードにロックをかけるのか

こんにちは、YOUTRUSTのやまでぃ(YOUTRUST/X)です。

最近のわたくしごとですが

毎朝レタスに豆腐と納豆としらすを乗せて、軽くオリーブオイルをかけたものを食べるのにハマっています。納豆のタレとしらすの塩分が良い感じです。ドレッシングはなくても良かったのかもしれません。

画像を載せようかと思いましたが特に映えるものでもないので、代わりに先月プロダクトチームみんなで焼肉を食べにいったときの画像を載せておきます。

舌の上でとろけました

今回はなんの話?

今回はYOUTRUSTにおけるデータベースの排他制御についてです。

結論としては、YOUTRUSTではシンプルなルールによる、いわゆる悲観的ロックを採用しています。

本記事では、排他制御の必要性と、YOUTRUSTではどのようにして排他制御を行っているかについて説明します。

背景

YOUTRUSTのデータベースに対して、下記のような様々なトリガーによって同時多発的に更新処理が実行されています。

  • APIリクエス
  • スケジュールタスク
  • 非同期ジョブ
  • 手動によるタスク実行

そのため、ある処理が行われている間は別の処理が同時に実行されないようにしないと、データの不整合が発生してしまう可能性があります。(例. 最初にレコードを取得して、そのレコードの特定のカラムの値を+1する場合等)

また、更新対象となるテーブルが複数になる場合があり、適切な更新順を保たないとデッドロックが発生してしまう可能性もあります。 (例. ロジックAではリソースa→リソースbの順番、ロジックBではリソースb→リソースaの順番の場合等)

上記のような問題に対して、適切に排他制御をしないと下記のようなことになってしまいます。

  • あるロジックに手を加える際に、他のロジックのこと(更新順)も気にしないといけない
  • もしミスしてしまった場合、デッドロックを発生させてしまう可能性がある
  • その原因調査は時間が掛かってとっても大変
  • 利用者に迷惑を掛けてしまい、サービスに対する信頼にも影響してしまう

全てを解決するシンプルなルール

そこでYOUTRUSTでは以下のシンプルなルールを採用しています。

「全ての更新ロジックの実行前に、更新対象リソースの親となるリソースに対してロックを掛ける」

親リソースとは下記のような、対象リソースをサブリソースとして抱えているリソースのことです。

  • UserJobCategory(ユーザーの職種リソース)ならUser
  • PostCommentLike(投稿に付いたコメントへのいいね)ならPost
  • ChatRoomComment(チャットルームへのメッセージ)ならChatRoom

RailsActiveRecord的に言うと、「更新対象モデルのbelongs_toに指定されているモデル」と表現するのが分かりやすいかもしれません。)

このルールを採用することによって、問題点で挙げたような他のロジックを気にすることも、デッドロックに怯えることもなくなります。

トレードオフとしては、ロックのための余計なSELECT FOR UPDATEクエリが発行されることによる遅延や負荷になりますが、十分許容範囲であり、このルールによって得られるメリットの方が大きく勝るという判断をしています。

終わりだよ

ここまで読んでいただきありがとうございました!

読者の方の中には「ここまでやるのか?課金等クリティカルな箇所だけで良くない?」と思われた方もいらっしゃるかもしれません。

ですがYOUTRUSTでは(というか僕は)「ここはクリティカルかどうか?」すら考えたくないので、一律で全ての更新ロジックの前にロックを掛けるようにしています。(僕が前職で高負荷なソーシャルゲームのバックエンドエンジニアをやっていたという育ちの影響もあるかもしれません笑)

引き続きYOUTRUSTでは、一緒にこんなことを考えたり切磋琢磨して開発していける仲間を募集しているので、もし話を聞いてみたいという方がいらっしゃったらカジュアル面談して欲しいです!

career.youtrust.co.jp

以上です!

また次回の記事でお会いしましょう〜👐