大家好,我是洋仔,JanusGraph圖解系列文章,實時更新
~
圖數據庫文章總目錄:
- 整理所有圖相關文章,請移步(超鏈):圖數據庫系列-文章總目錄
- 地址:https://liyangyang.blog.csdn.net/article/details/111031257
源碼分析相關可查看github(求star~~)
: https://github.com/YYDreamer/janusgraph
下述流程高清大圖地址:https://www.processon.com/view/link/5f471b2e7d9c086b9903b629
版本:JanusGraph-0.5.2
轉載文章請保留以下聲明:
作者:洋仔聊編程 微信公衆號:匠心Java 原文地址:https://liyangyang.blog.csdn.net/
一:存儲模式
留言或私信我,邀請你加入“圖數據庫交流”微信羣!
1、圖內容
本文以下所有內容基於:JanusGraph基於屬性圖
來進行構造圖數據:
屬性圖: 屬性圖是由 頂點(Vertex),邊(Edge),屬性(Property)組成的有向圖
Vertex可以包含Properties;Edge也可以包含Properties;
2、存儲方法
圖存儲的方式常用的有兩種:鄰接列表
和 鄰接矩陣
JanusGraph採用鄰接列表
進行圖數據的存儲,如下圖所示:(此處將圖中節點抽象爲 只有節點,沒有屬性)
在Janusgraph中一個頂點的鄰接列表包含該節點對應的屬性
和關聯的邊
,下述會詳細說明 Janusgraph中鄰接列表是如何實現的;
3、圖切割方式
圖的切割方式分爲兩種:按節點切割(Vertex Cut)
和按邊切割(Edge Cut)
- Vertex Cut:根據點進行切割,每個邊只存儲一次,只要是節點對應的邊便會多一份該節點的存儲
- Edge Cut:根據邊進行切割,以節點爲中心,邊會存儲兩次,源節點的鄰接列表存儲一次,目標節點的鄰接列表存儲一次
在Janusgraph中既存在Edge Cut,也存在Vertex Cut的情況;
在默認的情況下使用邊切割
,而針對熱點
節點可以通過配置makeVertexLabel('product').partition()
來將節點類型爲product
類型的節點進行Vertex Cut
;
也就是說,在沒有上述makeVertexLabel('product').partition()
配置的話,JanusGraph所有的圖數據都是以Edge Cut
的方式來進行切割存儲的;
具體可以查看文章:《JanusGraph-分區》中自定義分區部分中關於圖切割部分的介紹;
我們例子來說明一下:
如下圖: 張三
用戶節點通過手機號
關聯出來李四
用戶節點
- 張三 和 李四 代表Vertex;指向的name、age、gender代表張三的屬性
- edgeA 和edgeB 代表Edge;也可以包含邊的屬性,例如下圖中邊包含屬性create_time
按邊切割後:
節點 | ||||
---|---|---|---|---|
張三 | name(property) | age(property) | gender(property) | edgeA(edge) |
phone | phone(property) | edgeA(edge) | edgeB(edge) | |
李四 | name(property) | age(property) | gender(property) | edgeB(edge) |
上述可以看到,按照邊切割後每一條邊會存儲兩次!
二:BigTable模型
在JanusGraph的存儲中, JanusGraph將圖形的鄰接列表的表示存儲在支持Bigtable數據模型的任何存儲後端中
BigTable模型如下圖:
在Bigtable數據模型中,每個表是行的集合,由一個key唯一標識。
每行由任意(可以很大數量但是必須有限數量)數量的cell組成;cell由column和value組成,column唯一標識某一個cell。
上述圖中,有兩部分需要排序的支持:sorted by key
和 sorted by column
:
- sorted by key:標識存儲後端存儲的數據時按照key的大小進行排序存儲的
- sorted by column:這是JanusGraph對Bigtable數據模型有一個額外要求,存儲
edge(邊)
的單元格必須按column排序,並且列範圍指定的單元格子集必須是有效可檢索的; 這句話詳細解答在下述文章中有體現
在Bigtable模型中的行稱爲“寬行”,因爲它們支持大量cell,並且不必像關係數據庫中那樣預先定義這些cell的column。
在關係型數據庫中我們必須先定義好表的schema,纔可以存儲數據,如果存儲過程中想要改變表結構,則所有的數據都要對變化的列做出變化。但是Bigtable模型存儲中就不必如此,每個行的column不同,我們可以隨時僅對某一行進行變化,也不許預先定義行的schema,只需要定義圖的schema即可。
此外,特定的Bigtable實現可以使行按其鍵的順序排序。JanusGraph可以利用這樣的鍵序來有效地劃分圖形,從而爲非常大的圖形提供更好的加載和遍歷性能。
JanusGraph是如何基於BigTable數據模型針對於自身的圖數據特性進行設計的呢?
下面我們看下JanusGraph的邏輯存儲結構
三:存儲邏輯結構
JanusGraph基於使用BigTable模型的存儲後端 實現了自己的存儲的邏輯結構
ps:爲了更好的理解,下面部分知識點會基於HBase存儲後端進行進一步的解釋!
1、整體結構
在JanusGraph中,以節點爲中心,按切邊的方式存儲數據的。比如在Hbase中節點的ID作爲HBase的Rowkey,節點上的每一個屬性和每一條邊,作爲該Rowkey行的一個個獨立的Cell。即每一個屬性、每一條邊,都是一個個獨立的KCV結構(Key-Column-Value)
上圖中,我們可以發現圖的存儲整體分爲三部分:vertex id
、property
、edge
:
-
vertex id: 對應節點的唯一id,如果底層存儲使用的是Hbase則代表着當前行的Rowkey,唯一代表某一個節點
-
property: 代表節點的屬性
-
edge: 代表節點的對應的邊
排序方式分爲三種:sorted by id
、sorted by type
、sorted by sort key
:
-
sorted by id: 依據vertex id在存儲後端進行順序存儲
-
sorted by type:此處的個人理解爲針對於property 和 edge的類型進行排序,保證同種類型的屬性或者邊連續存儲在一塊便於遍歷查找; // TODO 深層次理解
-
sorted by sort key: sort key是邊組成以的一部分,主要作用是,在同種類型的edge下,針對於sort key進行排序存儲,提升針對於指定sort key的檢索速度;下面
edge結構
部分有詳細介紹
2、Vertex id 的結構
此處的Vertex id
唯一標識圖中的某一個節點;節點vertex id的組成結構我們在源碼類IDManager
的一段註釋中可以發現:
/* --- JanusGraphElement id bit format ---
* [ 0 | count | partition | ID padding (if any) ]
*/
這是在Janusgraph在生成所有的id時統一的格式包含vertex id\edge id\property id的時候,這個順序也 就是標識我們再使用gremlin查詢出節點時,節點上標識的vertex id; 這個id值的順序不同於hbase真實存儲Rowkey的順序!!!!!!!
在對vertex id進行序列化存儲時,位置有所調整爲:[ partition | 0 | count | ID padding (if any) ]
如下圖:
從圖中可以看出:
-
Vertex ID共包含一個字節、8位、64個bit
-
Vertex ID由partition id、count、ID padding三部分組成
-
最高位5個bit是partition id。partition是JanusGraph抽象出的一個概念。當Storage Backend是HBase時,JanusGraph會根據partition數量,自動計算並配置各個HBase Region的split key,從而將各個partition均勻映射到HBase的多個Region中。然後通過均勻分配partition id最終實現數據均勻打散到Storage Backend的多臺機器中
-
中間的count部分是流水號,其中最高位比特固定爲0;出去最高位默認的0,count的最大值爲2的(64-5-1-3)=55次冪大小:3 6028 7970 1896 3968,總共可以生成30000兆個id,完全滿足節點的生成
-
最後幾個bit是ID padding, 表示Vertex的類型。具體的位數長度根據不同的Vertex類型而不同。最常用的普通Vertex,其值爲'000'
爲什麼在序列化存儲vertex id
時,需要調整順序序列化作爲RowKey存儲到Hbase呢?
我們通過下面的3個問題來回答:
- 爲什麼JausGraph分配的邏輯區間值,可以影響hbase物理存儲呢? 可以將分區相同的數據存放的更近呢?
在上述描述中,hbase使用vertex id作爲rowkey,hbase根據rowkey順序排序存儲; 每個
hbase region
存儲是一段連續的Rowkey行;在
janusgraph的vertex id
的設計中,可以發現將分區值放到了64位的前5位存儲! 在存儲數據到hbase時,對rowkey進行排序,因爲partition id
在前5位,所以同一個分區的vertex id
對應的rowkey值相差較小,所以會存儲在一塊;
- 如何快速的查詢到不同類型的節點呢? 換個說法如何快速的確定當前的行就是我們需要的節點類型的行呢?
在JanusGraph的vertex id中包含的 ID padding就代表當前的節點類型(注意此處的類型!=lable)。000標識爲普通節點,在id的組成部分中,我們經過前面的分析,最前面是partition id,只有把 ID padding放在最後幾個字節便於查找了;
- 爲什麼查詢出的節點顯示的vertex id要把
0|count
放在最前面、partiton和id padding
放在後面呢?
這裏我們猜測一下:count佔用55位數據! 試想如果把count不放在最前面,那麼id的最小值比2的55次冪還大,顯示不友好! 如果把0|count放在最前面呢?就會有兩個效果:
0在有符號表示中標識當前id始終爲正整數!
count是趨勢遞增的,所以id值也是從小到大趨勢遞增的,所以節點id的最小值在2的8次冪周邊大小; 比把count放在後面顯示的id值友好多了~~~
vertex id是如何保證全局唯一性的呢?
主要是基於數據庫 + 號段
模式進行分佈式id的生成;
體現在圖中就是partition id + count
來保證分佈式全局唯一性; 針對不同的partition
都有自己的0-2的55次冪的範圍的id; 每次要生成vertex id時,首先獲取一個partition,獲取對應partition對應的一組還未使用的id,用來做count;
janusgraph在底層存儲中存儲了對應的partition使用了多少id,從而保證了再生成新的分佈式vertex id時,不會重複生成!
ps : JanusGraph中分佈式唯一vertex id、edge id、property id的生成分析,請看《圖解JanusGraph系列-分佈式唯一id的生成機制》
3、edge 和 property的結構
在上述的JanusGraph的整體結構中,property
和edge
都是作爲cell
存儲在底層存儲中;其中cell又分爲column
和value
兩部分,下圖展示了這兩部分的邏輯結構:
下面我們詳細分析一下 property 和 edge對應的邏輯結構;
3.1 edge的結構
Edge的Column組成部分:
- label id:邊類型代表的id,在創建圖schema的時候janusgraph自動生成的label id,不同於邊生成的唯一全局id
- direction:圖的方向,out:0、in:1
- sort key:可以指定邊的屬性爲sort key,可多個;在同種類型的edge下,針對於sort key進行排序存儲,提升針對於指定sort key的檢索速度;
- 該key中使用的關係類型必須是屬性非唯一鍵或非唯一單向邊標籤;
- 存儲的爲配置屬性的value值,可多個(只存property value是因爲,已經在schema的配置中保存有當前Sort key對應的屬性key了,所以沒有必要再存一份)
- adjacent vertex id:target節點的節點id,其實存儲的是目標節點id和源節點id的差值,這也可以減少存儲空間的使用
- edge id:邊的全局唯一id
Edge的value組成部分:
- signature key:邊的簽名key
- 該key中使用的關係類型必須是屬性非唯一鍵或非唯一單向邊標籤;
- 存儲壓縮後的配置屬性的value值,可多個(只存property value是因爲,已經在schema的配置中保存有當前signature key對應的屬性key了,所以沒有必要再存一份)
- 主要作用提升edge的屬性的檢索速度,將常用檢索的屬性設置爲signature key,提升查找速度
- other properties:邊的其他屬性
- 注意! 不包含配置的sort key和signature key屬性值,因爲他們已經在對應的位置存儲過了,不需要多次存儲!
- 此處的屬性,要插入屬性key label id和屬性value來標識是什麼屬性,屬性值是什麼;
- 此處的property的序列化結構不同於下述所說的vertex節點的property結構,edge中
other properties
這部分存儲的屬性只包含:proeprty key label id + property value;不包含property全局唯一id
!
詳細解釋及思考:
在進行詳細分析前,請大家思考幾個問題,如下:
- 基於上述的edge邏輯結構,JanusGraph是如何構造鄰接列表的 或者 是如何獲取源節點的鄰接節點的?
- 上述的Edge邏輯結構中的,每部分的排列的順序的含義是什麼?
1、基於上述的edge邏輯結構,JanusGraph是如何構造鄰接列表的 或者 是如何獲取源節點的鄰接節點的?
從上述的整體結構
部分中,我們可以知道,vertexId行後跟着當前的節點關聯的所有的edge;
而在上述的edge
的邏輯結構中,有一個adjacent vertex id
字段,通過這個字段就可以獲取到target節點的vertex id,就相當於指向了target節點,整理一下:
如上圖,通過上述的條件,就可以構造一個VertexA指向VertexB 和 VertexC的鄰接鏈表;
其實,JanusGraph可以理解爲構造的是雙向鄰接列表
, 依據上圖,我們知道vertexA 和 vertexB 和 vertexC存在邊關係; 當我們構造vertexB的鄰接列表時,會包含指向vertexA的節點,只是說在edge對應的邏輯結構中邊的方向不同而已:
總結:JanusGraph通過vertex id行中包含所有關聯的edge,edge邏輯結構中包含指向target節點的數據來組成雙向鄰接列表的結構;
2、上述的Edge邏輯結構中的,每部分的排列的順序的含義是什麼?
首先,在查詢的時候爲了提升查詢速度,我們首先要過濾的是什麼,針對於edge毋庸置疑是邊的類型
和邊的方向
;
所以,爲了我們可以更快的拿到類型和方向,所以在edge
的存儲結構中,我們發現作者將類型和方向存放在了column中,並且是column的最前面部分;這樣我們可以直接通過判斷column的第一部分字節就可以對邊類型
和方向
進行過濾!
ps:雖然我們在寫Gremlin語句的時候,可能是語句寫的是先過濾邊的屬性或者其他,但是JanusGraph會針對我們的gremlin語句進行優化爲先過濾
邊類型
和方向
接下來,我們可能對邊的屬性進行過濾,我們怎樣提升經常要過濾的屬性的查詢速度呢? 我們將經常用於範圍查詢的屬性配置爲sort key,然後就可以在過濾完邊類型和方向後快速的進行屬性的範圍過濾(此處快速的指過濾配置爲sort key的屬性);
3.2 property的結構
property的存儲結構十分的簡單,只包含key id
、property id
和value
三部分:
- key id:屬性label對應的id,有創建schema時JanusGraph創建; 不同於屬性的唯一id
- property id:屬性的唯一id,唯一代表某一個屬性
- value:屬性值
注意:屬性的類型包含SINGLE
、LIST
和SET
三種Cardinality;當屬性被設置爲LIST
類型時,因爲LIST
允許當前的節點存在多個相同的屬性kv對,僅通過key id
也就是屬性的label id是無法將相同的屬性label區分出來的
所以在這種情況下,JanusGraph的property
的存儲結構有所變化, property id
也將會被存儲在column
中,如下圖:
四:index存儲結構
1、Composite Index-vertex index結構
圖一(唯一索引Composite Index結構圖):
圖二(非唯一索引Composite Index結構圖):
Rowkey由index label id
和properties value
兩大部分組成:
- index label id:標識當前索引類型
- properties value:索引中包含屬性的所有屬性值,可多個; 存在壓縮存儲,如果超過
16000
個字節,則使用GZIP
對property value進行壓縮存儲!
Column由第一個字節0
和 vertex id
組成:
- 第一個字節0:無論是唯一索引,還是非唯一索引此部分都會存在;如圖一
- vertex id:非唯一索引纔會在column中存在,用於分別多個相同索引值對應的不同節點;如圖二
value由vertex id
組成:
- vertex id:針對於rowkey + column查詢到的value是vertex id,然後通過vertex id查詢對應的節點
2、Composite Index-edge index結構
圖一(唯一索引Composite Index結構圖):
圖二(非唯一索引Composite Index結構圖):
Rowkey同Vertex index部分
Column由第一個字節0
和 edge id
組成:
- 第一個字節0:無論是唯一索引,還是非唯一索引此部分都會存在;如圖一
- edge id:非唯一索引纔會在column中存在,用於分別多個相同索引值對應的不同節點;如圖二
value由以下4部分組成:
- edge id:邊id
- out vertex id:邊對應的出邊id
- type id:edge 的label type id
- in vertex id:邊對應的入邊id
2、Mixed Index結構
這裏以ES
作爲第三方索引庫爲例,這裏只介紹普通的範圍查找的mixed index的構造:
ES的概念爲:index 包含多個 type;每個type包含多個document id,每個document id包含多個field name 和對應的field value;
在Jausgraph
中
-
index:包含兩種,
janusgraph_edge
和janusgraph_vertex
兩種 -
type:可自定義
-
document id:edge id或者 vertex id
-
field name:索引對應屬性的label string
-
field value:屬性對應的property value
基於倒排索引
的查詢順序爲,給定過一個property label 和 property value查詢對應的Vertex id 或者 edge id,則查詢滿足要求的field name 和 field value,就可以獲取到對應的document id即Vertex id 或者 edge id;
五:序列化數據案例
以序列化實例來看下上述所說的整體結構
測試節點數據:
{
"label":"user",
"propertyMap":{
"create_time":"2016-12-09 02:29:26",
"user_name":"張三",
"user_id":"test110"
},
"vertexId":4152
}
測試邊數據:
{
"edgeId":17514510,
"label":"user_login_phone_number",
"propertyMap":{
"productid":"2"
},
"sourceId":4152,
"targetId":40964120
}
跟蹤Janusgraph源碼,獲取其序列化信息,後端存儲使用Hbase
:
節點序列化後數據(不包含索引):
邊序列化後數據(不包含索引):
節點的vertex id序列化後的數據爲56 0 0 0 0 0 0 -128
;一個節點對應的屬性和邊的Rowkey相同,依據qualifier
也就是column來進行區分;
在邊的序列化結果中,包含兩部分:一部分是節點4152
的kcv,一個是節點40964120
的kcv;這地方也可以說明JanusGraph是採用的雙向鄰接鏈表進行圖存儲的
五:Schema的使用
從上述來看,我們可以知道,JanusGraph圖的schema該怎樣定義主要是由edge labels 、property keys 和vertex labels 組成(Each JanusGraph graph has a schema comprised of the edge labels, property keys, and vertex labels used therein)
JanusGraph的schema可以顯式或隱式創建,推薦用戶採用顯式定義的方式。JanusGraph的schema是可以在使用過程中修改的,而且不會導致服務宕機,也不會拖慢查詢速度。
比如一個簡單的顯示定義的銷售圖的scheme:
<propertyKey value="salesman_id" explain="銷售人員id" index="" type="java.lang.String" />
<propertyKey value="real_name" explain="姓名" index="" type="java.lang.String" />
<propertyKey value="role" explain="角色" type="" />
<propertyKey value="city_code" explain="所處城市代碼" index="" type="" />
<propertyKey value="create_time" explain="創建時間" index="" type="" />
<edgeLabel value="saleman_service_for" explain="銷售引導">
<propertys>
<property value="create_time"/>
</propertys>
</edgeLabel>
<edgeLabel value="own_salaman_Idcard" explain="銷售身份">
<propertys>
<property value="create_time"/>
</propertys>
</edgeLabel>
<index elementType="vertex" indexType="compositeIndex" name="salesman_id_I" >
<propertyKeys>
<propertyKey value="salesman_id" />
</propertyKeys>
</index>
<vertexLabel value="salesman" explain="銷售" >
<propertys>
<property value="salesman_id" />
<property value="real_name" />
<property value="role" />
<property value="city_code" />
</propertys>
<edges>
<edge value="saleman_service_for" direction="out" />
<edge value="own_salaman_Idcard" direction="out" />
</edges>
</vertexLabel>
當然,我們也可以添加一些其他的可以組成schema的元素,上述三個是必須的,另外的比如索引(index)等,主要的結構還是:
JanusGraph Schema
|-----------Vertex Lables
|-----------Property Keys
|-----------Edge Labels
和關係型數據庫不同,圖數據的schema是定義一張圖,而非定義一個vertex的。在Mysql中,我們通常將建立一張表定義爲創建一個schema,而在JanusGraph中,一個Graph用於一個schema。
六:源碼分析
源碼分析已經push到github:https://github.com/YYDreamer/janusgraph
七:總結
- JanusGraph採用
Edge cut
的方式進行圖切割,並且按照雙向鄰接列表
的形式進行圖存儲 - JanusGraph每個節點都是對應的kcv結構; vertex id唯一標識節點;對應的行cell存儲節點屬性和對應的邊
- 節點id的分佈式唯一性採用
數據庫+號段
模式進行生成;