Java 创建引用可以在带有objectify的事务内部抛出ConcurrentModificationException

Java 创建引用可以在带有objectify的事务内部抛出ConcurrentModificationException,java,google-app-engine,google-cloud-datastore,objectify,Java,Google App Engine,Google Cloud Datastore,Objectify,我在事务内部进行祖先查询,如下所示: Task task = OfyService.ofy().load().type(Task.class) .ancestor(jobKey) .filter("locationKey", locationKey) .first().now(); 稍后在事务中,我创建并保存一个新实体,该实体使用我

我在事务内部进行祖先查询,如下所示:

Task task = OfyService.ofy().load().type(Task.class)
                        .ancestor(jobKey)
                        .filter("locationKey", locationKey)
                        .first().now();
稍后在事务中,我创建并保存一个新实体,该实体使用我在
祖先()
中使用的键作为
Ref
属性:

Task newTask=新任务(作业密钥)

//具有以下属性和构造函数的任务POJO:
@母公司
私有Ref作业密钥;
公共任务(密钥jobKey){
this.jobKey=Ref.create(jobKey);
}
当我的整个方法在一秒钟内运行几次时,我在
jobKey
上得到一个
ConcurrentModificationException
。这很奇怪,因为我所做的就是创建一个引用并将其设置为属性。我看了一下
Ref
的描述,它说:

请注意,这些方法可能会也可能不会引发运行时异常 与数据存储操作相关;ConcurrentModificationException, DatastoreTimeoutException、DatastoreFailureException和 数据存储需要IndexException。某些引用隐藏了以下数据存储操作: 可以抛出这些异常


有人能给我解释一下
Ref
是怎么回事,为什么它会给我带来
ConcurrentModificationException
?这似乎是罪魁祸首。

这是Objectify弄乱和误用异常系统的API,目的是传递一个重试

事务系统有三种主要方法来解决一个基本问题。想象一下,这一系列命令,都是单个事务的一部分(用SQL编写,假设它可读性强,熟悉程度足以理解。这只是一个示例):

//把斯皮蒂的10块钱转给我
int rBalance=[从用户='rzwitserloot'所在的帐户中选择余额]
int sBalance=[从用户='Speedy'所在的帐户中选择余额]
如果(平衡<10)抛出新平衡不足异常();
sBalance-=10;
rBalance+=10;
[更新帐户设置余额=%r余额%,其中用户='rzwitserroot']
[更新帐户设置余额=%s余额%,其中用户='Speedy']
犯罪
看起来很安全,对吗

不,事实上,这真的很棘手。想象一下,在右边,在<代码> Sale==10;<代码>,你从ATM机上从你的账户中提取50美元(而你的账户一开始就有50美元)

你现在多了50美元,你的账户余额应该是-10,但实际上是40美元

哇哦

太可怕了

解决此问题的方法有三种:

  • 锁定
  • 假设事务在我从中读取时就锁定了整个
    accounts表。在提交此事务之前,地球上没有其他任何东西可以写入此表。这就解决了问题:你的自动取款机只需挂一会儿,等待余额转移完成,然后就可以完成它的工作了。事实上,它甚至不识字。如果读取,那么该事务会写入一个新值?同样的问题也可能发生。因此,全局锁定整个表

    解决了这个问题,但这并不能扩展

  • 呃,该死。谁在乎呢
  • 只是,别在意这个。有基本的R/W锁或行锁,银行在这里只损失50美元。听起来很疯狂,但许多事务系统都是这样工作的。i、 它们都坏了

  • 重试
  • 魔法来了。要想两全其美,银行不可能搞砸,给你50美元免费,同时避免锁定地球的情况,一个迂回的方法是重新运行所有查询并双重检查结果是否相同

    在这个假设场景中,交易系统的任务是认识到
    [从帐户中选择余额,其中用户='Speedy']
    命令现在返回的结果与先前返回的结果不同,这意味着整个交易现在无效,需要从顶部重新运行。这就解决了问题:整个区块重新运行,意识到您现在的余额为0,并通过抛出
    不足余额异常
    正确中止转移资金的尝试。我们避免了世界锁,代价是一些簿记和对任何提交执行原子“快速检查是否有任何查询涉及到自那时以来发生的任何更改”操作

    这正是您在这里遇到的问题——这就是objectify在抛出ConcurrentModificationException时的含义。这是糟糕的API设计:这不是正确的异常,一般来说,您不应该仅仅因为名称听起来似乎模糊匹配就重用现有的异常。但是,无论如何,你必须接受客观化在这方面犯了一个错误的事实

    如果你从一开始就没有正确编程,那么一般的解决方案是非常复杂的,而且听起来你好像没有

    看,这里有一个巨大的问题:代码不仅仅是db/持久层中的原语。数据库引擎无法重播该块。毕竟,这个块包含了一堆java代码

    不,需要告诉代码本身重新开始

    这就更加复杂了。计算机是非常可靠的机器。如果两个单独的进程(比如,你向我订购10美元资金转账的银行网络界面和ATM机)发生冲突,并且都被迫从头开始执行命令,运气不好,两台机器可靠地重试,并可靠地相互干扰第二次,再次重试,并将继续相互吻合,总是强迫对方重试,永远卡住

    解决办法是掷骰子。不,真的。爸爸需要一双新鞋。解决方案是:如果发生冲突,则随机等待一段时间(但对于发生的每个冲突,从越来越大的潜在暂停中进行选择,直到某件事情成功),从而确保两个系统最终停止
    // Task POJO with the following property and constructor:
    @Parent
    private Ref<Job> jobKey;
    
    public Task(Key<Job> jobKey) {
        this.jobKey = Ref.create(jobKey);
    }
    
    
    // transfer 10 bucks from speedy to me
    int rBalance = [SELECT balance FROM accounts WHERE user = 'rzwitserloot']
    int sBalance = [SELECT balance FROM accounts WHERE user = 'Speedy']
    if (sBalance < 10) throw new BalanceInsufficientException();
    sBalance -= 10;
    rBalance += 10;
    [UPDATE accounts SET balance = %rBalance% WHERE user = 'rzwitserloot']
    [UPDATE accounts SET balance = %sBalance% WHERE user = 'Speedy']
    COMMIT;