跳到内容

事务

几乎所有与 JanusGraph 的交互都与事务相关联。JanusGraph 事务是线程安全的,可供多个线程并发使用。JanusGraph 实例上的方法(如 graph.V(...)graph.tx().commit())会执行 ThreadLocal 查找以检索或创建与调用线程关联的事务。调用者也可以选择放弃 ThreadLocal 事务管理,转而调用 graph.tx().createThreadedTx(),该方法返回一个事务对象的引用,该对象具有读取/写入图数据以及提交或回滚的方法。

JanusGraph 事务不一定是 ACID 的。它们可以在 BerkeleyDB 上如此配置,但在 Cassandra 或 HBase 上通常不是,因为底层存储系统不提供可序列化隔离或多行原子写入,并且模拟这些属性的成本会很高。

本节描述 JanusGraph 的事务语义和 API。

事务处理

JanusGraph 中的每个图操作都发生在事务的上下文中。根据 TinkerPop 的事务规范,每个线程在图上的第一个操作(即检索或修改)时都会打开自己的图数据库事务

graph = JanusGraphFactory.open("berkeleyje:/tmp/janusgraph")
juno = graph.addVertex() //Automatically opens a new transaction
juno.property("name", "juno")
graph.tx().commit() //Commits transaction

在此示例中,打开了一个本地 JanusGraph 图数据库。添加顶点“juno”是第一个操作(在此线程中),它会自动打开一个新事务。所有后续操作都发生在该事务的上下文中,直到事务显式停止或图数据库关闭。如果在调用 close() 时事务仍处于打开状态,则未完成事务的行为在技术上是未定义的。实际上,任何非线程绑定的事务通常都会有效地回滚,但调用关闭的线程所属的线程绑定事务将首先提交。请注意,读写操作都发生在事务的上下文中。

事务范围

所有图元素(顶点、边和类型)都与它们被检索或创建的事务范围相关联。在 TinkerPop 的默认事务语义下,事务在图上的第一个操作时自动创建,并使用 commit()rollback() 显式关闭。一旦事务关闭,与该事务关联的所有图元素都将变为陈旧且不可用。但是,JanusGraph 会自动将顶点和类型转换为新的事务范围,如本例所示

graph = JanusGraphFactory.open("berkeleyje:/tmp/janusgraph")
juno = graph.addVertex() //Automatically opens a new transaction
graph.tx().commit() //Ends transaction
juno.property("name", "juno") //Vertex is automatically transitioned

另一方面,边不会自动转换,也无法在其原始事务之外访问。它们必须显式转换

e = juno.addEdge("knows", graph.addVertex())
graph.tx().commit() //Ends transaction
e = g.E(e).next() //Need to refresh edge
e.property("time", 99)

事务失败

提交事务时,JanusGraph 将尝试将所有更改持久化到存储后端。这可能不总是成功,因为 IO 异常、网络错误、机器崩溃或资源不可用。因此,事务可能会失败。事实上,在足够大的系统中,事务 最终会失败。因此,我们强烈建议您的代码预期并适应此类故障

try {
    if (g.V().has("name", name).iterator().hasNext())
        throw new IllegalArgumentException("Username already taken: " + name)
    user = graph.addVertex()
    user.property("name", name)
    graph.tx().commit()
} catch (Exception e) {
    //Recover, retry, or return error message
    println(e.getMessage())
}

上面的示例演示了一个简化的用户注册实现,其中 name 是希望注册的用户的名称。首先,检查是否已存在具有该名称的用户。如果没有,则创建一个新的用户顶点并分配名称。最后,提交事务。

如果事务失败,则会抛出 JanusGraphException。事务失败的原因有很多。JanusGraph 区分 潜在的临时 故障和 永久 故障。

潜在的临时故障与资源不可用和 IO 故障(例如网络超时)有关。JanusGraph 会通过在延迟后重试持久化事务状态来自动尝试从临时故障中恢复。重试次数和重试延迟是可配置的(请参阅 配置参考)。

永久性故障可能由完全连接丢失、硬件故障或锁争用引起。为了理解锁争用的原因,请考虑上面的注册示例,并假设用户尝试使用用户名“juno”注册。该用户名在事务开始时可能仍然可用,但到事务提交时,另一个用户可能已经并发注册了“juno”,并且该事务持有用户名的锁,从而导致另一个事务失败。根据事务语义,可以通过重新运行整个事务来从锁争用故障中恢复。

可能导致事务失败的永久异常包括

  • PermanentLockingException(本地锁争用):另一个本地线程已获得冲突的锁。

  • PermanentLockingException(X 的预期值不匹配:预期=Y vs 实际=Z):在申请锁后,验证此事务中读取的值与数据存储中的值是否相同失败。换句话说,另一个事务在值被读取和修改后修改了该值。

多线程事务

JanusGraph 通过 TinkerPop 的线程化事务支持多线程事务。因此,为了加快事务处理并利用多核架构,多个线程可以在单个事务中并发运行。

使用 TinkerPop 的默认事务处理,每个线程都会自动打开自己的图数据库事务。要打开一个线程独立的事务,请使用 createThreadedTx() 方法。

threadedGraph = graph.tx().createThreadedTx();
threads = new Thread[10];
for (int i=0; i<threads.length; i++) {
    threads[i]=new Thread({
        println("Do something with 'threadedGraph'");
    });
    threads[i].start();
}
for (int i=0; i<threads.length; i++) threads[i].join();
threadedGraph.tx().commit();

createThreadedTx() 方法返回一个新的 Graph 对象,该对象表示这个新打开的事务。图对象 tx 支持原始图支持的所有方法,但它不会为每个线程打开新事务。这允许我们启动多个线程,所有这些线程都在同一个事务中并发工作,其中一个线程在所有线程完成工作后最终提交事务。

JanusGraph 依赖于优化的并发数据结构来支持数百个并发线程在一个事务中高效运行。

并发算法

通过 createThreadedTx() 启动的线程独立事务在实现并发图算法时特别有用。大多数遍历或消息传递(以自我为中心)的图算法是 易于并行的,这意味着它们可以很容易地通过多个线程并行化和执行。这些线程中的每一个都可以在 createThreadedTx() 返回的单个 Graph 对象上操作,而不会相互阻塞。

嵌套事务

线程独立事务的另一个用例是应该独立于周围事务的嵌套事务。

例如,假设一个长时间运行的事务作业必须创建一个具有唯一名称的新顶点。由于强制执行唯一名称需要获取锁(有关详细信息,请参阅 最终一致的存储后端),并且由于事务运行时间很长,因此很可能发生锁拥塞和昂贵的事务失败。

v1 = graph.addVertex()
//Do many other things
v2 = graph.addVertex()
v2.property("uniqueName", "foo")
v1.addEdge("related", v2)
//Do many other things
graph.tx().commit() // This long-running tx might fail due to contention on its uniqueName lock

解决此问题的一种方法是在短的嵌套线程独立事务中创建顶点,如以下伪代码所示

v1 = graph.addVertex()
//Do many other things
tx = graph.tx().createThreadedTx()
v2 = tx.addVertex()
v2.property("uniqueName", "foo")
tx.commit() // Any lock contention will be detected here
v1.addEdge("related", g.V(v2).next()) // Need to load v2 into outer transaction
//Do many other things
graph.tx().commit() // Can't fail due to uniqueName write lock contention involving v2

常见的事务处理问题

事务会在对图执行的第一个操作时自动启动。用户不必手动启动事务。方法 newTransaction 仅用于启动 多线程事务

事务在 TinkerPop 语义下会自动启动,但 不会 自动终止。事务必须使用 commit()rollback() 手动终止。如果 commit() 事务失败,则应在捕获失败后使用 rollback() 手动终止。手动终止事务是必要的,因为只有用户知道事务边界。

事务将尝试从事务开始时保持其状态。这可能会导致多线程应用程序中出现意外行为,如以下人工示例所示

v = g.V(4).next() // Retrieve vertex, first action automatically starts transaction
g.V(v).bothE()
>> returns nothing, v has no edges
//thread is idle for a few seconds, another thread adds edges to v
g.V(v).bothE()
>> still returns nothing because the transactional state from the beginning is maintained

在客户端-服务器应用程序中,服务器维护多个线程来响应客户端请求时,这种意外行为很可能会发生。因此,在工作单元(例如代码片段、查询等)之后终止事务非常重要。所以,上面的例子应该是

v = g.V(4).next() // Retrieve vertex, first action automatically starts transaction
g.V(v).bothE()
graph.tx().commit()
//thread is idle for a few seconds, another thread adds edges to v
g.V(v).bothE()
>> returns the newly added edge
graph.tx().commit()

当通过 newTransaction 使用多线程事务时,在该事务范围内检索或创建的所有顶点和边都 在该事务范围之外可用。在事务关闭后访问此类元素将导致异常。如上例所示,此类元素必须在新的事务中使用 g.V(existingVertex)g.E(existingEdge) 显式刷新。

事务配置

JanusGraph 的 JanusGraph.buildTransaction() 方法允许用户配置并启动针对 JanusGraph 的新 多线程事务。因此,它与 JanusGraph.newTransaction() 相同,但具有额外的配置选项。

buildTransaction() 返回一个 TransactionBuilder,它允许配置事务的以下方面

  • readOnly() - 使事务为只读,任何修改图的尝试都将导致异常。

  • enableBatchLoading() - 为单个事务启用批量加载。此设置通过禁用一致性检查和其他优化,实现了与图范围设置 storage.batch-loading 相似的效率。与 storage.batch-loading 不同,此选项不会改变存储后端的行为。同样,您可以调用 disableBatchLoading() 以禁用单个事务的批量加载。

  • propertyPrefetching(boolean) - 为单个事务启用或禁用属性预取,即 query.fast-property。如果启用,在首次访问顶点属性时,将预取特定顶点的所有属性,从而消除后续访问同一顶点属性时的后端调用。

  • multiQuery(boolean) - 为单个事务启用或禁用查询批处理,即 query.batch。如果启用,针对存储后端执行时,单个遍历的查询将被批处理。

  • setTimestamp(long) - 将此事务的时间戳设置为与存储后端通信以进行持久化。根据存储后端,此设置可能会被忽略。对于最终一致的后端,这是用于解决写入冲突的时间戳。如果未明确指定此设置,JanusGraph 将使用当前时间。

  • setVertexCacheSize(long size) - 此事务在内存中缓存的顶点数量。此数字越大,事务可能消耗的内存就越多。如果此数字太小,事务可能需要重新获取数据,这会特别延长长时间运行事务的延迟。

  • checkExternalVertexExistence(boolean) - 此事务是否应验证用户提供的顶点 ID 的顶点是否存在。此类检查需要访问数据库,这需要时间。只有在用户绝对确定顶点必须存在时才应禁用存在检查 - 否则可能导致数据损坏。

  • checkInternalVertexExistence(boolean) - 此事务是否应在查询执行期间双重检查顶点的存在。这对于避免最终一致的存储后端上的 幻影顶点 可能很有用。默认禁用。启用此设置可能会降低查询处理速度。

  • consistencyChecks(boolean) - JanusGraph 是否应强制执行架构级别的一致性约束(例如,多重性约束)。禁用一致性检查可带来更好的性能,但要求用户在应用程序级别确保一致性确认以避免不一致。请务必小心使用!

  • skipDBCacheRead() - 在读取操作期间禁用 JanusGraph 数据库级别缓存访问。如果通过配置 cache.db-cache 禁用了数据库级别缓存,则没有任何效果。

  • lazyLoadRelations() - 为顶点的所有属性和边设置延迟加载:ID 和值按需反序列化。如果启用,如果只从顶点读取某些类型的关系,则可以大大提高大规模读取操作的性能。

一旦指定了所需的配置选项,新的事务将通过 start() 启动,该方法返回一个 JanusGraphTransaction