2

没关系:问题出在客户端,显然 google chrome 在缓存 get 请求的结果方面比我预期的更激进。使用 firefox 进行测试会产生预期的结果。我将投票赞成删除这个问题。

我有 2 个模型,用户和项目。用户有一个“credits”属性,表示用户有多少钱。项目有一个“所有者”属性,如果它不拥有,则为“无”,如果拥有,则为用户的 id。我正在尝试将以下步骤放入事务中:

  1. 通过 id 获取用户(不是祖先查询)
  2. 通过 id 获取项目(不是祖先查询)
  3. 如果该项目已被拥有,则中止。
  4. 如果用户没有足够的积分,则中止。
  5. 通过项目的价格减少用户的信用。
  6. 将项目的“所有者”属性设置为用户的 ID。

如果一切顺利,我会向客户表明他们的购买已经完成,在 @ndb.transactional 装饰函数之外。

为了测试,我快速敲击这个功能(模拟快速按下购买按钮)。作为回应,我得到了几个迹象表明我的购买已经完成,但我预计只有一个这样的迹象。我的期望是第一个事务会通过,其他事务会在第 3 步失败。如果他们在第 3 步没有失败,那么我假设它们是一起开始的,但只有一个事务会在第 3 步结束时通过测试检查更新时间并抛出异常的事务。我的两个假设似乎都是错误的。

值得注意的是,我的交易“有效”,因为我不会超支信用。我只是多次告诉客户他们进行了相同的购买,因为我没有例外。

此外,第三步的中止确实会在短暂延迟后开始触发,但还不足以捕获最初的垃圾邮件。

class BuyItem(webapp2.RequestHandler):

    @ndb.transactional(xg=True)
    def buyItemTransaction(self, user_id, item_id):
        user = User.get_by_id(user_id)
        item = Item.get_by_id(item_id)
        if item.owner_id is not None:
            return dict(result='error', message='Item already owned.')
        if user.credits < 5000:
            return dict(result='error', message='Not enough credits.')
        user.credits -= 5000
        user.put()
        item.owner_id = user_id
        item.put()
        return dict(result='success', message='You bought the item.') 

    def get(self):
        user_id = users.get_current_user().user_id()
        item_id = self.request.get('item_id')
        try:
            response = self.buyItemTransaction(user_id, item_id)
        catch TransactionFailedError, e:
            # Transaction went through previously, so send no response.
            return
        self.response.out.write(json.dumps(response))

我的误解是什么?

4

1 回答 1

1

正如我所见,您的实施还可以。

你的问题:

为了测试,我快速敲击这个功能(模拟快速按下购买按钮)。作为回应,我得到了几个迹象表明我的购买已经完成,但我预计只有一个这样的迹象。

所以想象一下,你没有使用交易,只是在购买后点击按钮。

它不应该总是在第 3 步失败吗?是吗?

所以你的问题不在于交易。那应该没问题。您的问题在于对步骤 3 等的检查

编辑

为同时写入实体组的机会而变化的多个进程称为争用。两个进程在争夺写入;最先提交获胜

换句话说,您的第二个(并发事务)可能正在记录消息,但如果第一个事务成功,则不会应用实际的读/写

在重复发布同一消息之前,应检查您添加的最新代码。

这可能会有所帮助:

数据存储使用实体组来确定当两个进程尝试同时更新实体组中的数据时会发生什么。发生这种情况时,完成“获胜”的第一个更新,而另一个更新被取消。App Engine 通过引发异常来通知其更新被取消的进程。在大多数情况下,该过程可以再次尝试更新并成功。但是应用程序必须自己决定如何重试,因为重要数据可能在尝试之间发生了变化。这种管理并发访问的方式称为乐观并发控制。从某种意义上说,它是“乐观的”,即数据库尝试执行操作而不检查另一个进程是否正在处理相同的数据(例如使用“锁定”机制),并且只在最后检查冲突,乐观地认为这些行动会成功。更新不能保证成功,如果更新期间数据发生变化,应用程序必须重新尝试操作或采取其他措施。

还要尝试处理异常:

  try:
      response = self.buyItemTransaction(user_id, item_id)
  except TransactionFailedError, e:
      # Report an error to the user.
于 2012-12-24T12:43:49.160 回答