【图数据库内核】事务、锁与隔离:读已提交、建边锁与 dense 并发
内容提要
本文介绍Neo4j图数据库的事务与锁机制。默认隔离级别为读已提交,写锁自动获取,遍历数据不受保护。锁落在节点或关系上,dense节点建边时锁粒度更细。死锁是可重试的瞬时错误,需固定更新顺序。删除节点需先删关系。
延伸解读
默认隔离级别下的图遍历风险
Neo4j默认采用读已提交隔离级别,这意味着遍历过程中读取的数据不受保护,可能被其他事务修改,导致不可重复读或索引扫描时出现重复或遗漏。对于需要稳定多跳查询的场景,不能依赖默认隔离,需考虑显式锁或应用层重试。
dense节点的锁优化与超节点瓶颈
当节点超过关系分组阈值(默认50)成为dense节点后,建边锁粒度变细,不再需要排他节点锁,从而提升并发写入能力。但这并不消除超节点作为业务热点的瓶颈,建模拆点与锁优化应并行,而非相互替代。
死锁处理与MERGE的锁顺序陷阱
死锁在Neo4j中被视为可重试的瞬时错误,应用应有限次重试并固定更新顺序。需注意MERGE可能乱序取锁,削弱引擎内部排序,建议优先使用CREATE配合唯一性约束,避免不必要的死锁风险。
Q&A
Neo4j 图数据库的默认隔离级别是什么?它允许哪些并发异常?
Neo4j 的默认隔离级别是读已提交(read-committed)。在这种隔离级别下,写锁在节点或关系上自动获取,但遍历读取的数据不受保护,可能发生不可重复读(non-repeatable reads)、丢失更新(lost updates)以及索引扫描时的实体重复或跳过(missing/double reads)。
在 Neo4j 中,如何模拟 serializable 隔离级别?
Neo4j 没有完整的 SET TRANSACTION ISOLATION LEVEL 命令,但可以通过显式获取写锁来模拟 serializable 效果。例如,对公共节点或关系获取写锁,使相关事务在该锁上串行执行,从而实现确定性的读改写。
Neo4j 中 dense 节点和 sparse 节点在创建关系时锁的获取有何不同?
对于 sparse 节点,创建关系时需要获取该节点的排他锁(NODE 锁);而对于 dense 节点,事务执行期间不会获取排他 NODE 锁,而是使用共享锁防止节点被删除,并使用度数相关共享锁与标签变更同步。提交时,关系插入允许并发修改的后备结构,偶尔按序加排他锁保证一致性。因此 dense 节点的锁粒度更细,写入并发性更好。
在 Neo4j 中遇到死锁错误应该如何处理?
死锁在 Neo4j 中被视为可重试的瞬时错误,错误码为 Neo.TransientError.Transaction.DeadlockDetected(5.25+ 含 GQLSTATUS 50N05)。应用应有限次重试(可加退避),并覆盖其他瞬时错误。同时应固定更新顺序,避免使用 MERGE 以防乱序取锁,必要时使用 CREATE 代替。
在 Neo4j 中删除节点时需要注意什么?
删除节点时,其属性会自动清除,但不会自动删除其关系。如果提交时节点仍挂有关系,事务会失败。因此需要先删除关系,或使用 DETACH DELETE。事务内可以短暂持有已删除但未提交实体的引用,但后续写操作会抛错;提交后再使用旧引用也会抛错。
Neo4j 中索引扫描在并发修改下可能出现什么问题?
对索引进行扫描(如 NodeIndexScan 或范围类 RelationshipIndexSeek)时,并发修改属性可能使实体在扫描游标前方或后方移动,导致实体重复出现或完全跳过。即使索引有唯一性约束也可能发生。
如何配置 Neo4j 的锁等待超时?
可以通过配置参数 db.lock.acquisition.timeout 来设置锁等待超时,默认值为 0(关闭)。设置为正时长(如 10 秒)后,如果事务在指定时间内无法获取锁,则会失败。该参数不支持动态设置。