5.1 存储引擎内幕:免索引邻接的磁盘真相 本节摘要:免索引邻接不是一个抽象卖点,而是一组具体的磁盘布局决策:节点与关系各占一条定长记录,关系记录用双向链表挂在两端节点上,属性拆成条块按需续接。本节拆开这层布局,推演出三个工程结论——遍历为何与总量无关、写入为何有固定开销、内存为何该优先喂页缓存。 第 1 章我们用"节点旁挂着小纸条"比喻免索引邻接。本节把纸条翻过来,看看上面到底写了什么、占几个字节、为什么这样设计。 一、存储分区:五类记录文件 Neo4j 的原生图存储把数据按类型分文件存放,每类都是定长记录的数组: 节点记录里最重要的字段是指向第一条关系的指针;
本节摘要:免索引邻接不是一个抽象卖点,而是一组具体的磁盘布局决策:节点与关系各占一条定长记录,关系记录用双向链表挂在两端节点上,属性拆成条块按需续接。本节拆开这层布局,推演出三个工程结论——遍历为何与总量无关、写入为何有固定开销、内存为何该优先喂页缓存。
第 1 章我们用"节点旁挂着小纸条"比喻免索引邻接。本节把纸条翻过来,看看上面到底写了什么、占几个字节、为什么这样设计。
Neo4j 的原生图存储把数据按类型分文件存放,每类都是定长记录的数组:
neostore 结构(示意): neostore.nodestore.db 节点记录 定长(含首关系指针、标签指针) neostore.relationshipstore.db 关系记录 定长(前后指针 + 两端节点 id) neostore.propertystore.db 属性记录 条块式(内联短值 + 动态块) neostore.labelscanstore.db 标签索引 加速按标签扫描 neostore.countsstore.db 计数缓存 加速 count 估计
节点记录里最重要的字段是指向第一条关系的指针;关系记录则是整张布局的枢纽,它同时记录着:
关系记录 #8412 (类型:ACTED_IN) ├─ 起点节点 id 与 起点侧的前驱/后继关系 ├─ 终点节点 id 与 终点侧的前驱/后继关系 └─ 首个属性记录指针
每个节点在每种方向/类型组合上最多挂两条"最近使用"的关系指针,其余关系通过关系记录里的前后继指针串成双向链表。

结论一:遍历成本与图总量无关。 从节点记录出发,沿关系链跳两步就到邻居,全程不查任何全局索引。这解释了 2.4 里"索引 Seek 后每一步 Expand 都很便宜"的底层原因:Seek 只发生在锚点定位那一次。
结论二:写入有固定的记录开销。 建一条关系要写:关系记录本身、两端节点记录的指针维护、可能的属性记录。批量导入前"先建约束后导数据"依然有效,但"先建全部索引再导"在 admin import 路线上反而是负担——它不走事务引擎,索引在导入完成后统一构建更快。
结论三:内存优先喂页缓存。 定长记录让任意记录都能算出物理页位置——页缓存命中率接近 100% 时,每次"跳转"都是内存操作而非磁盘读。这就是 3.4 节"页缓存应装下整个图"的物理依据。
定长布局的另一个红利是可估算。用记录开销口径算一笔账:
假设:1 亿节点、10 亿关系、属性总量 20 GB 节点:1e8 × 15 B ≈ 1.5 GB 关系:1e9 × 34 B ≈ 34 GB 属性:约 20 GB 索引与标签存储:约 5 GB 合计 ≈ 60 GB → 页缓存目标 60 GB,磁盘预留 84 GB(40% 余量)
数字是量级示意,方法论是重点:存储体积在写入前就能推算,这比"导完了再看磁盘"的被动方式好用得多。3.4 节的容量规划用的正是这套口径。
| 维度 | Neo4j 记录式 | 关系库 B+ 树 |
|---|---|---|
| 关联定位 | 记录内指针直跳 | 索引查找或表扫描 |
| 热点数据 | 邻接天然聚在附近页 | 行分布在整张表 |
| 写入开销 | 记录 + 指针维护 | 行 + 全部二级索引维护 |
| 点查 | 需索引(锚点一次) | 索引高度内命中 |
两类引擎没有绝对高下:图存储赢在"顺藤摸瓜",B+ 树赢在"按条件大海捞针"。Neo4j 的解法是两者兼收——锚点定位用 B+ 树索引,之后的全程用指针。2.4 节那张对比图的物理本质,就是"索引用一次,指针走全程"。
属性存储值得单独一段,因为它的设计直接影响"节点该挂多少属性"。属性记录采用条块式布局:
属性记录结构(示意): [属性块头] [key 引用] [类型] [值] 短值(数字、短字符串)直接内联在记录内 长字符串 / 大列表 拆成动态块链,跨记录续接
推论很直接:短小的属性读写最便宜;一个节点塞几 MB 的 JSON 大字段,读取时要多跳好几块动态块,页缓存的效率也被拉低。1.2 建模惯例里"只存查得着、看得懂的属性"在这里找到了物理依据——不是风格洁癖,是省指针跳转。
每条关系的写入要动多处记录:关系记录本体、两端节点的指针、可能的关系属性。这意味着图存储存在天然写放大——写入一条边,实际落盘不止一条记录。
这解释了 3.3 的一个设计:admin import 不走事务引擎,直接按最终布局写文件,绕开逐条写放大的路径,所以快出两个数量级。也解释了为什么在线写入适合"持续中小批"而不是"一次性洪峰"——洪峰时写放大叠加上锁排队,延迟曲线会非常难看。
| 写入形态 | 路径 | 适合 |
|---|---|---|
| 逐条在线写 | 事务引擎,逐记录落盘 | 业务事件、持续小批 |
| UNWIND 分批 | 事务引擎,批内合并提交 | 定时同步(每分钟级) |
| admin import | 直写存储文件 | 初始建库、全量迁移 |
内核知识若不能回流向日常决策,就只是谈资。三点回望:
回望建模:属性短小、关系精炼的图,页缓存效率天然更高——5.1 给了 2.1 的建模纪律一个物理注脚。
回望调优:2.4 里"锚点 Seek + 指针 Expand"的计划形状,现在你知道它的每一个算子在磁盘上做什么。看到 Expand 行数爆炸,你想的不该是"加机器",而是"这一步的模式是不是太宽"。
回望容量:存储可估算、页缓存可规划、内存该给谁,3.4 与本节互相引用的口径完全一致——运维与内核讲的是同一件事的两面。
单机的故事讲完了。当一台机器装不下或扛不住时,下一节把多台机器组成一个逻辑整体。