创建引用可以在带有 objectify

问题描述

在这样的交易中进行祖先查询

Task task = OfyService.ofy().load().type(Task.class)
                        .ancestor(jobKey)
                        .filter("locationKey",locationKey)
                        .first().Now();

稍后在事务中,我创建并保存了一个新实体,该实体使用我在 ancestor() 中使用的密钥作为 Ref<?> 属性

Task newTask = new Task(jobKey)

// Task POJO with the following property and constructor:
@Parent
private Ref<Job> jobKey;

public Task(Key<Job> jobKey) {
    this.jobKey = Ref.create(jobKey);
}

当我的整个方法在一秒钟内运行几次时,我在 ConcurrentModificationException 上得到一个 jobKey。这很奇怪,因为我对它所做的只是创建一个引用并将其设置为属性。我查看了 Ref<?> 的说明,它说:

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

有人可以向我解释一下 Ref<?> 发生了什么以及为什么它向我抛出 ConcurrentModificationException?看起来它是这里的罪魁祸首。

解决方法

这是 Objectify 的 API 搞砸和滥用异常系统,以传达重试

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

// 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;

看起来足够安全了吧?

不,实际上,这真的很棘手。想象一下,就在中间,大约 sBalance -= 10;,您从 ATM 的帐户中提取了 50 美元(并且您的帐户开始时有 50 美元)。

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

哎呀。

可怕。

有3种方法可以解决这个问题:

  1. 锁定

想象一下,当我读取它时,事务锁定了整个帐户表。在提交此事务之前,地球上没有其他任何东西可以写入此表。这将解决问题:您的 ATM 将挂起一段时间,等待此余额转移完成,然后会做它的事情。实际上,它甚至无法阅读。如果你读了,那么这个事务写了一个新值怎么办?可能会出现同样的问题。因此,全局锁定整个表,适用于所有内容。

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

  1. 嗯,该死。谁在乎?

只是,别在意这个。有基本的 R/W 锁或行锁,银行在这里只损失 50 美元。听起来很疯狂,但许多交易系统都是这样工作的。即坏了。

  1. 重试

魔法来了。获得两全其美的一种狡猾的方法是,银行不可能搞砸并免费给您 50 美元,同时避免锁定地球的情况,是重新运行所有查询并仔细检查结果是否相同。

在这个假设场景中,事务系统的任务是实现 [SELECT balance FROM accounts WHERE user = 'Speedy'] 命令现在返回的结果与之前返回的结果不同,这意味着 整个事务现在无效,需要从顶部重新运行。这解决了问题:整个块重新运行,意识到您现在的余额为 0,并通过抛出 InsufficientBalanceException 正确地中止转移资金的尝试。我们避免了世界锁,代价是一些簿记和原子性的“快速检查是否有任何查询触及了自那时以来发生变化的任何内容”对任何提交的操作。

这正是您在这里遇到的问题 - 这就是 objectify 在抛出 ConcurrentModificationException 时的含义。这是糟糕的 API 设计:这不是正确的例外,通常您不应该仅仅因为名称听起来模糊匹配就重用现有的例外。但是,无论如何,您将不得不接受客观化在这方面犯了错误的事实。

如果您从一开始就没有以正确的方式进行编程,那么一般的修复会非常复杂,而且听起来您没有这样做。

看,有一个大问题:该代码不只是 db/persistence 层中的原语。数据库引擎无法重放块。块里面毕竟是一堆java代码!

不,代码本身需要被告知重新开始。

这就更复杂了。计算机是非常可靠的机器。如果 2 个单独的进程(比如说,您向我订购 10 美元资金转账的银行 Web 界面和那台 ATM 机)发生冲突,并且都被迫从头开始命令,运气不好,两台机器都可靠地重试并可靠地再次阻挠对方,再试一次,并不断地衔接在一起,总是迫使对方重试,永远卡住。

解决办法是骰子。不完全是。爸爸需要一双新鞋骰子。解决方案是:如果发生冲突,则等待一段随机的时间(但从每次发生的冲突中选择越来越大的潜在暂停,直到某事成功),从而确保 2 个系统最终会停止衔接。听起来很疯狂,但没有这个,你就不会阅读这个页面 - 这个算法是以太网的基本部分,它至少为 Stack Overflow 和/或你家的互联网服务提供动力。

问题就变成了你不能只用一个while循环来解决这个问题。 “糟糕,需要重试”代码很复杂。

唯一的解决方案是闭包。与您的事务系统交互的所有代码都必须放在 lambda 中,并且必须在修改存储系统中数据的那些部分之外是幂等的(运行一次和多次运行没有区别)。这样,框架本身就可以捕获重试问题,应用适当的随机指数退避,然后重新开始。

像 JDBI 这样的 SQL 抽象可以做到这一点(这是你永远不应该为实际应用程序编写 JDBC 的一个非常重要的原因。总是使用 JDBI 或 JOOQ 或类似的东西)。我不知道 objectify 是否有这样的 API。如果没有,您将不得不自己编写。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...