2012-04-15 5 views
6

Пожалуйста, помогите мне найти мое непонимание.App Engine, транзакции и идемпотентность

Я пишу RPG в App Engine. Определенные действия, которые игрок берет, потребляют определенный стат. Если стат достигает нуля, игрок не может принимать больше никаких действий. Однако я начал беспокоиться об обманах игроков - что, если игрок отправил два действия очень быстро, прямо рядом друг с другом? Если код, который уменьшает стат, не находится в транзакции, тогда у игрока есть шанс выполнить действие дважды. Итак, я должен обернуть код, который уменьшает стат в транзакции, правильно? Все идет нормально.

В GAE Python, хотя, у нас есть это в documentation:

Примечание: Если ваше приложение получает исключение при подаче транзакции, он не всегда означает, что сделка не удалась. Вы можете получить Timeout, TransactionFailedError или Исключения InternalError в случаях, когда транзакции были совершены, и в конечном итоге будет применено . Когда это возможно, сделайте свои транзакции Datastore idempotent так, чтобы , что если вы повторите транзакцию, конечный результат будет таким же.

Упс. Это означает, что функция я бегу, что выглядит следующим образом:


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

Ну, это не сработает, потому что дело не идемпотентна, верно? Если я поставил вокруг него цикл повторения (нужно ли мне на Python? Я читал, что мне не нужно на SO ... но я не могу найти его в документах), он может увеличить значение дважды, правильно? Так как мой код может поймать исключение, но хранилище данных все еще передало данные ... а? Как это исправить? Это случай, когда мне нужно distributed transactions? Неужели?

+1

Ну, да, и это хороший момент ... но прежде, чем я поместил свой код с кучей труднодоступных для диагностики, воспроизведите ошибки, которые я хотел бы узнать, какой шаблон мне нужно делать здесь. –

+0

Ваша модель находится на правильном пути, но у GAE есть довольно много неудобных нюансов, которые делают сложную хирургическую точную реализацию, такую ​​как это. По моему опыту с GAE, иногда это стоит усилий, а иногда и нет. –

+1

@TravisWebb Не согласен. Транзакционная безопасность не является «преждевременной оптимизацией», и транзакционные столкновения особенно маловероятны. –

ответ

13

Во-первых, ответ Ника неправильный. Транзакция DHayes не является идемпотентной, поэтому, если она запускается несколько раз (т. Е. Повторить попытку, когда считалось, что первая попытка потерпела неудачу, когда она этого не сделала), тогда значение будет уменьшаться несколько раз. Ник говорит, что «хранилище данных проверяет, были ли сущности изменены с момента их получения», но это не мешает этой проблеме, поскольку две транзакции имели отдельные выборки, а вторая выборка ПОСЛЕ первой завершенной транзакции.

Чтобы решить эту проблему, вы можете сделать транзакцию идемпотентной, создав «транзакционный ключ» и записывая этот ключ в новый объект как часть транзакции. Вторая транзакция может проверить этот ключ транзакции, и если он найден, ничего не сделает. Ключ транзакции можно удалить, как только вы убедитесь, что транзакция завершена, или вы откажитесь от повторной попытки.

Я хотел бы знать, что означает «чрезвычайно редкий» для AppEngine (1-в-миллион, или 1-в-миллиард?), Но мой совет заключается в том, что идемпотентные транзакции необходимы для финансовых вопросов , но не для игр, или даже «жизней» ;-)

1

Не следует пытаться хранить этот вид информации в Memcache, который намного быстрее, чем хранилище данных (что-то вам понадобится, если этот стат используется в вашем приложении). Memcache предоставляет вам приятную функцию: decr который:

Атомно уменьшает значение ключа. Внутренне значение представляет собой неподписанное 64-битное целое число. Memcache не проверяет 64-битные переполнения. Значение, если оно слишком велико, обернется.

Поиск decrhere. Затем вы должны использовать задачу для сохранения значения в этом ключе в хранилище данных либо каждые x секунд, либо при выполнении определенного условия.

+0

Спасибо за ответ, но я не думаю, что это сработает. Если бы это был какой-то глобальный счетчик, который я мог бы позволить себе нечеткие значения, это было бы идеально, но я бы подумал, что игроки будут довольно безумны, если значение будет испорчено из-за выселения из memcache. –

+0

Обратите внимание, что функция memcache decr() также «колпачки уменьшаются до нуля до нуля» [\ [1 \]] (https://cloud.google.com/appengine/docs/python/refdocs/google.appengine.api. Memcache # google.appengine.api.memcache.Client.decr). – Lee

1

Если вы внимательно относитесь к тому, что вы описываете, это может быть не проблема. Подумайте об этом так:

У игрока есть один стат. Затем он злонамеренно отправляет 2 действия (A1 и A2) мгновенно, каждый из которых должен потреблять эту точку. Как A1, так и A2 являются транзакционными.

Вот что может случиться:

A1 успешно. Затем A2 прерывается. Все хорошо.

A1 не может быть законным (без изменения данных). Повторите попытку. A2 затем пытается, преуспевает. Когда A1 попытается снова, он будет прерван.

A1 успешно принят, но сообщает об ошибке. Повторите попытку. В следующий раз, когда А1 или А2 попытаются, они будут прерваны.

Для этого необходимо следить за тем, завершены ли A1 и A2 - возможно, дать им задачу UUID и сохранить список готовых задач? Или просто используйте очередь задач.

+0

Спасибо, но я не уверен, что работает, так как хранение самого идентификатора может столкнуться с этой проблемой ... но я думаю, что ответ Ник выше охватывает мой случай. –

4

Редактировать: Это неверно - просмотрите комментарии.

Ваш код в порядке. Идемпотентность, на которую ссылаются документы, относится к побочным эффектам. Как объясняют документы, ваша транзакционная функция может быть запущена более одного раза; в таких ситуациях, если функция имеет какие-либо побочные эффекты, они будут применяться несколько раз. Поскольку ваша функция транзакций не делает этого, все будет хорошо.

Пример проблемной функции в отношении идемпотентности будет что-то вроде этого:

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

В этом случае self.counter может быть увеличен на 1 или потенциально более 1. Этого можно избежать, выполнив побочные эффекты, не связанные с транзакцией:

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

Спасибо, Ник.Вы говорите, что любые операции с хранилищем данных, которые я делаю в транзакции, будут выполняться только один раз, даже если мой код получает исключение и повторяет его? Если моя транзакция 'decment' вызывается дважды из-за повтора, она будет только уменьшаться один раз? В ndb-land это потому, что моей транзакции присваивается некоторый идентификатор, который хранилище данных уже знает? –

+0

(и «мой код ... повторяет». Я имею в виду «ndb-попытки для меня») –

+1

@ D.Hayes Они будут происходить несколько раз, но только один из них будет передан обратно в хранилище данных. В хранилище данных используется оптимистичный параллелизм, поэтому, когда он пытается совершить транзакцию, хранилище данных проверяет, были ли сущности изменены с момента их получения, и только принимает транзакцию, если они еще не были. –

Смежные вопросы