作者: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 是一个类似 Twitter 的微博示例应用。用户由其用户名唯一标识。用户使用密码登录。博客由博客 Id 唯一标识。博客由用户创建。用户可以关注其他用户。
该应用支持以下博客查询:
示例应用是在 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:管理变量。
示例代码作为两个 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 文本。
根据这种表示法,Login 和 Follower 可表示为:
/Login/$userName: $password /Follower/$blogger/-/$follower
Login 只有主键分量:String 文本 Login
(或多或少充当标记或分类符)和变量 $userName。值部分只包含一个字段:$password。
Follower 由主键和次键组成。主键是 String 文本标记 Follower
和变量 $blogger(博主用户名)。次键是变量 $follower(关注者用户名)。Follower 没有有意义的值部分,稍后再讨论。
UML 类图也可用于表示数据建模。具体来说,使用属性原型指定主键分量和次键分量。例如,可以将 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) 操作。以下示例代码显示了对 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);
组合键是 Oracle NoSQL Database 一个相当吸引人的特性。使用此特性,我们无需用 String 串接即可创建复合键。更重要的是,它是一个多功能的建模工具。
首先,根据主键分量的哈希值将数据分布到多个分区或分片。这为我们提供了一种控制数据局部性的简单方法。保证相同主键路径的各项存储在同一分区中。查看以下两种为 Blog 和 Follower 建模的方式:
/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 Avro 和 Kryo。
在 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 检索:multiGetKeys、multiGetKeysIterator 或 storeKeysIterator。下面显示了如何获取反向排序的迭代器:
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 应用的模式:
图 3:KVitter 模式。
Userline、UserBlog 和 Timeline 本质上是 Blog 上的辅助索引。Follower 和 Following 是 Login 之间的多对多关系。
为简单起见,功能保持在最低限度。不包括转发、回复、提及和 hashtag 等受喜爱的特性。此外,登录(用户)由用户名唯一标识,因此该名称不能更改。
Java 企业设计模式得以完善,主要归功于基于 SQL 的 RDBMS 上进行的开发工作。这些模式的主题是通过适当的封装实现跨平台的数据库,有趣的是,这使它们非常适合 NoSQL 数据库。
在 Kvitter 中,数据访问对象 (DAO) 在内部用于抽象对持久存储的访问。UserService 和 BlogService 这两个应用服务进一步集中了业务逻辑。客户端与这两个业务服务交互,因此对数据存储一无所知。
图 4:DAO。
请注意,虽然我们有 Follower 和 Following,但只有一个 FollowDao 类。这是因为 Follower 和 Following 只是彼此的镜像,可以使用一个类同时处理二者。事实上,要泛化 FollowDao 来处理通用的多对多关系并不太难。
本文中未使用但值得一提的一种技术是对象关系映射 (ORM)。在 NoSQL 数据库中,这可能应被称作对象持久性映射。这是一个已经取得很大进展的领域。EclipseLink 近期新增了对 NoSQL 数据库(包括 Oracle NoSQL)的 JPA 访问的支持。DataNucleus 是另一种流行的开源数据访问平台,也可以利用它实现自定义 JPA 映射。
Kvitter Web 应用基于 JSF 2.0 构建。Kvitter 使用 Facelet 模板在 /templates/main.xhtml 中定义页面布局。在 /resources/kvitter 下创建了一些自定义标记。
Kvitter 还利用了 JSF 与 CDI 的集成。CDI 提供了强大的依赖注入标准。在 Kvitter 中,ServiceProducer 发出应用服务:KVStore、UserService 和 BlogService。数据库查询数据由 DataProducer 生成。
以下是用户行页面的示例屏幕截图:
图 5:KVitter 示例。
本文介绍了如何在 Oracle NoSQL Database 上构建 Java 企业 Web 应用。文中调查了 Oracle NoSQL Database 中的主要特性和数据建模方法。还介绍了 Kvitter(一个 JSF 示例 Twitter 克隆应用)。