第十四章 持久化

原文:Chapter 14 Persistence

译者:飞龙

协议:CC BY-NC-SA 4.0

自豪地采用谷歌翻译

在接下来的几个练习中,我们将返回到网页搜索引擎的构建。为了回顾,搜索引擎的组件是:

  • 抓取:我们需要一个程序,可以下载一个网页,解析它,并提取文本和任何其他页面的链接。
  • 索引:我们需要一个索引,可以查找检索项并找到包含它的页面。
  • 检索:我们需要一种方法,从索引中收集结果,并识别与检索项最相关的页面。

如果你做了练习 8.3,你使用 Java 映射实现了一个索引。在本练习中,我们将重新审视索引器,并创建一个新版本,将结果存储在数据库中。

如果你做了练习 7.4,你创建了一个爬虫,它跟踪它找到的第一个链接。在下一个练习中,我们将制作一个更通用的版本,将其查找到的每个链接存储在队列中,并对其进行排序。

然后,最后,你将处理检索问题。

在这些练习中,我提供较少的起始代码,你将做出更多的设计决策。这些练习也更加开放。我会提出一些最低限度的目标,你应该尝试实现它们,但如果你想挑战自己,有很多方法可以让你更深入。

现在,让我们开始编写一个新版本的索引器。

14.1 Redis

索引器的之前版本,将索引存储在两个数据结构中:TermCounter将检索词映射为网页上显示的次数,以及Index将检索词映射为出现的页面集合。

这些数据结构存储在正在运行的 Java 程序的内存中,这意味着当程序停止运行时,索引会丢失。仅在运行程序的内存中存储的数据称为“易失的”,因为程序结束时会消失。

在创建它的程序结束后,仍然存在的数据称为“持久的”。通常,存储在文件系统中的文件,以及存储在数据库中的数据是持久的。

使数据持久化的一种简单方法是,将其存储在文件中。在程序结束之前,它可以将其数据结构转换为 JSON 格式(http://thinkdast.com/json),然后将它们写入文件。当它再次启动时,它可以读取文件并重建数据结构。

但这个解决方案有几个问题:

  • 读取和写入大型数据结构(如 Web 索引)会很慢。
  • 整个数据结构可能不适合单个运行程序的内存。
  • 如果程序意外结束(例如,由于断电),则自程序上次启动以来所做的任何更改都将丢失。

一个更好的选择是提供持久存储的数据库,并且能够读取和写入数据库的部分,而无需读取和写入整个数据。

有多种数据库管理系统(DBMS)提供不同的功能。你可以在 http://thinkdast.com/database 阅读概述。

我为这个练习推荐的数据库是 Redis,它提供了类似于 Java 数据结构的持久数据结构。具体来说,它提供:

字符串列表,与 Java 的List类似。
哈希,类似于 Java 的Map
字符串集合,类似于 Java 的Set

译者注:另外还有类似于 Java 的LinkedHashSet的有序集合。

Redis 是一个“键值数据库”,这意味着它包含的数据结构(值)由唯一的字符串(键)标识。Redis 中的键与 Java 中的引用相同:它标识一个对象。我们稍后会看到一些例子。

14.2 Redis 客户端和服务端

Redis 通常运行为远程服务;其实它的名字代表“REmote DIctionary Server”(远程字典服务,字典其实就是映射)。为了使用 Redis,你必须在某处运行 Redis 服务器,然后使用 Redis 客户端连接到 Redis 服务器。有很多方法可用于设置服务器,也有许多你可以使用的客户端。对于这个练习,我建议:

不要自己安装和运行服务器,请考虑使用像 RedisToGo(http://thinkdast.com/redistogo)这样的服务,它在云主机运行 Redis。他们提供了一个免费的计划(配置),有足够的资源用于练习。
对于客户端,我推荐 Jedis,它是一个 Java 库,提供了使用 Redis 的类和方法。

以下是更详细的说明,以帮助你开始使用:

  • 在 RedisToGo 上创建一个帐号,网址为 http://thinkdast.com/redissign ,并选择所需的计划(可能是免费的起始计划)。
  • 创建一个“实例”,它是运行 Redis 服务器的虚拟机。如果你单击“实例”选项卡,你将看到你的新实例,由主机名和端口号标识。例如,我有一个名为dory-10534的实例。
  • 单击实例名称来访问配置页面。记下页面顶部附近的网址,如下所示:

    redis://redistogo:1234567feedfacebeefa1e1234567@dory.redistogo.com:10534

这个 URL 包含服务器的主机名称dory.redistogo.com,端口号10534和连接到服务器所需的密码,它是中间较长的字母数字的字符串。你将需要此信息进行下一步。

14.3 制作基于 Redis 的索引

在本书的仓库中,你将找到此练习的源文件:

  • JedisMaker.java包含连接到 Redis 服务器并运行几个 Jedis 方法的示例代码。
  • JedisIndex.java包含此练习的起始代码。
  • JedisIndexTest.java包含JedisIndex的测试代码。
  • WikiFetcher.java包含我们在以前的练习中看到的代码,用于阅读网页并使用jsoup进行解析。

你还将需要这些文件,你在以前的练习中碰到过:

Index.java使用 Java 数据结构实现索引。
TermCounter.java表示从检索项到其频率的映射。
WikiNodeIterable.java迭代jsoup生成的 DOM 树中的节点。

如果你有这些文件的有效版本,你可以使用它们进行此练习。如果你没有进行以前的练习,或者你对你的解决方案毫无信心,则可以从solutions文件夹复制我的解决方案。

第一步是使用 Jedis 连接到你的 Redis 服务器。JedisMaker.java展示了如何实现。它从文件读取你的 Redis 服务器的信息,连接到它并使用你的密码登录,然后返回一个可用于执行 Redis 操作的 Jedis 对象。

如果你打开JedisMaker.java,你应该看到JedisMaker类,它是一个帮助类,它提供静态方法make,它创建一个 Jedis 对象。一旦该对象认证完毕,你可以使用它来与你的 Redis 数据库进行通信。

JedisMaker从名为redis_url.txt的文件读取你的 Redis 服务器信息,你应该放在目录src/resources中:

  • 使用文本编辑器创建并编辑ThinkDataStructures/code/src/resources/redis_url.txt
  • 粘贴服务器的 URL。如果你使用的是 RedisToGo,则 URL 将如下所示:

    redis://redistogo:1234567feedfacebeefa1e1234567@dory.redistogo.com:10534

因为此文件包含你的 Redis 服务器的密码,你不应将此文件放在公共仓库中。为了帮助你避免意外避免这种情况,仓库包含.gitignore文件,使文件难以(但不是不可能)放入你的仓库。

现在运行ant build来编译源文件,以及ant JedisMaker来运行main中的示例代码:

    public static void main(String[] args) {Jedis jedis = make();// Stringjedis.set("mykey", "myvalue");String value = jedis.get("mykey");System.out.println("Got value: " + value);// Setjedis.sadd("myset", "element1", "element2", "element3");System.out.println("element2 is member: " + jedis.sismember("myset", "element2"));// Listjedis.rpush("mylist", "element1", "element2", "element3");System.out.println("element at index 1: " + jedis.lindex("mylist", 1));// Hashjedis.hset("myhash", "word1", Integer.toString(2));jedis.hincrBy("myhash", "word2", 1);System.out.println("frequency of word1: " + jedis.hget("myhash", "word1"));System.out.println("frequency of word1: " + jedis.hget("myhash", "word2"));jedis.close();}

这个示例展示了数据类型和方法,你在这个练习中最可能使用它们。当你运行它时,输出应该是:

Got value: myvalue
element2 is member: true
element at index 1: element2
frequency of word1: 2
frequency of word2: 1

下一节中我会解释代码的工作原理。

14.4 Redis 数据类型

Redis 基本上是一个从键到值的映射,键是字符串,值可以是字符串,也可以是几种数据类型之一。最基本的 Redis 数据类型是字符串。我将用斜体书写 Redis 类型,来区别于 Java 类型。

为了向数据库添加一个字符串,请使用jedis.set,类似于Map.put; 参数是新的键和相应的值。为了查找一个键并获取其值,请使用jedis.get

        jedis.set("mykey", "myvalue");String value = jedis.get("mykey");

在这个例子中,键是"mykey",值是"myvalue"

Redis 提供了一个集合结构,类似于 Java 的Set<String>。为了向 Redis 集合添加元素,你可以选择一个键来标识集合,然后使用jedis.sadd

        jedis.sadd("myset", "element1", "element2", "element3");boolean flag = jedis.sismember("myset", "element2");

你不必用单独的步骤来创建集合。如果不存在,Redis 会创建它。在这种情况下,它会创建一个名为myset的集合,包含三个元素。

jedis.sismember方法检查元素是否在一个集合中。添加元素和检查成员是常数时间的操作。

Redis 还提供了一个列表结构,类似于 Java 的List<String>jedis.rpush方法在末尾(右端)向列表添加元素:

        jedis.rpush("mylist", "element1", "element2", "element3");String element = jedis.lindex("mylist", 1);

同样,你不必在开始添加元素之前创建结构。此示例创建了一个名为mylist的列表,其中包含三个元素。

jedis.lindex方法使用整数索引,并返回列表中指定的元素。添加和访问元素是常数时间的操作。

最后,Redis 提供了一个哈希结构,类似于 Java 的Map<String, String>jedis.hset方法为哈希表添加新条目:

        jedis.hset("myhash", "word1", Integer.toString(2));String value = jedis.hget("myhash", "word1");

此示例创建一个名为的myhash哈希表,其中包含一个条目,该条目从将键word1映射到值"2"

键和值都是字符串,所以如果我们要存储Integer,在我们调用hset之前,我们必须将它转换为String。当我们使用hget查找值时,结果是String,所以我们可能必须将其转换回Integer

使用 Redis 的哈希表可能会令人困惑,因为我们使用一个键来标识我们想要的哈希表,然后用另一个键标识哈希表中的值。在 Redis 的上下文中,第二个键被称为“字段”,这可能有助于保持清晰。所以类似myhash的“键”标志一个特定的哈希表,然后类似word1的“字段”标识一个哈希表中的值。

对于许多应用程序,Redis 哈希表中的值是整数,所以 Redis 提供了一些特殊的方法,比如hincrby将值作为数字来处理:

        jedis.hincrBy("myhash", "word2", 1);

这个方法访问myhash,获取word2的当前值(如果不存在则为0),将其递增1,并将结果写回哈希表。

在哈希表中,设置,获取和递增条目是常数时间的操作。

你可以在 http://thinkdast.com/redistypes 上阅读 Redis 数据类型的更多信息。

14.5 练习 11

这个时候,你可以获取一些信息,你需要使用它们来创建搜索引擎的索引,它将结果储存在 Redis 数据库中。

现在运行ant JedisIndexTest。它应该失败,因为你有一些工作要做!

JedisIndexTest测试了这些方法:

  • JedisIndex,这是构造器,它接受Jedis对象作为参数。
  • indexPage,它将一个网页添加到索引中;它需要一个StringURL和一个jsoup Elements对象,该对象包含应该建立索引的页面元素。
  • getCounts,它接收检索词,并返回Map<String, Integer>,包含检索词到它在页面上的出现次数的映射。

以下是如何使用这些方法的示例:

        WikiFetcher wf = new WikiFetcher();String url1 = "http://en.wikipedia.org/wiki/Java_(programming_language)";Elements paragraphs = wf.readWikipedia(url1);Jedis jedis = JedisMaker.make();JedisIndex index = new JedisIndex(jedis);index.indexPage(url1, paragraphs);Map<String, Integer> map = index.getCounts("the");

如果我们在结果map中查看url1,我们应该得到339,这是 Java 维基百科页面(即我们保存的版本)中,the出现的次数。

如果我们再次索引相同的页面,新的结果将替换旧的结果。

将数据结构从 Java 翻译成 Redis 的一个建议是:记住 Redis 数据库中的每个对象都以唯一的键标识,它是一个字符串。如果同一数据库中有两种对象,则可能需要向键添加前缀来区分它们。例如,在我们的解决方案中,我们有两种对象:

  • 我们将URLSet定义为 Redis 集合,它包含URLURL又包含给定检索词。每个URLSet的键的起始是"URLSet:",所以要获取包含单词the的 URL,我们使用键"URLSet:the"来访问该集合。
  • 我们将TermCounter定义为 Redis 哈希表,将出现在页面上的每个检索词映射到它的出现次数。TermCounter每个键的开头都以"TermCounter:"开头,以我们正在查找的页面的 URL 结尾。

在我的实现中,每个术语都有一个URLSet,每个索引页面都有一个TermCounter。我提供两个辅助方法,urlSetKeytermCounterKey来组装这些键。

14.6 更多建议(如果你需要的话)

到了这里,你拥有了完成练习所需的所有信息,所以如果准备好了就可以开始了。但是我有几个建议,你可能想先阅读它:

  • 对于这个练习,我提供的指导比以前的练习少。你必须做出一些设计决策;特别是,你将必须弄清楚如何将问题分解成,你可以一次性测试的部分,然后将这些部分组合成一个完整的解决方案。如果你尝试一次写出整个项目,而不测试较小的部分,调试可能需要很长时间。
  • 使用持久性数据的挑战之一是它是持久的。存储在数据库中的结构可能会在每次运行程序时发生更改。如果你弄乱了数据库,你将不得不修复它或重新开始,然后才能继续。为了帮助你控制住自己,我提供的方法叫deleteURLSetsdeleteTermCountersdeleteAllKeys,你可以用它来清理数据库,并重新开始。你也可以使用printIndex来打印索引的内容。
  • 每次调用 Jedis 的方法时,你的客户端会向服务器发送一条消息,然后服务器执行你请求的操作并发回消息。如果执行许多小操作,可能需要很长时间。你可以通过将一系列操作分组为一个Transaction,来提高性能。

例如,这是一个简单的deleteAllKeys版本:

    public void deleteAllKeys() {Set<String> keys = jedis.keys("*");for (String key: keys) {jedis.del(key);}}

每次调用del时,都需要从客户端到服务器的双向通信。如果索引包含多个页面,则该方法需要很长时间来执行。我们可以使用Transaction对象来加速:

    public void deleteAllKeys() {Set<String> keys = jedis.keys("*");Transaction t = jedis.multi();for (String key: keys) {t.del(key);}t.exec();}

jedis.multi返回一个Transaction对象,它提供Jedis对象的所有方法。但是当你调用Transaction的方法时,它不会立即执行该操作,并且不与服务器通信。在你调用exec之前,它会保存一批操作。然后它将所有保存的操作同时发送到服务器,这通常要快得多。

14.7 几个设计提示

现在你真的拥有了你需要的所有信息;你应该开始完成练习。但是如果你卡住了,或者如果你真的不知道如何开始,你可以再来一些提示。

在运行测试代码之前,不要阅读以下内容,尝试一些基本的 Redis 命令,并在JedisIndex.java中编写几个方法。

好的,如果你真的卡住了,这里有一些你可能想要处理的方法:

    /*** 向检索词相关的集合中添加 URL*/public void add(String term, TermCounter tc) {}/*** 查找检索词并返回 URL 集合*/public Set<String> getURLs(String term) {}/*** 返回检索词出现在给定 URL 中的次数*/public Integer getCount(String url, String term) {}/*** 将 TermCounter 的内容存入 Redis*/public List<Object> pushTermCounterToRedis(TermCounter tc) {}

这些是我在解决方案中使用的方法,但它们绝对不是将项目分解的唯一方法。所以如果他们有帮助,请接受这些建议,但是如果没有,请忽略它们。

对于每种方法,请考虑首先编写测试。当你弄清楚如何测试一个方法时,你经常会了解如何编写它。

祝你好运!

数据结构思维 第十四章 持久化相关推荐

  1. 数据结构思维 第十五章 爬取维基百科

    第十五章 爬取维基百科 原文:Chapter 15 Crawling Wikipedia 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 在本章中,我展示了上一个练习的解决方案, ...

  2. 数据结构思维 第十二章 `TreeMap`

    第十二章 TreeMap 原文:Chapter 12 TreeMap 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 这一章展示了二叉搜索树,它是个Map接口的高效实现.如果我们 ...

  3. 【信息系统项目管理师】第十四章 文档与配置管理思维导图

    [信息系统项目管理师]第十四章 文档与配置管理思维导图 文档管理和配置管理是第十四章总两大知识板块. 文档管理 信息系统中的文档按质量分级可以分为1-4级分别是最低限度文档,内部文档,工作文档,正式文 ...

  4. 【Java数据结构与算法】第十四章 红黑树

    第十四章 红黑树 文章目录 第十四章 红黑树 一.红黑树 1.介绍 2.插入结点 3.删除结点 4.与平衡二叉树的对比 一.红黑树 1.介绍 红黑树(Red Black Tree)是平衡二叉树的其中一 ...

  5. 《Reids 设计与实现》第十四章 集群(上)

    <Reids 设计与实现>第十四章 集群(上) 文章目录 <Reids 设计与实现>第十四章 集群(上) 一.简介 二.节点 1.启动节点 2.集群数据结构 3.CLUSTER ...

  6. 第十四章 Linux核心资源

    Table of Contents, Show Frames, No Frames 第十四章 Linux核心资源 本章主要描叙寻找某个特殊核心函数时用到的Linux核心资源. 本书并不要求读者具有C编 ...

  7. 《深入理解 Spring Cloud 与微服务构建》第十四章 服务链路追踪 Spring Cloud Sleuth

    <深入理解 Spring Cloud 与微服务构建>第十四章 服务链路追踪 Spring Cloud Sleuth 文章目录 <深入理解 Spring Cloud 与微服务构建> ...

  8. 【JAVA SE】第十四章 集合框架、语法糖和泛型

    第十四章 集合框架.语法糖和泛型 文章目录 第十四章 集合框架.语法糖和泛型 一.集合框架 1.概念 2.接口 二.语法糖 1.概念 2.解语法糖 三.泛型 1.概念 2.泛型类 3.泛型接口 4.泛 ...

  9. 第十一、十二、十三、十四章 网络配置管理、归档和远程复制同步文件、软件包管理、创建访问linux文件系统

    第十一章 网络配置管理 网络地址获取方式: 1)DHCP自动获取 2)手动配置 1.网卡配置文件: /etc/sysconfig/network-scripts/ [root@server0 Desk ...

最新文章

  1. Java是如何实现Future模式的?万字详解!
  2. Python变量的作用域的使用
  3. 最短路问题之Bellman-ford算法
  4. Java多版本国际化_Java -- 国际化 多语化
  5. locate用主动还是被动_用英文形容地理位置lie、sit、locate、situate 怎样区别?
  6. 一个简单的从windows系统往AWS上直接拷贝文件的脚本
  7. Python中将array类型不按科学计数法存在文件中的方法
  8. netlify支持php吗,hexo netlify 搭建简易博客
  9. 12-22 挑战留给自己,积极面对
  10. 苹果iOS 13暗黑模式概念图曝光 将于iOS 13.1版本更新
  11. Deep Learning for 3D Recognition
  12. JS:ES6-2 const 关键字
  13. NET 自定义配置文件 Configuration
  14. java8 bigdecimal_在java 8中并行使用BigDecimal进行分组和求和
  15. python ca模块_23 Python常用模块(一)
  16. oracle去除逗号函数,oracle逗号分隔函数
  17. Web全栈工程师养成记
  18. 本特利振动前置器330180-51-00
  19. ansys部分边界条件详解
  20. priority inversion

热门文章

  1. (67)FPGA模块调用(Verilog调用system Verilog)
  2. java instance关键字_Java中instanceof关键字和isInstance()方法的区别是什么
  3. 动态库加载的函数总结
  4. 13004.循环数组队列(C语言)
  5. 6阶群的非平凡子群_离散数学复习笔记
  6. sqlyong导出sql没有数据_sqlyog怎么导入外部数据库-sqlyog导入数据库的方法 - 河东软件园...
  7. 鸿蒙应用开发--事件
  8. 数据结构之栈与递归的应用(八皇后递归解法)
  9. Linux netfilter源码分析(4)
  10. 位置式PID与增量式PID区别浅析