導入
Webアプリケーションにおいて、「いいね!」や「投票」機能はユーザーエンゲージメントを高める重要な要素です。しかし、単純な実装では「短時間の連続クリックによる数値の不整合」や「同一ユーザーによる多重投票」といった課題が発生します。本記事では、実務レベルで求められる整合性を意識した投票機能の実装方法を解説します。
基礎知識
投票機能の核となるのは「データの整合性」です。単に数値をインクリメントするのではなく、誰がどの対象に投票したかを記録する「投票ログテーブル」と、現在の合計値を保持する「集計テーブル」を分ける設計が一般的です。また、データベースの同時書き込みによる競合を防ぐため、トランザクション処理と行ロック(FOR UPDATE)の概念が不可欠となります。
実装/解決策
実務では、以下の手順で実装を行います。
1. トランザクション開始: 整合性を担保します。
2. 二重投票チェック: ユーザーIDと対象IDの組み合わせが既に存在しないか確認します。
3. ロックの取得: 集計テーブルの対象レコードをSELECT … FOR UPDATEでロックし、他からの更新を待機させます。
4. レコード更新: 投票ログの追加と、集計テーブルの数値を更新します。
5. コミット: 処理を確定させます。
サンプルプログラム
PDOを使用した、トランザクションと悲観的ロックによる投票処理の例です。
beginTransaction();
$userId = 1;
$targetId = 101;
// 1. すでに投票済みかチェック
$stmt = $pdo->prepare(“SELECT id FROM votes WHERE user_id = ? AND target_id = ?”);
$stmt->execute([$userId, $targetId]);
if ($stmt->fetch()) {
throw new Exception(“既に投票済みです。”);
}
// 2. 集計テーブルをロックして取得(悲観的ロック)
$stmt = $pdo->prepare(“SELECT count FROM vote_counts WHERE target_id = ? FOR UPDATE”);
$stmt->execute([$targetId]);
$currentCount = $stmt->fetchColumn();
// 3. 投票ログを挿入
$stmt = $pdo->prepare(“INSERT INTO votes (user_id, target_id) VALUES (?, ?)”);
$stmt->execute([$userId, $targetId]);
// 4. 集計テーブルを更新
$stmt = $pdo->prepare(“UPDATE vote_counts SET count = count + 1 WHERE target_id = ?”);
$stmt->execute([$targetId]);
// コミット
$pdo->commit();
echo “投票が完了しました。”;
} catch (Exception $e) {
// エラー時はロールバック
$pdo->rollBack();
echo “エラー: ” . $e->getMessage();
}
?>
応用・注意点
現場で注意すべき点は「パフォーマンス」と「セキュリティ」です。
パフォーマンス: 投票数が極めて多い場合、都度集計テーブルを更新するとDBのボトルネックになります。この場合はRedis等のインメモリDBに一時的にカウントを貯め、バッチ処理でRDBに反映させる手法が有効です。
セキュリティ: クライアントサイドでの連打防止(ボタンの無効化)は必須ですが、サーバーサイドでの「同一ユーザーによる多重投票制限」を必ず実装してください。また、SQLインジェクションを防ぐため、必ずプリペアドステートメントを使用しましょう。