2012-04-15 9 views
6

Bitte helfen Sie mir mein Missverständnis zu finden.App Engine, Transaktionen und Idempotenz

Ich schreibe ein RPG auf App Engine. Bestimmte Aktionen des Spielers verbrauchen einen bestimmten Wert. Wenn der Wert Null erreicht, kann der Spieler keine Aktionen mehr ausführen. Ich fing an, mir Gedanken darüber zu machen, ob ich Spieler betrügen könnte - was wäre, wenn ein Spieler zwei Aktionen sehr schnell, direkt nebeneinander schickte? Wenn der Code, der den Wert dekrementiert, nicht in einer Transaktion ist, hat der Spieler die Chance, die Aktion zweimal auszuführen. Also sollte ich den Code, der die Statistik dekrementiert, in eine Transaktion einbinden, oder? So weit, ist es gut.

In GAE Python, aber wir haben dies in den documentation:

Hinweis: Wenn Ihre Anwendung eine Ausnahme empfängt, wenn eine Transaktion einreichen, ist es nicht bedeutet immer, dass die Transaktion fehlgeschlagen. Sie können Timeout-, TransactionFailedError- oder InternalError-Exceptions in Fällen empfangen, in denen Transaktionen festgeschrieben wurden und schließlich erfolgreich angewendet werden. Wenn möglich, machen Sie Ihre Datastore-Transaktionen so Idempotent, dass , dass, wenn Sie eine Transaktion wiederholen, das Endergebnis das gleiche sein wird.

Whoops. Das bedeutet, dass die Funktion, die ich, dass lief wie folgt aussieht:


def decrement(player_key, value=5): 
    player = Player.get(player_key) 
    player.stat -= value 
    player.put() 

Nun, das wird nicht funktionieren, weil die Sache nicht idempotent, nicht wahr? Wenn ich eine Wiederholungsschleife darum herumlege (muss ich in Python? Ich habe gelesen, dass ich nicht auf SO muss ... aber ich kann es nicht in den Dokumenten finden), könnte es den Wert zweimal erhöhen, Recht? Da mein Code eine Ausnahme abfangen kann, aber der Datenspeicher die Daten trotzdem festlegt ... hm? Wie behebe ich das? Ist das ein Fall, wo ich distributed transactions brauche? Tue ich wirklich?

+1

Nun, ja, und es ist ein guter Punkt ... aber bevor ich meinen Code mit einer Reihe von schwer zu diagnostizieren, schwer zu Fehler zu reproduzieren Ich möchte gerne lernen, welches Muster ich hier anwenden sollte. –

+0

Ihr Muster ist auf dem richtigen Weg, aber GAE hat einige frustrierende Nuancen, die eine solche chirurgisch präzise Implementierung erschweren. Nach meiner Erfahrung mit der GAE ist es manchmal die Mühe wert und manchmal nicht. –

+1

@TravisWebb Stimme nicht zu. Transaktionssicherheit ist keine "vorschnelle Optimierung", ebenso wenig sind Transaktionskollisionen besonders unwahrscheinlich. –

Antwort

13

Zunächst ist Nick's Antwort nicht korrekt. Die Transaktion von DHayes ist nicht idempotent. Wenn sie mehrmals ausgeführt wird (dh ein erneuter Versuch, wenn der erste Versuch als fehlgeschlagen galt, wenn dies nicht der Fall war), wird der Wert mehrmals dekrementiert. Nick sagt, dass "der Datenspeicher überprüft, ob die Entitäten seit dem Abrufen geändert wurden", aber das verhindert das Problem nicht, da die beiden Transaktionen getrennte Abrufe hatten und der zweite Abruf NACH der ersten abgeschlossenen Transaktion erfolgte.

Um das Problem zu lösen, können Sie die Transaktion idempotent machen, indem Sie einen "Transaktionsschlüssel" erstellen und diesen Schlüssel in einer neuen Entität als Teil der Transaktion aufzeichnen. Die zweite Transaktion kann nach diesem Transaktionsschlüssel suchen und wird, wenn sie gefunden wird, nichts tun. Der Transaktionsschlüssel kann gelöscht werden, sobald Sie mit dem Abschluss der Transaktion zufrieden sind oder wenn Sie den Vorgang erneut abbrechen.

Ich würde gerne wissen, was "extrem selten" für AppEngine bedeutet (1-in-einer-Million oder 1-in-einer-Milliarde?), Aber mein Rat ist, dass idempotente Transaktionen für finanzielle Angelegenheiten erforderlich sind , aber nicht für Spielstände oder sogar "Leben" ;-)

1

Sollten Sie nicht versuchen, diese Art von Informationen in Memcache zu speichern, das ist viel schneller als der Datenspeicher (etwas, das Sie benötigen, wenn dieser Wert häufig in Ihrer Anwendung verwendet wird). Memcache bietet Ihnen eine nette Funktion: decr welche:

Atomically dekrementiert den Wert eines Schlüssels. Intern ist der Wert eine vorzeichenlose 64-Bit-Ganzzahl. Memcache überprüft keine 64-Bit-Überläufe. Der Wert wird, wenn er zu groß ist, umlaufen.

Suchen Sie nach decrhere. Sie sollten dann eine Aufgabe verwenden, um den Wert in diesem Schlüssel entweder alle x Sekunden oder wenn eine bestimmte Bedingung erfüllt ist, im Datenspeicher zu speichern.

+0

Danke für die Antwort, aber ich denke nicht, dass das funktionieren wird. Wenn dies eine Art globaler Zähler wäre, den ich mir Fuzzy-Werte dafür leisten könnte, wäre das perfekt, aber ich könnte mir vorstellen, dass Spieler ziemlich wütend wären, wenn der Wert wegen einer Memcache-Räumung versagt würde. –

+0

Beachten Sie, dass die Memcache decr() -Funktion auch "Caps dekrementiert unter Null auf Null" [\ [1 \]] (https://cloud.google.com/appengine/docs/python/refdocs/google.appengine.api. memcache # google.appengine.api.memcache.Client.decr). – Lee

1

Wenn Sie genau darüber nachdenken, was Sie beschreiben, ist es möglicherweise kein Problem. Denken Sie darüber nach:

Ihr Spieler hat einen Statpunkt übrig. Er sendet dann böswillig 2 Aktionen (A1 und A2), die diesen Punkt sofort verbrauchen müssen. Sowohl A1 als auch A2 sind transaktional.

Hier ist, was passieren könnte:

A1 erfolgreich ist. A2 wird dann abgebrochen. Alles gut.

A1 schlägt rechtmäßig fehl (ohne Daten zu ändern). Wiederholung geplant A2 versucht dann, gelingt. Wenn A1 es erneut versucht, wird es abgebrochen.

A1 ist erfolgreich, meldet jedoch einen Fehler. Wiederholung geplant Wenn A1 oder A2 das nächste Mal versuchen, werden sie abgebrochen.

Damit dies funktioniert, müssen Sie nachverfolgen, ob A1 und A2 abgeschlossen sind - geben Sie ihnen vielleicht eine Aufgaben-UUID und speichern Sie eine Liste abgeschlossener Aufgaben? Oder benutze einfach nur die Aufgabenwarteschlange.

+0

Danke, aber ich bin mir nicht sicher, ob das funktioniert, da das Speichern der ID selbst zu diesem Problem führen könnte ... aber ich denke, Nicks Antwort oben deckt meinen Fall ab. –

4

Edit: Das ist falsch - bitte beachten Sie die Kommentare.

Ihr Code ist in Ordnung. Die Idempotenz, auf die sich die Dokumente beziehen, bezieht sich auf Nebenwirkungen. Wie die Dokumentation erklärt, kann Ihre Transaktionsfunktion mehr als einmal ausgeführt werden. In solchen Situationen, wenn die Funktion irgendwelche Nebenwirkungen hat, werden sie mehrmals angewendet. Da Ihre Transaktionsfunktion dies nicht tut, ist es in Ordnung.

Ein Beispiel für eine problematische Funktion in Bezug auf idempotence etwas so sein würde:

def do_something(self): 
    def _tx(): 
    # Do something transactional 
    self.counter += 1 
    db.run_in_transaction(_tx) 

In diesem Fall, indem Sie durch 1 oder möglicherweise mehr als 1. Dies könnte vermieden werden, erhöht werden kann self.counter die Nebenwirkungen außerhalb der Transaktion:

def do_something(self): 
    def _tx(): 
    # Do something transactional 
    return 1 
    self.counter += db.run_in_transaction(_tx) 
+0

Danke, Nick.Sie sagen, dass alle Datenspeicheroperationen, die ich in einer Transaktion durchführe, nur einmal vorkommen, selbst wenn mein Code eine Ausnahme erhält und es erneut versucht? Wenn meine 'Dekrement'-Transaktion wegen einer Wiederholung zweimal aufgerufen wird, wird sie nur einmal dekrementiert? Gibt es in ndb-land das, weil meiner Transaktion eine ID zugewiesen wird, die der Datenspeicher bereits kennt? –

+0

(und durch "mein Code ... Wiederholungen" meine ich "ndb Wiederholungen für mich") –

+1

@ D.Hayes Sie werden mehrmals vorkommen, aber nur einer von ihnen wird zurück in den Datenspeicher übergeben werden. Der Datenspeicher verwendet optimistische Parallelität. Wenn er versucht, eine Transaktion zu bestätigen, prüft der Datenspeicher, ob die Entitäten seit dem Abrufen geändert wurden, und akzeptiert die Transaktion nur, wenn sie nicht ausgeführt wurde. –