1.简单介绍
OrientDB是第一个多模型开源NoSQL DBMS,它将图形的功能和文档的灵活性结合到一个可扩展的高性能操作数据库中,简单地说它是一个开源的图形数据库。OrientDB Overview
根据操作来分的话,它支持command line、Studio UI、APIs(目前代码示例只有Java开发),以及其他工具的访问。
而从链接的形式来说,它支持 远端模式Remote Pattern
和本地模式Local Pattern
两种,在当前3.0
的官方文档中基本上所有讲解都是围绕着Remote Pattern
来讲解的。我写这篇文章的出发点就是因为在使用Remote Pattern
遇到了性能瓶颈,不得不使用Local Pattern
时又遇到了点小问题,经过两番尝试优化终于开窍
搞定,特此记录一下。
2.Remote Pattern through HTTP
所谓的Remote Pattern
其实就是要先执行 server.sh
命令启动一个Server
,然后所有的操作(CURD
)都是通过HTTP
访问到Server
上来执行的。
比如通过Studio UI
界面操作,server
启动后默认运行在2480
端口,server shutdown
的时候当然没法访问。而在command line
中如果使用的url
是以remote
为前缀,其实也是Remote Pattern
.
对了,Remote Pattern
的标志性url
就是remote:localhost/demo
,其中localhost
则是OrientDB Server
所运行的地址,它自动映射到内部的databases
文件夹,所有的DB instances
都默认放在它下面,而demo
就是db instance name
。
具体参考OrientDB for Java Developers in Five Minutes - 3,这里使用的依赖包是orientdb-core。
OrientDB orient = new OrientDB("remote:localhost", OrientDBConfig.defaultConfig());
ODatabaseSession db = orient.open("test", "admin", "admin");
//let's do something with this session!
db.close();
orient.close();
3.问题出现
在使用初期,上手开发还是比较简单愉快的,但是当项目迭代到性能测试的时候,问题来了。
在一次测试中,使用到了近127M的数据,跑了5个小时,最后还需要连续创建80多万条边的时候,报了ODatabaseSession
链接的异常,当时查看Exception StackTrace
后搜索了一番,大概原因是线程池耗尽了,不知道为什么没有及时释放。
然后在网上查找解决方案(想抄代码)的时候,得到了关于本地模式
的信息:
- OrientDB连接数据库
当中提到了
以及一段日志:- 定义模式,即本地模式或远程模式。
orientdb> connect remote:localhost/graphxdb root root
其中remote表示远程连接,连接成功显示:
Connecting to database [remote:localhost/graphxdb] with user 'root'...OK
orientdb {db=graphxdb}>
Connecting to database [plocal:/opt/orientdb/databases/demo] with user 'admin'…OK
Orientdb {db = demo}>
作者在这里并没有解释为什么是root
和admin
两个账户的调试信息,估计他也不清楚?
- Orientdb最快的批量导入
这里唯一的回答提到了要在本地工作
:
但是所给出的参考链接是OrientDB 2.0
的,已经失效了。
其实我之前看官方文档的时候,也确实有看到过本地模式和远端模式的说法,但文档写的平平无奇,并没有强调什么Key Point
,所以也就没留下特别的印象。
4.Local Pattern by file
本地模式是通过文件直接写数据的,而且没有http链接访问的问题,当然更快。有了方向当然就是继续前进,但是当我把url
改为plocal:$OrientDB_DirPath/databases/
之后再去跑的时候,却一直就这么来来回回报这两个错误:
- 先是报账号密码错误,我设的是
root/root
- 然后我用
studio
打开对应的db
后又一直报Orientdb File is allowed to be opened only once.
<1> 我之前的代码,考虑了兼容从零新建、删除后新建和重复插入的三种情况:
OrientDB orient = new OrientDB(url, username, password, OrientDBConfig.defaultConfig());
if (orient.exists(dbName)) {
if (isNew != null && isNew.equals("yes")) {
orient.drop(dbName);
LOG.info("drop the old instance: " + dbName + " by required.");
orient.create(dbName, ODatabaseType.PLOCAL);
LOG.info("create new instance: " + dbName + " by required.");
}
} else {
orient.create(dbName, ODatabaseType.PLOCAL);
LOG.info("create instance: " + dbName + " by default.");
}
orient.close();
orient = new OrientDB(url, OrientDBConfig.defaultConfig());
ODatabaseSession db = orient.open(dbName, username, password);
<2> 而这是官方文档中搜索plocal
的结果之一:connect语法和示例:
<3> 而这是 orientdb-core
中OrientDB.java
的源代码中的注释:
* Usage example:
*
* Remote Example:
*
*
* OrientDB orientDb = new OrientDB("remote:localhost","root","root");
* if(orientDb.createIfNotExists("test",ODatabaseType.MEMORY)){
* ODatabaseDocument session = orientDb.open("test","admin","admin");
* session.createClass("MyClass");
* session.close();
* }
* ODatabaseDocument session = orientDb.open("test","writer","writer");
* //...
* session.close();
* orientDb.close();
*
*
*
* Embedded example:
*
*
* OrientDB orientDb = new OrientDB("embedded:./databases/",null,null);
* orientDb.create("test",ODatabaseType.PLOCAL);
* ODatabaseDocument session = orientDb.open("test","admin","admin");
* //...
* session.close();
* orientDb.close();
*
*
*
<4> 破案了
是不是平平无奇?也没有任何强调说明?
最后还是灵光一闪(实际耗时好几天 ),联想到公司另一个产品中内置了OrientDB
,用的时候并不需要启动Server
,而且账号密码是admin/admin
,然后就大胆猜测:
- 在使用
OrientDB
的本地模式访问时,只能使用admin
这个账号,其余账号都是无效的 - 本地模式不能访问远端模式打开过的
db实例
,除非shutdown server
或者删除后通过本地模式新建 - 而本地模式下
admin
创建的db实例
,在远端模式下root
和admin
账号均可以打开 访问 - 通过JAVA API访问时,本地模式创建
OrientDB
是不能传入账号密码的
最后修改代码后一调试果然如此,所经历过的这些折腾,最后也就是一声唉
,不就是在文档或者代码注释里多写一个提示的事么,为什么不呢?为啥子哟!(此处无力吐槽)
<5> 调整后的代码:
OrientDB orient;
String username;
String password;
if (url.startsWith("embedded:") || url.startsWith("plocal:")) {
/**
*!IMPORTANT: required hard code to access OrientDB through local pattern
*/
username = "admin";
password = "admin";
orient = new OrientDB(url, OrientDBConfig.defaultConfig());
} else {
username = dbConfig.get("username");
password = dbConfig.get("password");
orient = new OrientDB(url, username, password, OrientDBConfig.defaultConfig());
}
// 1) If the DB Instance is existing, and requires re-creating it, drop it and then create a new Instance.
// 2) If the DB Instance isn't existing, create it by default.
if (orient.exists(dbName)) {
if (isNew) {
orient.drop(dbName);
LOG.info("drop the old instance: " + dbName + " by required.");
orient.create(dbName, ODatabaseType.PLOCAL);
LOG.info("create new instance: " + dbName + " by required.");
}
} else {
orient.create(dbName, ODatabaseType.PLOCAL);
LOG.info("create instance: " + dbName + " by default.");
}
ODatabaseSession db = orient.open(dbName, username, password);
<6> Local Pattern
和 Remote Pattern
的性能对比
上面说了在使用Remote Pattern
导入大概127M数据时,跑了5个小时之后因为http
线程池耗尽而不断异常. 在使用Local Pattern
之后,50分钟就完成了这127M数据的导入。
而另一份常用的测试数据,大概12M,花费的时间分别是120秒和45秒。
可见不论数据量的多少,Local Pattern
都比Remote Pattern
更快,这就不难理解为什么那个产品要把OrientDB
内嵌了。
不过127M数据50分钟,这个速度还是不够快,因为这只是拿到的实际数据的1/7,也就说光这一份数据全部导入就可能需要350分钟(6个小时),而且这还只是这一个环节,还是要加加速。
Index的使用
继续分析当时的代码包括观察导入数据时的日志发现:在插入Vertex
时会比插入Edge
要快许多
这是因为orientdb-core.jar中实现的save()
方法只有简单的Insert
功能,并没有想JPA
封装的save
或者saveAll()
那样是InsertOrUpsert()
功能。
所以我必须要自己解决重复插入的问题,所以在创建Vertex类
的时候添加了一个id
属性(默认的叫#rid
)来存放原本的源数据中的一个唯一值,然后据此建立了Unique index
,就可以通过getNodeById()
的方法来操作Insert or Upsert
。
private OVertex getNodeById(String classType, String id) {
OVertex node = null;
OResultSet rs;
if (classType == null || classType.equals("")) {
rs = this.db.query("SELECT FROM V WHERE id = ?", id);
} else {
rs = this.db.query("SELECT FROM ? WHERE id = ?", classType, id);
}
while (rs.hasNext()) {
OResult row = rs.next();
if (row.isVertex()) {
node = row.getVertex().get();
}
}
rs.close();
return node;
}
而Edge类
在源数据中是通过多个属性来联合确定唯一性的,再加上最开始追求实现功能,也没有仔细地想过这个问题,就通过for
循环来遍历两端顶点的所有边的方式来操作Insert or Upsert
的,当然要慢很多。
public void buildOrCheckEdge(OVertex source, OVertex target, String relationshipName) {
boolean existing = false;
if (source != null && target != null) {
for (OEdge edge : source.getEdges(ODirection.OUT, relationshipName)) {
if (edge.getTo().asVertex().get().compareTo(target) == 0) {
existing = true;
}
}
// create relationship if not exist
if (!existing) {
OEdge relationship = source.addEdge(target, relationshipName);
relationship.save()
}
}
}
当然现在经过一番查询关于OrientDB 索引
的文档(3.0官方文档,第三方翻译 )以及思考后搞定了这个事。
具体思路和改进的演进过程:
- 1)创建
Edge Schema
时新增source
和target
两个属性(from
、to
、in
、out
这四个都是自带的隐藏属性)对应两边的Vertex id
- 2)基于
source
和target
创建了一个INDEX_TYPE.UNIQUE
的索引,结果一跑才发现他是要求两个属性都唯一,对于多边关系来说没法正常存储。 - 3)基于
source
和target
创建了一个INDEX_TYPE.UNIQUE_HASH_INDEX
的索引,结果发现这种索引不支持“无条件查询”,就是说select
语句里必须配一个where
条件,这样一来我就无法根据已知的两个属性来查询、推测出未知的哈希算法,从而没法去生成正确的哈希值进而来匹配查找了。 - 4)尝试放弃索引,只凭借
select * from EdgeClass where source=$source and target=$target
来匹配查找,结果跑出来发现导入数据的效率慢了一个量级 - 5)最终在一个周末的下午又是一个灵感来了:只新增一个属性
source_target
,由两端的Vertex id
拼接而成,再据此创建INDEX_TYPE.UNIQUE
的索引,这样就跟Vertex id
一样好使了。
最后,同样的127M数据,给Edge
添加索引之后, 完成导入的时间是13分钟,而完全不做一致性检查只是插入的时候,也要12分钟。
public void createOrUpdateDLASchema() {
String[] nodes = new String[]{
"a", "b", "c"
};
for (String node : nodes) {
OClass nodeClass = db.getClass(node);
if (nodeClass == null) {
nodeClass = db.createVertexClass(node);
}
if (nodeClass.getProperty("id") == null) {
nodeClass.createProperty("id", OType.STRING);
nodeClass.createIndex(node + "_idx", OClass.INDEX_TYPE.UNIQUE, "id");
}
}
LOG.info("OVertex schema have been checked.");
String[] relationships = new String[]{
"x", "y", "z"
};
for (String relationship : relationships) {
OClass edgeClass = db.getClass(relationship);
if (edgeClass == null) {
edgeClass = db.createEdgeClass(relationship);
}
if (edgeClass.getProperty("source_target") == null) {
edgeClass.createProperty("source_target", OType.STRING);
edgeClass.createIndex(relationship + "_idx", OClass.INDEX_TYPE.UNIQUE, "source_target");
}
}
LOG.info("OEdge schema have been checked.");
}
对Orient-core的一点改进想法
我当前使用的是orientdb-core 3.0.25
:
- 首先当然就是文档描述应该有突出和对比,像
Local Pattern
这样平平无奇,真的很容易造成困惑。 - 其次,就是真的可以向
JPA
学习一下,把save
扩展一下内在兼容upsert
而不只是insert
,还可以顺便推出saveAll
,不过这就要求新增主键功能,而当前它自身的#rid
是不具备这个作用的,或许这也是一个难点吧。 - 再次就是对创建
schema
功能的增强了,我在使用期间发现在新增property
的时候只能设置类型,无法指定Not Null
这样的 ,而在Studio
手动操作时是可以的。 - 还有就是给
property
赋值只能通过setProperty(propertyName, value)
这种形式,实在是很hard code
了。