圖形數據庫Neo4J簡介

最近我在用圖形數據庫來完成對一個初創項目的支持。在使用過程中覺得這種圖形數據庫實際上挺有意思的。因此在這裏給大家做一個簡單的介紹。

NoSQL數據庫相信大家都聽說過。它們常常可以用來處理傳統的關係型數據庫所難以解決的一系列問題。通常情況下,這些NoSQL數據庫分爲Graph,Document,Column Family以及Key-Value Store等四種。這四種類型的數據庫分別使用了不同的數據結構來記錄數據。因此它們所適用的場景也不盡相同。

  其中最爲特別的便是圖形數據庫了。可以說,它和其它的一系列NoSQL數據庫非常不同:豐富的關係表示,完整的事務支持,卻沒有一個純正的橫向擴展解決方案。

  在本文中,我們就將對業界非常流行的圖形數據庫Neo4J進行簡單的介紹。

 

圖形數據庫簡介

  相信您和我一樣,在使用關係型數據庫時常常會遇到一系列非常複雜的設計問題。例如一部電影中的各個演員常常有主角配角之分,還要有導演,特效等人員的參與。通常情況下這些人員常常都被抽象爲Person類型,對應着同一個數據庫表。同時一位導演本身也可以是其它電影或者電視劇的演員,更可能是歌手,甚至是某些影視公司的投資者(沒錯,我這個例子的確是以趙薇爲模板的)。而這些影視公司則常常是一系列電影,電視劇的資方。這種彼此關聯的關係常常會非常複雜,而且在兩個實體之間常常同時存在着多個不同的關係:

  在嘗試使用關係型數據庫對這些關係進行建模時,我們首先需要建立表示各種實體的一系列表:表示人的表,表示電影的表,表示電視劇的表,表示影視公司的表等等。這些表常常需要通過一系列關聯表將它們關聯起來:通過這些關聯表來記錄一個人到底參演過哪些電影,參演過哪些電視劇,唱過哪些歌,同時又是哪些公司的投資方。同時我們還需要創建一系列關聯表來記錄一部電影中哪些人是主角,哪些人是配角,哪個人是導演,哪些人是特效等。可以看到,我們需要大量的關聯表來記錄這一系列複雜的關係。在更多實體引入之後,我們將需要越來越多的關聯表,從而使得基於關係型數據庫的解決方案繁瑣易錯。

  這一切的癥結主要在於關係型數據庫是以爲實體建模這一基礎理念設計的。該設計理念並沒有提供對這些實體間關係的直接支持。在需要描述這些實體之間的關係時,我們常常需要創建一個關聯表以記錄這些數據之間的關聯關係,而且這些關聯表常常不用來記錄除外鍵之外的其它數據。也就是說,這些關聯表也僅僅是通過關係型數據庫所已有的功能來模擬實體之間的關係。這種模擬導致了兩個非常糟糕的結果:數據庫需要通過關聯表間接地維護實體間的關係,導致數據庫的執行效能低下;同時關聯表的數量急劇上升。

  這種執行效能到底低下到什麼程度呢?就以建立人和電影之間的投資關係爲例。一個使用關聯表的設計常常如下所示:

  如果現在我們想要通過該關係找到一部電影的所有投資人,關係型數據庫常常會執行哪些操作呢?首先,在關聯表中執行一個Table Scan操作(假設沒有得到索引支持),以找到所有film域的值與目標電影id相匹配的記錄。接下來,通過這些記錄中的person域所記錄的Person的主鍵值來從Person表中找到相應的記錄。如果記錄較少,那麼這步就會使用Clustered Index Seek操作(假設是使用該運算符)。整個操作的時間複雜度將變爲O(nlogn):

  可以看到,通過關聯表組織的關係在運行時的性能並不是很好。如果我們所需要操作的數據集包含了非常多的關係,而且主要是在對這些關係進行操作,那麼可以想象到關係數據庫的性能將變得有多差。

  除了性能之外,關聯表數量的管理也是一個非常讓人頭疼的問題。剛剛我們僅僅是舉了一個具有四個實體的例子:人,電影,電視劇,影視公司。現實生活中的例子可不是這麼簡單。在一些場景下,我們常常需要對更多的實體進行建模,從而完整地描述某一領域內的關聯關係。這種關聯關係所涵蓋的可能包含影視公司的控股關係,各控股公司之間複雜的持股關係以及各公司之間的借貸款情況及擔保關係等,更可能是人之間的關係,人與各個品牌之間的代言關係,各個品牌與所屬公司之間的關係等。

  可以看到,在需要描述大量關係時,傳統的關係型數據庫已經不堪重負。它所能承擔的是較多實體但是實體間關係略顯簡單的情況。而對於這種實體間關係非常複雜,常常需要在關係之中記錄數據,而且大部分對數據的操作都與關係有關的情況,原生支持了關係的圖形數據庫纔是正確的選擇。它不僅僅可以爲我們帶來運行性能的提升,更可以大大提高系統開發效率,減少維護成本。

  在一個圖形數據庫中,數據庫的最主要組成主要有兩種,結點集和連接結點的關係。結點集就是圖中一系列結點的集合,比較接近於關係數據庫中所最常使用的表。而關係則是圖形數據庫所特有的組成。因此對於一個習慣於使用關係型數據庫開發的人而言,如何正確地理解關係則是正確使用圖形數據庫的關鍵。

  注:這裏的結點集是我自己的翻譯。在Neo4J官方文檔中,其被稱爲label。原文爲:A label is a named graph construct that is used to group nodes into sets; all nodes labeled with the same label belongs to the same set。我個人覺得生硬地取名爲標籤反而容易讓別人混淆,所以選取了“group nodes into sets”的意譯,也好讓label和node,即結點集和結點之間的關係能夠更好地對應。

  但是不用擔心,在瞭解了圖形數據庫對數據進行抽象的方式之後,您就會覺得這些數據抽象方式實際上和關係型數據庫還是非常接近的。簡單地說,每個結點仍具有標示自己所屬實體類型的標籤,也既是其所屬的結點集,並記錄一系列描述該結點特性的屬性。除此之外,我們還可以通過關係來連接各個結點。因此各個結點集的抽象實際上與關係型數據庫中的各個表的抽象還是有些類似的:

  但是在表示關係的時候,關係型數據庫和圖形數據庫就有很大的不同了:

  從上圖中可以看到,在需要表示多對多關係時,我們常常需要創建一個關聯表來記錄不同實體的多對多關係,而且這些關聯表常常不用來記錄信息。如果兩個實體之間擁有多種關係,那麼我們就需要在它們之間創建多個關聯表。而在一個圖形數據庫中,我們只需要標明兩者之間存在着不同的關係,例如用DirectBy關係指向電影的導演,或用ActBy關係來指定參與電影拍攝的各個演員。同時在ActBy關係中,我們更可以通過關係中的屬性來表示其是否是該電影的主演。而且從上面所展示的關係的名稱上可以看出,關係是有向的。如果希望在兩個結點集間建立雙向關係,我們就需要爲每個方向定義一個關係。

  也就是說,相對於關係數據庫中的各種關聯表,圖形數據庫中的關係可以通過關係能夠包含屬性這一功能來提供更爲豐富的關係展現方式。因此相較於關係型數據庫,圖形數據庫的用戶在對事物進行抽象時將擁有一個額外的武器,那就是豐富的關係:

  因此在爲圖形數據庫定義數據展現時,我們應該以一種更爲自然的方式來對這些需要展現的事物進行抽象:首先爲這些事物定義其所對應的結點集,並定義該結點集所具有的各個屬性。接下來辨識出它們之間的關係並創建這些關係的相應抽象。

  因此一個圖形數據庫中所承載的數據最終將有類似於下圖所示的結構:

 

設計一個優質的圖

  在瞭解了圖形數據庫的基礎知識之後,我們就要開始嘗試使用圖形數據庫了。首先我們要搞清楚一個問題,那就是如何爲我們的圖形數據庫定義一個設計良好的圖?實際上這並不困難,您只需要瞭解圖數據庫設計時所使用的一系列要點即可。

  首先就是,分清圖中結點集,結點以及關係之間的相互聯繫。在以往的基於關係型數據庫的設計中,我們常常會使用一個表來抽象一類事物。如對於人這個概念,我們常常會抽象出一個表,並在表中添加表示兩個人的記錄,Alice和Bob:

  而在圖數據庫中,這裏對應着兩個概念:結點集和結點。在通常情況下,圖形數據庫中的數據展示並不使用結點集,而是獨立的結點:

  而如果需要在圖中添加對書籍的支持,那麼這些書籍將仍然被表示爲一個結點:

  也就是說,雖然在一個圖數據庫中常常擁有結點集的概念,但是它已經不再作爲圖數據庫的最重要抽象方式了。甚至從某些圖形數據庫已經允許軟件開發人員使用Schemaless結點這一點上來看,它們已經將結點集的概念弱化了。反過來,我們思考的角度就應該是結點個體,以及這些個體之間所存在的一系列關係。

  那麼我們是不是可以隨便定義各個結點所具有的數據呢?不是的。這裏最爲常用的一個準則就是:Schemaless這種靈活度能爲你帶來好處。例如相較於強類型語言,弱類型語言可以爲軟件開發人員帶來更大的開發靈活度,但是其維護性和嚴謹性常常不如強類型語言。同樣地,在使用Schemaless結點時也要兼顧靈活性和維護性。

  這樣我們就可以在結點中添加多種多樣的關係,而不用像在關係型數據庫中那樣需要擔心是否需要通過更改數據庫的Schema來記錄一些外鍵。這進而允許軟件開發人員在各結點間添加多種多樣的關係:

  因此在一個圖形數據庫中,結點集這個概念已經不是最重要的那一類概念了。例如在某些圖形數據庫中,各個結點的ID並不是按照結點集來組織的,而是根據結點的創建順序來賦予的。在調試時您可能會發現,某個結點集內的第一個結點的ID是1,第二個結點的ID就是3了。而具有2這個ID的結點則處於另一個結點集中。

  那麼我們應該如何爲業務邏輯定義一個合適的圖呢?簡單地說,單一事物應該被抽象爲一個結點,而同一類型的結點被記錄在同一個結點集中。結點集內各結點所包含的數據可能有一些不同,如一個人可能有不同的職責並由此通過不同的關係和其它結點關聯。例如一個人既可能是演員,可能是導演,也可能是演員兼導演。在關係型數據庫中,我們可能需要爲演員和導演建立不同的表。而在圖形數據庫中,這三種類型的人都是人這個結點集內的數據,而不同的僅僅是它們通過不同的關係連接到不同的結點上了而已。也就是說,在圖形數據庫中,結點集並不會像關係型數據庫中的表一樣粒度那麼小。

  一旦抽象出了各個結點集,我們就需要找出這些結點之間所可能擁有的關係。這些關係不僅僅是跨結點集的。有時候,這些關係是同一結點集內的結點之間的關係,甚至是同一結點指向自身的關係:

  這些關係通常都具有一個起點和終點。也就是說,圖形數據庫中的關係常常是有向的。如果希望在兩個結點之間創建一個相互關係,如Alice和Bob彼此相識,我們就需要在他們之間創建兩個KNOW_ABOUT關係。其中一個關係由Alice指向Bob,而另一個關係則由Bob指向Alice:

  需要注意的一點就是,雖然說圖形數據庫中的關係是單向的,但是在一些圖形數據庫的實現中,如Neo4J,我們不僅僅可以查找到從某個結點所發出的關係,也可以找到指向某個結點的各個關係。也就是說,雖然圖中的關係是單向的,但是關係在起點和終點都可以被查找到。

 

在項目中使用Neo4J

  OK,在大概瞭解了圖形數據庫的一些基礎知識之後,我們就將以Neo4J爲例講解如何使用一個圖形數據庫了。Neo4J是Neo Technology所提供的開源圖形數據庫。其按照上面所介紹的結點/關係模型組織數據,並擁有以下一系列特性:

  • 對事務的支持。Neo4J強制要求每個對數據的更改都需要在一個事務之內完成,以保證數據的一致性。
  • 強大的圖形搜索能力。Neo4J允許用戶通過Cypher語言來操作數據庫。該語言是特意爲操作圖形數據庫設計的,因此其可以非常高效地操作圖形數據庫。同時Neo4J也提供了面向當前市場一系列流行語言的客戶端,以供使用這些語言的開發人員能夠快速地對Neo4J進行操作。除此之外,一些項目,如Spring Data Neo4J,也提供了一系列非常簡單明瞭的數據操作方式,使得用戶上手變得更爲容易。
  • 具有一定的橫向擴展能力。由於圖中的一個結點常常具有和其它結點相關聯的關係,因此像一系列Sharding解決方案那樣對圖進行切割常常並不現實。因此Neo4J當前所提供的橫向擴展方案主要是通過Read Replica進行的讀寫分割。反過來,由於單個Neo4J實例可以存儲幾十億個結點及關係,因此對於一般的企業級應用,這種橫向擴展能力已經足夠了。

  好,現在我們就來看一個通過Cypher來創建並操作圖形數據庫的例子(來自http://neo4j.com/developer/guide-data-modeling/):

複製代碼
 1 // 創建Sally這個Person類型的結點,該結點的name屬性爲Sally,age屬性爲32
 2 CREATE (sally:Person { name: 'Sally', age: 32 })
 3 
 4 // 創建John結點
 5 CREATE (john:Person { name: 'John', age: 27 })
 6 
 7 // 創建Graph Databases一書所對應的結點
 8 CREATE (gdb:Book { title: 'Graph Databases',
 9                    authors: ['Ian Robinson', 'Jim Webber'] })
10 
11 // 在Sally和John之間建立朋友關係,這裏的since值應該是timestamp。請自行回憶各位的日期是如何在關係數據庫中記錄的~~~
12 CREATE (sally)-[:FRIEND_OF { since: 1357718400 }]->(john)
13 
14 // 在Sally和Graph Databases一書之間建立已讀關係
15 CREATE (sally)-[:HAS_READ { rating: 4, on: 1360396800 }]->(gdb)
16 
17 // 在John和Graph Databases一書之間建立已讀關係
18 CREATE (john)-[:HAS_READ { rating: 5, on: 1359878400 }]->(gdb)
複製代碼

  該段語句創建了三個結點:Person結點Sally和John,以及Book結點gdb。同時還指定了它們之間的關係:

注:原圖來自http://neo4j.com/developer/guide-data-modeling/

想節省時間花在有用的地方,但是爲了完整性不得不寫

  這裏有一點需要注意的地方,那就是關係是單向的。如果希望建立一個雙向的關係,就像上面Sally和John互爲朋友關係那樣,我們按理來說應該需要重複執行創建關係的過程。由於我沒有試過最新版本的Neo4J(因爲它最近有一個破壞了後向兼容性的更改,我們暫時沒有辦法升級Neo4J,也就沒有辦法確認上面的代碼是不是少創建了一次FRIEND_OF),因此請讀者注意。如果有誰實驗了,請將結果添加到Comment中,感激不盡。

  有了數據,我們就可以對數據進行操作了。雖然Cypher和SQL操作的是不同的數據結構,但是他們的語法結構還是非常相似的。例如下面的語句就用來獲得Sally和John是什麼時候成爲朋友的(來自http://neo4j.com/developer/guide-data-modeling/):

1 MATCH (sally:Person { name: 'Sally' })
2 MATCH (john:Person { name: 'John' })
3 MATCH (sally)-[r:FRIEND_OF]-(john)
4 RETURN r.since as friends_since

  而且還有一些更復雜的語法。如下面的操作就用來判斷Sally和John誰先讀了《Graph Databases》一書:

1 MATCH (people:Person)
2 WHERE people.name = 'John' OR people.name = 'Sally'
3 MATCH (people)-[r:HAS_READ]->(gdb:Book { title: 'Graph Databases' })
4 RETURN people.name as first_reader
5 ORDER BY r.on
6 LIMIT 1

  當然,誰都不願意寫SQL,否則Hibernate也發展不起來。一個當前較爲流行的解決方案就是Spring Data Neo4J。通過定義一系列Java類並在其上使用一系列標記,我們就能在系統中使用Neo4J了。現在我們就以3.4.4版本的Spring Data Neo4J爲例講解如何對其進行使用。

  首先,我們需要爲將要存入到Neo4J中的數據定義相應的數據類型(來自於http://projects.spring.io/spring-data-neo4j/):

複製代碼
 1 // 通過NodeEntity標記來創建一個需要被存入到Neo4J的數據類型
 2 @NodeEntity
 3 public class Movie {
 4    // 通過GraphId標記來指定作爲ID的域。如果是新建一個結點,那麼我們需要將該域置空(null)。不知道4.0.0是否還有這個限制
 5    @GraphId Long id;
 6 
 7    // 創建針對該域的索引
 8    @Indexed(type = FULLTEXT, indexName = "search")
 9    String title;
10 
11    // 對Person類的直接引用。在保存時,其將會被自動保存到Person結點集中並保持Movie類對該實例的引用
12    Person director;
13 
14    // 通過RelatedTo標記來標示當前集合所引用的各個實體對應於當前Movie實例的關係是ACTS_IN。注意這裏標明瞭方向是INCOMING,也就是說,其方向是從Person指向Movie,也既是Person ACTS_IN Movie。而在Person中,我們同樣可以擁有一個Movie的集合,同樣使用RelatedTo標記使用ACTS_IN關係,而direction爲OUTGOING
15    // 另,RelatedTo和RelatedToVia標記按理來說在4.0.0裏已經被丟棄了,但是在官方示例中依然被使用
16    @RelatedTo(type="ACTS_IN", direction = INCOMING)
17    Set<Person> actors;
18 
19    @RelatedToVia(type = "RATED")
20    Iterable<Rating> ratings;
21 
22    // 使用自定義Query讀取數據
23    @Query("start movie=node({self}) match 
24              movie-->genre<--similar return similar")
25    Iterable<Movie> similarMovies;
26 }
複製代碼

  接下來,您就可以創建一個用來對剛剛所定義的類型進行CRUD操作的Repository了:

複製代碼
 1 // 從GraphRepository接口直接得到對Movie類進行CRUD的功能
 2 interface MovieRepository extends GraphRepository<Movie> {
 3    // 通過Cypher語句來執行特定的操作
 4    @Query("start movie={0} match m<-[rating:RATED]-user
 5              return rating")
 6    Iterable<Rating> getRatings(Movie movie);
 7 
 8    // 和Spring Data JPA一樣,可以通過特定的規則組合函數名來添加篩選條件
 9    Iterable<Person> findByActorsMoviesActorName(name)
10 }
複製代碼

  最後我們需要在Spring的配置文件中指定這些組成所在的位置:

1 <neo4j:config storeDirectory="target/graph.db" base-package="com.example.neo4j.entity"/>
2 <neo4j:repositories base-package="com.example.neo4j.repository" />

 

Neo4J集羣

  OK,在瞭解瞭如何使用Neo4J之後,下一步要考慮的就是如何通過搭建一個Neo4J集羣來提供一個具有高可用性,高吞吐量的解決方案了。首先您要知道的是,和其它NoSQL數據庫所提供的近乎無限的橫向擴展能力相比,Neo4J集羣實際上是有一定限制的。爲了能更好地理解這些限制,就讓我們首先看一看Neo4J集羣的架構以及它到底是如何工作的:

  上圖展示了一個由三個Neo4J結點所組成的Master-Slave集羣。通常情況下,每個Neo4J集羣都包含一個Master和多個Slave。該集羣中的每個Neo4J實例都包含了圖中的所有數據。這樣任何一個Neo4J實例的失效都不會導致數據的丟失。集羣中的Master主要負責數據的寫入,接下來Slave則會將Master中的數據更改同步到自身。如果一個寫入請求到達了Slave,那麼該Slave也將會就該請求與Master通信。此時該寫入請求將首先被Master執行,再異步地將數據更新到各個Slave中。所以在上圖中,您可以看到表示數據寫入方式的紅線有從Master到Slave,也有從Slave到Master,但是並沒有從Slave到Slave。而所有這一切都是通過Transaction Propagation組成來協調完成的。

  有些讀者可能已經注意到了:Neo4J集羣中數據的寫入是通過Master來完成的,那是不是Master會變成系統的寫入瓶頸呢?答案是幾乎不會。首先是圖數據修改的複雜性導致其並不會像棧,數組等數據類型那樣容易被修改。在修改一個圖的時候,我們不但需要修改圖結點本身,還要維護各個關係,本身就是一個比較複雜的過程,對用戶而言也是較難理解的。因此對圖所進行的操作也常常是讀比寫多很多。同時Neo4J內部還有一個寫隊列,可以用來暫時緩存向Neo4J實例的寫入操作,從而使得Neo4J能夠處理突然到來的大量寫入操作。而在最壞的情況就是Neo4J集羣需要面對持續的大量的寫入操作。在這種情況下,我們就需要考慮Neo4J集羣的縱向擴展了,因爲此時橫向擴展無益於解決這個問題。

  反過來,由於數據的讀取可以通過集羣中的任意一個Neo4J實例來完成,因此Neo4J集羣的讀吞吐量可以在理論上做到隨集羣中Neo4J實例的個數線性增長。例如如果一個擁有5個結點的Neo4J集羣可以每秒響應500個讀請求,那麼再添加一個結點就可以將其擴容爲每秒響應600個讀請求。

  但在請求量非常巨大而且訪問的數據分佈非常隨機的情況下,另一個問題就可能發生了,那就是Cache-Miss。Neo4J內部使用一個緩存記錄最近所訪問的數據。這些緩存數據會保存在內存中以便快速地響應數據讀取請求。但是在請求量非常巨大而且所訪問數據分佈隨機的情況下,Cache-Miss將會持續地發生,使得每次對數據的讀取都要經過磁盤查找來完成,從而大大地降低了Neo4J實例的運行效率。而Neo4J所提供的解決方案被稱爲Cache-based Sharding。簡單地說,就是使用同一個Neo4J實例來響應一個用戶所發送的所有需求。其背後的原理也非常簡單,那就是同一個用戶在一段時間內所訪問的數據常常是類似的。因此將這個用戶的一系列數據請求發送到同一個Neo4J服務器實例上可以很大程度上降低發生Cache-Miss的概率。

  Neo4J數據服務器中的另一個組成Cluster Management則用來負責同步集羣中各個實例的狀態,並監控其它Neo4J結點的加入和離開。同時其還負責維護領導選舉結果的一致性。如果Neo4J集羣中失效的結點個數超過了集羣中結點個數的一半,那麼該集羣將只接受讀取操作,直到有效結點重新超過集羣結點數量的一半。

  在啓動時,一個Neo4J數據庫實例將首先嚐試着加入由配置文件所標明的集羣。如果該集羣存在,那麼它將作爲一個Slave加入。否則該集羣將被創建,並且其將被作爲該集羣的Master。

  如果Neo4J集羣中的一個Neo4J實例失效了,那麼其它Neo4J實例會在短時間內探測到該情況並將其標示爲失效,直到其重新恢復到正常狀態並將數據同步到最新。這其中有一個特殊情況,那就是Master失效的情況。在該情況下,Neo4J集羣將會通過內置的Leader選舉功能選舉出新的Master。

  在Cluster Management組成的幫助下,我們還可以創建一個Global Cluster。其擁有一個Master Cluster以及多個Slave Cluster。該集羣組建方式允許Master Cluster和Slave Cluster處於不同區域的服務集羣中。這樣就可以允許服務的用戶訪問距離自己最近的服務。和Neo4J集羣中的Master及Slave實例的關係類似,數據的寫入通常都是在Master Cluster中進行,而Slave Cluster將只負責提供數據讀取服務。

 

提高Neo4J的性能

  相信您在上面對Neo4J集羣的講解中已經看出,Neo4J集羣實際上還是有一些限制的。這些限制將可能導致Neo4J集羣在總的系統容量,如存儲結點的數目或寫吞吐量等衆多方面存在着一定的瓶頸。在《服務的擴展性》一文中我們曾經介紹過,通過縱向擴展,我們同樣可以提高服務的整體性能。除了通過爲Neo4J提供具有更高容量的硬件之外,更有效地使用Neo4J也是縱向擴展的一個重要方法。

  和SQL Server等數據庫所提供的Execution Plan類似,Neo4J也提供了Execution Plan。在執行一個請求時,Neo4J將會把這個請求拆解爲一系列較小的操作符(Operator)。每個操作符都將執行一部分工作,並彼此相互協作完成對請求的響應。與SQL Server的Execution Plan類似,Neo4J的Execution Plan同樣擁有Scan,Seek,Merge,Filter等多種類型的操作。我們可以通過EXPLAIN或PROFILE命令得到一個請求將被如何執行的樹形表示。通過查看這些樹形表示,軟件開發人員能夠了解一個請求在Neo4J中是如何運行的:

  以下是當前版本的Neo4J所支持的所有操作符:http://neo4j.com/docs/stable/execution-plans.html

  有通過Execution Plan調優經驗的讀者可能第一眼就看到了一個操作符:Node Index Seek。它的名字直接透露了Neo4J中的另一個調優利器:索引。我們知道,只要查找出的結果集中記錄的數據並不是很多,那麼SQL Server中的Clusted Index Seek常常是具有最優效率的操作。因此在Neo4J中,我們同樣需要儘量合理地使用索引,從而使得Neo4J所生成的Execution Plan能使用基於索引的一系列操作符。讓我們回憶一下之前展示給大家的按照Spring Data Neo4J方式抽象出的Movie類:

複製代碼
1 @NodeEntity
2 public class Movie {
3    @GraphId Long id;
4 
5    @Indexed(type = FULLTEXT, indexName = "search")
6    String title;
7 
8    ……
9 }
複製代碼

  上面的代碼展示了我們應該如何通過@Indexed標記來創建一個索引。如果您是直接使用Cypher來操作Neo4J的,那麼您可以通過以下語句來創建一個索引:

1 CREATE INDEX ON :Movie(title)

  而這裏有一個和SQL Server略有不一致的地方,那就是對@GraphId標記的理解。在SQL Server中,Primary Key實際上是與索引沒有任何關聯的,只是在默認情況下,其常常會被自動地添加一個索引。而在Neo4J中,由@GraphId標記所修飾的域則更像是Neo4J的內部實現。Neo4J通過該域所記錄的值執行對結點的訪問,而不是在其上自動地添加了一個索引。

  還有一個可能非常容易影響Neo4J性能的可能,那就是嘗試使用Neo4J記錄不適合它記錄的數據。在本文的一開始我們就已經介紹了Neo4J所適合的領域,那就是記錄圖形數據,及結點集和結點之間的關係。而對於其它一些類型的數據,如用戶的用戶名/密碼對,那就不是圖形數據庫所擅長的領域了。在這些情況下,我們應該選取合適的數據庫來記錄這些數據。在一個大型系統中,多種不同類型的數據庫相互協作是經常有的事情,所以沒有必要非要將一些本來應該由其它類型的數據庫所記錄的數據硬生生地記錄在Neo4J中。

 

和其它數據庫合作

  在上面我們剛剛提到了不應該由Neo4J記錄不適合它記錄的數據,以保證Neo4J不被不合理的使用方式拉低其執行效率。那麼這些數據應該記錄在哪裏呢?答案非常簡單:適合記錄這些數據的其它類型的數據庫。

  可能你覺得我這句話是廢話。其實我也這麼覺得。而我想在這裏介紹的是,如何完成Neo4J和其它數據庫之間的集成,從而使它們協同工作,向用戶提供完整的服務。對於某些系統,我們可以允許這些數據庫之間擁有一定程度的不一致;而對於另外一些系統,我們則需要時刻保證數據的一致性。

  Neo4J所提出支持的技術方案主要有三種:Event-based Synchronization,Periodic Synchronization以及Periodic Full Export/Import of Data。Event-based Synchronization實際上就是通過同時向基於Neo4J的後臺和基於其它數據庫的後臺發送相同的消息,並由這些後臺完成對數據的寫入。Periodic Synchronization則是定時地將一個數據庫中對數據的更改同步到另一個數據庫中。而Periodic Full Export/Import of Data則是通過將一個數據庫中的所有數據導入到另外一個數據庫中的方式來完成的。

  這三種解決方案都是用來處理Neo4J所記錄的數據與其它數據庫相同的情況。而更爲常見的情況則是,Neo4J記錄實體關係比較複雜的圖,其它數據庫則用來記錄具有其它類型表現形式的數據。Neo4J和這些數據庫之間的數據只有一部分交集,而每個數據庫都擁有自己所特有的數據。針對這種情況的處理方法則常常是多步提交。例如在一個交友網站中,用戶可以在頁面上完成自身賬戶的設置,如用戶名,密碼等,並可以在下一步添加好友界面中添加一系列好友以及有關於該好友的註釋。那麼在該系統中,用戶自身的賬戶設置就可能記錄在關係型數據庫中,而有關好友的相關信息則記錄在圖形數據庫中。如果將這兩步中的所有信息作爲一個請求發送到後臺,那麼就可能出現在某個數據庫上成功保存而在另一個數據庫上保存失敗的情況。爲了避免這種情況,我們就需要將填充這兩部分資料的信息分爲兩個頁面,而在每個頁面下部提供一個”保存並進行下一步”的按鈕。這樣如果第一步設置賬戶的步驟無法正常保存,那麼用戶就沒有辦法進行下一步添加朋友的操作。而在添加朋友這步中,如果圖形數據庫無法正常保存,那麼我們將可以明確地告訴用戶添加朋友失敗,從而允許用戶重試。

  其實很多時候,跨不同數據庫保存數據的問題都可以通過調整設計的方式來解決,況且這些數據庫所記錄的數據常常具有非常不同的數據結構。因此就用戶來說,分成多步提交常常是一個非常自然的使用方式。


 轉自點擊打開鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章