2012-04-15 23 views
6

请帮我找到我的误解。应用程序引擎,交易和幂等性

我正在App Engine上撰写RPG。玩家采取的某些行动消耗某些属性。如果统计数字达到零,玩家可以不采取任何行动。然而,我开始担心作弊的球员,如果一名球员非常快速地发出两个动作,那么彼此相邻呢?如果减少属性的代码不在交易中,那么玩家有可能执行两次动作。所以,我应该包装在交易中减少stat的代码,对吧?到现在为止还挺好。

在GAE的Python,虽然,我们有这样的documentation

注意:如果提交事务,当你的应用程序接收到一个例外,它并不总是 意味着该交易失败。如果事务已提交并且最终将成功应用 ,则可能会收到Timeout,TransactionFailedError或InternalError异常。只要有可能,使您的数据存储交易幂等 ,如果您重复交易,最终结果将是相同的。

哎呦。这意味着,我是跑的功能如下:


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

嗯,这不是要去工作,因为事情是不是幂,对不对?如果我在它周围放置一个重试循环(是否需要使用Python?我读过,我不需要这么做,但是我无法在文档中找到它),它可能会增加两次值,对?由于我的代码可以捕获异常,但数据存储区仍然提交数据......呵?我该如何解决?这是我需要distributed transactions的情况吗?我真的吗?

+1

嗯,是的,这是一个很好的观点......但在我用一堆难以诊断的,重现错误我想知道我应该在这里做什么模式。 – 2012-04-15 04:55:10

+0

您的模式正处于正确的轨道上,但GAE有很多令人沮丧的细微差别,使得这种难以精确的手术操作成为可能。根据我对GAE的经验,有时候这是值得的,有时不值得。 – 2012-04-15 05:07:56

+1

@TravisWebb不同意。交易安全性不是“过早优化”,交易冲突也不是特别不可能的。 – 2012-04-15 10:24:48

回答

13

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

要解决此问题,您可以通过创建“事务密钥”并将该密钥记录在新实体中作为事务的一部分来使交易幂等。第二个事务可以检查该事务密钥,如果找到,将不会执行任何操作。一旦您完成交易或您放弃重试,交易密钥可以被删除。

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

1

您不应该尝试在Memcache中存储这种信息,这比Datastore快得多(如果此属性在您的应用程序中经常使用,您将需要这些信息)。 Memcache为您提供了一个很好的功能:decr其中:

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

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

+0

感谢您的回答,但我认为不会奏效。如果这是我能够承受的模糊值的某种全球性计数器,那将是完美的,但我可以想象如果由于memcache驱逐造成的值变得混乱,玩家会很生气。 – 2012-04-15 05:29:13

+0

请注意,Memcache decr()函数还会“封闭在零之下递减到零”[[[1 \]](https://cloud.google.com/appengine/docs/python/refdocs/google.appengine.api)。内存缓存#google.appengine.api.memcache.Client.decr)。 – Lee 2015-09-10 13:03:27

1

如果您仔细考虑您所描述的内容,可能并不是一个问题。想想这样:

你的球员有一个统计点左。然后他会立即恶意发送两个动作(A1和A2),每个动作需要消耗该点。 A1和A2都是事务性的。

以下是可能发生的情况:

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

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

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

为了这个工作,你需要跟踪A1和A2是否已经完成 - 也许给他们一个任务UUID并存储完成任务列表?甚至只是使用任务队列。

+0

谢谢,但我不知道这是行不通的,因为存储ID本身可能会遇到这个问题......但我认为尼克的答案涵盖了我的情况。 – 2012-04-15 16:37:30

4

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

你的代码没问题。文档所指的幂等性是关于副作用的。正如文档解释的那样,您的交易功能可能会运行多次;在这种情况下,如果功能有任何副作用,它们将被多次应用。由于你的交易功能不这样做,它会没事的。

与关于幂等性有问题的功能的一个例子是这样的:

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) 
+0

谢谢,尼克。你说我在一个事务中做的任何数据存储操作只会发生一次,即使我的代码得到一个异常并重试它?如果我的'decrement'事务由于重试而被调用两次,它只会减少一次?在ndb-land中,是因为我的事务被分配了一些ID,数据存储库知道它已经提交了? – 2012-04-15 16:36:13

+0

(以及“我的代码...重试”我的意思是“ndb为我重试”) – 2012-04-15 16:42:19

+1

@ D.Hayes他们会发生多次,但只有其中一人会被提交回数据存储。数据存储使用乐观并发性,所以当它试图提交事务时,数据存储会检查实体是否在被提取后被修改,并且只有在事务没有被执行时才接受。 – 2012-04-15 23:59:59

相关问题