AppEngine:创建记录时维护DataStore一致性

时间:2022-04-14 09:43:53

I've hit a small dilemma! I have a handler called vote; when it is invoked it sets a user's vote to whatever they have picked. To remember what options they previously picked, I store a VoteRecord options which details what their current vote is set to.

我遇到了一个小困境!我有一个叫做投票的处理程序;当它被调用时,它将用户的投票设置为他们所选择的任何内容。为了记住他们之前选择的选项,我存储了一个VoteRecord选项,详细说明了他们当前的投票设置。

Of course, the first time they vote, I have to create the object and store it. But successive votes should just change the value of the existing VoteRecord. But he comes the problem: under some circumstances two VoteRecords can be created. It's rare (only happened once in all 500 votes we've seen so far) but still bad when it does.

当然,他们第一次投票时,我必须创建对象并存储它。但连续投票应该只改变现有VoteRecord的价值。但他遇到了问题:在某些情况下可以创建两个VoteRecords。这种情况很罕见(到目前为止我们所见过的所有500张选票中只发生过一次),但它确实很糟糕。

The issue happens because two separate handlers both do essentially this:

出现问题是因为两个独立的处理程序基本上都是这样做的:

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')

My question is: what is the most effective and fastest way to handle these requests while ensuring that no request is lost and no request creates two VoteRecord objects?

我的问题是:在确保没有请求丢失且没有请求创建两个VoteRecord对象的同时,处理这些请求的最有效和最快的方法是什么?

2 个解决方案

#1


1  

The issue is this part:

问题是这部分:

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

Without a transaction, your code could run in this order in two interpreter instances:

如果没有事务,您的代码可以在两个解释器实例中按此顺序运行:

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()

Or any weird combination thereof. The problem is the count test runs again before the put has occured, so the second thread goes through the first part of the conditional instead of the second.

或者任何奇怪的组合。问题是计数测试在put发生之前再次运行,所以第二个线程通过条件的第一部分而不是第二部分。

You can fix this by putting the code in a function and then using

您可以通过将代码放在函数中然后使用来解决此问题

db.run_in_transaction()

to run the function.

运行该功能。

Problem is you seem to be relying on the count of objects returned by a query for your decision logic that needs to be put in the transaction. If you read the Google I/O talks or look at the group you'll see that this is not recommended. That's because you can't transactionalize a query. Instead, you should store the count as an entity value somewhere, query for it outside of the transaction function, and then pass the key for that entity to your transaction function.

问题是您似乎依赖于查询返回的对象计数,这些对象需要放入事务中。如果您阅读Google I / O会谈或查看该论坛,您会发现不建议这样做。那是因为您无法对查询进行事务处理。相反,您应该将计数存储在某个地方的实体值,在事务函数之外查询它,然后将该实体的密钥传递给您的事务函数。

Here's an example of a transaction function that checks an entity property. It's passed the key as a parameter:

以下是检查实体属性的事务函数示例。它作为参数传递了密钥:

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

Only one user at a time can lock this entity, and there will never be any duplicate locks.

一次只有一个用户可以锁定此实体,并且永远不会有任何重复锁。

#2


3  

The easiest way to do this is to use key names for your vote objects, and use Model.get_or_insert. First, come up with a naming scheme for your key names - naming it after the poll is a good idea - and then do a get_or_insert to fetch or create the relevant entity:

最简单的方法是使用投票对象的键名,并使用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()

#1


1  

The issue is this part:

问题是这部分:

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

Without a transaction, your code could run in this order in two interpreter instances:

如果没有事务,您的代码可以在两个解释器实例中按此顺序运行:

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()

Or any weird combination thereof. The problem is the count test runs again before the put has occured, so the second thread goes through the first part of the conditional instead of the second.

或者任何奇怪的组合。问题是计数测试在put发生之前再次运行,所以第二个线程通过条件的第一部分而不是第二部分。

You can fix this by putting the code in a function and then using

您可以通过将代码放在函数中然后使用来解决此问题

db.run_in_transaction()

to run the function.

运行该功能。

Problem is you seem to be relying on the count of objects returned by a query for your decision logic that needs to be put in the transaction. If you read the Google I/O talks or look at the group you'll see that this is not recommended. That's because you can't transactionalize a query. Instead, you should store the count as an entity value somewhere, query for it outside of the transaction function, and then pass the key for that entity to your transaction function.

问题是您似乎依赖于查询返回的对象计数,这些对象需要放入事务中。如果您阅读Google I / O会谈或查看该论坛,您会发现不建议这样做。那是因为您无法对查询进行事务处理。相反,您应该将计数存储在某个地方的实体值,在事务函数之外查询它,然后将该实体的密钥传递给您的事务函数。

Here's an example of a transaction function that checks an entity property. It's passed the key as a parameter:

以下是检查实体属性的事务函数示例。它作为参数传递了密钥:

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

Only one user at a time can lock this entity, and there will never be any duplicate locks.

一次只有一个用户可以锁定此实体,并且永远不会有任何重复锁。

#2


3  

The easiest way to do this is to use key names for your vote objects, and use Model.get_or_insert. First, come up with a naming scheme for your key names - naming it after the poll is a good idea - and then do a get_or_insert to fetch or create the relevant entity:

最简单的方法是使用投票对象的键名,并使用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()