
项目里有个 CRM 客户管理助手其中规则五关系链挖掘需要处理节点间的关系遍历。选型时经历了三轮思考SQL 能不能搞定要不要上 Neo4j最终为什么选了 JGraphT记录一下完整的决策过程。项目背景CRM 助手是一个 Java 后端服务Spring Boot有一套规则引擎处理各种业务规则。其中规则五是关系链挖掘给定一个关键人沿指定关系类型同事、同司、拜访查找关联人用于推荐引荐路径。图结构很简单节点客户、联系人边隶属于、同事、同司、拜访记录数据来源全部来自 CRM 自有的 MySQL 表规则五的查询本质是带条件的受限局部遍历从关键人出发沿指定边类型走一跳优先、二跳兜底命中即停还要过滤离职、拜访超时等条件。第一轮SQL 能不能搞定先想的是最直接的方案SQL 查。一跳勉强能做一跳查询找同部门同事中有拜访记录的人用 JOIN 就能做SELECTDISTINCTc2.person_idFROMcolleague_relation c1JOINcolleague_relation c2ONc1.dept_idc2.dept_idJOINvisit_record vONv.visitor_idc2.person_idWHEREc1.person_id?ANDv.client_id?能做但没有任何优势——图查询就是一行neighborListOf。二跳开始失控二跳同事的同事、跨部门关系要上递归 CTEWITHRECURSIVE colleague_chainAS(SELECTperson_id,1ASdepthFROMcolleague_relationWHEREperson_id?UNIONALLSELECTc.person_id,cc.depth1FROMcolleague_relation cJOINcolleague_chain ccONc.dept_idcc.person_idWHEREcc.depth2)SELECTDISTINCTperson_idFROMcolleague_chain;写法复杂、容易出错还要自己控制层数和去重。这是图遍历的活硬塞给 SQL 是拿关系代数模拟图算法。最短路径 命中即终止SQL 的死穴规则五要的是跳数最少的引荐链命中即停。BFS 天然按跳数递增扩展第一次命中就是最短路径遍历器自带终止。SQL 要表达取到最优就停得写存储过程或复杂递归最短路径还要自己做回溯——极难维护且每次扫描都要重写查询。边类型叠加查询爆炸图里有四类边隶属、同事、同司、拜访。遍历时还要过滤只走同事和同司边“排除离职”“拜访超时过滤”。关系每多一种JOIN 条件就成倍叠加谓词一多查询就爆炸。根本问题查询逻辑与存储死绑JGraphT 把图遍历BFS/最短路径作为内存中的原地操作图结构怎么变、规则怎么改不碰存储层。SQL 则是把图逻辑硬编码进查询语句图模型一变就要改 SQL、改索引扫描逻辑和表结构死死绑在一起。还有一个隐患MySQL 的递归 CTE 对层数上限的控制是逐层 UNION ALL 直到不产生新行。写 2 跳还勉强能看一旦未来要扩到 3 跳、4 跳CTE 的层数扩展和路径回溯复杂度会指数级恶化。而 JGraphT 里MAX_DEPTH就是个常量改一行完事。结论一跳勉强可做但没优势一旦涉及二跳、路径回溯、边类型叠加递归 CTE 复杂易错、性能随跳数恶化。不是 SQL 不行是这个查询本质上是图问题SQL 表达不了它的遍历语义。第二轮要不要上 Neo4jSQL 不行那上图数据库考虑了 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里publicGraphString,RelationshipEdgebuild(){GraphString,RelationshipEdgegraphnewDefaultDirectedGraph(RelationshipEdge.class);// 添加节点客户、联系人for(Contactcontact:contactDao.findAll()){graph.addVertex(contact.getId());}// 添加边同事关系for(ColleagueRelationrel:colleagueDao.findAll()){graph.addEdge(rel.getPersonA(),rel.getPersonB(),newRelationshipEdge(RelationType.COLLEAGUE));}// 添加边同司关系for(CompanyRelationrel:companyDao.findAll()){graph.addEdge(rel.getPersonA(),rel.getPersonB(),newRelationshipEdge(RelationType.SAME_COMPANY));}// 添加边拜访记录for(VisitRecordrecord:visitDao.findAll()){graph.addEdge(record.getVisitorId(),record.getClientId(),newRelationshipEdge(RelationType.VISITED));}returngraph;}规则五带条件的受限 BFS查询逻辑在KnowledgeGraphService中核心方法是带条件的受限 BFS/** * 规则五关系链挖掘 * 从关键人出发沿指定边类型做受限 BFS命中即停 */publicListStringfindRelationChain(StringkeyPerson,StringtargetClientId){intMAX_DEPTH2;// 一跳优先二跳兜底QueueStringqueuenewLinkedList();MapString,IntegerdepthMapnewHashMap();queue.offer(keyPerson);depthMap.put(keyPerson,0);while(!queue.isEmpty()){Stringcurrentqueue.poll();intdepthdepthMap.get(current);// 命中即停当前节点拜访过目标客户if(!current.equals(keyPerson)hasVisited(current,targetClientId)){returnbuildChain(keyPerson,current);}if(depthMAX_DEPTH)continue;// 沿指定边类型扩展只走同事、同司边for(RelationshipEdgeedge:graph.edgesOf(current)){if(edge.getType()RelationType.COLLEAGUE||edge.getType()RelationType.SAME_COMPANY){StringneighborGraphs.getOppositeVertex(graph,edge,current);if(!depthMap.containsKey(neighbor)){depthMap.put(neighbor,depth1);queue.offer(neighbor);}}}}returnCollections.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 擅长内存中的图算法计算。规则五的需求落在最后一个区间那就用它。