@GarryWeldingからのコメントのエコー:データベースの更新は、説明されているユースケースを処理するためのコード内の適切な場所ではありません。ユーザーテーブルの行をロックすることは正しい修正ではありません。
ステップをバックアップします。ユーザーの購入をきめ細かく制御したいようです。ユーザーの購入の記録を保存する場所が必要なようです。そうすれば、それを確認できます。
データベース設計に飛び込むことなく、ここでいくつかのアイデアを投げ出します...
「ユーザー」エンティティに加えて
user
username
account_balance
ユーザーが行った購入に関する情報に関心があるようです。私は、私たちが興味を持つかもしれない情報/属性についていくつかのアイデアを投げかけていますが、これらがすべてあなたのユースケースに必要であるとは主張していません:
user_purchase
username that made the purchase
items/services purchased
datetime the purchase was originated
money_amount of the purchase
computer/session the purchase was made from
status (completed, rejected, ...)
reason (e.g. purchase is rejected, "insufficient funds", "duplicate item"
特にユーザーからの購入が複数ある可能性があるため、ユーザーの「口座残高」でそのすべての情報を追跡しようとはしません。
ユースケースがそれよりもはるかに単純で、ユーザーによる最新の購入のみを追跡する場合は、それをユーザーエンティティに記録できます。
user
username
account_balance ("money")
most_recent_purchase
_datetime
_item_service
_amount ("money")
_from_computer/session
そして、購入するたびに、新しいaccount_balanceを記録し、以前の「最新の購入」情報を上書きすることができます
私たちが気にするのが「同時に」複数の購入を防ぐことだけである場合、それを定義する必要があります...それは同じ正確なマイクロ秒以内を意味しますか? 10ミリ秒以内?
異なるコンピューター/セッションからの「重複」購入のみを防止したいですか?同じセッションでの2つの重複するリクエストはどうですか?
これはではありません 問題をどのように解決するか。しかし、あなたが尋ねた質問に答えるために、単純なユースケースである場合-「互いにミリ秒以内に2つの購入を防ぐ」、そしてこれをUPDATE
で行いたい user
の テーブル
次のようなテーブル定義があるとします:
user
username datatype NOT NULL PRIMARY KEY
account_balance datatype NOT NULL
most_recent_purchase_dt DATETIME(6) NOT NULL COMMENT 'most recent purchase dt)
(データベースから返された時間を使用して)ユーザーテーブルに記録された最新の購入の日時(マイクロ秒まで)
UPDATE user u
SET u.most_recent_purchase_dt = NOW(6)
, u.account_balance = u.account_balance - :money1
WHERE u.username = :user
AND u.account_balance >= :money2
AND NOT ( u.most_recent_purchase_dt >= NOW(6) + INTERVAL -1000 MICROSECOND
AND u.most_recent_purchase_dt < NOW(6) + INTERVAL +1001 MICROSECOND
)
次に、ステートメントの影響を受ける行数を検出できます。
影響を受ける行がゼロの場合は、:user
のいずれかです。 見つかりませんでした、または:money2
アカウントの残高、またはmost_recent_purchase_dt
よりも大きかった 現在の+/-1ミリ秒の範囲内でした。どちらかわかりません。
影響を受ける行が0を超える場合は、更新が発生したことがわかります。
編集
見落とされていたかもしれないいくつかの重要なポイントを強調するために...
サンプルSQLは、MySQL5.7以降を必要とする分数秒のサポートを期待しています。 5.6以前では、DATETIMEの解像度は秒単位でした。 (サンプルテーブルの列定義に注意してください。SQLはマイクロ秒までの解像度を指定しています... DATETIME(6)
およびNOW(6)
。
SQLステートメントの例では、username
が必要です。 user
のPRIMARYKEYまたはUNIQUEキーになります テーブル。これは、テーブル定義の例に記載されています(ただし強調表示されていません)。
サンプルのSQLステートメントは、user
の更新をオーバーライドします 1ミリ秒以内に実行された2つのステートメントの場合 お互いの。テストのために、そのミリ秒の解像度をより長い間隔に変更します。たとえば、1分に変更します。
つまり、1000 MICROSECOND
の2つのオカレンスを変更します 60 SECOND
へ 。
その他の注意事項:bindValue
を使用してください bindParam
の代わりに (ステートメントに値を提供しているため、ステートメントから値を返すことはありません。
また、エラーが発生したときにPDOが例外をスローするように設定されていることを確認してください(コード内のPDO関数からの戻りをチェックしない場合)。これにより、コードは(比喩的な)小指をの隅に置きません。私たちの口のDr.Evilスタイル「私はそれがすべて計画通りに進むと思います。何ですか?」)
# enable PDO exceptions
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "
UPDATE user u
SET u.most_recent_purchase_dt = NOW(6)
, u.account_balance = u.account_balance - :money1
WHERE u.username = :user
AND u.account_balance >= :money2
AND NOT ( u.most_recent_purchase_dt >= NOW(6) + INTERVAL -60 SECOND
AND u.most_recent_purchase_dt < NOW(6) + INTERVAL +60 SECOND
)";
$sth = $dbh->prepare($sql)
$sth->bindValue(':money1', $amount, PDO::PARAM_STR);
$sth->bindValue(':money2', $amount, PDO::PARAM_STR);
$sth->bindValue(':user', $user, PDO::PARAM_STR);
$sth->execute();
# check if row was updated, and take appropriate action
$nrows = $sth->rowCount();
if( $nrows > 0 ) {
// row was updated, purchase successful
} else {
// row was not updated, purchase unsuccessful
}
そして、私が以前に指摘したことを強調するために、「列をロックする」は問題を解決するための正しいアプローチではありません。また、例で示した方法でチェックを行っても、購入が失敗した理由(資金が不足しているか、前回の購入から指定された期間内)はわかりません。