こんにちは。ママリアプリ開発チームでエンジニアをしております高橋です。
今日は3月1日よりリリースされているママリ抽選会の実装についてお話ししていきます。
ママリ抽選会とは
ママリポイントを使用して参加できる抽選会のことです。
ママリポイントとはアプリ内で回答をしたり、ベストアンサーをもらったりすると付与されるポイントのことです。ただ現状でポイントを使用する用途があまりなく、ユーザーからのたくさんのお声とコネヒトとしてポイントを活用してユーザーに感謝の気持ちを伝えたいという背景から今回の「ママリ抽選会」プロジェクトがスタートしました。
テーブル設計
まずは簡単にテーブル設計を説明します。
以下は今回新規で作成した抽選会に関わるデータベースのER図です。
前提として開催期間に対して賞品それぞれに最大在庫があります。
在庫管理をするだけなら履歴テーブルは必要なく商品テーブルの最大在庫数を減らしていくという方法もあります。
ですが以下の懸念点を考えて最大在庫は固定でデータベースに保存するようにしました。
- 本来の最大在庫がわからなくなる
- 抽選会の参加履歴画面などが仕様が出てきた時に対応できなくなる
テーブル設計についてはそこまで複雑なことはないので本題の内部の実装についてお話しして行きます。
排他処理
排他処理とは簡単に説明すると「ダブルブッキングしないように制御すること」です。
前提として履歴を登録する前に次の抽選処理が始めると残在庫に誤差が生まれてしまうので順番に抽選する必要があります。 そこで排他処理を利用して正しく処理できるようにしました。
今回はテーブル全体ではなく行に対してロックをかけて実現しました。
SQLでは排他処理はこのように書きます。idが1のデータのみ行ロックされています。
select * from table_name where id = 1 for update; // 行ロック
CakePHPでは以下のように書くことができます。念の為キャッシュ削除をすると安心です。
$table->find() ->modifier('SQL_NO_CACHE') ->epilog('FOR UPDATE') ->first(); // 行ロック
実装の流れは以下のようになっています。
$user = $this->ユーザーテーブル->get($id); try { $connection->begin(); // トランザクション開始 $lottery = $this->抽選会テーブル->find('active') ->contain(['賞品テーブル']) ->modifier('SQL_NO_CACHE') ->epilog('FOR UPDATE') // 行ロック ->first(); // 参加資格があるかバリデーションをする // 抽選をする $connection->commit(); // トランザクション終了 } catch (Exception $e) { $connection->rollback(); throw new InternalErrorException($e->getMessage()); }
よくある排他処理は複数のユーザーが同じデータを更新できないように制御していくイメージですが、今回に関しては抽選処理自体を止めたいので1番最初の抽選会テーブルのデータを取得するところでを制御する必要があります。
トランザクションを開始した後にselect文にfor updateを指定すると行ロックが発生し、この間は順番待ち状態になります。
トランザクション終了と共に解放され順番待ちしていた処理が開始する仕組みです。
これでどんなにリクエストが集中しても安全に順番に処理できます。
最後に
私は入社後大きな新機能実装は初めてだったので、テーブル設計、API設計、API実装、テストコード、レビューを一貫してやることができてとてもいい学びになりました。
設計をしていく上で難しいのが変更のしやすさだったり、どれだけ先を見通すかはすごく悩むポイントでした。
またチーム開発をしていく上で実装に入る前にモデリングだったり、設計の認識を合わせるということが大切だなと思いました。
色々大変なことはありましたが、新機能をリリースできたことはとても嬉しく思います。自分の中ではかなり大きな経験だったので次に活かしていきたいです!
【秘話】ママリ抽選会のリリース🎉までにデザイナーがやってきたこと↓
コネヒトでは一緒に働く仲間を募集しています! そして興味持っていただけた方は気軽にご連絡ください! https://www.wantedly.com/companies/connehito/projects