下面给出一份「能直接跑」的详细设计与实现,用 纯 PHP + Redis 手工搭建一套 最小但完整的图数据库。
代码全部在一个文件 GraphRedis.php
里,依赖只有:
- PHP ≥ 7.4
- 已安装 phpredis 扩展(Predis 也行,改
$redis = new Predis\Client()
即可)
一、存储模型(Redis 端)
概念 | Redis 结构 | Key 命名 | 说明 |
---|---|---|---|
节点 | Hash | node:<id> | 存任意属性 name , age , tag … |
出边 | SortedSet | edge:<from>:out | score 存 权重,member 存 目标 id |
入边 | SortedSet | edge:<to>:in | 同上,反向索引,方便「谁指向我」 |
边属性 | Hash | edge_prop:<from>:<to> | 存这条边上的额外字段,如 label , since … |
全局自增 ID | String | global:node_id | 保证节点 id 唯一 |
用 SortedSet 而不是 Set,是为了天然支持 带权边 与 按权重分页。
二、PHP 类图
GraphRedis
├─ addNode(array $prop) : int 创建节点,返回 id
├─ getNode(int $id) : ?array
├─ updateNode(int $id, array $diff) 增量更新
├─ delNode(int $id) 级联删除所有边
├─ addEdge(int $from,int $to,float $w=1,array $prop=[]) : void
├─ delEdge(int $from,int $to) : void
├─ neighbors(int $id,string $dir='out',int $page=1,int $size=100) : array
├─ shortestPath(int $from,int $to,int $maxDepth=6) : ?array BFS
├─ dfs(int $start,int $maxDepth=6) : array 深度优先
└─ _redis() : \Redis 内部连接
三、完整源码
保存为 GraphRedis.php
:
<?php
/**
* 纯 PHP + Redis 手工图数据库
* php GraphRedis.php // 会跑一段 demo
*/
class GraphRedis
{
private \Redis $redis;
private int $pageSize = 100; // neighbors 默认分页大小
public function __construct(string $host = '127.0.0.1', int $port = 6379)
{
$this->redis = new \Redis();
$this->redis->connect($host, $port);
}
/* ---------- 节点相关 ---------- */
/** 创建节点,返回全局唯一 id */
public function addNode(array $prop): int
{
$id = $this->redis->incr('global:node_id');
$this->redis->hSet("node:$id", $prop);
return $id;
}
public function getNode(int $id): ?array
{
$raw = $this->redis->hGetAll("node:$id");
return $raw ?: null;
}
/** 增量更新 */
public function updateNode(int $id, array $diff): void
{
if ($diff) {
$this->redis->hSet("node:$id", $diff);
}
}
/** 级联删除节点 + 所有出入边 */
public function delNode(int $id): void
{
$pipe = $this->redis->multi();
// 1. 删出边
$out = $pipe->zRange("edge:$id:out", 0, -1);
foreach ($out as $to) {
$pipe->del("edge_prop:$id:$to");
$pipe->zRem("edge:$to:in", $id);
}
$pipe->del("edge:$id:out");
// 2. 删入边
$in = $pipe->zRange("edge:$id:in", 0, -1);
foreach ($in as $from) {
$pipe->del("edge_prop:$from:$id");
$pipe->zRem("edge:$from:out", $id);
}
$pipe->del("edge:$id:in");
// 3. 删节点本身
$pipe->del("node:$id");
$pipe->exec();
}
/* ---------- 边相关 ---------- */
/** 添加/更新一条边(有向) */
public function addEdge(int $from, int $to, float $weight = 1.0, array $prop = []): void
{
$pipe = $this->redis->multi();
$pipe->zAdd("edge:$from:out", $weight, $to);
$pipe->zAdd("edge:$to:in", $weight, $from);
if ($prop) {
$pipe->hSet("edge_prop:$from:$to", $prop);
}
$pipe->exec();
}
public function delEdge(int $from, int $to): void
{
$pipe = $this->redis->multi();
$pipe->zRem("edge:$from:out", $to);
$pipe->zRem("edge:$to:in", $from);
$pipe->del("edge_prop:$from:$to");
$pipe->exec();
}
/** 取邻居,支持分页,默认出边 */
public function neighbors(int $id, string $dir = 'out', int $page = 1, int $size = null): array
{
$size = $size ?: $this->pageSize;
$key = "edge:$id:$dir";
$start = ($page - 1) * $size;
$stop = $start + $size - 1;
return $this->redis->zRange($key, $start, $stop, true); // true 返回 [id=>score]
}
/* ---------- 遍历算法 ---------- */
/** BFS 最短路径,返回 [距离, 路径数组] 或 null */
public function shortestPath(int $from, int $to, int $maxDepth = 6): ?array
{
if ($from === $to) return [0, [$from]];
$q = new SplQueue();
$q->enqueue([$from, 0, [$from]]);
$seen = [$from => true];
while (!$q->isEmpty()) {
[$id, $depth, $path] = $q->dequeue();
if ($depth >= $maxDepth) continue;
foreach ($this->neighbors($id, 'out', 1, 100) as $next => $weight) {
if ($next == $to) {
return [$depth + 1, array_merge($path, [$next])];
}
if (!isset($seen[$next])) {
$seen[$next] = true;
$q->enqueue([$next, $depth + 1, array_merge($path, [$next])]);
}
}
}
return null;
}
/** 非递归 DFS,只返回访问顺序 */
public function dfs(int $start, int $maxDepth = 6): array
{
$stack = new SplStack();
$stack->push([$start, 0]);
$seen = [$start => true];
$order = [];
while (!$stack->isEmpty()) {
[$id, $d] = $stack->pop();
$order[] = $id;
if ($d >= $maxDepth) continue;
// 逆序推栈,保证顺序友好
$neighbors = array_keys($this->neighbors($id, 'out', 1, 100));
foreach (array_reverse($neighbors) as $next) {
if (!isset($seen[$next])) {
$seen[$next] = true;
$stack->push([$next, $d + 1]);
}
}
}
return $order;
}
}
/* --------------------------------------------------
* 下面是一段 Demo:建图 → 遍历 → 最短路径
* php GraphRedis.php
* -------------------------------------------------- */
if (PHP_SAPI === 'cli') {
$g = new GraphRedis();
// 1. 造 5 个用户
$bob = $g->addNode(['name' => 'Bob', 'age' => 32]);
$alice = $g->addNode(['name' => 'Alice', 'age' => 28]);
$tom = $g->addNode(['name' => 'Tom', 'age' => 25]);
$jack = $g->addNode(['name' => 'Jack', 'age' => 30]);
$anni = $g->addNode(['name' => 'Anni', 'age' => 27]);
// 2. 加关注关系(有向边)
$g->addEdge($bob, $alice, 1, ['label' => 'follow']);
$g->addEdge($alice, $tom, 1, ['label' => 'follow']);
$g->addEdge($tom, $jack, 1, ['label' => 'follow']);
$g->addEdge($jack, $anni, 1, ['label' => 'follow']);
$g->addEdge($bob, $jack, 2, ['label' => 'colleague']); // 权重 2
// 3. 查邻居
echo "Bob 的关注列表:\n";
print_r($g->neighbors($bob));
// 4. DFS 遍历
echo "从 Bob 开始的 DFS 顺序:\n";
print_r($g->dfs($bob));
// 5. 最短路径
$path = $g->shortestPath($bob, $anni);
echo "Bob -> Anni 最短路径:\n";
print_r($path);
// 6. 级联删除演示
// $g->delNode($tom);
}
四、如何运行
# 1. 启动 Redis(默认端口即可)
redis-server
# 2. 安装 phpredis(若未装)
pecl install redis
echo "extension=redis.so" >> php.ini
# 3. 跑 demo
php GraphRedis.php
五、可以继续加的功能
- 加权最短路径
把BFS
换成SplPriorityQueue
即可实现 Dijkstra。 - 分页深度遍历
目前 neighbors 一次性拉 100 条,可继续用zRangeByScore
做游标分页。 - 反向关注查询
直接$g->neighbors($id, 'in')
就行,因为维护了反向索引。 - 事务性批量写
把addEdge/addNode
包进Lua
脚本,保证 「节点存在」 才能加边。 - 标签过滤
在node:<id>
里加tags
字段,再用 Redis Search 模块做索引。
至此,一个 轻量级、无第三方依赖、可直接嵌入业务代码 的 PHP 图数据库就完成了。Enjoy hacking!
GraphRedis – 基于 Redis 的图数据库
一个纯 PHP 实现的极简轻量级图数据库,使用 Redis 作为存储后端,提供高性能的图数据存储与查询功能。
https://github.com/linkerlin/GraphRedis/blob/main/README_CN.md