Graph DataBase介紹-圖數據庫


前言
分析社會關系這類復雜圖壯結構的海量數據,使用圖形數據庫(Graph DataBase)是最好的選擇。– 作者:李禕

《程序員》介紹各種NoSQL 數據庫的文章已經很多,不過大部分都是基於文檔存儲 (例如mongo DB)或鍵值(key-value) 存儲(例如Redis 和Hbase)的。 本文介紹NoSQL 數據庫中大家不是很熟悉的圖形數據庫(Graph DataBase),它擁有什么特性,以及它適合那些項目。希望開發者閱讀本文后,能從中獲得一些啟發,在今后實施類似項目時多一種技術選擇。

作為NoSQL數據庫的一個類型,圖形數據庫(Graph Database)很長時間都局限在圖理論相關的學術圈子里,在業界使用得並不廣泛。直到近十年電子商務業務模式逐步成熟,電商們需要對用戶在他們網站的購買行為進行分析、發掘用戶潛在喜好的商品,開始大量使用到了圖理論相關的數據挖掘算法,圖形數據庫才從實驗室慢慢走進實際的軟件工程項目中。特別是隨着Facebook為代表的SNS網站興起,對上千萬的用戶行為和關系數據進行挖掘,發掘其商業價值越發成為迫切的需求和工程難題。 工程師們發現使用圖形數據庫來存儲和計算這些海量數據會更為高效和方便, 這極大推動了圖形數據庫的發展和它在NoSQL世界的知名度。 現在業內比較著名的圖形數據庫有Twitter網站的FlockDB , 以及開源項目中的Neo4j 和Infogrid。

Graph Database 的基礎概念
點(Node)表示一個實體,例如人,商品,或是一個賬戶。
邊 (Edge)也稱做關系(relation),表示點和點之間的連接關系,例如用戶A買了商品B。通常邊是有方向性的,例如用戶A購買了商品B,就表示為A->B;如果是用戶A和用戶C互相都認識,這種關系就是雙向的,表示為A<->B。
屬性(propertis)表示點(Node)和邊(Edge)所附帶的信息,例如一個用戶,他帶有的屬性可能是年齡30歲,愛好籃球等等。需要注意的是,每個點的屬性(properties)是動態的,例如同樣是用戶A和C,A有屬性“年齡30歲”,C卻沒有。但是它卻包含屬性“職業工程師”。
把點(Node),邊(Edge),屬性(properties) 概念融合在一起,就可描述出一個圖(graph)。圖1描述了模擬《黑客帝國》的小型社會圖(social graph)。

 

 


(圖1)

其中每個頭像都代表了一個點(Node), 注意Morpheus有的屬性(propertis) rank:Captain, 在其他點都沒有這個屬性。 邊(Edge)用藍色箭頭表示,箭頭方向表明兩者的關系,邊也可擁有屬性(properties),例如Morpheus 認識Trinity 包含屬性 age:12 years。當千萬個點和邊被關聯起來,這張Social Graph就被無限放大,構成了一張描述了整個虛擬社區的人際關系圖。圖形數據庫就是用來存儲這張海量數據關系圖的最優工具,沒有之一。

傳統的關系型數據庫和其他NoSQL數據庫不能最優化的存儲上述社會關系數據。 一方面,每個點包含的屬性是不同的。 如果是在關系型數據庫中建立的表結構,則需要有許多列的大型表,其中很多行的許多列是空的(稀疏表), 查詢時需要聯結大量表、使用深度嵌套的SQL。這導致了拙劣的性能,不適合高性能查詢。另一方面,圖形數據庫針對圖算法,提供了很多高效的操作特性,這也是它在圖計算中優於關系數據庫和其他類型NoSQL數據庫的地方。例如:

遍歷(Traversal): 不同於其他NoSQL數據庫,圖形數據庫支持一系列圖遍歷的操作。例如圖1中,找到Thomas和Architect之間的最短路徑。 通常遍歷(Traversal)操作需要傳入兩個參數,一個是啟始點(star Node),一個是使用何種遍歷規則(traversal specification)。遍歷返回的結果,可能是零個,一個或多個點Node的集合。
事件(Events):這個和關系型數據庫中的觸發器比較類似,當某個事件發生觸發某個操作。不同之處在於,關系數據庫不能將事件通知到數據庫外的應用程序。而對於圖形數據庫來說,它有可編程的監聽接口來供外部程序監聽各類事件。例如圖1中,外部程序可以注冊事件監聽器(Event Listener),當 Smith 的version 屬性從1.0變到5.0時,圖形數據庫將這個事件通知事件監聽器。事件的支持對於管理和維護圖形數據庫非常有用,后面會介紹如何通過事件來解決NoSQL數據庫中常見的數據老化問題。
索引(index):在關系型數據庫中,對某張表進行過多的索引,當數據量大增時,效率就及其地下。而在graph Database中,對所有點的屬性都可以建立索引,即使是千萬級別的數據量,也能提供高效的查詢。
圖形數據庫在數據結構,存儲,遍歷,以及查詢方面都有別於其他NoSQL數據庫。所以,對於社會關系這種復雜圖壯結構的海量數據進行分析,使用圖形數據庫是最好的選擇。不過現在的圖形數據庫都是霧里看花,在業界還沒有一個稱得上成熟的產品級應用,國內實際使用過圖形數據庫的項目更是少之又少。以其苦苦等待它的逐漸成熟,還不如我們自己動手搭建一個可用的圖形數據庫。下面以139說客(現在的移動微博)開發的圖形數據庫Skull DB為例,介紹在實際項目中如何實現和使用圖形數據庫。

Skull DB介紹
Skull DB實現了上述圖形數據庫的所有特性,不過在具體實現的數據結構上和上文略有差別。在Skull DB 的數據結構定義中(見圖2)。

 

 

(圖2)

Node類代表點,id用來標識Node在圖中的唯一性,size表示該Node包含的邊總數。Relationship類代表另一個點和另一點的邊, 表示兩個點的唯一關系。rel_id表示另一點的id。rel_value字段用於存儲和邊相關的任何屬性(例如收聽關系中被重復收聽了多少次等信息)。modify_time表示邊建立和修改的時間。RelationshipList代表一個Node和它所有的Relationship集合,它內部包含一個按Relationship的modify_time字段倒序排列的指針鏈表(LinkArray)組成。

以下圖3中的收聽關系為例,Thomas 表示為一個點:

Node thomas_node = new Node(“Thomas”);
1
Thomas收聽了Trinity 和Morpheus,其收聽關系可表示為:

RelationshipList followingList = new RelationshipList(“Thomas”);
Relationship following_trinity = new Relationship(“trinity”, “1”); // “1” 代表Relationship 的value字段,在這里用“1”表示收聽了1次。
followingList.add(following_trinity);
Relationship following_morpheus = new Relationship(“morpheus”, “1”);
followingList.add(following_ morpheus);
1
2
3
4
5

圖3

 

 

下面以圖3的收聽數據為例,詳細介紹如何用Skull DB建立和進行你收聽的人間接收聽了誰的推薦。

第一步是將用戶收聽的行為數據導入Skull DB。 一般我們拿到的數據是從生產環境導出的txt文件,使用skull DB提供的命令行工具txt2skull,將其導入到Skull DB中。
命令行:

txt2skull.sh family txtfile
1
第一個參數family,類似於BigTable中的column family,指定數據存儲到那類數據集中。由於是用戶收聽關系,family我們用following表示
第二個參數txtfile,指明需要導入的用戶好友關系文件位置。這里文件名是 user_following.txt
命令行:   

txt2skull.sh following user_following.txt
1
接下來就可以我們使用Skullclient對SkullDB進行訪問,獲取Thomas的收聽關系:

RelationshipList thomasRelations = SkullClient.getRelationships(“following”, “thomas”, Direction.POSITIVE);

for(Relationship relation : thomasRelations) {
printRelation (relation)
}
1
2
3
4
5
結果是: Trinity, Morpheus

這里Direction.POSITIVE 指定了查詢Thomas收聽的人,如果想查詢都有誰收聽了Thomas,使用Direction.REVERSE:

RelationshipList thomasRelations = SkullClient.getRelationships(“following”, “thomas”, Direction.REVERSE);

for(Relationship relation : thomasRelations) {
printRelation (relation)
}
1
2
3
4
5
結果是: Agent Smith

第二步我們通過skull client查詢,編寫一個在微博應用中常見的推薦算法,查詢Thomas 收聽的人還間接收聽了誰。

//allMap中包含了Thomas收聽的人所有的收聽列表。
Map<String, RelationshipList> allMap = SkullClient.getMultiRelationships(“following”, aRelations.getIds(), Direction.POSITIVE);
//對所有列表進行遍歷,累計重復次數最高的Relationship.
SortedMap<String, Integer> topMap = new TreeMap<String, Integer>();
for(RelationshipList relationList : allMap.values()) {
for(Relationship relation : relationList) {
if (topMap.get(relation.getId()) != null) {
int repeats = topMap.get(relation.getId()).intValue() + 1; //累計重復被收聽ID的出現次數
topMap.put(relation.getId(), new Integer(repeats));
} else {
topMap.put(relation.getId(), new Integer(1));
}
}
}
System.out.print(“你關注的人間接關注了 :” + topMap.lastKey()) ; // 打印重復次數最高的 ID
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
結果是 Agent Smith.

第三步,為了阻止數據庫中的數據無限制增長,需要刪除收聽時間大於1年的過期數據。這里需要實現RelationEventListener 接口。當發現Node添加了新的Relationship時,遍歷RelationhipList,刪除收聽時間大於1年的Relationship。

public class OldRelationCleaner implements RelationEventListener {
private fina long one_year = 365 * 24 * 60 * 60 * 1000;
public boolean EventReceived( Event event, EventData data ) {
String node_id = event.getNode().getId();
String family = event.getFamily();
RelationshipList relationships = skullclient.getRelationships(family, node_id, Direction.POSITIVE);
for (Relationship relation : relationships) {
if (System. currentTimeMillis() - relation.getModifyTime() > one_year ) {
skullclient.removeRelationship(family, node_id, relation);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
在程序中注冊收聽事件.

EventManager manager = EventManager.getInstance();
manager.registerRelationEventListener(“following”,new OldRelationCleaner());
1
2
當Thomas收聽了Architect后,在EventManager上注冊的RelationEventListener被觸發,遍歷代表Thomas所有收聽關系的RelationshipList實例relationships,找到收聽時間大於1年的Relationship並刪除。

Skull DB實現
簡單的說Skull DB 是一個用Memcache做為數據緩存, Solr和Lucene負責數據物理存儲和索引,用MemcacheQ來保存事件(Event)的集成系統。由於Solr和Memcache都有很好的橫向擴展性, Skull DB很適合做為用戶行為和關系等的數據倉庫,提供高效率的查詢和運算。

 

 


圖4

客戶端SkullClient 封裝了所有對Skull DB的訪問。SkullClient使用多級緩存和多線程從Skull DB獲取數據。所有對Skull DB 中Node和Relationship的創建、讀、寫、以及刪除操作,都通過SkullClient完成。當調用讀接口時,SkullClient先訪問Memcache,如果找不到數據,再發送Http請求從Solr獲取。當調用寫接口時,SkullClient將被修改的Relationship調整到RelationshipList列表的第一位,更新Memcache中的數據,同時異步發送Http請求將修改后的Lucene文檔發送給Solr。同時發送異步Tcp請求,將寫事件通知Event Daemon 模塊。

Skull-admin.war模塊可部署在任何WEB容器內(例如Tomcat),提供REST接口對Skull DB進行讀寫操作。它還可以通過jmx控制台進行包括:實時獲取Memcache狀態(見圖5),查詢Solr 統計數據,管理Event Daemon內部隊列,查詢Skull DB內部數據,進行數據導入和導出的操作。

 

 


圖5

Mind模塊封裝了對Skull DB 的遍歷(Travel) 和事件(Events)監聽等方法。 同時還包含了一些常用的推薦算法。這個模塊對Skull DB的讀寫通過調用SkullClient來完成。當有Node和Relationship被創建或更新時,SkullClient 調用異步線程將事件發送到Event Deamon模塊,事件被直接保存到MemcacheQ中。事件后台進程(Event Deamon Processor)負責處理保存在MemcacheQ中的事件,更新Memcache和Lucene索引中的Node和Relationship對象。 處理完成后,事件后台進程將事件再發送給Mind模塊的EventManager,通知所有注冊事件的訂閱者。

性能分析
我們使用一個新搭建的Skull DB進行性能測試,用20個並發線程向其插入用戶好友關系。結果見圖6,Y軸代表響應時間, X軸是測試執行的時間間隔(精確到秒)。

 

 


圖6

本次測試總共插入 109645條Relationship記錄,共耗時6分28秒,每個線程平均85毫秒完成一次更新。其中有2個較陡的響應時間波峰,這是因為Lucene在進行索引寫文件操作,在IO上會有一些影響。

 

 


圖7

本次測試共查詢了15211個用戶的好友關系,共耗時12秒,每個線程平均15毫秒完成一次查詢。由於Relationship數據都被緩存在Memcache中,所以響應時間的的趨勢是前高后低。

總結
在移動微博網站(www.139.com), 我們用Skull DB 存儲用戶的關系數據,評論轉發數據, 登陸IP等數據。 配合Hadoop的Map-Reduce並行框架來計算親密度、關系度、熱度等指標。 Skull DB還被用於BI分析統計,保存用戶的標簽信息用於用戶標簽分類,發掘潛在用戶的商業價值。我們之所以實現自己的圖形數據庫,一方面是因為數據存儲方案,無法勝任高並發計算,高的IO吞吐效率,同時要對用戶關系進行兩步以上的遍歷的需求。同時,我們對數據一致性要求不高,即使有少數數據不一致也不影響整體結果。 在實際項目中,Skull DB 也還存在Lucene索引效率需要加強,以及數據安全不高等方面的問題。我們項目組也在隨着項目深入,不斷完善它。Skull DB的成功就在不遠處,我們還在路上。
————————————————
版權聲明:本文為CSDN博主「E哥-書影青山」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/robertlee32/article/details/49339745


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM