图数据库Neo4j学习笔记

889 阅读6分钟

一、背景

根 据 全 球 知 名 的 数 据 库 流 行 度 排 行 榜 网 站 DB-Engines数据显示,图数据库的关注度增速远超其他类型的数据库。更值得一提的是,全球最具权威的 IT 研究与顾问咨询公司 Gartner 在 2019 年的数据与分析峰会上预测 2020 年以后,全球图处理及图数据库的应用市场都将以每年 100%的速度迅猛增长。

➀ 图数据库定位

随着万物互联的 5G 时代到来,图数据库在人工智能、计算科学、生物信息、 金融科技、社交网络等越来越多的领域发挥着举足轻重的作用。截至 2019 年 6 月,支付宝及其本地钱包合作伙伴已经服务超 12 亿的全球用户,中文网页数量 达到 2.7 千亿,网页链接数量达到 12 万亿(2018 年),人脑神经突触链接数更 是达到了百亿级别。面对各种海量数据、尤其是对海量非结构化数据的存储, 传统的信息存储和组织模式已经无法满足客户需求,图数据库却能够很清晰地揭示各类复杂模式,尤其针对错综复杂的社交、物流、金融风控、反欺诈、企业关系图谱、安全监测等行业,其优势更为明显,发展潜力巨大

➁ 图数据库的领跑者--Neo4j

➂ 图数据库与其他类型数据库对比

分类模型优势劣势典型系统
关系型数据库表结构数据高度结构化,一致性强,软件成熟度高面向多跳的关联关系查询低效或不支持Mysql,Oracle
键值数据库哈希表查找速度快数据无结构化,通常只被作字符串或者二进制数据Redis
列存储数据库列式数据存储查找速度快:支持分布横向扩展,数据压缩率高数据插入效率偏低,按行的数据操作性能受限HBase
文档型数据库键值对扩展数据结构要求不严格:表结构可变,不需要预先定义表结构查询性能不高,缺乏统一的查询语法MongoDB
图数据库图结构针对关联关系的建模,操作非常高效高度结构化的数据处理能力不及关系型数据库Neo4j, ArangoDB

二、案例及使用

  • 案例

➀ 关系数据库表结构

其中,Person表是个人信息表,Friend表表示朋友关系。

image.png

Ⓐ 查找Bob的朋友

SELECT p1.name

FROM Person p1 JOIN Friend f1

ON f1.FriendID = p1.ID

WHERE p2.name = 'Bob'

Ⓑ Alice的朋友的朋友们

SELECT p1.name AS PERSON, p2.name AS FRIEND_OF_ERIEND

FROM Friend f1 

JOIN PERSON p1

ON f1.FriendID = p1.ID

JOIN Friend f2

ON f1.PersonID = f2.FriendID

JOIN Person p2

ON f2.FriendID = p2.ID

WHERE p1.name = 'Alice' AND f2.FriendID <> p1.ID

➁ 图数据库设计

image.png

Ⓐ 查找Bob的朋友

Match (:Person {name:'Bob'}) -[:Friend_of] -> (f) return f

Ⓑ Alice的朋友的朋友们

Match (:Person {name:'Bob'}) -[:Friend_of] -> () -[:Friend_of] ->(f) return f

➂ 实验结果

在一个社交网络里找到最大深度为5的朋友的朋友,大约100万人,每个人约有50个朋友的社交网络,结果如下,数据来自《图数据库》中Partner和Vukotic的实验结果。

深度关系型数据库的执行时间(s)Neo4j的执行时间(s)返回的记录条数
20.0160.01~2500
330.2670.168~110000
41543.5051.359~600000
5未完成2.132~800000

➃ 对比分析

  • RDBMS 大量 JOIN 操作带来的开销。 之前的查询语句使用了大量的 JOIN 操作来找到需要的结果。而大量的 JOIN 操作在数据量很大时会有巨大的性能损失,因为数据本身是被存放在指定的地方,查询本身只需要用到部分数据,但是 JOIN 操作本身会遍历整个数据库,这样就会导致查询效率低。
  • RDBMS 反向查询带来的开销。 查询单个节点如Person不需要多少开销,但是如果我们要去反向查询一个Friend,使用表结构,开销就会变得非常大。表结构设计得不合理,会对后续的分析、推荐系统产生性能上的影响。如果不支持反向查询,推荐系统的实时性就会大打折扣,进而带来经济损失。
  • RDBMS 解决性能问题手段有限。 除了索引与缓存,传统数据库提升性能的其他手段表现都不尽如人意。对于索引,大量的使用索引会导致写入、更新与删除的性能下降。对于缓存,缓存系统与RDBMS的配合使用需要关注数据一致性问题,另外与缓存系统的交互包含跨域访问,天然增加了请求时间。
  • 图数据库在解决关系问题上优势明显。 从N度查询来看,Neo4j能从百万数据中秒级返回查询结果,能很好的支持OLTP及一定量规模的OLAP查询。相对之下,关系型数据库使用JOIN会引发多次笛卡尔积,导致计算数据的规模陡增,从而导致查询慢,数据量大的情况下甚至有可能让数据库崩溃。

演示案例(Movie)

案例内容

场景:图中包含实体节点:电影、演员、导演、剧本著作人员及影评人员;其中,关系包含导演指导拍摄电影,演员演出电影,影评人员对电影做出影评,影评人员之前相互关注,剧本著作人员著作电影。

任务Cypher语句
创建节点,关系形成图CREATE (a:Person {name:'Brie Larson', born:1989}) RETURN a
CREATE (a:Movie {title:'Captain Marvel', released:2019,tagline:'Everything begins with a (her)o.'}) RETURN a
查询MATCH (a:Person {name:'Tom Hanks'}) RETURN a
修改MERGE (a:Person {name:'Brie Larson'})ON CREATE SET a.born = 1989ON MATCH SET a.stars = COALESCE(a.stars, 0) + 1RETURN a
删除MATCH (a:Person {name:'Brie Larson'}) DETACH DELETE a
查找Tom Hanks的合作演出人员MATCH (a:Person {name:'Tom Hanks'})-[:ACTED_IN]->(m)<-[:ACTED_IN]-(c) RETURN c.name
n度查找MATCH (bacon:Person {name:"Kevin Bacon"})-[*1..4]-(hollywood) RETURN DISTINCT hollywood
最短路径查找MATCH p=shortestPath( (bacon:Person {name:"Kevin Bacon"})-[*]-(a:Person {name:'Al Pacino'})) RETURN p
与Tom Hanks与Tom Cruise共同合作过的演员MATCH (a:Person {name:'Tom Hanks'})-[:ACTED_IN]->(m)<-[:ACTED_IN]-(coActors),(coActors)-[:ACTED_IN]->(m2)<-[:ACTED_IN]-(other:Person {name:'Tom Cruise'})RETURN a, m, coActors, m2, other

三、逻辑架构与核心原理

  • 逻辑架构

Neo4j将图数据存储在不同的存储文件中。每个存储文件包含图的特定部分数据(如:节点,关系,标签和属性都有各自独立的存储)。存储职责的划分,特别是图与属性的分离,促进了高性能的图遍历。

image.png

  • 核心原理

① 图存储结构与模型

Ⓐ 图存储结构

图数据库会使用的底层存储如下图,包含关系存储,面向对象存储,NoSQL, NewSQL,LPG,RDF,原生存储,宽表存储,文档存储,元组存储,KV储存等。

当前主流图数据库存储分类案例如下图,其中Neo4j使用LPG模型,ArangoDB使用文档存储。

Ⓑ 图模型

图模型(Graph Model)是图数据库表达图数据的抽象模型。目前主流图数据库采用的图模型主要包括资源描述框架(Resource Description Framework, RDF)和属性图(Property Graph)两种。

数据模型特性RDF 图模型属性图模型(LPG)
结构标准化程度已由 W3C 制定了标准化的语法和语义尚未形成工业标准
数学模型三元组有向标签属性图
属性表达通过额外方法,如“具体化”内置支持
概念层本体定义RDFS 、OWL不支持
查询语言SPARQLCypher 、Gremlin、 PGQL、G-CORE
约束约束语言RDF Shapes 约束语言(SHACL)

② Neo4j数据存储(原生存储模型)

Ⓐ 图数据存储结构( 邻接矩阵 + 邻接表

根据邻接表和邻接矩阵的结构特性可知,当图为稀疏图、顶点较多,即图结构比较大时,更适宜选择邻接表作为存储结构。当图为稠密图、顶点较少时,或者不需要记录图中边的权值时,使用邻接矩阵作为存储结构较为合适。Neo4j 是使用邻接表作为图数据存储结构的一个主要代表。每个顶点 使用一个顶点记录来表示,每条边使用一个边记录来表示。如下图所示,

每个顶点记录包括:

  • 一个指向该节点的第一条边的指针 nextEdgeID
  • 一个指 向该节点的属性的单向链表的指针 nextPropID
  • 节点的标签 label
  • 一些扩展位 flags

每个边记录包括 :

  • 1该条边所指向的两个顶点 first Vertex 与 second Vertex
  • 边的类型 relType
  • 该条边指向的两个顶点各自的边的双向邻接表 firstPrev/NextEdgeID、secondPrev/NextEdgeID
  • 一个指向边的属性的单向链表的指针nextPropID
  • 一些扩展位 flags

Ⓑ 免索引邻接(Index-Free Adjacency)

每个节点会保留连接节点的引用,从而这个节点本身就是连接节点的一个索引,这种操作的性能比使用全局索引好很多,同时假如我们根据图来进行查询,这种查询是与整个图的大小无关的,只与查询节点关联边的数目有关,如果用 B 树索引进行查询的复杂度是 O(logN),使用这种结构查询的复杂度就是 O(1)。当我们要查询多层数据时,查询所需要的时间也不会随着数据集的变大而呈现指数增长,反而会是一个比较稳定的常数,毕竟每次查询只会根据对应的节点找到连接的边而不会去遍历所有的节点。

Ⓒ 节点与关系存储文件的物 理结构

节点和关系分别存储在节点文件和关系文件中,存储区是固定大小的记录存储,记录占9个字节,关系为33个字节。如果有一个ID为100的节点,那么就能计算出这个节点的起始位置是900。这种存储方式能通过计算高效的获取数据,时间复杂度为O(1),而不用通过遍历或者索引的方式查找。

Ⓓ 图在Neo4j中物理存储的方式

③ 事务实现原理

  • 每个事务被标识为一个内存对象,同时把数据库中的状态表示为写入状态。该对象由锁管理器提供支持,锁管理器在节点和关系被创建、更新和删除时加锁。当事务回滚时,事务对象被丢弃,同时释放写锁,而事务成功后会被提交到磁盘。
  • 提交数据到磁盘时会使用 WAL ,将更改作为可操作的条目附加到活动事务日志中。 当提交事务时,提交条目会写入日志,日志刷入磁盘后就会将更改持久化。磁盘刷新后,更改就会应用到图本身,当所有的更改应用到图之后,与事务相关的写锁都会被释放。

集群与可恢复性

  • 集群

Ⓐ 因果集群(多个节点故障 + 大规模查询)

  • 集群由一组核心服务器(Primary Servers)和大量的读副本(Read Replica)组成。核心服务器的主要职责是保护数据,它通过raft协议复制所有事务来实现这个功能。当核心服务器不能无法进行写入时,将会转化成只读副本。
  • 只读副本主要职责是扩展图操作的工作负载(如:Cypher查询)。它以异步方式周期的从核心服务器获取数据(毫秒范围内),以查找自上次轮询后处理的任何新事务。整个过程通过事务保证。
  • 不同于核心服务器,只读副本的丢失不会影响集群的可用性。
Ⓑ 高可用集群(硬件故障容错 + 读密集型场景)

image.png

  • 由一个主实例与零个或多个从实例组成,集群中所有的实例都在其本地数据库文件中存储集群数据的完整副本。
  • 主机 上执行事务,提交成功后同步给从节点,从节点失败后从主机主动拉取事务。
  • 节点故障时,被标记为临时故障,标记过程类似于redis的客观下线。
  • 关于选举。当主机故障后,提交事务多的节点会被选为主机。提交事务一样多时,server_id小的选为主。
  • 可恢复性

机器非正常关机恢复后,会检查最近活动的事务日志,并从找到的存储中重新执行其中的所有事务。重新执行事务日志进行恢复,因为幂等,所以对之前执行过的事务不会有影响。

四、Neo4j特性

代码开源

  • 便于二次开发,通过开发定制特定的功能。
  • 带来活跃的社区,能构建更丰富的生态。

高性能

  • 底层存储结构。 能通过O(1)成本的计算快速获取节点与关系的具体位置。另外,免索引连接能使图直接获取对邻接节点、关系、属性,而不用通过Join连接通过大量计算获取。从而使得复杂查询的响应时间能比关系型数据库高出几个数量级。
  • 扩展性。 随着数据规模增大,图的性能趋向于不变,因为查询总是与图的一部分有关。每个查询执行时间只和满足条件的那部分遍历的图大小成正比。图天然的可扩展性能减少数据迁移的次数,从而降低维护成本和风险。

ACID事务

  • 能确保操作的可靠性。 事务能对竞争状态下的资源进行保护,也能保证业务对执行结果的预期。

  • 应用场景广泛。 CAP理论决定CP场景的业务处理需要使用事务保证处理结果的准确性,比如:金融场景,抢票场景等。支持事务后,图数据库能深入不同的业务场景应对更多的问题,能很好的促进图数据库的发展,也会为图数据库带来更多潜在的发展方向。

五、工程使用

  1. 添加maven依赖

<dependency>

   <groupId>org.springframework.boot</groupId>

   <artifactId>spring-boot-starter-data-neo4j</artifactId>

</dependency>
  1. 添加配置文件

spring.neo4j.uri=bolt://localhost:7687

spring.data.neo4j.username=neo4j

spring.data.neo4j.password=123123
  1. 添加主逻辑

import lombok.AllArgsConstructor;

import lombok.Data;

import org.springframework.data.neo4j.core.schema.Id;

import org.springframework.data.neo4j.core.schema.Node;


@Data

@Node("Person")

@AllArgsConstructor

public class PersonEntity {

    @Id

    private final String name;

    private final Integer born;

}
import lombok.AllArgsConstructor;

import lombok.Data;

import org.springframework.data.neo4j.core.schema.Id;

import org.springframework.data.neo4j.core.schema.Node;

import org.springframework.data.neo4j.core.schema.Property;

import org.springframework.data.neo4j.core.schema.Relationship;

import java.util.Set;


@Data

@Node("Movie")

@AllArgsConstructor

public class MovieEntity {

    @Id

    private final String title;

    @Property("tagline")

    private final String description;

    @Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)

    private Set<PersonEntity> actors;

    @Relationship(type = "DIRECTED", direction = Relationship.Direction.INCOMING)

    private Set<PersonEntity> directors;

}
import org.springframework.data.neo4j.repository.Neo4jRepository;

import org.springframework.data.neo4j.repository.query.Query;

import org.springframework.data.repository.query.Param;

import java.util.List;


public interface MovieRepository extends Neo4jRepository<MovieEntity, String> {

    MovieEntity findOneByTitle(String title);

 @Query("MATCH (movie:Movie) WHERE movie.title CONTAINS $title RETURN movie")

    List<MovieEntity> customQuery(@Param("title") String title);

    @Query("Match (m:Movie) <- [:ACTED_IN] - (a:Person) where a.name = $name return m")

    List<MovieEntity> queryMovieEntityByActor(@Param("name") String name);

    @Query("match (a:Person) -[:ACTED_IN]->(mv) where a.name = $name return mv")

    List<MovieEntity> recommend(@Param("name") String name);

}
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

import java.util.List;



@ResponseBody

@RestController

@RequestMapping("/api")

public class MovieController {

    @Resource

    private MovieRepository movieRepository;

    @Resource

    private PersonRepository personRepository;

    @GetMapping("/movies")

    List<MovieEntity> getMovies() {

        return movieRepository.findAll();

    }

    @GetMapping("movies/{title}")

    MovieEntity findOneByTitle(@PathVariable String title) {

        return movieRepository.findOneByTitle(title);

    }

    @GetMapping("movies/actor/{name}")

    public List<MovieEntity> queryMovieEntityByActor(@PathVariable String name){

        return movieRepository.queryMovieEntityByActor(name);

    }

    @GetMapping("movie/director/{title}")

    public List<PersonEntity> queryPersonByTitle(@PathVariable String title){

        return personRepository.findByTitle(title);

    }

    @GetMapping("movie/recommend/{name}")

    public List<MovieEntity> recommendByTitle(@PathVariable String name){

        return movieRepository.recommend(name);

    }

}

参考资料