图数据库介绍及应用,go和Java使用图数据库

一、背景

在社交网络、知识图谱、推荐系统等领域,常常需要处理 大规模、多跳、多属性的复杂关系网络。以社交网络为例:

用户之间存在「单向或双向」的关注、好友关系。

常见的业务场景包括:查找共同好友、N跳关系路径、基于属性筛选的路径分析等。

随着用户数量和关系数量增长,传统关系型数据库在表示、维护及查询这些关系时将面临性能瓶颈。

我们可以把这些问题抽象为图问题:

用户是节点(顶点)

关系是边

查询变成在图上找路径、多跳连接、筛选满足属性条件的节点

初始做法:用关系型数据库模拟图

在没用图数据库前,我们可能这么实现:

一张 user 表存用户信息

一张 user_relation 表存用户关系(from_id、to_id、type)

多跳查询靠 BFS、DFS 遍历模拟

但是当数据规模上来,比如几百万用户、上千万条关系,多跳查询会非常慢,因为每一跳都要在数据库里查,产生大量随机 I/O。

扩展性问题也很明显:

如果要加公司、城市等新类型实体

要改表结构,还要改查询逻辑,很难维护

图数据库就是为这种结构复杂、关系密集、查询灵活的问题设计的。

基本概念:

名称 含义
图(Graph) 实体 + 关系 构成的结构
顶点(Vertex) 也叫节点,代表具体实体,如"用户"、"城市"
边(Edge) 顶点之间的关系,如"朋友"、"工作于"
属性(Property) 节点或边上的附加信息,如"用户年龄"、"关系时间"

图数据库的两种存储模型

原生图(Native Graph DB)

底层就是用图结构存储,比如 Neo4j:

节点之间直接有引用(指针)

遍历节点时无需查索引(Index-free adjacency)

查询性能随子图规模线性增长(而不是整个数据量)

适合高频、多跳、实时的图计算场景。

非原生图(Non-native)

底层存的是其他数据库(如关系型),只是加了一层图语义支持,比如:

Apache TinkerPop + 外部存储

查询效率不如原生图,但接入简单、通用性好

Neo4j 简要介绍

Neo4j 是目前最广泛使用的图数据库之一,特点是:

原生图存储引擎

提供 Cypher 查询语言(类 SQL,很易上手)

查询效率高,尤其在多跳、多关系链场景表现优秀

提供 REST API 或嵌入式调用

Cypher

  1. 节点表示

节点使用圆括号 () 表示。

// 表示一个节点

(node)

添加标签(类似关系型数据库中的表)

// 带有 Person 标签的节点

(person:Person)

一个节点可以有多个标签

// 同时带有 Person 和 Employee 标签的节点

(person:Person:Employee)

关系可以有类型

// KNOWS 类型的关系

(person1:Person)-[:KNOWS]->(person2:Person)

节点和关系可以有属性,属性使用大括号 {} 表示

// 带属性的节点

(person:Person {name: "张三", age: 30})

// 带属性的关系

(person1:Person)-[:KNOWS {since: 2015}]->(person2:Person)

一些常用示例:

go 复制代码
// 创建用户节点
CREATE (:User {name: "Alice", age: 25})
// 如果需要引用这个节点,可以给节点一个变量名
CREATE (u:User {name: "Alice", age: 30})
// 创建关系
MATCH (a:User {name: "Alice"}), (b:User {name: "Bob"})
CREATE (a)-[:FRIEND {since: 2020}]->(b)

// 查询 Alice 的所有朋友
MATCH (a:User {name: "Alice"})-[:FRIEND]->(friend)
RETURN friend.name

// 查询 Alice 的"朋友的朋友"
MATCH (a:User {name: "Alice"})-[:FRIEND]->()-[:FRIEND]->(fof)
RETURN DISTINCT fof.name

//修改数据
// 修改 Bob 的年龄
MATCH (u:User {name: "Bob"})
SET u.age = 35

// 给 Charlie 增加城市属性
MATCH (u:User {name: "Charlie"})
SET u.city = "Beijing"
//删除数据
// 删除某个用户节点及其所有关系
MATCH (u:User {name: "Alice"})
DETACH DELETE u
//创建带属性的复杂结构
// 创建用户、公司、工作关系
CREATE (u:User {name: "Diana"})
CREATE (c:Company {name: "OpenAI"})
CREATE (u)-[:WORKS_AT {role: "Engineer", since: 2021}]->(c)
//路径与最短路径查询

// 查询 Alice 到 Charlie 的最短路径
MATCH (a:User {name: "Alice"}), (c:User {name: "Charlie"}),
p = shortestPath((a)-[*..5]-(c))
RETURN p
//使用 WHERE 条件过滤
// 查询30岁以上的用户
MATCH (u:User)
WHERE u.age > 30
RETURN u.name, u.age

// 查询工作在"OpenAI"的用户
MATCH (u:User)-[:WORKS_AT]->(c:Company {name: "OpenAI"})
RETURN u.name

底层结构

作为「原生图」模式的图数据库,免索引邻接是实现高效查询性能的重要因素,因此免索引邻接的实现机制就是neo4j 底层存储结构设计的关键。

在 neo4j 中,点、关系和属性等组成元素的结构长度是固定的,同时基于内部维护的ID进行访问。所以知道某点/关系/属性的ID,就能计算得到在对应文件中的偏移位置,直接进行访问。在遍历图时就省去了基于索引扫描的中间过程。

底层存储逻辑图:包括节点(label)、关系、属性要素,并通过指针维护关系

节点结构(指向关系和属性的单向链表,neostore.nodestore.db):共15字节。

<1bytes: 标志是否被使用>、<4bytes: 第一个关系ID>、<4bytes: 第一个属性ID>、<5bytes: 节点标签>、<1bytes: extra保留位>

关系结构(双向链表,neostore.relationshipstore.db):共34bytes

<1bytes: 标志是否被使用>、<4bytes: 起始节点ID>、<4bytes:结束节点ID>、<4bytes: 关系类型>、<4bytes: 起始节点上个关系>、<4bytes: 起始节点下个关系>、<4bytes: 结束节点上个关系>、<4bytes: 结束节点下个关系> 、<4bytes: 第一个属性ID>、<1bytes: 是否为该起点和终点的第一个关系>

属性结构(单项链表,neostore.propertystore.db):属性结构同理

golang使用Neo4j

go get github.com/neo4j/neo4j-go-driver/v5/neo4j

go 复制代码
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)

func main() {
	// 创建 driver 实例
	uri := "neo4j://localhost:7687"
	username := "neo4j"
	password := "your_password" // 请替换成你自己的密码

	// 连接数据库
	driver, err := neo4j.NewDriver(uri, neo4j.BasicAuth(username, password, ""))
	if err != nil {
		log.Fatal("创建 Neo4j driver 失败:", err)
	}
	defer driver.Close(context.Background())

	// 开始一个会话
	session := driver.NewSession(context.Background(), neo4j.SessionConfig{
		AccessMode: neo4j.AccessModeWrite,
	})
	defer session.Close(context.Background())

	// 执行 Cypher 创建节点
	_, err = session.ExecuteWrite(context.Background(), func(tx neo4j.ManagedTransaction) (any, error) {
		_, err := tx.Run(context.Background(),
			`CREATE (a:Person {name: $name, age: $age}) RETURN a`,
			map[string]any{
				"name": "Alice",
				"age":  30,
			})
		return nil, err
	})
	if err != nil {
		log.Fatal("写入节点失败:", err)
	}
	fmt.Println("成功创建节点")

	// 执行查询
	result, err := session.ExecuteRead(context.Background(), func(tx neo4j.ManagedTransaction) (any, error) {
		res, err := tx.Run(context.Background(),
			`MATCH (a:Person) RETURN a.name AS name, a.age AS age LIMIT 10`,
			nil)
		if err != nil {
			return nil, err
		}

		var results []map[string]any
		for res.Next(context.Background()) {
			record := res.Record()
			results = append(results, map[string]any{
				"name": record.Values[0],
				"age":  record.Values[1],
			})
		}
		return results, nil
	})

	if err != nil {
		log.Fatal("查询失败:", err)
	}

	// 输出结果
	for _, r := range result.([]map[string]any) {
		fmt.Printf("Person: name=%v, age=%v\n", r["name"], r["age"])
	}
}

Java使用neo4j

springdata-neo4j的实例:

xml 复制代码
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-neo4j</artifactId>
  </dependency>
</dependencies>

application.yml:

yml 复制代码
spring:
  neo4j:
    uri: bolt://localhost:7687
    authentication:
      username: neo4j
      password: your_password

person.java:

java 复制代码
import org.springframework.data.neo4j.core.schema.*;

import java.util.HashSet;
import java.util.Set;

@Node
public class Person {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @Relationship(type = "KNOWS", direction = Relationship.Direction.OUTGOING)
    private Set<Person> friends = new HashSet<>();

    public Person() {}

    public Person(String name) {
        this.name = name;
    }

    // getter 和 setter
}

repository:

java 复制代码
import org.springframework.data.neo4j.repository.Neo4jRepository;

public interface PersonRepository extends Neo4jRepository<Person, Long> {
    Person findByName(String name);
}

PersonService.java:

java 复制代码
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class PersonService {
    private final PersonRepository personRepository;

    public PersonService(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @Transactional
    public void createData() {
        Person alice = new Person("Alice");
        Person bob = new Person("Bob");

        alice.getFriends().add(bob);

        personRepository.save(alice); // 会级联保存 bob 和关系
    }

    public Person findByName(String name) {
        return personRepository.findByName(name);
    }
}

controller:

java 复制代码
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/person")
public class PersonController {
    private final PersonService personService;

    public PersonController(PersonService personService) {
        this.personService = personService;
    }

    @PostMapping("/init")
    public String init() {
        personService.createData();
        return "ok";
    }

    @GetMapping("/{name}")
    public Person get(@PathVariable String name) {
        return personService.findByName(name);
    }
}
相关推荐
huazhixuthink28 分钟前
PostgreSQL三种关闭方式的区别
数据库·postgresql
失散1332 分钟前
并发编程——17 CPU缓存架构详解&高性能内存队列Disruptor实战
java·缓存·架构·并发编程
only-qi5 小时前
146. LRU 缓存
java·算法·缓存
阿里小阿希5 小时前
Vue3 + Element Plus 项目中日期时间处理的最佳实践与数据库设计规范
数据库·设计规范
xuxie136 小时前
SpringBoot文件下载(多文件以zip形式,单文件格式不变)
java·spring boot·后端
白鹭6 小时前
MySQL源码部署(rhel7)
数据库·mysql
重生成为编程大王6 小时前
Java中的多态有什么用?
java·后端
666和7776 小时前
Struts2 工作总结
java·数据库
还听珊瑚海吗6 小时前
SpringMVC(一)
数据库
中草药z6 小时前
【Stream API】高效简化集合处理
java·前端·javascript·stream·parallelstream·并行流