使用 Oracle NoSQL Database 开发企业应用

作者:Re Lai

2012 年 12 月

在大数据和云计算的背景下,NoSQL 数据库备受关注。虽然 SQL 和 RDBMS 仍是企业存储的主力军,但 NoSQL 数据库已成为一个日益重要的工具。这有时被称为多态化存储。

近期推出的 Oracle NoSQL Database 进一步引发群情激昂。Oracle NoSQL Database 是一个水平可扩展的键值对数据库,它由备受赞誉的 Berkley DB 团队打造,具备以下特性:出色的性能、可调的一致性、集成了 Hadoop,是一个简单但功能强大的客户端 API。

本文介绍如何使用 Oracle NoSQL Database 开发应用。由于我们这个时代的应用开发人员都是在基于 SQL 的 RDBMS 环境中成长起来的,使用 NoSQL 数据库构建精心设计的企业应用代表了新的挑战。

出于演示目的,本文还介绍了 Kvitter,这是另一个类似 Twitter 的微博示例应用。对于创建 NoSQL 示例,Twitter 克隆版一直是一个深受喜爱的主题。我们的示例应用有两个目标:首先是演示 Oracle NoSQL Database,其次是使用大多数 Java 企业开发人员熟悉的概念构建应用:JavaServer Faces (JSF) 2.0、Java 上下文和依赖注入 (CDI) 以及 Java 企业设计模式

 

Kvitter 示例应用

Kvitter 是一个类似 Twitter 的微博示例应用。用户由其用户名唯一标识。用户使用密码登录。博客由博客 Id 唯一标识。博客由用户创建。用户可以关注其他用户。

该应用支持以下博客查询:

  • 用户博客:某用户发表的按时间排序的博客
  • 用户行:某用户订阅的按时间排序的博客,包括该用户自己的博客。
  • 时间行:所有用户发表的按时间排序的博客,仅限最新的 500 项。

示例应用是在 Oracle NoSQL Database 社区版 1.2.123 上开发的。请按照官方的快速入门指南安装和启动 KVLight(适用于开发人员的单进程版本)。如果您使用非默认值的自定义配置运行 KVLight,请在这里修改参数。

NetBeans 用作 IDE。经验证,NetBeans Java EE 7.1 和 7.2 均可用于本示例。请确保安装与安装捆绑在一起的 GlassFish 服务器。启动 NetBeans 之后,在 IDE 中添加一个 GlassFish 服务器实例

接下来,要在 NetBeans 中引用 Oracle NoSQL Database 安装,请打开 Tools > Variable(或 NetBeans 7.2 中的 Ant Variables)。添加变量名 KVHOME,指向 Oracle NoSQL Database 安装的根位置。

图 1:管理变量。

图 1:管理变量。

示例代码作为两个 NetBeans 项目附加:kvitterService 和 kvitterWeb。解压缩这两个项目之后,从 File > Open Project 打开它们。项目 kvitterService 是一个模型项目。您可以在 Project Navigator 中右键单击该项目,然后执行 Test 以运行 Junit 测试。sample 软件包下的测试包含本文全文中使用的示例代码。项目 kvitterWeb 是一个 JSF Web 应用。右键单击它即可运行该应用。

表示键值对

Oracle NoSQL Database 将数据存储为键值对。键由一个 Java 字符串列表组成,分为两个部分:主分量和次分量。一个键必须至少有一个主分量。另一方面,值只需不透明地存储为字节数组。字节与 Java 对象之间的转换由客户端处理。

为帮助数据建模可视化,本文采用并扩展了官方入门指南中使用的文件系统路径隐喻。它按以下示例中的方式指定键值对:

 /Majorkey1/Majorkey2/-/MinorKey1/MinorKey2: $Valuefield1 $Valuefield2

斜杠 (/) 分隔键分量。斜杠-连字符-斜杠 (/-/) 分隔主键路径和次键路径。我们再用冒号 (:) 分隔键分量和值。键分量和值域可以是 String 文本,也可以是变量。变量由前置美元符 ($) 指定,否则默认为 String 文本。

根据这种表示法,LoginFollower 可表示为:

 /Login/$userName: $password
 /Follower/$blogger/-/$follower

Login 只有主键分量:String 文本 Login(或多或少充当标记或分类符)和变量 $userName。值部分只包含一个字段:$password。

Follower 由主键和次键组成。主键是 String 文本标记 Follower 和变量 $blogger(博主用户名)。次键是变量 $follower(关注者用户名)。Follower 没有有意义的值部分,稍后再讨论。

UML 类图也可用于表示数据建模。具体来说,使用属性原型指定主键分量和次键分量。例如,可以将 LoginFollower 表示为:
图 2:Login 和 Follower。

图 2:Login 和 Follower。

值得注意的是,键值对 NoSQL 数据库本质上是无模式的。因此,模式更多地只是一个逻辑概念,实际上并不存在。值部分尤其如此。如何将多个字段存储到一个值字节数组取决于序列化方案,本文不再赘述。尽管如此,这些表示法还是有助于数据建模的可视化和通信,并将在本文中广泛使用。

连接数据库

客户端通过创建一个 KVStore 实例连接到 Oracle NoSQL Database。这可以通过以下步骤来完成:

Java 代码片段 (demo.kvitter.applicationService.DataStoreFactory)

 String storeName = "kvstore";
 String hostName = "localhost";
 String hostPort = "5000";

 KVStoreConfig config = new KVStoreConfig(storeName, 
 hostName + ":" + hostPort);
 KVStore kvstore = KVStoreFactory.getStore(config);

该设置使用了 KVLite 的默认配置。

KVStore 之于 Oracle NoSQL Database 正如 JDBC 之于 RDBMS。还值得注意的是,KVStore 是线程安全的。因此,一个 KVStore 实例可以服务多个 Web 会话。这简化了资源管理。

CRUD

完全支持创建、读取、更新和删除 (CRUD) 操作。以下示例代码显示了对 Login 的操作:

 // Login modeled as /Login/$userName: $password
 final String userName = "Jasper";
 final String password = "welcome";


 // Create a login for Jasper
 List<String> majorPath = Arrays.asList("Login", userName);
 Key key = Key.createKey(majorPath);


 byte[] bytes = password.getBytes();
 Value value = Value.createValue(bytes);

 kvStore.putIfAbsent(key, value);


 // Read 
 ValueVersion vv2 = kvStore.get(key);
 Value value2 = vv2.getValue();
 byte[] bytes2 = value2.getValue();
 String password2 = new String(bytes);


 // Update
 Value value3 = Value.createValue("welcome3".getBytes());
 kvStore.put(key, value3);
 
 // Delete
 kvStore.delete(key); 

更多详细信息,请参见 JavaDoc入门指南

组合键

组合键是 Oracle NoSQL Database 一个相当吸引人的特性。使用此特性,我们无需用 String 串接即可创建复合键。更重要的是,它是一个多功能的建模工具。

首先,根据主键分量的哈希值将数据分布到多个分区或分片。这为我们提供了一种控制数据局部性的简单方法。保证相同主键路径的各项存储在同一分区中。查看以下两种为 BlogFollower 建模的方式:

 /Blog/$blogId: $blogger $content $blogTime
 /Follower/$blogger/-/$follower

每个 Blog 都有一个唯一的主键。这样,博客就分布在多个分区上。这是合理的,因为博客构成了我们数据存储中的大量数据。尝试将全部博客存储在一个分区中必将使该分区不堪重负。另一方面,保证将给定博主的关注者名单存储在同一分区中。因此,我们可以快速检索一个博主的所有关注者。

其次,KVStore 提供了许多基于部分键匹配查询数据的方法:

  • 当匹配键具有完整的主键路径时,提取的结果可以是排序的键集合、排序的键中迭代器、排序的键值对映射或排序的键值中迭代器。
  • 当匹配键具有不完整的主键路径时,可以通过未排序的迭代器扫描数据存储。
  • 可以通过子范围和深度进一步约束匹配。当我们需要支持结果集的向前/向后分页时,这会非常方便。

最后,如果所有记录共享相同的主键路径,可以将一系列写操作应用为单个原子单位。

组合键用作重要的建模工具。它们不仅直观简单和强大,还可以在数据建模方面找到许多很好的用途。

处理值

值以字节形式存储在 Oracle NoSQL Database 中。大量简单的 Java 类型支持与字节数组之间的相互转换。更复杂的类型需要在客户端进行管理。以下是有关如何将博客转换成值对象的示例代码:

 final String blogger = "Jasper";
 final String blogContent = "Hello World!";
 final Date blogTime = Calendar.getInstance().getTime();

 ByteArrayOutputStream baos = new ByteArrayOutputStream();
 DataOutputStream dos = new DataOutputStream(baos);
 dos.writeUTF(blogger);
 dos.writeUTF(blogContent);
 dos.writeLong(blogTime.getTime());

 byte[] bytes = baos.toByteArray();
 Value value = Value.createValue(bytes);

反过来,将字节数组转换成博客:

 ValueVersion valueVersion = kvStore.get(key);
 byte[] bytes = valueVersion.getValue().getValue();
 ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
 DataInputStream dis = new DataInputStream(bais);
 String blogger = dis.readUTF();
 String blogContent = dis.readUTF();
 Date blogTime = new Date();
 blogTime.setTime(dis.readLong());

注意,我们不使用 Java 序列化,这是 Java 中序列化对象的默认机制。尽管 Java 序列化使用起来简单、直接,但并未针对紧凑性和性能进行优化。例如,类名与数据一起存储,每次持久保存条目时就会复制一份。因此,Java 序列化通常不用于大规模存储。对于这个简单示例,我们基于字节数组流手动转换。对于更广泛或更复杂的用法,可以考虑使用正式的序列化框架,如 Apache AvroKryo

实体建模

在 Oracle NoSQL Database 中,通常可以用两种方式进行实体建模:结构化值和名称/值对。

在结构化值方法中,键值对与 RDBMS 中的记录类似,其中键代表主键,值代表记录属性的序列化。我们在上一节中看到的 Blog 实体就是一个很好的示例。有一个存储值的隐式结构,是读写操作都须遵循的。

还可以利用键值存储直接将数据保存为多个名称/值对。例如,可以将 UserProfile 实体建模为:

 /UserProfile/$userName/-/$profileName: $profileValue

用户配置文件以名称/值对的形式存储在多条记录中。由于给定用户的配置文件有相同的主键路径,因此可以很轻松地进行查询或迭代,甚至可以通过批量操作以原子方式进行更新。以下是在单一原子事务中更新用户 Jason 的配置文件的示例代码:

 // User Profile as a Map
 Map<String, String> profile = new HashMap<String, String>();
 profile.put("Gender", "Male");
 profile.put("Hobbies", "Hiking");
 profile.put("Profession", "Engineer");
 
 // Create a batch of operations
 List<Operation> batch = new LinkedList<Operation>(); 
 List<String> majorPath = Arrays.asList("UserProfile", "Jasper"); 
 for (Map.Entry<String, String> entry : profile.entrySet()){
 Key key = Key.createKey(majorPath, entry.getKey());
 Value value = Value.createValue(entry.getValue().getBytes());
 Operation op = kvStore.getOperationFactory().createPut(key, value);
 batch.add(op);
 }
 
 // Execute the operation batch
 kvStore.execute(batch);

检索用户配置文件:

 List<String> majorPath = Arrays.asList("UserProfile", "Jasper");
 Key matchKey = Key.createKey(majorPath);
 
 Map<Key,Valueversion> resultMap;
 resultMap = kvStore.multiGet(matchKey, null, null); 

通常,可以采用任一方式进行实体建模。当结构是静态的并且倾向于一起访问属性时,青睐于结构化存储。另一方面,如果结构是动态的,或者通常单独访问属性,则应考虑名称/值方法。

这两种方法不一定要相互排斥。它们可以相得益彰。例如,我们可以通过结构化存储来存储用户配置文件的核心部分,将配置文件的即席或动态部分存储为名称/值对。

辅助索引建模

关系数据库 (RDBMS) 依靠索引来加快查询速度。例如,为了加快最近 10 条博客的检索,RDBMS 创建了一个组合索引来按时间对博客 ID 进行排序。这有时被称为辅助索引,与主键相对。

NoSQL 数据库(包括 Oracle NoSQL Database)一般不支持辅助索引。而是将此任务转移到客户端。幸运的是,按组合键进行索引建模很简单。在我们的 Blog 示例中,每当插入一个 Blog 项时,还会将一条记录插入 Timeline 中,即按时间排序的博客 ID 索引。

 // Timeline modeled as /Timeline/-/$blogTime/$blogId
 String majorPath = "Timeline";
 String time = Long.toHexString(blogTime.getTime());
 List<String> minorPath = Arrays.asList(time, blogId);
 Key key = Key.createKey(majorPath, minorPath);
 
 // Empty value
 Value value = Value.createValue(new byte[0]);
 
 kvStore.putIfAbsent(key, value); 

由于我们根本不关心值部分,我们置入一个空的数组。

为了检索时间线,我们基于部分键匹配使用查询 API。与常规键值对不同,Timeline 完全存储在键上,可以通过三种 API 从 KVStore 检索:multiGetKeysmultiGetKeysIteratorstoreKeysIterator。下面显示了如何获取反向排序的迭代器:

 Key matchKey = Key.createKey("Timeline");
 KeyRange subRange = null;
 Iterator<Key> it = kvStore.multiGetKeysIterator(Direction.REVERSE,
 0, matchKey, subRange, null);

请注意,一旦我们从索引检索主键,就需要发出另一个查询来根据主键提取对象。换句话说,与 RDBMS 相反,联接是在客户端完成。鉴于是 NoSQL,这是可以理解的。

多值与关系建模

多值无处不在。例如,一个博主可以有多个博客,每个博客又可能有很多关注者。它们清晰呈现了多样性关系:一对多和多对多关系。我们大多数人是从 SQL 开始了解关系的,但概念本身是通用的。

在 Oracle NoSQL Database 中处理多值与实体建模没有太大区别。对于简单情况,可以使用结构化存储。可以将一个集合对象序列化为一个字节数组并存储到单个值域中。在更灵活的情况下,首选名称/值对。例如,博主的关注者可按以下方式建模:

 /Follower/$blogger/-/$follower

由于 Follower 关系完全是在键上定义的,其本质上是索引,并可通过上述键多重获取 API 轻松检索。按此方式建模还允许添加、删除或查询关注者,而不影响其他关注者记录。例如:

 // Create: Jerry is a follower of Jason.
 List<String> majorPath = Arrays.asList("Follower", "Jason");
 String minorPath = "Jerry";
 Key key = Key.createKey(majorPath, minorPath);
 Value value = Value.createValue(new byte[0]);
 kvStore.putIfAbsent(key, value);

 //Read: is Jerry a follower of Jason?
 boolean isFollower = (null != kvStore.get(key));
 
 //Delete: unfollow
 kvStore.delete(key);

处理动态模式

由于需求变化、增强或错误修复,应用开发模式的发展是不争的事实。NoSQL 数据库本质上是无模式的,这使得它相对更容易支持动态模式。

如果我们将数据存储为名称/值对,则模式增强相当简单。为了适应新属性,我们只需添加一条新的名称/值对记录。

如果我们选择使用结构化存储,发展模式仍然相对容易。使用我们上面所用博客作为示例,添加一个新的可选属性。部署应用之后,我们决定引入一个新的名为“isPrivate”的布尔属性指定博客是否为私有。该字段将可用于新博客,但不适用于所有之前的条目。为使读取向后兼容,请在处理值部分的博客读取片段后面添加以下代码:

 // dynamically read from DataInputStream
 boolean isPrivate = false;
 if (dis.available() > 0) {
 isPrivate = dis.readBoolean();
 }

对于更复杂的场景,我们可以在存储中包含模式版本号,并在代码中处理多个版本。

我们还看到了如何在 Oracle NoSQL Database 中为索引建模。其灵活性让我们可以轻松添加或删除索引,而无需锁定表。Bret Taylor 介绍了一个有趣的案例研究,关于无模式索引如何让 FriendFeed 受益,虽然是无模式的 MySql。

KVitter 模式

现在该将所有东西组合起来了。以下是 Kvitter 应用的模式:

图 3:Kvitter 模式。

图 3:KVitter 模式。

UserlineUserBlogTimeline 本质上是 Blog 上的辅助索引。FollowerFollowingLogin 之间的多对多关系。

为简单起见,功能保持在最低限度。不包括转发、回复、提及和 hashtag 等受喜爱的特性。此外,登录(用户)由用户名唯一标识,因此该名称不能更改。

DAO 和应用服务

Java 企业设计模式得以完善,主要归功于基于 SQL 的 RDBMS 上进行的开发工作。这些模式的主题是通过适当的封装实现跨平台的数据库,有趣的是,这使它们非常适合 NoSQL 数据库。

在 Kvitter 中,数据访问对象 (DAO) 在内部用于抽象对持久存储的访问。UserService 和 BlogService 这两个应用服务进一步集中了业务逻辑。客户端与这两个业务服务交互,因此对数据存储一无所知。

图 4:DAO。

图 4:DAO。

请注意,虽然我们有 FollowerFollowing,但只有一个 FollowDao 类。这是因为 Follower 和 Following 只是彼此的镜像,可以使用一个类同时处理二者。事实上,要泛化 FollowDao 来处理通用的多对多关系并不太难。

本文中未使用但值得一提的一种技术是对象关系映射 (ORM)。在 NoSQL 数据库中,这可能应被称作对象持久性映射。这是一个已经取得很大进展的领域。EclipseLink 近期新增了对 NoSQL 数据库(包括 Oracle NoSQL)的 JPA 访问的支持。DataNucleus 是另一种流行的开源数据访问平台,也可以利用它实现自定义 JPA 映射。

JavaServer Faces Web 应用

Kvitter Web 应用基于 JSF 2.0 构建。Kvitter 使用 Facelet 模板在 /templates/main.xhtml 中定义页面布局。在 /resources/kvitter 下创建了一些自定义标记。

Kvitter 还利用了 JSF 与 CDI 的集成。CDI 提供了强大的依赖注入标准。在 Kvitter 中,ServiceProducer 发出应用服务:KVStoreUserServiceBlogService。数据库查询数据由 DataProducer 生成。

以下是用户行页面的示例屏幕截图:

图 5:KVitter 示例。

图 5:KVitter 示例。

总结

本文介绍了如何在 Oracle NoSQL Database 上构建 Java 企业 Web 应用。文中调查了 Oracle NoSQL Database 中的主要特性和数据建模方法。还介绍了 Kvitter(一个 JSF 示例 Twitter 克隆应用)。

另请参见