并发数据访问
隔离级别
Neo4j 支持以下隔离级别
- 读提交隔离级别
-
默认 读取节点/关系的事务不会阻止另一个事务在第一个事务完成之前写入该节点/关系。这种类型的隔离比可序列化隔离级别弱,但在提供足够性能的同时,对于绝大多数情况来说已经足够了。
- 可序列化隔离级别
-
节点和关系的显式锁定。使用锁允许通过显式获取和释放锁来模拟更高隔离级别的效果。例如,如果在公共节点或关系上获取写锁,则所有事务都将在此锁上序列化,从而产生可序列化隔离级别的效果。有关如何手动获取写锁的更多信息,请参阅丢失更新。
异常
取决于隔离级别,当多个事务同时读取或写入相同数据时,可能会发生不同的异常。
此处列出的所有异常都只能在读取已提交隔离级别下发生。
丢失更新
在 Cypher 中,可以在某些情况下获取写锁来模拟改进的隔离。考虑多个并发 Cypher 查询递增属性值的情况。由于读取已提交隔离级别的限制,增量可能不会导致确定性的最终值。如果存在直接依赖关系,Cypher 会在读取之前自动获取写锁。直接依赖关系是指SET
的右侧在表达式中具有依赖属性读取或文字映射中键值对的值。
例如,如果您由一百个并发客户端运行以下查询,则属性n.prop
很可能不会递增到 100,除非在读取属性值之前获取写锁。这是因为所有查询都在自己的事务中读取n.prop
的值,并且无法看到任何尚未提交的其他事务的增量值。在最坏的情况下,如果所有线程在任何线程提交其事务之前执行读取,则最终值将低至 1。
以下示例需要写锁,并且 Cypher 会自动获取一个
MATCH (n:Example {id: 42})
SET n.prop = n.prop + 1
此示例也需要写锁,并且 Cypher 会自动获取一个
MATCH (n)
SET n += {prop: n.prop + 1}
由于在一般情况下确定此类依赖关系的复杂性,因此 Cypher 不涵盖以下任何示例用例
变量取决于先前语句中读取属性的结果
MATCH (n)
WITH n.prop AS p
// ... operations depending on p, producing k
SET n.prop = k + 1
在同一查询中读取和写入的属性之间的循环依赖关系
MATCH (n)
SET n += {propA: n.propB + 1, propB: n.propA + 1}
为了确保在更复杂的情况下也能获得确定性行为,有必要在有问题的节点上显式获取写锁。在 Cypher 中,对此没有显式支持,但可以通过写入临时属性来解决此限制。
此示例通过在读取请求的值之前写入虚拟属性来获取节点的写锁
MATCH (n:Example {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_
语句确保在读取操作之前获取锁,并且由于对该特定节点上所有并发查询的强制序列化,不会丢失任何更新。
不可重复读
不可重复读是指同一事务读取相同数据但得到不一致的结果。如果在查询中两次读取相同数据并且数据在中间被另一个并发查询修改,则很容易发生这种情况。
以下示例查询显示,两次读取相同属性可能会产生不一致的结果。如果还有其他查询正在并发运行,则不能保证p1
和p2
具有相同的值。
MATCH (n:Example {id: 42})
WITH n.prop AS p1
// another concurrent query changes the value of n.prop here.
WITH *, n.prop AS p2
RETURN p1, p2
解决此问题的最简单方法是仅读取每个属性一次,并在查询中根据需要保留它。
丢失和重复读取
在扫描期间,如果另一个并发查询将实体的属性更改为扫描之前的某个位置,则该实体可能会再次出现在索引中。同样,如果属性更改为先前扫描的位置,则该实体可能根本不会出现。
此异常只能在使用扫描索引或索引部分的操作符时发生,例如NodeIndexScan
或DirectedRelationshipIndexSeekByRange
。
在以下查询中,预期每个具有属性prop
的节点n
都恰好出现一次。但是,在索引扫描期间修改prop
属性的并发更新可能会导致节点在结果集中多次出现或根本不出现。
MATCH (n:Example) WHERE n.prop IS NOT NULL
RETURN n
锁
当发生写事务时,Neo4j 会获取锁以在更新时保持数据一致性。
Neo4j 中使用锁来确保数据一致性和隔离级别。它们不仅保护逻辑实体(例如节点和关系),还保护内部数据结构的完整性。
锁由用户运行的查询自动获取。它们确保节点/关系被锁定到一个特定事务,直到该事务完成。换句话说,一个事务对节点或关系的锁定会暂停其他事务同时修改相同的节点或关系。因此,锁可以防止事务之间共享资源的并发修改。
默认锁定行为
锁被添加到事务中并在事务结束时释放。如果事务回滚,则立即释放锁。
以下是不同操作的默认锁定行为
-
在节点或关系上添加、更改或删除属性时,会在特定节点或关系上获取写锁。
-
创建或删除节点时,会为特定节点获取写锁。
-
创建或删除关系时,会在特定关系及其两个节点上获取写锁。
要查看执行具有queryId
的查询的事务持有的所有活动锁,请使用CALL dbms.listActiveLocks(queryId)
过程。您需要是管理员才能运行此过程。
名称 | 类型 | 描述 |
---|---|---|
|
|
对应于事务的锁模式。 |
|
|
锁定资源的资源类型。 |
|
|
锁定资源的资源 ID。 |
以下示例显示执行给定查询的事务持有的活动锁。
-
要获取当前正在执行的查询的 ID,请从
SHOW TRANSACTIONS
命令中生成currentQueryId
SHOW TRANSACTIONS YIELD currentQueryId, currentQuery
-
运行
CALL dbms.listActiveLocks
,传递感兴趣的currentQueryId
(在此示例中为query-614
)CALL dbms.listActiveLocks( "query-614" )
╒════════╤══════════════╤════════════╕ │"mode" │"resourceType"│"resourceId"│ ╞════════╪══════════════╪════════════╡ │"SHARED"│"SCHEMA" │0 │ └────────┴──────────────┴────────────┘ 1 row
锁争用
如果应用程序需要对相同的节点/关系执行并发更新,则可能会发生锁争用。在这种情况下,要完成,事务必须等待其他事务持有的锁释放。如果两个或多个事务尝试同时修改相同的数据,则会增加发生死锁的可能性。在更大的图中,两个事务同时修改相同数据的可能性较小,因此死锁的可能性降低。也就是说,即使在大型图中,如果两个或多个事务尝试同时修改相同数据,也可能发生死锁。
获取的锁的类型
下表显示了根据图修改获取的锁的类型
修改 | 获取的锁 |
---|---|
创建节点 |
无锁 |
更新节点标签 |
|
更新节点属性 |
|
删除节点 |
|
创建关系* |
如果节点是稀疏的: 如果节点是密集的: |
更新关系属性 |
|
删除关系* |
如果节点是稀疏的: 如果节点是密集的: 稀疏和密集节点的 |
*适用于源节点和目标节点。
通常会获取其他锁以维护索引和其他内部结构,具体取决于图中的其他数据如何受事务影响。对于这些附加锁,无法对将获取或不会获取哪个锁做出任何假设或保证。
密集节点的锁
此“密集节点的锁”部分描述了 |
如果节点在任何时候都具有 50 个或更多关系,则该节点被认为是密集的(即,即使它在将来任何时候的关系数少于 50,它也将被视为密集)。如果节点从未拥有超过 50 个关系,则该节点被认为是稀疏的。您可以通过设置db.relationship_grouping_threshold
配置参数来配置节点被视为密集时的关系计数阈值。
在 Neo4j 中创建或删除关系时,密集节点在事务期间不会被独占锁定。相反,内部共享锁会阻止节点的删除,并且会为那些节点的并发标签更改获取共享度锁,以确保正确的计数更新。
在提交时,关系会插入到其关系链中当前没有争议(即,当前没有被另一个事务修改)的位置,并且周围的关系会被独占锁定。
换句话说,关系修改在事务中执行操作时会获取粗粒度的共享节点锁,然后在提交期间获取精确的独占关系锁。
稀疏和密集节点的锁定非常相似。稀疏节点的最大争用是更新节点的度数(即关系数)。密集节点在并发数据结构中存储此数据,因此在几乎所有情况下都可以避免关系修改的独占节点锁。
配置锁获取超时
正在执行的事务在等待另一个事务释放某些锁时可能会卡住。要终止该事务并删除锁,请将db.lock.acquisition.timeout
设置为某个正时间间隔值(例如,10s
),表示在失败事务之前应获取任何特定锁的最大时间间隔。将db.lock.acquisition.timeout
设置为0
(这是默认值)会禁用锁获取超时。
此功能无法动态设置。
将超时设置为十秒。
db.lock.acquisition.timeout=10s
死锁
由于使用了锁,因此可能会发生死锁。当两个事务相互阻塞,因为它们尝试同时修改被另一个事务锁定的节点或关系时,就会发生死锁。在这种情况下,两个事务都无法继续执行。当 Neo4j 检测到死锁时,事务将被终止,并显示瞬态错误消息代码Neo.TransientError.Transaction.DeadlockDetected
。
事务获取的所有锁仍然保持,但将在事务完成后释放。一旦锁被释放,其他等待被导致死锁的事务持有的锁的事务就可以继续执行。如果需要,您可以重试导致死锁的事务执行的工作。
频繁出现死锁表明并发写请求以无法在同时满足预期隔离性和一致性的方式发生。解决方案是确保并发更新合理地进行。例如,给定两个特定节点(A 和 B),为每个事务以随机顺序添加或删除这两个节点的关系会导致两个或多个事务同时执行时发生死锁。一种选择是确保更新始终以相同的顺序发生(先 A 后 B)。另一种选择是确保每个线程/事务对节点或关系没有任何与其他并发事务冲突的写入操作。例如,可以通过让单个线程执行特定类型的所有更新来实现这一点。
由除 Neo4j 管理的锁之外的其他同步使用引起的死锁仍然可能发生。需要同步的其他代码应以一种方式进行同步,即它永远不会在同步块中执行任何 Neo4j 操作。 |
死锁检测
例如,在Cypher-shell中同时运行以下两个查询将导致死锁,因为它们尝试同时修改相同的节点属性
:begin
MATCH (n:Test) SET n.prop = 1
WITH collect(n) as nodes
CALL apoc.util.sleep(5000)
MATCH (m:Test2) SET m.prop = 1;
:begin
MATCH (n:Test2) SET n.prop = 1
WITH collect(n) as nodes
CALL apoc.util.sleep(5000)
MATCH (m:Test) SET m.prop = 1;
将抛出以下错误消息
The transaction will be rolled back and terminated. Error: ForsetiClient[transactionId=6698, clientId=1] can't acquire ExclusiveLock{owner=ForsetiClient[transactionId=6697, clientId=3]} on NODE(27), because holders of that lock are waiting for ForsetiClient[transactionId=6698, clientId=1].
Wait list:ExclusiveLock[
Client[6697] waits for [ForsetiClient[transactionId=6698, clientId=1]]]
Cypher 子句 |
代码中的死锁处理
在代码中处理死锁时,您可能需要解决几个问题
-
仅执行有限次数的重试,并在达到阈值时失败。
-
在每次尝试之间暂停,以允许其他事务在再次尝试之前完成。
-
重试循环不仅对死锁有用,对其他类型的瞬态错误也有用。
有关如何在过程、服务器扩展或使用 Neo4j 嵌入式时处理死锁的示例,请参阅Neo4j Java 参考中的事务管理。
避免死锁
大多数情况下,死锁可以通过重试事务来解决。但是,这会对数据库的总事务吞吐量产生负面影响,因此了解避免死锁的策略很有用。
Neo4j 通过内部排序操作来协助事务。请参阅以下内容以获取有关内部锁的更多信息)。但是,此内部排序仅适用于创建或删除关系时获取的锁。因此,鼓励用户在 Neo4j 不提供内部协助的情况下(例如,当为属性更新获取锁时)对操作进行排序。这是通过确保更新以相同的顺序发生来完成的。例如,如果三个锁A
、B
和C
始终以相同的顺序获取(例如A→B→C
),则事务永远不会在等待释放锁A
的同时持有锁B
,因此不会发生死锁。
另一种选择是避免锁争用,方法是不同时修改相同的实体。
为避免死锁,应按以下顺序获取内部锁
内部锁类型可能会在不同的 Neo4j 版本之间未经任何通知而更改。此处仅列出锁类型是为了了解内部锁定机制。 |
锁类型 | 锁定实体 | 描述 | ||
---|---|---|---|---|
|
令牌 ID |
模式锁,用于锁定特定标签或关系类型上的索引和约束。 |
||
|
模式名称 |
锁定模式名称以避免重复。
|
||
|
节点 ID |
在事务创建阶段对节点获取的锁,以防止删除该节点和/或关系组。这与 |
||
|
节点 ID |
节点上的锁,用于防止对节点记录进行并发更新(即添加/删除标签、设置属性、添加/删除关系)。请注意,更新关系仅在必须更新关系链/关系组链的头部时才需要对节点进行锁定,因为这是节点记录中唯一的数据库部分。 |
||
|
节点 ID |
用于锁定节点以避免在添加或删除关系时出现并发标签更改。否则,此类更新会导致计数存储不一致。 |
||
|
关系 ID |
锁定关系以在删除期间进行独占访问。 |
||
|
节点 ID |
锁定给定密集节点的完整关系组链。这不会锁定节点,与锁 |
||
|
关系 |
关系上的锁,或更具体地说关系记录上的锁,以防止并发更新。 |