java—创建引用可以在带有objectify的事务内部引发concurrentmodificationexception

qij5mzcb  于 2021-06-29  发布在  Java
关注(0)|答案(1)|浏览(572)

我在这样的事务中进行祖先查询:

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

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

当我的整个方法在一秒钟内运行几次时,我得到一个 `ConcurrentModificationException` 在 `jobKey` . 这很奇怪,因为我对它所做的只是创建一个引用并将其设置为属性。我看了一下 `Ref<?>` 上面写着:
请注意,这些方法可能会也可能不会抛出与数据存储操作相关的运行时异常;concurrentmodificationexception、datastoretimeoutexception、datastorefailureexception和datastoreneedindexexception。一些ref隐藏可能引发这些异常的数据存储操作。
有人能给我解释一下这是怎么回事吗 `Ref<?>` 为什么它会给我一个 `ConcurrentModificationException` ? 看来是罪魁祸首。
axkjgtzd

axkjgtzd1#

它是将混乱和误用异常系统对象化的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; ,你从自动取款机上取50美元(你的账户有50美元开始)。
你现在富了50美元,你的账户余额应该是-10,但实际上是40。
哇哦。
好可怕。
解决这个问题有三种方法:
锁定
想象一下,事务在我读取accounts表的那一刻就锁定了整个accounts表。在提交此事务之前,地球上没有其他任何东西可以写入此表。这就解决了问题:你的自动取款机只需挂一会儿,等待余额转移完成,然后就可以正常工作了。事实上,它甚至不识字。如果你读了,那么这个事务会写一个新的值呢?同样的问题也可能发生。所以,全局锁定整个表。
解决了问题,但这并不能扩展。
呃,该死。谁在乎呢?
只是,别在意这个。有基本的r/w锁或行锁,银行就损失了50美元。听起来很疯狂,但许多事务系统都是这样工作的。i、 e.坏了。
重试
魔法来了。要想两全其美,银行不可能搞砸,给你50块钱,同时避免锁定星球的情况,一个迂回的方法是重新运行所有查询,并再次检查结果是否相同。
在这个假设场景中,事务系统的任务是实现 [SELECT balance FROM accounts WHERE user = 'Speedy'] 命令现在返回的结果与之前返回的结果不同,这意味着整个事务现在无效,需要从头开始重新运行。这解决了问题:整个块重新运行,意识到您现在有一个余额为0,并通过抛出一个 InsufficientBalanceException . 我们避免了世界锁,代价是一些簿记和对任何提交执行的原子“快速检查是否有任何查询触及了自那时以来发生的任何更改”操作。
这正是您在这里遇到的问题—这就是objectify抛出concurrentmodificationexception时的含义。这是一个糟糕的api设计:这不是正确的异常,一般来说,您不应该仅仅因为名称听起来很模糊就重用现有的异常。但是,不管怎样,你必须接受这样一个事实,objectify在这方面犯了一个错误。
如果你从一开始就没有用正确的方式编程,那么一般的解决方法是非常复杂的,而且听起来你好像没有。
看,这里有一个巨大的问题:代码不仅仅是db/持久层中的原语。db引擎无法重放该块。毕竟,这个块包含了一堆java代码!
不,代码本身需要重新开始。
这就更复杂了。计算机是非常可靠的机器。如果两个独立的进程(例如,你向我订购10美元资金转账的银行web界面和atm机)发生冲突,并且都被迫从头开始执行命令,如果运气不好,这两台机器可靠地重试,并且可靠地第二次相互妨碍,再次重试,并且将继续吻合在一起,总是强迫对方重试,永远卡住。
解决方法是骰子。不,真的。爸爸需要一双新鞋。解决方案是:如果发生冲突,则随机等待一段时间(但要为发生的每个冲突选择一个越来越大的潜在暂停,直到某个冲突成功),从而确保两个系统最终停止吻合。听起来很疯狂,但是如果没有这个,你就不会读到这一页了——这个算法是以太网的一个基本部分,它至少可以为堆栈溢出和/或你家的互联网服务提供动力。
问题就这样变成了你不能仅仅用while循环来解决这个问题。“哎呀,需要重试”的代码很复杂。
唯一的解决方案是闭包。所有与事务系统交互的代码都必须放在lambda中,并且在修改存储系统中数据的那些部分之外必须是幂等的(运行一次与多次运行没有区别)。这样,框架本身就可以捕获重试问题,应用适当的随机指数退避,然后重新开始。
像jdbi这样的sql抽象就做到了这一点(这是为什么永远不应该为实际应用程序编写jdbc的一个非常重要的原因)。始终使用jdbi或jooq或类似的工具)。我不知道objectify是否有这样的api。否则你就得自己写了。

相关问题