概念
Java操作Neo4j时,有两种运行的模式可以选择:内嵌模式和独立服务器模式。
-
内嵌模式: 内嵌模式下,数据库会作为你的应用程序的一个部分,与你的应用程序代码在同一个Java进程中运行。你的代码可以直接访问数据库,而无需通过网络进行通信,这使得读写操作的性能非常高。
此外,内嵌模式允许你的应用程序直接访问Neo4j的API,提供更底层的数据库操作。然而,内嵌模式的缺点是它不能支持多个应用程序同时访问数据库,对高并发环境的支持较差。
-
独立服务器模式: 在独立服务器模式下,Neo4j数据库作为一个独立的服务器运行,你的应用程序通过网络与数据库服务器进行通信。这允许了多个应用程序可以同时连接到数据库服务器,有利于构建高并发、分布式的应用。
此外,数据库服务器可以在与应用程序不同的计算机上运行,使得数据库的维护和扩展更为灵活。然而,由于所有数据库操作都需要通过网络进行,因此在网络延迟或带宽不佳的情况下,数据库操作的性能可能会受到影响。
总的来说:这两种模式各有优劣,应根据你的应用程序的需求和运行环境来选择。如果你需要构建的是一个高性能的单用户应用,或者是一个需要紧密集成数据库的复杂应用,那么内嵌模式可能会是一个更好的选择。而如果你需要构建的是一个分布式、高并发的Web应用,那么独立服务器模式可能更适合你。
相关配置
网络连接配置
Neo4j支持三种网络协议(Protocol) 分别是Bolt,HTTP 和 HTTPS,默认的连接器配置有三种,为了使用这三个端口,需要在Windows防火墙中创建Inbound Rules,允许通过端口7687,7474和7473访问本机
内嵌服务器不需要配置,只要在配置类中指定好数据库的存放路径
使用
不同的版本,使用起来api不同。本文使用都是以 5.12.0 版本进行测试
内嵌模式
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j</artifactId>
<version>5.12.0</version>
</dependency>
配置类:基于构建 GraphDatabaseService 的实现类,操作事务。
内嵌模式下,data目录下面会记录锁,不能两个程序操作同一个neo4j 数据库
@Configuration
public class Neo4jConfig{
//使用内嵌式数据库
@Bean
public GraphDatabaseService graphDatabaseService() {
Path directory = null;
directory = Paths.get("E:\\DeskTop\\222");// db存放地址
if (Files.notExists(directory, new LinkOption[0])) {
try {
Files.createDirectory(directory);
} catch (IOException var5) {
var5.printStackTrace();
}
}
DatabaseManagementServiceBuilder builder =
(new DatabaseManagementServiceBuilder(directory))
.setConfig(GraphDatabaseSettings.transaction_timeout, Duration.ofSeconds(86400L))
.setConfig(GraphDatabaseSettings.preallocate_logical_logs, true)
.setConfig(
GraphDatabaseSettings.memory_transaction_global_max_size, ByteUnit.gibiBytes(10L))
.setConfig(BoltConnector.enabled, true)
.setConfig(BoltConnector.listen_address, new SocketAddress("localhost", 7688));// 暴露一个端口,方便查询
DatabaseManagementService managementService = builder.build();
GraphDatabaseService database = managementService.database("neo4j");
registerShutdownHook(managementService);
return database;
}
private static void registerShutdownHook(final DatabaseManagementService managementService) {
Runtime var10000 = Runtime.getRuntime();
Objects.requireNonNull(managementService);
var10000.addShutdownHook(new Thread(managementService::shutdown));
}
}
注意: .setConfig(BoltConnector.enabled, true) // 启用bolt 协议
.setConfig(BoltConnector.listen_address, new SocketAddress("localhost", 7688));// 暴露端口
增加这两个配置,可以让内置服务器对外暴露一个端口,可以使用 Neo4j-DeskTop 链接,访问内嵌服务的数据
简单使用
=== Cypher 方式
private final GraphDatabaseService graphDB;
// ...
try ( Transaction tx = graphDB.beginTx();) {
for (Document doc : documents) {
Map<String, Object> parameters = new HashMap<>();
parameters.put("container", doc.getContainer());
parameters.put("branchOid", doc.getBranchOid());
parameters.put("statusInfo", doc.getStatusInfo());
// Cypher command
String query =
"CREATE (d:Document {" +
"container: $container, " +
"branchOid: $branchOid, " +
"statusInfo: $statusInfo "+
"})";
tx.execute(query, parameters);
}
// 批量提交
tx.commit();
}
==== 或者使用 createNode 的方式(Java API ) ====
try ( Transaction tx = graphDB.beginTx()) {
int batchSize = documents.size(); // 设置批处理大小
int count = 0;
for (Document doc : documents) {
Node node = tx.createNode(Label.label("Document"));
node.setProperty("container", doc.getContainer());
node.setProperty("branchOid", doc.getBranchOid());
node.setProperty("statusInfo", doc.getStatusInfo());
}
tx.commit(); // 提交事务
}
服务器模式
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
<version>5.12.0</version>
</dependency>
配置类:基于构建 org.neo4j.driver.Driver 的实现类,操作事务
// 配置包括服务器的url,用户名、密码
@Configuration
public class Neo4jConfig {
@Value(value = "${spring.neo4j.uri}")
private String noe4jUrl;
@Value(value = "${spring.neo4j.authentication.username}")
private String username;
@Value(value = "${spring.neo4j.authentication.password}")
private String password;
//使用服务器数据库
@Bean
public Session getDrive() {
Driver driver = GraphDatabase.driver(noe4jUrl, AuthTokens.basic(username, password));
return driver.session(SessionConfig.builder().withDatabase("neo4j").build());
}
}
yml配置
spring:
application:
name: neo4jDemo
neo4j:
uri: bolt://localhost:7687
authentication:
username: neo4j
password: corilead
简单使用
private void methodBatchCypher(List<Document> documents) {
try (Transaction tx = session.beginTransaction()) {
int batchSize = documents.size(); // 设置批处理大小
int count = 0;
List<Map<String, Object>> allParameters = new ArrayList<>();
for (Document doc : documents) {
Map<String, Object> parameters = new HashMap<>();
parameters.put("container", doc.getContainer());
parameters.put("branchOid", doc.getBranchOid());
parameters.put("statusInfo", doc.getStatusInfo());
})";
Map<String, Object> batchParameters = Collections.singletonMap("batch", allParameters);
tx.run(query, batchParameters);
}
}
tx.commit();
}
}
// 上面这是利用 session 手动开启一个事务,然后手动提交或者回滚 核心就是 tx.run(...); 这是一个重载的方法
private void methodBatchCypher2(List<Document> documents) {
int batchSize = 1000; // 设置批处理大小
int count = 0;
List<Map<String, Object>> allParameters = new ArrayList<>();
for (Document doc : documents) {
Map<String, Object> parameters = new HashMap<>();
parameters.put("container", doc.getContainer());
parameters.put("branchOid", doc.getBranchOid());
parameters.put("statusInfo", doc.getStatusInfo());
allParameters.add(parameters);
count++;
if (count % batchSize == 0 || count == documents.size()) {
// 执行批处理操作
String query = "UNWIND $batch AS row CREATE (d:Document { " +
"container: row.container, " +
"branchOid: row.branchOid, " +
"statusInfo: row.statusInfo, " +
})";
Map<String, Object> batchParameters = Collections.singletonMap("batch", allParameters);
session.executeWrite(tx -> {
tx.run(query, batchParameters).consume(); // 处理查询结果
return null; // 返回结果
});
// 也可以用 session.executeWrite 执行cql操作,只不过不需要手动控制事务了
// 最后要关闭 Driver和Session,来释放资源
Spring Data Neo4j 也可以用于直接访问服务器模式下的 Neo4j, 类似于Jpa的形式。
注: Spring Data Neo4j 同时支持了服务器模式和内嵌模式。然而在5.x版本之后的Spring Data Neo4j,官方选择了将其定位为一个纯粹的客户端,也就是说,它只支持通过Bolt协议或者Http协议连接到远程的Neo4j服务器,而不再支持直接访问内嵌的Neo4j实例。
这是因为使用内嵌模式的Neo4j更多是在测试环境或者特定的用途中,在生产环境中,我们一般都会使用服务器模式的Neo4j,这样可以更好地利用多核CPU,更有效地进行内存管理,以及提供是集群、HA(高可用性)、备份等特性
neo4j 数据插入性能分析
目前看使用Java操作neo4j 常用的方式有
- Spring Data Neo4j
- Neo4j Java API 可以使用两种方式: Cypher查询 和 **Java API **
内嵌数据库
基于 5.12.0 <dependency> <groupId>org.neo4j</groupId> <artifactId>neo4j</artifactId> <version>5.12.0</version> </dependency>
基于空的数据库
基于一个空 的neo4J数据库开始测试。统计插入4w 个Document节点耗时情况
都是采用批处理的方式
数据量(递增) | Spring Data Neo4j(耗时) | Cypher(耗时) | Java API(耗时) |
---|---|---|---|
4w | 513 s | 6.9 s | 7.2 s |
4w | 23 min | 4.8 s | 6.8 s |
4w | - | 4.5 s | 6.7 s |
4w | - | 4.7 s | 7.0 s |
基于60w节点的数据库
从200 环境拿到数据,目前200 环境的图库中有62w数据,包括节点以及关系
批处理的方式
数据量(递增) | Spring Data Neo4j(耗时) | Cypher(耗时) | Java API(耗时) |
---|---|---|---|
4w | > 1h | 6.2s | 6.8s |
4w | - | 6.3s | 6.6s |
4w | - | 6.3s | 6.5s |
4w | - | 5.5s | 6.5s |
单条插入测试:
基于空的数据库
数据量(递增) | Spring Data Neo4j(耗时) | Cypher(耗时) | Java API(耗时) |
---|---|---|---|
100条 | 0.6 s | 0.09 s | 0.06 s |
100条 | 0.66 s | 0.08 s | 0.08 s |
100条 | 0.72 s | 0.06s | 0.08 s |
100条 | 0.88 s | 0.06 s | 0.06 s |
基于60w节点
数据量(递增) | Spring Data Neo4j(耗时) | Cypher(耗时) | Java API(耗时) |
---|---|---|---|
100条 | 13.2 s | 0.065 s | 0.062 s |
100条 | 12.6 s | 0.066 s | 0.064 s |
100条 | 13.4 s | 0.069 s | 0.064 s |
100条 | 14.2 s | 0.067 s | 0.066 s |
服务器模式
5.17.0 社区版
注意:
当前springboot 版本为 2.7.11,对应的 spring-boot-starter-data-neo4j 中 关联的 neo4j-java-driver 依赖为4.4 版本,所以api的使用 与内嵌neo4j服务器的5.x 版本会有所区别.
可以单独引入一个 5.12.0 的 neo4j-java-driver的依赖,并排除 spring-boot-starter-data-neo4j 中 包含的driver依赖
使用起来与内嵌模式下的api有部分区别:
- 内嵌模式:直接使用 Neo4j 的 Java API 来进行数据库操作。
- 独立服务器模式:使用 Neo4j 的官方驱动程序(如 Bolt 驱动程序)通过网络连接到 Neo4j 服务器来进行数据库操作。
基于空的数据库
数据量(递增) | Spring Data Neo4j(耗时) | Java驱动(耗时) |
---|---|---|
4w | 420s | 5.15 s |
4w | > 20 min | 5.02 s |
4w | - | 5.3 s |
4w | - | 4.7s |
基于60w节点数据测试
数据量(递增) | Spring Data Neo4j(耗时) | Java驱动(耗时) |
---|---|---|
4w | - | 5s |
4w | - | 4.8s |
4w | - | 5.02s |
4w | - | 4.7s |
单条插入测试:
基于空的数据库
数据量(递增) | Spring Data Neo4j(耗时) | Java驱动(耗时) |
---|---|---|
100条 | 0.7 s | 0.58s |
100条 | 0.58 s | 0.48s |
100条 | 0.43 s | 0.34s |
100条 | 0.6 s | 0.38s |
基于60w节点数据测试
数据量(递增) | Spring Data Neo4j(耗时) | Java驱动(耗时) |
---|---|---|
100条 | 7.9s | 0.24 |
100条 | 7.5s | 0.18 |
100条 | 7.2s | 0.28 |
100条 | 7.2s | 0.23 |
导入大量数据方式
方式一: 导入 CSV 文件
文件地址可以是网络上的静态资源也可以是本地磁盘上的目录
// import java.util.Map
// import org.neo4j.driver.SessionConfig
try (var session = driver.session(SessionConfig.builder().withDatabase("neo4j").build())) {
var result = session.run("""
LOAD CSV FROM 'https://data.neo4j.com/bands/artists.csv' AS line
CALL {
WITH line
MERGE (:Artist {name: line[1], age: toInteger(line[2])})
} IN TRANSACTIONS OF 2 ROWS
""");
var summary = result.consume();
System.out.println(summary.counters());
}
将 LOAD CSV 命令在Java中执行
方式二: Cypher脚本
用 Cypher脚本创建节点,批量提交数据。
插入数据时间对比
基于空的数据库
批量插入数据
数据量(递增) | Spring Data (服务器) | Spring Data(内嵌) | api(服务器) | api(内嵌) | Cypher(内嵌) |
---|---|---|---|---|---|
4w | 420s | 513 s | 5.15 s | 7.2 s | 6.9 s |
4w | > 20 min | 23 min | 5.02 s | 6.8 s | 4.8 s |
4w | - | - | 5.3 s | 6.7 s | 4.5 s |
4w | - | - | 4.7s | 7.0 s | 4.7 s |
循环插入100条
数据量(递增) | Spring Data (服务器) | Spring Data(内嵌) | api(服务器) | api(内嵌) | Cypher(内嵌) |
---|---|---|---|---|---|
100 | 0.7 s | 0.6 s | 0.58s | 0.06 s | 0.09 s |
100 | 0.58 s | 0.66 s | 0.48s | 0.08 s | 0.08 s |
100 | 0.43 s | 0.72 s | 0.34s | 0.08 s | 0.06s |
100 | 0.6 s | 0.88 s | 0.38s | 0.06 s | 0.06 s |
基于60w 节点数据库
批量插入数据
数据量(递增) | Spring Data (服务器) | Spring Data(内嵌) | api(服务器) | api(内嵌) | Cypher(内嵌) |
---|---|---|---|---|---|
4w | > 1h | > 1h | 5s | 6.8s | 6.2s |
4w | - | - | 4.8s | 6.6s | 6.3s |
4w | - | - | 5.02s | 6.5s | 6.3s |
4w | - | - | 4.7s | 6.5s | 5.5s |
循环插入100条
数据量(递增) | Spring Data (服务器) | Spring Data(内嵌) | api(服务器) | api(内嵌) | Cypher(内嵌) |
---|---|---|---|---|---|
100 | 7.9s | 13.2 s | 0.24 | 0.062 s | 0.065 s |
100 | 7.5s | 12.6 s | 0.18 | 0.064 s | 0.066 s |
100 | 7.2s | 13.4 s | 0.28 | 0.064 s | 0.069 s |
100 | 7.2s | 14.2 s | 0.23 | 0.066 s | 0.067 s |
使用tips
1、查找Lable 以及对应的数量:
MATCH (n)
WITH LABELS(n) AS labels
UNWIND labels AS label
WITH label, COUNT(*) AS count
RETURN label, count
ORDER BY count DESC
2、创建唯一索引
为Label 以及属性id(4.0 以后的语法)
#创建约束
CREATE CONSTRAINT id_unique_for_wenzi FOR (n:wenzi)
REQUIRE n.id IS UNIQUE
CREATE CONSTRAINT id_unique_for_wenziMaster FOR (n:wenziMaster)
REQUIRE n.id IS UNIQUE
CREATE CONSTRAINT id_unique_for_wenziBranch FOR (n:wenziBranch)
REQUIRE n.id IS UNIQUE
CREATE CONSTRAINT id_unique_for_Document FOR (n:Document)
REQUIRE n.id IS UNIQUE
CREATE CONSTRAINT id_unique_for_DocumentBranch FOR (n:DocumentBranch)
REQUIRE n.id IS UNIQUE
CREATE CONSTRAINT id_unique_for_DocumentMaster FOR (n:DocumentMaster)
REQUIRE n.id IS UNIQUE
CREATE CONSTRAINT id_unique_for_part FOR (n:Part) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT id_unique_for_part_branch FOR (n:PartBranch) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT id_unique_for_part_master FOR (n:PartMaster) REQUIRE n.id IS UNIQUE;
-- j
CREATE CONSTRAINT id_unique_for_group FOR (n:Group) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT id_unique_for_user FOR (n:USER) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT id_unique_for_team FOR (n:Team) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT id_unique_for_folder FOR (n:Folder) REQUIRE n.id IS UNIQUE;
// 创建节点索引
CREATE INDEX index_for_document_id FOR (n:Document) ON (n.id);
CREATE INDEX index_for_document_branch_id FOR (n:DocumentBranch) ON (n.id);
CREATE INDEX index_for_document_master_id FOR (n:DocumentMaster) ON (n.id);
CREATE INDEX FOR (n:yanshi) ON (n.id);
CREATE INDEX FOR (n:yanshiMaster) ON (n.id);
CREATE INDEX FOR (n:yanshiBranch) ON (n.id);
CREATE INDEX index_for_part_id FOR (n:Part) ON (n.id);
CREATE INDEX index_for_part_branch_id FOR (n:PartBranch) ON (n.id);
CREATE INDEX index_for_part_master_id FOR (n:PartMaster) ON (n.id);
CREATE INDEX FOR (n:CadDocument) ON (n.id);
CREATE INDEX FOR (n:CadDocumentBranch) ON (n.id);
CREATE INDEX FOR (n:CadDocumentMaster) ON (n.id);
CREATE INDEX FOR (n:DeliveryDocuments) ON (n.id);
CREATE INDEX FOR (n:RemainProblem) ON (n.id);
CREATE INDEX FOR (n:User) ON (n.id);
CREATE INDEX FOR (n:Team) ON (n.id);
CREATE INDEX FOR (n:Group) ON (n.id);
CREATE INDEX FOR (n:Folder) ON (n.id);
CREATE INDEX FOR (n:Container) ON (n.id);
CREATE INDEX FOR (n:Tenant) ON (n.id);
创建关系的索引:
CREATE INDEX FOR ()-[r:GroupMember]-() ON (r.id)
CREATE INDEX FOR ()-[r:TeamMember]-() ON (r.id)
CREATE INDEX FOR ()-[r:MEMBER]-() ON (r.id)
CREATE INDEX FOR ()-[r:FolderMember]-() ON (r.id)
CREATE INDEX FOR ()<-[r:Access]-() ON (r.id) --使用较多 注意节点名称的大小写
CREATE INDEX FOR ()-[r:MASTER_ITERATION]-() ON (r.id)--使用较多
CREATE INDEX FOR ()-[r:BRANCH_ITERATION]-() ON (r.id)--使用较多
创建lookup索引:
CREATE LOOKUP INDEX lookup_index_name FOR (n) ON EACH labels(n)
在具有任何标签的节点上创建一个名为 lookup_index_name 的令牌查找索引。
CREATE LOOKUP INDEX FOR ()-[r]-() ON EACH type(r)
在具有任何关系类型的关系上创建标记查找索引。
日志查询
tail -F /logs/casic2a-plm/casic2a-plm.log | grep “StopWatch” -A 6
tail -F /logs/paas-platform/paas-platform.log | grep “StopWatch” -A 6
查询两个节点之间的路径
match p=(a:PDM2_RENWUSHU{id:'OR:PDM2_RENWUSHU:825642319166308633'})-[*..3]-(b:User{id:'OR:com.corilead.user.User:823821476544708687'})
return p;
// 查询两个节点之间的路径,最大路径按3条查找
查询两个节点之间,关系为Access的路径
MATCH p=(a:PDM2_RENWUSHU{id:'OR:PDM2_RENWUSHU:825642319166308633'})-[*..3]-(b:User{id:'OR:com.corilead.user.User:823821476544708687'})
UNWIND relationships(p) AS r
WITH r
WHERE type(r) = 'Access'
RETURN collect(r) AS AccessRelations;
Cql优化处理
背景:查询数据节点n和用户之间是否有指定的权限。数据和用户的权限大致分为三类
- 用户节点直接指向数据
- 用户所属组指向数据
- 用户所属容器团队指向数据
注意:优先级依次递减。并且节点和数据之间的关系可能存在多条。这时有一个是拒绝就拿拒绝的权限。
代码思路:
- 用户直接指向节点的话。直接从这里找权限,授予/拒绝。找不到的话查找下一级
- 如果组有直接指向数据的权限,就需要查询这个组和User之间是否存在
GroupMember
的关系。存在的话就可以认为用户和这个数据之间有数据的权限关系。再根据要查询的权限进行查询- 如果用户和组都没有直接指向数据的权限。就需要查询与Team节点关联的所有
Group
节点。它们是通过TeamMember
关联的。然后再查询这些Group节点与User之间是否存在关联。他们的关联关系也是GroupMember
1、最慢的查询:查询节点n以及与他有关的Access关系。
MATCH (n {id:'OR:PDM2_RENWUSHU:827071382446538785'})-[r]-(related)
WHERE labels(related) IN [['Group'], ['Team'], ['User']]
RETURN n, r, related
直接查询所有的权限种类:
1、group
MATCH (a:PDM2_RENWUSHU{id:'OR:PDM2_RENWUSHU:829232340539736099'})<-[R:Access]-(:Group)-[:GroupMember]->(:User{id:'OR:com.corilead.user.User:797010985202483410'})
RETURN R
2、team
MATCH (a:PDM2_RENWUSHU{id:'OR:PDM2_RENWUSHU:829232340539736099'})<-[R:Access]-(:Team)-[:TeamMember]->(:Group)-[:GroupMember]->(:User{id:'OR:com.corilead.user.User:797010985202483410'})
RETURN R
3、user
MATCH (a:PDM2_RENWUSHU{id:'OR:PDM2_RENWUSHU:829232340539736099'})<-[R:Access]-(User{id:'OR:com.corilead.user.User:797010985202483410'})
RETURN R
4、tenant
MATCH (a:PDM2_RENWUSHU{id:'OR:PDM2_RENWUSHU:829232340539736099'})<-[R:Access]-(:Tenant)-[:MEMBER]->(:User{id:'OR:com.corilead.user.User:797010985202483410'})
RETURN R
_RENWUSHU{id:‘OR:PDM2_RENWUSHU:829232340539736099’})<-[R:Access]-(:Team)-[:TeamMember]->(:Group)-[:GroupMember]->(:User{id:‘OR:com.corilead.user.User:797010985202483410’})
RETURN R
3、user
MATCH (a:PDM2_RENWUSHU{id:‘OR:PDM2_RENWUSHU:829232340539736099’})<-[R:Access]-(User{id:‘OR:com.corilead.user.User:797010985202483410’})
RETURN R
4、tenant
MATCH (a:PDM2_RENWUSHU{id:‘OR:PDM2_RENWUSHU:829232340539736099’})<-[R:Access]-(:Tenant)-[:MEMBER]->(:User{id:‘OR:com.corilead.user.User:797010985202483410’})
RETURN R