Simon: get_lock, release_lock funktioniert nicht richtig

Hallo,

ich habe folgendes Problem.
Ein Benutzer erstellt eine Verkaufs anfrage (Ihm werden in der Datenbank EUR abgezogen)

Diese Verkaufs anfrage kann wieder storniert werden. Bei dem Storno frage ich folgendes ab:

  
$stmt = $db->prepare("SELECT eur FROM offer WHERE active = 1 AND id = ?");  
$stmt->execute(array($_POST['id']));  
$row = $stmt->fetch(PDO::FETCH_ASSOC);  
if($row) {  
  //EUR wieder gutschreiben  
  //Active = 0 setzen  
}  

Jetzt könnte es sein, das ein Benutzer das Script innerhalb Sekunden mehrmals aufruft.
Es passiert also, das der Benutzer 2x die EUR wieder gutgeschrieben bekommt.

Jetzt habe ich folgendes versucht:

  
$query = mysql_query("SELECT GET_LOCK('".$userid."', 10) AS free");  
$row = mysql_fetch_assoc($query);  
if($row['free']) {  
  $stmt = $db->prepare("SELECT eur FROM offer WHERE active = 1 AND id = ?");  
  $stmt->execute(array($_POST['id']));  
  $row = $stmt->fetch(PDO::FETCH_ASSOC);  
  if($row) {  
    //EUR wieder gutschreiben  
    //Active = 0 setzen  
  }  
  mysql_query("SELECT RELEASE_LOCK('".$userid."')");  
}  

Bei meinem Test hat es funktioniert. Ich habe das Script mehrmals gleichzeitig aufgerufen. Es wird jetzt immer gewartet, bis das Lock wieder frei ist und erst dann wird das Script weiter ausgeführt (mit sleep() getestet)

Jedoch ist es schon wieder vor gekommen, das ein Benutzer 2x EUR wieder gutgeschrieben bekommen hat.

Gibt es dafür noch eine andere Lösung?
Sollte ich mit IS_FREE_LOCK() vorher das Lock abfragen?

Besten Gruß
Simon

  1. Hello,

    ich hoffe, dass ich deinen Vortrag richtig interpretiere und denke daher, dass hier ein "academic lock" angebrachter wäre.

    OK - diese Bezeichnung für diese _extrem_ zeitverzögerte Variante eines "Locks" habe ich mal selber erfunden. Christian Seiler hat sich seinerzeit darüber ausgelassen. Aber im HTTP-Umfeld nützen meistens weder pessimistic noch optimistic Locks. (Diese Begriffe findest Du bestimmt auch bei Wikipedia).

    Mein Academic Lock muss sowohl im datenmodell, als auch in der Ablauflogik berücksichtigt werden. Zwischen Holen der Daten aus der Datenbank und (veränderndem) Zurückschreiben könnten Tage vergehen. Trotzdem muss sichergestellt werden, dass kein anderer User zwischendurch eine Veränderung vorgenommen hat.

    Wenn Du das meinst, dann sollten wir diesen Thread _hier_ fortsetzen, sonst entschuldige bitte den Ausflug...

    Liebe Grüße aus dem schönen Oberharz

    Tom vom Berg

    --
     ☻_
    /▌
    / \ Nur selber lernen macht schlau
    http://restaurant-zur-kleinen-kapelle.de
  2. Moin!

    Jetzt könnte es sein, das ein Benutzer das Script innerhalb Sekunden mehrmals aufruft.
    Es passiert also, das der Benutzer 2x die EUR wieder gutgeschrieben bekommt.

    Dein Problem liegt nicht in dem Select, auch nicht im Lock, sondern im Gutschreiben des Geldes.

    Dazu fehlen aber Details.

    Pauschal würde ich sagen: Nutze Transaktionen. Das spart dir die Locks und den sehr fragilen Mechanismus, den du da gerade baust.

    - Sven Rautenberg

    1. Hello,

      Pauschal würde ich sagen: Nutze Transaktionen. Das spart dir die Locks und den sehr fragilen Mechanismus, den du da gerade baust.

      Transaktionen sind nur innerhalb eines verbindungsorientierten/zustandsorentierten Dialoges sinnvoll, nicht aber über einen oder mehrere HTTP-Roundturn(s). Da muss man zwangsweise ins Datenmodell eingreifen und dies dann in der Vorgangsbearbeitung berücksichtigten.

      Liebe Grüße aus dem schönen Oberharz

      Tom vom Berg

      --
       ☻_
      /▌
      / \ Nur selber lernen macht schlau
      http://restaurant-zur-kleinen-kapelle.de
      1. Moin!

        Hello,

        Pauschal würde ich sagen: Nutze Transaktionen. Das spart dir die Locks und den sehr fragilen Mechanismus, den du da gerade baust.

        Transaktionen sind nur innerhalb eines verbindungsorientierten/zustandsorentierten Dialoges sinnvoll, nicht aber über einen oder mehrere HTTP-Roundturn(s). Da muss man zwangsweise ins Datenmodell eingreifen und dies dann in der Vorgangsbearbeitung berücksichtigten.

        Konkret? Man kann also nicht innerhalb einer Transaktion, die man gestartet hat, den SELECT machen und die notwenigen Inserts/Updates für die Geldgutschrift, und dann die Transaktion committen, weil der Browser-Request "Storno-Aktion machen" gesagt hat?

        - Sven Rautenberg

        1. Hello,

          Pauschal würde ich sagen: Nutze Transaktionen. Das spart dir die Locks und den sehr fragilen Mechanismus, den du da gerade baust.

          Transaktionen sind nur innerhalb eines verbindungsorientierten/zustandsorentierten Dialoges sinnvoll, nicht aber über einen oder mehrere HTTP-Roundturn(s). Da muss man zwangsweise ins Datenmodell eingreifen und dies dann in der Vorgangsbearbeitung berücksichtigten.

          Konkret? Man kann also nicht innerhalb einer Transaktion, die man gestartet hat, den SELECT machen und die notwenigen Inserts/Updates für die Geldgutschrift, und dann die Transaktion committen, weil der Browser-Request "Storno-Aktion machen" gesagt hat?

          _Innerhalb_ einer Transaktion...

          Aber wann endet die Transaktion und ist der Vorgang dann auch schon beendet?

          Dabei übersetze ich jetzt grob:

          Transaktion = datenverändernde Maßnahme (innerhalb eines HTTP-Zyklus)
              Vorgang = datenverändernde Maßnahme über mehrere (HTTP-)Zyklen hinweg.

          Liebe Grüße aus dem schönen Oberharz

          Tom vom Berg

          --
           ☻_
          /▌
          / \ Nur selber lernen macht schlau
          http://restaurant-zur-kleinen-kapelle.de
    2. Pauschal würde ich sagen: Nutze Transaktionen.

      Ich hab es mal so versucht:

        
      $db->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE);  
      try {  
        $db->beginTransaction();  
        $stmt = $db->prepare("INSERT INTO lockbit (user_id, locked) VALUES (?, ?)");  
        $stmt->execute(array(11111111, 1));  
        $stmt = $db->prepare("INSERT INTO lockbit (user_ida, locked) VALUES (?, ?)");  
        $stmt->execute(array(11111111, 1));  
        $db->commit();  
      } catch(PDOException $e) {  
        $db->rollBack();  
      }  
      
      

      Dabei sollte das zweite INSERT INTO einen Fehler erzeugen (user_ida existiert nicht) und eigentlich doch das RollBack vollzogen werden oder?

      Leider ist das nicht der fall. Es wird das erste INSERT INTO eingefügt, aber es findet kein rollBack statt.

      1. Tach!

        Dabei sollte das zweite INSERT INTO einen Fehler erzeugen (user_ida existiert nicht) und eigentlich doch das RollBack vollzogen werden oder?
        Leider ist das nicht der fall. Es wird das erste INSERT INTO eingefügt, aber es findet kein rollBack statt.

        Hast du zum einen getestet, dass die Exception geworfen und der catch-Zweig generell abgearbeitet wird? Für Exceptions muss unter PDO der Error-Mode auf PDO::ERRMODE_EXCEPTION gestellt sein.

        Zum anderen musst du auch eine transaktionsfähige Storage Engine für die betroffene(n) Tabelle(n) verwenden. MyISAM ist keine, InnoDB ist eine.

        dedlfix.

        1. Zum anderen musst du auch eine transaktionsfähige Storage Engine für die betroffene(n) Tabelle(n) verwenden. MyISAM ist keine, InnoDB ist eine.

          Das hat mir schon geholfen und nun funktioniert es soweit mit der transaktion.
          Macht denn dann solch eine Transaktion sinn?

            
          try {  
            $db->beginTransaction();  
            $stmt = $db->prepare("SELECT data FROM lockbit WHERE user_id = 1");  
            $stmt->execute();  
            $row = $stmt->fetch(PDO::FETCH_ASSOC);  
            if(!$row) {  
              $stmt = $db->prepare("INSERT INTO lockbit (user_id, locked) VALUES (?, ?)");  
              $stmt->execute(array(1, 1));  
            }  
            $db->commit();  
          } catch(PDOException $e) {  
            $db->rollBack();  
            die($e->getMessage());  
          }  
          
          

          Wird dir if abfrage etc korrekt durchgeführt?
          Kann ich das GET_LOCK() einfach mit der Transaktion ersetzen und schon werden doppelte Ausführungen zur selben Zeit vermieden?

          Was geschieht, wenn ein Benutzer 2 Transaktionen startet?
          Wird gewartet bis die eine Transaktion durchgeführt wurde?