在RedisGraph的整体架构中,非常简略的概括了RedisGraph的图存储模型:
DataBlock是如何存储node和edge的属性的?使用稀疏矩阵表示图的具体设计有哪些?本文关注RedisGraph的图存储模型。
RedisGraph中的Graph数据结构定义如下:
struct Graph {
DataBlock *nodes; // graph nodes stored in blocks
DataBlock *edges; // graph edges stored in blocks
RG_Matrix adjacency_matrix; // adjacency matrix, holds all graph connections
RG_Matrix *labels; // label matrices
RG_Matrix node_labels; // mapping of all node IDs to all labels possessed by each node
RG_Matrix *relations; // relation matrices
RG_Matrix _zero_matrix; // zero matrix
pthread_rwlock_t _rwlock; // read-write lock scoped to this specific graph
bool _writelocked; // true if the read-write lock was acquired by a writer
SyncMatrixFunc SynchronizeMatrix; // function pointer to matrix synchronization routine
GraphStatistics stats; // graph related statistics
};
RedisGraph中存储一张属性图。
属性图由node、relation和property(属性)组成。relation在图中对应edge,node和edge均存在属性(键值对)。
RedisGraph使用DataBlock来存储node和edge的属性。
RedisGraph使用RG_Matrix来表示图。
DataBlock是一种容器数据结构,用于存储同一类型的item。简单理解,DataBlock是一种数组,可以根据index对Block进行查询,只不过是,这个数组设计比较复杂,需要支持高效率的增删查改以及resize操作。
在RedisGraph中,DataBlock的用途之一是存储node和edge的属性(key-value键值对,比如, name: redis-server, ip: 127.0.0.1)。
首先,我们看一下DataBlock这个数据结构的定义:
typedef struct {
uint64_t itemCount; // Number of items stored in datablock.
uint64_t itemCap; // Number of items datablock can hold.
uint64_t blockCap; // Number of items a single block can hold.
uint blockCount; // Number of blocks in datablock.
uint itemSize; // Size of a single item in bytes.
Block **blocks; // Array of blocks.
uint64_t *deletedIdx; // Array of free indicies.
fpDestructor destructor; // Function pointer to a clean-up function of an item.
} DataBlock;
typedef struct Block {
size_t itemSize; // Size of a single item in bytes.
struct Block *next; // Pointer to next block.
unsigned char data[]; // Item array. MUST BE LAST MEMBER OF THE STRUCT!
} Block;
DataBlock要求每一个item的size相同,一般只存储同一种类型的item。
每一个item的第一个bit,用于表示item是否被删除,itemSize = sizeof(item)+1。
blockCount 表示为block的数量。blockCount = itemCap/blockCap。
当DataBlock的容量不足的时候,就会对DataBlock扩容,增加一个Block。此时,会realloc()
blocks数组,将新增的Block指针加到blocks数组末尾。仅支持扩容,不支持缩容。
deletedIdx是一个队列,用于暂时存放被释放的item的index。当需要分配新的item的空间时,则优先从deletedIdx取空闲的item的index。
Block中存在指向下一个Block的指针。目的是,方便对DataBlock进行遍历操作。
查询的时间复杂度为O(1)。
对于查询DataBlock中第idx个item:
Block *block = idx / dataBlock->blockCap;
idx = idx % dataBlock->blockCap;
void* target_item = block->data + (idx * block->itemSize);
总的来说,DataBlock是一个增删查改均十分高效的容器数据结构。
在RedisGraph中,使用DataBlock存储node和edge的属性。
首先,我们先来看一下Graph_New()
中有关创建DataBlock的语句。
#define NODE_CREATION_BUFFER_DEFAULT 16384
Graph *Graph_New
(
size_t node_cap,
size_t edge_cap
) {
.......
g->nodes = DataBlock_New(node_cap, node_cap, sizeof(AttributeSet), cb);
g->edges = DataBlock_New(edge_cap, edge_cap, sizeof(AttributeSet), cb);
.......
}
在上文中提到了,RedisGraph使用DataBlock存储node和edge的属性。其实,这个说法是不准确的,DataBlock中存储的是指向node和edge属性的指针。毕竟,node和edge的属性(key-value键值对)是不定长的,而指向node和edge属性的指针是定长的。
我们来看一下AttributeSet
的定义:
typedef unsigned short Attribute_ID;
typedef struct {
Attribute_ID id; // attribute identifier
SIValue value; // attribute value
} Attribute;
typedef struct {
ushort attr_count; // number of attributes
Attribute attributes[]; // key value pair of attributes
} _AttributeSet;
typedef _AttributeSet* AttributeSet;
RedisGraph使用稀疏矩阵来表示图,稀疏矩阵的存储格式为按行压缩的稀疏矩阵(Compressed Sparse Row Matrix, CSR_Matrix)。实际上,RedisGraph并没有完全自己实现矩阵的存储代码,而是对GraphBLAS中的GrB_Matrix进行了一下封装。
我们还是先从代码入手,对RG_Matrix进行分析。
struct _RG_Matrix {
bool dirty; // Indicates if matrix requires sync
GrB_Matrix matrix; // Underlying GrB_Matrix
GrB_Matrix delta_plus; // Pending additions
GrB_Matrix delta_minus; // Pending deletions
RG_Matrix transposed; // Transposed matrix
pthread_mutex_t mutex; // Lock
};
typedef struct _RG_Matrix _RG_Matrix;
typedef _RG_Matrix *RG_Matrix;
RedisGraph使用GraphBLAS中的GrB_Matrix进行构造RG_Matrix矩阵。RG_Matrix使用了GrB_Matrix的两种存储格式:稀疏矩阵(GxB_SPARSE)和超稀疏矩阵(GxB_HYPERSPARSE)。这两种存储格式将在3.2节中具体介绍。
matrix为基础(主)矩阵
其存储格式为GxB_SPARSE或GxB_HYPERSPARSE。
当matrix中非零元素数量达到一定的阈值时,会由GxB_HYPERSPARSE转变为GxB_SPARSE。
高效的插入和删除
对于一些存储图关系的矩阵,RedisGraph会存储其转置矩阵,方便图查询。
(N0)-[A]->(N1)-[B]->(N2)<-[A]-(N3)
被用作查询的一部分时,需要使用A的转置矩阵Transpose(A)。在RG_Matrix进行以下操作前,需要先获取锁mutex
4.1节中提到,RG_Matrix使用了GrB_Matrix的两种存储格式:稀疏矩阵(GxB_SPARSE)和超稀疏矩阵(GxB_HYPERSPARSE)。
稀疏矩阵(GxB_SPARSE)
index pointrs
存储每一行数据元素的起始位置indices
这是存储每行中数据的列号,与data
中的元素一一对应。超稀疏矩阵(GxB_HYPERSPARSE)
我们还是先从代码入手:
struct Graph {
RG_Matrix adjacency_matrix; // adjacency matrix, holds all graph connections
RG_Matrix *labels; // label matrices
RG_Matrix node_labels; // mapping of all node IDs to all labels possessed by each node
RG_Matrix *relations; // relation matrices
};
Graph
数据结构维护4种矩阵:
矩阵均为NxN的方阵,N为顶点数。矩阵中的row、col对应上图中的NodeID
adjacency_matrix
邻接矩阵会标记图中的所有关系连接,关系类型不可知。
一张图仅对应一个adjacency_matrix
A d j = [ 0 1 1 1 0 0 1 0 0 1 0 1 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 ] Adj = \begin{bmatrix} 0 & 1 & 1 & 1 & 0 & 0 & 1\\ 0 & 0 & 1 & 0 & 1 & 1 & 0\\ 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 & 0 & 0 & 0 \end{bmatrix} Adj= 0000101100101011000001000000010000001000001000000
labels
为了适应类型化节点,每个标签分配一个额外的矩阵,并且标签矩阵与沿主对角线的矩阵对称。
上图中存在4种类型的节点,也就存在4个label矩阵:city、person、post和comment。仅展示comment矩阵。
c o m m e n t = [ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 ] comment = \begin{bmatrix} 0 & 0 & 0 & 0 & 0 & 0 & 0\\ 0 & 0 & 0 & 0 & 0 & 0 & 0\\ 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 \end{bmatrix} comment= 0000000000000000000000001000000010000000100000000
为什么要设置label矩阵?
当匹配(person)-[like]->(comment)
这个查询时,不仅仅会查询出person喜欢的comment,还会查询出post,这时候就需要comment矩阵过滤出最后的comment节点。
node_labels
在一些场景中,一个节点可能会对应多个label。每一个label也会有一个label_ID。
node_label矩阵是一个映射:将所有节点ID映射到每个节点拥有的所有label
一张图仅对应一个node_label矩阵
假设:city的label_ID为0,person的label_ID为1,comment的label_ID为2,post的label_ID为3,node_label矩阵如下:
c o m m e n t = [ 0 1 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 ] comment = \begin{bmatrix} 0 & 1 & 0 & 0 & 0 & 0 & 0\\ 0 & 1 & 0 & 0 & 0 & 0 & 0\\ 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 \end{bmatrix} comment= 0010000110000000011100000001000000000000000000000
relations
每个类型的关系都有自己的专用矩阵。
上图中存在4种类型的关系,也就存在4个relation矩阵:located、friend、hasCreator和like。仅展示like矩阵。
l i k e = [ 0 0 0 0 0 0 1 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ] like = \begin{bmatrix} 0 & 0 & 0 & 0 & 0 & 0 & 1\\ 0 & 0 & 0 & 1 & 1 & 1 & 0\\ 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 \end{bmatrix} like= 0000000000000000000000100000010000001000001000000