当前位置: 首页 > 工具软件 > JanusGraph > 使用案例 >

Janusgraph事务

顾梓
2023-12-01

事务

原文连接:http://www.janusgraph.cn/#%E4%BA%8B%E5%8A%A1

几乎所有与JanusGraph的交互都与事务相关。JanusGraph事务对于多线程并发使用是安全的。例如graph.V(…)和graph.tx().commit()方法都通过ThreadLocal查找以检索或创建与调用线程关联的事务。调用者也可以选择放弃ThreadLocal的事务管理方式,而改为调用graph.tx().createThreadedTx(),它返回对事务对象的引用,该对象具有读/写图形数据和提交或回滚事务的方法。

JanusGraph事务不一定是ACID。当使用BerkeleyDB作为存储后端的时候可以支持ACID。但是当使用Cassandra或HBase作为存储后端的时候是不支持ACID的,因为这些存储系统本身不提供串行划写入和多行原子性写入特性。

本节将介绍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()时事务仍处于打开状态,则未完成事务的行为在技术上是未定义的。实际上,任何非线程绑定的事务通常都会被有效地回滚,但是绑定线程的事务当线程调用shutdown时将首先被提交。

事务范围

所有图形元素(顶点、边和类型)都与查询或创建它们的事务范围相关联。在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是希望注册的用户的名称。首先,检查具有该名称的用户是否已经存在。如果没有,将创建一个新的用户顶点并指定名称。最后,事务被提交。

如果事务失败,则抛出janusGraphyException。事务失败的原因有很多种。JanusGraph区分临时性故障和永久性故障。

临时故障是指与资源临时不可用和IO临时中断(如网络超时)等相关的故障。JanusGraph对这种临时故障可以自动尝试从临时故障中恢复,方法是在延迟一段时间后重试并保持事务状态。重试次数和重试延迟是可配置的(请参阅配置参考)。

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

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

  • PermanentLockingException(本地锁争用):另一个本地线程已被授予冲突锁。
  • PermanentLockingException(预期值不匹配:Expected=Y vs actual=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

事务处理遇到的问题

Janusgraph在第一个图操作时自动启动事务而不必手动启动事务。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.buildTransaction()方法可以让用户针对Janusgraph进行配置和开启多线程事务。因此和JanusGraph.newTransaction()方法一样有很多的配置项如下:

buildTransaction方法返回TransactionBuilder允许对事务进行以下配置:

  • readOnly() -事务为只读,任何修改图形的尝试都将导致异常。
  • enableBatchLoading() -为单个事务启用批加载。此设置与 storage.batch-loading的设置效率一样高,因为减少了对数据一致性的检查。和storage.batch-loading设置不一样的是它不会改变后端存储的行为。
  • setTimestamp(long) -将此事务的时间戳设置为与存储后端通信以进行持久性。
  • setVertexCacheSize(long size) -此事务在内存中缓存的顶点数。这个数字越大,事务可能消耗的内存就越多。如果这个数字太小,事务可能需要重新获取数据,这会导致延迟,特别是对于长时间运行的事务。
  • checkExternalVertexExistence(boolean) -此事务是否应验证用户提供的顶点ID的顶点是否存在。这种检查需要访问数据库,这需要时间。只有当用户绝对确定顶点必须存在时,才应禁用存在性检查,否则会导致数据损坏。
  • checkInternalVertexExistence(boolean) -此事务是否应在查询执行期间再次检查顶点的存在。这对于避免最终一致的存储后端上的虚拟顶点非常有用。默认禁用。启用此设置会减慢查询处理速度。
  • consistencyChecks(boolean) -JanusGraph是否应该实施模式级的一致性约束(例如多重性约束)。禁用一致性检查可以获得更好的性能,但需要用户确保在应用程序级别进行一致性确认,以避免不一致。小心使用!
 类似资料: