2012-04-15 15 views
6

Proszę, pomóżcie mi znaleźć moje nieporozumienie.App Engine, transakcje i idempotencja

Piszę RPG na App Engine. Pewne działania, które gracz bierze, zużywają pewną statystykę. Jeśli stat osiągnie zero, gracz nie może wykonać więcej akcji. Zacząłem się jednak martwić o oszustów - co jeśli gracz wysłał dwie akcje bardzo szybko, tuż obok siebie? Jeśli kod, który zmniejsza statystyki, nie znajduje się w transakcji, wówczas gracz ma szansę wykonać akcję dwukrotnie. Więc powinienem owinąć kod, który zmniejsza statystyki w transakcji, prawda? Jak na razie dobrze.

W GAE Python, choć mamy to w documentation:

Uwaga: Jeśli aplikacja odbiera wyjątek przy składaniu transakcję, to nie zawsze znaczy że transakcja nie powiodła się. Możesz otrzymywać wyjątki Timeout, TransactionFailedError lub InternalError w przypadkach, w których transakcje zostały zatwierdzone i ostatecznie zostaną pomyślnie zastosowane . Gdy tylko jest to możliwe, twórz transakcje Datastore idempotent tak, aby po powtórzeniu transakcji wynik końcowy był taki sam.

Whoops. Oznacza to, że funkcja biegałam, które wygląda następująco:


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

Dobrze, że nie zadziała, ponieważ sprawa nie jest idempotent, prawda? Jeśli umieściłem wokół niego pętlę retry (czy muszę w Pythonie? Czytałem, że nie muszę na SO ... ale nie mogę znaleźć go w dokumentach) może on zwiększyć wartość dwukrotnie, dobrze? Ponieważ mój kod może wychwycić wyjątek, ale magazyn danych nadal zatwierdził dane ... huh? Jak to naprawić? Czy jest to przypadek, w którym potrzebuję distributed transactions? Czy ja naprawdę?

+1

Cóż, tak, i to jest dobra uwaga ... ale zanim zaśmiecę mój kod z garstką trudnych do zdiagnozowania, trudnych do ... rozmnażaj błędy Chciałbym się dowiedzieć, jaki wzór powinienem tu wybrać. –

+0

Twój wzór jest na dobrej drodze, ale GAE ma sporo frustrujących niuansów, które utrudniają precyzyjne wykonanie chirurgiczne. W moim doświadczeniu z GAE, czasami jest to warte wysiłku, a czasem nie. –

+1

@ TravisWebb Nie zgadzam się. Bezpieczeństwo transakcji nie jest "przedwczesną optymalizacją", a kolizje transakcji nie są szczególnie prawdopodobne. –

Odpowiedz

13

pierwsze, odpowiedź Nicka nie jest poprawna. Transakcja DHayesa nie jest idempotentna, więc jeśli jest uruchamiana wiele razy (tj. Ponowna próba, gdy pierwsza próba została uznana za zakończoną niepowodzeniem, a kiedy nie), to wartość zostanie wielokrotnie zmniejszona. Nick mówi, że "magazyn danych sprawdza, czy jednostki zostały zmodyfikowane, ponieważ zostały pobrane", ale to nie zapobiega problemowi, ponieważ dwie transakcje miały oddzielne pobory, a drugie pobranie było PO pierwszej transakcji zakończonej.

Aby rozwiązać problem, można uczynić transakcję idempotentną, tworząc "klucz transakcji" i rejestrując ten klucz w nowej jednostce jako część transakcji. Druga transakcja może sprawdzić ten klucz transakcji, a jeśli zostanie znaleziony, nic nie da. Klucz transakcji można usunąć, gdy jesteś zadowolony z ukończenia transakcji lub zrezygnujesz z ponawiania próby.

Chciałbym wiedzieć, co "niezwykle rzadkie" oznacza dla AppEngine (1 na milion lub 1 na miliard?), Ale moja rada jest taka, że ​​idempotentne transakcje są wymagane w sprawach finansowych , ale nie dla wyników gier, ani nawet "żyć" ;-)

1

Nie powinieneś próbować przechowywać tego rodzaju informacji w Memcache, który jest znacznie szybszy niż Datastore (coś, czego będziesz potrzebować, jeśli ta stat jest często używana w twojej aplikacji). Memcache zapewnia piękny funkcję: decr których:

atomowo zmniejsza wartość klucza jest. Wewnętrznie wartość to 64-bitowa liczba całkowita bez znaku. Memcache nie sprawdza przekroczeń 64-bitowych. Wartość, jeśli jest zbyt duża, będzie się zawijać.

Szukaj decrhere. Następnie należy użyć zadania, aby zapisać wartość w tym kluczu do magazynu danych co x sekund lub gdy określony warunek zostanie spełniony.

+0

Dzięki za odpowiedź, ale nie sądzę, że to zadziała. Gdyby to był jakiś rodzaj globalnego kontrpropku, na który mógłbym pozwolić sobie na niewyraźne wartości, byłoby to idealne, ale wyobrażam sobie, że gracze byliby bardzo wściekli, gdyby wartość została pomieszana z powodu eksmisji z pamięci. –

+0

Należy zauważyć, że funkcja memcache decr() również "ogranicza liczbę dekrementacji poniżej zera do zera" [\ [1 \]] (https://cloud.google.com/appengine/docs/python/refdocs/google.appengine.api. memcache # google.appengine.api.memcache.Client.decr). – Lee

1

Jeśli uważnie zastanowisz się nad tym, co opisujesz, może to nie być problem. Pomyśl o tym w ten sposób:

Gracz ma jeszcze jeden punkt statystyk. Następnie złośliwie wysyła natychmiast 2 akcje (A1 i A2), z których każda musi skonsumować ten punkt. Zarówno A1, jak i A2 są transakcyjne.

Oto co może się zdarzyć:

A1 powiedzie. A2 następnie przerwie. Wszystko dobrze.

A1 nie działa prawidłowo (bez zmiany danych). Spróbuj ponownie. A2 następnie próbuje, kończy się sukcesem. Gdy A1 spróbuje ponownie, zostanie przerwane.

A1 powiedzie się, ale zgłasza błąd. Spróbuj ponownie. Następnym razem, gdy A1 lub A2 spróbuje, zostaną przerwane.

Aby to zadziałało, musisz mieć kontrolę nad tym, czy A1 i A2 zostały zakończone - może dać im UUID zadania i zapisać listę wykonanych zadań? Lub nawet po prostu użyć kolejki zadań.

+0

Dzięki, ale nie jestem pewien, czy działa, ponieważ przechowywanie samego identyfikatora może napotkać na ten problem ... ale myślę, że powyższa odpowiedź Nicka dotyczy mojej sprawy. –

4

Edytuj: To jest nieprawidłowe - zapoznaj się z komentarzami.

Twój kod jest w porządku. Idempotencja, do której odnoszą się docs, dotyczy skutków ubocznych. Jak wyjaśniają doktorzy, twoja funkcja transakcyjna może być uruchamiana więcej niż jeden raz; w takich sytuacjach, jeśli funkcja ma jakieś skutki uboczne, będą one stosowane wielokrotnie. Ponieważ funkcja transakcyjna tego nie robi, wszystko będzie dobrze.

Przykładem problematycznej funkcji w odniesieniu do idempotentność byłoby coś takiego:

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

W tym przypadku self.counter może być zwiększony o 1, lub potencjalnie więcej niż 1. Można tego uniknąć, wykonując skutki uboczne poza transakcją:

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

Dzięki, Nick.Mówisz, że wszelkie operacje związane z datastore, które wykonuję w transakcji, zdarzają się tylko raz, nawet jeśli mój kod otrzymuje wyjątek i ponawia próbę? Jeśli moja transakcja 'dérement' zostanie wywołana dwa razy z powodu ponownej próby, to zmniejszy się tylko jeden raz? W ndb-land, czy to dlatego, że moja transakcja ma przypisany jakiś identyfikator, który datastore wie, że już popełnił? –

+0

(i przez "mój kod ... ponawia" mam na myśli "ponowne wyszukiwanie ndb") –

+1

@ D.Hayes Będą się zdarzać wiele razy, ale tylko jeden z nich zostanie przywrócony do magazynu danych. Magazyn danych używa optymistycznej współbieżności, więc gdy próbuje wykonać transakcję, magazyn danych sprawdza, czy jednostki zostały zmodyfikowane, ponieważ zostały pobrane i akceptuje transakcję tylko wtedy, gdy nie zostały. –