sql >> データベース >  >> RDS >> Mysql

PHPPDOトランザクションの複製

    @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
    }
    

    そして、私が以前に指摘したことを強調するために、「列をロックする」は問題を解決するための正しいアプローチではありません。また、例で示した方法でチェックを行っても、購入が失敗した理由(資金が不足しているか、前回の購入から指定された期間内)はわかりません。



    1. Drupalデータベース構造-効率的/非効率的?

    2. MySQLのDATETIMEフィールドから日付のみを選択するにはどうすればよいですか?

    3. OracleのLOG()関数

    4. 更新して1つのクエリで選択