AppEngine: поддержание согласованности хранилища данных при создании записей

Я столкнулся с небольшой дилеммой! У меня есть обработчик, называемый голосованием; когда он вызывается, он устанавливает голос пользователя за то, что он выбрал. Чтобы запомнить, какие варианты они выбрали ранее, я сохраняю параметры VoteRecord, в которых подробно описывается, какой установлен их текущий голос.

Конечно, в первый раз, когда они голосуют, я должен создать объект и сохранить его. Но последующие голоса должны просто изменить значение существующего VoteRecord. Но тут возникает проблема: при некоторых обстоятельствах могут быть созданы два VoteRecords. Это редко (произошло только один раз из всех 500 голосов, которые мы видели до сих пор), но все же плохо, когда это происходит.

Проблема возникает из-за того, что два отдельных обработчика делают по существу следующее:

query = VoteRecord.all().filter('user =', session.user).filter('poll =', poll)

if query.count(1) > 0:
 vote = query[0]

 poll.votes[vote.option] -= 1
 poll.votes[option] += 1
 poll.put()

 vote.option = option
 vote.updated = datetime.now()
 vote.put()
else:
 vote = VoteRecord()
 vote.user = session.user
 vote.poll = poll
 vote.option = option
 vote.put()

 poll.votes[option] += 1
 poll.put()

 session.user.votes += 1
 session.user.xp += 3
 session.user.put()

 incr('votes')

Мой вопрос: каков наиболее эффективный и быстрый способ обработки этих запросов, гарантирующий, что ни один запрос не будет потерян и ни один запрос не создаст два объекта VoteRecord?


person Community    schedule 06.02.2009    source источник
comment
Транзакции — ваш друг: code.google.com/appengine/docs /python/хранилище данных/   -  person Hans Nowak    schedule 07.02.2009
comment
прочитайте эту статью code.google.com/appengine/articles/sharding_counters.html   -  person Kasun    schedule 24.03.2012


Ответы (2)


Проблема в этой части:

if vote.count(1) == 0:
    obj = VoteRecord()
    obj.user = user
    obj.option = option
    obj.put()

Без транзакции ваш код мог бы выполняться в этом порядке в двух экземплярах интерпретатора:

if vote.count(1) == 0:
    obj = VoteRecord()
    obj.user = user


if vote.count(1) == 0:
    obj = VoteRecord()
    obj.user = user
    obj.option = option
    obj.put()


    obj.option = option
    obj.put()

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

Вы можете исправить это, поместив код в функцию, а затем используя

db.run_in_transaction()

для запуска функции.

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

Вот пример функции транзакции, которая проверяет свойство объекта. Ему передается ключ в качестве параметра:

def checkAndLockPage(pageKey):
  page = db.get(pageKey)
  if page.locked:
    return False
  else:
    page.locked = True
    page.put()
    return True

Одновременно только один пользователь может заблокировать этот объект, и никогда не будет дублирующихся блокировок.

person gravitation    schedule 07.02.2009
comment
Проблема в том, что я не могу помещать запросы в транзакцию. Итак, как мне его сформировать? - person ; 07.02.2009
comment
Большое спасибо за ваше обновление. Это хорошая логика - проблема решена! :) - person ; 08.02.2009
comment
Это решение с излишне высокими накладными расходами, и оно по-прежнему допускает условия гонки. - person Nick Johnson; 31.05.2009

Самый простой способ сделать это — использовать имена ключей для ваших объектов голосования и использовать Model.get_or_insert. Во-первых, придумайте схему именования для ваших имен ключей — хорошей идеей будет назвать ее после опроса — а затем выполните get_or_insert для извлечения или создания соответствующего объекта:

vote = VoteRecord.get_or_insert(pollname, parent=session.user, user=session.user, poll=poll, option=option)
if vote.option != option:
  # Record already existed; we need to update it
  vote.option = option
  vote.put()
person Nick Johnson    schedule 31.05.2009