AppEngine:Поддержание согласованности хранилища данных при создании записей
-
22-08-2019 - |
Вопрос
Я столкнулся с небольшой дилеммой!У меня есть обработчик voice;когда он вызывается, он устанавливает голос пользователя на то, что он выбрал.Чтобы запомнить, какие варианты они выбрали ранее, я сохраняю параметры VoteRecord, в которых подробно указано, на что установлен их текущий голос.
Конечно, при первом голосовании мне придется создать объект и сохранить его.Но последующие голоса должны просто изменить значение существующей записи VoteRecord.Но у него возникает проблема:при некоторых обстоятельствах могут быть созданы две записи голосования.Это редко (только один раз из всех 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?
Решение
Проблема в этой части:
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()
Или любая странная комбинация этого.Проблема в том, что проверка счетчика запускается снова до того, как произошла операция put, поэтому второй поток проходит через первую часть условия вместо второй.
Вы можете исправить это, поместив код в функцию, а затем используя
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
Только один пользователь одновременно может заблокировать этот объект, и дублирующих блокировок никогда не будет.
Другие советы
Самый простой способ сделать это — использовать имена ключей для объектов голосования и использовать 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()