首页 > > > 图数据库JanusGraph浅析

Apache TinkerPop是一款开源的图计算框架,JanusGraph通过原生支持TinkerPop来提供图数据库(OLTP)和图分析系统 (OLAP)的能力。下图包括了TinkerPop的几个组成部分。最上层是Gremlin Server,为用户提供了访问图数据库的通道,通过脚本可以连上Gremlin Server,使用Gremlin查询/遍历语言来操作JanusGraph等支持Gremlin的图数据库,Gremlin语言支持Cypher语法;再下层是TinkerPop提供的用于表示属性图的核心API,包括图、定点和边的各种操作接口;Graph Computer是TinkPop的图计算框架,与Spark、Giraph或Hadoop等大数据平台配合并通过调用JanusGraph提供的OLAP I/O接口实现图分析和处理能力。JanusGraph等图数据库和图分析系统通过实现Provider API来对接TinkPop框架,定制所需的功能。

JanusGraph支持多种搜索引擎作为索引后端,索引后端和存储后端并存,但两者相互独立,且索引后端不是必须的。目前已支持使用索引后端进行地理位置、数字范围和全文检索等查询类型。所支持的查询引擎包括:
JanusGraph可以与Apache中现有的大数据处理框架相结合,基于图提供强大的大数据分析、统计报告能力,并可通过ETL工具与其他系统对接。目前支持的框架包括:
JanusGraph基于Gremlin/TinkerPop,因此支持Gremlin查询语言的图数据库可视化管理和操作工具均可用在JanusGraph中。包括但不限于:
Gephi plugin for Apache TinkerPop
KeyLines by Cambridge Intelligence

在开始后续介绍前,先来认识一张JanusGraph图,它是JanusGraph使用文档中关于希腊诸神的一张示例图。type图中的各种符号和含义如下表所示:

假如想知道Jupiter的孩子所打败过的怪兽(monster)中,居住在tartarus的怪兽是什么名字?
遍历查找流程应该是使用在name上的唯一索引先找到Jupiter,再通过标签为father的边找到其孩子为hercules,进一步通过标签为battled的边查找所有它打败过的怪兽,对于每个怪兽,都通过标签为lives的边来找到居住位置,判断位置是否为tartarus。最后返回满足条件的怪兽名为cerberus。
对应的Gremlin操作命令如下:
gremlin> g.V().has('name', 'jupiter').in('father').out('battled').as('themonster').out('lives').has('name','tartarus').select('themonster').values('name')
==>cerberus
gremlin> http://sql2gremlin.com/ 提供了Gremlin和SQL语句的转化样例,帮助快速上手Gremlin图查询语言。
hzwenzhh@innosql2:~$ ./jb/gremlin.sh
gremlin> graph = JanusGraphFactory.build().set("storage.backend", "hbase").set("storage.hostname", "xxx.163.org:2181,xxx1.163.org:2181,xxx2.163.org:2181").set("storage.hbase.ext.zookeeper.znode.parent", "/hbase").open()
gremlin> GraphOfTheGodsFactory.loadWithoutMixedIndex(graph,true) //只需要执行一次
==>null
gremlin> g = graph.traversal()
==>graphtraversalsource[standardjanusgraph[hbase:[xxx.163.org:2181, xxx1.163.org:2181, xxx2.163.org:2181]], standard]
gremlin> gremlin> theseus = graph.addVertex('human')
==>v[40964272]gremlin> theseus.property('name', 'theseus')
==>vp[name->theseus]
gremlin> g.V().hasLabel('human').has('name','theseus').property('age', '1000')
==>v[40964272]gremlin> cerberus = g.V().has('name', 'cerberus').next()
==>v[4232]
gremlin> battle = theseus.addEdge('battled', cerberus, 'time', 22)
==>e[odyja-oe08g-7x1-39k][40964272-battled->4232]gremlin> g.V().as('a').out('battled').has('name','cerberus').select('a').valueMap()
==>[name:[theseus],age:[1000]]
==>[name:[hercules],age:[30]]gremlin> g.V().hasLabel('human').as('a').out('battled').has('name','cerberus').select('a').valueMap()
==>[name:[theseus],age:[1000]]
gremlin> gremlin> g.V().has('human','name','theseus').property('age','50').valueMap()
==>[name:[theseus],age:[50]]
gremlin> gremlin> g.E().has(label,'battled').has('time','22').drop()
gremlin> g.V().has(label,'human').has('name','theseus').drop()
gremlin> gremlin> mgmt = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@2ceca2ef
gremlin> friend = mgmt.makeEdgeLabel('friend').multiplicity(MULTI).make()
==>friend
gremlin> mgmt.commit()
==>nullgremlin> mgmt = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@5b39a3e6
gremlin> fighter = mgmt.makeVertexLabel('fighter').make()
==>fighter
gremlin> mgmt.commit()
==>null
gremlin> gremlin> theseus = g.V().has('human','name','theseus').next()
==>v[40964344]
gremlin> hercules = g.V().has('name','hercules').next()
==>v[8440]
gremlin> theseus.addEdge('friend', hercules)
==>e[odzbz-oe0ag-4etx-6ig][40964344-friend->8440]gremlin> graph.tx().rollback()
==>null
gremlin> mgmt = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@673e76b3
gremlin> deep = mgmt.makePropertyKey('deep').dataType(Long.class).cardinality(Cardinality.SINGLE).make()
==>null
gremlin> mgmt.commit()
==>null
gremlin> g.E().hasLabel('friend').as('a').outV().has('name','theseus').select('a').property('deep','100')
==>e[odzq7-6ig-4etx-oe0ag][8440-friend->40964344]JanusGraph包括图索引(Graph Index)和顶点为中心的索引(Vertex-centric Indexes)
图索引支持过滤索引,就是仅在满足特定条件的边或顶点建立索引。其又包括复合索引(Composite Index)和混合索引(Mixed Index)
复合索引只能进行等值的,包括所有索引键的查询。也就是说非等的查询,或者只包含部分键的查询都无法走复合索引。复合索引不依赖索引后端,其仅依赖存储后端,所以未配置索引后端不影响复合索引使用。order by需要在内存中排序,因为没有索引后端。可以通过unique()来限定某个复合索引是唯一索引。
在deep上创建一个索引
gremlin> mgmt = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@151ab2b9
gremlin> deep = mgmt.getPropertyKey('deep')
==>deep
gremlin> mgmt.buildIndex('byDeepComposite', Vertex.class).addKey(deep).buildCompositeIndex()
==>byDeepComposite
gremlin> mgmt.commit()
==>null混合索引类似于数据库常规索引,提供比复合索引更大的灵活性。其依赖索引后端,一个JanusGraph实例可以同时配置多个索引后端。混合索引支持全文索引,地理位置索引,范围查询等,默认创建的是全文索引类型。混合索引不具备唯一性。如果order by字段为索引的一部分,则可以下推到索引后端执行。在此不做展开。
对于已经有数据的边或顶点创建索引,在索引覆盖所有数据之前,索引不生效。可使用如下命令完成索引重建
gremlin> mgmt = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@411cd156
gremlin> mgmt.updateIndex(mgmt.getGraphIndex("byDeepComposite"), SchemaAction.REINDEX).get()
==>org.janusgraph.diskstorage.keycolumnvalue.scan.StandardScanMetrics@5bcec67e
gremlin> mgmt.commit()
==>null顶点中心索引,是局部索引,只为与顶点相连的某类边建立索引。其是前缀索引,所以键的先后顺序对索引效率有较大影响。仅支持等值,范围和包含类查询
gremlin> mgmt = graph.openManagement()
==>org.janusgraph.graphdb.database.management.ManagementSystem@6c8f60f3
gremlin> time = mgmt.getPropertyKey('time')
==>time
gremlin> battled = mgmt.getEdgeLabel('battled')
==>battled
gremlin> mgmt.buildEdgeIndex(battled, 'battlesByTime1', Direction.BOTH, Order.decr, time)
==>battlesByTime1
gremlin> mgmt.commit()
==>nullJanusGraph中的事务是自动开启(与MySQL类似),但不会自动commit或rollback的(与MySQL不同),所有的读写操作都在事务中执行。形如:
juno = graph.addVertex() //Automatically opens a new transaction
juno.property("name", "juno")
graph.tx().commit() //Commits transaction所以,如果一直执行插入或更新,那么会导致隐式开启的事务越来越大。如果一个图数据库实例被close()时,还有事务在执行中,那么该事务的状态是未定的(TinkPop语义),对于隐式开启的与该实例线程绑定的事务,jg会将其提交掉,如果是显式开启的非线程绑定的事务,则会被回滚掉。
JanusGraph采用乐观事务模型,在提交时才会进行冲突检测,那么会导致在一个长事务中较早插入的具有唯一性约束的记录,在事务执行过程中被其他事务提交了。针对这种情况,可以在一个事务里面开启子事务,让子事务执行插入操作并先提交,这样能够提交长事务执行效率,避免无谓的失败。
多个事务间的隔离。JanusGraph中的事务类似于可重复读级别。事务开启后就会获取系统的状态(Snapshot?),在提交前不会更新,那么如果其他事务做了新的操作并提交,该事务是无法看到这些操作的。
JanusGraph包含了事务层(Transaction-Level)、数据库层(Database Level)和后端存储(Storage Backend)缓存,其中第三种位于所配置的后端存储上,下面简单介绍下前2种。
事务层缓存包括顶点缓存(Vertex Cache)和索引缓存(Index Cache)。
数据库层缓存在事务间共享缓存数据。对于读多写少的场景,有较好的加速效果。默认关闭,可通过 cache.db-cache=true开启。cache.db-cache-time是影响该性能和查询行为最重要的参数,若仅有一个JanusGraph实例打开存储后端,或仅有该实例有修改存储后端数据,则可以将过期设置为0,表示不过期。
从cache.db-cache-time描述可发现,JanusGraph各个实例之间并没有建立数据同步关系,也就是说,一个实例修改了数据,另一个实例是不感知的。可以把JanusGraph理解为HBase等存储后端的客户端。客户端的缓存是本地缓存,服务器端数据被修改后,客户端的缓存当然不会感知。 cache.db-cache-size 设置数据库层缓存的大小,可以是其所属的JVM内存的百分比,或者是具体的内存byte大小。过大的缓存会引起过多的GC,从而反过来影响性能,默认为50%。cache.db-cache-clean-wait表示本地事务修改了某数据后,等待多少时间将对应数据从缓存中失效掉,从存储后端重新获取。
作为一种分布式图数据库,JanusGraph可将数据切分存储到多台机器上。JanusGraph采用的分片方式是按边切割,而且是对于每一条边,都会被切断。切断后,该边会在起始顶点上和目的顶点上各存储一次。通过这种方式,JanusGraph不管是通过起始顶点,还是从目的顶点,都能快速找到对端顶点。下图所示就是按边切割的方式:

JanusGraph的数据以邻接表(adjacency list)的方式保存:

我们考虑下面最简单的图的数据在HBase中的存储方式:

下图为按边切分后的结果:

JanusGraph以点为中心,按切边的方式存储数据。节点的ID作为HBase的Rowkey,节点上的每一个属性和每一条边,作为该Rowkey的一个个独立的Cell。即每一个属性、每一条边,都是一个个独立的KCV结构(Key-Column-Value)。

1、janusgraph官方文档;
2、tinkerpop官方文档;
3、NoSQL漫谈公众号;
部分资料来自网络,侵删
原文作者:;如有侵权,请联系管理员删除。