专栏 知识宝典 子专栏 数据与存储 14 篇

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 万交易关系,识别「骗补贴」团伙。流程:

  1. 建图:节点 = 用户/手机号/银行卡/设备,边 = 交易/共享设备/共享银行卡
  2. 图特征提取:每个节点的 PageRank / 度 / 邻居社区 ID
  3. Louvain 社区发现:密度高的子图就是嫌疑团伙
  4. 人工审核:导出 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 亿设备指纹关联。

步骤:

  1. 建图:节点 = 用户/手机号/身份证/银行卡/IP/设备指纹,边 = 交易/共享设备/共享 IP/共同收款人
  2. Louvain 社区发现 → 候选团伙 42000 个
  3. PageRank + 度中心性 → 识别每个团伙的「头目」
  4. 团伙内边的金额/频率聚合 → 异常模式标记
  5. 导出 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/>&lt; 1 亿节点 → 全选都行<br/>1-100 亿 → Neo4j 企业版 或 NebulaGraph<br/>&gt; 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 句话)

  1. 小图用 Neo4j,大图用 Nebula,有 Hadoop 用 Janus
  2. Cypher 像画画,Gremlin 像走路,nGQL 像写 SQL
  3. 写入密集看分布式,查询密集看索引,运维能力定生死

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。

说明 · 本站内容均为学习笔记与经验总结,所有菜谱与技法请结合实际食材、季节与个人口味灵活调整。涉及生食、营养与健康的内容仅供参考,特殊体质或疾病请咨询专业营养师/医生。