【问题标题】:App Engine, transactions, and idempotencyApp Engine、事务和幂等性
【发布时间】:2012-04-15 04:14:53
【问题描述】:

请帮我找出我的误解。

我正在 App Engine 上编写 RPG。玩家采取的某些行动会消耗一定的统计数据。如果统计数据达到零,则玩家不能再采取任何行动。不过,我开始担心作弊玩家——如果玩家非常快速地发送两个动作,紧挨着对方怎么办?如果减少统计数据的代码不在事务中,则玩家有机会执行该操作两次。所以,我应该将减少统计数据的代码包装在事务中,对吗?到目前为止,一切顺利。

不过,在 GAE Python 中,我们在 documentation 中有这个:

注意:如果您的应用在提交交易时收到异常,则不会 总是意味着事务失败。您可能会收到 Timeout、TransactionFailedError 或 在事务已提交并最终将要提交的情况下出现 InternalError 异常 申请成功。尽可能使您的 Datastore 事务具有幂等性,以便 如果您重复交易,最终结果将是相同的。

哎呀。这意味着我正在运行的函数如下所示:


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

好吧,那是行不通的,因为它不是幂等的,对吧?如果我在它周围放置一个重试循环(我需要在 Python 中吗?我已经读过我不需要在 SO 上......但我在文档中找不到它)它可能会将值增加两次,对?由于我的代码可以捕获异常,但数据存储仍然提交数据......嗯?我该如何解决?这是我需要distributed transactions 的情况吗?真的吗?

【问题讨论】:

  • 嗯,是的,这是一个很好的观点......但在我用一堆难以诊断、难以重现的错误乱扔代码之前,我想了解什么模式我应该去这里。
  • 您的模式在正确的轨道上,但是 GAE 有很多令人沮丧的细微差别,使得像这样的手术精确实施变得困难。根据我使用 GAE 的经验,有时值得付出努力,有时则不然。
  • @TravisWebb 不同意。交易安全不是“过早的优化”,交易冲突也不是特别不可能。
  • @TravisWebb 有关“噩梦”的详细信息,请参阅我的答案。

标签: python google-app-engine transactions


【解决方案1】:

首先,尼克的回答不正确。 DHayes 的事务不是幂等的,所以如果它运行多次(即,当第一次尝试被认为失败时重试,当它没有失败时),那么该值将被多次递减。 Nick 说“数据存储会检查实体在获取后是否已被修改”,但这并不能防止问题发生,因为两个事务具有单独的获取,而第二个获取是在第一个事务完成之后。

要解决此问题,您可以通过创建“交易密钥”并将该密钥作为交易的一部分记录在新实体中来使交易具有幂等性。第二个事务可以检查该事务密钥,如果找到,则什么也不做。一旦您对交易完成感到满意,或者您放弃重试,就可以删除交易密钥。

我想知道“极其罕见”对于 AppEngine 意味着什么(百万分之一,还是十亿分之一?),但我的建议是,对于财务问题,幂等交易是必需的,但不适用于游戏得分,甚至“生命”;-)

【讨论】:

    【解决方案2】:

    编辑:这是不正确的 - 请参阅 cmets。

    您的代码很好。文档提到的幂等性是关于副作用的。正如文档所解释的,您的事务功能可能会运行不止一次;在这种情况下,如果函数有任何副作用,它们将被多次应用。既然你的事务函数没有那样做,那就没问题了。

    关于幂等性的问题函数示例如下:

    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)
    

    【讨论】:

    • 谢谢,尼克。您是说我在事务中执行的任何数据存储操作都只会发生一次,即使我的代码出现异常并重试也是如此?如果我的decrement 事务因为重试而被调用两次,它只会递减一次?在 ndb-land 中,是不是因为我的事务被分配了一些数据存储知道它已经提交的 ID?
    • @D.Hayes 它们会发生多次,但只有其中一个会被提交回数据存储。数据存储使用乐观并发,因此当它尝试提交事务时,数据存储会检查实体在获取后是否已被修改,如果没有修改,则仅接受该事务。
    • 正如mrated所说,这个答案(“你的代码很好。”)是不正确的。
    • 你是对的,这是错误的。我不知道我写它的时候在做什么。
    【解决方案3】:

    您是否应该尝试将此类信息存储在 Memcache 中,它比 Datastore 快得多(如果您的应用程序中经常使用此统计信息,您将需要它)。 Memcache 为您提供了一个不错的功能:decr 其中:

    自动递减键的值。在内部,该值是一个无符号的 64 位整数。 Memcache 不检查 64 位溢出。该值,如果太大,将环绕。

    搜索decrhere。然后,您应该每隔 x 秒或在满足特定条件时使用任务将此键中的值保存到数据存储区。

    【讨论】:

    • 感谢您的回答,但我认为这行不通。如果这是某种全局计数器,我可以负担得起模糊值,那将是完美的,但我想如果值因为内存缓存驱逐而搞砸了,玩家会非常生气。
    • 请注意,Memcache decr() 函数还“限制从零以下递减到零”[1]
    【解决方案4】:

    如果您仔细考虑您所描述的内容,它实际上可能不是问题。这样想:

    你的玩家还剩一个统计点。然后,他立即恶意发送 2 个动作(A1 和 A2),每个动作都需要消耗该点。 A1 和 A2 都是事务性的。

    以下是可能发生的情况:

    A1 成功。然后 A2 将中止。都很好。

    A1 正常失败(不更改数据)。计划重试。 A2然后尝试,成功。当 A1 再次尝试时,它将中止。

    A1 成功但报错。计划重试。下次 A1 或 A2 尝试时,它们将中止。

    为此,您确实需要跟踪 A1 和 A2 是否已完成 - 也许给他们一个任务 UUID 并存储已完成任务的列表?甚至只使用任务队列。

    【讨论】:

    • 谢谢,但我不确定这是否有效,因为存储 ID 本身可能会遇到这个问题......但我认为尼克上面的回答涵盖了我的情况。
    猜你喜欢
    • 1970-01-01
    • 2014-08-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多