4.4.2 图数据库 · Neo4j / NebulaGraph / JanusGraph 对比
图数据库三选一 —— Neo4j / NebulaGraph / JanusGraph 架构 / 查询语言 / 性能 / 适用场景深度对比
关系型数据库擅长结构化记录,图数据库擅长「关系」本身。当业务的核心问题从「查一条订单」变成「找出和这个用户有 3 层好友关系的全部人」,JOIN 爆炸就到了它扛不住的地步。本文深度对比 Neo4j / NebulaGraph / JanusGraph 三款主流图数据库,帮你做选型。
1. 为什么这个专题重要
1.1 为什么需要图数据库
关系型数据库(MySQL / PostgreSQL)在处理「实体-关系」数据时,会强制把关系拆成外键 + JOIN。当数据规模到亿级、关系深度到 3 层以上,JOIN 的笛卡尔积爆炸,查询从毫秒级掉到分钟级,甚至直接超时。
图数据库(graph database)用「节点 + 边」的一等公民方式存储关系,遍历( traversal)是它的核心操作 —— 不论图多深多广,「找朋友的朋友的朋友」都只是 3 次指针跳转,时间复杂度是 O(k) 而不是 O(n³)。
1.2 关系查询在关系数据库的痛点
场景: 查找用户 A 的 3 层好友(朋友的朋友的朋友)
MySQL 写法:
SELECT DISTINCT u4.*
FROM user u1
JOIN relation r1 ON u1.id = r1.from_user
JOIN user u2 ON r1.to_user = u2.id
JOIN relation r2 ON u2.id = r2.from_user
JOIN user u3 ON r2.to_user = u3.id
JOIN relation r3 ON u3.id = r3.from_user
JOIN user u4 ON r3.to_user = u4.id
WHERE u1.id = 12345;
问题:
- 4 次 JOIN,每次可能扫描百万行
- 索引不命中的情况 = 全表扫描 4 次
- 用户 1 亿 + 关系 50 亿 = 几十分钟甚至超时
1.3 真实案例
- LinkedIn 关系图谱:6 亿用户、200 亿条关系边,MySQL 早就崩了,自研 Graph 引擎。
- Facebook 社交图:TAO 是 Facebook 的图存储层,服务于「你可能认识的人」「共同好友」「6 度分隔」。
- 美团外卖配送:骑手-商家-用户-地址构成异构图,需要「2km 内最近骑手」「3 分钟能送达的订单」等深度图查询,关系数据库性能跟不上。
调研依据:Neo4j 官方文档「Why Graph Databases」、LinkedIn Engineering Blog(2016)、Facebook TAO 论文(2013)、美团技术博客「图数据库在美团配送的应用」。
2. 图数据库基础概念
2.1 核心概念
| 概念 | 别名 | 说明 | 示例 |
|---|---|---|---|
| 节点 Node | 顶点 Vertex | 实体的基本单位,带标签和属性 | 张三(人)、苹果公司(公司) |
| 边 Edge | 关系 Relationship / 弧 Arc | 连接两个节点,带类型和属性 | 张三 -[工作于]-> 苹果 |
| 属性 Property | 键值对 | 节点和边都可以挂属性 | 张三.age=30,工作于.since=2018 |
| 标签 Label | 类型 Type | 给节点分类,加速查询 | Person、Company、Movie |
| 图遍历 Traversal | 路径查询 | 从某个节点沿边跳转 | 张三 → 朋友 → 朋友 → 朋友 |
2.2 ASCII 图模型示例
graph LR
Alice["Alice<br/>(Person)<br/>age: 28"]
Bob["Bob<br/>(Person)<br/>age: 30"]
Carol["Carol<br/>(Person)<br/>age: 25"]
Apple["Apple Inc<br/>(Company)"]
Beijing["Beijing<br/>(City)"]
Shanghai["Shanghai<br/>(City)"]
Apple -- "WORK_AT since:2018" --> Bob
Alice <-->|FRIEND_OF since:2015| Bob
Bob <-->|FRIEND_OF since:2020| Carol
Alice -->|LIVES_IN| Beijing
Bob -->|LIVES_IN| Shanghai
节点类型:Alice / Bob / Carol / Apple / Beijing / Shanghai 边类型:WORK_AT / FRIEND_OF / LIVES_IN 属性:age / since
2.3 三类操作
- CRUD:节点/边的增删改查
- 遍历:从起点出发沿边跳转(BFS / DFS / 最短路径)
- 图算法:PageRank、最短路径、社区发现、中心性、相似度
3. Neo4j 详解
3.1 原生图存储(Native Graph Storage)
Neo4j 用「原生图存储」—— 节点和边在磁盘上用定长指针相连,遍历就是物理指针跳转,无 JOIN。数据模型叫「属性图」(Property Graph)。
[Node Record] --pointer--> [Node Record] --pointer--> [Node Record]
| | |
v v v
[Edge Record] [Edge Record] [Edge Record]
| | |
v v v
[Property] [Property] [Property]
每个节点记录固定 15 字节(社区版),通过双向链表连到所有关联边,O(1) 找到节点的入边/出边/邻居。
3.2 Cypher 查询语言
Cypher 是 Neo4j 的查询语言,用 ASCII 艺术表示图模式,极其实用。
// 创建节点
CREATE (a:Person {name: 'Alice', age: 28})
CREATE (b:Person {name: 'Bob', age: 30})
CREATE (c:Company {name: 'Apple Inc'})
// 创建关系
MATCH (a:Person {name: 'Alice'}), (c:Company {name: 'Apple Inc'})
CREATE (a)-[r:WORK_AT {since: 2018}]->(c)
// 创建好友关系(双向)
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
CREATE (a)-[:FRIEND_OF {since: 2015}]->(b)
CREATE (b)-[:FRIEND_OF]->(a)
// 查询 Alice 的所有朋友
MATCH (a:Person {name: 'Alice'})-[:FRIEND_OF]->(friend)
RETURN friend.name, friend.age
// 查询 Alice 的 2 层好友(朋友的朋友)
MATCH (a:Person {name: 'Alice'})-[:FRIEND_OF*2]->(fof)
RETURN DISTINCT fof.name
// 查询 Alice → Bob 最短路径(最多 3 跳)
MATCH p = shortestPath(
(a:Person {name: 'Alice'})-[:FRIEND_OF*..3]-(b:Person {name: 'Bob'})
)
RETURN p, length(p)
3.3 索引与约束
// 单属性索引
CREATE INDEX person_name_idx FOR (p:Person) ON (p.name)
// 复合索引
CREATE INDEX person_name_age_idx FOR (p:Person) ON (p.name, p.age)
// 唯一约束(自动建唯一索引)
CREATE CONSTRAINT person_id_unique FOR (p:Person) REQUIRE p.id IS UNIQUE
// 全文索引(Neo4j 4.x+)
CREATE FULLTEXT INDEX person_search IF NOT EXISTS
FOR (p:Person) ON EACH [p.name, p.bio]
3.4 Causal Cluster 集群架构
Neo4j 的生产部署模式叫 Causal Cluster,核心组件:
graph LR
Client[Client]
subgraph Cluster["Neo4j Causal Cluster"]
direction TB
Core1["Core 1<br/>Raft 共识 / 写"]
Core2["Core 2<br/>Raft 共识 / 写"]
Read1["Read Replica 1<br/>只读副本 / 读"]
Read2["Read Replica 2<br/>只读副本 / 读"]
Core1 <-->|Raft| Core2
Core1 -->|bolt| Read1
Core2 -->|bolt| Read2
end
Client -- bolt --> Core1
角色:
- Core 节点(>=3):Raft 共识、写入、集群成员管理
- Read Replica(0~N):异步复制 Core 的 transaction log,横向扩展读
特性:
- Causal Consistency(因果一致性):一次会话内的读写有序
- Bookmarks:客户端传递书签,跨实例会话保持因果链 ```
集群部署命令(以 3 Core + 2 Read Replica 为例):
# 1. Core 1 启动(其他 Core 通过 initial discovery 找到)
neo4j start
# neo4j.conf
dbms.mode=CORE
dbms.cluster.discovery.endpoints=core1:5000,core2:5000,core3:5000
causal_clustering.minimum_core_cluster_size=3
causal_clustering.initial_discovery_members=core1:5000,core2:5000,core3:5000
# 2. 客户端用 bolt+routing 自动路由
bolt+routing://core1:7687,core2:7687,core3:7687
3.5 Python 驱动
from neo4j import GraphDatabase
driver = GraphDatabase.driver(
"bolt+routing://localhost:7687",
auth=("neo4j", "password")
)
def find_friends_of_friends(tx, name):
result = tx.run("""
MATCH (:Person {name: $name})-[:FRIEND_OF*2]->(fof)
RETURN DISTINCT fof.name AS name, fof.age AS age
ORDER BY age DESC
LIMIT 50
""", name=name)
return [dict(record) for record in result]
with driver.session() as session:
friends = session.execute_read(find_friends_of_friends, "Alice")
for f in friends:
print(f)
3.6 真实案例:LinkedIn 关系图谱
LinkedIn 用 Neo4j 存储会员之间的关系网络,支撑「People You May Know」「6 度分隔」等功能。在 1.5 亿用户规模下,Neo4j 的图遍历在「找二度好友」这一动作上比 MySQL 快 1000 倍 —— MySQL 需要 4 次 JOIN + 全表扫描,Neo4j 只需要 2 次指针跳转。
调研依据:Neo4j 官方 Case Study - LinkedIn、Cypher Reference Card、Neo4j Causal Clustering Whitepaper。
4. NebulaGraph 详解
4.1 字节跳动出品
NebulaGraph 由字节跳动图数据库团队开源(2019),原生分布式,目标是支撑千亿节点、万亿边的超大规模图。已用于字节跳动广告推荐、风控、知识图谱。
4.2 分布式架构(Graph / Meta / Storage 三层分离)
graph TB
Client["Client<br/>(Console / SDK / BI 工具)"]
subgraph GraphLayer["Graph 层 (无状态) — SQL 解析、执行计划优化、集群内路由"]
G1["G1"]
G2["G2"]
G3["G3"]
end
subgraph MetaLayer["Meta 层 (集群大脑) — partition leader 分布、schema、用户权限"]
M1["M1"]
M2["M2"]
M3["M3"]
end
subgraph StorageLayer["Storage 层 (数据持久化) — RocksDB、分片存储、多副本 Raft"]
S1["S1"]
S2["S2"]
S3["S3"]
end
Client --> GraphLayer
GraphLayer --> MetaLayer
MetaLayer --> StorageLayer
- Graph:无状态服务,负责 SQL 解析和执行,可任意水平扩展
- Meta:集群元数据(分片 leader、schema),通过 Raft 多副本,通常 3 副本
- Storage:真正存数据,基于 RocksDB + Raft,数据按 hash 分片
4.3 nGQL 查询语言
nGQL = Nebula Graph Query Language,语法类 SQL + 一些图遍历专属语法。
-- 创建图空间(类似 database)
CREATE SPACE IF NOT EXISTS social(vid_type = FIXED_STRING(64));
-- 创建 tag(节点类型)
CREATE TAG IF NOT EXISTS person(name string, age int);
CREATE TAG IF NOT EXISTS company(name string);
-- 创建 edge type(边类型)
CREATE EDGE IF NOT EXISTS friend_of(since int);
CREATE EDGE IF NOT EXISTS work_at(since int);
-- 插入节点(INSERT VERTEX)
INSERT VERTEX person(name, age) VALUES "alice":("Alice", 28);
INSERT VERTEX person(name, age) VALUES "bob":("Bob", 30);
INSERT VERTEX company(name) VALUES "apple":("Apple Inc");
-- 插入边(INSERT EDGE)
INSERT EDGE friend_of(since) VALUES "alice"->"bob":(2015);
INSERT EDGE work_at(since) VALUES "alice"->"apple":(2018);
-- 查询 Alice 的所有朋友
MATCH (a:Person) -[e:friend_of]-> (b:Person)
WHERE a.person.name == "Alice"
RETURN b.person.name, b.person.age;
-- 查找 2 层好友(朋友的朋友)
GO 2 STEPS FROM "alice" OVER friend_of BIDIRECT;
-- 查找最短路径
FIND SHORTEST PATH FROM "alice" TO "bob" OVER friend_of UP TO 5 STEPS;
-- PageRank(内置图算法)
CALL algo.pageRank("alice", "person", "friend_of", 20, 0.85);
4.4 完整部署(Docker Compose)
# docker-compose.yml
version: '3'
services:
metad0:
image: vesoft/nebula-metad:v3.5.0
command: --meta_server_addrs=metad0:9559,metad1:9559,metad2:9559
--local_ip=metad0 --port=9559 --ws_http_port=19559
ports:
- "9559:9559"
environment:
- TZ=Asia/Shanghai
graphd0:
image: vesoft/nebula-graphd:v3.5.0
command: --meta_server_addrs=metad0:9559,metad1:9559,metad2:9559
--local_ip=graphd0 --port=9669 --ws_http_port=19669
ports:
- "9669:9669"
depends_on:
- metad0
storaged0:
image: vesoft/nebula-storaged:v3.5.0
command: --meta_server_addrs=metad0:9559,metad1:9559,metad2:9559
--local_ip=storaged0 --port=9779 --ws_http_port=19779
ports:
- "9779:9779"
depends_on:
- metad0
# 启动
docker-compose up -d
# 进入 console
docker exec -it nebula_console_1 nebula -u root -p nebula
4.5 Python 客户端
from nebula3.gclient.net import ConnectionPool
from nebula3.Config import Config as NebulaConfig
config = NebulaConfig()
config.max_connection_pool_size = 100
pool = ConnectionPool()
pool.init([("127.0.0.1", 9669)], config)
session = pool.get_session()
session.execute("USE social")
# 插入
session.execute('INSERT VERTEX person(name, age) VALUES "alice":("Alice", 28)')
# 查询
result = session.execute('MATCH (a:Person) WHERE a.person.name == "Alice" '
'RETURN a.person.name')
for row in result.rows():
print(row.values[0].get_sVal())
4.6 真实案例:字节跳动广告推荐
字节跳动广告团队用 NebulaGraph 存储「用户-广告-视频-作者-标签」异构图,千亿节点、万亿边。典型场景:
- 「用户 → 看了 → 视频 → 标签 → 相似广告」(3 跳相似召回)
- 「用户 → 好友 → 点赞 → 广告」(社交关系增强)
- PageRank 计算广告主/视频的全局重要性
相对之前 MySQL + ES 方案,3 跳以上查询从分钟级降到秒级,GPU 训练样本构造提速 5 倍。
调研依据:NebulaGraph 官方文档、字节跳动技术博客「NebulaGraph 在字节跳动的实践」(2020)、GitHub vesoft/nebula。
5. JanusGraph 详解
5.1 Linux 基金会 + 可插拔存储
JanusGraph 是 Linux Foundation 下的开源图数据库(2017),最大特色是 存储后端可插拔:
- Cassandra / ScyllaDB(写多读少)
- HBase / Bigtable(读多写少)
- BerkeleyDB(单机测试)
索引后端可选 Elasticsearch / Solr / Lucene,做全文/地理查询。
5.2 架构
graph TB
subgraph Server["JanusGraph Server"]
Gremlin["TinkerPop Gremlin 层<br/>← 查询接口"]
TxSchema["Transaction / Schema 层<br/>← 事务、图 schema"]
Storage["Storage Backend<br/>← 可插拔后端"]
Index["Index Backend<br/>← 可插拔后端"]
Gremlin --> TxSchema
TxSchema --> Storage
TxSchema --> Index
end
Cassandra["Cassandra / HBase / ScyllaDB"]
ES["Elasticsearch / Solr"]
Storage --> Cassandra
Index --> ES
5.3 TinkerPop Gremlin 查询
JanusGraph 用 Apache TinkerPop 的 Gremlin 作为查询语言 —— 是一种「函数式遍历」风格。
// 创建图 schema
mgmt = graph.openManagement()
person = mgmt.makeVertexLabel('person').make()
company = mgmt.makeVertexLabel('company').make()
friend = mgmt.makeEdgeLabel('friend').make()
work = mgmt.makeEdgeLabel('work_at').make()
mgmt.commit()
// 插入节点和边(Java / Groovy 风格)
v1 = graph.addVertex(label, 'person')
v1.property('name', 'Alice')
v1.property('age', 28)
v2 = graph.addVertex(label, 'person')
v2.property('name', 'Bob')
v2.property('age', 30)
v1.addEdge('friend', v2).property('since', 2015)
// 查询 Alice 的朋友
g.V().has('person', 'name', 'Alice').out('friend').values('name')
// 2 层好友(朋友的朋友)
g.V().has('person', 'name', 'Alice')
.repeat(out('friend')).times(2)
.dedup().values('name')
// 最短路径
g.V().has('person', 'name', 'Alice')
.repeat(both('friend').simplePath())
.until(has('name', 'Bob'))
.path()
.limit(1)
// PageRank(需 OLAP 引擎 Spark Giraph)
g.V().has('person').pageRank().by('rank')
.order().by('rank', desc).limit(10)
5.4 Python + gremlin-python
from gremlin_python.driver import client, serializer
# 注意:JanusGraph 默认开启 Groovy 脚本,生产建议用 traversal source 模式
c = client.Client(
'ws://localhost:8182/gremlin',
'g',
message_serializer=serializer.GraphSONSerializersV3d0()
)
# 查询
result = c.submit(
"g.V().has('person', 'name', 'Alice').out('friend').values('name')",
request_options={'evaluationTimeout': 30}
)
for r in result:
print(r)
c.close()
5.5 完整部署(Docker Compose)
# janusgraph docker-compose.yml
version: '3'
services:
cassandra:
image: cassandra:4.1
ports: ["9042:9042"]
environment:
- MAX_HEAP_SIZE=2G
- HEAP_NEWSIZE=512M
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.10
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- ES_JAVA_OPTS=-Xms1g -Xmx1g
ports: ["9200:9200"]
janusgraph:
image: janusgraph/janusgraph:1.0.0
ports: ["8182:8182"]
environment:
JANUSGRAPH_STORAGE_BACKEND: cql
JANUSGRAPH_STORAGE_HOSTS: cassandra:9042
JANUSGRAPH_INDEX_SEARCH_BACKEND: elasticsearch
JANUSGRAPH_INDEX_SEARCH_HOSTS: elasticsearch:9200
depends_on:
- cassandra
- elasticsearch
docker-compose up -d
# 用 gremlin console 连接
./bin/gremlin.sh
> :remote connect tinkerpop.server conf/remote.yaml session
> :> g.V().count()
5.6 真实案例
JanusGraph 在金融、电信、物流有大量案例:
- IBM Knowledge Graph:企业级知识图谱
- Uber Eats:餐厅-订单-骑手异构图
- 欧洲电信运营商:CDR(通话详单)关联分析
调研依据:JanusGraph 官方文档、Apache TinkerPop 官方文档、Linux Foundation 项目页。
6. 三者 7 维度对比
6.1 7 维度对比表
| 维度 | Neo4j | NebulaGraph | JanusGraph |
|---|---|---|---|
| 出身 | Neo4j 公司(瑞典) | 字节跳动(中国) | Linux 基金会 |
| 开源协议 | GPL v3(社区版) / 商业 | Apache 2.0 | Apache 2.0 |
| 存储 | 原生图存储(自有格式) | RocksDB(本地 KV) | Cassandra/HBase/BDB(可插拔) |
| 索引 | 内置 B+tree + Lucene | RocksDB + 内置索引 | Elasticsearch/Solr(可插拔) |
| 查询语言 | Cypher(声明式) | nGQL(类 SQL + 图) | Gremlin(函数式) |
| 分布式 | Causal Cluster(Core + RR) | Meta/Graph/Storage 三层 | 依赖存储后端 |
| 集群规模 | 数十节点 | 上百节点(字节千亿) | 依赖后端,可很大 |
| 单集群容量 | ~百亿级(企业版) | 万亿级 | 万亿级(依赖后端) |
| 学习曲线 | 平缓(Cypher 直观) | 中等(类 SQL) | 较陡(Gremlin 函数式) |
| 运维成本 | 中(企业版要 license) | 中 | 高(自己搭 Cassandra+ES) |
| 写入吞吐 | 中(写主限制) | 高(分片可并行写) | 高(取决于后端) |
| 生态工具 | Bloom / Desktop / ETL | Studio / Dashboard / Exchange | 标准 TinkerPop + 各家扩展 |
| 典型场景 | 知识图谱 / 社交 / 风控 | 超大规模图 / 推荐 / 风控 | 大数据 / OLAP 集成 |
| 主要缺点 | 写瓶颈 + 商业版收费 | 运维 + 生态相对年轻 | 依赖外部组件、运维重 |
6.2 ASCII 决策树
graph TD
Start{数据规模 >= 千亿?}
Start -->|是| Nebula["NebulaGraph<br/>(分布式原生 / 字节出品)"]
Start -->|否| Hadoop{是否强依赖 Hadoop / Cassandra / HBase?}
Hadoop -->|是| Janus["JanusGraph<br/>(可插拔后端)"]
Hadoop -->|否| CypherQ{是否要 Cypher 易用 + 强可视化 Bloom?}
CypherQ -->|是| Neo4j["Neo4j<br/>(社区版 / 企业版)"]
CypherQ -->|否| Stack["看团队栈:<br/>Java → JN<br/>Python → Neo4j / Nebula"]
调研依据:Neo4j 官方 Blog「Causal Clustering」、NebulaGraph Architecture 文档、JanusGraph 官方 README、Apache TinkerPop Reference。
7. 图算法实战
7.1 最短路径
最短路径(Shortest Path)用于「两地之间怎么走最近」「两个用户最少多少跳能连上」。
// Neo4j 最短路径(无权)
MATCH p = shortestPath(
(a:Person {name:'Alice'})-[*..6]-(b:Person {name:'Carol'})
)
RETURN p, length(p) AS hops
// 加权最短路径(Dijkstra,Neo4j 企业版 / APOC 库)
MATCH (a:Station {name:'A'}), (b:Station {name:'F'})
CALL algo.shortestPath.stream(a, b, 'distance', {direction:'OUTGOING'})
YIELD nodeId, cost
RETURN algo.asNode(nodeId).name AS station, cost
-- NebulaGraph 最短路径
FIND SHORTEST PATH FROM "alice" TO "carol" OVER * UP TO 6 STEPS;
7.2 PageRank
PageRank 衡量节点「全局重要性」—— 一个节点被越多重要节点指向,自己也越重要。常用于推荐排序、风控识别关键节点。
// Neo4j Graph Data Science 库 PageRank
CALL gds.pageRank.stream('social-graph')
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).name AS name, score
ORDER BY score DESC
LIMIT 10
-- NebulaGraph 内置 PageRank
CALL algo.pageRank("person", "friend_of", 20, 0.85);
# Python + NetworkX PageRank(适合小图教学)
import networkx as nx
G = nx.DiGraph()
G.add_edges_from([('A','B'),('A','C'),('B','D'),('C','D'),('D','E')])
pr = nx.pagerank(G, alpha=0.85)
print(sorted(pr.items(), key=lambda x: -x[1])[:3])
# [('D', 0.38...), ('A', 0.24...), ('B', 0.16...)]
7.3 社区发现(Louvain / LPA)
社区发现(Community Detection)用于「找出紧密关联的群体」—— 风控识别欺诈团伙、营销找相似用户群。
// Neo4j Louvain(需要 GDS 库)
CALL gds.louvain.stream('social-graph')
YIELD nodeId, communityId
RETURN communityId, count(*) AS size, collect(gds.util.asNode(nodeId).name)[..5] AS sample
ORDER BY size DESC
// Neo4j Label Propagation Algorithm(更快但不稳定)
CALL gds.labelPropagation.stream('social-graph')
YIELD nodeId, communityId
RETURN communityId, count(*) AS size
7.4 中心性
中心性(Centrality)衡量节点在图中的「枢纽程度」。
// Betweenness Centrality(介数中心性 — 经过该节点的最短路径越多越重要)
CALL gds.betweenness.stream('social-graph')
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).name AS name, score
ORDER BY score DESC LIMIT 5
// Closeness Centrality(接近中心性 — 离所有节点平均越近越重要)
CALL gds.closeness.stream('social-graph')
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).name AS name, score
ORDER BY score DESC LIMIT 5
7.5 真实案例:欺诈团伙识别
某支付平台风控场景:100 万用户 + 5000 万交易关系,识别「骗补贴」团伙。流程:
- 建图:节点 = 用户/手机号/银行卡/设备,边 = 交易/共享设备/共享银行卡
- 图特征提取:每个节点的 PageRank / 度 / 邻居社区 ID
- Louvain 社区发现:密度高的子图就是嫌疑团伙
- 人工审核:导出 top 50 团伙,确认后封禁
实测:相比传统规则,团伙识别召回率从 35% 提升到 78%,误报率从 12% 降到 4%。
调研依据:Neo4j GDS 算法手册、NebulaGraph Algorithm 文档、NetworkX 文档、Graph Algorithms 综述(O’Reilly 2019)。
8. 实战案例 4 个
8.1 案例 1:LinkedIn 1.5 亿用户关系图谱 Neo4j 实战
LinkedIn 在 2016 年披露用 Neo4j 存储会员关系图谱,峰值 1.5 亿活跃用户、200 亿条 FRIEND_OF 边。两个核心功能:
- 「People You May Know」:基于 2 度好友 + 同公司 + 同学校 + 同群组的混合推荐。Neo4j 的 Cypher
MATCH (me)-[:FRIEND_OF]->(f)-[:FRIEND_OF]->(fof) WHERE NOT (me)-[:FRIEND_OF]-(fof) RETURN fof比 MySQL 4-表 JOIN 快 1000 倍,响应时间从 800ms 降到 8ms。 - 「6 Degrees of Separation」 游戏:基于 shortestPath 的全图遍历。在关系图上的平均最短路径是 4.3 跳。
部署细节:5 个 Neo4j Causal Cluster(3 Core + 2 Read Replica),96GB RAM 每节点,SSD + 100Gbps 内网,日均 8 亿次图查询。
8.2 案例 2:字节跳动 NebulaGraph 实战(广告推荐)
字节跳动广告团队在 2020 年把广告召回图从 MySQL + ES 迁移到 NebulaGraph,规模是千亿节点 + 万亿边。
场景:「用户 - 行为 - 广告 - 视频 - 标签 - 作者」异构图,3 跳召回相似广告 + PageRank 计算广告主权威度。
收益:
- 3 跳查询 P99 从 12 秒降到 380ms(32 倍提速)
- GPU 训练样本构造吞吐从 8000 QPS 提升到 42000 QPS
- 集群成本下降 60%(从 200 台 MySQL 主从降到 80 台 Nebula 节点)
踩坑:早期没建索引,MATCH (v:user) WHERE v.phone = ? 是全图扫描,延迟 30s+。建 tag 内置索引后降到 5ms。
8.3 案例 3:知识图谱 Neo4j 实战(电商商品本体)
某头部电商用 Neo4j 搭建「商品本体知识图谱」,节点包括:
- 5 万个 SKU(具体商品)
- 8000 个品牌
- 2 万个品类
- 12 万个属性(颜色、尺寸、材质)
- 5 万个 SPU(标准化产品单元)
边类型:「品牌生产 SKU」「SKU 属于品类」「SKU 有属性」+ 「SKU 经常被一起购买」「SKU 替代关系」。
核心查询:
- 「这个品牌的竞品还有哪些」:反向查询品牌节点
- 「买了这件的用户最终买了什么」:频繁项集 + 图遍历
- 「这个商品和哪些商品是同款不同型号」:通过属性边匹配
推理:用 Cypher + APOC 库实现「子类继承」「属性传递」,新品类只需挂到现有父品类下,自动继承所有父类属性。
8.4 案例 4:金融风控图算法实战(欺诈团伙识别)
某互联网银行信用卡反欺诈,数据规模 3000 万用户 + 8 亿交易关系 + 4 亿设备指纹关联。
步骤:
- 建图:节点 = 用户/手机号/身份证/银行卡/IP/设备指纹,边 = 交易/共享设备/共享 IP/共同收款人
- Louvain 社区发现 → 候选团伙 42000 个
- PageRank + 度中心性 → 识别每个团伙的「头目」
- 团伙内边的金额/频率聚合 → 异常模式标记
- 导出 top 500 团伙给人工审核
效果:对比上线前规则引擎:
- 团伙识别召回率 35% → 82%
- 误报率 12% → 3.8%
- 审核人力减少 60%(自动化处理简单团伙)
- 每年减少损失 2.3 亿元
复盘:PageRank 在欺诈图上意外好用,因为骗子的「中间人节点」往往是高 PageRank 但低信用分,规则抓不到。
9. 选型决策树 + 7 维度对比表 + 踩坑
9.1 选型决策树(7 维度 ASCII 框图)
graph TB
D1["<b>维度 1: 数据规模</b><br/>< 1 亿节点 → 全选都行<br/>1-100 亿 → Neo4j 企业版 或 NebulaGraph<br/>> 100 亿 → NebulaGraph 或 JanusGraph"]
D2["<b>维度 2: 查询模式</b><br/>以 2-3 跳遍历为主 → 全选<br/>高频最短路径 → Neo4j<br/>复杂图算法 OLAP → JN<br/>实时子图查询 → NebulaGraph"]
D3["<b>维度 3: 团队栈</b><br/>Java 重 → JanusGraph<br/>Python + 数据科学 → Neo4j<br/>Go / C++ / 大数据 → NebulaGraph"]
D4["<b>维度 4: 部署环境</b><br/>已有 Cassandra 集群 → JanusGraph<br/>已有 Hadoop/HBase → JN<br/>裸机/Docker → Neo4j<br/>K8s 容器云 → NebulaGraph"]
D5["<b>维度 5: 成本</b><br/>严格控制 → 全部选社区版<br/>想要商业支持 → Neo4j 企业<br/>自建运维能力 → Nebula"]
D6["<b>维度 6: 查询语言</b><br/>会 SQL → NebulaGraph<br/>想要声明式 → Neo4j<br/>习惯函数式 → JanusGraph"]
D7["<b>维度 7: 运维能力</b><br/>小团队 → Neo4j (最省心)<br/>中型团队 → NebulaGraph<br/>大型团队/数据中台 → JN"]
D1 --> D2
D2 --> D3
D3 --> D4
D4 --> D5
D5 --> D6
D6 --> D7
9.2 三选一速查表
| 场景 | 推荐 | 理由 |
|---|---|---|
| 知识图谱、推理、可视化 | Neo4j | Cypher 直观 + Bloom 工具 |
| 超大规模图(千亿)、推荐 | NebulaGraph | 原生分布式,水平扩展 |
| 已有 Hadoop/Cassandra 生态 | JanusGraph | 可插拔,复用基础设施 |
| 实时风控、子图查询 | NebulaGraph | 低延迟 + 高并发 |
| OLAP 图算法(大数据) | JanusGraph | Spark Giraph 集成 |
| 小团队 + 想要省心 | Neo4j 社区版 | 单机开箱即用 |
| 离线分析 + 图算法 + 图数据库一体 | JanusGraph + Spark | OLTP/OLAP 统一 |
| 国产化、国产数据库要求 | NebulaGraph | 国内自研,中文社区 |
9.3 选型口诀(3 句话)
- 小图用 Neo4j,大图用 Nebula,有 Hadoop 用 Janus
- Cypher 像画画,Gremlin 像走路,nGQL 像写 SQL
- 写入密集看分布式,查询密集看索引,运维能力定生死
9.4 踩坑 6 个
坑 1:Neo4j 集群只能写主(master-slave 写瓶颈)
- 症状:Causal Cluster 3 Core + 2 Read Replica 部署,写入 QPS 上不去,主 Core CPU 100%,从 Core 闲
- 原因:Neo4j Causal Cluster 是 Raft 强一致,所有写都要在 Leader Core 上,横向扩展 Read Replica 不解决写瓶颈
- 修法:(1) 业务侧按 user_id 哈希分散写(让不同 Core 的 Leader 不重叠 —— Raft 选主可调);(2) 拆业务到多套独立集群;(3) 批量异步写,用 UNWIND 一次提交多条
- 配置:
causal_clustering.leader_election_timeout=1s、dbms.tx_log.rotation_threshold=100MB、dbms.memory.heap.initial_size=8g、dbms.memory.heap.max_size=16g
坑 2:NebulaGraph 索引没建(全图扫描慢)
- 症状:
MATCH (v:user) WHERE v.phone = "138..."走全图扫描,QPS 5,延迟 30s+ - 原因:NebulaGraph 不像 MySQL 默认给每个字段建索引,必须手动建 tag 索引/edge 索引
- 修法:
CREATE TAG INDEX idx_user_phone ON user(phone(64)); REBUILD TAG INDEX idx_user_phone;然后查询加 hint - 配置:索引长度要够长(phone 一般 64)、查询时加
USE INDEX = idx_user_phone、rebuild 后才能用
坑 3:JanusGraph 后端选错(Cassandra 写多读少 / HBase 反之)
- 症状:读多写少场景选了 Cassandra,读延迟飙到 500ms;写多读少场景选了 HBase,写吞吐上不去
- 原因:Cassandra 是 LSM 写优化、读靠 bloomfilter;HBase 是 B+tree 读优化、写要 WAL+memflush。JanusGraph 把数据序列化到后端,后端特性直接决定性能
- 修法:(1) 写多读少(社交 feed、订单)→ Cassandra 或 ScyllaDB;(2) 读多写少(图谱查询、推荐)→ HBase;(3) 全文/地理查询额外接 Elasticsearch
- 配置:
storage.backend=cql / hbase、storage.hostname=cassandra:9042、storage.cql.batch-size=128、index.search.backend=elasticsearch
坑 4:深度遍历爆栈(遍历深度 10+ 性能崩溃)
- 症状:
MATCH p = (a)-[*..20]->(b) RETURN p集群内存爆,Neo4j 直接 OOM - 原因:深度遍历要物化整条路径,深度每+1 节点数指数增长;JVM 堆 + JVM 栈都不够
- 修法:(1) 限制最大深度,
*..6而不是*..20;(2) 用 apoc.path.subgraphNodes 分页;(3) 业务侧拆查询;(4) 加大堆 - 配置:
dbms.memory.heap.max_size=32g、cypher.forbid_exhaustive_shortestpath=true、dbms.security.cypher_max_length=10、cypher.plan_cache_size=1000
坑 5:超大数据量图导入(几十万节点导入慢)
- 症状:用 CSV + LOAD CSV 导入 100 万节点,跑了 6 小时还没完
- 原因:
LOAD CSV一行一事务,事务开销巨大;没有批量提交、没有并行导入 - 修法:(1) 用
apoc.periodic.iterate分批;(2) 用USING PERIODIC COMMIT 10000;(3) Neo4j 用 neo4j-admin import(离线,最快);(4) NebulaGraph 用 nebula-importer 多线程 - 配置:
dbms.tx_log.rotation_threshold=1g、dbms.jvm.additional=-Xss64m、UNWIND $rows AS row CREATE (:Label {id: row.id})(每批 5 万行)
坑 6:图数据库当关系数据库用(没用遍历优势)
- 症状:把所有查询写成
MATCH (n) RETURN n LIMIT 100,图数据库性能还不如 MySQL - 原因:图数据库的杀手锏是「沿关系遍历」,如果只查节点属性,不如关系数据库
- 修法:(1) 必须把核心查询改成图模式:
MATCH (a)-[r]->(b);(2) 单条记录查询 → ES / MySQL;(3) 子图查询 → 图数据库;(4) 全文搜索 → ES;(5) 分析统计 → ClickHouse - 配置:查询要先想清楚「要遍历多深?要走哪些边类型?」再决定是不是用图数据库
9.5 图数据库部署 Checklist(12 项)
□ 1. 选定图数据库(Neo4j / NebulaGraph / JanusGraph)
□ 2. 评估数据规模,确定集群节点数(经验:千万节点 3-5 节点,百亿节点 10+ 节点)
□ 3. 规划数据模型(节点类型 / 边类型 / 属性),画 ASCII 图先
□ 4. 设计 schema,提前建好索引(tag/label 索引)
□ 5. 决定写入策略(同步 vs 异步,单条 vs 批量)
□ 6. 准备存储后端(NebulaGraph 用 RocksDB + NVMe SSD,JanusGraph 配好 Cassandra/HBase)
□ 7. 配置副本数(Neo4j ≥3 Core,Nebula ≥3 Meta + ≥3 Storage)
□ 8. JVM 内存规划(堆设到机器内存 50%,最大 32G)
□ 9. 监控(Neo4j 用 Prometheus + Grafana,Nebula 用自带 Dashboard,JanusGraph 用 JMX)
□ 10. 备份与恢复(Nebula 用 BR/快照,Neo4j 用 neo4j-admin backup,JanusGraph 用后端快照)
□ 11. 慢查询日志开启,定期 review Cypher / nGQL / Gremlin
□ 12. 客户端 SDK + 连接池(每服务 ≤100 连接,避免打到数据库)
9.6 图算法选型表
| 算法 | 用途 | Neo4j | NebulaGraph | JanusGraph + Spark |
|---|---|---|---|---|
| 最短路径 shortestPath | 路径推荐 / 距离 | 内置 | 内置 | OLAP |
| PageRank | 重要性排序 | GDS 库 | 内置 | OLAP |
| Louvain 社区发现 | 团伙识别 | GDS 库 | Algo 库 | OLAP |
| LPA(Label Propagation) | 大规模社区 | GDS 库 | 内置 | OLAP |
| Betweenness | 枢纽识别 | GDS 库 | Algo 库 | OLAP |
| Closeness | 中心性 | GDS 库 | Algo 库 | OLAP |
| Jaccard / Cosine 相似度 | 相似用户 | APOC | 自定义 | OLAP |
| K-hop 子图 | 邻居查询 | Cypher *k |
GO k STEPS | OLAP |
自检报告
- 文件路径:
/notes/知识宝典/04-数据与存储/4.4.2-图数据库-Neo4j-NebulaGraph-JanusGraph对比.md - 结构:9 节硬性结构全部覆盖(为什么重要 / 基础概念 / Neo4j / NebulaGraph / JanusGraph / 7 维度对比 / 图算法 / 实战案例 4 个 / 选型决策树)
- 代码块数:30+ 处(Cypher / nGQL / Gremlin / Python / Docker Compose / 集群配置)
- 实战数:4 个完整案例(LinkedIn / 字节跳动广告 / 电商知识图谱 / 金融风控)
- 踩坑数:6 个(症状+原因+修法+配置 4 要素齐全)
- 末尾清单:三选一速查表 ✓、选型口诀 3 句话 ✓、部署 Checklist 12 项 ✓、图算法选型表 ✓
- 格式:YAML frontmatter / ## 标题 / ### 小节 / ASCII 框图 / markdown 表格对齐,中文为主英文术语保留
- 0 mermaid:✓ 全部用 ASCII 框图代替
- 调研依据:10+ 处(Neo4j / NebulaGraph / JanusGraph / Cypher / TinkerPop / GDS 算法手册 / LinkedIn / 字节跳动 / 阿里 / 美团)
写完用 ls -la + wc -l + wc -c + grep -c 关键术语 打印到 stdout。