原文链接:https://neo4j.com/docs/java-reference/current/transaction-management/
为了充分维护数据完整性并确保良好的事务行为,Neo4j 支持 ACID 的四大属性:
具体来说:
有些数据库操作必须在事务中执行以确保 ACID 属性。具体来说,访问图、索引或模式的操作就属于这类操作。事务是单线程的、受限的和独立的。多个事务可以在单个线程中启动,并且它们彼此保持独立。
处理事务的交互周期如下所示:
完成每个事务非常重要。事务在完成之前不会释放它所获得的锁或内存。
Neo4j 中事务的惯用用法是使用 try-with-resources 语句并将事务声明为资源之一,然后启动事务并尝试执行图操作。 try 块中的最后一个操作应该提交或回滚事务,具体取决于业务逻辑。在这种情况下,try-with-resources 被用作防止异常的保护措施和额外的安全机制,以确保无论语句块内发生什么事情都可以关闭事务。所有未提交的事务将在语句结束时作为资源清理的一部分回滚。如果事务已明确提交或回滚,则不需要资源清理,事务关闭将是一个空操作。
在事务中执行的所有修改都保存在内存中,这意味着需要将非常大的更新拆分为多个事务以避免内存不足。
Neo4j 中的事务使用读提交隔离级别,这意味着它们会在数据提交后立即看到数据,但不会看到其他尚未提交的事务中的数据。这种类型的隔离比序列化弱,但提供了显着的性能优势,同时足以满足绝大多数情况。
此外,Neo4j Java API 支持显式锁定节点和关系。使用锁可以通过显式获取和释放锁来模拟更高级别隔离的效果。例如,如果在公共节点或关系上获取写锁,则所有事务都将在该锁上进行序列化 – 相当于序列化隔离级别的效果。
在 Cypher 中,在某些情况下可以获取写锁来模拟改进的隔离级别。想象一下当多个并发 Cypher 查询在增加同一属性值的情况。由于已提交读隔离级别的限制,增量可能不会产生确定性的最终值。如果存在“直接依赖“”,Cypher 会在读取前自动获取写锁。“直接依赖”是指在 SET 语句的右侧表达式中有对依赖的属性的读取操作,或者一个Map中有对键-值对的中值的读取。
例如以下查询,如果由一百个并发客户端运行,则很可能不会将属性 n.prop 增加到 100,除非在读取属性值之前获取了写锁。这是因为所有查询都会在它们自己的事务中读取 n.prop 的值,并且不会从任何其他尚未提交的事务中看到已经增加值。在最坏的情况下,如果所有线程在任何其他线程提交其事务之前完成读取,则最终值将可能就是1(假设n.prop的初始值是0)。
示例 1. Cypher 可以获取写锁
下面的例子需要一个写锁,Cypher 会自动获取一个:
MATCH (n:X {id: 42})
SET n.prop = n.prop + 1
示例 2. Cypher 可以获得写锁
这个例子也需要一个写锁,Cypher 会自动获取一个:
MATCH (n)
SET n += { prop: n.prop + 1 }
由于确定此类依赖项存在相当的复杂,Cypher 不涵盖下面的示例情况。
示例 3. 复杂Cypher
变量取决于读取语句之前若干行的某个属性的结果
MATCH (n)
WITH n.prop as p
// ... operations depending on p, producing k
SET n.prop = k + 1
示例 4. 复杂Cypher
在同一查询中存在读取和写入属性之间的循环依赖:
MATCH (n)
SET n += { propA: n.propB + 1, propB: n.propA + 1 }
为了在更复杂的情况下也保证执行的确定性,有必要很清楚的获取有关节点的写锁。 在 Cypher 中,对此没有明确的支持,但可以通过写入临时属性来解决此限制。
示例 5. 明确获取写锁
此示例通过在读取请求值之前写入虚拟属性来获取节点的写锁:
MATCH (n:X {id: 42})
SET n._LOCK_ = true
WITH n.prop as p
// ... operations depending on p, producing k
SET n.prop = k + 1
REMOVE n._LOCK_
在读取 n.prop 之前添加一个无实际意义的SET n._LOCK_ 语句,它确保在读取操作之前获取写锁,并且不会由于该特定节点上所有并发查询的强制序列化而丢失更新。
锁将被添加到事务中并在事务完成时释放。
既然使用了锁,就有可能会发生死锁,但Neo4j 会在它们发生之前检测出来(由获取锁导致的死锁)并抛出异常。在这个异常被抛出之前,事务会被标记为回滚。事务获取的所有锁仍将被持有,但会在事务完成时释放(在前面指出的 finally 块中)。一旦锁被释放,等待锁(由被死锁的事务持有)的其他事务就可以继续进行。如果需要,用户可以重试导致死锁的事务执行的工作。
频繁的死锁往往意味着并发写入请求的正在以某种方式发生,即不可能在执行它们的同时满足预期的隔离和一致性。解决方案是确保并发更新以合理的方式发生。例如,给定两个特定节点(A 和 B),为每个事务以随机顺序添加或删除这两个节点的关系,当有两个或更多事务同时执行时将导致死锁。一种选择是确保更新总是以相同的顺序发生(首先是 A,然后是 B)。另一种选择是确保每个线程/事务不会像其他并发事务那样对节点或关系进行任何有冲突的写入。例如,可以通过让单个线程执行特定类型的所有更新来实现。
使用 Neo4j 管理的锁之外的其他同步导致的死锁仍然可能发生。由于 Neo4j API 中的所有操作都是线程安全(thread safe)的,除非另有说明,因此不需要外部同步。其他需要同步的代码应该以这样的方式同步,即它永远不会在同步的块中执行任何 Neo4j 操作。
下面,您将找到如何在过程、服务器扩展或使用 嵌入式Neo4j时处理死锁的示例。
完整源代码可以在 DeadlockDocTest.java 中找到。
在处理代码中的死锁时,您可能需要解决几个问题:
下面是一个示例,说明如何实现这一点。
示例 6. 使用重试循环处理死锁
这个例子展示了如何使用重试循环来处理死锁:
Throwable txEx = null;
int RETRIES = 5;
int BACKOFF = 3000;
for ( int i = 0; i < RETRIES; i++ )
{
try ( Transaction tx = databaseService.beginTx() )
{
Object result = doStuff(tx);
tx.commit();
return result;
}
catch ( Throwable ex )
{
txEx = ex;
// Add whatever exceptions to retry on here
if ( !(ex instanceof DeadlockDetectedException) )
{
break;
}
}
// Wait so that we don't immediately get into the same deadlock
if ( i < RETRIES - 1 )
{
try
{
Thread.sleep( BACKOFF );
}
catch ( InterruptedException e )
{
throw new TransactionFailureException( "Interrupted", e );
}
}
}
if ( txEx instanceof TransactionFailureException )
{
throw ((TransactionFailureException) txEx);
}
else if ( txEx instanceof Error )
{
throw ((Error) txEx);
}
else
{
throw ((RuntimeException) txEx);
}
删除节点或关系时,将自动删除该实体的所有属性,但节点的关系不会删除。 Neo4j 强制执行一个约束(提交时),即所有关系都必须具有有效的开始节点和结束节点。实际上,这意味着尝试删除仍然具有关系的节点将在提交时引发异常。但是,只要在提交事务时不存在关系,就可以选择删除节点和附加关系的顺序。
删除语义可以总结如下:
在许多用例中,实体之间需要一定程度的唯一性。例如,系统中可能只存在一个具有特定电子邮件地址的用户。如果多个并发线程都尝试创建用户,则会创建重复项。
以下是确保唯一性的主要策略,它们都适用于跨集群和单实例部署。
通过使用单个线程,没有两个线程甚至会尝试同时创建特定实体。在集群中,外部单线程客户端可以执行这样的操作。
定义唯一性约束并使用 Cypher MERGE 子句是获取或创建唯一节点的最有效方法。有关更多信息,请参阅使用Cypher和唯一性约束获取或创建唯一节点。
可以注册一个 neo4j.org.graphdb.event.TransactionEventListener 来接收 Neo4j 数据库事务事件。一旦它在 org.neo4j.dbms.api.DatabaseManagementService 实例上注册,它就会接收它注册的数据库的事务事件。侦听器会收到有关已执行的任何写操作、并将被提交的事务的通知。如果 Transaction#commit() 没有被调用,或者事务被 Transaction#rollback() 回滚,它将被回滚并且没有事件被发送到侦听器。
在提交事务之前,会调用侦听器的 beforeCommit 方法,并使用事务中所做修改的整个差异。此时事务仍在运行,因此仍然可以进行更改。该方法也可能抛出异常,这将阻止事务被提交。如果事务回滚,则会调用侦听器的 afterRollback 方法。
侦听器的执行顺序未定义 ,不能保证任何一个侦听器所做的更改会被其他侦听器收到。
如果 beforeCommit 在所有注册的侦听器中成功执行,则提交事务并使用相同的事务数据调用 afterCommit 方法。此调用还包括从 beforeCommit 返回的对象。
在 afterCommit 事务已关闭,访问 org.neo4j.graphdb.event.TransactionData 之外的任何内容都需要打开一个新事务。 Neo4j.org.graphdb.event.TransactionEventListener 收到有关通过 org.neo4j.graphdb.event.TransactionData 可访问的任何更改的事务的通知。某些索引和架构更改不会触发这些事件。
以下示例显示如何为特定数据库注册侦听器,并在事务更改集之上执行基本操作。
完整源代码可以在 TransactionEventListenerExample.java 中找到。
示例 7. TransactionEventListener 注册事务事件侦听器并检查更改集:
public static void main( String[] args ) throws IOException
{
FileUtils.deleteDirectory( HOME_DIRECTORY );
var managementService = new DatabaseManagementServiceBuilder( HOME_DIRECTORY ).build();
var database = managementService.database( DEFAULT_DATABASE_NAME );
var countingListener = new CountingTransactionEventListener();
managementService.registerTransactionEventListener( DEFAULT_DATABASE_NAME, countingListener );
var connectionType = RelationshipType.withName( "CONNECTS" );
try ( var transaction = database.beginTx() )
{
var startNode = transaction.createNode();
var endNode = transaction.createNode();
startNode.createRelationshipTo( endNode, connectionType );
transaction.commit();
}
}
private static class CountingTransactionEventListener implements TransactionEventListener
{
@Override
public CreatedEntitiesCounter beforeCommit( TransactionData data, Transaction transaction, GraphDatabaseService databaseService ) throws Exception
{
return new CreatedEntitiesCounter( size( data.createdNodes() ), size( data.createdRelationships() ) );
}
@Override
public void afterCommit( TransactionData data, CreatedEntitiesCounter entitiesCounter, GraphDatabaseService databaseService )
{
System.out.println( "Number of created nodes: " + entitiesCounter.getCreatedNodes() );
System.out.println( "Number of created relationships: " + entitiesCounter.getCreatedRelationships() );
}
@Override
public void afterRollback( TransactionData data, CreatedEntitiesCounter state, GraphDatabaseService databaseService )
{
}
}
private static class CreatedEntitiesCounter
{
private final long createdNodes;
private final long createdRelationships;
public CreatedEntitiesCounter( long createdNodes, long createdRelationships )
{
this.createdNodes = createdNodes;
this.createdRelationships = createdRelationships;
}
public long getCreatedNodes()
{
return createdNodes;
}
public long getCreatedRelationships()
{
return createdRelationships;
}
}