在不同事务中插入相同类型的顶点时的 Neptune ConcurrentModificationException

Neptune ConcurrentModificationException when inserting same type of vertices in different transaction

提问人:WobiHH 提问时间:11/2/2023 最后编辑:WobiHH 更新时间:11/7/2023 访问量:61

问:

我们正在使用 AWS Neptune 数据库,目前我们遇到了两个用户同时将某些内容保存到数据库的问题。

在这张图片中,您可以以简化的方式看到我们的模型。我们有带有标签“A”的根顶点,然后可以有多个“B”顶点,这些顶点又可以有多个“C”顶点,然后是一堆相连的顶点,请参阅此处的图形模型

我们所做的是启动一个事务,添加 C、D、E、F、G 顶点和边,提交事务。

当现在两个用户同时点击完全独立的“A”顶点的“保存”时,就会出现问题。 用户 1 想要为顶点 A (ID: 4711) 保存一个新的子图,用户 2 想要为顶点 A (ID: 4712) 保存一个新的子图。这个子图是完全独立的,它们之间没有联系。我的第一个假设是,在顶点 A (ID: 4711) 下添加子图对在不同的顶点 A (ID: 4712) 下添加子图没有影响。 添加操作可能需要几秒钟。现在,如果两个事务同时尝试添加一个子图,我们会得到以下异常:

Caused by: java.util.concurrent.CompletionException: org.apache.tinkerpop.gremlin.driver.exception.ResponseException: {"detailedMessage":"Failed to complete Insert operation for a Vertex due to conflicting concurrent operations. Please retry. 0 transactions are currently rolling back.","requestId":"64be5fdb-4208-4729-bc68-dccfecbfc87f","code":"ConcurrentModificationException"}

Caused by: org.apache.tinkerpop.gremlin.driver.exception.ResponseException: {"detailedMessage":"Failed to complete Insert operation for a Vertex due to conflicting concurrent operations. Please retry. 0 transactions are currently rolling back.","requestId":"64be5fdb-4208-4729-bc68-dccfecbfc87f","code":"ConcurrentModificationException"}

我在事务中的调试器中停了下来,并尝试在 Jupyter Notebook 上添加顶点。调试器停止时,我无法添加顶点“A”。但是插入一个新的顶点“XYZ”是没有问题的,它以前在图中没有使用过。

我假设添加顶点会阻止其他事务的索引。这是我在文档中找到的:

我想说的是,我们使用 SERIALIZABLE 作为隔离级别:https://docs.aws.amazon.com/neptune/latest/userguide/transactions-isolation-levels.html: READ UNCOMMITTED – 允许所有三种类型的交互(即脏读取、不可重复读取和幻像读取)。 READ COMMITTED – 脏读取是不可能的,但不可重复和幻像读取是可能的。 可重复读取 – 脏读取和不可重复读取都是不可能的,但幻像读取仍然是可能的。 **可序列化 **– 三种类型的交互现象都不会发生。

https://docs.aws.amazon.com/neptune/latest/userguide/access-graph-gremlin-transactions.html: 无会话只读查询在 SNAPSHOT 隔离下执行,但在显式事务中运行的只读查询在 SERIALIZABLE 隔离下执行。与在 SNAPSHOT 隔离下运行的查询不同,在 SERIALIZABLE 隔离下执行的只读查询会产生更高的开销,并且可能会阻止或被并发写入阻止。

查询如下所示:

g.V("f6e9a3ed-5b2e-400a-83f4-074af0fee71f").fold().coalesce(unfold(),addV("C").property(single,"status",'"abc"').property(T.id,"f6e9a3ed-5b2e-400a-83f4-074af0fee71f")).id().fold()

我只是认为合并可能是这篇文章中的问题:使用 gremlin javascript 语言变体的 Amazon Neptune 中的 ConcurrentModificationException,但我试图在事务中停止我的调试器并刚刚执行 g.addV(“C”) 并遇到了同样的问题。

是否可以同时为不同的图添加子图?

经过一些更深入的调查,我发现我们事务中的第一个查询会阻止整个数据库。你知道为什么这个查询会阻止一切吗?

g.V().hasLabel("A").has("v",4711).outE("s").inV()
.hasLabel("B").hasId("c4...a","a0...f","ac...3")
.outE("h").inV()
.bothE("sv","ev")
.bothV().not(hasLabel("C")).simplePath()
.barrier()
.repeat(outE().not(hasLabel("sv","ev")).simplePath().inV())
.until(or(outE().count().is(0),hasLabel("C"),hasLabel("AP","AC").bothE("sv","ev").count().is(P.gt(0))))
.path().unfold().dedup().or(hasLabel("sp").has("ev"),hasLabel("ev")).barrier()
.drop()
异常 gremlin Amazon-Neptune ConcurrentModification 子图

评论


答:

0赞 Taylor Riggan 11/3/2023 #1

我相信你所看到的是间隙锁[1]的效果。如果您使用的是基于会话的事务,则一旦您开始写入给定的顶点、边或属性,则存储这些值的关联索引中的间隙将被锁定。因此,任何其他试图写入同一间隙的东西都会遇到死锁情况。在 Neptune 中,死锁被视为“赢家优先”,因此在死锁情况下写入的任何后续请求都会引发 ConcurrentModificationException。

另一点需要注意的是,写入索引的值不是您在查询中写入的值。Neptune 使用字典并将给定值的字典 ID 存储在索引中。该字典值是非确定性的,不是您可以提前预测的。这使得试图避免 CME 有点困难。

目前对 CME 的指导是在应用程序中实施重试和指数退避策略 [2]。在大多数情况下,立即重试将成功。如果您有长时间运行的事务,则可能需要调整等待/重试期,或者为每次尝试的重试时间设置更陡峭的指数曲线。

[1] https://docs.aws.amazon.com/neptune/latest/userguide/transactions-neptune.html#transactions-neptune-false-conflicts

[2] https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/retry-backoff.html

评论

0赞 WobiHH 11/3/2023
感谢您的回复。这基本上是否意味着无法并行写入 Neptune 数据库?我们有一些后台进程,具有长事务/大查询,可以更新数据库 - 我们让用户处理这些数据。有没有办法让进程同时写入?我的意思是他们肯定在写不同的东西,不会影响每个用户。添加的子图彼此完全独立。重试策略不是一个选项,因为这些事务很长,而且尝试并行写入的进程数量很大。
0赞 Taylor Riggan 11/3/2023
您可以执行并行写入,但在长时间运行的事务中出现死锁的可能性很高。为了保持数据一致性,这是必需的。您可以尝试一种设置,但如果您不小心(确保不会覆盖来自并发线程的相同数据),可能会导致数据完整性问题:docs.aws.amazon.com/neptune/latest/userguide/...。它通过参数组应用,需要重新启动数据库中的每个实例才能生效(通过 /status API 检查)。
0赞 WobiHH 11/6/2023
我做了进一步的调查,发现我们交易中的第一件事是“复杂的获取查询”。此查询似乎阻止了所有其他写入查询。如果我删除该提取查询,数据库不会被阻止,其他写入都可以。使用“PartitionStrategy”为我们数据库中的每个独立图添加一个单独的分区是一个好主意吗?我认为这将是大约 300-400 个不同的图表。
0赞 Taylor Riggan 11/7/2023
PartitionStrategy 实际上只是一个装饰器。它的工作方式类似于您为所有请求添加一个额外的步骤。怀疑它对这种用例是否有很大帮助。你最好使用一种模式,将子图提取到内存中(可能使用 TinkerGraph 对象),对内存中的图进行修改,然后将这些更改写回 Neptune。然后也许在应用程序层中拥有您的冲突解决机制。这样,您就不会在尝试进行更改时锁定数据库。has()
0赞 WobiHH 11/7/2023
谢谢,为什么你认为 PartitionStrategy 对我们不起作用?我刚刚编辑了我的原始帖子并添加了阻止查询。因此,在执行该查询并且事务仍在运行后,整个数据库将被阻止。你能明白为什么这个查询会产生问题吗?