【问题标题】:Confusion about objectify transactions retrying关于对象化事务重试的困惑
【发布时间】:2026-02-04 10:15:01
【问题描述】:

objectify documentation 声明了关于交易的这一点:

工作必须是幂等的。各种条件,包括 ConcurrentModificationException,可能导致事务重试。如果 您需要限制可以尝试交易的次数,使用 transactNew(int, Work)。

但是,谷歌数据存储区documentation 指出:

Datastore API不会自动重试事务,但您 可以添加您自己的逻辑来重试它们,例如处理冲突 当另一个请求同时更新同一个实体时。

这些陈述似乎相互矛盾。对象化交易真的重试了吗?为了安全起见,我使用transactNew(1, Work) 来确保它只运行一次,但幕后发生了什么以及为什么?

Google 文档指出,交易的一种用途是执行诸如将资金从一个帐户转移到另一个帐户之类的事情。如果交易正在重试,那么这将不起作用,因为本质上转移资金不是幂等的。在这种情况下,使用transactNew(1, Work) 是正确的做法吗?基本上,我希望安全地进行非幂等事务。

【问题讨论】:

  • 这可能取决于使用的数据存储客户端库。例如pythonndb库会自动重试因冲突而失败的事务。

标签: google-app-engine google-cloud-datastore objectify


【解决方案1】:

您正在查看 2 个不同的客户端库:

  • objectify 之一,似乎包括自动重试,可自定义
  • 普通数据存储不包括此类重试,您必须自己处理重试

汇款问题不一定不是幂等的,因为它可以通过交易实现幂等。关键是将两个账户修改都包含在同一个事务中,如the datastore client example所示:

void transferFunds(Key fromKey, Key toKey, long amount) {
  Transaction txn = datastore.newTransaction();
  try {
    List<Entity> entities = txn.fetch(fromKey, toKey);
    Entity from = entities.get(0);
    Entity updatedFrom =
        Entity.newBuilder(from).set("balance", from.getLong("balance") - amount).build();
    Entity to = entities.get(1);
    Entity updatedTo = Entity.newBuilder(to).set("balance", to.getLong("balance") + amount)
        .build();
    txn.put(updatedFrom, updatedTo);
    txn.commit();
  } finally {
    if (txn.isActive()) {
      txn.rollback();
    }
  }
}

这种方式要么更新两个帐户,要么都不更新 - 如果事务失败,所有更改要么不提交,要么回滚。

FWIW,为了验证我的(基于ndb)事务重试逻辑和幂等性,我将事务(带有相关调试消息)放在推送任务队列处理程序中,并同时触发多个任务以引起冲突。请求和应用日志足以进行验证。

【讨论】:

  • 如果有一个分片计数器,一个分片(一个实体)的计数会递增并保存在事务中,那会怎样? Stickfigure 的回答和 cmets 表明事务可能会以您不知道事务是否成功的方式失败,因此(我认为)不会发生回滚。
  • 如果正常执行回滚的代码无法确定是否应该回滚,那么我想不会有回滚。正如 Stickfigure 所建议的那样,该方法是为其编写代码。例如,使用单独的实体来执行待处理的资金转账,然后在转账交易中添加对意图实体的引用,以便您可以执行最终检查是否正确完成了意图并完成/清理转账或在需要时重试。昂贵,但如果需要,它可以变得可靠——比如货币交易。
  • “如果正常执行回滚的代码无法确定它是否应该回滚,那么我想不会有回滚” - 这似乎是谷歌文档应该强调的,因为如果这个语句是可能的那么他们的汇款示例可能会适得其反。我只是想弄清楚这是否属实。
  • Micro,数据存储中的事务是原子的——它们要么完全完成,要么完全回滚。所以消除这种担心。问题是,如果您天真地处理事务处理,您将无法知道在某些类型的(不常见但常见的)问题发生时发生了什么(提交或回滚)。如果你不在乎这些,没关系。 Objectify 是否在 CME 上重试并不重要 - 您可能想要重试。
【解决方案2】:

Objectify 将在 CME 上重试。关于在交易实际提交时您是否可以获得 CME 存在一些问题 - 曾有记录表明这是可能的,但 Google 可能已经消除了这一点。

不过,确保(比如说)银行转账完成的“正确方法”不是限制重试次数。

  1. 在重试之外创建一个事务 ID。只是一些独特的价值。
  2. 开始您的事务,尝试加载该事务 ID。它存在吗?您的交易已经完成。
  3. 如果不存在,请创建您的交易对象(使用 id)并进行借记 + 贷记。

对于任何类似银行的分类账来说,这最终都会成为标准行为;您与借方+贷方一起创建交易记录。如果创建交易记录,则很容易实现幂等性。

【讨论】:

  • 如何处理需要保存实体并增加计数器的事务?如果重试,实体将被保存两次(这是幂等的)但计数器会增加额外的时间(这不是幂等的。)
  • 同样的算法——你不保存实体两次,你首先加载并检查它的存在。您可以通过这种方式可靠地增加计数。但是,由于突变吞吐量限制,数据存储中的计数器很困难。您可以对计数器进行分片,但老实说,如果您只是保持对点击数之类的计数,最好将其放在不同的商店中。
  • 我应该注意到,所有数据库技术都受到这种计数幂等性问题的一些变化的影响——如果你增加(比如说)postgres,并且你得到一个超时,你的计数是否真的增加了?
  • 我有分片计数器,所以在保存某个实体时分片会增加。听起来我最好的选择是将重试次数限制为 1。如果出现错误,用户可以重试。您是否预见到任何问题?
  • 不,这行不通。如果您收到超时(或其他非特定)错误,您将无法知道事务是成功还是失败。您的计数器可能已经增加,也可能没有。你不能知道。此问题与 Cloud Datatastore 无关,限制重试次数对您没有帮助。 “一次且仅一次”语义从根本上来说是困难的,但几乎所有数据存储技术的解决方法都是相同的。