图数据库NEO4J 底层分析

图数据库NEO4J

Neo4j主要通过构成图来存储数据,图中的数据包括节点、关系以及节点的属性和关系的属性,关系可以是双向的,也可以是只有单向的.

以下是它的一些特点 + 支持完整的ACID(原子性、一致性、隔离性和持久性) +
支持常数级时间复杂度的图遍历 + 支持查询的数据导出为JSON和XLS格式 +
支持通过浏览器图形化界面形式访问 +
可以通过多种语言进行访问管理(Java、Python、Ruby、PHP、C#、Js)

原生图处理 native processing

原生图处理:存在免索引邻接属性(Index Free
Adjacency),提供快速高效的图遍历

使用免索引邻接的数据库引擎中的每个节点都会维护其对相邻节点的引用。因此每个节点都表现为其附近节点的微索引,这比使用全局索引代价小很多。这意味着查询时间与图的整体规模无关,它仅和所搜索图的数量成正比。

相反,一个非原生图数据库引擎使用(全局)索引连接各个节点。这些索引对每个遍历都添加一个间接层,因此会导致更大的计算成本。原生图处理的拥护者认为免索引邻接至关重要,因为它提供快速、高效的图遍历。

索引查找在小型网络中可以工作,但对于大图的查询代价太高。具有原生图处理能力的图数据库在查询是不是使用索引查找来扮演联系的角色,而是使用免索引邻接来确保高性能遍历的。
 非原生图处理引擎使用索引进行节点间遍历
 

上图中要寻找Alice的朋友,我们必须要首先执行索引查找,成本为O(log n )
,这对于偶尔或者浅层的查找来说是可以接受的,但当我们改变遍历的方向时,他的代价就变得非常昂贵起来,如果相对于寻找alice的朋友,就必须要执行多个索引来完成查找,每个节点所代表的人都有可能把Alice当作他的朋友,这使得成本很高,找到Alice的朋友代价是O(log
n ) ,而找到和Alice交朋友的人的代价则是O(mlogn)。

索引查找在小型网络中还可以,但是在大图中的查询代价太高,具有原生图处理能力的图数据库在查询时不是使用索引查找的,而是使用免索引零连接来确保高性能的遍历的,下图为Neo4j使用关系而非索引实现快速遍历

在通用图数据库中,可以以极小的代价双向(从尾部到头部或者从头部到尾部)遍历关系,上图中寻找ALICE的朋友,直接向外寻找friend就可以。其遍历的成本为O(1),要寻和Alice交朋友的人,我们只需要所有指向ALICE的friend关系联系在一起即可,这样的成本是O(1).

原生图存储 native graph storage

免索引邻接(index-free adjacency) 是图数据库相比于传统的 mysql
的优势的核心 key,那么图数据库用什么结构去存储 index-free adjacency
是关键设计点。

架构上层是对外访问的 api,右边是事务管理,左边有 cache 等,下面我们看下
disk 上存储的结构:


neo4j 在磁盘上会分不同的 store file 存储

  • neostore.nodestore.db:存储 node
  • neostore.propertystore.db:存储属性
  • neostore.relationshipstore.db:存储关系

一个重要的设计点是 store 中存储的 record
都是固定大小的,固定大小带来的好处是:因为每个 record
的大小固定,因此给定 id就能快速进行定位。

节点与关系的存储文件的物理结构图 上图第一个是 node record 的结构:

  • 1byte:in-use flag,表明该 node 是否在使用
  • 4byte:第一个 relation id(-1表示无)
  • 4byte:第一个 property id(-1表示无)
  • 5byte:label 信息(可能直接 inline 存储)
  • 1byte:reversed

图中的节点和联系的存储文件都是固定大小的,每个记录长度为9字节,因此可以可以在O(1)的时间复杂度下计算位置.

节点(指向联系和属性的单向链表,neostore.nodestore.db):第一个字节,表示是否被使用的标志位,后面4个字节,代表关联到这个节点的第一个关系的ID,再接着的4个字符,代表第一个属性ID,后面紧接着的5个字符是代表当前节点的标签,指向该节点的标签存储,最后一个字符作为保留位.

联系(双向链表,neostore.relationshipstore.db):第一个字节,表示是否被使用的标志位,后面4个字节,代表起始节点的ID,再接着的4个字符,代表结束个节点的ID,然后是关系类型占用5个字节,然后依次接着是起始节点的上下联系和结束节点的上下节点,以及一个指示当前记录是否位于联系链的最前面.

同时还有属性存储(neostore.propertystore.db)也是固定大小,每个属性记录包括4个属性块(一个属性记录最多容纳4个属性)和指向属性链中下一个属性的ID.
属性记录包括属性类型和指向属性索引文件的指针(neostore.propertysotre.db.index).
同时属性记录中可以内联和动态存储,在属性值存储占用小时,会直接存储在属性记录中,对于大属性值,可以分别存储在动态字符存储(neostore.propertysotre.db.strings)和动态数组存储(neostore.propertysotre.db.arrays)中,由于动态记录同样由记录大小固定的记录链表组成,因此大字符串和大数组会占据多个动态记录.

节点存储文件用来存储节点的记录。每个用户级的图中创建的节点最终会终结于节点存储,其物理文件是"neostore.nodestore.db"。像大多数Neo4j存储文件一样,节点存储区是固定大小的记录存储,每个记录长度为9字节。通过大小固定的记录可以快速查询存储文件中的节点。 一个节点记录的第一个字节是“是否在使用”标志位。它告诉数据库该记录目前是被用于存储节点,还是可回收用于表示一个新的节点。接下来的4字节表示关联到该节点的第一个联系,随后4字节表示该节点的第一个属性的ID。标签的5字节指向该节点的标签存储(如果标签很少的话也可以内联到节点中)。最后的字节extra是标志保留位。这样一个标志是用来标识紧密连接节点的,而省下的空间为将来预留。节点记录是相当轻量级的:它真的只是几个指向联系和属性列表的指针。 相应的,联系被存储于联系存储文件中,物理文件是neostore.relationshipstore.db。像节点存储一样,联系存储区的记录的大小也是固定的。每个联系记录包含联系的起始点ID和结束节点ID、联系类型的指针(存储在联系类型存储区),起始节点和结束节点的上一个联系和下一个联系,以及一个指示当前记录是否位于联系链最前面。

下面是 relation record 的结构:

刚开始是开始和结束节点的 node id,接着是 relation type
pointer,然后开始和结束节点的前驱和后继 relation id

更形象一点的图


一个可能的搜索过程是:对于给定的一个 node record,可以通过 id
进行简单的偏移计算得到 node,然后通过 relation_id 定位到 relation
record,然后得到 end node id,通过偏移计算得到 node

两个节点记录都包含一个指向该节点的第一个属性的指针和联系链中第一个联系的指针。要读取节点的属性,我们从指向第一个属性的指针开始遍历单向链表结构。要找到一个节点的联系,我们从指向第一个联系(在示例中为LIKES联系)的节点联系指针开始,顺着特定节点的联系的双向链表寻找(即起始节点的双向链表或结束节点的双向链表),直到找到感兴趣的联系。一旦找到了我们想要的联系记录,我们可以使用和寻找节点属性一样的单向链表结构读取这种联系的属性(如果有的话),也可以使用联系关联的起始节点ID和结束节点ID检查它们的节点记录。用这些ID乘以节点记录的大小,就可以立即算出每个节点在节点存储文件中的偏移量。 联系存储文件中的双向链表:

双向存储

这种 partner
的关系天然就是双向的,但是我们存储的时候,难道要存储两个关系吗,如下图:

那肯定是不需要的,这种存储就是一种浪费,那到底 neo4j 中是怎么存储
partner 这种双向关系的呢?
答案是:以任意一个节点为开端,另一个为尾端,即存储成为单向的关系

在 neo4j 中任意的关系都有一个 start node 和一个 end node,而且 start
node 和 end node
都会有个关联的双向链表,这个双向链表中就记录了从该节点出去和进入的所有关系

示例1

在这个例子中,A ~ E表示Node 的编号,R1~R7 表示 Relationship
编号,P1~P10 表示Property 的编号。

Node 的存储示例图如下,每个Node 保存了第1个Property 和
第1个Relationship:

关系的存储示意图如下:

从示意图可以看出,从 Node-B 开始,可以通过关系的 next 指针,遍历Node-B
的所有关系,然后可以到达与其有关系的第1层Nodes,在通过遍历第1层Nodes的关系,可以达到第2层Nodes,...

neo4j graph db的存储文件介绍

下载neo4j-community-3.5.6并安装,运行几个例子后
在安装目录的/data/databases/graph.db下

::: {#cb5 .sourceCode}

[root@localhost graph.db]# ll
总用量 596
drwxr-xr-x 2 root root     10 5月  27 10:32 index
-rw-r--r-- 1 root root   8192 5月  28 09:30 neostore
-rw-r--r-- 1 root root   1152 5月  28 09:30 neostore.counts.db.a
-rw-r--r-- 1 root root   1152 5月  28 09:26 neostore.counts.db.b
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.id
-rw-r--r-- 1 root root  49152 5月  28 09:30 neostore.labelscanstore.db
-rw-r--r-- 1 root root   8190 5月  27 16:59 neostore.labeltokenstore.db
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.labeltokenstore.db.id
-rw-r--r-- 1 root root   8192 5月  27 16:59 neostore.labeltokenstore.db.names
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.labeltokenstore.db.names.id
-rw-r--r-- 1 root root   8190 5月  28 09:30 neostore.nodestore.db
-rw-r--r-- 1 root root    297 5月  28 09:30 neostore.nodestore.db.id
-rw-r--r-- 1 root root   8192 5月  27 10:32 neostore.nodestore.db.labels
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.nodestore.db.labels.id
-rw-r--r-- 1 root root  48954 5月  28 09:30 neostore.propertystore.db
-rw-r--r-- 1 root root  73728 5月  28 09:30 neostore.propertystore.db.arrays
-rw-r--r-- 1 root root    137 5月  28 09:30 neostore.propertystore.db.arrays.id
-rw-r--r-- 1 root root    265 5月  28 09:30 neostore.propertystore.db.id
-rw-r--r-- 1 root root   8190 5月  27 16:59 neostore.propertystore.db.index
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.propertystore.db.index.id
-rw-r--r-- 1 root root   8192 5月  27 16:59 neostore.propertystore.db.index.keys
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.propertystore.db.index.keys.id
-rw-r--r-- 1 root root  16384 5月  28 09:30 neostore.propertystore.db.strings
-rw-r--r-- 1 root root     89 5月  28 09:30 neostore.propertystore.db.strings.id
-rw-r--r-- 1 root root   8192 5月  27 10:32 neostore.relationshipgroupstore.db
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.relationshipgroupstore.db.id
-rw-r--r-- 1 root root  32640 5月  28 09:30 neostore.relationshipstore.db
-rw-r--r-- 1 root root    273 5月  28 09:30 neostore.relationshipstore.db.id
-rw-r--r-- 1 root root   8190 5月  27 16:59 neostore.relationshiptypestore.db
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.relationshiptypestore.db.id
-rw-r--r-- 1 root root   8192 5月  27 16:59 neostore.relationshiptypestore.db.names
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.relationshiptypestore.db.names.id
-rw-r--r-- 1 root root   8192 5月  27 10:32 neostore.schemastore.db
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.schemastore.db.id
-rw-r--r-- 1 root root 229292 5月  28 09:30 neostore.transaction.db.0

:::

存储 node 的文件

  1. 存储节点数据及其序列Id
    • neostore.nodestore.db: 存储节点数组,数组的下标即是该节点的ID
    • neostore.nodestore.db.id :存储最大的ID 及已经free的ID
  2. 存储节点label及其序列Id
    • neostore.nodestore.db.labels
      :存储节点label数组数据,数组的下标即是该节点label的ID
    • neostore.nodestore.db.labels.id

存储 relationship 的文件

  1. 存储关系数据及其序列Id
    • neostore.relationshipstore.db 存储关系 record 数组数据
    • neostore.relationshipstore.db.id
  2. 存储关系组数据及其序列Id
    • neostore.relationshipgroupstore.db 存储关系 group数组数据
    • neostore.relationshipgroupstore.db.id
  3. 存储关系类型及其序列Id
    • neostore.relationshiptypestore.db 存储关系类型数组数据
    • neostore.relationshiptypestore.db.id
  4. 存储关系类型的名称及其序列Id
    • neostore.relationshiptypestore.db.names存储关系类型 token
      数组数据
    • neostore.relationshiptypestore.db.names.id

存储 label 的文件

  1. 存储label token数据及其序列Id
    • neostore.labeltokenstore.db 存储lable token 数组数据
    • neostore.labeltokenstore.db.id
  2. 存储label token名字数据及其序列Id
    • neostore.labeltokenstore.db.names 存储 label token 的 names 数据
    • neostore.labeltokenstore.db.names.id

存储 property 的文件

  1. 存储属性数据及其序列Id
    • neostore.propertystore.db 存储 property 数据
    • neostore.propertystore.db.id
  2. 存储属性数据中的数组类型数据及其序列Id
    • neostore.propertystore.db.arrays 存储 property (key-value
      结构)的Value值是数组的数据。
    • neostore.propertystore.db.arrays.id
  3. 属性数据为长字符串类型的存储文件及其序列Id
    • neostore.propertystore.db.strings 存储 property (key-value
      结构)的Value值是字符串的数据。
    • neostore.propertystore.db.strings.id
  4. 属性数据的索引数据文件及其序列Id
    • neostore.propertystore.db.index 存储 property (key-value
      结构)的key 的索引数据。
    • neostore.propertystore.db.index.id
  5. 属性数据的键值数据存储文件及其序列Id
    • neostore.propertystore.db.index.keys 存储 property (key-value
      结构)的key 的字符串值。
    • neostore.propertystore.db.index.keys.id

其他的文件

  1. 存储版本信息
    • neostore
    • neostore.id
  2. 存储 schema 数据
    • neostore.schemastore.db
    • neostore.schemastore.db.id

neo4j
中,主要有4类节点,属性,关系等文件是以数组作为核心存储结构;同时对节点,属性,关系等类型的每个数据项都会分配一个唯一的ID,在存储时以该ID
为数组的下标。这样,在访问时通过其ID作为下标,实现快速定位。所以在图遍历等操作时,可以实现
free-index。

store
部分类图

CommonAbstractStore

CommonAbstractStore 是所有 Store 类的基类,下面的代码片段是
CommonAbstractStore
的成员变量,比较重要的是飘红的几个,特别是IdGenerator,每种Store
的实例都有自己的 id 分配管理器; StoreChannel
是负责Store文件的读写和定位;WindowsPool 是与Store
Record相关的缓存,用来提升性能的。

public abstract class CommonAbstractStore<RECORD extends AbstractBaseRecord,HEADER extends StoreHeader>
        implements RecordStore<RECORD>, AutoCloseable
{
    static final String UNKNOWN_VERSION = "Unknown";

    protected final Config configuration;
    protected final PageCache pageCache;
    protected final IdType idType;
    protected final IdGeneratorFactory idGeneratorFactory;
    protected final Log log;
    protected final String storeVersion;
    protected final RecordFormat<RECORD> recordFormat;
    final File storageFile;
    private final File idFile;
    private final String typeDescriptor;
    protected PagedFile pagedFile;
    protected int recordSize;
    private IdGenerator idGenerator;
    private boolean storeOk = true;
    private RuntimeException causeOfStoreNotOk;

    private final StoreHeaderFormat<HEADER> storeHeaderFormat;
    private HEADER storeHeader;

    private final OpenOption[] openOptions;

:::

neo4j 的db文件及对应的存储格式类型


文件名 文件存储格式


neostore.labeltokenstore.db LabelTokenStore(TokenStore)

neostore.labeltokenstore.db.id ID 类型

neostore.labeltokenstore.db.names StringPropertyStore (AbstractDynamicStore,
NAME_STORE_BLOCK_SIZE = 30)

neostore.labeltokenstore.db.names.id ID 类型

neostore.nodestore.db NodeStore

neostore.nodestore.db.id ID 类型

neostore.nodestore.db.labels ArrayPropertyStore
(AbstractDynamicStorelabel_block_size=60)

neostore.nodestore.db.labels.id ID 类型

neostore.propertystore.db PropertyStore

neostore.propertystore.db.arrays ArrayPropertyStore
(AbstractDynamicStorearray_block_size=120)

neostore.propertystore.db.arrays.id ID 类型

neostore.propertystore.db.id ID 类型

neostore.propertystore.db.index PropertyIndexStore

neostore.propertystore.db.index.id ID 类型

neostore.propertystore.db.index.keys StringPropertyStore (AbstractDynamicStore,
NAME_STORE_BLOCK_SIZE = 30)

neostore.propertystore.db.index.keys.id ID 类型

neostore.propertystore.db.strings StringPropertyStore
(AbstractDynamicStorestring_block_size=120)

neostore.propertystore.db.strings.id ID 类型

neostore.relationshipgroupstore.db RelationshipGroupStore

neostore.relationshipgroupstore.db.id ID 类型

neostore.relationshipstore.db RelationshipStore

neostore.relationshipstore.db.id ID 类型

neostore.relationshiptypestore.db RelationshipTypeTokenStore(TokenStore)

neostore.relationshiptypestore.db.id ID 类型

neostore.relationshiptypestore.db.names StringPropertyStore (AbstractDynamicStore,
NAME_STORE_BLOCK_SIZE = 30)

neostore.relationshiptypestore.db.names.id ID 类型

neostore.schemastore.db SchemaStore(AbstractDynamicStore, BLOCK_SIZE =
56)

neostore.schemastore.db.id ID 类型

通用的Store 类型

下面是 neo4j db 中,每种Store都有自己的ID文件(即后缀.id
文件),它们的格式都是一样的。

::: {#cb7 .sourceCode}

[root@localhost graph.db]# ll | grep .id
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.id
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.labeltokenstore.db.id
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.labeltokenstore.db.names.id
-rw-r--r-- 1 root root    297 5月  28 09:30 neostore.nodestore.db.id
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.nodestore.db.labels.id
-rw-r--r-- 1 root root    137 5月  28 09:30 neostore.propertystore.db.arrays.id
-rw-r--r-- 1 root root    265 5月  28 09:30 neostore.propertystore.db.id
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.propertystore.db.index.id
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.propertystore.db.index.keys.id
-rw-r--r-- 1 root root     89 5月  28 09:30 neostore.propertystore.db.strings.id
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.relationshipgroupstore.db.id
-rw-r--r-- 1 root root    273 5月  28 09:30 neostore.relationshipstore.db.id
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.relationshiptypestore.db.id
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.relationshiptypestore.db.names.id
-rw-r--r-- 1 root root      9 5月  28 09:30 neostore.schemastore.db.id

:::

ID类型文件的存储格式

neo4j 中后缀为 ".id"的文件格式如上图所示,由文件头(9 Bytes)和 long类型
数组 2部分构成:

  • sticky(1 byte) : if sticky the id generator wasn't closed properly
    so it has to berebuilt (go through the node, relationship, property,
    rel type etc files).
  • nextFreeId(long) :
    保存最大的ID,该值与对应类型的存储数组的数组大小相对应。
  • reuseId(long):用来保存已经释放且可复用的ID值。通过复用ID
    ,可以减少资源数组的空洞,提高磁盘利用率。

IdGeneratorImpl.java

原先,每一种资源类型的ID 分配 neo4j 中是通过 IdGeneratorImpl
来实现的,其功能是负责ID管理分配和回收复用。对于节点,关系,属性等每一种资源类型,都可以生成一个IdGenerator
实例来负责其ID管理分配和回收复用。

当前版本(v3.5)有所变动,应该是将原 IdGeneratorImpl 拆成了 IdGeneratorImpl
和 IdContainer .

FreeIdKeeper.java

/**
 * Instances of this class maintain a list of free ids with the potential of overflowing to disk if the number
 * of free ids becomes too large. This class has no expectations and makes no assertions as to the ids freed.
 * Such consistency guarantees, for example uniqueness of values, should be imposed from users of this class.
 * <p>
 * There is no guarantee as to the ordering of the values returned (i.e. FIFO, LIFO or any other temporal strategy),
 * primarily because the aggressiveMode argument influences exactly that behaviour.
 * <p>
 * The {@link #aggressiveMode} parameter controls whether or not IDs which are freed during this lifecycle will
 * be allowed to be reused during the same lifecycle. The alternative non-aggressive behaviour is that the IDs
 * will only be reused after a close/open cycle. This would generally correlate with a restart of the database.
 */

:::

此类的实例维护一个可用ID列表,如果可用ID的数量太大,则可能溢出到磁盘。
此类没有期望,也没有对释放的ID做出断言。
一致性应有用户保证(诸如值的唯一性)

对于返回值的顺序(即FIFO、LIFO或任何其他时间策略)没有任何保证,主要是因为参数
aggressiveMode 影响. 参数 aggressiveMode 控制了在生命周期中已经被释放的
ID 是否会在同个生命周期中被复用.
另一种非激进行为是指,ID只在一个周期关闭/打开后才重新使用.这通常与重新启动数据库接近。

IDContainer.java

/**
 * This class handles the persisting of a highest id in use. A sticky byte is present in the header to indicate
 * whether the file was closed properly. It also handel delegation of reusable ids to the {@link FreeIdKeeper}
 * class.
 *
 * This class is <b>not thread-safe</b> and synchronization need to be handed by the caller.
 */

:::

此类处理使用中的最高ID的持久化。 头部中的 sticky byte
指示文件是否正确关闭. 它还将可重用ID委托给 freeidmeeper 类.

此类是 非线程安全 的,同步需要由调用者进行处理。

IdGeneratorImpl.java
/**
 * This class generates unique ids for a resource type. For example, nodes in a
 * nodes space are connected to each other via relationships. On nodes and
 * relationship one can add properties. We have three different resource types
 * here (nodes, relationships and properties) where each resource needs a unique
 * id to be able to differ resources of the same type from each other. Creating
 * three id generators (one for each resource type ) will do the trick.
 * <p>
 * <CODE>IdGenerator</CODE> makes use of so called "defragged" ids. A
 * defragged id is an id that has been in use one or many times but the resource
 * that was using it doesn't exist anymore. This makes it possible to reuse the
 * id and that in turn makes it possible to write a resource store with fixed
 * records and size (you can calculate the position of a record by knowing the
 * id without using indexes or a translation table).
 * <p>
 * The id returned from {@link #nextId} may not be the lowest
 * available id but will be one of the defragged ids if such exist or the next
 * new free id that has never been used.
 * <p>
 * The {@link #freeId} will not check if the id passed in to it really is free.
 * Passing a non free id will corrupt the id generator and {@link #nextId}
 * method will eventually return that id.
 * <p>
 * The {@link #close()} method must always be invoked when done using an
 * generator (for this time). Failure to do will render the generator as
 * "sticky" and unusable next time you try to initialize a generator using the
 * same file. There can only be one id generator instance per id generator file.
 * <p>
 * In case of disk/file I/O failure an <CODE>IOException</CODE> is thrown.
 */

:::

此类为资源类型生成唯一的ID.例如,节点空间中的节点(node)通过关系(relationship)相连.节点和关系上都可以添加属性(properties).
这里有3种不同的资源类型(节点,关系,属性),每种资源类型都需要唯一的 ID
以和同类资源区分开来. 创建 3 种 ID
生成器(Idgenerator)(每类资源1种)以实现.

IdGenerator 使用所谓的'碎片'id.碎片 id 指的是一类
id,它们被使用过一次或者多次,但使用其的资源已不存在. 这使得重用 id
成为可能,反过来,可以写入固定记录和大小的资源存储(不使用索引和转换表,只需要知道id就可以计算记录的地址).

从 {[@link]{.citation cites="link"} #nextId}
返回的ID可能不是最低的可用ID,但将是碎片ID之一(如果存在)或下一个从未使用过的新空闲ID。

{[@link]{.citation cites="link"} freeId}不检查传递给它的 ID
是否真的是空闲.传非空闲ID将损坏ID生成器,{[@link]{.citation
cites="link"} nextid}方法最终将返回该ID。

每次调用完生成器(generator)后,总是要调用{[@link]{.citation
cites="link"}
#close()}方法.否则将使生成器'粘滞',这样下次你试图用同一个文件初始化生成器时就不可用.
每个id 生成器文件只能有一个id生成器实例.

如果磁盘/文件的I/O出现故障,将抛出一个 IOException

读取id 文件进行初始化

public IdContainer( FileSystemAbstraction fs, File file, int grabSize, boolean aggressiveReuse )
{
    if ( grabSize < 1 )
    {
        throw new IllegalArgumentException( "Illegal grabSize: " + grabSize );
    }

    this.file = file;
    this.fs = fs;
    this.grabSize = grabSize;
    this.aggressiveReuse = aggressiveReuse;
}

public boolean init()
{
    boolean result = true;
    try
    {
        if ( !fs.fileExists( file ) )
        {
            createEmptyIdFile( fs, file, 0, false );
            result = false;
        }

        fileChannel = fs.open( file, OpenMode.READ_WRITE );
        initialHighId = readAndValidateHeader();
        markAsSticky();

        this.freeIdKeeper = new FreeIdKeeper( new OffsetChannel( fileChannel, HEADER_SIZE ), grabSize, aggressiveReuse );
        closed = false;
    }
    catch ( IOException e )
    {
        throw new UnderlyingStorageException( "Unable to init id file " + file, e );
    }
    return result;
}

:::

/***************************
FreeIdKeeper.java
*****************************/
/*
 * After this method returns, if there were any entries found, they are placed in the readFromDisk list.
 */
private void readIdBatch()
{
    try
    {
        readIdBatch0();
    }
    catch ( IOException e )
    {
        throw new UnderlyingStorageException( "Failed reading free id batch", e );
    }
}

private void readIdBatch0() throws IOException
{
    if ( stackPosition == 0 )
    {
        return;
    }

    long startPosition = max( stackPosition - batchSize * ID_ENTRY_SIZE, 0 );
    int bytesToRead = toIntExact( stackPosition - startPosition );
    ByteBuffer readBuffer = ByteBuffer.allocate( bytesToRead );

    channel.position( startPosition );
    channel.readAll( readBuffer );
    stackPosition = startPosition;

    readBuffer.flip();
    int idsRead = bytesToRead / ID_ENTRY_SIZE;
    for ( int i = 0; i < idsRead; i++ )
    {
        long id = readBuffer.getLong();
        readFromDisk.enqueue( id );
    }
    if ( aggressiveMode )
    {
        truncate( startPosition );
    }
}

:::

释放id(freeId)

/********* 
IdGeneratorImpl.java 
**********/
public synchronized void freeId( long id )
{
    idContainer.assertStillOpen();

    if ( IdValidator.isReservedId( id ) )
    {
        return;
    }

    if ( id < 0 || id >= highId )
    {
        throw new IllegalArgumentException( "Illegal id[" + id + "], highId is " + highId );
    }
    idContainer.freeId( id );
}

/********* 
IdContainer.java 
**********/
public void freeId( long id )
{
    freeIdKeeper.freeId( id );
}

/**********************
FreeIdKeeper.java
**********************/
public void freeId( long id )
{
    freeIds.enqueue( id );
    freeIdCount++;

    if ( freeIds.size() >= batchSize )
    {
        long endPosition = flushFreeIds( ByteBuffer.allocate( batchSize * ID_ENTRY_SIZE ) );
        if ( aggressiveMode )
        {
            stackPosition = endPosition;
        }
    }
}

:::

申请id ( nextId)

当用户申请一个 id 时,IdGeneratorImpl
在分配时,有2种分配策略:"正常的分配策略"
和"激进分配策略"(aggressiveMode),可以根据配置进行选择。

/**
 * Returns the next "free" id. If a defragged id exist it will be returned
 * else the next free id that hasn't been used yet is returned. If no id
 * exist the capacity is exceeded (all values <= max are taken) and a
 * {@link UnderlyingStorageException} will be thrown.
 *
 * @return The next free id
 * @throws UnderlyingStorageException
 *             If the capacity is exceeded
 * @throws IllegalStateException if this id generator has been closed
 */
@Override
public synchronized long nextId()
{
    assertStillOpen();
    long nextDefragId = idContainer.getReusableId();
    if ( nextDefragId != IdContainer.NO_RESULT )
    {
        return nextDefragId;
    }

    if ( IdValidator.isReservedId( highId ) )
    {
        highId++;
    }
    IdValidator.assertValidId( idType, highId, max );
    return highId++;
}


/*********************
IdContainer.java
**********************/
/**
 * @return next free id or {@link IdContainer#NO_RESULT} if not available
 */
public long getReusableId()
{
    return freeIdKeeper.getId();
}

/***************************
FreeKeeper.java
****************************/
public long getId()
{
    long result;
    if ( freeIds.size() > 0 && aggressiveMode )
    {
        result = freeIds.dequeue();
        freeIdCount--;
    }
    else
    {
        result = getIdFromDisk();
        if ( result != NO_RESULT )
        {
            freeIdCount--;
        }
    }
    return result;
}

private long getIdFromDisk()
{
    if ( readFromDisk.isEmpty() )
    {
        readIdBatch();
    }
    if ( !readFromDisk.isEmpty() )
    {
        return readFromDisk.dequeue();
    }
    else
    {
        return NO_RESULT;
    }
}

:::

  • 激进分配策略(aggressiveMode)
    1. freeIds (刚回收的ID Queue)中分配。
    2. getIdFromDisk()
  • 正常的分配策略
    1. getIdFromDisk()

其中, getIdFromDisk() 从 readFromDisk 文件中读取已释放且可复用的 id.
没有已释放且可复用的 id了,则分配全新的id.

DynamicStore 类型

AbstractDynamicStore 的存储格式

/**
 * An abstract representation of a dynamic store. Record size is set at creation as the contents of the
 * first record and read and used when opening the store in future sessions.
 * <p>
 * Instead of a fixed record this class uses blocks to store a record. If a
 * record size is greater than the block size the record will use one or more
 * blocks to store its data.
 * <p>
 * A dynamic store don't have a {@link IdGenerator} because the position of a
 * record can't be calculated just by knowing the id. Instead one should use
 * another store and store the start block of the record located in the
 * dynamic store. Note: This class makes use of an id generator internally for
 * managing free and non free blocks.
 * <p>
 * Note, the first block of a dynamic store is reserved and contains information
 * about the store.
 * <p>
 * About configuring block size: Record size is the whole record size including the header (next pointer
 * and what not). The term block size is equivalent to data size, which is the size of the record - header size.
 * User configures block size and the block size is what is passed into the constructor to the store.
 * The record size is what's stored in the header (first record). {@link #getRecordDataSize()} returns
 * the size which was configured at the store creation, {@link #getRecordSize()} returns what the store header says.
 */

:::

动态存储的抽象表示。记录大小在创建时设置,为在以后的会话中打开存储时,首先记录并读取并使用。
此类使用块来存储记录,而不是固定记录。

如果记录大小大于块大小,则记录将使用一个或多个块来存储其数据。

动态存储没有{[@link]{.citation cites="link"}
IdGenerator},因为不能只通过ID来计算记录。应该使用另一个存储,存储储位于动态存储中的记录的起始块。
注意:此类在内部使用 Id生成器管理空闲和非空闲块。

注意,动态存储的第一个块被保留,并包含有关存储的信息。

关于配置块大小:记录大小是包括头(下一个指针等等)在内的整个记录大小。术语块大小等价于数据大小,数据大小是记录头大小。
户配置块大小,块大小是传递到构造函数到存储的。
记录大小是存储在头文件中的内容(第一个记录)。 {[@link]{.citation
cites="link"}
#getRecordDataSize()}返回在创建存储时配置的大小,{[@link]{.citation
cites="link"} #getRecordSize()}返回存储头的内容。

AbstractDynamicStore 类对应的存储文件格式如上图所示, 整个文件是有一个
block_size=BLOCK_HEADER_SIZE(8Bytes)+block_content_size
的定长数组和一个字符串 "StringPropertyStore v0.A.2" 或
"ArrayPropertyStore v0.A.2"或"SchemaStore v0.A.2"
(文件类型描述TYPE_DESCRIPTOR和 neo4j 的 ALL_STORES_VERSION构成)。
访问时,可以通过 id 作为数组的下标进行访问。其中,文件的第1个 record
中前4 字节用来保存 block_size。文件的第2个
record开始保存实际的block数据,它由8个字节的block_header和定长的
block_content(可配置)构成. block_header 结构如下:

  • inUse(1 Byte):第1字节,共分成3部分 [x__ , ] 0: start record, 1:
    linked record [ x, ] inUse [ ,xxxx] high next block bits
    • 第1~4 bit 表示next_block 的高4位
    • 第5 bit表示block 是否在 use;
    • 第8 bit 表示 block 是否是单向链表的第1个 block;0表示第1个block,
      1表示后续 block.
  • nr_of_bytes(3Bytes):本 block 中保存的数据的长度。
  • next_block(4Bytes): next_block 的低 4 个字节,加上 inUse 的第1~4
    位,next_block 的实际长度共 36
    bit。以数组方式存储的单向链表的指针,指向保存同一条数据的下一个
    block 的id.

类DynamicArrayStore, DynamicStringStore

类SchemaStore,DynamicArrayStore(ArrayPropertyStore),
DynamicStringStore(StringPropertyStore)都是继承成自类AbstractDynamicStore,所以与类DynamicArrayStore,
DynamicStringStore和
SchemaStore对应文件的存储格式,都是遵循AbstractDynamicStore的存储格式,除了block块的大小(block_size)不同外。


db 文件 存储类型 block_size


neostore.labeltokenstore.db.names StringPropertyStore NAME_STORE_BLOCK_SIZE=30

neostore.propertystore.db.index.keys StringPropertyStore NAME_STORE_BLOCK_SIZE=30

neostore.rela tionshiptypestore.db.names StringPropertyStore

neostore.propertystore.db.strings StringPropertyStore string_block_size=120

neostore.nodestore.db.labels ArrayPropertyStore label_block_size=60

neostore.propertystore.db.arrays ArrayPropertyStore array_block_size=120

neostore.schemastore.db SchemaStore BLOCK_SIZE=56

block_size
通过配置文件或缺省值来设置的,下面的代码片段展示了neostore.propertystore.db.strings
文件的创建过程及block_size 的大小如何传入。

GraphDatabaseSettings.java
/**
 * Block size properties values depends from selected record format.
 * We can't figured out record format until it will be selected by corresponding edition.
 * As soon as we will figure it out properties will be re-evaluated and overwritten, except cases of user
 * defined value.
 */
@Description( "Specifies the block size for storing strings. This parameter is only honored when the store is " +
        "created, otherwise it is ignored. " +
        "Note that each character in a string occupies two bytes, meaning that e.g a block size of 120 will hold " +
        "a 60 character long string before overflowing into a second block. " +
        "Also note that each block carries a ~10B of overhead so record size on disk will be slightly larger " +
        "than the configured block size" )
@Internal
public static final Setting<Integer> string_block_size = buildSetting( "unsupported.dbms.block_size.strings", INTEGER,
        "0" ).constraint( min( 0 ) ).build();

@Description( "Specifies the block size for storing arrays. This parameter is only honored when the store is " +
        "created, otherwise it is ignored. " +
        "Also note that each block carries a ~10B of overhead so record size on disk will be slightly larger " +
        "than the configured block size" )
@Internal
public static final Setting<Integer> array_block_size = buildSetting( "unsupported.dbms.block_size.array_properties",
        INTEGER, "0" ).constraint( min( 0 ) ).build();

@Description( "Specifies the block size for storing labels exceeding in-lined space in node record. " +
        "This parameter is only honored when the store is created, otherwise it is ignored. " +
        "Also note that each block carries a ~10B of overhead so record size on disk will be slightly larger " +
        "than the configured block size" )
@Internal
public static final Setting<Integer> label_block_size = buildSetting( "unsupported.dbms.block_size.labels", INTEGER,
        "0" ).constraint( min( 0 ) ).build();

@Description( "Specifies the size of id batches local to each transaction when committing. " +
        "Committing a transaction which contains changes most often results in new data records being created. " +
        "For each record a new id needs to be generated from an id generator. " +
        "It's more efficient to allocate a batch of ids from the contended id generator, which the transaction " +
        "holds and generates ids from while creating these new records. " +
        "This setting specifies how big those batches are. " +
        "Remaining ids are freed back to id generator on clean shutdown." )
@Internal
public static final Setting<Integer> record_id_batch_size = buildSetting( "unsupported.dbms.record_id_batch_size", INTEGER,
        "20" ).constraint( range( 1, 1_000 ) ).build();

:::

NeoStores.java

CommonAbstractStore createPropertyStringStore()
{
    return createDynamicStringStore( layout.propertyStringStore(), layout.idPropertyStringStore(), IdType.STRING_BLOCK,
            GraphDatabaseSettings.string_block_size );
}

CommonAbstractStore createPropertyArrayStore()
{
    return createDynamicArrayStore( layout.propertyArrayStore(), layout.idPropertyArrayStore(), IdType.ARRAY_BLOCK,
            GraphDatabaseSettings.array_block_size );
}

:::

Property 的存储

下面是neo4j graph db 中,Property数据存储对应的文件:


neostore.propertystore.db PropertyStore

neostore.propertystore.db.arrays ArrayPropertyStore
(AbstractDynamicStorearray_block_size=120)

neostore.propertystore.db.arrays.id ID 类型

neostore.propertystore.db.id ID 类型

neostore.propertystore.db.index PropertyIndexStore

neostore.propertystore.db.index.id ID 类型

neostore.propertystore.db.index.keys StringPropertyStore (AbstractDynamicStore,
NAME_STORE_BLOCK_SIZE = 30)

neostore.propertystore.db.index.keys.id ID 类型

neostore.propertystore.db.strings StringPropertyStore
(AbstractDynamicStorestring_block_size=120)

neostore.propertystore.db.strings.id ID 类型


参考资料

posted @ 2019-05-28 17:09  菁芜  阅读(2245)  评论(2编辑  收藏  举报