云计算百科
云计算领域专业知识百科平台

JGraphT 选型实录:为什么不用 SQL,也不用 Neo4j

项目里有个 CRM 客户管理助手,其中规则五(关系链挖掘)需要处理节点间的关系遍历。选型时经历了三轮思考:SQL 能不能搞定?要不要上 Neo4j?最终为什么选了 JGraphT?记录一下完整的决策过程。

项目背景

CRM 助手是一个 Java 后端服务(Spring Boot),有一套规则引擎处理各种业务规则。其中规则五是"关系链挖掘":给定一个关键人,沿指定关系类型(同事、同司、拜访)查找关联人,用于推荐引荐路径。

图结构很简单:

  • 节点:客户、联系人
  • 边:隶属于、同事、同司、拜访记录
  • 数据来源:全部来自 CRM 自有的 MySQL 表

规则五的查询本质是带条件的受限局部遍历:从关键人出发,沿指定边类型走一跳优先、二跳兜底,命中即停,还要过滤离职、拜访超时等条件。

第一轮:SQL 能不能搞定?

先想的是最直接的方案:SQL 查。

一跳:勉强能做

一跳查询(找同部门同事中有拜访记录的人)用 JOIN 就能做:

SELECT DISTINCT c2.person_id
FROM colleague_relation c1
JOIN colleague_relation c2 ON c1.dept_id = c2.dept_id
JOIN visit_record v ON v.visitor_id = c2.person_id
WHERE c1.person_id = ? AND v.client_id = ?

能做,但没有任何优势——图查询就是一行 neighborListOf。

二跳:开始失控

二跳(同事的同事、跨部门关系)要上递归 CTE:

WITH RECURSIVE colleague_chain AS (
SELECT person_id, 1 AS depth
FROM colleague_relation
WHERE person_id = ?
UNION ALL
SELECT c.person_id, cc.depth + 1
FROM colleague_relation c
JOIN colleague_chain cc ON c.dept_id = cc.person_id
WHERE cc.depth < 2
)
SELECT DISTINCT person_id FROM colleague_chain;

写法复杂、容易出错,还要自己控制层数和去重。这是"图遍历"的活,硬塞给 SQL 是拿关系代数模拟图算法。

最短路径 + 命中即终止:SQL 的死穴

规则五要的是"跳数最少的引荐链,命中即停"。BFS 天然按跳数递增扩展,第一次命中就是最短路径,遍历器自带终止。

SQL 要表达"取到最优就停"得写存储过程或复杂递归,最短路径还要自己做回溯——极难维护,且每次扫描都要重写查询。

边类型叠加:查询爆炸

图里有四类边:隶属、同事、同司、拜访。遍历时还要过滤"只走同事和同司边"“排除离职”“拜访超时过滤”。关系每多一种,JOIN 条件就成倍叠加,谓词一多查询就"爆炸"。

根本问题:查询逻辑与存储死绑

JGraphT 把图遍历(BFS/最短路径)作为内存中的原地操作,图结构怎么变、规则怎么改,不碰存储层。SQL 则是把图逻辑硬编码进查询语句,图模型一变就要改 SQL、改索引,扫描逻辑和表结构死死绑在一起。

还有一个隐患:MySQL 的递归 CTE 对"层数上限"的控制是"逐层 UNION ALL 直到不产生新行"。写 2 跳还勉强能看,一旦未来要扩到 3 跳、4 跳,CTE 的层数扩展和路径回溯复杂度会指数级恶化。而 JGraphT 里 MAX_DEPTH 就是个常量,改一行完事。

结论:一跳勉强可做但没优势;一旦涉及二跳、路径回溯、边类型叠加,递归 CTE 复杂易错、性能随跳数恶化。不是 SQL 不行,是这个查询本质上是图问题,SQL 表达不了它的"遍历语义"。

第二轮:要不要上 Neo4j?

SQL 不行,那上图数据库?考虑了 Neo4j,最终还是没选。

1. 规模根本不匹配

图谱只有两类节点(客户 + 联系人),边数约为联系人数的 2~4 倍。纯内存建图毫秒级完成,每周扫描前 rebuild 一次即可。这个量级下,Neo4j 的持久化、索引、集群能力全都用不上。

项目文档明确写了升级条件:客户量超过 10 万时再考虑 Neo4j。当前量级远没到那一步。

2. 查询复杂度用不上图数据库

规则五本质是带条件的受限局部遍历:一跳优先、二跳兜底、命中即停。JGraphT 的 BreadthFirstIterator / Graphs.neighborListOf / 边类型过滤原地就能做,还天然支持"最短路径、命中即终止"。

Neo4j 的优势场景是全图多跳分析(如全链路引荐推荐),在这里属于杀鸡用牛刀。

3. 部署运维成本

JGraphT 是纯 Java 库,一个 Maven 依赖,随应用一起跑,零额外依赖、无额外进程、无端口、无备份、无监控。

Neo4j 是独立数据库服务:需要下载安装、配置、开端口、License 授权、备份策略、监控告警、版本升级。对每周只跑一次的定时任务来说,这个运维成本是纯浪费。

4. 双写同步问题

所有关系数据都来自 CRM 自有的 MySQL 表。如果用 Neo4j,需要维护一份与 MySQL 同步的图副本——引入双写或 ETL 同步,增加一致性问题。数据变更时还得担心两边数据是否对齐。

用 JGraphT:每次从源头重建,天然一致。没有同步就没有不一致。

第三轮:JGraphT 实现

核心代码分两部分:图构建和图查询。

图构建

每周定时任务前从数据库重建图,逻辑封装在 KnowledgeGraphBuilder 里:

public Graph<String, RelationshipEdge> build() {
Graph<String, RelationshipEdge> graph =
new DefaultDirectedGraph<>(RelationshipEdge.class);

// 添加节点:客户、联系人
for (Contact contact : contactDao.findAll()) {
graph.addVertex(contact.getId());
}

// 添加边:同事关系
for (ColleagueRelation rel : colleagueDao.findAll()) {
graph.addEdge(rel.getPersonA(), rel.getPersonB(),
new RelationshipEdge(RelationType.COLLEAGUE));
}

// 添加边:同司关系
for (CompanyRelation rel : companyDao.findAll()) {
graph.addEdge(rel.getPersonA(), rel.getPersonB(),
new RelationshipEdge(RelationType.SAME_COMPANY));
}

// 添加边:拜访记录
for (VisitRecord record : visitDao.findAll()) {
graph.addEdge(record.getVisitorId(), record.getClientId(),
new RelationshipEdge(RelationType.VISITED));
}

return graph;
}

规则五:带条件的受限 BFS

查询逻辑在 KnowledgeGraphService 中,核心方法是带条件的受限 BFS:

/**
* 规则五:关系链挖掘
* 从关键人出发,沿指定边类型做受限 BFS,命中即停
*/

public List<String> findRelationChain(String keyPerson, String targetClientId) {
int MAX_DEPTH = 2; // 一跳优先,二跳兜底

Queue<String> queue = new LinkedList<>();
Map<String, Integer> depthMap = new HashMap<>();
queue.offer(keyPerson);
depthMap.put(keyPerson, 0);

while (!queue.isEmpty()) {
String current = queue.poll();
int depth = depthMap.get(current);

// 命中即停:当前节点拜访过目标客户
if (!current.equals(keyPerson) && hasVisited(current, targetClientId)) {
return buildChain(keyPerson, current);
}

if (depth >= MAX_DEPTH) continue;

// 沿指定边类型扩展(只走同事、同司边)
for (RelationshipEdge edge : graph.edgesOf(current)) {
if (edge.getType() == RelationType.COLLEAGUE
|| edge.getType() == RelationType.SAME_COMPANY) {
String neighbor = Graphs.getOppositeVertex(graph, edge, current);
if (!depthMap.containsKey(neighbor)) {
depthMap.put(neighbor, depth + 1);
queue.offer(neighbor);
}
}
}
}
return Collections.emptyList();
}

核心就这几行:BFS 按跳数递增扩展、边类型过滤、命中即终止。MAX_DEPTH 改一行就能调层数,不用重写查询、不用改索引。

也可以用 JGraphT 自带的 BreadthFirstIterator 更简洁,项目里因为要控制边类型和命中终止条件,手动写 BFS 更灵活。对上层调度器来说,图计算模块就是一个普通的 Java 服务,感知不到 JGraphT 的存在。

什么时候该换?

选 JGraphT 不是"永远不用 Neo4j",而是"当前不该用"。项目文档画了明确的升级边界,满足以下条件时再评估:

  • 客户量超过 10 万,内存建图扛不住
  • 需要全图多跳分析(如全链路引荐推荐),不再是局部遍历
  • 关系数据不再全部来自 CRM 自有表,需要多源图存储
  • 查询频率变高(从每周一次变成实时查询)

图构建逻辑封装在 KnowledgeGraphBuilder 里,查询逻辑在 KnowledgeGraphService 中。将来真要换 Neo4j,只需改这两个类,对上层调度器完全透明,迁移成本可控。

总结

整个选型过程其实就三个问题:

  • SQL 能不能做? 一跳勉强,二跳失控,最短路径和边类型叠加是 SQL 的死穴。查询本质是图遍历,SQL 表达不了遍历语义。
  • 要不要上图数据库? 数据量小、一跳二跳局部遍历、数据源单一——Neo4j 的持久化、索引、集群能力全是过度设计。
  • JGraphT 够不够? 纯 Java 库、零运维、BFS 和边过滤开箱即用、MAX_DEPTH 一行调层数。成本最低,且升级路径清晰。
  • 选型的关键不是选"更强的工具",而是选"对路的工具"。SQL 擅长集合筛选,Neo4j 擅长大规模图存储和多跳查询,JGraphT 擅长内存中的图算法计算。规则五的需求落在最后一个区间,那就用它。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » JGraphT 选型实录:为什么不用 SQL,也不用 Neo4j
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!