Redis-学习手册-全-

Redis 学习手册(全)

原文:zh.annas-archive.org/md5/5363559C03089BFE85663EC2113016AB

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

《学习 Redis》旨在成为开发人员、架构师、解决方案提供商、顾问、工程师以及计划学习、设计和构建企业解决方案并寻找一种灵活快速且能够扩展其能力的内存数据存储的指南和手册。

本书首先介绍了 NoSQL 不断发展的格局,通过易于理解的示例探索命令,然后在一些示例应用中使用 Redis 作为支撑。书的后面部分着重于管理 Redis 以提高性能和可伸缩性。

本书涵盖了设计和创建快速、灵活和并发应用的核心概念,但不是 Redis 官方文档指南的替代品。

本书涵盖的内容

第一章,“NoSQL 简介”,涵盖了 NoSQL 的生态系统。它讨论了 NoSQL 格局的演变,并介绍了各种类型的 NoSQL 及其特点。

第二章,“开始使用 Redis”,涉及到 Redis 的世界。它还涵盖了在各种平台上安装 Redis 以及在 Java 中运行示例程序连接到 Redis 等领域。

第三章,“Redis 中的数据结构和通信协议”,涵盖了 Redis 中可用的数据结构和通信协议。它还涵盖了用户可以执行并感受其使用的示例。通过本章结束时,您应该对 Redis 的能力有了基本的了解。

第四章,“Redis 服务器中的功能”,从学习命令到 Redis 的各种内置功能。这些功能包括 Redis 中的消息传递、事务以及管道功能,它们之间有所不同。本章还向用户介绍了一种名为 LUA 的脚本语言。

第五章,“Redis 中的数据处理”,着重于 Redis 的深度数据处理能力。这包括主从安排、数据存储方式以及其提供的各种持久化数据选项。

第六章,“Web 应用中的 Redis”,是关于在 Web 应用中定位 Redis 的内容。为了增加趣味性,书中提供了一些示例应用,您可以从中获取有关 Redis 可用的广泛用例的想法。

第七章,“业务应用中的 Redis”,是关于在业务应用中定位 Redis 的内容。为了进一步扩展其在企业解决方案设计领域中的适用性,书中解释了一些示例应用,您可以从中看到其多功能性。

第八章,“集群”,讨论了集群能力,最终用户如何利用 Redis 中的各种集群模式,并相应地在其解决方案中使用这些模式。

第九章,“维护 Redis”,是关于在生产环境中维护 Redis 的内容。

本书需要什么

本书需要以下软件:

  • Redis

  • JDK 1.7

  • Jedis(Redis 的 Java 客户端)

  • Eclipse,用于开发的集成开发环境

本书适合对象

本书适用于开发人员、架构师、解决方案提供商、顾问和工程师。主要需要 Java 知识,但也可以被任何有一点编程背景的人理解。

除此之外,还有关于如何设计解决方案并在生产中维护它们的信息,不需要编码技能。

约定

在本书中,您会发现一些区分不同信息类型的文本样式。以下是这些样式的一些示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:"以下代码是新的 Hello World 程序,现在称为 HelloWorld2:"

代码块设置如下:

package org.learningredis.chapter.two;

public class Helloworld2  {
  JedisWrapper jedisWrapper = null;
  public Helloworld2() {
    jedisWrapper = new JedisWrapper();
  }

  private void test() {
    jedisWrapper.set("MSG", "Hello world 2 ");

    String result = jedisWrapper.get("MSG");
    System.out.println("MSG : " + result);
  }

  public static void main(String[] args) {
    Helloworld2 helloworld2 = new Helloworld2();
    helloworld2.test();
  }
}

新术语重要单词 以粗体显示。例如,屏幕上显示的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:"请注意命令提示符上显示的最后一行:服务器现在已准备好在端口 6379 上接受连接"。

注意

警告或重要说明会以这样的方式显示在一个框中。

提示

提示和技巧会以这样的方式显示。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者的反馈对我们很重要,因为它有助于我们开发您真正能充分利用的标题。

要向我们发送一般反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在主题中提及书名。

如果您在某个专题上有专业知识,并且有兴趣撰写或为一本书做出贡献,请参阅我们的作者指南 www.packtpub.com/authors

客户支持

既然您已经是 Packt 图书的自豪所有者,我们有很多事情可以帮助您充分利用您的购买。

下载示例代码

您可以从 www.packtpub.com 的帐户中下载您购买的所有 Packt Publishing 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便文件直接通过电子邮件发送给您。

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误确实会发生。如果您在我们的书中发现错误——也许是文本或代码中的错误——我们将不胜感激,如果您能向我们报告。通过这样做,您可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问 www.packtpub.com/submit-errata,选择您的书,点击 勘误提交表格 链接,并输入您的勘误详情。一旦您的勘误经过验证,您的提交将被接受,并且勘误将被上传到我们的网站或添加到该标题的勘误列表下的勘误部分。

要查看先前提交的勘误,请转到 www.packtpub.com/books/content/support 并在搜索框中输入书名。所需信息将显示在 勘误 部分下。

盗版

互联网上的盗版行为是跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上发现我们作品的任何非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。

请通过链接 <copyright@packtpub.com> 与我们联系,提供涉嫌盗版材料的链接。

我们感谢您帮助保护我们的作者和我们提供有价值内容的能力。

问题

如果您对本书的任何方面有问题,可以通过 <questions@packtpub.com> 与我们联系,我们将尽力解决问题。

第一章:NoSQL 简介

在本章中,您将了解新兴的 NoSQL 领域,并介绍 NoSQL 领域的各种分类。我们还将了解Redis在 NoSQL 领域的位置。我们将涵盖以下主题:

  • 企业中的数据

  • NoSQL

  • NoSQL 的用例

互联网化的世界

我们生活在一个有趣的时代;在过去的十年里,发生了许多改变,改变了我们体验互联网世界及其周围生态系统的方式。在本章中,我们将重点讨论一些促成进步的原因,并讨论数据存储领域的发展。

下图是对网络空间中发生的演变过程的粗略草图,其数据来自互联网,并大致展示了互联网服务的增长情况:

互联网化的世界

演变:社交媒体、处理器和核心、数据库(NoSQL)

前面的图表表明,硬件行业在第一个十年的中期发生了一场范式转变。新处理器不再以增加时钟速度为目标,而是新一代处理器采用了多核技术,随后发布的处理器数量也在增加。过去,大型机器配备大量内存和强大的处理器可以解决任何问题,或者换句话说,企业依赖垂直扩展来解决性能问题的日子一去不复返。这在某种程度上预示着并行计算是未来,并且将部署在基于商品的机器上。

随着硬件行业预示着并行计算的到来,新一代解决方案必须是分布式和并行的。这意味着它们需要以并行方式执行逻辑,并将数据存储在分布式数据存储中;换句话说,水平扩展是未来的发展方向。此外,随着 Web 2.0 的出现,社交媒体、在线游戏、在线购物、协作计算、云计算等也开始出现。互联网正在成为一个无处不在的平台。

互联网的普及和使用互联网的人数每天都在增加,使用互联网的时间也在增加。需要注意的另一个重要方面是,来自不同地理位置的用户在这个互联网化的世界中聚集在一起。这有很多原因;首先,网站变得更加智能,以一种更有效地吸引终端用户的方式进行交互。另一个使互联网采用速度更快、更容易的因素是创新的手持设备,如智能手机、平板电脑等。如今,这些手持设备具有的计算能力可以与计算机相媲美。在这个动态变化的世界中,基于互联网的软件解决方案和服务正在拓展社交媒体的视野,将人们聚集在一个共同的平台上。这创造了一个新的商业领域,如社交企业媒体,其中社交媒体与企业融合。这肯定会对传统企业解决方案产生影响。

互联网的影响使得企业解决方案经历了一次变革性的转变。企业架构的转变从通常期望的企业解决方案的微妙需求,转向了采纳社交媒体解决方案的新需求。如今,企业解决方案正在与社交媒体网站整合,以了解他们的客户在谈论什么;他们自己也开始创建平台和论坛,让客户可以来贡献他们对产品和服务的印象。所有这些数据交换都是实时进行的,需要一个高并发和可扩展的生态系统。总之,企业解决方案希望采纳社交媒体解决方案的特性,这直接和成比例地影响了他们架构的非功能性需求。故障管理、实时大数据处理、最终一致性、大量读写、响应性、水平扩展性、可管理性、可维护性、灵活性等特性,以及它们对企业架构的影响,都受到了新的关注。社交媒体架构中使用的技术、范例、框架和模式正在被研究和重新应用到企业架构中。

在任何解决方案(社交媒体或企业)中,关键层之一是数据层。数据的排列和管理方式,以及数据存储的选择构成了数据层。从设计师的角度来看,任何数据存储中的数据处理都受到一致性、可用性和分区容忍性等视角的影响,也就是著名的 Eric Brewer 的 CAP 定理。虽然同时拥有这三个视角是可取的,但实际上,任何数据层都可能同时具有两种以上的视角。这意味着解决方案中的数据可能具有多种视角的组合,比如可用性-分区容忍性(这种组合必须放弃数据处理中的一致性),可用性-一致性(这种组合必须放弃分区容忍性,这将影响数据层处理的数据量),以及一致性-分区容忍性(这种组合必须放弃可用性)。

CAP 定理直接影响系统的行为、读写速度、并发性、可维护性、集群模式、容错性、数据负载等。

设计数据模型时最常见的方法是以关系型和规范化的方式排列数据。当数据处于事务模式、需要一致性并且是结构化的时候,这种方法效果很好,也就是说,它有一个固定的模式。但当数据是半结构化的、具有树状结构或者是无模式的时候,这种规范化数据的方法就显得过度设计了,这种情况下一致性可以放松。将半结构化数据适应到结构化数据模型中的结果就是表的爆炸和一个复杂的数据模型来存储简单的数据。

由于缺乏替代方案,解决方案过度依赖关系型数据库管理系统(RDBMS)来解决数据处理方面的问题。这种方法的问题在于 RDBMS 最初是为了解决数据处理的一致性和可用性问题而设计的,但后来也开始存储具有分区容忍性问题的数据。最终的结果是一个臃肿的 RDBMS 和一个非常复杂的数据模型。这开始对解决方案的非功能性需求产生负面影响,包括故障管理、性能、可扩展性、可管理性、可维护性和灵活性等方面。

另一个关注领域是数据解释,在设计数据层时非常重要。在一个解决方案中,同一数据被不同的相关团队以不同的方式查看和解释。为了更好地理解,假设我们有一个销售产品的电子商务网站。在设计这个数据层时,有三个基本的功能领域涉及其中,它们是库存管理、账户管理和客户管理。从核心业务的角度来看,所有领域在其数据管理中都需要原子性、一致性、隔离性、持久性ACID)属性,从 CAP 定理的角度来看,它们需要一致性和可用性。然而,如果网站需要实时了解其客户,分析团队需要分析来自库存管理、账户管理和客户管理领域的数据。除了实时收集的其他数据。分析团队查看相同数据的方式与其他团队的方式完全不同;对于他们来说,一致性不是一个问题,因为他们更感兴趣的是整体统计数据,一些不一致的数据对整体报告没有影响。如果来自这些领域的所有分析所需数据都保存在与核心业务相同的数据模型中,分析将会遇到困难,因为现在它必须使用这些高度规范化和优化的结构化数据进行业务操作。分析团队还希望将其数据去规范化以加快分析速度。

现在,在 RDBMS 系统上对这些规范化数据进行实时分析将需要大量的计算资源,这将影响核心业务在营业时间的性能。因此,如果为这些领域创建单独的数据模型,一个用于业务,一个用于分析,每个都分开维护,对整体业务更有利。我们将在后续主题中看到为什么 RDBMS 不适合分析和其他用例,以及 NoSQL 如何解决数据爆炸问题。

NoSQL 入门

非仅 SQLNoSQL,正如它被普遍称呼的那样,是由 Carlo Strozzi 在 1998 年创造的,并在 2009 年由 Eric Evans 重新引入。这是数据处理中一个令人兴奋的领域,以某种方式填补了数据处理层中存在的许多空白。在 NoSQL 作为存储数据的备选选择出现之前,面向 SQL 的数据库(RDBMS)是开发人员定位或改装其数据的唯一选择。换句话说,RDBMS 是一把钉所有数据问题的锤子。当 NoSQL 及其不同类别开始出现时,那些不适合 RDBMS 的数据模型和数据大小开始发现 NoSQL 是一个完美的数据存储。还有一个关注点是从一致性的角度转变;从 ACID 转变为 BASE 属性。

ACID 属性代表 CAP 定理的一致性和可用性。这些属性由 RDBMS 展示,并代表以下内容:

  • 原子性:在事务中,所有操作将完成或全部不会完成(回滚)

  • 一致性:数据库在事务开始和结束时将处于一致状态,并且不能在中间状态离开

  • 隔离:并发事务之间不会有干扰

  • 持久性:一旦事务提交,即使服务器重新启动或失败,它也将保持不变

NoSQL 表现出BASE属性;它们代表了 CAP 定理的可用性和分区容忍性。它们基本上放弃了 RDBMS 所显示的强一致性。BASE 代表以下特性:

  • 基本可用:这保证了对请求的响应,即使数据处于陈旧状态。

  • 软状态:数据的状态始终处于接受更改的状态,即使没有请求更改其状态。这意味着假设有两个节点持有相同的数据状态(数据的复制),如果有一个请求在一个节点中更改状态,另一个节点中的状态在请求的生命周期内不会更改。另一个节点中的数据将由数据存储触发的异步过程更改其状态,从而使状态变得软化。

  • 最终一致性:由于节点的分布性,系统最终将变得一致。

注意

数据的写入和读取应该更快更容易。

在软件开发领域发生了另一个有趣的发展。垂直可扩展性已经达到了极限,必须设计出具有水平可扩展性的解决方案,因此数据层也必须是分布式和分区容错的。除了社交媒体解决方案外,在线游戏和基于游戏理论的网站(进行目标营销,即根据用户的购买历史奖励用户。这类网站需要实时分析)开始受到关注。社交媒体希望在最短时间内同步来自各地的大量数据,游戏世界对高性能感兴趣。电子商务网站对实时了解他们的客户和产品以及对客户进行概括以在客户意识到需求之前了解他们的需求感兴趣。根据不同数据模型出现的 NoSQL 中的类别如下:

  • 面向图形的 NoSQL

  • 面向文档的 NoSQL

  • 面向键值的 NoSQL

  • 面向列的 NoSQL

面向图形的 NoSQL

图形数据库是一种特殊类型的 NoSQL 数据库。图形数据库存储的数据模型是图形结构,与其他数据存储有些不同。图形结构由节点、边和属性组成。理解图形数据库的方法是将它们视为具有双向关系的思维导图。这意味着如果 A 与 B 相关,B 与 C 相关,那么 C 与 A 相关。图形数据库倾向于解决在运行时形成的非结构化实体之间形成的关系所引发的问题,这些关系可以是双向的。相比之下,关系型数据库也有一种称为表连接的关系概念,但这些关系是在结构化数据上的,不能是双向的。

此外,这些表连接会在数据集随着时间的推移而增长时,对具有外键的数据模型增加复杂性,并对基于表连接的查询产生性能惩罚。一些最有前途的图形数据存储包括 Neo4i、FlockDB、OrientDB 等。

为了更好地理解这一点,让我们来看一个示例用例,并看看如何使用面向图形的 NoSQL 解决复杂的基于图形的业务用例变得多么容易。以下图是一个示例用例,一个电子商务网站可能有兴趣解决。用例是捕获访问者的购买历史和网站微博组件中的人际关系。

面向图形的 NoSQL

图形数据库的示例模块

业务实体,如出版商、作者、客户、产品等,在图中表示为节点。例如,由作者、出版商发布的关系等在图中由边表示。有趣的是,来自博客网站的用户-1等非业务节点可以与其关系关注一起在图中表示。通过结合业务和非业务实体,网站可以为产品找到目标客户。在图中,节点和边都有在运行分析时使用的属性。

基于关系存储在系统中的图形数据库可以轻松回答以下一组问题:

  • 谁是Learning Redis的作者?

答案:Vinoo Das

  • Packt Publishing 和Learning Redis有什么关系?

答案:发布者

  • 谁有自己的 NoSQL 书由 Packt Publishing 出版?

答案:user-2

  • 谁正在关注购买了Learning Redis并对 NoSQL 感兴趣的客户?

答案:user-1

  • 列出所有价格低于 X 美元且可以被 user-2 的关注者购买的 NoSQL 书籍。

答案:Learning Redis

面向文档的 NoSQL

面向文档的数据存储设计用于存储具有存储文档哲学的数据。简单地说,这里的数据以书的形式排列。一本书可以分为任意数量的章节,每个章节可以分为任意数量的主题,每个主题进一步分为子主题等等。

面向文档的 NoSQL

一本书的组成

如果数据具有类似的结构,即层次化且没有固定深度或模式,则面向文档的数据存储是存储此类数据的完美选择。MongoDB 和 CouchDB(Couchbase)是目前备受关注的两种知名的面向文档的数据存储。就像一本书有索引以进行更快的搜索一样,这些数据存储也有存储在内存中的键的索引以进行更快的搜索。

面向文档的数据存储以 XML、JSON 和其他格式存储数据。它们可以保存标量值、映射、列表和元组作为值。与关系型数据库管理系统(RDBMS)不同,后者将数据视为以表格形式存储的数据行,这里存储的数据是以分层树状结构存储的,其中存储在这些数据存储中的每个值始终与一个键相关联。另一个独特的特点是面向文档的数据存储是无模式的。以下截图显示了一个示例,展示了数据存储在面向文档的数据存储中的方式。数据存储的格式是 JSON。面向文档的数据存储的一个美妙之处在于信息可以以您所想到的数据方式存储。从某种意义上说,这是与关系型数据库管理系统的范式转变,后者将数据分解为各种较小的部分,然后以规范化的方式存储在行和列中。

面向文档的 NoSQL

JASON 格式示例数据的组成

目前使用最广泛的两种面向文档的存储是 MongoDB 和 CouchDB,将它们相互对比将有助于更好地了解它们。

MongoDB 和 CouchDB 的显著特点

MongoDB 和 CouchDB 都是面向文档的事实已经确立,但它们在各个方面有所不同,这将对想要了解面向文档的数据存储并在其项目中采用它们的人们感兴趣。以下是 MongoDB 和 CouchDB 的一些特点:

  • 插入小型和大型数据集:MongoDB 和 CouchDB 都非常适合插入小型数据集。在插入大型数据集时,MongoDB 比 CouchDB 稍微更好。总体而言,这两种文档数据存储的速度一致性都非常好。

  • 随机读取:在读取速度方面,MongoDB 和 CouchDB 都很快。当涉及到读取大数据集时,MongoDB 稍微更好一些。

  • 容错性:MongoDB 和 CouchDB 都具有可比较且良好的容错能力。CouchDB 使用 Erlang/OTP 作为其实现的基础技术平台。Erlang 是一种语言和平台,旨在实现容错、可扩展和高并发的系统。Erlang 作为 CouchDB 的支撑使其具有非常好的容错能力。MongoDB 使用 C++作为其底层实现的主要语言。在容错领域的行业采用和其经过验证的记录使 MongoDB 在这一领域具有很好的优势。

  • 分片:MongoDB 具有内置的分片功能,而 CouchDB 没有。然而,建立在 CouchDB 之上的另一个文档数据存储 Couchbase 具有自动分片功能。

  • 负载平衡:MongoDB 和 CouchDB 都具有良好的负载平衡能力。然而,由于 CouchDB 中的底层技术,即 Actor 范式,具有良好的负载平衡规定,可以说 CouchDB 的能力胜过 MongoDB 的能力。

  • 多数据中心支持:CouchDB 具有多数据中心支持,而在撰写本书时,MongoDB 并没有这种支持。然而,我猜想随着 MongoDB 的普及,我们可以期待它在未来具有这种支持。

  • 可扩展性:CouchDB 和 MongoDB 都具有高度可扩展性。

  • 可管理性:CouchDB 和 MongoDB 都具有良好的可管理性。

  • 客户端:CouchDB 使用 JSON 进行数据交换,而 MongoDB 使用 BSON,这是 MongoDB 专有的。

列式 NoSQL

列式 NoSQL 的设计理念是将数据存储在列而不是行中。这种存储数据的方式与 RDBMS 中存储数据的方式完全相反,RDBMS 中数据是按行存储的。列式数据库从一开始就被设计为高度可扩展的,因此具有分布式特性。它们放弃了一致性以获得这种大规模的可扩展性。

以下截图描述了基于我们的感知的智能平板电脑的小型库存;在这里,想要展示 RDBMS 中存储的数据与列式数据库中存储的数据的对比:

列式 NoSQL

以列和行的形式呈现数据

上述表格数据以如下格式存储在硬盘的 RDBMS 中:

列式 NoSQL

数据序列化为列

上述截图信息的来源是en.wikipedia.org/wiki/Column-oriented_DBMS

列式数据存储中的相同数据将存储如下图所示;在这里,数据是按列序列化的:

列式 NoSQL

数据序列化为行

在垂直可扩展性达到极限、水平可扩展性是组织希望采用的存储数据方式的世界中,列式数据存储提供了可以以非常具有成本效益的方式存储百万兆字节数据的解决方案。谷歌、雅虎、Facebook 等公司率先采用了列式存储数据的方式,而这些公司存储的数据量是众所周知的事实。HBase 和 Cassandra 是一些以列为基础的知名产品,可以存储大量数据。这两种数据存储都是以最终一致性为目标构建的。在 HBase 和 Cassandra 的情况下,底层语言是 Java;将它们相互对比将会很有趣,以便更好地了解它们。

HBase 和 Cassandra 的显著特点

HBase 是一种属于列定向数据存储类别的数据存储。这种数据存储在 Hadoop 变得流行之后出现,受到了 2003 年发布的Google 文件系统论文的启发。HBase 基于 Hadoop,使其成为数据仓库和大规模数据处理和分析的绝佳选择。HBase 在现有的 Hadoop 生态系统上提供了类似于我们在关系型数据库管理系统中查看数据的 SQL 类型接口,即面向行,但数据在内部以列为导向的方式存储。HBase 根据行键存储行数据,并按行键的排序顺序进行排序。它具有诸如 Region Server 之类的组件,可以连接到 Hadoop 提供的 DataNode。这意味着 Region Server 与 DataNode 共存,并充当与 HBase 客户端交互的网关。在幕后,HBase master 处理 DDL 操作。除此之外,它还管理 Region 分配和与之相关的其他簿记活动。Zookeeper 节点负责集群信息和管理,包括状态管理。HBase 客户端直接与 Region Server 交互以放置和获取数据。诸如 Zookeeper(用于协调主节点和从节点之间的协调)、Name Node 和 HBase 主节点等组件不直接参与 HBase 客户端和 Region Server 节点之间的数据交换。

HBase 和 Cassandra 的显著特点

HBASE 节点设置

Cassandra 是一种属于列定向数据存储类别的数据存储,同时也显示了一些键-值数据存储的特性。Cassandra 最初由 Facebook 启动,但后来分叉到 Apache 开源社区,最适合实时事务处理和实时分析。

Cassandra 和 HBase 之间的一个关键区别在于,与 HBase 依赖于 Hadoop 的现有架构不同,Cassandra 是独立的。Cassandra 受亚马逊的 Dynamo 的启发来存储数据。简而言之,HBase 的架构方法使得 Region Server 和 DataNodes 依赖于其他组件,如 HBase master、Name Node、Zookeeper,而 Cassandra 中的节点在内部管理这些责任,因此不依赖于外部组件。

Cassandra 集群可以被视为一个节点环,其中有一些种子节点。这些种子节点与任何节点相似,但负责最新的集群状态数据。如果种子节点出现故障,可以在可用节点中选举出一个新的种子。数据根据行键的哈希值均匀分布在环上。在 Cassandra 中,数据可以根据其行键进行查询。Cassandra 的客户端有多种类型;也就是说,Thrift 是最原生的客户端之一,可以用来与 Cassandra 环进行交互。除此之外,还有一些客户端暴露了与 SQL 非常相似的 Cassandra 查询语言(CQL)接口。

HBase 和 Cassandra 的显著特点

Cassandra 节点设置

  • 插入小型和大型数据集:HBase 和 Cassandra 都非常擅长插入小型数据集。事实上,这两种数据存储都使用多个节点来分发写入。它们都首先将数据写入基于内存的存储,如 RAM,这使得其插入性能很好。

  • 随机读取:在读取速度方面,HBase 和 Cassandra 都很快。在设计架构时,HBase 考虑到了一致性是其中的一个关键特性。在 Cassandra 中,数据一致性是可调的,但为了获得更高的一致性,必须牺牲速度。

  • 最终一致性:HBase 具有强一致性,Cassandra 具有最终一致性,但有趣的是,Cassandra 中的一致性模型是可调节的。它可以调整为具有更好的一致性,但必须在读写速度上牺牲性能。

  • 负载均衡:HBase 和 Cassandra 内置了负载均衡。其想法是让许多节点在商品级节点上提供读写服务。一致性哈希用于在节点之间分配负载。

  • 分片:HBase 和 Cassandra 都具有分片能力。这是必不可少的,因为两者都声称可以从商品级节点获得良好的性能,而商品级节点的磁盘和内存空间有限。

  • 多数据中心支持:在这两者中,Cassandra 具有多数据中心支持。

  • 可扩展性:HBase 和 Cassandra 都具有非常好的可扩展性,这是设计要求之一。

  • 可管理性:在这两者中,Cassandra 的可管理性更好。这是因为在 Cassandra 中,需要管理节点,但在 HBase 中,有许多需要协同工作的组件,如 Zookeeper、DataNode、Name Node、Region Server 等。

  • 客户端:HBase 和 Cassandra 都有 Java、Python、Ruby、Node.js 等客户端,使其在异构环境中易于使用。

键值导向的 NoSQL

键值数据存储可能是最快和最简单的 NoSQL 数据库之一。在其最简单的形式中,它们可以被理解为一个大的哈希表。从使用的角度来看,数据库中存储的每个值都有一个键。键可以用来搜索值,通过删除键可以删除值。在键值数据库中一些受欢迎的选择包括 Redis、Riak、亚马逊的 DynamoDB、voldermort 项目等。

Redis 在作为键值数据存储的一些非功能性需求方面表现如何?

Redis 是最快的键值存储之一,在整个行业中得到了非常快的采用,涵盖了许多领域。由于本书侧重于 Redis,让我们简要了解一下 Redis 在一些非功能性需求方面的表现。随着本书的进展,我们将会更详细地讨论它们:

  • 数据集的插入:在键值数据存储中,数据集的插入非常快,Redis 也不例外。

  • 随机读取:在键值数据存储中,随机读取非常快。在 Redis 中,所有键都存储在内存中。这确保了更快的查找速度,因此读取速度更快。虽然如果所有键和值都保留在内存中将会很好,但这也有一个缺点。这种方法的问题在于内存需求会非常高。Redis 通过引入一种称为虚拟内存的东西来解决这个问题。虚拟内存将所有键保留在内存中,但将最近未使用的值写入磁盘。

  • 容错性:Redis 中的故障处理取决于集群的拓扑结构。Redis 在其集群部署中使用主从拓扑结构。主节点中的所有数据都会异步复制到从节点;因此,如果主节点进入故障状态,其中一个从节点可以通过 Redis sentinel 晋升为主节点。

  • 最终一致性:键值数据存储具有主从拓扑结构,这意味着一旦主节点更新,所有从节点都会异步更新。这在 Redis 中可以想象,因为客户端使用从节点进行只读模式;可能主节点已经写入了最新值,但在从节点读取时,客户端可能会得到旧值,因为主节点尚未更新从节点。因此,这种滞后可能会导致短暂的不一致性。

  • 负载均衡:Redis 有一种简单的实现负载均衡的方法。如前所述,主节点用于写入数据,从节点用于读取数据。因此,客户端应该在其内部构建逻辑,将读取请求均匀分布在从节点上,或者使用第三方代理,如 Twemproxy 来实现。

  • 分片:可能会有比可用内存更大的数据集,这使得在各个对等节点之间预先分片数据成为一种水平可扩展的选择。

  • 多数据中心支持:Redis 和键值 NoSQL 不提供内在的多数据中心支持,其中复制是一致的。但是,我们可以在一个数据中心拥有主节点,在另一个数据中心拥有从节点,但我们必须接受最终一致性。

  • 可扩展性:在扩展和数据分区方面,Redis 服务器缺乏相应的逻辑。主要的数据分区逻辑应该由客户端或者使用第三方代理(如 Twemproxy)来实现。

  • 可管理性:Redis 作为一个键值 NoSQL 数据库,管理起来很简单。

  • 客户端:Redis 有 Java、Python 和 Node.js 的客户端,实现了REdis Serialization Protocol(RESP)。

NoSQL 的用例

首先了解你的业务;这将帮助你了解你的数据。这也将让你深入了解你需要拥有的数据层的类型。关键是要有一个自上而下的设计方法。首先决定持久性机制,然后将业务用例的数据适配到该持久性机制中是一个不好的想法(自下而上的设计方法)。因此,首先定义你的业务需求,然后决定未来的路线图,然后再决定数据层。在理解业务需求规范时,另一个重要因素是考虑每个业务用例的非功能性需求,我认为这是至关重要的。

如果在业务或功能需求中没有添加非功能性需求,那么当系统进行性能测试或更糟的是上线时会出现问题。如果你觉得从功能需求的角度来看数据模型需要 NoSQL,那么可以问一些问题,如下所示:

  • 你的数据模型需要什么类型的 NoSQL?

  • 数据可以增长到多大,需要多大的可扩展性?

  • 你将如何处理节点故障?它对你的业务用例有什么影响?

  • 在数据增长时,数据复制和基础设施投资哪个更好?

  • 处理读/写负载的策略是什么,计划的并发量有多大?

  • 业务用例需要什么级别的数据一致性?

  • 数据将存放在哪里(单个数据中心还是跨地理位置的多个数据中心)?

  • 集群策略和数据同步策略是什么?

  • 数据备份策略是什么?

  • 你计划使用什么样的网络拓扑?网络延迟对性能有什么影响?

  • 团队在处理、监控、管理和开发多语言持久性环境方面有多舒适?

以下是一些 NoSQL 数据库及其根据 CAP 定理的放置方式的摘要。以下图表并不是详尽无遗的,但是是最受欢迎的数据库的一个快照:

NoSQL 的用例

根据 CAP 定理放置的 NoSQL 数据库

让我们分析一下公司如何使用 NoSQL,这将给我们一些关于如何有效地在我们的解决方案中使用 NoSQL 的想法:

  • 大数据:这个术语让人联想到数百甚至数千台服务器处理数据以进行分析。大数据的用例是不言而喻的,很容易证明使用 NoSQL 数据存储的必要性。作为 NoSQL 的一种模式,列式数据库是这种活动的明显选择。由于分布式的特性,这些解决方案也没有单点故障,可以进行并行计算、写入可用性和可扩展性。以下是一些不同类型的用例列表,其中公司已经成功地在他们的业务中使用了列式数据存储:

  • Spotify 使用 Hadoop 进行数据聚合、报告和分析

  • Twitter 使用 Hadoop 处理推文和日志文件

  • Netflix 使用 Cassandra 作为其后端数据存储以提供流媒体服务

  • Zoho 使用 Cassandra 为邮件服务生成收件箱预览

  • Facebook 使用 Cassandra 进行 Instagram 操作

  • Facebook 在其消息基础设施中使用 HBase

  • Su.pr 使用 HBase 进行实时数据存储和分析平台

  • HP IceWall SSO 使用 HBase 存储用户数据,以便为其基于 Web 的单点登录解决方案对用户进行身份验证

  • 大量读/写:这个非功能性需求立即让我们联想到社交或游戏网站。对于这是一个要求的企业,他们可以从 NoSQL 的选择中获得灵感。

  • LinkedIn 使用 Voldermort(键值数据存储)为数百万读写每天提供服务,在几毫秒内完成

  • Wooga(社交网络游戏和移动开发者)使用 Redis 进行游戏平台;一些游戏每天有超过一百万用户

  • Twitter 每天处理 2 亿条推文,并使用 NoSQL,如 Cassandra、HBase、Memcached 和 FlockDB,还使用关系型数据库,如 MySQL

  • Stack overflow 使用 Redis 为每月 3000 万注册用户提供服务

  • 文档存储:Web 2.0 采用的增长和互联网内容的增加正在创造无模式的数据。专门设计用于存储这种数据的 NoSQL(文档导向)使开发人员的工作更简单,解决方案的稳定性更强。以下是一些使用不同文档存储的公司的示例:

  • SourceForge 使用 MongoDB 存储首页、项目页面和下载页面;SourceForge 上的 Allura 基于 MongoDB

  • MetLife 使用 MongoDB 作为the wall的数据存储,这是一个客户服务平台

  • Semantic News Portal 使用 CouchDB 存储新闻数据

  • 佛蒙特公共广播网站的主页使用 CouchDB 存储新闻标题、评论等

  • AOL 广告使用 Couchbase(CouchDB 的新化身)为 10 亿多用户提供每月数十亿次印象

  • 实时体验和电子商务平台:购物车、用户资料管理、投票、用户会话管理、实时页面计数器、实时分析等是公司提供的服务,以给用户提供实时体验。以下是一些使用实时体验和电子商务平台的公司的示例:

  • Flickr push 使用 Redis 推送实时更新

  • Instagram 使用 Redis 存储数以百万计的媒体内容,并实时提供服务

  • Digg 使用 Redis 进行页面浏览和用户点击的解决方案

  • 百思买使用 Riak 进行电子商务平台

总结

在本章中,您看到互联网世界正在经历一场范式转变,NoSQL 世界的演变,以及社交媒体如何领导 NoSQL 的采用。您还看到了 NoSQL 世界中的各种替代方案以及它们的等价性。最后,您看到了 Redis 如何在 NoSQL 生态系统中映射。

在下一章中,我们将深入探讨 Redis 的世界。

第二章:开始使用 Redis

Redis 是由 Salvatore Sanfilippo 开发的基于键值的 NoSQL 数据存储,于 2009 年推出。Redis 的名称来自于REmote DIctionary Server。Redis 是用 C 语言编写的高性能单线程服务器。

Redis 可以安装在所有符合 POSIX 标准的 Unix 系统上。尽管没有 Windows 系统的生产级发布,但仍然可以在 Windows 环境中进行开发目的的安装。在本章中,我们将在 Windows 和 Mac OS 环境中安装 Redis,用 Java 编写程序,并使用分发包中附带的内置客户端进行操作。

在 Windows 上安装 Redis

微软开放技术组已经将 Redis 移植并在 win32/win64 机器上进行维护。有两种方法可以在 Windows 上安装 Redis,如下所示:

  • 使用预构建的二进制文件

  • 在 Microsoft 环境中获取代码并编译它

对于急切的人来说,下载 Redis 2.8 的二进制文件是一个更简单的选择。首先,我们需要按照以下步骤开始:

  1. 转到github.com/MSOpenTech/redis并下载Clone in Desktop按钮下的 ZIP 文件。在本书中,我们将下载最新版本的 Redis,即redis-2.8.zip文件。

  2. 右键单击链接并将其保存在 Windows 机器上的适当位置。我已经将其保存在F:\sw2\redis\redis-2.8.zip

  3. 右键单击并解压缩压缩文件到适当的文件夹。我将文件夹命名为redis-2.8,解压缩后的文件夹结构看起来与以下屏幕截图相似:在 Windows 上安装 Redis

解压缩压缩文件后的文件夹结构

  1. 进入bin文件夹。您将找到release文件夹;单击它,您将看到该文件夹内的文件列表,如下面的屏幕截图所示:在 Windows 上安装 Redis

bin/release 文件夹内的文件夹结构

  1. 打开命令提示符并运行redis-server.exe。提供redis-server.exe --maxheap 1024mb堆大小,您应该会看到一个控制台窗口弹出,类似于以下屏幕截图。在 Windows 7 的情况下,用户可能会被要求信任软件以进一步进行。在 Windows 上安装 Redis

Redis 服务器的默认启动

  1. 注意命令提示符上显示的最后一行:服务器现在准备好在端口 6379 上接受连接

  2. 现在,让我们启动一个预构建的客户端,该客户端随分发包一起提供,并连接到服务器。我们将执行的客户端是一个命令行解释器,当我们点击它时,客户端程序将被启动:在 Windows 上安装 Redis

Redis 客户端在 Redis 服务器运行时启动

  1. 您的简单安装已完成(集群设置和其他管理主题将在后续章节中进行)。

提示

下载示例代码

您可以从www.packtpub.com的帐户中下载示例代码文件,以获取您购买的所有 Packt Publishing 图书。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

在 Mac OS 上安装 Redis

在 Mac OS 上安装 Redis 真的很简单。按照这些步骤,您就可以开始了:

  1. 从互联网下载包。为此,您可以使用以下命令:wget http://download.redis.io/releases/redis-2.8.3.tar.gz

  2. 解压缩tar xzf redis-2.8.3.tar.gz文件。

  3. 这将创建一个文件夹;通过发出cd redis-2.8.3命令进入文件夹。

  4. 通过发出make命令来编译文件。这将编译二进制文件并创建文件夹结构,如下面的屏幕截图所示:在 Mac OS 上安装 Redis

Mac 分发的文件夹结构

  1. 输入src/redis-server命令;这将启动服务器,如下截图所示:在 Mac OS 上安装 Redis

在苹果环境中启动 Redis 服务器

  1. 您的 Redis 服务器正在运行,并且准备接受端口 6379 上的请求。打开另一个终端并转到安装 Redis 的同一文件夹。输入命令src/redis-client;这将启动客户端 shell,如下截图所示:在 Mac OS 上安装 Redis

在苹果环境中启动 Redis 客户端

  1. 您的客户端已准备就绪,您已准备好进行 Hello World 程序,但在继续之前,最好先了解一下名为redis.conf的配置文件。

redis.conf 简介

Redis 附带redis.windows.conf文件,位于解压分发的 ZIP/tar 文件时创建的父文件夹中。可以通过此配置文件对服务器在启动时需要的任何自定义进行设置。如果需要包含redis.conf文件,则在服务器启动时提供文件路径作为参数。

当您在启动时提供配置文件时,命令提示符将显示以下消息:

redis.conf 简介

Redis 服务器在启动时使用配置路径进行启动

如前所述,Redis 是基于 Unix 的软件,已移植到 Windows 环境。许多配置参数是为 Unix 环境而设的;然而,了解在转移到基于 Unix 的环境时对您有益的参数总是好的。这些参数如下所述:

  • Port 6379:这个数字表示服务器将监听在端口 6379 上的消息。此端口号可以根据您的项目设置进行更改,并且服务器将在该端口上监听消息。这将需要重新启动服务器。

  • # bind 127.0.0.1:这是您希望服务器绑定的 IP 地址。默认情况下,此参数已被注释,这意味着服务器将监听所有接口的消息。

  • Timeout 0:这意味着如果客户端处于空闲状态,服务器将不会关闭连接。

  • tcp-keepalive 0:这是向服务器发送的命令,以保持与客户端的连接开放。您可以将其设置为SO_KEEPALIVE,这将指示服务器向客户端发送ACK消息。

  • loglevel notice:这是您希望服务器具有的日志级别。您可以拥有的日志级别包括 debug、verbose、notice 和 warning。

  • logfile stdout:这是您希望将日志消息发送到的通道,在 Windows 中为命令行,Unix-based 系统中为终端。

  • syslog-enabled no:如果更改为yes,则会将消息发送到系统日志。

  • dir:这应设置为用户希望运行 Redis 服务器的工作目录。这将告诉 Redis 服务器适当地创建文件,如服务器文件。

其余的配置参数可以视为高级参数,在后续章节中需要时我们将使用大部分。

Redis 中的 Hello World

这一部分将最激发程序员的兴趣。让我们动手写一些代码。但在此之前,我们必须了解 Redis 是基于客户端-服务器模型工作的,并使用 Redis 协议与服务器通信。为了客户端连接到服务器,客户端必须知道服务器的位置。在本节中,我将展示使用 redis-cli 和 Java 客户端的示例。

使用 redis-cli 进行 Hello World

启动 Redis 客户端命令提示符(确保服务器正在运行)。输入以下命令,如下截图所示,并查看结果:

使用 redis-cli 进行 Hello World

尝试使用 Redis 客户端进行简单的 Set 和 Get 命令

我们写的命令有三个部分。它们的解释如下:

  • Set:此命令用于在 Redis 服务器中设置值

  • MSG:这是要存储在 Redis 服务器中的消息的键

  • Hello World:这是存储在服务器上MSG键的值

因此,这清除了我们在使用 Redis 时必须记住的一个模式。请记住,Redis 是一个键值 NoSQL 数据存储。其语法为COMMAND <space> KEY <space> VALUE

继续进行Hello world程序,我们将做更多的事情。让我们输入set MSG Learning Redis,我们会收到一个错误消息,当我们输入set MSG "Hello World"时,服务器将返回的值是OK

使用 redis-cli 的 Hello World

用新值覆盖键

给定键的旧值被新值覆盖。让我们为这个示例添加另一个维度,即打开另一个客户端以打开我们已经打开的客户端命令提示符。在第二个命令提示符中,让我们将命令和键输入为get MSG。它将返回的值再次是"Hello World"。如下截图所示:

使用 redis-cli 的 Hello World

在一个客户端中写入并在另一个客户端中读取

此时,人们会想知道如果我们将一个数字作为值写入,也许是为了存储一些时间戳,而不是一个字符串,会发生什么。

让我们将新命令的键值设为set new_msg 1234,当我们写入命令键以检索值为get new_msg时,我们得到结果"1234"。注意值周围的双引号;这告诉我们有关 Redis 以及它存储数据的方式的更多信息,即 Redis 中存储的每个值都是字符串类型:

使用 redis-cli 的 Hello World

将整数值作为字符串获取

redis-cli 工具非常适用于调试解决方案并执行命令以检查系统和解决方案。

需要回答的下一个问题是如何以编程方式访问 Redis。

使用 Java 的 Hello World

在上一节中,您学习了如何使用redis-cli.exe应用程序来连接到 Redis 服务器。在本节中,我们将介绍一个 Java 客户端 API 来连接到 Redis 服务器并执行一些命令。实际上,要在解决方案中使用 Redis,需要一个 API 来连接服务器。除了连接到服务器、传递命令和命令参数以及返回结果之外,API 还需要一些其他属性,但我们将在后面的章节中进行介绍。

本书中选择的用于演示示例的 Java 客户端 API 是 Jedis。

在 Java 中运行Hello World示例有三个步骤。它们将在接下来的章节中进行解释。

安装 Jedis 并创建环境

Jedis是 Redis 的Apache 许可 2.0 Java 客户端。本书中演示示例将使用此客户端。因此,最重要的是确保您拥有开发环境。对于本书,我们选择了 Eclipse 作为开发环境(www.eclipse.org/downloads/)。如果您没有 Eclipse,可以获取并安装它(它是免费的和有许可的)。本书的示例同样适用于其他集成开发环境。现在,执行以下步骤:

  1. 打开 Eclipse 并创建一个名为learning redis的项目,如下截图所示:安装 Jedis 并创建环境

在 Eclipse 中创建一个项目

  1. 如果您使用 Maven,则为 Jedis 添加以下依赖项:安装 Jedis 并创建环境

Jedis 的 Maven 依赖项

如果您使用其他构建工具,请按照说明相应地添加 Jedis 的依赖项。

编写程序

以下 Java 程序是使用 Redis 作为数据存储的:

 package org.learningredis.chapter.two;

import redis.clients.jedis.*;

public class HelloWorld {
  private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");

  private void test() {
    try 
        { 
            Jedis jedis = pool.getResource(); 
            jedis.set("MSG", "Hello World"); 
            String result = jedis.get("MSG"); 
            System.out.println(" MSG : " + result); 
            pool.returnResource(jedis); 

        } 
        catch (Exception e) 
        { 
            System.err.println(e.toString()); 
        }finally{
             pool.destroy(); 
        }

  } 

    public static void main(String args[]) 
    { 
        HelloWorld helloWorld = new HelloWorld();
        helloWorld.test();
    }

}

确保您的 Redis 服务器正在运行。在此示例中,使用的端口是默认端口 6379。

让我们逐步了解程序中正在进行的操作:

  1. 我们正在设置一个连接池,以连接到 Redis 服务器。池配置为服务器将绑定到的默认 IP 地址。

  2. 我们从池中获取资源(包装连接的客户端存根)。

  3. 我们将键值设置到其中。这将推送要插入到 Redis 数据存储中的值。

  4. 我们根据键获取值。在这种情况下,是根据前一步中插入的键的值。

  5. 我们将资源返回到池中以便重用,并关闭池。

关闭服务器

与任何服务器一样,优雅地关闭服务器非常重要。在关闭任何 Redis 服务器之前,需要牢记几件事,这里进行了解释:

  1. 关闭所有客户端连接。对于我们的 Java 程序,我们通过编写"pool.destoy();"来指示客户端关闭所有连接。

  2. 我们需要做的下一件事是转到客户端提示符并命令服务器关闭。

  3. 如果您打算将 Redis 用作缓存服务器,则无需保存其持有的数据。在这种情况下,只需键入shutdown nosave。这将清除内存中的所有数据并释放它。

  4. 如果您打算保存数据以便以后使用,那么您必须传递shutdown save命令。即使没有配置保存点,这将使数据持久化在RDB文件中,我们将在后面的章节中介绍。

以下图显示了从资源生命周期的角度来看示例中发生的情况:

关闭服务器

为 Jedis 客户端管理资源

在生命周期中,我们必须考虑三种资源。它们的解释如下:

  • Jedis 连接池:这是系统/应用程序启动时应该创建的池。这为池分配资源。应用程序服务器生命周期应该管理池的生命周期。

  • 连接:在 Jedis 中,创建的客户端存根包装了连接并充当 Redis 的客户端。在前面列出的程序中,客户端存根被引用为Jedis,它是在pool.getResource()语句中获取的。

  • 请求生命周期:这是命令正在执行的地方。因此,基本上在这里发生的是使用 Redis 协议,将命令和有效负载发送到服务器。有效负载包括键(如果是“getter”)或键和值(如果是“setter”)。生命周期由服务器的积极确认来管理。如果失败,它可能是成功或异常。在某种程度上,我们不需要为此语句进行显式的生命周期管理。

我们如何在 Jedis 中管理连接,如果我们不管理它们会发生什么?

对于问题“如果我们不管理它会发生什么”,答案很简单。池将耗尽连接,客户端应用程序将受到影响。我们在诸如 JDBC 之类的领域中遇到了与连接相关的问题,当客户端没有连接可连接到服务器时,应用程序会受到影响。总是服务器为连接保留内存,并关闭连接是服务器释放内存的指示。

对于问题“我们如何在 Jedis 中管理连接”的答案有点有趣,并且需要一些代码更改。我们将采用先前的代码示例并对其进行更改,其中我们将处理连接资源管理。对于以下示例,我正在添加一个包装器,但在您的应用程序中,您可以使用更奇特的方法来解决提到的问题。也就是说,您可以使用 Spring 来注入连接,或者使用cglib动态创建代理,在命令之前设置连接并在命令之后返回连接。

以下代码是新的 Hello World 程序,现在称为HelloWorld2

package org.learningredis.chapter.two;

public class Helloworld2  {
  JedisWrapper jedisWrapper = null;
  public Helloworld2() {
    jedisWrapper = new JedisWrapper();
  }

  private void test() {
    jedisWrapper.set("MSG", "Hello world 2 ");

    String result = jedisWrapper.get("MSG");
    System.out.println("MSG : " + result);
  }

  public static void main(String[] args) {
    Helloworld2 helloworld2 = new Helloworld2();
    helloworld2.test();
  }
}

以下是处理连接的包装器代码:

package org.learningredis.chapter.two;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import redis.clients.jedis.JedisPoolConfig;

public class JedisWrapper {
  static JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");");");");");

  public void set(String key,String value){
    Jedis jedis = pool.getResource(); 
        jedis.set(key, value); 
        pool.returnResource(jedis);
  }

  public String get(String key){
    Jedis jedis = pool.getResource(); 
        String result = jedis.get("MSG"); ");");");"); 
        pool.returnResource(jedis);
        return result;
  }
}

在这种情况下,有两件事变得清楚,这里进行了解释:

  • 我们不必管理连接/资源,因为这将由“包装器”类来处理

  • 代码行数减少了,因为我们不必重复资源管理的代码

在 Redis 中加载一个测试 Hello World 程序

好吧,您已经看到了 Java 和命令行中的Hello world程序的示例。但是为您的Hello World程序添加一个负载测试维度总是很好的。Redis 附带了一个名为redis-benchmark的工具,可以在发布文件夹中找到。

以下命令将对 Redis 服务器进行 10 万次调用:

在 Redis 中加载一个测试 Hello World 程序

Hello World 的负载测试

结果是您的机器每秒处理的请求总数。这个工具对于负载测试您的目标环境非常有用。这是我在 Windows 机器上执行时得到的结果的快照,这将根据您的机器和操作系统的配置而有所不同:

在 Redis 中加载一个测试 Hello World 程序

工具执行负载测试

这里发生的是redis-benchmark打开了 50 个并行连接到 Redis 服务器并发送了 1 万个请求。请求包含 Redis 命令和 3 字节的有效负载。近似结果被打印出来进行分析;在我的情况下,1 万个Set命令总共花费了 0.30 秒,也就是说,每秒处理了 33,670 个请求。

总结

Redis 是一个简单的面向键值的 NoSQL,可以用作缓存服务器和持久化服务器。本章展示了在多个环境中安装 Redis 是多么简单,包括 Windows(Redis 也可以在云环境中使用,如 Amazon EC2)。Windows 的安装仅用于开发和抽样目的。

Redis 具有一组有趣的数据结构,有时被称为数据结构服务器。下一章将详细介绍数据结构。

第三章:Redis 中的数据结构和通信协议

上一章介绍了安装 Redis 和运行一些简单程序。由于 Redis 是一个数据存储,因此了解 Redis 如何通过提供数据结构来处理和存储数据是很重要的。同样重要的是 Redis 在将数据传输给客户端时如何处理数据,比如通信协议。

数据结构

数据结构,顾名思义,意味着用于存储数据的结构。在计算世界中,数据总是以一种对存储它的程序有意义的方式组织的。数据结构可以从简单的字符顺序排列到复杂的地图,其中键不是顺序排列的,而是基于算法的。数据结构通常是复合的,这意味着一个数据结构可以容纳另一个数据结构,这是一个包含另一个地图的地图。

设计数据结构的关键影响因素是数据结构的性能和内存管理。一些常见的数据结构示例包括列表,集合,地图,图和树,元组等。作为程序员,我们一次又一次地在程序中使用数据结构。在面向对象的世界中,一个简单的对象也是一个数据结构,因为它包含数据和访问这些数据的逻辑。每个数据结构都受算法的控制,算法决定了其效率和功能能力。因此,如果算法可以分类,那么它将清楚地表明数据结构的性能;当数据被注入数据结构或当数据被读取或从数据结构中删除时。

大 O 表示法是一种对算法(数据结构)在数据增长时性能进行分类的方法。从 Redis 的角度来看,我们将根据以下符号对数据结构进行分类:

  • O(1):命令在数据结构上花费的时间是恒定的,不管它包含多少数据。

  • O(N):命令在数据结构上花费的时间与其包含的数据量成线性比例,其中N是元素的数量。

  • O(log(N)):命令在数据结构上花费的时间是对数性质的,其中N是元素的数量。表现出这种特性的算法非常高效,用于在排序数组中查找元素。这可以解释为随着时间的推移而相当恒定。

  • O(log(N)+ M):命令花费的时间取决于对数值,其中M是排序集中的元素总数,N是搜索范围。这可以解释为相当依赖于M的值。随着M值的增加,搜索所需的时间也会增加。

  • O(M log(M)):命令花费的时间是对数线性的。

Redis 中的数据类型

Redis 是一个数据结构服务器,具有许多内置数据类型,这使得它与生态系统中的其他键值 NoSQL 数据存储有所不同。与其他 NoSQL 不同,Redis 为用户提供了许多内置数据类型,这提供了一种语义方式来安排其数据。可以这样想:在设计解决方案时,我们需要领域对象,这些对象在某种程度上塑造了我们的数据层。在决定领域对象之后,我们需要设计要保存在数据存储中的数据的结构,为此我们需要一些预定义的数据结构。这样做的好处是节省了程序员外部创建和管理这些数据的时间和精力。例如,假设在我们的程序中需要一种类似 Set 的数据结构。使用 Java,我们可以轻松地使用内置数据结构,如 Set。如果我们要将这些数据作为键值存储,我们将不得不将整个集合放在一个键值对中。现在,如果我们要对这个集合进行排序,通常的方法是提取数据并以编程方式对数据进行排序,这可能很麻烦。如果数据存储本身提供了内部对数据进行排序的机制,那就太好了。Redis 内置了以下数据类型来存储数据:

  • 字符串

  • 哈希

  • 列表

  • 集合

  • 有序集合

以下图表示可以映射到键的数据类型。在 Redis 中,键本身是字符串类型,它可以存储其中的任何一个,如下所示:

Redis 中的数据类型

键和其可以存储的值的表示

字符串数据类型

字符串类型是 Redis 中的基本数据类型。尽管术语上有些误导,但在 Redis 中,字符串可以被视为一个可以容纳字符串、整数、图像、文件和可序列化对象的字节数组。这些字节数组在性质上是二进制安全的,它们可以容纳的最大大小为 512MB。在 Redis 中,字符串被称为Simple Dynamic StringSDS),在 C 语言中实现为Char数组,还有一些其他属性,如lenfree。这些字符串也是二进制安全的。SDS 头文件在sds.h文件中定义如下:

struct sdshdr {
            long len;
           long free;
              char buf[];
          };

因此,Redis 中的任何字符串、整数、位图、图像文件等都存储在buf[]Char数组)中,len存储缓冲数组的长度,free存储额外的字节以进行存储。Redis 具有内置机制来检测数组中存储的数据类型。有关更多信息,请访问redis.io/topics/internals-sds

Redis 中的命令可以按以下部分对字符串进行分类:

  • 设置器和获取器命令:这些是用于在 Redis 中设置或获取值的命令。有单个键值和多个键值的命令。对于单个获取和设置,可以使用以下命令:

  • Get key:获取键的值。该命令的时间性能为O(1)

  • Set key:该键设置一个值。该命令的时间性能为O(1)

  • SETNX key:如果键不存在,则该键设置一个值 - 不会覆盖。该命令的时间性能为O(1)

  • GETSET key:获取旧值并设置新值。该命令的时间性能为O(1)

  • MGET key1 key:获取所有键的相应值。该命令的时间性能为O(N)

  • MSET key:设置所有键的相应值。该命令的时间性能为O(N),其中N是要设置的键的数量。

  • MSETNX key:如果所有键都不存在,则设置所有键的相应值,即如果一个键存在,则不设置任何值。该命令的时间性能为O(N),其中N是要设置的键的数量。

  • 数据清理命令:这些是用于管理值的生命周期的命令。默认情况下,密钥的值没有到期时间。但是,如果您有一个值需要具有生命周期的用例,那么请使用以下密钥:

  • SET PX/ EX:在到期时间后删除值,密钥在毫秒后过期。该命令的时间性能为O(1)

  • SETEX:在到期时间后删除值,密钥在秒后过期。该命令的时间性能为O(1)

  • 实用命令:以下是一些这些命令:

  • APPEND:此命令将附加到现有值,如果不存在则设置。该命令的时间性能为O(1)

  • STRLEN:此命令返回存储为字符串的值的长度。该命令的时间性能为O(1)

  • SETRANGE:此命令在给定的偏移位置上覆盖字符串。该命令的时间性能为O(1),前提是新字符串的长度不会花费太长时间来复制。

  • GETRANGE:此命令从给定的偏移量获取子字符串值。该命令的时间性能为O(1),前提是新子字符串的长度不会太大。

以下是一个演示字符串命令简单用法的示例程序。执行程序并自行分析结果。

package org.learningredis.chapter.three.datastruct;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class MyStringTest {
  private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
  Jedis jedis = null;

  public Jedis getResource() {
    jedis = pool.getResource();
    return jedis;
  }
  public void setResource(Jedis jedis){
    pool.returnResource(jedis);
  }
  public static void main(String[] args) throws InterruptedException {
    MyStringTest myStringTest  = new MyStringTest();
    myStringTest.test();

  }
  private void test() throws InterruptedException {
    Jedis jedis = this.getResource();
    String commonkey = "mykey";
    jedis.set(commonkey, "Hello World");
    System.out.println("1) "+jedis.get("mykey"));
    jedis.append(commonkey, " and this is a bright sunny day ");
    System.out.println("2) "+jedis.get("mykey"));
    String substring=jedis.getrange(commonkey, 0 , 5);
    System.out.println("3) "+"substring value = "+substring);
    String commonkey1 = "mykey1";
    jedis.set(commonkey1, "Let's learn redis");
    for(String value : jedis.mget(commonkey,commonkey1)){
      System.out.println("4) "+" - "+ value);
    }
    jedis.mset("mykey2","let's start with string","mykey3","then we will learn other data types");
    for(String value : jedis.mget(commonkey,commonkey1,"mykey2","mykey3")){
      System.out.println("5) "+"   -- "+ value);
    }
    jedis.msetnx("mykey4","next in line is hashmaps");
    System.out.println("6) "+jedis.get("mykey4"));
    jedis.msetnx("mykey4","next in line is sorted sets");
    System.out.println("7) "+jedis.get("mykey4"));
    jedis.psetex("mykey5", 1000, "this message will self destruct in 1000 milliseconds");
    System.out.println("8) "+jedis.get("mykey5"));
    Thread.currentThread().sleep(1200);
    System.out.println("8) "+jedis.get("mykey5"));
    Long length=jedis.strlen(commonkey);
    System.out.println("9) "+" the length of the string 'mykey' is " + length);
    this.setResource(jedis);
  }
}

Redis 中的整数和浮点数的命令可以分为以下部分:

  • 设置器和获取器命令:命令集与字符串中提到的相同。

  • 数据清理命令:命令集与字符串中提到的相同。

  • 实用命令:这里的命令将帮助操作整数和浮点值。对于整数,此操作仅限于 64 位有符号整数:

  • APPEND:这将将现有整数与新整数连接。该命令的时间性能为O(1)

  • DECR:这将使值减少一。该命令的时间性能为O(1)

  • DECRBY:这将使值减少给定的值。该命令的时间性能为O(1)

  • INCR:这将使值增加一。该命令的时间性能为O(1)

  • INCRBY:这将使值增加给定的值。该命令的时间性能为O(1)

  • INCRBYFLOAT:这将使值增加给定的浮点值。该命令的时间性能为O(1)

除了常规数字、字符串等,字符串数据类型可以存储一种称为BitSetbitmap的特殊数据结构。让我们更多地了解它们并看看它们的用法。

BitSet 或位图数据类型

这些是特殊的高效利用空间的数据结构类型,用于存储特殊类型的信息。位图特别用于实时分析工作。尽管位图只能存储二进制值(1 或 0),但它们占用的空间较少,获取值的性能为O(1),这使它们对实时分析非常有吸引力:

BitSet 或位图数据类型

位图的表示

密钥可以是任何基于日期的密钥。假设这里的密钥表示了 2014 年 12 月 12 日购买书籍的用户的位图。

例如,12/12/2014-user_purchased_book_learning_redis。这里的偏移量表示与用户关联的唯一整数 ID。这里我们有与数字 0、1、2...n 等关联的用户。每当用户购买商品时,我们找到用户的相应唯一 ID,并在该偏移位置将值更改为1

以下问题可以借助这个空间优化、高性能的位图来回答:

  • 2014 年 12 月 12 日有多少次购买?

答案:计算位图中的 1 的数量,即进行购买的用户数量,例如 9。

  • 与 ID(偏移编号)为 15 的用户是否进行了购买?

答案:偏移量为 15 的值为 0,因此用户没有购买。

这些位图的集合可以用于联合,以找到更复杂的分析答案。让我们向现有样本添加另一个位图,例如称为12/12/2014-user_browsed_book_learning_redis。使用这两个位图,我们可以找到以下问题的答案:

  • 浏览产品(学习 Redis)页面的用户有多少?

  • 购买产品(学习 Redis)页面的用户有多少?

  • 浏览产品页面的用户中有多少人购买了这本书?

  • 没有浏览产品页面的用户购买了这本书有多少?

使用情况场景

Redis 字符串可用于存储对象 ID。例如,会话 ID、XML、JSON 等配置值。Redis 字符串(存储整数)可用作原子计数器。Redis 字符串(存储位图)可用作实时分析引擎。

哈希数据类型

哈希是 Redis 中类似 Java 中的映射的版本。Redis 中的哈希用于存储属性和它们的值的映射。为了更好地理解,假设我们有一个名为学习 Redis的对象;这个对象将有许多属性,比如作者、出版商、ISBN 号等。为了在存储系统中表示这些信息,我们可以将信息存储为 XML、JSON 等,并与我们的键学习 Redis相关联。如果我们需要某个特定的值,例如存储在学习 Redis中的作者,那么就必须检索整个数据集,并筛选出所需的值。以这种方式工作将不高效,因为需要大量数据通过网络传输,并且客户端的处理会增加。Redis 提供了哈希数据结构,可以用于存储这种类型的数据。以下图示出了前面示例的图解表示:

哈希数据类型

哈希数据类型

哈希以占用更少的空间存储,Redis 中的每个哈希可以存储多达 2³²个字段-值对,即超过 40 亿。

哈希中的命令以H开头,Redis 中哈希的命令可以分为以下几部分:

  • 设置器和获取器命令:以下是此命令:

  • HGET:该命令获取键的字段的值。该命令的基于时间的性能为O(1)

  • HGETALL:该命令获取键的所有值和字段。该命令的基于时间的性能为O(1)

  • HSET:该命令为键的字段设置值。该命令的基于时间的性能为O(1)

  • HMGET:该命令获取键的字段的值。该命令的基于时间的性能为O(N),其中N是字段的数量。但是,如果N很小,则为O(1)

  • HMSET:该命令为键的各个字段设置多个值。该命令的基于时间的性能为O(N),其中N是字段的数量。但是,如果N很小,则为O(1)

  • HVALS:该命令获取键的哈希中的所有值。该命令的基于时间的性能为O(N),其中N是字段的数量。但是,如果N很小,则为O(1)

  • HSETNX:该命令为提供的键设置字段的值,前提是该字段不存在。该命令的基于时间的性能为O(1)

  • HKEYS:该命令获取键的哈希中的所有字段。该命令的基于时间的性能为O(1)

  • 数据清理命令:以下是此命令:

  • HDEL:该命令删除键的字段。该命令的基于时间的性能为O(N),其中N是字段的数量。但是,如果N很小,则为O(1)

  • 实用命令:以下是此命令:

  • HEXISTS:该命令检查键的字段是否存在。该命令的基于时间的性能为O(1)

  • HINCRBY:此命令递增键的字段的值(假设该值是整数)。此命令的基于时间的性能为O(1)

  • HINCRBYFLOAT:此命令递增键的字段的值(假设该值是浮点数)。此命令的基于时间的性能为O(1)

  • HLEN:此命令获取键的字段数。此命令的基于时间的性能为O(1)

以下是一个演示哈希命令简单用法的示例程序。执行程序并自行分析结果。

  package org.learningredis.chapter.three.datastruct;
import java.util.HashMap;
import java.util.Map;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class MyHashesTest {
  private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
  Jedis jedis = null;

  public Jedis getResource() {
    jedis = pool.getResource();
    return jedis;
  }
  public void setResource(Jedis jedis){
    pool.returnResource(jedis);
  }
  public static void main(String[] args) 
throws InterruptedException  {
    MyHashesTest myHashesTest  = new MyHashesTest();
    myHashesTest.test();    
  }
  private void test() {
    Jedis jedis = this.getResource();
    String commonkey = "learning redis";
    jedis.hset(commonkey, "publisher", "Packt Publisher");
    jedis.hset(commonkey, "author", "Vinoo Das");
    System.out.println(jedis.hgetAll(commonkey));
Map<String,String> attributes = new HashMap<String,String>();
    attributes.put("ISBN", "XX-XX-XX-XX");
    attributes.put("tags", "Redis,NoSQL");
    attributes.put("pages", "250");
    attributes.put("weight", "200.56");
    jedis.hmset(commonkey, attributes);
    System.out.println(jedis.hgetAll(commonkey));
    System.out.println(jedis.hget(commonkey,"publisher"));
    System.out.println(jedis.hmget(commonkey,"publisher","author"));
    System.out.println(jedis.hvals(commonkey));
    System.out.println(jedis.hget(commonkey,"publisher"));
    System.out.println(jedis.hkeys(commonkey));
    System.out.println(jedis.hexists(commonkey, "cost"));
    System.out.println(jedis.hlen(commonkey));
    System.out.println(jedis.hincrBy(commonkey,"pages",10));
    System.out.println(jedis.hincrByFloat(commonkey,"weight",1.1) + " gms");
    System.out.println(jedis.hdel(commonkey,"weight-in-gms"));
    System.out.println(jedis.hgetAll(commonkey));
    this.setResource(jedis);
  }
}

使用案例场景

哈希提供了一种语义接口,用于在 Redis 服务器中存储简单和复杂的数据对象。例如,用户配置文件,产品目录等。

列表数据类型

Redis 列表类似于 Java 中的链表。Redis 列表可以在头部或尾部添加元素。执行此操作的性能是恒定的,或者可以表示为O(1)。这意味着,假设您有一个包含 100 个元素的列表,添加元素到列表所需的时间等于添加元素到包含 10,000 个元素的列表所需的时间。但另一方面,访问 Redis 列表中的元素将导致整个列表的扫描,这意味着如果列表中的项目数量较多,则性能会下降。

Redis 列表以链表形式实现的优势在于,Redis 列表作为一种数据类型,设计为具有比读取更快的写入速度(这是所有数据存储所显示的特性)。

列表数据类型

列表数据类型

Redis 中的列表命令通常以L开头。这也可以解释为所有命令将从列表的左侧或头部执行,而从列表的右侧或尾部执行的命令则以 R 开头。这些命令可以分为以下几个部分:

  • 设置器和获取器命令:以下是此类命令的示例:

  • LPUSH:此命令从列表的左侧添加值。此命令的基于时间的性能为O(1)

  • RPUSH:此命令从列表的右侧添加值。此命令的基于时间的性能为O(1)

  • LPUSHX:此命令如果键存在,则从列表的左侧添加值。此命令的基于时间的性能为O(1)

  • RPUSHX:此命令如果键存在,则从列表的右侧添加值。此命令的基于时间的性能为O(1)

  • LINSERT:此命令在枢轴位置之后插入一个值。此枢轴位置是从左边计算的。此命令的基于时间的性能为O(N)

  • LSET:此命令根据指定的索引在列表中设置元素的值。此命令的基于时间的性能为O(N)

  • LRANGE:此命令根据起始索引和结束索引获取元素的子列表。此命令的基于时间的性能为O(S+N)。这里S是偏移的起始位置,N是我们在列表中请求的元素数量。这意味着,如果偏移离头部越远,范围的长度越大,查找元素所需的时间就会增加。

  • 数据清理命令:以下是此类命令的示例:

  • LTRIM:此命令删除指定范围之外的元素。此命令的基于时间的性能为O(N)。这里的N是列表的长度。

  • RPOP:此命令删除最后一个元素。此命令的基于时间的性能为O(1)

  • LREM:此命令删除指定索引点的元素。此命令的基于时间的性能为O(N)。这里的N是列表的长度。

  • LPOP:此命令删除列表的第一个元素。此命令的基于时间的性能为O(1)

  • 实用命令:以下是此类命令的示例:

  • LINDEX:此命令根据指定的索引获取列表中的元素。该命令的时间性能为O(N)。这里的N是要遍历以达到所需索引处的元素数量。

  • LLEN:此命令获取列表的长度。该命令的时间性能为O(1)

  • 高级命令:此类型包括以下命令:

  • BLPOP:此命令从所述列表序列中的非空索引处获取元素,如果头部没有值,则阻止调用,直到至少设置一个值或超时发生。BLPOP中的字母B提示此调用是阻塞的。该命令的时间性能为O(1)

  • BRPOP:此命令从所述列表序列中的尾部获取元素,如果头部没有值,则阻止调用,直到至少设置一个值或超时发生。该命令的时间性能为O(1)

  • RPOPLPUSH:此命令作用于两个列表。假设源列表和目标列表,它将获取源列表的最后一个元素并将其推送到目标列表的第一个元素。该命令的时间性能为O(1)

  • BRPOPLPUSH:此命令是RPOPLPUSH命令的阻塞变体。在这种情况下,如果源列表为空,则 Redis 将阻止操作,直到将一个值推送到列表中或达到超时。这些命令可用于创建队列。该命令的时间性能为O(1)

以下是一个演示列表命令简单用法的示例程序。执行程序并自行分析结果:

package org.learningredis.chapter.three.datastruct;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.BinaryClient.LIST_POSITION;
public class MyListTest {
  private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
  Jedis jedis = null;
  public Jedis getResource() {
    jedis = pool.getResource();
    return jedis;
  }
  public void setResource(Jedis jedis){
    pool.returnResource(jedis);
  }
  public static void main(String[] args) throws InterruptedException {
    MyListTest myListTest  = new MyListTest();
    myListTest.test();
  }
  private void test() {
    Jedis jedis = this.getResource();
    System.out.println(jedis.del("mykey4list"));
    String commonkey="mykey4list";
    String commonkey1="mykey4list1";
    for(int index=0;index<3;index++){
      jedis.lpush(commonkey, "Message - " + index);
    }
    System.out.println(jedis.lrange(commonkey, 0, -1));
    for(int index=3;index<6;index++){
      jedis.rpush(commonkey, "Message - " + index);
    }
    System.out.println(jedis.lrange(commonkey, 0, -1));
    System.out.println(jedis.lindex(commonkey, 0));
    System.out.println(jedis.linsert(commonkey,LIST_POSITION.AFTER,"Message - 5", "Message - 7"));
    System.out.println(jedis.lrange(commonkey, 0, -1));
    System.out.println(jedis.linsert(commonkey,LIST_POSITION.BEFORE,"Message - 7", "Message - 6"));
    System.out.println(jedis.lrange(commonkey, 0, -1));
    System.out.println(jedis.llen(commonkey));
    System.out.println(jedis.lpop(commonkey));
    System.out.println(jedis.lrange(commonkey, 0, -1));
    System.out.println(jedis.lpush(commonkey,"Message - 2","Message -1.9"));
    System.out.println(jedis.lrange(commonkey, 0, -1));
    System.out.println(jedis.lpushx(commonkey,"Message - 1.8"));
    System.out.println(jedis.lrange(commonkey, 0, -1));
    System.out.println(jedis.lrem(commonkey,0,"Message - 1.8"));
    System.out.println(jedis.lrange(commonkey, 0, -1));
    System.out.println(jedis.lrem(commonkey,-1,"Message - 7"));
    System.out.println(jedis.lrange(commonkey, 0, -1));
    System.out.println(jedis.lset(commonkey,7,"Message - 7"));
    System.out.println(jedis.lrange(commonkey, 0, -1));
    System.out.println(jedis.ltrim(commonkey,2,-4));
    System.out.println(jedis.lrange(commonkey, 0, -1));
    jedis.rpoplpush(commonkey, commonkey1);
    System.out.println(jedis.lrange(commonkey, 0, -1));
    System.out.println(jedis.lrange(commonkey1, 0, -1));
  }
}

用例场景

列表提供了一个语义接口,用于在 Redis 服务器中按顺序存储数据,其中速度比性能更可取。例如,日志消息。

集合数据类型

Redis 集合是无序的 SDS 的数据结构集合。集合中的值是唯一的,不能有重复值。在 Redis 集合的性能方面,一个有趣的方面是,它们在添加、删除和检查元素存在方面显示出恒定的时间。集合中可以有的最大条目数为 2³²,即每个集合最多有 40 亿个条目。这些集合值是无序的。从外观上看,集合可能看起来像列表,但它们有不同的实现,这使它们成为解决集合理论问题的完美候选者。

集合数据类型

集合数据类型

Redis 中用于集合的命令可以分为以下几部分:

  • 设置器和获取器命令:此类型包括以下命令:

  • SADD:此命令向集合中添加一个或多个元素。该命令的时间性能为O(N)。这里的N是需要添加的元素数量。

  • 数据清理命令:以下是属于此类别的一些命令:

  • SPOP:此命令从集合中移除并返回一个随机元素。该命令的时间性能为O(1)

  • SREM:此命令从集合中移除并返回指定的元素。该命令的时间性能为O(N)。这里的N是要移除的元素数量。

  • 实用命令:以下是属于此类型的命令:

  • SCARD:此命令获取集合中的元素数量。该命令的时间性能为O(1)

  • SDIFF:此命令从其他所述集合中减去其元素后获取第一个集合的元素列表。该命令的时间性能为O(N)。这里的N是所有集合中的元素数量。

  • SDIFFSTORE:此命令从其他指定的集合中减去第一个集合的元素后获取元素列表。然后将这个集合推送到另一个集合中。该命令的基于时间的性能为O(N)。这里的N是所有集合中元素的数量。

  • SINTER:此命令获取所有指定集合中的公共元素。该命令的基于时间的性能为O(N*M)。这里的N是最小集合的基数,M是集合的数量。基本上是 Redis 将取最小集合,并查找该集合与其他集合之间的公共元素。然后再次比较结果集中的公共元素,如前述过程,直到只剩下一个集合包含所需的结果。

  • SINTERSTORE:此命令与SINTER命令的工作方式相同,但结果存储在指定的集合中。该命令的基于时间的性能为O(N*M)。这里的N是最小集合的基数,M是集合的数量。

  • SISMEMBER:此命令查找值是否是集合的成员。该命令的基于时间的性能为O(1)

  • SMOVE:此命令将成员从一个集合移动到另一个集合。该命令的基于时间的性能为O(1)

  • SRANDMEMBER:此命令从集合中获取一个或多个随机成员。该命令的基于时间的性能为O(N)。这里的N是传递的成员数量。

  • SUNION:此命令添加多个集合。该命令的基于时间的性能为O(N)。这里的N是所有集合中元素的数量。

  • SUNIONSTORE:此命令将多个集合添加到一个集合中并将结果存储在一个集合中。该命令的基于时间的性能为O(N)。这里的N是所有集合中元素的数量。

以下是用于集合的简单用法的示例程序。执行程序并自行分析结果:

package org.learningredis.chapter.three.datastruct;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class MySetTest {
  private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
  Jedis jedis = null;
  public Jedis getResource() {
    jedis = pool.getResource();
    return jedis;
  }
  public void setResource(Jedis jedis){
    pool.returnResource(jedis);
  }
  public static void main(String[] args) {
    MySetTest mySetTest = new MySetTest();
    mySetTest.test();
  }
  private void test() {
    Jedis jedis = this.getResource();
    jedis.sadd("follow:cricket", "vinoo.das@junk-mail.com","vinoo.das1@junk-mail.com","vinoo.das3@junk-mail.com");
    System.out.println(jedis.smembers("follow:cricket"));
    System.out.println(jedis.scard("follow:cricket"));
    jedis.sadd("follow:redis", "vinoo.das1@junk-mail.com","vinoo.das2@junk-mail.com");
    System.out.println(jedis.smembers("follow:redis"));
    System.out.println(jedis.scard("follow:redis"));
    // intersect the above sets to give name who is interested in cricket and redis
    System.out.println(jedis.sinter("Cricket:followers","follow:redis"));
    jedis.sinterstore("follow:redis+cricket","follow:cricket","follow:redis");
    System.out.println(jedis.smembers("follow:redis+cricket"));
    System.out.println(jedis.sismember("follow:redis+cricket", "vinoo.das@junk-mail.com"));
    System.out.println(jedis.sismember("follow:redis+cricket", "vinoo.das1@junk-mail.com"));
    jedis.smove("follow:cricket", "follow:redis", "vinoo.das3@junk-mail.com");
    System.out.println(jedis.smembers("follow:redis"));
    System.out.println(jedis.srandmember("follow:cricket"));
    System.out.println(jedis.spop("follow:cricket"));
    System.out.println(jedis.smembers("follow:cricket"));
    jedis.sadd("follow:cricket","wrong-data@junk-mail.com");
    System.out.println(jedis.smembers("follow:cricket"));
    jedis.srem("follow:cricket","wrong-data@junk-mail.com");
    System.out.println(jedis.smembers("follow:cricket"));
    System.out.println(jedis.sunion("follow:cricket","follow:redis"));
    jedis.sunionstore("follow:cricket-or-redis","follow:cricket","follow:redis");
    System.out.println(jedis.smembers("follow:cricket-or-redis"));
    System.out.println(jedis.sdiff("follow:cricket", "follow:redis"));
  }
}

使用情景

集合提供了一种语义接口,可以将数据存储为 Redis 服务器中的一个集合。这种类型数据的用例更多用于分析目的,例如有多少人浏览了产品页面,有多少最终购买了产品。

有序集合数据类型

Redis 有序集合与 Redis 集合非常相似,它们都不存储重复值,但它们与 Redis 集合不同的地方在于,它们的值是根据分数或整数、浮点值进行排序的。在设置集合中的值时提供这些值。有序集合的性能与元素数量的对数成正比。数据始终以排序的方式保存。这个概念在下图中以图表的方式解释:

有序集合数据类型

有序集合的概念

Redis 中有关有序集合的命令可以分为以下几个部分:

  • 设置器和获取器命令:以下是属于此类别的命令:

  • ZADD:此命令向有序集合中添加或更新一个或多个成员。该命令的基于时间的性能为O(log(N))。这里的N是有序集合中的元素数量。

  • ZRANGE:此命令根据有序集合中元素的排名获取指定范围。该命令的基于时间的性能为O(log(N)+M)。这里的N是元素的数量,M是返回的元素的数量。

  • ZRANGEBYSCORE:此命令根据给定的分数范围从有序集合中获取元素。默认集合中的值是按升序排列的。该命令的基于时间的性能为O(log(N)+M)。这里的N是元素的数量,M是返回的元素的数量。

  • ZREVRANGEBYSCORE:此命令根据给定的分数从有序集合中获取元素。该命令的基于时间的性能为O(log(N)+M)。这里的N是元素的数量,M是被移除的元素的数量。

  • ZREVRANK:此命令返回有序集合中成员的排名。该命令的基于时间的性能为O(log(N))

  • ZREVRANGE:此命令返回有序集合中指定范围的元素。该命令的基于时间的性能为O(log(N) + M)

  • 数据清理命令:以下是属于此类别的命令:

  • ZREM:此命令从有序集合中移除指定的成员。该命令的基于时间的性能为O(M*log(N))。这里的M是移除的元素数量,N是有序集合中的元素数量。

  • ZREMRANGEBYRANK:此命令在给定的索引范围内移除有序集合中的成员。该命令的基于时间的性能为O(log(N)*M)。这里的N是元素数量,M是被移除的元素数量。

  • ZREMRANGEBYSCORE:此命令在给定分数范围内移除有序集合中的成员。该命令的基于时间的性能为O(log(N)*M)。这里的N是元素数量,M是被移除的元素数量。

  • 实用命令:以下是属于此类别的命令:

  • ZCARD:此命令获取有序集合中成员的数量。该命令的基于时间的性能为O(1)

  • ZCOUNT:此命令获取有序集合中在分数范围内的成员数量。该命令的基于时间的性能为O(log(N)*M)。这里的N是元素数量,M是结果。

  • ZINCRBY:此命令增加有序集合中元素的分数。该命令的基于时间的性能为O(log(N))。这里的N是有序集合中的元素数量。

  • ZINTERSTORE:此命令计算由指定键给出的有序集合中的公共元素,并将结果存储在目标有序集合中。该命令的基于时间的性能为O(N*K) + O(M*log(M))。这里的N是最小的有序集合,K是输入集的数量,M是结果有序集合中的元素数量。

  • ZRANK:此命令获取有序集合中元素的索引。该命令的基于时间的性能为O(log(N))

  • ZSCORE:此命令返回成员的分数。该命令的基于时间的性能为O(1)

  • ZUNIONSTORE:此命令计算给定有序集合中键的并集,并将结果存储在结果有序集合中。该命令的基于时间的性能为O(N) + O(M log(M))。这里的N是输入有序集合大小的总和,M是有序集合中的元素数量。

以下是一个演示有序集合命令简单用法的示例程序。执行程序并自行分析结果:

package org.learningredis.chapter.three.datastruct;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class MySortedSetTest {
  private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
  Jedis jedis = null;
  public Jedis getResource() {
    jedis = pool.getResource();
    return jedis;
  }
  public void setResource(Jedis jedis){
    pool.returnResource(jedis);
  }
  public static void main(String[] args) {
    MySortedSetTest mySortedSetTest = new MySortedSetTest();
    mySortedSetTest.test();
  }
  private void test() {
    Jedis jedis = this.getResource();
    jedis.zadd("purchase", 0, "learning-redis");
    jedis.zadd("purchase", 0, "cassandra");
    jedis.zadd("purchase", 0, "hadoop");
    System.out.println(jedis.zcard("purchase"));
    // purchase a 4 books on redis
    jedis.zincrby("purchase", 1, "learning-redis");
    jedis.zincrby("purchase", 1, "learning-redis");
    jedis.zincrby("purchase", 1, "learning-redis");
    jedis.zincrby("purchase", 1, "learning-redis");
    // purchase a 2 books on cassandra
    jedis.zincrby("purchase", 1, "cassandra");
    jedis.zincrby("purchase", 1, "cassandra");
    // purchase a 1 book on hadoop
    jedis.zincrby("purchase", 1, "hadoop");
    System.out.println(jedis.zcount("purchase", 3, 4));
    System.out.println(jedis.zrange("purchase", 0, 2));
    System.out.println(jedis.zrangeByScore("purchase", 3, 4));
    System.out.println(jedis.zrank("purchase", "learning-redis"));
    System.out.println(jedis.zrank("purchase", "cassandra"));
    System.out.println(jedis.zrank("purchase", "hadoop"));
    System.out.println(jedis.zrevrank("purchase", "learning-redis"));
    System.out.println(jedis.zrevrank("purchase", "cassandra"));
    System.out.println(jedis.zrevrank("purchase", "hadoop"));
    System.out.println(jedis.zscore("purchase", "learning-redis"));
    System.out.println(jedis.zscore("purchase", "cassandra"));
    System.out.println(jedis.zscore("purchase", "hadoop"));
    jedis.zunionstore("purchase:nosql", "purchase");
    System.out.println("-- " + jedis.zrange("purchase:nosql",0,-1));
    System.out.println("-- " + jedis.zrank("purchase:nosql","learning-redis"));
    jedis.zrem("purchase:nosql", "hadoop");
    System.out.println("-- " + jedis.zrange("purchase:nosql",0,-1));
    jedis.zremrangeByRank("purchase:nosql", 0,0);
    System.out.println("-- " + jedis.zrange("purchase:nosql",0,-1));
    jedis.zremrangeByScore("purchase:nosql", 3,4);
    System.out.println("-- " + jedis.zrange("purchase:nosql",0,-1));
    this.setResource(jedis);
  }
}

使用情景

有序集合提供了一个语义接口,可以将数据存储为 Redis 服务器中的有序集合。这种数据的用例更多地用于分析目的和游戏世界中。例如,有多少人玩了特定的游戏,并根据他们的得分对他们进行分类。

通信协议 - RESP

Redis 原则上是基于客户端-服务器模型工作的。因此,就像在每个客户端-服务器模型中一样,客户端和服务器需要有一个通信协议。通信协议可以理解为基于客户端和服务器之间的某种固定协议或规则进行的消息交换。因此,每个通信协议都必须遵循一些语法和语义,这些语法和语义应该由双方(客户端和服务器)遵循,以便通信成功。此外,还有另一个维度的通信协议,即网络层交互,或者更为人所熟知的 TCP/IP 模型。TCP/IP 模型可以分为四个部分:

  • 应用层

  • 传输层

  • 互联网层

  • 网络接口

由于两个应用程序之间的通信协议位于应用层,因此我们打算只关注应用层。以下图表是应用层通信协议级别上发生的事情的表示:

通信协议 - RESP

应用层通信协议的表示

在任何应用程序协议中,我们都会有头部和主体。头部将包含有关协议的元信息,即协议名称、版本、安全相关细节、关于请求的元信息(参数数量、参数类型)等等,而主体将包含实际的数据。任何服务器的第一件事就是解析头部信息。如果头部成功解析,那么才会处理主体的其余部分。在某种程度上,服务器和客户端需要具有类似管道的架构来处理头部消息。

Redis 使用的协议相当简单易懂。本节将重点介绍 Redis 中使用的通信协议。在本节结束时,我们将了解协议并创建一个连接到 Redis 服务器的客户端,发送请求并从服务器获取响应。与任何其他协议一样,Redis 协议也有一个头部(元信息)和一个主体部分(请求数据)。请求数据部分包括命令和命令数据等信息。在响应中,它将包含元信息(如果请求成功或失败)和实际的响应数据负载。以下解释了这个概念:

通信协议 - RESP

元信息和请求数据的表示

Redis 中的任何请求基本上由两部分组成,并且它们如下所述:

  • 关于请求的元信息,比如参数数量

  • Body 部分还将有三个更多的信息:

  • 每个参数的字节数

  • 实际参数

  • 回车和换行CRLF

通信协议 - RESP

请求的主体部分的表示

因此,我们将在元信息中保存的信息将是两个,因为这是我们将传递的参数的数量,如前图所示。在主体部分,我们将捕获信息,例如我们发送的参数的字节数是多少,即如果参数的名称是GET,那么字节数将是3

Redis 中的响应可以分为两种类型:

  • 将去添加或操作数据的命令的响应(不期望返回值):

  • +符号表示请求成功

  • -符号表示请求失败

  • 将去获取数据的命令的响应(期望返回字符串类型的值):

  • 如果错误是响应,则$-1将是响应

  • $以及如果响应成功则响应的大小,然后是实际的字符串数据

作为练习,让我们用 Java 代码制作一个小型测试客户端,并记录我们与 Redis 服务器的交互。该客户端只是一个示例,用于教育 Redis 协议,并不打算替换我们在本书中将使用的客户端,即 Jedis。让我们简要概述参与小型测试客户端的类。首先,从设计模式的角度来看,我们将为此示例使用命令模式:

  • Command:这个类是抽象类,所有的命令类都将从这个类扩展。

  • GetCommand:这是一个类,将获取给定键的字符串值并打印服务器响应和响应值。

  • SetCommand:这是一个类,将为命令设置键和值数据并打印服务器响应。

  • ConnectionProperties:这是将保存主机和端口地址的接口。(这将更像是一个属性文件。)

  • TestClient:这是将调用所有命令的类。

以下是简单测试客户端应用程序的领域图。这里命令对象承担了大部分工作:

通信协议 - RESP

简单客户端应用程序的领域图

查看代码将更清楚地了解 Redis 的简单测试客户端:

  • ConnectionProperties.java:这是将保存主机和端口的配置值的类。
package org.learningredis.chapter.three;
public interface ConnectionProperties {
  public String host="localhost";
  public int   port =6379;
}
  • TestClient.java:如图所示,这是客户端,将执行设置值和获取值的命令。
package org.learningredis.chapter.three;
public class TestClient {
  public void execute(Command command){
      try{
        /*Connects to server*/
        command.excute();
      }catch(Exception e){
        e.printStackTrace();
      }
    }
  public static void main(String... args) {
    TestClient testclient = new TestClient();
    SetCommand set = new  SetCommand("MSG","Hello world : simple test client");
    testclient.execute(set);

    GetCommand get = new GetCommand("MSG");
    testclient.execute(get);
    }
}

如果一切顺利,您应该在控制台中看到以下消息。请记住,这是一个成功的操作,您的控制台应该类似于以下截图:

通信协议 - RESP

在这个示例中,我们连续执行了两个命令:

  • SetCommand

  • GetCommand

SetCommand的结果是+OK+符号表示服务器返回了成功的结果,后跟消息OK

GetCommand的结果是多行结果。第一行是字符串$32,表示结果的大小为 32 个字节,后跟结果Hello world : simple test client

现在,让我们尝试传递一个在Get命令中不存在的键。代码片段将看起来像下面显示的样子(在wrong-key中传递的键不存在):

package org.learningredis.chapter.three;
public class TestClient {
  public void execute(Command command){
      try{
        /*Connects to server*/
        command.excute();
      }catch(Exception e){
        e.printStackTrace();
      }
    }
  public static void main(String... args) {
    TestClient testclient = new TestClient();
    SetCommand set = new  SetCommand("MSG","Hello world : simple test client");
    testclient.execute(set);

    GetCommand get = new GetCommand("Wrong-key");
    testclient.execute(get);
    }
}

前面代码的结果应该看起来像$-1命令。这里返回值为 null,因为键不存在。因此长度为-1。接下来,我们将用更易读的方式包装这条消息,比如This key does not exist!以下是一些讨论过的类:

  • Command.java:这是所有命令都将扩展的抽象类。该类负责为实现命令实例化套接字,并创建要发送到 Redis 服务器的适当有效负载。了解这一点将给我们一个提示,即 Redis 服务器实际上是如何接受请求的。通信协议 - RESP

Command.java 的表示

第一个字符是*字符,后跟我们将传递的参数数量。这意味着如果我们打算执行Set命令,即SET MSG Hello,那么这里的参数总数是三。如果我们打算传递Get命令,比如GET MSG,那么参数数量是两。在参数数量之后,我们将使用CRLF作为分隔符。随后的消息将遵循一个重复的模式。这个模式非常简单易懂,即$后跟参数的字节长度,后跟CRLF,后跟参数本身。如果有更多的参数,那么将遵循相同的模式,但它们将由 CRLF 分隔符分隔。以下是Command.java的代码:

package org.learningredis.chapter.three;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
public abstract class Command {
  protected Socket socket;
  public Command() {
    try {
      socket = new Socket(ConnectionProperties.host,
          ConnectionProperties.port);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
public String createPayload(ArrayList<String> 
messageList) 
{
    int argumentSize = messageList.size();
    StringBuffer payload = new StringBuffer();
    payload.append('*');
    payload.append(argumentSize);
    payload.append("\r\n");
    for (int cursor = 0; cursor < messageList.size(); cursor++) {
      payload.append("$");
      payload.append(messageList.get(cursor).length());
      payload.append("\r\n");
      payload.append(messageList.get(cursor));
      payload.append("\r\n");
    }
    return payload.toString().trim();
  }
  public abstract String createPayload();
  public abstract void execute() throws IOException;
}

代码简单易懂,它对消息有效负载进行准备和格式化。Command类有两个抽象方法,需要由实现命令来实现。除此之外,Command类根据ConnectionProperties.java中设置的属性创建一个新的套接字。

  • GetCommand.java:这是实现GET KEY命令的类。该类扩展了Command.java。以下是GetCommand的源代码:
package org.learningredis.chapter.three;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
public class GetCommand extends Command{
  private String key;
  public GetCommand(String key) {
    this.key=key;
  }
  @Override
  public String createPayload() {
    ArrayList<String> messageList = new ArrayList<String>();
    messageList.add("GET");
    messageList.add(key);
    return super.createPayload(messageList);
  }
  @Override
  public void excute() throws IOException  {
    PrintWriter out = null;
    BufferedReader in=null;
    try {
    out = new PrintWriter(super.socket.getOutputStream(),true);
    out.println(this.createPayload());
      //Reads from Redis server
in = new BufferedReader(new 
          InputStreamReader(socket.getInputStream()));
          String msg=in.readLine();
          if (! msg.contains("-1")){
            System.out.println(msg);
            System.out.println(in.readLine());
          }else{
          // This will show the error message since the 
          // server has returned '-1'
          System.out.println("This Key does not exist !");
      }
    } catch (IOException e) {
      e.printStackTrace();
    }finally{
      out.flush();
      out.close();
      in.close();
      socket.close();
    }
  }
}

实现类原则上做两件事。首先,它将参数数组传递给超类,并将其格式化为 Redis 能理解的方式,然后将有效负载发送到 Redis 服务器并打印结果。

  • SetCommand:这与前一个命令类似,但在这个类中,我们将设置值。该类将扩展Command.java类。以下是SetCommand的源代码:
package org.learningredis.chapter.three;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
public class SetCommand extends Command{
  private String key;
  private String value;
  public SetCommand(String key, String value) {
    this.key=key;
    this.value=value;
  }
  public String createPayload(){
ArrayList<String> messageList = new 
                  ArrayList<String>();
    messageList.add("SET");
    messageList.add(key);
    messageList.add(value);
    return super.createPayload(messageList);
  }
  @Override
  public void excute() throws IOException  {
    PrintWriter    out = null;
    BufferedReader in = null;
    try {
      out = new
PrintWriter(super.socket.getOutputStream (), true);
      out.println(this.createPayload());
      //Reads from Redis server
      in = new BufferedReader(new 
      InputStreamReader(socket.getInputStream()));  
      // This is going to be a single line reply..
      System.out.println(in.readLine());
    } catch (IOException e) {
      e.printStackTrace();
    }finally{
      in.close();
      out.flush();
      out.close();
      socket.close();
    }
  }
}

这个命令与之前的命令类似,原则上有两个作用。首先,它将参数数组传递给超类,并使用适当的值对其进行格式化,以便 Redis 能够理解,然后将有效负载传递给 Redis 服务器并打印结果。

编译程序并运行它时要玩得开心;添加更多命令并扩展它以满足您的业务需求。我强烈建议使用 Jedis,因为它很稳定,社区非常活跃,并且提供了对 Redis 新版本引入的新命令的实现。

总结

在本章中,我们涉及了 Redis 提供的各种数据结构或数据类型。我们还编写了一些程序来查看它们的工作原理,并尝试理解这些数据类型可以如何使用。最后,我们了解了 Redis 如何与客户端进行通信,以及反之亦然。

在下一章中,我们将进一步加深对 Redis 服务器的理解,并尝试理解将处理它的功能。

第四章:Redis 服务器中的功能

在前几章中,我们看到了 Redis 服务器的一些特性,使其成为键值 NoSQL。我们还看到 Redis 除了存储原始键值之外,还提供了以结构化方式存储数据的语义。这个特性使 Redis 在众多数据库中脱颖而出,因为大多数其他数据库(关系型数据库和其他 NoSQL)都没有提供程序员可以使用的接口。其他数据存储有固定的存储信息方式,如文档或映射,程序员必须将他们的数据转换为这些语义来保存信息。然而,在 Redis 中,程序员可以以与他们在程序中使用的相同语义存储信息,如映射,列表等。这种方式提供了更好更容易理解程序的方式。除此之外,Redis 提供了功能,使其不仅仅是一个数据存储,更像是一个框架构建者,或者换句话说,更像是一把瑞士军刀。在本章中,我们将探讨这些功能并试图理解它们。

以下是我们将讨论的功能:

  • 实时消息传递(发布/订阅)

  • 管道

  • 事务

  • 脚本

  • 连接管理

实时消息传递(发布/订阅)

企业和社交媒体解决方案以类似的方式使用消息传递,从某种程度上说,这构成了任何框架或解决方案的支柱。消息传递还使我们能够拥有松散耦合的架构,其中组件通过消息和事件进行交互。Redis 提供了在组件之间进行实时消息传递的机制。与其他消息系统不同,Redis 中提供的消息模型的最大区别如下:

  • 在传递消息后不会存储消息

  • 如果客户端(订阅者)无法消费消息,则不会存储消息

与传统消息系统相比,这可能是一个缺点,但在数据实时重要且无需存储的情况下是有利的。消息始终按顺序发送。除此之外,Redis 消息系统简单易学,没有一些其他消息系统的多余内容。

实时消息传递(发布/订阅)

Redis 的发布订阅模型

以下是 Redis 中可用于创建消息框架的命令:

  • PUBLISH:这将向给定的频道或模式发布消息。

此命令的时间复杂度由O(N+M)给出,其中N是订阅此频道的客户端数,M是客户端订阅的模式数。

  • SUBSCRIBE:这将订阅客户端以接收频道的消息。例如,如果客户端订阅了频道news.headlines,那么它将收到为news.headlines频道发布的任何消息。

此命令的时间复杂度由O(N)给出,其中N是客户端订阅的频道数。

  • PSUBSCRIBE:这将订阅客户端到模式名称与频道名称匹配的频道。例如,假设频道由以下名称注册:

  • news.sports.cricket

  • news.sports.tennis

然后,对于像news.sports.*这样的模式,订阅者将收到news.sports.cricketnews.sports.tennis频道的消息。

此命令的时间复杂度为O(N),其中N是客户端订阅的模式数。

  • PUBSUB:这是一个命令,结合一些子命令,可以帮助了解 Redis 中注册的模式和频道的情况。

注意

这仅适用于 Redis 2.8.0 版本。Windows 版本的 Redis 基于 2.6 分支,不支持此命令。

其他与PUBSUB相关的命令,可帮助查找有关发布者和订阅者的信息,如下所示:

  • PUBSUB CHANNELS [pattern]:这列出当前活动的频道

  • PUBSUB NUMSUB [channel]:这将列出订阅指定频道的订阅者数量。

  • 发布订阅 NUMPAT:这列出了对所有模式的订阅数量

  • PUNSUBSCRIBE:此命令取消订阅客户端的模式

  • UNSUBSCRIBE:此命令取消订阅客户端的频道

让我们使用 Jedis 编写一个简单的 Java 程序来演示一个简单的 PUB/SUB 程序。Jedis 公开了发布的接口,并且支持 Redis 的所有功能。订阅消息的接口有点棘手,因为订阅者在发布者发布消息之前应该处于就绪状态。这是因为如果订阅者不可用,Redis 无法存储消息。发布者的代码:SubscriberProcessor.java

package org.learningRedis.chapter.four.pubsub;
import Redis.clients.jedis.Jedis;
import Redis.clients.jedis.JedisPool;
import Redis.clients.jedis.JedisPoolConfig;
public class SubscriberProcessor implements Runnable{
  private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
  private Subscriber subscriber = new Subscriber();
  private Thread simpleThread;
  private Jedis jedis = getResource();
  public Jedis getResource() {
    jedis = pool.getResource();
    return jedis;
  }
  public void setResource(Jedis jedis){
    pool.returnResource(jedis);
  }
  @SuppressWarnings("static-access")
  public static void main(String[] args) {
    SubscriberProcessor test = new SubscriberProcessor();
    test.subscriberProcessor();
    try {
      Thread.currentThread().sleep(10000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    test.unsubscribe();
  }
  private void unsubscribe() {
    simpleThread.interrupt();
    if(subscriber.isSubscribed()){
      subscriber.unsubscribe();
  }
  }
  private void subscriberProcessor() {
    simpleThread = new Thread(this);
    simpleThread.start();
  }
  @Override
  public void run() {
    while (!Thread.currentThread().isInterrupted()) {
      jedis.subscribe(subscriber, "news");
      //jedis.psubscribe(subscriber, "news.*");
    }
  }
}

订阅者处理器需要订阅一个频道。为此,它需要一个始终处于监听模式的实例。在此示例中,Subscriber.java是通过扩展 Jedis PUB/SUB 来实现的类。这个抽象类提供了管理订阅者生命周期的方法。接下来是提供必要钩子来订阅频道模式并监听频道或模式消息的代码。订阅模式的代码已被注释;要看到它的实际效果,我们需要取消注释它并注释订阅频道的代码:

package org.learningRedis.chapter.four.pubsub;
import Redis.clients.jedis.JedisPubSub;
public class Subscriber extends  JedisPubSub{
  @Override
  public void onMessage(String arg0, String arg1) {
    System.out.println("on message : " + arg0 + " value = " + arg1);
  }
  @Override
  public void onPMessage(String arg0, String arg1, String arg2) {
    System.out.println("on pattern message : " + arg0 + " channel = " + arg1 + " message =" + arg2);
  }
  @Override
  public void onPSubscribe(String arg0, int arg1) {
    System.out.println("on pattern subscribe : " + arg0 + " value = " + arg1);
  }
  @Override
  public void onPUnsubscribe(String arg0, int arg1) {
    System.out.println("on pattern unsubscribe : " + arg0 + " value = " + arg1);
  }
  @Override
  public void onSubscribe(String arg0, int arg1) {
    System.out.println("on subscribe : " + arg0 + " value = " + arg1);
  }
  @Override
  public void onUnsubscribe(String arg0, int arg1) {
    System.out.println("on un-subscribe : " + arg0 + " value = " + arg1);
  }
}

在启动发布者发送消息到频道之前,最好先启动订阅者处理器,该处理器将监听发布到其订阅频道或模式的任何消息。在这种情况下,订阅者处理器将监听新闻频道或将订阅模式[news.*]

在这些示例中使用的一个常见类是连接管理器,其代码如下所示:

package org.learningredis.chapter.four.pipelineandtx;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class ConnectionManager {
  private static JedisPool jedisPool = new JedisPool("localhost");
  public static Jedis get(){
    return jedisPool.getResource();
  }
  public static void set(Jedis jedis){
    jedisPool.returnResource(jedis);
  }
  public static void close(){
    jedisPool.destroy();
  }
}

要触发发布者,请使用以下发布者代码。发布者的代码Publisher.java如下:

package org.learningRedis.chapter.four.pubsub;
import Redis.clients.jedis.Jedis;
import Redis.clients.jedis.JedisPool;
import Redis.clients.jedis.JedisPoolConfig;
public class Publisher {
  private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
  Jedis jedis = null;
  public Jedis getResource() {
    jedis = pool.getResource();
    return jedis;
  }
  public void setResource(Jedis jedis){
    pool.returnResource(jedis);
  }
  private void publisher() {
    Jedis jedis = this.getResource();
    jedis.publish("news", "Houstan calling texas... message published !!");
  }
  public static void main(String[] args) {
    Publisher test = new Publisher();
    test.publisher();
  }
}

在此示例中,该代码将向名为news的频道发布消息,要查看其工作情况,请确保订阅者已准备就绪,并且要发布消息到模式,请注释发布到频道的代码,并取消注释发布消息到模式的代码。

Redis 中的管道

Redis 提供了一种更快执行的机制,称为管道。它将所有命令组合成一个命令块,并将其发送到服务器进行执行。所有命令的结果都排队在一个响应块中并发送回来。

将管道工作方式与通过连接发送多个单独命令的方式进行比较,可以让我们了解管道更有效的地方以及需要使用管道的地方。假设我们必须向 Redis 发送三个命令的情况。发送任何命令到 Redis 的时间为X秒,因此发送响应需要相同的时间。去程和回程所花费的总时间为2X秒。还假设执行所需的时间为另外X秒。现在在管道命令中,由于我们将三个命令作为一个块发送,因此去 Redis 所需的时间约为X秒,处理所有三个命令所需的时间为3X秒,回程所需的时间也为X秒。管道命令所需的总时间为5X秒。将其与必须发送单独命令的情况进行比较。发送单个命令及其回程所需的时间等于2X,包括执行所需的时间为3X。由于我们谈论的是三个命令,因此总时间等于9X。与5X秒相比,9X秒的时间证明了它的效率。

我们必须记住的一件事是,管道确保原子性,但只执行多个命令并在一个响应块中返回响应。以下是管道中调用的命令的简单表示:

Redis 中的管道

Redis 中的管道

接下来是跨多个连接发送的多个命令的表示。正如我们所看到的,通过使用管道命令,可以节省发送响应的时间:

Redis 中的管道

在 Redis 中使用单独连接的多个命令

这种批量发送命令的方式在 RDBMS 中也可以看到,我们可以将批量 JDBC 作为批处理发送。为了验证这一点,让我们编写一个程序,并检查在使用管道和不使用管道运行程序之间的时间差异:

package org.learningRedis.chapter.four.simplepipeline;
import java.util.List;
import Redis.clients.jedis.Jedis;
import Redis.clients.jedis.Pipeline;
public class PipelineCommandTest {
  Jedis jedis = ConnectionManager.get();
  long starttime_withoutpipeline = 0;
  long starttime_withpipeline = 0;
  long endtime_withoutpipeline = 0;
  long endtime_withpipeline = 0;
  public static void main(String[] args) throws InterruptedException {
    PipelineCommandTest test = new PipelineCommandTest();
    test.checkWithoutPipeline();
    Thread.currentThread().sleep(1000);
    test.checkWithPipeline();
    Thread.currentThread().sleep(1000);
    test.getStats();
  }
  private void getStats() {
    System.out.println(" time taken for test without pipeline "+ (endtime_withoutpipeline - starttime_withoutpipeline ));
    System.out.println(" time taken for test with    pipeline "+ (endtime_withpipeline - starttime_withpipeline ));
  }
  private void checkWithoutPipeline() {
    starttime_withoutpipeline = System.currentTimeMillis();
    for(int keys=0;keys<10;keys++){
      for(int nv=0;nv<100;nv++){
        jedis.hset("keys-"+keys, "name"+nv, "value"+nv);
      }
      for(int nv=0;nv<100;nv++){
        jedis.hget("keys-"+keys, "name"+nv);
      }
    }
    endtime_withoutpipeline = System.currentTimeMillis();
    // this will delete all the data.
    jedis.flushDB();
  }
  private void checkWithPipeline() {
    starttime_withpipeline = System.currentTimeMillis();
    for(int keys=0;keys<10;keys++){
      Pipeline commandpipe = jedis.pipelined();
      for(int nv=0;nv<100;nv++){
        commandpipe.hset("keys-"+keys, "name"+nv, "value"+nv);
      }
      List<Object> results = commandpipe.syncAndReturnAll();
      for(int nv=0;nv<results.size();nv++){
        results.get(nv);
      }
    }
    endtime_withpipeline = System.currentTimeMillis();
    jedis.flushDB();
  }
}

在我的计算机上的结果如下,当然,这可能会根据所使用的机器配置而有所不同:

time taken for test without pipeline 4015
time taken for test with    pipeline 250

管道提供了更快执行的优势,但也带来了一些限制。这仅在目标 Redis 实例相同时有效,也就是说,在分片环境中不起作用,因为每个 Redis 实例的连接都是不同的。当命令不相互依赖或需要编写自定义逻辑以形成复合命令时,管道也存在不足。在这种情况下,Redis 还提供了一种脚本的机制,我们将在本章后面进行介绍。

Redis 中的事务

作为 NOSQL 数据存储的 Redis 提供了一种宽松的事务。与传统的 RDBMS 一样,事务以BEGIN开始,以COMMITROLLBACK结束。所有这些 RDBMS 服务器都是多线程的,因此当一个线程锁定资源时,除非释放了锁,否则另一个线程无法操作它。Redis 默认使用MULTI开始,EXEC执行命令。在事务中,第一个命令始终是MULTI,之后所有的命令都被存储,当接收到EXEC命令时,所有存储的命令都按顺序执行。因此,在内部,一旦 Redis 接收到EXEC命令,所有命令都将作为单个隔离的操作执行。以下是 Redis 中可用于事务的命令:

  • MULTI:这标志着事务块的开始

  • EXEC:这在MULTI之后执行管道中的所有命令

  • WATCH:这会监视键以条件执行事务

  • UNWATCH:这会移除事务的WATCH

  • DISCARD:这会刷新管道中之前排队的所有命令

以下图表示了 Redis 中事务的工作原理:

Redis 中的事务

Redis 中的事务

管道与事务

正如我们在管道中看到的,命令被分组并执行,并且响应被排队并发送。但是在事务中,直到接收到EXEC命令,MULTI之后接收到的所有命令都会被排队,然后执行。为了理解这一点,重要的是要考虑一个多线程环境,并观察结果。

在第一种情况下,我们使用两个线程向 Redis 发送管道命令。在这个示例中,第一个线程发送了一个管道命令,它将多次更改一个键的值,第二个线程将尝试读取该键的值。以下是将在 Redis 中启动两个线程的类:MultiThreadedPipelineCommandTest.java

package org.learningRedis.chapter.four.pipelineandtx;
public class MultiThreadedPipelineCommandTest {
  public static void main(String[] args) throws InterruptedException {
    Thread pipelineClient = new Thread(new PipelineCommand());
    Thread singleCommandClient = new Thread(new SingleCommand());
    pipelineClient.start();
    Thread.currentThread().sleep(50);
    singleCommandClient.start();
  }
}
The code for the client which is going to fire the pipeline commands is as follows:
package org.learningRedis.chapter.four.pipelineandtx;
import java.util.Set;
import Redis.clients.jedis.Jedis;
import Redis.clients.jedis.Pipeline;
public class PipelineCommand implements Runnable{
  Jedis jedis = ConnectionManager.get();
  @Override
  public void run() {
      long start = System.currentTimeMillis();
      Pipeline commandpipe = jedis.pipelined();
      for(int nv=0;nv<300000;nv++){
        commandpipe.sadd("keys-1", "name"+nv);
      }
      commandpipe.sync();
      Set<String> data= jedis.smembers("keys-1");
      System.out.println("The return value of nv1 after pipeline [ " + data.size() + " ]");
    System.out.println("The time taken for executing client(Thread-1) "+ (System.currentTimeMillis()-start));
    ConnectionManager.set(jedis);
  }
}

当执行管道时,用于读取键的客户端的代码如下:

package org.learningRedis.chapter.four.pipelineandtx;
import java.util.Set;
import Redis.clients.jedis.Jedis;
public class SingleCommand implements Runnable {
  Jedis jedis = ConnectionManager.get();
  @Override
  public void run() {
    Set<String> data= jedis.smembers("keys-1");
    System.out.println("The return value of nv1 is [ " + data.size() + " ]");
    ConnectionManager.set(jedis);
  }
}

结果将根据机器配置而异,但通过更改线程休眠时间并多次运行程序,结果将与下面显示的结果类似:

The return value of nv1 is [ 3508 ]
The return value of nv1 after pipeline [ 300000 ]
The time taken for executing client(Thread-1) 3718

注意

请在每次运行测试时执行FLUSHDB命令,否则您将看到上一次测试运行的值,即 300,000

现在我们将以事务模式运行示例,其中命令管道将以MULTI关键字开头,并以EXEC命令结尾。这个客户端类似于之前的示例,其中两个客户端在单独的线程中向 Redis 发送命令。

以下程序是一个测试客户端,它给两个线程,一个处于事务模式的命令,第二个线程将尝试读取和修改相同的资源:

package org.learningRedis.chapter.four.pipelineandtx;
public class MultiThreadedTransactionCommandTest {
  public static void main(String[] args) throws InterruptedException {
    Thread transactionClient = new Thread(new TransactionCommand());
    Thread singleCommandClient = new Thread(new SingleCommand());
    transactionClient.start();
    Thread.currentThread().sleep(30);
    singleCommandClient.start();
  }
}

这个程序将尝试修改资源并在事务进行时读取资源:

package org.learningRedis.chapter.four.pipelineandtx;
import java.util.Set;
import Redis.clients.jedis.Jedis;
public class SingleCommand implements Runnable {
  Jedis jedis = ConnectionManager.get();
  @Override
  public void run() {
    Set<String> data= jedis.smembers("keys-1");
    System.out.println("The return value of nv1 is [ " + data.size() + " ]");
    ConnectionManager.set(jedis);
  }
}

这个程序将以MULTI命令开始,尝试修改资源,以EXEC命令结束,并稍后读取资源的值:

package org.learningRedis.chapter.four.pipelineandtx;
import java.util.Set;
import Redis.clients.jedis.Jedis;
import Redis.clients.jedis.Transaction;
import chapter.four.pubsub.ConnectionManager;
public class TransactionCommand implements Runnable {
  Jedis jedis = ConnectionManager.get();
  @Override
  public void run() {
      long start = System.currentTimeMillis();
      Transaction transactionableCommands = jedis.multi();
      for(int nv=0;nv<300000;nv++){
        transactionableCommands.sadd("keys-1", "name"+nv);
      }
      transactionableCommands.exec();
      Set<String> data= jedis.smembers("keys-1");
      System.out.println("The return value nv1 after tx [ " + data.size() + " ]");
    System.out.println("The time taken for executing client(Thread-1) "+ (System.currentTimeMillis()-start));
    ConnectionManager.set(jedis);
  }
}

上述程序的结果将根据机器配置而有所不同,但通过更改线程休眠时间并运行程序几次,结果将与下面显示的结果类似:

The return code is [ 1 ]
The return value of nv1 is [ null ]
The return value nv1 after tx [ 300000 ]
The time taken for executing client(Thread-1) 7078

注意

每次运行测试时都要执行FLUSHDB命令。这个想法是程序不应该获取由于上一次运行程序而获得的值。单个命令程序能够写入键的证据是如果我们看到以下行:返回代码是[1]

让我们分析一下结果。在管道的情况下,一个单独的命令读取该键的值,而管道命令则为该键设置一个新值,如下结果所示:

The return value of nv1 is [ 3508 ]

现在将这与在事务的情况下发生的情况进行比较,当一个单独的命令尝试读取值但因事务而被阻塞时。因此该值将是NULL或 300,000。

  The return value of nv1 after tx [0] or
  The return value of nv1 after tx [300000] 

因此,输出结果的差异可以归因于在事务中,如果我们已经开始了MULTI命令,并且仍在排队命令的过程中(也就是说,我们还没有给服务器EXEC请求),那么任何其他客户端仍然可以进来并发出请求,并且响应将发送给其他客户端。一旦客户端发出EXEC命令,那么所有其他客户端在所有排队的事务命令执行时都会被阻止。

管道和事务

为了更好地理解,让我们分析一下在管道的情况下发生了什么。当两个不同的连接向 Redis 请求相同的资源时,我们看到了一个结果,即客户端-2 在客户端-1 仍在执行时获取了该值:

管道和事务

Redis 中的管道在多连接环境中

它告诉我们的是,来自第一个连接的请求(即管道命令)被堆叠为一个命令在其执行堆栈中,而来自另一个连接的命令则保留在其自己的与该连接相关的堆栈中。Redis 执行线程在这两个执行堆栈之间进行时间切片,这就是为什么当客户端-1 仍在执行时,客户端-2 能够打印一个值的原因。

让我们分析一下在事务的情况下发生了什么。同样,两个命令(事务命令和GET命令)被保留在它们自己的执行堆栈中,但当 Redis 执行线程给予GET命令时间并去读取值时,看到锁定,它被禁止读取值并被阻塞。Redis 执行线程再次回到执行事务命令,然后再次回到GET命令,它再次被阻塞。这个过程一直持续,直到事务命令释放了对资源的锁定,然后GET命令才能获取值。如果GET命令碰巧在事务锁定之前能够到达资源,它会得到一个空值。

请记住,Redis 在排队事务命令时不会阻止其他客户端的执行,但在执行它们时会阻止。

管道和事务

Redis 多连接环境中的事务

这个练习让我们了解了在管道和事务的情况下会发生什么。

Redis 中的脚本编写

Lua是一种性能优越的脚本语言,解释器是用 C 编写的。Redis 提供了一种机制,通过在服务器端提供对 Lua 的支持来扩展 Redis 的功能。由于 Redis 是用 C 实现的,因此 Lua 作为服务器附加组件与 Redis 一起提供是很自然的。随 Redis 一起提供的 Lua 解释器具有有限的功能,并且随其一起提供以下库:

  • base

  • table

  • string

  • math

  • debug

  • cjson

  • cmsgpack

注意

不能执行文件 I/O 和网络操作的库未包含在内,因此无法从 REDIS 中的 LUA 脚本向另一个外部系统发送消息。

在开始有趣的事情之前,最好先了解一下这种语言。LUA 有自己专门的网站,并且有大量资源可供 LUA 使用,但下一节专注于了解 Redis 的 LUA 的足够内容。

Lua 简介

好了,到目前为止,我们都知道 LUA 是一种解释性语言,并且它在 Redis 中得到了支持。为了利用 Redis 的功能,让我们学习一些关于 LUA 的东西。LUA 中支持的类型和值如下:

  • Nil:Nil 是具有单个值nil的类型。将其与 Java 进行比较,可以将其视为null

  • 布尔值:这些将具有 true 或 false 作为值。

  • 数字:这些表示双精度浮点数。因此,我们可以将我们的数字写为 1、1.1、2e+10 等。

  • 字符串:这些表示字符序列,与大多数脚本和编程语言中的常见情况相同。在 LUA 中,字符串是不可变的;例如,"Learning Redis"'Learning Redis'。LUA 提供了字符串库中的方法来查找子字符串,替换字符等。

  • :这些类似于可以使用数字和字符串索引的数组,除了nil

LUA 中的控制语句和循环如下:

  • if then else语句:与 Java 中的if/else类似,LUA 支持类似if/then/else的形式。以下是其代码示例:
local  myvariable = 4
local  myothervariable = 5
if myvariable >  myothervariable then
  print("4 is greater than 5".."Please add 2 dots to concatenate strings")
else
  print("4 is not greater than 5".."Please add 2 dots to concatenate strings")
end
  • while循环:这类似于 Java 中的循环,其语法类似:
local index=1
while index <= 5 do
  print("Looping done interation "..index)
  index=index+1
end
  • repeat语句:这类似于 Java 中的do/while。这将保证至少进行一次迭代:
local index=1
repeat
  print("Looping done interation "..index)
  index=index+1
until index==5 
  • for循环:这类似于 Java 中的for循环:
for i=1,3 do
  print("Looping in for loop ")
end

在执行控制语句时,LUA 中经常使用的两个关键字是returnbreak。以下是一个简单的示例,演示了在函数中使用 return 关键字:

function greaterThanFunction( i , j )
  if i >  j then
    print(i.." is greater than"..j)
    return true
  else
    print(i.." is lesser than"..j)
    return false
  end
end
print(greaterThanFunction(4,5))

接下来是一个简单的示例,演示了在函数中使用 break 关键字:

local mylist={"start","pause","stop","resume"}
function parseList ( k )
  for i=1,#mylist do
    if mylist[i] == "stop" then break end
    print(mylist[i])
  end
end
print(parseList(mylist))

有了对 LUA 工作原理的最基本理解,让我们在 Redis 中运行一个示例,然后继续深入了解。但在此之前,让我们了解一下 LUA 在 Redis 中的工作原理。

以下图描述了 LUA 如何与 Redis 一起工作。要了解内部发生的事情,重要的是要记住 Redis 以单线程模型工作,所有 Redis 命令和 LUA 逻辑都将按顺序执行:

Lua 简介

Redis 中的 LUA 脚本

当客户端将脚本发送到 Redis 服务器时,脚本会被验证其语法,并存储在 Redis 内部映射中与 SHA-1 摘要对应。SHA-1 摘要会返回给客户端。

让我们尝试在 LUA 中编写一个简单的程序,基本上是读取一个键的值,并检查该值是否等于传递的参数。如果是,则将其设置为传递的第二个参数,否则将其设置为传递给脚本的第三个参数。好的,让我们准备测试环境。打开 Redis 命令行客户端,并将msg键的值设置为"Learning Redis"

Lua 简介

准备测试执行 LUA 脚本

现在msg的值已设置好,让我们执行以下列出的 Java 程序:

package org.learningRedis.chapter.four.luascripting;
import java.util.Arrays;
import Redis.clients.jedis.Jedis;
import Redis.clients.jedis.JedisPool;
import Redis.clients.jedis.JedisPoolConfig;
public class TestLuaScript {
  public String luaScript = Reader.read("D:\\path\\of\\file\\location\\LuaScript.txt");
  private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
  Jedis jedis = null;
  public Jedis getResource() {
    jedis = pool.getResource();
    return jedis;
  }
  public void setResource(Jedis jedis){
    pool.returnResource(jedis);
  }
  public static void main(String[] args) {
    TestLuaScript test = new TestLuaScript();
    test.luaScript();
  }
  private void luaScript() {
    Jedis jedis = this.getResource();
    String result = (String) jedis.eval(luaScript,Arrays.asList("msg"),
        Arrays.asList("Learning Redis",
            "Now I am learning Lua for Redis",
            "prepare for the test again"));
    System.out.println(result);
    this.setResource(jedis);
  }
}

Reader的代码是一个简单的 Java 程序,它从文件位置读取程序:

package org.learningRedis.chapter.four.luascripting;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Reader {
  public static String read(String filepath) {
    StringBuffer string = new StringBuffer();
    try (BufferedReader br = new BufferedReader(new FileReader(filepath)))
    {
      String currentline;
      while ((currentline = br.readLine()) != null) {
        string.append(currentline);
      }
    } catch (IOException e) {
      e.printStackTrace();
    } 
    return string.toString();
  }
}

现在让我们看一下写在文件LuaScript.txt中的 LUA 脚本,我们将把它传递给 Java 程序:

local data= Redis.call('GET',KEYS[1])
if data==ARGV[1] then 
  Redis.call('SET',KEYS[1],ARGV[2])
  return "The value that got sent is = "..ARGV[2]
else
  Redis.call('SET',KEYS[1],ARGV[3])
  return "The value that got sent is = "..ARGV[3]
end

程序的第一次运行应该给您以下结果:

The value that got sent is = Now I am learning Lua for Redis

程序的第二次运行应该给您以下结果:

The value that got sent is = prepare for the test again

因此,如果您看到如前面的代码中所示的消息打印,那么您实际上已成功在 Redis 中执行了您的第一个 LUA 程序。以下是我们从这个示例中学到的内容:

  • Redis 将 LUA 脚本视为一个函数。

  • LUA 脚本使用Redis.call()方法来触发 Redis 命令。

  • 返回值的 Redis 命令可以赋值给本地变量。在这里,我们将值赋给一个名为data的变量。

  • LUA 中的数组从1开始索引,而不是0。因此,您永远不会有数组索引,比如ARGV[0]KEYS[0]

Redis 对 Lua 脚本引擎施加了一些进一步的限制,如下所示:

  • 在 Redis 中,LUA 脚本不能有全局变量。

  • 在 Redis 中,LUA 脚本不能调用诸如MULTIEXEC之类的事务命令。

  • 在 Redis 中,LUA 脚本不能使用 LUA 中的 I/O 库访问外部系统。它与外部系统通信的唯一方式是通过诸如PUBLISH之类的 Redis 命令。

  • 不支持通过 LUA 访问系统时间的脚本。而是使用TIME命令,即Redis.call('TIME')

  • 诸如Redis.call('TIME')之类的函数是非确定性的,因此在WRITE命令之前不允许使用。

  • 不允许嵌套条件,因为嵌套条件将以END关键字结束,这会影响外部条件,外部条件也必须以END结束。

以下命令支持在 Redis 中管理 LUA 脚本。让我们来看看它,并了解它们如何使用:

  • EVAL:此命令将处理 Redis 脚本,并响应将是执行脚本的结果。

  • EVALSHA:此命令将根据脚本的 SHA-1 摘要处理缓存的脚本,并响应将是执行脚本的结果。

  • SCRIPT EXISTS:此命令将检查脚本在脚本缓存中的存在。通过传递脚本的 SHA-1 摘要来进行此检查。

  • SCRIPT FLUSH:这将从脚本缓存中清除 LUA 脚本。

  • SCRIPT KILL:此命令将终止执行时间较长的脚本。

  • SCRIPT LOAD:此命令将加载脚本到缓存中,并返回脚本的 SHA-1 摘要。

用例 - 可靠的消息传递

通过使用 Redis 的 PUB/SUB 功能,我们可以创建一个实时消息传递框架,但问题是,如果预期的订阅者不可用,那么消息就会丢失。为了解决这个问题,我们可以借助 LUA 脚本来存储消息,如果订阅者不可用。

这个实现将根据解决方案的框架设计而有所不同,但在我们的情况下,我们将采取一种简单的方法,即每个订阅者和发布者都将就一个频道达成一致。当订阅者下线时,发布者将把消息存储在一个唯一的消息框中,以便订阅者再次上线时,它将开始消费丢失的消息,以及来自发布者的实时消息。以下图表示了我们将要遵循的步骤以实现可靠的消息传递:

用例 - 可靠的消息传递

简单可靠的消息传递

首先,发布者将向频道 client-1 发送消息,不知道订阅者是否处于接收模式。假设订阅者正在运行,则发布者的消息将实时消耗。然后,如果我们将订阅者关闭一段时间并发布更多消息,在我们的情况下,发布者将足够智能,以知道订阅者是否正在运行,并且感知到订阅者已关闭,它将在MSGBOX中存储消息。

与此同时,订阅者运行起来后,它将首先从MSGBOX中获取错过的消息,并将其发布给自己。发布者的代码如下:

package org.learningRedis.chapter.four.pubsub.reliable;
import java.util.Arrays;
import Redis.clients.jedis.Jedis;
import Redis.clients.jedis.JedisPool;
import Redis.clients.jedis.JedisPoolConfig;
import org.learningRedis.chapter.four.luascripting.Reader;
public class Publisher {
  public String luaScript = Reader.read("D:\\pathtoscript \\RELIABLE-MSGING.txt");
  private JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
  Jedis jedis = null;
  public Jedis getResource() {
    jedis = pool.getResource();
    return jedis;
  }
  public void setResource(Jedis jedis){
    pool.returnResource(jedis);
  }
  public static void main(String[] args) {
    Publisher test = new Publisher();
    test.sendingAreliableMessages();
  }
  private void sendingAreliableMessages() {
    Jedis jedis = this.getResource();
    String result = (String) jedis.eval(luaScript,Arrays.asList(""),
        Arrays.asList("{type='channel',publishto='client1',msg='"+System.currentTimeMillis()+"'}"));
    System.out.println(result);
    this.setResource(jedis);
  }
}

LUA 脚本的代码如下:

local payload = loadstring("return"..ARGV[1])()
local result = Redis.call("PUBLISH",payload.publishto,payload.msg)
if result==0 then
  Redis.call('SADD','MSGBOX',payload.msg)
  return 'stored messages:  '..ARGV[1]
else
  return 'consumed messages:  '..ARGV[1]
end

以下是 LUA 中编写的步骤的简要解释:

  1. 在第一行中,我们获取消息并将其转换为表对象。在 LUA 中,数组索引从1开始。

  2. 我们在第二行中发布消息并获取结果。结果告诉我们有多少订阅者消费了消息。

  3. 如果结果等于0,则所有侦听器都已关闭,我们需要将其持久化。此处使用的数据类型是Set,随后将消息返回给服务器(此返回是可选的)。

  4. 如果消息被订阅者消费,则执行Else中的语句。

  5. 最后,我们end函数。(确保脚本中只有一个end。如果有多个end,Redis 中的 LUA 将无法编译。)

Redis 将在 LUA 中将代码包装为一个函数。Subscriber的代码如下:

package org.learningRedis.chapter.four.pubsub.reliable;
import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import org.learningRedis.chapter.four.luascripting.Reader;
import Redis.clients.jedis.Jedis;
import Redis.clients.jedis.JedisPubSub;
import chapter.four.pubsub.ConnectionManager;
public class SimpleMsgSubscriber {
  static Thread lostMsgWorker;
  static Thread msgWorker;
  public static void main(String[] args) {
    SimpleMsgSubscriber source = new SimpleMsgSubscriber();
  msgWorker = new Thread(source.new MsgProcessor());
lostMsgWorker = new Thread(source.new LostMsgProcessor());
  msgWorker.start();
lostMsgWorker.start();
  }
public class MsgProcessor extends JedisPubSub implements Runnable {
Jedis jedis = ConnectionManager.get();
@Override
public void run() {
  jedis.subscribe(this, "client1");
}
@Override
public void onMessage(String arg0, String arg1) {
  System.out.println("processing the msg = " + arg1);
}
@Override
public void onPMessage(String arg0, String arg1, String arg2) {
    }
@Override
public void onPSubscribe(String arg0, int arg1) {
    }
@Override
public void onPUnsubscribe(String arg0, int arg1) {
     }
@Override
public void onSubscribe(String arg0, int arg1) {
    }
@Override
public void onUnsubscribe(String arg0, int arg1) {
    }
  }
public class LostMsgProcessor implements Runnable {
    Jedis jedis = ConnectionManager.get();
    @Override
    public void run() {
      String event;
      Jedis jedis = ConnectionManager.get();
      String msg;
      while((msg=jedis.spop("MSGBOX")) != null){
        MessageHandler.push(msg);
      }
    }
  }
  public static class MessageHandler {
    static Jedis jedis = ConnectionManager.get();
        public static void push(String msg)
        {
            String luaScript = "";
            try
            {
                luaScript = read("D:\\path\\to\\file\\RELIABLE-MSGING.txt");
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
            String result = (String) jedis.eval(luaScript, Arrays.asList(""), Arrays.asList("{type='channel',publishto='client1',msg='" + msg + "'}"));
        }
        private static String read(String luaScriptPath) throws IOException
        {
            Path file = Paths.get(luaScriptPath);
            BufferedReader reader = Files.newBufferedReader(file, Charset.defaultCharset());
            StringBuilder content = new StringBuilder();
            String line = null;
            while ((line = reader.readLine()) != null)
            {
                content.append(line).append("/n");
            }
            System.out.println("Content: " + content.toString());
            return content.toString();
        }
  }
}

该程序有以下职责,并对程序进行了简要解释:

  • 启动时应检查是否有消息在消息框中,例如MSGBOX,当它关闭时。如果有消息,则其工作是将其发布给自己。

  • 它应该做的第二件事是监听其订阅的消息。

  • 为了获得更好的性能,运行SCRIPT LOAD命令,该命令将加载脚本并返回 SHA-1 摘要,而不是使用EVAL,使用EVALSHA命令,其中传递相同的 SHA-1 摘要。这将防止脚本被检查语法正确性,并将直接执行。

连接管理

在本节中,我们将重点关注如何管理与 Redis 的连接。Redis 中提供的连接管理功能帮助我们执行以下操作:

  • AUTH:此命令允许请求在密码匹配配置的密码时被处理。Redis 服务器可以在config文件中配置requirepass以及密码。

  • ECHO:此命令将回显发送到 Redis 实例的文本。

  • PING:当发送到 Redis 实例时,此命令将回复PONG

  • QUIT:此命令将终止 Redis 实例为客户端持有的连接。

  • SELECT:此命令有助于在 Redis 中选择要执行命令的数据库。Redis 中的数据可以有关注点的分离,通过创建一个筒仓并将数据存储在其中来实现。每个筒仓中的数据不会相互干扰,而是被隔离的。

Redis 身份验证

通过 Redis 客户端向 Redis 服务器添加简单密码,并通过 Java 客户端进行测试,具体步骤如下所述:

  1. 打开 Redis 客户端并键入CONFIG SET requirepass "Learning Redis"。您已将 Redis 服务器的密码设置为"Learning Redis"

  2. 使用 Jedis 在 Java 中编写以下程序,该程序将在不对 Redis 服务器进行身份验证的情况下执行一些简单的 getter 和 setter:

package org.learningRedis.chapter.four.auth;
import Redis.clients.jedis.Jedis;
public class TestingPassword {
  public static void main(String[] args) {
    TestingPassword test = new TestingPassword();
    test.authentication();
  }
  private void authentication() {
    Jedis jedis = new Jedis("localhost");
    jedis.set("foo", "bar");
    System.out.println(jedis.get("foo"));
  }
}
  1. 控制台中的结果将是ERR operation not permitted,或者根据版本,您可能会得到NOAUTH Authentication required,这表明由于未在请求中传递密码,无法允许操作。为使程序工作,客户端需要传递密码进行身份验证:
package org.learningRedis.chapter.four.auth;
import Redis.clients.jedis.Jedis;
public class TestingPassword {
  public static void main(String[] args) {
    TestingPassword test = new TestingPassword();
    test.authentication();
  }
  private void authentication() {
    Jedis jedis = new Jedis("localhost");
    jedis.auth("Learning Redis");
    jedis.set("foo", "bar");
    System.out.println(jedis.get("foo"));
  }
}

控制台中程序的结果将是bar

Redis SELECT

Redis 提供了一种将 Redis 服务器分隔成数据库的机制。在一些数据库中,Redis 没有复杂的命名机制,而是有一个简单的过程将数据库分成单独的键空间,每个键空间由一个整数表示。

Redis SELECT

Redis 中的多个数据库

该程序试图将一些数据存储在数据库中,并尝试成功地从中检索数据。然后更改数据库并尝试检索相同的数据,这当然会以失败告终。请记住,为了使此代码运行,请删除之前程序中设置的任何身份验证,或者只需重新启动 Redis 服务器。

package org.learningRedis.chapter.four.selectdb;
import Redis.clients.jedis.Jedis;
public class TestSelectingDB {
  public static void main(String[] args) {
    TestSelectingDB test = new TestSelectingDB();
    test.commandSelect();
  }
  private void commandSelect() {
    Jedis jedis = new Jedis("localhost");
    jedis.select(1);
    jedis.set("msg", "Hello world");
    System.out.println(jedis.get("msg"));
    jedis.select(2);
    System.out.println(jedis.get("msg"));
  }
}

该程序的结果应该如下所示:

Hello world
null

Redis ECHO 和 PING

Redis 提供了一些实用功能,比如ECHOPING,可以用来检查服务器是否响应,以及响应请求所花费的时间。这可以让我们了解网络和 I/O 级别的延迟。

以下程序将演示一个示例用法,当服务器没有其他连接时,将触发ECHOPING命令,然后当 Redis 服务器承受 100 个连接的负载时,再次触发这些命令(ECHOPING)。没有其他连接时的结果如下:

PONG in 47 milliseconds
hi Redis  in 0 milliseconds
PONG in 0 milliseconds
hi Redis  in 0 milliseconds
PONG in 0 milliseconds
hi Redis  in 0 milliseconds
PONG in 0 milliseconds
hi Redis  in 0 milliseconds

当服务器上有 100 个其他连接在进行活动时,结果如下:

PONG in 16 milliseconds
hi Redis  in 16 milliseconds
PONG in 0 milliseconds
hi Redis  in 15 milliseconds
PONG in 16 milliseconds
hi Redis  in 0 milliseconds
PONG in 15 milliseconds

当服务器上有 50 个其他连接在进行活动时,结果如下:

PONG in 15 milliseconds
hi Redis  in 0 milliseconds
PONG in 0 milliseconds
hi Redis  in 16 milliseconds
PONG in 0 milliseconds
hi Redis  in 0 milliseconds
PONG in 16 milliseconds
hi Redis  in 0 milliseconds
PONG in 0 milliseconds
hi Redis  in 15 milliseconds

这证明了 Redis 服务器的活动量并不重要,而取决于 I/O 和网络资源的可用性。以下程序仅供参考:

package org.learningRedis.chapter.four.echoandping;
import Redis.clients.jedis.Jedis;
public class TestEchoAndPing {
  public static void main(String[] args) throws InterruptedException {
    TestEchoAndPing echoAndPing = new TestEchoAndPing();
    Thread thread = new Thread(new LoadGenerator());
    thread.start();
    while(true){
      Thread.currentThread().sleep(1000);
      echoAndPing.testPing();
      echoAndPing.testEcho();
    }
  }
  private void testPing() {
    long start = System.currentTimeMillis();
    Jedis jedis = new Jedis("localhost");
    System.out.println(jedis.ping() + " in " + (System.currentTimeMillis()-start) + " milliseconds");
  }
  private void testEcho() {
    long start = System.currentTimeMillis();
    Jedis jedis = new Jedis("localhost");
    System.out.println(jedis.echo("hi Redis ") + " in " + (System.currentTimeMillis()-start) + " milliseconds");
  }
}

LoadGenerator的代码如下所示,仅供参考:

package org.learningRedis.chapter.four.echoandping;
import java.util.ArrayList;
import java.util.List;
import Redis.clients.jedis.Jedis;
public class LoadGenerator implements Runnable{
  List<Thread> clients = new ArrayList<Thread>();
  public LoadGenerator() {
    for(int i=0;i<50;i++){
      clients.add(new Thread(new Sample()));
    }
  }
  @Override
  public void run() {
    for(int i=0;i<50;i++){
      clients.get(i).start();
    }
  }
  public class Sample implements Runnable{
    Jedis jedis = new Jedis("localhost");
    @Override
    public void run() {
      int x=0;
      while(!Thread.currentThread().isInterrupted()){
        jedis.sadd(Thread.currentThread().getName(), "Some text"+new Integer(x).toString());
        x++;
      }
    }
  }
}

我们可以通过更改线程数量并在TestEchoAndPing中注释线程启动代码来玩弄这个程序,并自己看到结果。结果将显示与前面代码中显示的一致性。

总结

在本章中,我们看到了如何使用 Redis,不仅仅作为数据存储,还可以作为管道来处理命令,这更像是批量处理。除此之外,我们还涵盖了事务、消息传递和脚本等领域。我们还看到了如何结合消息传递和脚本,并在 Redis 中创建可靠的消息传递。这使得 Redis 的能力与其他一些数据存储解决方案不同。在下一章中,我们将专注于 Redis 的数据处理能力。

第五章:在 Redis 中处理数据

业务中的数据定义了业务。这意味着我们定义、存储、解释和使用数据的方式构成了我们业务的数据平台。很少有单独的数据具有意义;只有当与其他数据结合时,它才构成业务功能。因此,重要的是将数据连接、分组和过滤,以便同一数据集可以用于业务的各个方面。

为了拥有一个能够满足未来需求的平台,我们有必要以一种方式定义和分类数据,这种方式能够给我们指示我们对数据的期望。数据有许多方面,重要的是要了解这些方面,以从中提取出完整的商业价值。例如,公司的股票价格对于实时系统来说很重要,以决定是买入还是卖出,在几秒或几毫秒后就失去了重要性。然而,对于分析系统来说,预测其趋势变得重要。因此,在不同的时间点上,相同的数据具有不同的用途。因此,在制定数据架构时,考虑数据的各种期望是一个良好的做法。

分类数据

人们普遍倾向于只考虑适合关系模型的数据模型。这可能是某些类别数据的良好模型,但对于另一类数据可能会证明是无效的。由于本书是关于 Redis 的,我们将尝试根据某些行为对数据进行分类,并尝试看看 Redis 适用于哪些情况:

  • 消息和事件数据:在业务中分类为消息数据的数据具有以下特性:

  • 数据复杂性:消息数据具有低数据复杂性,因为它们通常是扁平结构的

  • 数据数量:消息数据通常具有大量数据

  • 持久性:消息数据可以存储在磁盘和内存中

  • CAP 属性:消息数据至少需要可用和分区容错

  • 可用性:消息数据可以在实时、软实时和离线中使用,并显示出重写入和低读取的特性

如果消息数据的需求是实时和软实时活动,并且数据量不是很大,那么可以使用 Redis 及其消息传递能力。

  • 缓存数据:在业务中分类为缓存数据的数据具有以下特性:

  • 数据复杂性:缓存数据具有低数据复杂性,大多以名称值对的形式存储

  • 数据数量:缓存数据通常具有较少到中等的数据量

  • 持久性:数据可以存储在缓存内存中

  • CAP 属性:缓存数据至少需要可用和一致

  • 可用性:缓存数据可以在实时中使用,并显示低写入和高读取

Redis 是缓存数据的完美选择,因为它提供了可以直接被程序用于存储数据的数据结构。此外,Redis 中的键具有生存时间选项,可以用于定期清理 Redis 中的数据。

  • 元数据:在业务中分类为元数据的数据具有以下特性:

  • 数据复杂性:元数据具有低数据复杂性,大多以名称值对的形式存储

  • 数据数量:元数据通常具有较少的数据量

  • 持久性:元数据可以存储在内存中

  • CAP 属性:元数据至少需要可用和一致

  • 可用性:元数据可以在实时中使用,并且通常显示出低写入和低到高读取的特性

Redis 是元数据的完美选择,因为它提供了可以直接被程序用于存储数据的数据结构。由于 Redis 速度快且具有消息传递能力,因此可以用于运行时操作元数据,并且还可以作为中央元数据存储库。以下图表示了 Redis 如何作为元数据存储使用:

分类数据

Redis 作为元数据存储

  • 事务数据:在业务中分类为事务数据的数据显示以下属性:

  • 数据复杂性:事务数据具有中等到高的数据复杂性,大多是关系型的

  • 数据量:事务数据通常具有中等到高的数据量

  • 持久性:事务数据可以存储在内存和磁盘中

  • CAP 属性:事务数据至少需要是一致的和分区容错的

  • 可用性:事务数据需要显示CRUD行为,而 Redis 没有这些功能

Redis 不是这种类型数据的正确数据存储。我们还可以看到,无论何时需要 CAP 特性的分区容错,都不应该使用 Redis。

  • 分析数据:在业务中分类为分析数据的数据显示以下属性:

  • 数据复杂性:数据复杂性可以根据在线分析和离线分析进一步分离。在线分析数据的数据复杂性低至中等,因为它们可能包含类似图形的关系。离线分析具有非常高的数据复杂性。

  • 数据量:这里的数据通常具有低到高的数据量,取决于我们想要的分析类型。与离线分析相比,在线分析的数据量可能较低。

  • 持久性:数据可以存储在磁盘和内存中。如果需要在线分析,则数据存储在内存中,但如果分析是离线的,则数据需要持久存储在磁盘中。

  • CAP 属性:在离线分析的情况下,数据至少需要是可用的和分区容错的,在在线分析的情况下,数据需要是可用的和一致的。

  • 可用性:消息数据可以在实时、软实时和离线中使用。

如果要进行在线分析,可以使用 Redis,前提是数据的复杂性较低。

在前面对数据的分类中,我们看到了 Redis 适合的一些领域以及应该避免使用 Redis 的领域。但是,要使 Redis 在业务解决方案环境中受到认真对待,它必须具备容错和故障管理、复制等能力。在接下来的部分中,我们将深入研究如何处理冗余和故障管理。

主从数据复制

在任何业务应用程序中,数据以复制的方式保存是至关重要的,因为硬件随时可能损坏而不会发出任何警告。为了保持业务的连续性,当主数据库崩溃时,可以使用复制的数据库,这在某种程度上保证了服务的质量。拥有复制数据的另一个优势是当一个数据库的流量增加并且对解决方案的性能产生负面影响时。为了提供性能,重要的是要平衡流量并减少每个节点的负载。

诸如 Cassandra 之类的数据存储提供了主-主配置,其中拓扑中的所有节点都像主节点一样,并且数据的复制是基于基于密钥生成的令牌哈希进行的,为了实现这一点,拓扑中的节点根据令牌范围进行分区。

与主主数据存储系统不同,Redis 具有更简单的主从安排。这意味着主节点将写入所有数据,然后将数据复制到所有从节点。复制是异步进行的,这意味着一旦数据被写入主节点,从节点并不会同步写入,而是由一个单独的过程异步写入,因此更新并不是立即的;换句话说是最终一致性。但是这种安排在性能方面有优势。如果复制是同步的,那么当对主节点进行更新时,主节点必须更新所有从节点,然后更新才会被标记为成功。因此,如果有更多的从节点,更新就会变得更加耗时。

下图表示了 Redis 中主从复制的过程。为了更好地理解这个过程,假设在时间T0,由Msg表示的 Set 的值在主节点以及所有从节点(S1S2S3)中都是"Hello"。在时间T1进行插入命令SADD插入值("Hello again")到 Set 中,那么在时间T2,值Msg变成了Hello Hello again,但是从节点的Msg值仍然是"Hello"。新值成功插入到主节点,并且成功插入的回复代码被发送回客户端。与此同时,主节点将开始向所有从节点插入新值,这发生在时间T3。因此,在时间T3,所有节点(主节点和从节点)都更新为新值。主节点更新和从节点更新之间的时间差非常小(毫秒级)。

为了更好地理解 Redis 中主从是如何工作的,让我们回顾一下之前讨论的 Redis 中实时消息传递的章节。为了在这种情况下应用相同的功能,我们可以认为所有的从节点都已经订阅了主节点,当主节点更新时,它会将新数据发布到所有的从节点。

主从数据复制

主从数据复制

那么,当从节点宕机并且主节点发生更新时会发生什么呢?在这种情况下,特定的从节点会错过更新,仍然保留旧值。然而,当从节点再次连接到主节点时,它首先会向主节点发送一个SYNC命令。这个命令将数据发送到从节点,从而使其更新自身。

设置主节点和从节点

在 Redis 中设置主从节点非常简单。我们在本地机器上为 Redis 设置一个主节点和一个从节点。我们首先要做的是将 Redis 文件夹(在我们的例子中是redis 2.6)复制到一个合适的位置。现在我们在两个不同的位置有了 Redis 分发。

设置主节点和从节点

主节点文件夹和从节点文件夹

为了更好地理解,我们将Redis-2.6称为主节点,Redis-2.6.slave称为从节点。现在打开主节点,转到bin/release文件夹并启动 Redis-server。这将在本地主机上以端口地址 6379 启动 Redis 服务器。现在打开从节点,并在适当的文本编辑器中打开Redis.conf文件。至少需要更改两个属性才能启动从节点。需要编辑的第一个属性是port。在我们的情况下,让我们将值从 6379 更改为 6380。由于主节点将在 6379 端口监听请求,从节点必须在不同的端口监听请求(我们将从同一台机器上启动主节点和从节点)。需要进行的第二个属性更改是slaveof,其值将是127.0.0.1 6379。这基本上告诉从节点主节点在何处以及在哪个端口运行。这很有帮助,因为从节点将使用此地址向主节点发送SYNC和其他命令。进行这些最小更改后,我们就可以开始了。现在转到从节点的bin/release文件夹并启动 Redis-server。

注意

当启动 Redis-server 时,请提供从节点的Redis.conf路径,即 Redis-server F:\path\to\config-file\Redis.conf

当我们启动从节点时,我们会看到的第一件事是它会尝试连接到主节点。从其Redis.conf中,从节点将找出主节点的主机和端口。Redis 与其他数据存储相比的另一件事是,它使用一个端口来处理业务请求,同时还使用SYNC和其他端口来处理从节点的类似请求。这主要是因为 Redis 是单线程服务器,线程只监听传入套接字的消息。

以下图表示从节点启动时命令提示符的外观(请确保主节点正在运行):

设置主节点和从节点

从节点在端口 6380 启动

这里有几件事需要注意。第一件事是,从节点启动时,它会向主节点发送SYNC命令。该命令是非阻塞命令,这意味着单个线程不会阻止其他请求以满足此请求。主要是主节点将其放入该连接的请求堆栈中,并将其与其他连接的时间片进行切割,当该连接的命令活动完成时(在我们的情况下是从节点的SYNC),它将其发送到从节点。在这种情况下,它发送回的是命令和从节点需要的数据,以使其与主节点保持一致。该命令与数据一起执行,然后随后加载到从节点的数据库中。主节点发送的所有命令都是更改数据而不是获取数据的命令。主用于连接到从节点的协议是Redis 协议

让我们看一些场景,并看看 Redis 在主从模式下的行为:

  • 主节点正在运行,telnet 会话连接到主节点:
  1. 确保 Redis 主节点正在运行。

  2. 确保主 Redis 客户端正在运行。

  3. 打开命令提示符,并使用命令telnet 127.0.0.1 6379连接到主机。

  4. 在 telnet 客户端中键入SYNC命令。命令提示符中应出现以下文本:设置主节点和从节点

主节点 ping telnet 客户端

  1. 转到主客户端提示符,并键入命令SET MSG "Learning Redis master slave replication"并执行它。立即切换到 telnet 命令提示符,您将看到以下输出:设置主节点和从节点

主节点向 telnet 客户端发送数据

  1. 现在在主节点的客户端提示符中执行GET MSG命令
  • 主节点已启动,从节点首次连接:
  1. 从节点控制台与上一图类似。

  2. 从主节点的 Redis-cli 中发出命令SET MSG "学习 Redis"

  3. 从从节点的 Redis-cli 中发出命令GET MSG

  4. 确保您提供主机和端口地址;在我们的情况下,因为我们已将其配置为 localhost 并且端口配置为 6380,命令看起来像Redis-cli.exe -h localhost -p 6380

  5. 结果应该是“学习 Redis”

  • 主节点已启动,从节点再次连接:
  1. 杀死从节点和客户端。

  2. 转到主节点的客户端命令提示符并编写命令SET MSG "从节点已关闭"

  3. 现在启动从节点及其客户端(提供主机和端口信息)。

  4. 从从节点的客户端命令提示符执行命令GET MSG,结果应该是“从节点已关闭”

  • 主节点已启动并正在执行管道命令,我们正在从从节点读取值:
  1. 确保主节点和从节点正在运行。

  2. 在从节点客户端的命令提示符中写入SCARD MSG命令,但不要执行它。我们将得到集合MSG中成员的数量。

  3. 打开您的 Java 客户端并编写以下程序:

package org.learningRedis.chapter.five;
import Redis.clients.jedis.Jedis;
import Redis.clients.jedis.Pipeline;
public class PushDataMaster {
          public static void main(String[] args) {
            PushDataMaster test = new PushDataMaster();
            test.pushData();
          }
          private void pushData() {
            Jedis jedis = new Jedis("localhost",6379);
            Pipeline pipeline = jedis.pipelined();
for(int nv=0;nv<900000;nv++){
              pipeline.sadd("MSG", ",data-"+nv);
            }
            pipeline.sync();
          }
}
  1. 执行此命令,立即切换到从节点客户端命令提示符并执行您编写的命令。结果将类似于下图所示。它告诉我们的是,当在更改数据集的主节点中执行命令时,主节点开始缓冲这些命令并将它们发送到从节点。在我们的情况下,当我们对集合执行SCARD时,我们以递增的方式看到结果。设置主节点和从节点

从节点上SCARD命令的结果

  1. 主节点已启动,并正在执行事务命令,我们正在从从节点读取值。
  • 当主节点关闭并重新启动为从节点时提升从节点为主节点:
  1. 启动主节点和从节点 Redis 服务器。

  2. 从您的 IDE 执行以下 Java 程序:

package org.learningRedis.chapter.five.masterslave;
import Redis.clients.jedis.Jedis;
public class MasterSlaveTest {
  public static void main(String[] args) throws InterruptedException {
    MasterSlaveTest test = new MasterSlaveTest();
    test.masterslave();
  }
  private void masterslave() throws InterruptedException {
    Jedis master = new Jedis("localhost",6379);
    Jedis slave = new Jedis("localhost",6380);
    master.append("msg", "Learning Redis");
    System.out.println("Getting message from master: " + master.get("msg"));
    System.out.println("Getting message from slave : " + slave.get("msg"));
    master.shutdown();
    slave.slaveofNoOne();
    slave.append("msg", " slave becomes the master");
    System.out.println("Getting message from slave turned master : " + slave.get("msg"));
    Thread.currentThread().sleep(20000);
    master = new Jedis("localhost",6379);
    master.slaveof("localhost", 6380);
    Thread.currentThread().sleep(20000);
    System.out.println("Getting message from master turned slave : " + master.get("msg"));
    master.append("msg", "throw some exceptions !!");
  }
}
  1. 当程序第一次进入睡眠状态时,快速转到主节点的命令提示符并重新启动它(不要触摸从节点)。允许程序完成,输出将类似于以下图像:设置主节点和从节点

主节点变为从节点,从节点变为主节点

  1. 程序中的第二次睡眠是为了主节点与新主节点同步。

  2. 当旧主节点尝试写入密钥时,它会失败,因为从节点无法写入。

  3. 服务器消息,旧奴隶成为新主人时。设置主节点和从节点

从奴隶变成主人

  1. 旧主节点作为新从节点启动时的服务器消息。我们还可以看到,旧主节点重新启动时,作为从节点的第一件事是与新主节点同步并更新其数据集。设置主节点和从节点

主节点变为从节点

  1. 如果在程序中不给第二次睡眠,旧主节点将没有时间与新主节点同步,如果有客户端请求一个密钥,那么它将最终显示密钥的旧值

到目前为止,我们已经了解了 Redis 的主从能力以及在主节点关闭或从节点关闭时它的行为。我们还讨论了主节点向从节点发送数据并复制数据集。但问题仍然是,当 Redis 主节点必须向从节点发送数据时,它发送了什么?为了找出答案,让我们进行一个小实验,这将澄清幕后的活动。

性能模式 - 高读取

在生产环境中,当并发性高时,拥有某种策略变得很重要。采用复制模式肯定有助于在环境中分发负载。在这种模式中遵循的复制模式是向主节点写入并从从节点读取。

性能模式 - 高读取

主节点和从节点中的复制策略

我们将运行的示例不会是之前提到的解决方案的正确复制,因为主节点和从节点将从同一台机器(我的笔记本电脑)运行。通过在同一台机器上运行主节点和从节点,我们利用了共同的内存和处理能力。此外,客户端程序也使用相同的资源。但仍然会观察到差异,因为服务器 I/O 在两个不同的端口上发生,这意味着至少有两个独立的服务器线程(Redis 是单线程服务器)处理读取请求时绑定到两个独立的套接字内存。

在生产环境中,最好是每个节点都在自己的核心上工作,因为 Redis 无法利用多核。

在这个示例中,我们将使用一个主节点和两个从节点。在第一个用例中,我们将使用主节点写入数据,并使用从节点读取数据。我们将记录仅读取所需的总时间,并将其与完全在主节点上进行读取的情况进行比较。

为了准备示例,我们需要准备环境,以下图表简要描述了这个示例的设置应该是什么。在这里请注意,所有资源都来自一台单独的机器:

性能模式 - 高读取

示例设置

以下编写的程序可以适应之前讨论过的两种情况。要在USECASE-1模式下工作(从主节点写入并从主节点读取),请调用以下函数:

  1. 在第一次运行中调用test.setup()

  2. 在第二次运行中调用test.readFromMasterNode()

  3. 请注释以下函数调用,这将不允许USECASE-2运行// test.readFromSlaveNodes();

要在USECASE-2模式下工作(从主节点写入并从两个从节点读取),请调用以下函数,但在此之前,执行FLUSHDB命令清理数据,或者不执行test.setup();函数:

  1. 在第一次运行中调用test.setup();(可选)。

  2. 在第二次运行中调用test.readFromSlaveNodes();

  3. 请注释以下函数调用,这将不允许USECASE-1运行// test.readFromMasterNode();

代码有三个简单的类,类的简要描述如下:

  • MasterSlaveLoadTest:这个类具有以下特点:

  • 这是主类

  • 这个类协调USECASE-1USECASE-2的流程

  • 这个类负责为USECASE-1USECASE-2创建线程

  • 以下是MasterSlaveLoadTest的代码:

package org.learningRedis.chapter.five.highreads;
import java.util.ArrayList;
import java.util.List;
import Redis.clients.jedis.Jedis;
public class MasterSlaveLoadTest {
  private List<Thread> threadList = new ArrayList<Thread>();
  public static void main(String[] args) throws InterruptedException {
    MasterSlaveLoadTest test = new MasterSlaveLoadTest();
    test.setup();
//make it sleep so that the master finishes writing the //values in the datastore otherwise reads will have either //null values
//Or old values.
    Thread.currentThread().sleep(40000); 
    test.readFromMasterNode();
    test.readFromSlaveNodes();
  }
  private void setup() {
    Thread pumpData = new Thread(new PumpData());
    pumpData.start();
  }
  private void readFromMasterNode() {
    long starttime = System.currentTimeMillis();
    for(int number=1;number<11;number++){
      Thread thread = new Thread(new FetchData(number,starttime,"localhost",6379));
      threadList.add(thread);
    }
    for(int number=0;number<10;number++){
      Thread thread =threadList.get(number);
      thread.start();
    }
  }
  private void readFromSlaveNodes() {
    long starttime0 = System.currentTimeMillis();
    for(int number=1;number<6;number++){
      Thread thread = new Thread(new FetchData(number,starttime0,"localhost",6381));
      threadList.add(thread);
    }
    long starttime1 = System.currentTimeMillis();
    for(int number=6;number<11;number++){
      Thread thread = new Thread(new FetchData(number,starttime1,"localhost",6380));
      threadList.add(thread);
    }
    for(int number=0;number<10;number++){
      Thread thread =threadList.get(number);
      thread.start();
    }
  }
}
  • PumpData:这个类具有以下特点:

  • 这个类负责将数据推送到主节点

  • 数据推送是单线程的

  • PumpData的代码如下:

package org.learningRedis.chapter.five.highreads;
import Redis.clients.jedis.Jedis;
public class PumpData implements Runnable {
  @Override
  public void run() {
    Jedis jedis = new Jedis("localhost",6379);
    for(int index=1;index<1000000;index++){
      jedis.append("mesasge-"+index, "my dumb value "+ index);
    }
  }
}
  • FetchData:这个类具有以下特点:

  • 这个类负责从 Redis 节点中获取数据

  • 这个类以多线程模式调用

  • 这个类在启动时传递,因此返回的最后结果将指示执行所花费的总时间

  • FetchData的代码如下:

package org.learningRedis.chapter.five.highreads;
import Redis.clients.jedis.Jedis;
import Redis.clients.jedis.JedisPool;
public class FetchData implements Runnable {
  int endnumber  = 0;
  int startnumber= 0;
  JedisPool jedisPool = null;
  long starttime=0;
  public FetchData(int number, long starttime, String localhost, int port) {
    endnumber   = number*100000;
    startnumber = endnumber-100000;
    this.starttime = starttime;
    jedisPool = new JedisPool(localhost,port);
  }
  @Override
  public void run() {
    Jedis jedis = jedisPool.getResource();
    for(int index=startnumber;index<endnumber;index++){
      System.out.println("printing values for index = message"+index+" = "+jedis.get("mesasge-"+index));
      long endtime = System.currentTimeMillis();
      System.out.println("TOTAL TIME" + (endtime-starttime));
    }
  }
}
  • 运行前面的程序几次,并取出最好和最差的记录,然后取出平均结果。在我运行的迭代中,我得到了以下结果:

  • 对于 USECASE-1,平均时间为 95609 毫秒

  • 对于 USECASE-2,平均时间为 72622 毫秒

  • 尽管在您的机器上结果可能不同,但结果将是相似的。这清楚地表明从从节点读取并写入主节点明显更好。

性能模式 - 高写入

在生产环境中,当对写入的并发需求很高时,有一种策略变得很重要。复制模式确实有助于在环境中分发负载,但是当对写入的并发需求很高时,仅有复制模式是不够的。此外,在 Redis 中,从节点无法进行写入。为了使数据库中的数据写入高并发,重要的是在环境中将数据集分片到许多数据库节点上。许多数据库都具有内置的能力,可以根据需要在节点之间分片数据。除了写入的高并发性外,将数据集分片的优势在于提供部分故障容忍的机制。换句话说,即使其中一个节点宕机,它将使其中包含的数据不可用,但其他节点仍然可以处理它们持有的数据的请求。

作为数据库,Redis 缺乏在许多节点之间分片数据的能力。但是可以在 Redis 之上构建某种智能,来完成分片的工作,从而实现对 Redis 的高并发写入。整个想法是将责任从 Redis 节点中移出,并保留在一个单独的位置。

性能模式-高写入

基于分片逻辑在节点之间分发数据

可以在 Redis 之上构建各种逻辑,用于分发写入负载。逻辑可以基于循环轮询,其中数据可以在顺序排列的节点上分发;例如,数据将会先到M1,然后到M2,然后到M3,依此类推。但是这种机制的问题在于,如果其中一个节点宕机,循环轮询逻辑无法考虑到丢失的节点,它将继续向有问题的节点发送数据,导致数据丢失。即使我们构建逻辑来跳过有问题的节点并将数据放入后续的节点,这种策略将导致该节点拥有自己的数据份额,并且有问题的节点的数据将迅速填满其内存资源。

一致性哈希是一种算法,可以在节点之间平均分发数据时非常有用。基本上,我们根据算法生成一个哈希,将密钥平均分布在整个可用的 Redis 服务器集合中。

Java 的 Redis 客户端已经内置了一致性哈希算法来分发写入。具体如下:

package org.learningRedis.chapter.five.sharding;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.pool.impl.GenericObjectPool.Config;
import Redis.clients.jedis.Jedis;
import Redis.clients.jedis.JedisSentinelPool;
import Redis.clients.jedis.JedisShardInfo;
import Redis.clients.jedis.ShardedJedis;
import Redis.clients.jedis.ShardedJedisPool;
public class MyShards {
  List<JedisShardInfo> shards = new ArrayList<JedisShardInfo>();
  public static void main(String[] args) {
    MyShards test = new MyShards();
    test.setup();
    test.putdata();
  }
  private void setup() {
    JedisShardInfo master0 = new JedisShardInfo("localhost", 6379);
    JedisShardInfo master1 = new JedisShardInfo("localhost", 6369);
    shards.add(master0);
    shards.add(master1);
  }
  private void putdata() {
    ShardedJedisPool pool = new ShardedJedisPool(new Config(), shards);
    for(int index=0;index<10;index++){
      ShardedJedis jedis = pool.getResource();
      jedis.set("mykey"+index, "my value is " + index);
      pool.returnResource(jedis);
    }
    for(int index=0;index<10;index++){
      ShardedJedis jedis = pool.getResource();
      System.out.println("The value for the key is "+ jedis.get("mykey"+index));
      System.out.println("The following information is from master running on port : " + jedis.getShardInfo("mykey"+index).getPort());
      pool.returnResource(jedis);
    }
  }
}

Redis 中的持久化处理

Redis 提供了各种持久化数据的选项。这些机制有助于决定我们的数据需要什么样的持久化模型,这完全取决于我们想要在 Redis 中存储的数据类型。Redis 中有四种选项:

  • 通过 RDB 选项进行持久化

  • 通过 AOF 选项进行持久化

  • 通过 AOF 和 RDB 选项的组合进行持久化

  • 根本不进行持久化

让我们运行一个简单的程序,看看持久化机制的重要性,因为只有这样我们才能意识到持久化的重要性。按照步骤操作,亲自看看缺乏持久化会导致数据丢失:

  1. 启动 Redis 服务器。

  2. 打开 Redis 客户端命令提示符。

  3. 执行命令SET msg 'temporary value'

  4. 手动快速关闭 Redis 服务器,可以在 Linux 中使用Kill-9选项,也可以在 Windows 的命令提示符中使用close选项。

  5. 重新启动 Redis 服务器。

  6. 执行命令get msg

没有持久化处理的 msg

通过 RDB 选项进行持久化

Redis 数据库文件RDB)是 Redis 服务器在定期间隔内持久化数据集的选项,换句话说,定期间隔内在内存中对数据进行快照。该格式是一个单一的、非常紧凑的文件,对于保留数据作为备份非常有用。在灾难发生时,该文件可以充当救命稻草,因此非常重要。Redis 服务器可以配置为在各种间隔内拍摄快照。从性能的角度来看,这种持久化数据的方式将导致更高的性能,因为 Redis 服务器将 fork 一个子进程以非阻塞的方式执行此操作。另一个优点是,由于 RDB 文件中仅存储数据集,因此在 RDB 文件的情况下,服务器的启动非常快。但是,将数据集存储在 RDB 中也有其缺点,因为如果 Redis 在两个快照之间失败,可能会发生数据丢失的可能性。如果数据集的体积非常大,可能会出现另一个问题,因为在这种情况下,Redis 服务器的 fork 子进程将花费时间来加载数据,而这段时间可能会阻塞客户端请求。在生产场景中,这个问题不会出现,因为服务器重新启动和服务器处理客户端请求之间总是有时间差的。从硬件的角度来看,具有更快处理器的机器总是可以解决问题的。

为 RDB 持久性配置 Redis

在这里,我们将学习如何将数据持久化到 RDB 文件中。在 Redis 中,可以通过编辑Redis.conf文件或通过客户端提示来配置 RDB 持久性机制。当我们打开我们的Redis.conf文件并转到快照部分时,我们会看到以下选项:

  • Save 900 1:如果一个键已更改,则在 15 分钟内保存

  • Save 300 10:如果 10 个键已更改,则在 5 分钟内保存

  • Save 60 10000:如果有 10,000 个键已更改,则在 1 分钟内保存

除了这些预配置的选项之外,我们还可以通过调整Redis.conf文件中的值来添加我们自己的选项。客户端还可以用于在运行时为数据集快照添加配置。例如,CONFIG SET SAVE "900 2 300 10"将设置快照为如果 2 个键已更改,则在 15 分钟内保存如果一个键已更改,则在 10 分钟内保存,这将覆盖先前的值。

让我们运行一个简单的程序,就像之前的程序一样,我们会看到由于缺乏持久性而导致的数据丢失,我们将配置 Redis 以具有持久性机制:

  1. 启动您的 Redis 服务器。

  2. 打开一个 Redis 客户端命令提示符。

  3. 执行命令Set msg 'temp value'

  4. 快速手动关闭 Redis 服务器,可以通过 Linux 中的Kill-9选项或 Windows 命令提示符中的close选项。

  5. 重新启动您的 Redis 服务器。

  6. 执行命令get msg为 RDB 持久性配置 Redis

没有持久性处理的获取 msg

  1. 现在执行命令CONFIG SET SAVE "60 1",这告诉 Redis 服务器,如果一个键已更改,则在一分钟内保存数据。

  2. 执行命令Set msg 'temp value'

  3. 等待一分钟或去拿您最喜欢的饮料。

  4. 关闭服务器。

  5. 重新启动您的 Redis 服务器。

  6. 打开一个新的客户端连接并执行命令get msg,将显示如下内容:为 RDB 持久性配置 Redis

获取 msg RDB 持久性处理

  1. 您也可以使用save命令,而不是等待一分钟,该命令将立即将内存中的数据推送到 RDB 文件中。

  2. 将您需要注意的参数为了将数据持久化到 RDB 文件中,如下所示:

  • dbfilename:给出您的 RDB 文件的名称

  • dir:只给出 RDB 文件的路径

  • rdbchecksum yes:这是默认值,它在文件末尾添加 CRC64 校验和,以使其抵抗损坏,但在服务器重新启动时会有轻微的性能损失

使用 RDB 持久性的用例

Redis 可以在数据是无状态的情况下配置 RDB 持久化机制。我想要传达的是,如果数据是一条信息,与之前存储的数据或即将存储的数据没有关系,那么它就是 RDB 持久化的完美候选者。此外,关系可以是序列、时间、排名等,或者数据本身可以包含状态信息。例如,存储的数据是STARTPAUSERESUMESTOP。在这种情况下,如果我们在快照期间丢失PAUSERESUME等数据,那么可能会使整个系统变得不稳定。

让我们来看一个使用情况,网站记录用户在浏览会话中访问的 URL。这些数据被分析以对用户行为进行个人资料化,以便为用户提供更好的服务。在这种情况下,访问的页面的 URL 与之前存储的数据或将来存储的数据没有关系,因此它没有状态。因此,即使在两个快照之间发生故障,如果丢失了一些数据,也不会影响整体分析。

另一个可以使用 RDB 持久化的使用情况是当我们想要将 Redis 用作缓存引擎时,数据写入较少,而数据读取非常频繁。

通过 AOF 选项进行持久化

追加文件AOF)是在 Redis 数据存储中存储数据的持久机制。启用 AOF 后,Redis 将追加所有写入数据集的命令和相关数据,因此当 Redis 服务器重新启动时,它将重建数据集到正确的状态。这种持久性模式在存储具有状态的数据时非常有用。这是因为当我们进行状态管理或者数据集与状态相关联时,在服务器关闭的情况下,存储在内存中的信息(状态信息)将会丢失。这反过来会导致某种状态不匹配。假设我们有一条信息处于状态 A,并且随后对该信息进行的活动将其状态从 A 变为 B,从 B 变为 C,依此类推。现在从用户的角度来看,最后的状态变化将信息带入了 D 状态,这个状态原则上应该在内存中,并且在服务器关闭(崩溃)的情况下,信息将会丢失,因此状态变化信息 D 也将会丢失。因此,当服务器重新启动时,如果用户将该信息的状态更改为 E,状态变化历史将看起来像 A 到 B,B 到 C,C 到 E。在某些情况下,这可能导致数据损坏。AOF 持久化方式解决了由此可能引起的问题。

配置 Redis 进行 AOF 持久化

可以通过更改Redis.conf文件来启用 AOF。需要将属性appendonly设置为yes。通过将其设置为 true,我们告诉 Redis 记录写命令和数据到一个文件中,当服务器重新启动时,它将重新加载这些数据,使其恢复到关闭之前的状态。

Redis 提供了三种策略来缓解由不一致状态引起的问题。第一种策略是记录 AOF 文件中的每个写入事件。这种机制是最安全的,但性能不是很好。可以通过appendfsync always来实现这一点。

第二种机制是基于时间的,我们指示 Redis 服务器缓冲每个写入命令,并安排每秒进行一次 AOF 追加。这种技术更有效,因为它是每秒发生一次,而不是在每次写入时。可以通过告诉 Redisappendfsync everysec来实现这一点。在这种机制中,状态丢失的可能性非常小。

第三种机制更像是一种委托,其中将附加控制权交给底层操作服务器,以将写命令从缓冲区刷新到 AOF 文件。附加的频率是每隔几秒一次(在基于 Linux 的机器上,频率接近每 30 秒一次)。这种技术的性能是最快的,因为这是每 30 秒发生一次。然而,在这种机制中,数据丢失的机会和数量也很高。可以通过告诉 Redis appendfsync no 来实现这种附加方式。

让我们运行一个简单的程序,就像之前的程序一样,其中由于缺乏持久性而导致数据丢失,我们将配置 Redis 以具有 AOF 持久性机制:

  1. 启动 Redis 服务器。

  2. 打开 Redis 客户端命令提示符。

  3. 执行命令Set msg 'temp value'

  4. 快速手动关闭 Redis 服务器,可以在 Linux 中使用Kill-9选项,或者在 Windows 命令提示符中使用close选项。

  5. 重新启动 Redis 服务器。

  6. 执行命令get msg为 AOF 持久性配置 Redis

没有持久性处理的获取消息

  1. 打开您的Redis.conf文件,转到APPEND ONLY MODE部分,并将appendonly no更改为appendonly yes

  2. 取消注释appendfilename appendonly.aof属性。在这里,您可以选择提供自己的名称,但默认名称是appendonly.aof

  3. 将附加机制更改为appendfsync always

  4. 使用以下参数启动 Redis 服务器 --appendonly yes --appendfilename C:\appendonly.aof(如果不想在Redis.conf文件中进行更改,则使用此技术)。

  5. 执行命令Set msg 'temp value'

  6. 快速手动关闭 Redis 服务器,可以在 Linux 中使用Kill-9选项,或者在 Windows 命令提示符中使用close选项。

  7. 使用以下参数重新启动 Redis 服务器 --appendonly yes --appendfilename C:\appendonly.aof(如果不想在Redis.conf文件中进行更改,则使用此技术)。

  8. 执行命令get msg为 AOF 持久性配置 Redis

使用 AOF 持久性处理获取消息

  1. C:\appendonly.aof打开文件并查看以下内容:为 AOF 持久性配置 Redis

打开appendonly.aof

这里可以观察到的一件事是,没有记录get命令,因为它们不会改变数据集。需要记住的一个问题是,如果写入非常频繁,那么 AOF 文件将变得越来越大,服务器重新启动将需要更长的时间。

使用 AOF 持久性的用例

Redis 可以配置为在数据是有状态时具有 AOF 持久性机制。我想在这里传达的是,如果数据是与之前存储的数据有关,或者下一个要存储的数据与之有关,那么它就成为 AOF 持久性的完美候选者。假设我们正在构建一个工作流引擎,其中每个状态都负责下一个状态;在这种情况下,使用 AOF 持久性是最佳选择。

Redis 中的数据集处理命令

我们已经看到客户端程序使用的命令,要么设置数据,要么获取 Redis 中的数据,但是还有一些有用的命令需要处理 Redis 作为数据存储。这些命令有助于在生产环境中维护 Redis,并且通常是 Redis 管理的领域。由于这些命令对 Redis 中存储的数据产生影响,因此在执行它们时应该小心。以下是一些命令:

  • FLUSHDB:此命令删除所选数据库中的所有键(及其保存的数据)。正如我们所见,在 Redis 中,我们可以创建一个更像是 SILO 的数据库,可以以分离的方式存储数据(更像是关注点的分离)。此命令永远不会失败。

  • FLUSHALL:此命令删除 Redis 节点中所有数据库中的所有键。此命令永远不会失败。

  • 监视器:这个命令是一个调试命令,它传递了 Redis 服务器正在处理的所有命令。您可以使用 Redis-cli 或 telnet 来监视服务器正在执行的操作。Redis 中的数据集处理命令

使用 telnet 监视命令

在这里,我们使用 telnet 来监视 Redis 服务器,并且客户端发出的任何命令都会在这里复制。监视命令可以让我们深入了解 Redis 的工作方式,但会有性能损失。您可以使用此命令来监视从节点。

  • SAVE:这是一个同步阻塞调用,将内存中的所有数据保存到 RDB 文件中。在生产环境中,应谨慎使用此命令,因为它会阻塞每个客户端命令并执行此任务。

  • BGSAVE:这个命令更像是后台保存。之前的SAVE命令是一个阻塞调用,但是这个命令不会阻塞客户端调用。通过发出这个命令,Redis 会 fork 另一个进程,该进程开始在后台持久化数据到 RDB 文件中。发出此命令会立即返回OK代码,但客户端可以通过发出LASTSAVE命令来检查结果。让我们尝试一个小例子,看看它是否有效:

  1. 启动 Redis 服务器和一个客户端。

  2. 从客户端执行LASTSAVE命令;在我的情况下,它显示的值是整数1391918354,但在您的情况下可能显示不同的时间。

  3. 打开您的 telnet 提示符并执行MONITOR命令(这是故意为了减缓 Redis 服务器的性能)。

  4. 打开您的 Java 编辑器,并输入以下程序,它将向 Redis 服务器插入大量值:

package org.learningRedis.chapter.five;
import Redis.clients.jedis.Jedis;
public class PushLotsOfData {
  public static void main(String[] args) {
    PushLotsOfData test = new PushLotsOfData();
    test.pushData();
  }
  private void pushData() {
    Jedis jedis = new Jedis("localhost",6379);
    for(int nv=0;nv<900000;nv++){
      jedis.sadd("MSG-0", ",data-"+nv);
    }
  }
}
  1. 在客户端提示符中,我发出了以下命令,结果如下:

Redis 中的数据集处理命令

检查 BGSAVE 的非阻塞特性

我在BGSAVE命令之后发出了TIME命令,但当我发出LASTSAVE时,我得到的时间比BGSAVE命令晚。所以我们可以得出结论,BGSAVE是一种非阻塞保存数据的方式。由于FLUSHALL命令操作整个数据集,它在执行后会自动调用SAVE命令。查看LASTSAVE命令,显示时间为1391920265,以及在FLUSHALL之前的上一个LASTSAVE,显示时间为1391920077,证明了FLUSHALL确实进行了保存。

  • LASTSAVE:这个命令类似于BGSAVE命令,它显示了数据上次持久化到 RDB 文件的时间。

  • SHUTDOWN SAVE/NOSAVE:这个命令基本上退出服务器,但在这之前会关闭整个客户端集合的连接并执行一个阻塞保存,然后如果启用了 AOF,会刷新 AOF。

  • DBSIZE:返回数据库中键的数量。

  • BGREWRITEAOF:这指示 Redis 服务器启动后台写入 AOF。如果此指令失败,旧的 AOF 文件将被保留。

  • CLIENT SETNAME:这个命令设置客户端的名称,当我们执行CLIENT LIST时可以看到设置的名称。在客户端提示符中执行以下命令CLIENT SETNAME "myclient",您应该看到类似以下图像的东西:Redis 中的数据集处理命令

给客户端命名

  • CLIENT LIST:获取连接到 IP 地址和PORT地址的客户端列表。让我们做一个简单的实验:
  1. 使用telnet localhost 6379打开到 Redis 服务器的 telnet 客户端,并执行MONITOR命令。

  2. 打开 Redis 服务器主节点客户端提示符并执行CLIENT LIST命令。命令提示符应该类似于以下图像:

Redis 中的数据集处理命令

获取客户端列表

  • CLIENTKILL:这个命令杀死客户端。现在,对于之前的实验,在我们打开的客户端中发出以下命令:
  1. 执行命令CLIENT KILL 127.0.0.1:1478

  2. 执行CLIENT LIST命令,我们将看到显示的行数减少了一行。

  • DEBUG sEGFAULT:这会导致 Redis 服务器崩溃。该实用程序可用于在开发过程中模拟错误。此命令可用于模拟我们想要通过故意使 Redis 服务器宕机来检查系统的容错性的场景。有趣的是看到从节点的行为,客户端如何处理容错等。

  • SLOWLOG:此命令显示执行过程中哪些命令花费了时间。执行你在性能模式 - 高读取部分编写的程序,并在执行后打开主机的客户端并执行此命令。以下图像中所见的结果是一个快照,不是您在命令提示符中可能得到的完整结果:Redis 中的数据集处理命令

Slowlog 命令

总结

在这一章中,我们看到并学习了如何在 Redis 中处理整个数据集。除此之外,我们还学习了在生产环境中提高性能的模式。我们还学习了管理 Redis 服务器生态系统的命令。

在下一章中,我们将应用我们到目前为止学到的知识来开发 Web 编程中的常见组件,并看看 Redis 如何成为解决这一领域中一些问题的强大工具。

第六章:Web 应用中的 Redis

在当前情况下,Web 是世界今天进行交流的普遍平台。从简单的门户到大规模可扩展的电子商务,协作网站,银行业,社交媒体,移动网络上的 Web 应用等等,每个人都使用 Web 协议作为与外部世界交互的接口。我们通常看到的 Web 平台只是 Web 操作下的一个小部分应用,后端 Web 应用,如供应链管理,订单管理,在线,离线分析等等,也是 Web 应用,或者使用 Web 协议进行集成,例如 HTTP,SOAP,REST 等等。

Web 成功的原因之一是其有效的简单性,开放标准以及多个渠道的操作。它的流行正在迫使人们和公司提出简单,成本效益高,性能卓越,易于维护和开发的解决方案。这种新型软件应该具有内在或外在的能力来扩展和表现良好。

Redis,这种更像瑞士军刀的数据存储,是多面手,是我们在前几章中看到的那些能力的证明。在本章中,我们将扩展和映射 Redis 的能力,用于 Web 领域中使用的组件,并为任何 Web 应用程序的固有部分创建一些概念验证。

为了更好地理解 Redis 的概念,让我们制作一个示例 Web 应用程序,并将 Redis 用作数据存储。这个示例 Web 应用程序无论如何都不是一个完整的端到端 Web 应用程序,但意在突出 Redis 可以派上用场的领域。解决方案本身在功能上并不完整,但意在成为一个从业者可以继续扩展的演示。

Simple E-Commerce,正如我们打算称呼这个演示网站,是一个由 Redis 支持的网站,它没有网页,而是通过简单的服务进行通信。这个想法是暴露简单的服务,而不是引入网页(包含 HTML,CSS 等),以将服务与呈现层解耦。随着我们更多地向单页面应用的时代迈进,我们需要采取一种方法,其中驻留在客户端浏览器内存中的应用程序进行所有协调,而传统的 Web 服务器则通过其提供的服务来处理请求。这种机制的优势在于开发和测试变得容易,因为每个服务都独立于其他服务,并且与 Web 应用程序的呈现方面没有紧密耦合。由于我们都曾经参与过 Web 开发,我们可以理解当我们看到一个错误时所面临的挫败感,以及当花费大量时间来调试问题是因为客户端代码还是它调用的业务方法。随着单页面应用程序的能力不断增强,这个问题在很大程度上可以得到解决,因为业务方法被公开为独立的服务,并且可以与呈现组件分开测试。单页面应用程序的一个显着特点是它将大量的计算活动从服务器端转移到客户端(浏览器),这导致服务器获得更多的计算资源。

简单的电子商务-一个由 Redis 支持的电子商务网站

这个示例电子商务网站,像其他电子商务网站一样,有产品,注册用户可以浏览,购买等等。该网站还根据用户的浏览和购买习惯推荐产品。同时,该网站实时统计网站上发生的活动,并提供实时和软实时分析的功能。因此,让我们开始构建这个网站,就像在任何设计中一样,让我们将需求分成命令,列举如下:

  • 会话和目录管理:以下命令作为服务提供:

  • 注册用户:命令名称为register;此命令将用户注册到系统中。

  • 查看我的数据:命令名称为mydata;此命令将允许用户查看自己的数据。

  • 编辑我的数据:命令名称为editmydata;此命令将允许用户编辑自己的数据。

  • 登录用户:命令名称为login;此命令将登录用户并为用户生成会话 ID,以便与服务器通信。

  • 重新登录用户:命令名称为relogin;此命令将再次登录用户,但会话 ID 将保持不变。用户的所有会话或配置文件数据也将保持不变。

  • 注销用户:命令名称为logout;此命令将注销用户并终止其会话或配置文件数据。

  • 加入购物车:命令名称为add2cart;此命令将商品添加到购物车中。

  • 查看我的购物车:命令名称为showmycart;此命令将显示购物车中的商品。

  • 编辑我的购物车:命令名称为editcart;此命令将编辑用户在购物车中的偏好设置。

  • 购买产品:命令名称为buy;此命令将购买用户购物车中的商品。对于当前应用程序,我们不会将您带到某个商家的网站,而是为您生成一个样本收据。理念是进行分析,所以当有人购买产品时,我们为该产品提供信用积分,这将有助于我们的推荐服务。购买的信用积分为10

  • 委托产品:命令名称为commission;此命令将委托产品并在系统中创建其配置文件。

  • 显示产品:命令名称为display;此命令将显示产品。

  • 浏览产品:命令名称为browse;此命令将记录用户当前浏览的产品。理念是当有人浏览产品时,我们为该产品提供信用积分,这将有助于我们的推荐服务。浏览的信用积分为1

  • 在线分析:以下命令属于此类:

  • 推荐:命令名称为recommendbyproduct;此命令将根据用户正在浏览的产品的热度推荐其他类似产品。

  • 用户统计:命令名称为stats;此命令将显示用户的统计信息。

  • 按类别显示:命令名称为displaytag;此命令将显示某一类别下的产品。

  • 按类别显示历史记录:命令名称为taghistory;此命令将按类别显示历史记录。

  • 书籍访问量:命令名称为visittoday;这将给出一天内独立访客的总数。

  • 购买书籍:命令名称为purchasestoday;这将给出一天内购买该物品的独立访客总数。

为这个简单的电子商务网站保持了非常简单的设计。要了解整个应用程序,请查看以下图表:

简单电子商务-基于 Redis 的电子商务网站

简单设计适用于我们简单的电子商务网站

此练习的先决条件如下:

  • 客户端:任何带有REST插件或 HTTP 客户端插件的浏览器。我将使用带有名为POSTMANREST客户端插件的 Chrome 浏览器。如果您对其他插件感到满意,也可以使用其他插件。如果我们将此客户端替换为纯 Java 程序,例如 Apache Http Client,应用程序将可以正常工作。此简单电子商务应用程序中的服务是基于Get的。在生产系统中,我们应该使用POST,但出于显示目的,这里选择了Get

  • 服务器:任何 Web 应用程序服务器。我们将使用 Tomcat。您可以使用您选择的任何 Web 应用程序服务器,但应相应地创建 Servlet。如果您想使用类似 Node.js 的东西,那么代码将相应更改,但设计理念将保持不变。

  • 数据存储:毋庸置疑,Redis 将是这里的数据存储。

在我们深入代码之前,了解导致我们使用 Redis 的演变过程是很重要的。如前所述,基于这个 Web 应用程序被分为两类,如下所述:

  • 会话和目录管理

  • 在线分析

让我们花点时间了解它们是如何随着时间的推移发展的,以及 Redis 是如何出现的。之后我们将了解这个应用程序的代码。

会话管理

每个 Web 应用程序都以某种方式具有会话。会话管理捕获用户活动的信息,这些信息可以被用户使用,也可以被用户使用。购物车或愿望清单的信息可以被用户使用,后端系统也可以使用相同的信息来分析用户偏好,并将促销和活动管理方案传递给用户。这是电子商务平台中的常见用例之一。存储在会话管理中的信息始终是最新的信息,最终用户期望围绕它进行性能,换句话说,用户将他最近的记忆外包给系统,并期望系统照顾好它。最终用户可能不知道幕后发生的详细和活动水平,但期望会话中存储的信息能够快速和高效地被处理。

在某些情况下,用户的期望甚至超出了他的大脑可以处理的范围;无论是购物车购买,还是把物品放入愿望清单,或者提醒他某个可能已经忘记的活动。换句话说,与任何其他数据相比,最终用户最接近这些数据。他们记住这些数据,并期望系统与之匹配,这导致用户与系统或网站的更个性化的参与。

会话管理

用户及其与电子商务平台的互动

上图是用户与系统(网站)互动的表示。当用户浏览网站时,他/她知道自己在寻找什么。比如在我们的案例中,他正在寻找一些音乐,搜索音乐后,用户将音乐曲目放入购物车。用户也可能对同一流派的其他音乐 CD 感兴趣,或者对评论部分的其他买家的评论感兴趣。在这一点上,用户可能有兴趣购买他/她的音乐 CD,或者将其放在购物车中以便将来购买。用户在这里期望的一件事是,当他再次登录系统时,系统应该记住他放在购物车中的产品。

这里发生了几件事。首先,用户与系统互动,系统通过存储用户的选择、记录用户的活动等方式做出响应。其次,用户已经推送了他可能会感兴趣的信息,从而为他提供了广泛的选择,同时也教育他关于其他人对产品的评论,从而帮助他做出决定。在这一部分,我们将更多地讨论用户存储信息的部分,并称之为会话管理。

会话数据非常重要,留存在用户的记忆中,但这些数据的生命周期很短(直到产品交付或者注意力转移到另一个产品为止)。这就是会话管理的作用所在,在本节中,我们将深入探讨 Redis 如何帮助我们解决这个非常关键的问题。

为了处理会话数据,最早和最简单的选择是使用应用服务器本身的内存。在过去,Web 应用程序的能力有限,提供的服务也有限。使用应用服务器内存是当时的常规。但随着 Web 变得更加普及,人们开始在日常生活中更多地使用 Web,网站迅速增长,为了在 Web 应用程序之间生存下来,必须具备更多的计算和内存资源。

会话管理

使用内存存储会话数据来扩展 Web 应用程序

常见的技术是复制数据并平衡系统,以便所有 Web 服务器处于相同状态,并且可以从任何 Web 应用程序中处理请求。这种技术存在一些问题,因为会话管理与 Web 服务器紧密耦合,它提供了有限的可扩展性,当并发性增加时,这种模式变成了反模式。这种技术的另一个局限性是,随着会话管理中的数据增长,这种模式变得有问题,因为会话数据存储在内存中,而为会话管理分配的内存量受到业务逻辑内存需求的限制。

下一个合乎逻辑的步骤是将会话管理与执行业务逻辑的 Web 应用程序服务器分离。这一步是正确的,因为现在它提供了更多的可扩展性,因为 Web 服务器不再需要进行会话管理,这需要频繁地与对等方同步状态。

会话管理

使用 RDBMS 存储会话数据来扩展 Web 应用程序

尽管这种方法是朝着正确的方向发展的,但也存在一些问题,主要是选择使用的数据存储。RDBMS 用于存储关系数据,并且在处理这些类型的数据时非常高效。另一方面,会话数据更像是键值对,而不具有事务数据所期望的那种关系。将会话数据存储在 RDBMS 中的问题在于性能受到影响,因为 RDBMS 从未为这种类型的数据而设计,尽管 Web 应用程序服务器的扩展更加容易。

这个演进过程的下一步是使用一个既提供可扩展性又提供性能的数据存储。显而易见的选择是使用一个缓存引擎,它将信息存储在内存中,以便性能更快,可扩展性保持良好,因为会话数据与 Web 应用程序服务器分离。

会话管理

使用缓存作为前端,通过 RDBMS 存储会话数据来扩展 Web 应用程序

这种方法的问题在于功能需求和可维护性的角度。从可维护性的角度来看,缓存引擎依赖于 RDBMS 进行数据持久化,因为大多数缓存引擎没有磁盘持久性,并依赖于 RDBMS 进行故障管理。有一些缓存引擎提供持久性机制,但从功能的角度来看,存在一个大问题,因为它们将所有内容存储为键值,其中值是一个字符串。程序的责任是将字符串数据转换为他们感兴趣的信息模式,然后取出值。例如,存储在用户配置文件中的值,其中会话数据中存储了数百个属性。如果用户想要取出一些属性,那么用户必须获取整个数据集,构造对象,然后获取所需的属性。另一个问题是,很多时候我们需要会话数据在固定的时间段内可用,之后数据的可用性就不存在了。在这种情况下,缓存引擎和 RDBMS 都不会证明有益,因为它们没有内置的数据存储的生存时间机制。为了实现这个功能,我们必须编写触发器来从 RDBMS 和缓存中清除数据。

Redis 在这些情况下非常方便,因为它提供了存储信息的方式,可以根据我们的需求使用数据结构来保存值。在会话管理的情况下,我们可以使用映射来逻辑地将属性分组在一起。如果我们需要取出值,我们可以选择要更改的值或向其添加更多属性。此外,Redis 中的性能方面也使其适用于会话管理。Redis 还具有称为生存时间TTL)的功能,以在时间结束后清除数据。这样,我们可以根据需求为所需的键设置单独的 TTL,并且可以在运行时更改 TTL。Redis 可用于具有可扩展和高性能的会话管理。

会话管理

使用缓存作为前端的 Web 应用程序扩展 RDBMS 以存储会话数据

目录管理

目录管理是关于网站希望提供的产品和项目的信息。目录管理下存储的信息可以是产品的成本、尺寸、颜色等,即产品的元信息。与会话信息不同,目录数据是以读为中心的。但与会话数据一样,目录数据也经历了演变,从 RDBMS 系统开始,当时由于缺乏存储数据的选择,RDBMS 系统是当时的自然选择。RDBMS 系统的问题在于它没有提供性能。此外,固定的基于模式的系统也增加了问题,因为产品的元信息随着产品本身的变化而变化。一些产品有颜色、长度、宽度和高度,而一些产品有作者、页数和 ISBN。创建适应这一需求的模式总是很麻烦,而且在某个时候我们都面临过这个问题。

目录管理

使用 RDBMS 作为数据存储的目录管理

克服固定模式问题的自然演化过程是以 XML 格式存储信息,并将此信息缓存到某个缓存引擎中。这种机制帮助设计师和架构师克服了固定模式和性能的问题。但这种技术也带来了自己的问题;在 XML 中的数据在使用之前必须转换为编程语言对象。另一个问题是,如果要更改属性值,那么要么首先在 XML 中更改值,然后在关系数据库管理系统中更改值,要么首先在关系数据库管理系统中更改值,然后在缓存中更改值。这些技术在维护关系数据库管理系统和缓存引擎之间的一致状态方面存在问题,特别是如果属性与产品成本相关。

目录管理

在缓存引擎和关系数据库管理系统之间处理状态管理

Redis 再次派上用场,用于存储目录数据。Redis 是无模式的,作为数据存储提供了数据结构,比如可以用来存储产品所需的许多属性的映射。除此之外,它还提供了改变、添加和读取属性的能力,而无需将整个数据集带到工作中。拥有 Redis 的另一个优势是我们无需进行对象到数据的转换,反之亦然,因为这消除了系统中需要数百个数据对象的必要性;从而使代码库更小,开发更快。

在线分析

在线分析或实时分析是一个相对较新的需求,正在变得流行。在线分析的整个理念是为用户提供更丰富和吸引人的用户体验。在线分析的工作方式是实时收集、分析和处理数据。

在早期的网络革命时代,分析只有一个主要的利益相关者,那就是网站管理团队。他们过去会在离线模式下收集数据并进行分析,然后应用于业务。离线分析技术仍然是必要的。然而,在今天的世界,当一切都与社交媒体相连时,用户的观点、他/她的社交群体和他/她的意见应该反映在他/她的购物体验中。例如,假设一个用户及其社交群体对某种音乐或书籍持有积极看法。当用户登录到他最喜欢的电子商务网站时,该网站的主页上会在推荐部分显示这种产品。这很可能会导致用户最终购买该产品。这种程度的个性化对于网站的成功非常重要。

在这种情况下发生的分析是软实时的,也就是当用户与他的社交群体互动时,数据同时被处理并创建上下文,网站利用这一上下文为用户创建个性化的购物体验。

另一种发生的分析是基于用户在网站浏览产品时创建的上下文。这种上下文的创建是协作性的,尽管用户可能对此并不知情。搜索某种产品或购买某种产品的用户数量越多,该产品就越受欢迎。这种类型的分析的复杂性在于它是实时的,性能至关重要。

从某种意义上说,如果我们将离线分析引擎与实时分析进行比较,不同之处在于,原本不属于业务逻辑范围的分析引擎实际上成为业务逻辑的一部分,实际上共享相同的计算资源。另一个不同之处在于,实时分析的数据量相对较小,但从用户的购物角度来看,它的上下文数据对于业务来说非常重要。以下图表简明地解释了离线和在线(实时)分析之间的差异:

在线分析

Web 应用程序中的离线和在线分析

现在,如果要使用 RDBMS 等数据存储来进行实时处理,问题将在于性能,因为这种处理将消耗大量的计算资源,并且并行执行的其他业务用例可能会受到影响。例如,Oracle 等 RDBMS 可以提供扩展的能力,但它们的价格相当昂贵。

Redis 可以是一个非常好的数据存储,可以用于在线分析。由于 Redis 是基于内存的,它非常快速,并且在 Redis 中实现可伸缩性要容易得多。此外,Redis 提供了诸如 Set 和 Sorted set 之类的数据结构,对于实时分析来说非常有帮助。

Redis 提供的另一个优势是它是开源的,而且 Redis 的运行时资源需求非常少。此外,Redis 在处理并发调用方面的能力非常令人印象深刻。

在我们将开发的示例应用程序中,我们将看到一些实时分析,例如基于其流行度推荐产品的推荐引擎。

实施-简单的电子商务

让我们从一些代码开始,以便清楚地了解如何使用 Redis 进行会话、目录管理和在线分析。但在这之前,让我们确定要创建的存储数据的桶:

  • @userdata”桶:该桶将存储用户配置文件数据,例如姓名、电子邮件、电话号码、地址等。从应用程序的角度来看,这个桶将是用户的sessionID,它将把这个桶与"<sessionID>@sessiondata"绑定在一起。这里使用的数据结构是 Map。

  • @sessiondata”桶:该桶将存储用户的会话数据,例如上次登录和登录状态。除了会话数据,这里还将存储用户名,因为这是将"<username>@userdata"桶绑定到该桶的关键。这里使用的数据结构是 Map。

  • @browsinghistory”桶:该桶将根据用户的会话 ID 存储用户的浏览历史。这里使用的数据结构是 Sorted Set。

  • @purchasehistory”桶:这将提供用户的购买历史。这里使用的数据结构是 Sorted Set。

  • @shoppingcart”桶:该桶将存储用户的购物车项目。这里使用的数据结构是 Map。

  • “sessionIdTracker”桶:这将跟踪系统中的用户总数。这里使用的数据结构是 Bitmap。

  • ”桶:这将存储产品属性。由于无模式,它可以存储产品的任意数量的属性。这里使用的数据结构是 Map。

  • ”桶:这将存储与该标签相关联的产品。例如,“学习 Redis”可以被标记为 Redis、NoSQL、数据库等标签。这里使用的数据结构是 Sorted Set。

  • @visit”桶:这将存储独立访问者的数量。在生产系统中,这可以每天进行一次,以便统计每天有多少人访问了该产品,并帮助计算每月有多少人访问了该网站。这里使用的数据结构是 Bitmap。

  • Bucket name "@purchase":这将存储购买产品的独立访问者数量。与之前的桶一样,可以每天制作这个桶,以便为一周或一个月提供聚合计数。这里使用的数据结构是位图。

现在我们已经了解了我们的数据库将会是什么样子,让我们来看看将要接受来自浏览器的服务请求并向客户端发送 HTTP 响应的 servlet。

在这个简单的电子商务网站中有两个 servlet。它们将接受所有命令,并列在下面:

  • UserApp servlet:这将处理与用户相关的所有命令

  • ProductApp servlet:这将处理与用户相关的所有命令

我们必须记住的一件事是,执行的顺序不依赖于 servlet 或 servlet 中的命令的顺序。例如,除非我们在系统中提供了一些产品,否则注册或登录是没有意义的,或者除非我们浏览或购买了一些产品,否则查看推荐是没有意义的,因为这将为推荐创建图形数据。

让我们先了解一下在本章节的其余部分中将在代码清单中使用的所有实用类。所有这些类的列表如下:

  • Commands:这是所有将在应用程序中实现的命令的父类和抽象类:
package org.learningRedis.web;
import org.learningRedis.web.util.Argument;
public abstract class Commands {
  private Argument argument;
  public Commands(Argument argument) {
    this.argument = argument;
  }
  public abstract String execute();
  public Argument getArgument() {
    return argument;
  }
}
  • 默认命令:这是默认命令,如果 URL 中传递的命令未被应用程序识别,将会执行该命令:
package org.learningRedis.web;
import org.learningRedis.web.util.Argument;
public class DefaultCommand extends Commands {
  public DefaultCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    return "Command Not Recognized !!";
  }
}
  • Argument:这个类的主要目标是封装请求中传入的所有名称值属性,并将其放入一个地图中,以便以后在程序中使用:
package org.learningRedis.web.util;
import java.util.HashMap;
import java.util.Map;
public class Argument {
  Map<String, String> argumentMap = new HashMap<String, String>();
  public Argument(String args) {
    String[] arguments = args.split(":");
    for (String argument : arguments) {
      String key = argument.split("=")[0];
      String value = argument.split("=")[1];
      argumentMap.put(key, value);
    }
  }
  public String getValue(String key) {
    return argumentMap.get(key);
  }
  public Map<String, String> getAttributes() {
    return argumentMap;
  }
}

现在我们已经涵盖了应用程序中的所有实用类,让我们来看看将对应用程序形成的类。

ProductApp

ProductApp servlet 将包含围绕产品管理的命令。ProductApp servlet 的代码如下:

package org.learningRedis.web;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.learningRedis.web.analytics.commands.PurchasesCommand;
import org.learningRedis.web.analytics.commands.VisitTodayCommand;
import org.learningRedis.web.productmgmt.commands.CommissionProductCommand;
import org.learningRedis.web.productmgmt.commands.DisplayTagCommand;
import org.learningRedis.web.productmgmt.commands.DisplayCommand;
import org.learningRedis.web.productmgmt.commands.TagHistoryCommand;
import org.learningRedis.web.productmgmt.commands.UpdateTagCommand;
import org.learningRedis.web.util.Argument;
public class ProductApp extends HttpServlet {
  public ProductApp() {
    super();
  }
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String command = request.getParameter("command");
    Argument argument = new Argument(request.getParameter("args"));
    PrintWriter out = response.getWriter();
    switch (command.toLowerCase()) {
    case "commission":
      Commands commission = new CommissionProductCommand(argument);
      out.println(commission.execute());
      break;
    case "display":
      Commands display = new DisplayCommand(argument);
      out.println(display.execute());
      break;
    case "displaytag":
      Commands displaytag = new DisplayTagCommand(argument);
      out.println(displaytag.execute());
      break;
    case "updatetag":
      Commands updatetag = new UpdateTagCommand(argument);
      out.println(updatetag.execute());
      break;
    case "visitstoday":
      Commands visittoday = new VisitTodayCommand(argument);
      out.println(visittoday.execute());
      break;
    case "purchasestoday":
      Commands purchasestoday = new PurchasesTodayCommand (argument);
      out.println(purchasestoday.execute());
      break;
    case "taghistory":
      Commands taghistory = new TagHistoryCommand(argument);
      out.println(taghistory.execute());
      break;
    default:
      Commands defaultUC = new DefaultCommand(argument);
      out.println(defaultUC.execute());
      break;
    }
  }
}

现在我们已经准备好了第一个 servlet,让我们来看看我们为此实现的命令:

  • CommisionProductCommand:这将实现委托命令。命令的实现如下:
package org.learningRedis.web.productmgmt.commands;
import java.util.Map;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.ProductDBManager;
public class CommissionProductCommand extends Commands {
    public CommissionProductCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    Map<String, String> productAttributes = this.getArgument().getAttributes();
    boolean commisioning_result = ProductDBManager.singleton.commisionProduct(productAttributes);
    boolean tagging_result = ProductDBManager.singleton.enterTagEntries(productAttributes.get("name"),
        productAttributes.get("tags"));
    if (commisioning_result & tagging_result) {
      return "commisioning successful";
    } else {
      return "commisioning not successful";
    }
  }
}

测试 URL:http://localhost:8080/simple-ecom/productApp?command=commission&args=name=Redisbook-1:cost=10:catagory=book:author=vinoo:tags=Redis@5,NoSql@3,database@2,technology@1

描述:出于所有原因,这应该是第一个应该调用的命令,因为这个命令将在系统中提供产品。需要关注 URL 中的两个部分,即等于commissioncommandargs部分。这里args包含书的属性,例如name=Redisbook-1。属性tags表示书将关联的单词。这本书的标签是Redis@5NoSQl@3database@2technology@1。标签与权重相关,当推荐引擎启动时,权重将发挥作用。每当用户浏览Redisbook-1时,他将看到更多关于 Redis 书籍的推荐。在这里,用户将看到关于 Redis 的五本书,关于 NoSQL 的三本书,依此类推。为了简化这个应用程序,权重的总和应该是 10。

ProductApp

成功产品委托的截图

为了创建测试数据,使用不同权重委托几本测试书籍,其中一些具有相同的标签,另一些具有略有不同的标签。确保权重的总和等于 10。

  • 显示命令:这将实现显示命令。命令的实现如下:
package org.learningRedis.web.productmgmt.commands;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.ProductDBManager;
public class DisplayCommand extends Commands {
  public DisplayCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    String display = ProductDBManager.singleton.getProductInfo(this.getArgument().getValue("name"));
    return display;
  }
}

测试 URL:http://localhost:8080/simple-ecom/productApp?command=display&args=name=Redisbook-1

描述:该程序将显示书的属性。需要关注 URL 中的两个部分,即等于显示的命令和参数部分,即 args。这里,args 包含一个名为 name 的属性。

ProductApp

成功显示产品属性的屏幕截图

  • DisplayTagCommand:这将实现browse命令。命令的实现如下:
package org.learningRedis.web.productmgmt.commands;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.ProductDBManager;
public class DisplayTagCommand extends Commands {
  public DisplayTagCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    String tagName = this.getArgument().getValue("tagname");
    String details = ProductDBManager.singleton.getTagValues(tagName);
    return details;
  }
}

测试 URL:http://localhost:8080/simple-com/productApp?command=displaytag&args=tagname=nosql

描述:该程序将根据书的点击量显示书籍。需要关注 URL 中的两个部分,即command,等于displaytag,以及参数部分,即args。这里args包含一个名为tagname的属性。由于我已经将一本书委托给系统,输出如下图所示。当用户开始浏览产品时,请稍后访问此标签;当您执行相同的命令时,顺序将发生变化。

ProductApp

成功显示属于 NoSQL 标签的产品的屏幕截图

  • UpdateTag:这将实现UpdateTagCommand命令。命令的实现如下:
package org.learningRedis.web.productmgmt.commands;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.AnalyticsDBManager;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.ProductDBManager;
public class UpdateTagCommand extends Commands {
  public UpdateTagCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    String sessionid = this.getArgument().getValue("sessionid");
    String productname = this.getArgument().getValue("productname");
    String details = this.getArgument().getValue("details");
    String actionType = this.getArgument().getValue("action");
    switch (actionType.toLowerCase()) {
    case "browse":
      if (productname != null & ProductDBManager.singleton.keyExist(productname)) {
        AnalyticsDBManager.singleton.updateRatingInTag(productname, 1);
        AnalyticsDBManager.singleton.updateProductVisit(sessionid, productname);
      }
      break;
    case "buy":
      System.out.println("Buying the products in the shopping cart !! ");
      String[] products = details.split(",");
      for (String product : products) {
        if (product != null & !product.trim().equals("")) {
          AnalyticsDBManager.singleton.updateRatingInTag(product, 10);
          AnalyticsDBManager.singleton.updateProductPurchase(sessionid, product);
        }
      }
      break;
    default:
      System.out.println("The URL cannot be acted uppon  ");
      break;
    }
    return "";
  }
}

测试 URL:http://localhost:8080/simple-ecom/productApp?command=updatetag&args=sessionid=<用户的 sessionID>:productname=<用户正在浏览或已购买的产品名称>:action=<浏览或购买>

描述:当用户浏览产品或购买产品时,将调用此命令。该命令背后的想法是,当用户浏览产品或购买产品时,该产品正在变得受欢迎,因此,该产品在同一标签下的其他产品中的受欢迎程度应该相应增加。简而言之,它有助于计算其类别(标签)中最受欢迎的产品。要测试此命令,请确保创建一些虚拟用户并使其登录系统,然后点击browse命令 URL 或buy命令 URL。

  • VisitTodayCommand:这将实现browse命令。命令的实现如下:
package org.learningRedis.web.analytics.commands;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.AnalyticsDBManager;
import org.learningRedis.web.util.Argument;
public class VisitTodayCommand extends Commands {
  public VisitTodayCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + "Entering the execute function");
    String productName = this.getArgument().getValue("productname");
    Integer visitCount = AnalyticsDBManager.singleton.getVisitToday(productName);
    System.out.println(this.getClass().getSimpleName() + ":  " + "Printing the result for execute function");
    System.out.println("Result = " + "Total Unique Visitors are: " + visitCount.toString());
    return "Total Unique Visitors are: " + visitCount.toString();
  }
}

测试 URL:http://localhost:8080/simple-ecom/productApp?command=visitstoday&args=productname=Redisbook-1

描述:如果我们想要检查有多少独立用户访问了该产品,可以执行此命令。实现此用例的数据结构是位图。 Redis 中的位图具有一致的性能,不受其持有的数据影响。

ProductApp

显示产品 redisbook-1 每天的观看者总数的屏幕截图

  • PurchasesTodayCommand:这将实现purchasestoday命令。命令的实现如下:
package org.learningRedis.web.analytics.commands;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.ProductDBManager;
public class PurchasesTodayCommand extends Commands {
  public PurchasesTodayCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + "Entering the execute function");
    String productName = this.getArgument().getValue("productname");
    Integer purchaseCount = ProductDBManager.singleton.getPurchaseToday(productName);
    System.out.println(this.getClass().getSimpleName() + ":  " + "Printing the result for execute function");
    System.out.println("Result = " + "Total Unique Customers are: " + purchaseCount.toString());
    return "Total Unique Customers are: " + purchaseCount.toString();
  }
}

测试 URL:http://localhost:8080/simple-ecom/productApp?command=purchasestoday&args=productname=Redisbook-1

描述:如果我们想要检查有多少独立用户购买了给定的产品,可以执行此命令。实现此用例的数据结构是位图。 Redis 中的位图具有一致的性能,不受其持有的数据影响。

ProductApp

显示产品 redisbook-1 每天的买家总数的屏幕截图

  • TagHistoryCommand:这将实现browse命令。命令的实现如下:
package org.learningRedis.web.productmgmt.commands;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.AnalyticsDBManager;
import org.learningRedis.web.util.Argument;
public class TagHistoryCommand extends Commands {
  public TagHistoryCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    String tagname = this.getArgument().getValue("tagname");
    String tagHistory = AnalyticsDBManager.singleton.getTagHistory(tagname);
    return tagHistory;
    }
    }

测试 URL:http://localhost:8080/simple-ecom/productApp?command=taghistory&args=tagname=Redis

描述:如果我们想要查看产品的标签历史记录,可以执行此命令。产品的排名基于属于该标签的各个产品积累的积分。在以下示例中,我们显示了标签Redis的排名:

ProductApp

显示标签 redis 的标签历史的屏幕截图

测试 URL:http://localhost:8080/simple-ecom/productApp?command=taghistory&args=tagname=nosql

描述:如果我们想要查看产品的标签历史记录,可以执行此命令。产品的排名基于属于该标签的各个产品积累的积分。在以下示例中,我们展示了标签nosql的排名以展示差异:

ProductApp

显示标签nosql的历史记录的屏幕截图

UserApp

UserApp servlet 将包含围绕用户管理和用户分析的命令。UserApp servlet 的代码如下:

package org.learningRedis.web;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.learningRedis.web.analytics.commands.MyStatusCommand;
import org.learningRedis.web.analytics.commands.RecomendByProduct;
import org.learningRedis.web.sessionmgmt.commands.Add2CartCommand;
import org.learningRedis.web.sessionmgmt.commands.BrowseCommand;
import org.learningRedis.web.sessionmgmt.commands.BuyCommand;
import org.learningRedis.web.sessionmgmt.commands.EditCartCommand;
import org.learningRedis.web.sessionmgmt.commands.EditMyDataCommand;
import org.learningRedis.web.sessionmgmt.commands.LoginCommand;
import org.learningRedis.web.sessionmgmt.commands.LogoutCommand;
import org.learningRedis.web.sessionmgmt.commands.MyDataCommand;
import org.learningRedis.web.sessionmgmt.commands.MyPurchaseHistory;
import org.learningRedis.web.sessionmgmt.commands.RegistrationCommand;
import org.learningRedis.web.sessionmgmt.commands.ReloginCommand;
import org.learningRedis.web.sessionmgmt.commands.ShowMyCartCommand;
import org.learningRedis.web.util.Argument;
public class UserApp extends HttpServlet {
  private static final long serialVersionUID = 1L;
  public UserApp() {
    super();
  }
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String command = request.getParameter("command");
    Argument argument = new Argument(request.getParameter("args"));
    PrintWriter out = response.getWriter();
    switch (command.toLowerCase()) {
    case "register":
      Commands register = new RegistrationCommand(argument);
      out.println(register.execute());
      break;
    case "login":
      Commands login = new LoginCommand(argument);
      out.println(login.execute());
      break;
    case "mydata":
      Commands mydata = new MyDataCommand(argument);
      out.println(mydata.execute());
      break;
    case "editmydata":
      Commands editMyData = new EditMyDataCommand(argument);
      out.println(editMyData.execute());
      break;
    case "recommendbyproduct":
      Commands recommendbyproduct = new RecomendByProductCommand (argument);
      String recommendbyproducts = recommendbyproduct.execute();
      out.println(recommendbyproducts);
      break;
    case "browse":
      Commands browse = new BrowseCommand(argument);
      String result = browse.execute();
      out.println(result);
      String productname = argument.getValue("browse");
      String sessionid = argument.getValue("sessionid");
      request.getRequestDispatcher(
          "/productApp?command=updatetag&args=sessionid=" + sessionid + ":productname=" + productname
              + ":action=browse").include(request, response);
      break;
    case "buy":
      Commands buy = new BuyCommand(argument);
      String[] details = buy.execute().split("#");
      out.println(details[0]);
      String sessionID = argument.getValue("sessionid");
      request.getRequestDispatcher(
          "/productApp?command=updatetag&args=sessionid=" + sessionID + ":action=buy:details=" + details[1])
          .include(request, response);
      break;
    case "stats":
      Commands stats = new MyStatusCommand(argument);
      out.println(stats.execute());
      break;
    case "add2cart":
      Commands add2cart = new Add2CartCommand(argument);
      out.println(add2cart.execute());
      break;
    case "showmycart":
      Commands showmycart = new ShowMyCartCommand(argument);
      out.println(showmycart.execute());
      break;
    case "editcart":
      Commands editCard = new EditCartCommand(argument);
      out.println(editCard.execute());
      break;
    case "relogin":
      Commands relogin = new ReloginCommand(argument);
      out.println(relogin.execute());
      break;
    case "logout":
      Commands logout = new LogoutCommand(argument);
      out.println(logout.execute());
      break;
    case "mypurchasehistory":
      Commands mypurchasehistory = new MyPurchaseHistoryCommand (argument);
      out.println(mypurchasehistory.execute());
      break;
    default:
      Commands defaultUC = new DefaultCommand(argument);
      out.println(defaultUC.execute());
      break;
    }
  }
}

现在我们已经准备好了第一个 servlet,让我们来看看我们为此实现的命令:

  • RegistrationCommand:这将实现register命令。命令的代码如下:
package org.learningRedis.web.sessionmgmt.commands;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.UserDBManager;
public class RegistrationCommand extends Commands {
  public RegistrationCommand(Argument argument) {
    super(argument);
  }
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    String name = this.getArgument().getValue("name");
    if (!UserDBManager.singleton.doesUserExist(name)) {
      UserDBManager.singleton.createUser(this.getArgument().getAttributes());
    } else {
      return "user already registered in ";
    }
    return "successful registeration  -> " + name;
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=register&args=name=vinoo:password=******:address=test address

描述:此命令将用户注册到系统中。需要关注 URL 中的两个部分,即command,等于register,以及参数部分,即args。这代表了键值对中的属性。下图表示注册成功的情况。下一个逻辑步骤将是登录用户。

UserApp

显示用户注册的屏幕截图

  • LoginCommand:这将实现login命令。命令的代码如下:
package org.learningRedis.web.sessionmgmt.commands;
import java.util.HashMap;
import java.util.Map;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.AnalyticsDBManager;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.ProductDBManager;
import org.learningRedis.web.util.UserDBManager;
public class LoginCommand extends Commands {
  public LoginCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    String name = this.getArgument().getValue("name");
    String password = this.getArgument().getValue("password");
    if (UserDBManager.singleton.doesUserExist(name)) {
      if (UserDBManager.singleton.getUserPassword(name).equals(password)
          & UserDBManager.singleton.getUserSessionId(name).equals("null")) {
        String sessionID = ProductDBManager.getRandomSessionID();
        UserDBManager.singleton.login(sessionID, name);
        Map<String, String> map = new HashMap<String, String>();
        map.put("sessionID", sessionID);
        UserDBManager.singleton.setRegistrationMap(name, map);
        System.out.println("login map : " + map);
        AnalyticsDBManager.singleton.registerInSessionTracker(sessionID);
        return "Login successful \n" + name + " \n use the following session id : " + sessionID;
      } else if (UserDBManager.singleton.getUserPassword(name).equals(password)
          & !UserDBManager.singleton.getUserSessionId(name).equals("null")) {
        return " Login failed ...u r already logged in \n please logout to login again \n or try relogin command ";
      } else {
        return " Login failed ...invalid password ";
      }
    } else {
      return " please register before executing command for login ";
    }
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=login&args=name=vinoo:password=******

描述:此命令将用户登录到系统中。需要关注 URL 中的两个部分,即command,等于login,以及参数部分,即args。参数将包含名称和密码。需要关注的重要部分是,执行此命令将返回一个会话 ID 代码。大多数用户将执行的命令都需要此会话 ID。因此,如果您正在运行此命令的示例,请确保将此数字存储在文本文件中以供以后使用。在生产系统中,可以将其存储在浏览器或客户端的内存中。下图告诉我为我生成的会话 ID 是26913441。我将在执行的其余示例中使用它:

UserApp

显示用户登录和用户会话 ID 的屏幕截图

  • MyDataCommand:这将实现mydata命令。命令的代码如下:
package org.learningRedis.web.sessionmgmt.commands;
import java.util.Map;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.UserDBManager;
public class MyDataCommand extends Commands {
  public MyDataCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    String sessionid = this.getArgument().getValue("sessionid");
    String name = UserDBManager.singleton.getUserName(sessionid);
    Map<String, String> map = UserDBManager.singleton.getRegistrationMap(name);
    return map.toString();
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=mydata&args=sessionid=26913441

描述:此命令将显示系统中用户的数据。需要关注 URL 中的两个部分,即command,等于mydata,以及参数部分,即args。参数在 URL 中只有会话 ID 作为键值对。下图显示了命令的结果。由于某些属性无法在图中显示,因此未显示。

UserApp

显示用户数据的屏幕截图

  • EditMyDataCommand:这将实现editmydata命令。命令的代码如下:
package org.learningRedis.web.sessionmgmt.commands;
import java.util.Map;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.UserDBManager;
public class EditMyDataCommand extends Commands {
  public EditMyDataCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    Map<String, String> editMap = this.getArgument().getAttributes();
    boolean result = UserDBManager.singleton.editRegistrationMap(editMap);
    if (result) {
      return "Edit is Done....";
    } else {
      return "Edit not Done.... please check sessionid and name combination";
    }
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=editmydata&args=name=vinoo:password=******:address=changed address:phone=9111111119:sessionid=26913441

描述:此命令将显示系统中用户的数据。需要关注 URL 中的两个部分,即command,等于mydata,以及参数部分,即args。参数具有新的和编辑后的键值对。确保 URL 中的会话 ID 是正确的。下图是您应该在输出中看到的内容。现在您可以随时返回并执行以前的mydata命令,这将显示更新后的值。

UserApp

成功编辑用户数据的屏幕截图

  • BrowseCommand:这将实现browse命令。命令的实现如下:
package org.learningRedis.web.sessionmgmt.commands;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.AnalyticsDBManager;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.ProductDBManager;
public class BrowseCommand extends Commands {
  public BrowseCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    String productname = this.getArgument().getValue("browse");
    if (ProductDBManager.singleton.keyExist(productname)) {
      AnalyticsDBManager.singleton.updateBrowsingHistory(this.getArgument().getValue("sessionid"), productname);
      StringBuffer stringBuffer = new StringBuffer();
      stringBuffer.append("You are browsing the following product = " + productname + "\n");
      stringBuffer.append(ProductDBManager.singleton.getProductInfo(productname));
      return stringBuffer.toString();
    } else {
      return "Error: The product you are trying to browse does not exist i.e. " + productname;
    }
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=browse&args=sessionid=26913441:browse=Redisbook-1

描述:此命令将显示系统中产品的数据。需要关注的 URL 中的两个部分是command,它等于browse,以及参数部分,即args。参数包含用户的会话 ID 和用户正在浏览的产品的名称。这里发生了几件事情。用户可以查看产品详情,同时后台会发送请求到updatetag命令,以增加相应产品的热度。在我们的案例中,产品是Redisbook-1。为了测试,多次浏览您已经委托到系统中的所有产品。

UserApp

用户想要浏览产品并查看其详情时的屏幕截图

  • RecommendByProductCommand:这将实现recommendbyproduct命令。命令的代码如下:
package org.learningRedis.web.analytics.commands;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.AnalyticsDBManager;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.ProductDBManager;
public class RecomendByProductCommand extends Commands {
  int totalrecomendations = 10;
  public RecomendByProductCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    StringBuffer buffer = new StringBuffer();
    String productname = this.getArgument().getValue("productname");
    buffer.append("If you are lookinging into " + productname + " you might also find the following \n");
    buffer.append("products interseting... \n");
    Map<String, Integer> tags = ProductDBManager.singleton.getProductTags(productname);
    // Lets get total sum of weights
    int totalweight = 0;
    Set<String> keys = tags.keySet();
    for (String key : keys) {
      totalweight = totalweight + tags.get(key);
    }
    for (String key : keys) {
      int slotfortag = Math.round(totalrecomendations * tags.get(key) / totalweight);
      List<String> productnames = AnalyticsDBManager.singleton.getTopProducts(slotfortag, key);
      for (String product : productnames) {
        if (!product.equals(productname)) {
          buffer.append("For tag = " + key + " the recomended product is " + product);
          buffer.append("\n");
        }
      }
    }
    System.out.println(this.getClass().getSimpleName() + ":  " + "Printing the result for execute function");
    System.out.println("Result = " + buffer.toString());
    return buffer.toString();
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=recommendbyproduct&args=sessionid=26913441:productname=Redisbook-1

描述:此命令将基于正在浏览的产品为用户推荐产品。需要关注的 URL 中的两个部分是command,它等于recommendbyproduct,以及参数部分,即args。参数包含用户的会话 ID 和产品Redisbook-1

该命令将基于产品的购买和浏览历史为用户推荐热门产品。这将考虑产品所属的类别以及需要考虑产品展示的权重。这在用户浏览产品时是实时在线分析的一种方式。在图中,最大数量的结果是Redis标签,因为该标签具有最大权重。在生产中,需要对可能出现相似产品的重复结果进行一些过滤。这种过滤可以在客户端完成,从而节省服务器端的计算资源。

UserApp

用户想要浏览产品并查看其他推荐产品时的屏幕截图

  • Add2CartCommand:这将实现add2cart命令。命令的实现如下:
package org.learningRedis.web.sessionmgmt.commands;
import java.util.HashMap;
import java.util.Map;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.ShoppingCartDBManager;
import org.learningRedis.web.util.UserDBManager;
public class Add2CartCommand extends Commands {
  public Add2CartCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    String result = "did not update the shopping cart";
    String sessionid = this.getArgument().getValue("sessionid");
    String product = this.getArgument().getValue("product");
    String[] productList = product.split(",");
    Map<String, String> productQtyMap = new HashMap<String, String>();
    for (String _product : productList) {
      String[] nameQty = _product.split("@");
      productQtyMap.put(nameQty[0], nameQty[1]);
    }
    if (UserDBManager.singleton.doesSessionExist(sessionid)) {
      result = ShoppingCartDBManager.singleton.addToShoppingCart(sessionid, productQtyMap);
    }
    return "Result : " + result;
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=add2cart&args=sessionid=26913441:product=Redisbook-1@2,Redisbook-4@1

描述:此命令将产品及其数量放入购物车。需要关注的 URL 中的两个部分是command,它等于add2cart,以及参数部分,即args。参数包含两个键值对。第一个是会话 ID,第二个是产品的名称和数量,用特殊字符@分隔。以下图显示了我已成功将产品添加到购物车中:

UserApp

用户想要将产品添加到购物车时的屏幕截图

  • ShowMyCartCommand:这将实现showmycart命令。命令的实现如下:
package org.learningRedis.web.sessionmgmt.commands;
import java.util.Map;
import java.util.Set;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.ShoppingCartDBManager;
public class ShowMyCartCommand extends Commands {
  public ShowMyCartCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    String sessionid = this.getArgument().getValue("sessionid");
    Map<String, String> productMap = ShoppingCartDBManager.singleton.myCartInfo(sessionid);
    StringBuffer stringBuffer = new StringBuffer();
    if (!productMap.isEmpty()) {
      stringBuffer.append("Your shopping cart contains the following : ");
      stringBuffer.append("\n");
      Set<String> set = productMap.keySet();
      int i = 1;
      for (String str : set) {
        stringBuffer.append("[" + i + "] product name = " + str + " Qty = " + productMap.get(str) + "\n");
        i++;
      }
      return stringBuffer.toString();
    } else {
      return " your shopping cart is empty.";
    }
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=showmycart&args=sessionid=26913441

描述:此命令将产品及其数量放入购物车。需要关注的 URL 中的两个部分是command,它等于showmycart,以及参数部分,即args。参数只包含会话 ID。以下图显示了我的购物车:

UserApp

用户想要查看他的购物车时的屏幕截图

  • EditCartCommand:这将实现editcart命令。命令的实现如下:
package org.learningRedis.web.sessionmgmt.commands;
import java.util.HashMap;
import java.util.Map;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.ShoppingCartDBManager;
import org.learningRedis.web.util.UserDBManager;
public class EditCartCommand extends Commands {
  public EditCartCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    String result = "did not edit the shopping cart";
    String sessionID = this.getArgument().getValue("sessionid");
    String product = this.getArgument().getValue("product");
    String[] productList = product.split(",");
    Map<String, String> productQtyMap = new HashMap<String, String>();
    for (String _product : productList) {
      String[] nameQty = _product.split("@");
      productQtyMap.put(nameQty[0], nameQty[1]);
    }
    if (UserDBManager.singleton.doesSessionExist(sessionID)) {
      result = ShoppingCartDBManager.singleton.editMyCart(sessionID, productQtyMap);
    }
    return "result : " + result;
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=editcart&args=sessionid=26913441:product=Redisbook-4@0,Redisbook-2@1

描述:此命令将编辑购物车中的产品和它们的数量。需要关注 URL 中的两个部分,一个是command,等于editcart,另一个是参数部分,即args。参数包含产品及其新数量的键值对。如果数量标记为0,则产品将从购物车中移除。再次执行showmycart命令,购物车应该反映出更新的值。以下图显示了更新的值:

UserApp

用户在编辑购物车后想要查看购物车的屏幕截图

  • BuyCommand:这将实现browse命令。命令的实现如下:
package org.learningRedis.web.sessionmgmt.commands;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.ShoppingCartDBManager;
public class BuyCommand extends Commands {
  public BuyCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    String sessionid = this.getArgument().getValue("sessionid");
    String shoppingdetails = ShoppingCartDBManager.singleton.buyItemsInTheShoppingCart(sessionid);
    return shoppingdetails;
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=buy&args=sessionid=26913441

描述:此命令将购买购物车中的产品。由于这是一个演示网站,与支付网关没有连接,但拥有此命令的意图是在进行购买时增加“点击”计数器。购买产品时,推荐引擎的点数会增加 10 个,而在浏览产品时只增加 1 个:

UserApp

进行虚拟购买

此时,回顾recommendbyproduct命令将会非常有趣。产品显示的顺序会改变,因为每次购买都会给产品的受欢迎程度增加 10 个点。recommendbyproduct是针对产品Redisbook-1的。测试 URL 如下:http://localhost:8080/simple-ecom/userApp?command=recommendbyproduct&args=sessionid=26913441:productname=Redisbook-1

UserApp

成功购买后重新排列产品列表的屏幕截图(在线分析)

  • MyStatusCommand:这将实现stats命令。命令的实现如下:
package org.learningRedis.web.analytics.commands;
import java.util.Iterator;
import java.util.Set;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.AnalyticsDBManager;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.UserDBManager;
public class MyStatusCommand extends Commands {
  public MyStatusCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + "Entering the execute function");
    String sessionID = this.getArgument().getValue("sessionid");
    if (UserDBManager.singleton.doesSessionExist(sessionID)) {
      Set<String> browsingHistory = AnalyticsDBManager.singleton.getBrowsingHistory(sessionID);
      StringBuffer buffer = new StringBuffer();
      buffer.append(" View your browsing history where the one on top is the least visited product");
      buffer.append("\n and the product at the bottom is the most frequented product ");
      buffer.append("\n");
      Iterator<String> iterator = browsingHistory.iterator();
      int i = 1;
      while (iterator.hasNext()) {
        buffer.append("[" + i + "] " + iterator.next() + "\n");
        i++;
      }
      System.out.println(this.getClass().getSimpleName() + ":  " + "Printing the result for execute function");
      System.out.println("Result = " + buffer.toString());
      return buffer.toString();
    } else {
      return "history is not available";
    }
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=stats&args=sessionid=26913441

描述:此命令将给出用户的浏览历史。结果将根据用户重新访问特定产品的频率列出。需要关注 URL 中的两个部分,一个是command,等于stats,另一个是参数部分,即args。参数包含用户的会话 ID。以下图表示了具有会话 ID 26913441 的用户的浏览历史:

UserApp

查看用户的浏览历史的屏幕截图

  • MyPurchaseHistoryCommand:这将实现mypurchasehistory命令。命令的实现如下:
package org.learningRedis.web.sessionmgmt.commands;
import java.util.List;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.AnalyticsDBManager;
import org.learningRedis.web.util.Argument;
public class MyPurchaseHistoryCommand extends Commands {
  public MyPurchaseHistoryCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    StringBuffer report = new StringBuffer();
    String sessionid = this.getArgument().getValue("sessionid");
    List<String> purchasehistory = AnalyticsDBManager.singleton.getMyPurchaseHistory(sessionid);
    report.append("Your purchase history is as follows : \n");
    int i = 0;
    for (String purchase : purchasehistory) {
      report.append("[" + i + "] You purchased " + purchase);
      report.append("\n");
      i++;
    }
    return report.toString();
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=mypurchasehistory&args=sessionid=26913441

描述:此命令将给出用户的购买历史。结果将根据用户购买特定产品的日期列出。需要关注 URL 中的两个部分,一个是command,等于stats,另一个是参数部分,即args。参数是用户的会话 ID:

UserApp

查看用户的购买历史的屏幕截图

  • ReloginCommand:这将实现relogin命令。命令的实现如下:
package org.learningRedis.web.sessionmgmt.commands;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.UserDBManager;
public class ReloginCommand extends Commands {
  public ReloginCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    String name = this.getArgument().getValue("name");
    String password = this.getArgument().getValue("password");
    if (UserDBManager.singleton.doesUserExist(name)) {
      if (UserDBManager.singleton.getUserPassword(name).equals(password)) {
        String sessionID = UserDBManager.singleton.getUserSessionId(name);
        return "ReLogin successful \n" + name + " \n use the following session id : " + sessionID;
      } else {
        return " ReLogin failed ...invalid password ";
      }
    } else {
      return " please register before executing command for login ";
    }
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=relogin&args=name=vinoo:password=******

描述:此命令将再次检查用户和用户的密码,并返回用户关联的会话 ID。想要有一个会话,可以存在用户的许多购物和浏览会话。

  • LogoutCommand:这将实现logout命令。命令的实现如下:
package org.learningRedis.web.sessionmgmt.commands;
import org.learningRedis.web.Commands;
import org.learningRedis.web.util.Argument;
import org.learningRedis.web.util.UserDBManager;
public class LogoutCommand extends Commands {
  public LogoutCommand(Argument argument) {
    super(argument);
  }
  @Override
  public String execute() {
    System.out.println(this.getClass().getSimpleName() + ":  " + " Entering the execute function");
    String sessionid = this.getArgument().getValue("sessionid");
    if (UserDBManager.singleton.expireSession(sessionid)) {
      return "logout was clean";
    } else {
      return "logout was not clean";
    }
  }
}

测试 URL:http://localhost:8080/simple-ecom/userApp?command=logout&args=sessionid=26913441

描述:此命令将登出用户系统,并根据会话 ID 删除用户的所有数据存储,如购买历史记录、购物车和浏览历史记录。

现在我们已经掌握了命令,让我们来看看这个包,它将负责管理连接和其他与 Redis 的功能调用。

RedisDBManager

这个类是这个应用程序的支撑,它负责与数据库连接和管理连接池。它还有一些实用功能。实现如下代码片段所示:

package org.learningRedis.web.util;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import Redis.clients.jedis.Jedis;
import Redis.clients.jedis.JedisPool;
public class RedisDBManager {
  private static Date date = new Date();
  private static int minimum = 1;
  private static int maximum = 100000000;
  // going with the default pool.
  private static JedisPool connectionPool = new JedisPool("localhost", 6379);
  public Jedis getConnection() {
    return connectionPool.getResource();
  }
  public void returnConnection(Jedis jedis) {
    connectionPool.returnResource(jedis);
  }
  public static String getDate() {
    DateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy");
    String dateValue = dateFormat.format(date);
    return dateValue;
  }
  public static String getRandomSessionID() {
    int randomNum = minimum + (int) (Math.random() * maximum);
    return new Integer(randomNum).toString();
  }
}

ProductDBManager

这个类扩展了RedisDBManager,负责向数据库发出与产品相关的功能调用。该类的实现如下:

package org.learningRedis.web.util;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import Redis.clients.jedis.Jedis;
public class ProductDBManager extends RedisDBManager {
  private ProductDBManager() {
  }
  public static ProductDBManager singleton = new ProductDBManager();
  public boolean commisionProduct(Map<String, String> productAttributes) {
    Jedis jedis = this.getConnection();
    String productCreationResult = jedis.hmset(productAttributes.get("name"), productAttributes);
    if (productCreationResult.toLowerCase().equals("ok")) {
      this.returnConnection(jedis);
      return true;
    } else {
      this.returnConnection(jedis);
      return false;
    }
  }
  public boolean enterTagEntries(String name, String string) {
    Jedis jedis = this.getConnection();
    String[] tags = string.split(",");
    boolean boolResult = false;
    List<String> tagList = new ArrayList<String>();
    for (String tag : tags) {
      String[] tagAndRating = tag.split("@");
      tagList.add(tagAndRating[0]);
    }
    for (String tag : tagList) {
      long result = jedis.zadd(tag.toLowerCase(), 0, name);
      if (result == 0) {
        break;
      } else {
        boolResult = true;
      }
    }
    this.returnConnection(jedis);
    return boolResult;
  }
  public String getProductInfo(String name) {
    Jedis jedis = this.getConnection();
    Map<String, String> map = jedis.hgetAll(name);
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("Following are the product attributes for  " + name);
    stringBuffer.append("\n");
    Set<String> keys = map.keySet();
    int i = 1;
    for (String key : keys) {
      stringBuffer.append("[" + i + "] . " + key + " value : " + map.get(key));
      stringBuffer.append("\n");
      i++;
    }
    this.returnConnection(jedis);
    return stringBuffer.toString();
  }
  public String getTagValues(String tagName) {
    Jedis jedis = this.getConnection();
    StringBuffer stringBuffer = new StringBuffer();
    Set<String> sortedTagList = jedis.zrange(tagName.toLowerCase(), 0, 10000);
    stringBuffer.append("The following products are listed as per the hit rate \n");
    int i = 1;
    for (String tagname : sortedTagList) {
      stringBuffer.append(" [" + i + "] " + tagname + "\n");
      i++;
    }
    this.returnConnection(jedis);
    return stringBuffer.toString();
  }
  public boolean keyExist(String keyName) {
    Jedis jedis = this.getConnection();
    boolean result = jedis.exists(keyName);
    this.returnConnection(jedis);
    return result;
  }
  public int getPurchaseToday(String productName) {
    Jedis jedis = this.getConnection();
    if (jedis.get(productName + "@purchase:" + getDate()) != null) {
      BitSet users = BitSet.valueOf(jedis.get(productName + "@purchase:" + getDate()).getBytes());
      this.returnConnection(jedis);
      return users.cardinality();
    } else {
      this.returnConnection(jedis);
      return 0;
    }
  }
  public Map<String, Integer> getProductTags(String productname) {
    Jedis jedis = this.getConnection();
    String producttags = jedis.hget(productname, "tags");
    Map<String, Integer> map = new HashMap<String, Integer>();
    String[] tagAndweights = producttags.split(",");
    for (String tagAndWeight : tagAndweights) {
      map.put(tagAndWeight.split("@")[0], new Integer(tagAndWeight.split("@")[1]));
    }
    this.returnConnection(jedis);
    return map;
  }
}

AnalyticsDBManager

这个类扩展了RedisDBManager,负责向数据库发出与分析相关的功能调用。该类的实现如下:

package org.learningRedis.web.util;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import Redis.clients.jedis.Jedis;
public class AnalyticsDBManager extends RedisDBManager {
  private AnalyticsDBManager() {
  }
  public static AnalyticsDBManager singleton = new AnalyticsDBManager();
  public void registerInSessionTracker(String sessionID) {
    Jedis jedis = this.getConnection();
    Long sessionvalue = new Long(sessionID);
    jedis.setbit("sessionIdTracker", sessionvalue, true);
    this.returnConnection(jedis);
  }
  public void updateBrowsingHistory(String sessionID, String productname) {
    Jedis jedis = this.getConnection();
    jedis.zincrby(sessionID + "@browsinghistory", 1.0, productname);
    this.returnConnection(jedis);
  }
  public Set<String> getBrowsingHistory(String sessionID) {
    Jedis jedis = this.getConnection();
    Set<String> range = jedis.zrange(sessionID + "@browsinghistory", 0, 1000000);
    this.returnConnection(jedis);
    return range;
  }
  public int getVisitToday(String productName) {
    Jedis jedis = this.getConnection();
    if (jedis.get(productName + "@visit:" + getDate()) != null) {
      BitSet users = BitSet.valueOf(jedis.get(productName + "@visit:" + getDate()).getBytes());
      this.returnConnection(jedis);
      return users.cardinality();
    } else {
      this.returnConnection(jedis);
      return 0;
    }
  }
  public void updateProductVisit(String sessionid, String productName) {
    Jedis jedis = this.getConnection();
    jedis.setbit(productName + "@visit:" + getDate(), new Long(sessionid), true);
    this.returnConnection(jedis);
  }
  public void updateProductPurchase(String sessionid, String productName) {
    Jedis jedis = this.getConnection();
    jedis.setbit(productName + "@purchase:" + getDate(), new Long(sessionid), true);
    this.returnConnection(jedis);
  }
  public void updateRatingInTag(String productname, double rating) {
    Jedis jedis = this.getConnection();
    String string = jedis.hget(productname, "tags");
    String[] tags = string.split(",");
    List<String> tagList = new ArrayList<String>();
    for (String tag : tags) {
      String[] tagAndRating = tag.split("@");
      tagList.add(tagAndRating[0]);
    }
    for (String tag : tagList) {
      jedis.zincrby(tag.toLowerCase(), rating, productname);
    }
    this.returnConnection(jedis);
  }
  public List<String> getMyPurchaseHistory(String sessionid) {
    Jedis jedis = this.getConnection();
    String name = jedis.hget(sessionid + "@sessiondata", "name");
    List<String> purchaseHistory = jedis.lrange(name + "@purchasehistory", 0, 100);
    this.returnConnection(jedis);
    return purchaseHistory;
  }
  public String getTagHistory(String tagname) {
    Jedis jedis = this.getConnection();
    Set<String> sortedProductList = jedis.zrange(tagname.toLowerCase(), 0, 10000);
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("The following products are listed as per the hit rate \n");
    int i = 1;
    for (String productname : sortedProductList) {
      stringBuffer.append(" [" + i + "] " + productname + " and the score is "
          + jedis.zscore(tagname.toLowerCase(), productname) + "\n");
      i++;
    }
    this.returnConnection(jedis);
    return stringBuffer.toString();
  }
  public List<String> getTopProducts(int slotfortag, String tag) {
    Jedis jedis = this.getConnection();
    Set<String> sortedProductList = jedis.zrevrange(tag.toLowerCase(), 0, 100000000);
    List<String> topproducts = new ArrayList<String>();
    Iterator<String> iterator = sortedProductList.iterator();
    int index = 0;
    while (iterator.hasNext()) {
      if (index <= slotfortag) {
        topproducts.add(iterator.next());
        index++;
      } else {
        break;
      }
    }
    this.returnConnection(jedis);
    return topproducts;
  }
}

ShoppingCartDBManager

这个类扩展了RedisDBManager,负责向数据库发出与购物车相关的功能调用。实现如下:

package org.learningRedis.web.util;
import java.util.Map;
import java.util.Set;
import Redis.clients.jedis.Jedis;
public class ShoppingCartDBManager extends RedisDBManager {
  private ShoppingCartDBManager() {
  }
  public static ShoppingCartDBManager singleton = new ShoppingCartDBManager();
  public String addToShoppingCart(String sessionid, Map<String, String> productQtyMap) {
    Jedis jedis = this.getConnection();
    String result = jedis.hmset(sessionid + "@shoppingcart", productQtyMap);
    this.returnConnection(jedis);
    return result;
  }
  public Map<String, String> myCartInfo(String sessionid) {
    Jedis jedis = this.getConnection();
    Map<String, String> shoppingcart = jedis.hgetAll(sessionid + "@shoppingcart");
    this.returnConnection(jedis);
    return shoppingcart;
  }
  public String editMyCart(String sessionID, Map<String, String> productQtyMap) {
    Jedis jedis = this.getConnection();
    String result = "";
    if (jedis.exists(sessionID + "@shoppingcart")) {
      Set<String> keySet = productQtyMap.keySet();
      for (String key : keySet) {
        if (jedis.hexists(sessionID + "@shoppingcart", key)) {
          Integer intValue = new Integer(productQtyMap.get(key)).intValue();
          if (intValue == 0) {
            jedis.hdel(sessionID + "@shoppingcart", key);
          } else if (intValue > 0) {
            jedis.hset(sessionID + "@shoppingcart", key, productQtyMap.get(key));
          }
        }
      }
      result = "Updated the shopping cart for user";
    } else {
      result = "Could not update the shopping cart for the user !! ";
    }
    this.returnConnection(jedis);
    return result;
  }
  public String buyItemsInTheShoppingCart(String sessionid) {
    Jedis jedis = this.getConnection();
    Map<String, String> cartInfo = jedis.hgetAll(sessionid + "@shoppingcart");
    Set<String> procductNameList = cartInfo.keySet();
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("RECEIPT: You have purchased the following \n");
    stringBuffer.append("-----------------------------------" + "\n");
    int i = 1;
    for (String productname : procductNameList) {
      String unitCost = jedis.hget(productname, "cost");
      int unitCostValue = new Integer(unitCost).intValue();
      String quantity = cartInfo.get(productname);
      int quantityValue = new Integer(quantity).intValue();
      stringBuffer.append("[" + i + "] Name of item : " + productname + " and quantity was : " + quantity
          + " the total cost is = " + quantityValue * unitCostValue + "\n");
      i++;
    }
    stringBuffer.append("-----------------------------------------");
    stringBuffer.append("#");
    for (String productname : procductNameList) {
      stringBuffer.append(productname);
      stringBuffer.append(",");
    }
    // Update the user purchase history:
    String name = jedis.hget(sessionid + "@sessiondata", "name");
    for (String productname : procductNameList) {
      jedis.lpush(name + "@purchasehistory", productname + " on " + getDate());
    }
    this.returnConnection(jedis);
    return stringBuffer.toString();
  }
}

UserCartDBManager

这个类扩展了RedisDBManager,负责向数据库发出与用户相关的功能调用。实现如下:

package org.learningRedis.web.util;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import Redis.clients.jedis.Jedis;
public class UserDBManager extends RedisDBManager {
  private UserDBManager() {
  }
  public static UserDBManager singleton = new UserDBManager();
  public String getUserName(String sessionID) {
    Jedis jedis = this.getConnection();
    String name = jedis.hget(sessionID + "@sessiondata", "name");
    this.returnConnection(jedis);
    return name;
  }
  public void createUser(Map<String, String> attriuteMap) {
    Jedis jedis = this.getConnection();
    Map<String, String> map = attriuteMap;
    map.put("creation-time", new Date().toString());
    map.put("sessionID", "null");
    jedis.hmset(attriuteMap.get("name") + "@userdata", map);
    this.returnConnection(jedis);
  }
  public Map<String, String> getRegistrationMap(String name) {
    Jedis jedis = this.getConnection();
    Map<String, String> attributeMap = new HashMap<String, String>();
    attributeMap = jedis.hgetAll(name + "@userdata");
    this.returnConnection(jedis);
    return attributeMap;
  }
  public boolean doesUserExist(String name) {
    Jedis jedis = this.getConnection();
    String value = jedis.hget(name + "@userdata", "name");
    this.returnConnection(jedis);
    if (value == null) {
      return false;
    } else if (value != null & value.equals(name)) {
      return true;
    } else {
      return false;
    }
  }
  public void setRegistrationMap(String name, Map<String, String> attributeMap) {
    Jedis jedis = this.getConnection();
    jedis.hmset(name + "@userdata", attributeMap);
    this.returnConnection(jedis);
  }
  public String getUserPassword(String name) {
    Jedis jedis = this.getConnection();
    String password = jedis.hget(name + "@userdata", "password");
    this.returnConnection(jedis);
    return password;
  }
  public void login(String sessionID, String name) {
    Jedis jedis = this.getConnection();
    Map<String, String> loginMap = new HashMap<String, String>();
    loginMap.put("LastLogin", new Date().toString());
    loginMap.put("loginstatus", "LoggedIn");
    loginMap.put("sessionID", sessionID);
    loginMap.put("name", name);
    jedis.hmset(sessionID + "@sessiondata", loginMap);
    this.returnConnection(jedis);
  }
  public boolean editRegistrationMap(Map<String, String> editMap) {
    Jedis jedis = this.getConnection();
    if (jedis.hget(editMap.get("name") + "@userdata", "sessionID").equals(editMap.get("sessionid"))) {
      jedis.hmset(editMap.get("name") + "@userdata", editMap);
      this.returnConnection(jedis);
      return true;
    } else {
      this.returnConnection(jedis);
      return false;
    }
  }
  public String getUserSessionId(String name) {
    Jedis jedis = this.getConnection();
    String sessionID = jedis.hget(name + "@userdata", "sessionID");
    this.returnConnection(jedis);
    return sessionID;
  }
  public boolean expireSession(String sessionid) {
    // Get name from session data structure
    Jedis jedis = this.getConnection();
    String name = jedis.hget(sessionid + "@sessiondata", "name");
    // remove session id from userdata
    if (name != null) {
      Long sessionvalue = new Long(jedis.hget(name + "@userdata", "sessionID"));
      jedis.hset(name + "@userdata", "sessionID", "null");
      // remove session data : use TTL
      if (jedis.exists(sessionid + "@sessiondata")) {
        jedis.expire(sessionid + "@sessiondata", 1);
      }
      // remove browsing history : use TTL
      if (jedis.exists(sessionid + "@browsinghistory")) {
        jedis.expire(sessionid + "@browsinghistory", 1);
      }
      // remove shopping cart : use TTL
      if (jedis.exists(sessionid + "@shoppingcart")) {
        jedis.expire(sessionid + "@shoppingcart", 1);
      }
      // make the value at offset as '0'
      jedis.setbit("sessionIdTracker", sessionvalue, false);
      this.returnConnection(jedis);
      return true;
    } else {
      this.returnConnection(jedis);
      return false;
    }
  }
  public boolean doesSessionExist(String sessionid) {
    Jedis jedis = this.getConnection();
    if (jedis.hexists(sessionid + "@sessiondata", "name")) {
      this.returnConnection(jedis);
      return true;
    } else {
      this.returnConnection(jedis);
      return false;
    }
  }
}

总结

因此,在本章中,我们学习了如何使用 Redis 作为其支撑构建一个简单的电子商务网站。此外,我们还学习了 Redis 如何在进行在线分析时变得方便。这个示例网站缺乏我们在之前章节中学到的可扩展性功能。我建议读者将这种能力添加到这个代码库中作为一种练习,并且享受这个令人敬畏的数据存储。

在下一章中,我将透露如何在业务应用程序中使用 Redis,并制作一些在所有业务应用程序中常用的应用程序。

第七章:Redis 在商业应用程序中

在第六章中,Redis 在 Web 应用程序中,你看到了 Redis 在 Web 应用程序中的用处。Redis 的这种用处可以扩展到商业应用程序。与任何企业一样,外层或边界应用程序通常由 Web 应用程序组成,这在某种程度上封装了核心异构的业务应用程序。这些业务应用程序构成了企业的核心骨干。

Redis 在商业应用程序中

企业生态系统中应用程序的简单表示

正如你们在多年的项目和任务中所经历的那样,这些商业应用程序在业务功能上是多种多样的。然而,它们都有一些共同的特征和方面。在本章中,我们将介绍其中一些特征,并看看 Redis 如何适应商业应用程序的环境。首先,任何应用程序中最常见和最基本的特性就是配置管理

随后的主题考虑了配置管理,并将 Redis 作为构建企业级应用程序的核心组件之一。

配置管理

经常会看到不当的配置管理或缺乏配置管理在开发和维护周期的后期造成问题。另一个问题是当可伸缩性成为问题时,添加更多的软件节点;然后,跨所有节点维护状态就成为一个挑战。商业应用程序一直依赖于 RDBMS 来存储配置数据。这种方法的问题在于性能;如果设计是基于 PULL的,那么PULL-based 设计的问题就是性能惩罚。另一个问题是如果并发性高(因为其他业务功能),那么这些 RDBMS 也必须满足这些请求以及配置数据的请求。

配置管理

基于 PULL 的配置管理设计

这个想法是将设计从基于 PULL改为基于 PUSH。这种技术的最大优势是性能。状态或配置数据保持接近应用程序,每当发生变化时,数据就会被推送到应用程序的本地缓存中。另一个要求是要有一个在计算资源占用方面较低的系统。

配置管理

基于 PUSH 的配置管理设计

Redis 凭借其类似瑞士军刀的功能、低资源占用、各种语言的客户端库的可用性以及大规模扩展的能力,使其成为处理这一需求的良好选择。我们将在随后的主题中讨论的示例应用程序将突出这一点。这个示例应用程序只是用于演示,并不保证在生产环境中使用。所以,让我们开心地开发一个以 Redis 为支撑的配置管理服务器,并称之为gossip server

Gossip server

gossip server 是一个集中管理配置数据并以同步方式分组服务的节点。Gossip Server将保存数据,并由一个名为Gossip Server (Admin)的节点管理。Gossip Server反过来将管理连接到它的所有其他节点。以下图表描述了 gossip server 的责任是将配置数据推送到连接到它的所有节点:

Gossip server

Gossip Server 设计概述

在这个八卦服务器内部是 Redis 服务器,它提供了所提议的配置管理系统可能需要的所有功能。节点可以用任何编程语言实现,但是为了与本书中的示例保持一致,我们将在这个示例中使用 Java 作为实现语言。这个八卦服务器的主要思想是在下次需要设计企业级解决方案时,为配置管理保留一个共同的组件,并在这样做时牢记 Redis。

在我们进入我们共同组件的实施和设计规范之前,让我们就这个八卦服务器的功能达成一致。

以下是八卦服务器的功能:

  • 八卦服务器维护所有信息或配置数据

  • 它充当中心枢并将信息或配置数据分发给所有连接的节点

  • 所有节点,包括主节点,都连接到中心枢以发送消息

  • 主节点负责向特定客户端节点或所有客户端节点推送数据

  • 所有客户端节点在层次结构中处于相同位置

  • 所有客户端节点可以嵌入到要成为配置管理一部分的解决方案中

  • 节点有一个生命周期,它们由自己管理

  • 当节点改变状态时,它们会通知主节点和其他对等客户端节点

  • 节点也可以根据业务逻辑向其他对等节点发送消息

节点

八卦服务器中的节点是所有消息流动的客户端组件。在当前示例中,节点可以分为两种类型,客户端节点和主节点。

客户端节点本质上是可以插入任何需要配置管理的解决方案中的组件。客户端节点负责它们在 Redis 中存储的应用程序数据。节点中的数据可以来自它们所插入的应用程序,也可以来自主节点,主节点可以将数据推送到客户端节点。允许主节点推送数据或者发布数据到八卦服务器的整个想法是将应用程序的配置数据管理责任从应用程序本身转移到另一个源头。这样做的好处是可以在运行时将新的配置数据引入应用程序,而无需停止应用程序。

以下图表是八卦服务器的配置数据推送能力的表示:

节点

通过应用程序或主节点将数据推送到八卦服务器

在我们进一步实施之前,最好了解客户端节点在其生命周期中可以遍历的各种状态。以下图表是客户端节点可以采取的各种路径的快照:

节点

通过应用程序或主节点将数据推送到八卦服务器

客户端节点从注册开始其旅程。在注册之后,客户端节点需要激活自己。一旦客户端节点被激活,它可以停用自己或者达到归档状态。归档状态可以通过关闭应用程序或者主节点发送Kill命令来实现。一旦客户端节点处于停用状态,它可以通过中间状态重新激活来激活自己。如果客户端节点处于归档状态,它可以通过中间状态重新连接来转换为激活状态。

客户端节点的命令围绕着上述状态进行建模,并且还有其他用于数据管理和在生态系统中传递数据的命令。不浪费时间,让我们深入了解系统的设计。

分层设计

八卦服务器的设计是极简主义的,非常简单易懂,但有一些需要考虑的事项。正如讨论的那样,参与八卦服务器的节点有两种类型:客户端节点和主节点。每个客户端节点对自己的生命周期负责,主节点对其有有限的控制。节点可以通过传递消息与彼此通信。设计包括四个主要层,如下图所示:

分层设计

八卦服务器结构层概述

八卦服务器中的包对应于前面图表中描述的层,并包括一些额外的内容。让我们简要介绍一下这些包和它们包含的类。以下是包和它们对应的层的列表:

  • org.redisch7.gossipserver.shell: 这对应于Shell 层

  • org.redisch7.gossipserver.commands: 这对应于命令层

  • org.redisch7.gossipserver.commandhandlers: 这对应于命令处理层

  • org.redisch7.gossipserver.datahandler: 这对应于数据处理层

  • org.redisch7.gossipserver.util.commandparser: 这是一个实用程序包

Shell

Shell 是一个程序,它像一个独立的网关一样作用于八卦服务器,同时也是一个应用程序的插件,该应用程序想要使用八卦服务器。Shell 激活节点,节点又为节点准备监听器和命令库。正如讨论的那样,有两种类型的节点:客户端节点和主节点;这些节点的详细讨论在本章的后半部分进行。

Shell

与 shell 的交互

八卦服务器的代码很简单,基本上是将命令委托给节点进行处理。在 Shell 作为独立程序的情况下,响应显示在命令提示符中,而在 Shell 作为 API 插件的情况下,结果对象CheckResult被传递回调用它的程序。Shell 被实现为单例。这是Shell.java的代码:

package org.redisch7.gossipserver.shell;
/** omitting the import statements**/
public class Shell {
  private Shell() {}
  private Node      node    = null;
  private static Shell  singleton  = new Shell();
  public static Shell instance() {
    return singleton;
  }
  // : as an shell API mode.
  public Shell asClient(String nodename) {
    if (node != null && nodename != null && nodename.trim().length() != 0) {
      node = new ClientNode(nodename);
      return this;
    } else {
      return null;
    }
  }
  public Shell asMaster() {
    if (node != null) {
      node = new MasterNode();
      return this;
    } else {
      return null;
    }
  }
  public CheckResult execute(String commands) {
    CheckResult checkResult = new CheckResult();
    if (commands != null && commands.trim().length() == 0) {
      checkResult = node.process(commands);
    }
    return checkResult;
  }
  // : as a shell standalone mode.
  public static void main(String[] args) throws IOException {
    Shell shell = Shell.instance();
    shell.startInteracting();
  }
  private void startInteracting() throws IOException {
    System.out.println("Please enter the name of the node..");
    BufferedReader nodenameReader = new BufferedReader(new InputStreamReader(System.in));
    String nodename = nodenameReader.readLine();
    if (nodename.equals("master")) {
      node = new MasterNode();
    } else {
      node = new ClientNode(nodename);
    }
    while (true) {
      BufferedReader commandReader = new BufferedReader(new InputStreamReader(System.in));
      String readline = commandReader.readLine();
      if (readline == null) {
        System.out.println("Ctrl + C ");
        break;
      } else {
        CheckResult checkResult = node.process(readline);
        System.out.println(":->" + checkResult.getResult());
        System.out.println(":->" + checkResult.getReason());
        System.out.println(":->" + checkResult.getValue());
      }
    }
    System.exit(0);
  }
}

监听器

监听器由节点生成并独立于执行 Shell 的线程执行。监听器的基本工作是不断监听传递给节点的任何消息事件。然后解析并相应执行消息。基本思想是为节点提供相互交互的机制。在当前的实现中,是主节点与客户端节点进行交互。这提供了主节点对客户端节点的有限远程控制。另一种通信方式的实现尚未完成,如果需要的话可以很容易地加入,即客户端节点与主节点进行交互。并非所有命令都可以通过这种安排在客户端节点上远程执行。可以远程执行(由主节点)的命令有SETKILLCLONE

监听器

节点、消息监听器管理器、消息监听器和订阅者之间的关系

监听器内部有一个订阅者,它扩展了JedisPubSub抽象类,这是 Jedis 客户端库对 Redis 消息传递能力的钩子。节点维护着监听器的生命周期。节点在一些命令上激活监听器,比如激活重新连接等,而在一些命令上停用监听器,比如停用KILL等。

这是客户端监听器的代码,即ClientEventMessageListener.Java:

package org.redisch7.gossipserver.shell;
/** omitting the import statements **/
public class ClientEventMessageListener implements Runnable {
  private Subscriber subscriber = null;
  private Node node;
  private Jedis jedis = ConnectionManager.get();
  private Validator validator = null;
  public ClientEventMessageListener(Node node) {
    this.node = node;
    this.subscriber = new Subscriber(node);
  }
  @Override
  public void run() {
    while (!Thread.currentThread().isInterrupted()) {
      jedis.subscribe(subscriber, node.getNodename());
    }
  }
  public void unsubscribe() {
    subscriber.unsubscribe(node.getNodename());
  }
  public class Subscriber extends JedisPubSub {
    public Subscriber(Node clientNode) {
    }
    @Override
    public void onMessage(String nodename, String readmessage) {
      validator = new Validator();
      validator.configureTemplate().add(new MapListToken());
      validator.setInput(readmessage);
      CheckResult checkResult = validator.validate();
      if (checkResult.getResult()) {
        MapListToken mapListToken = (MapListToken) validator
            .getToken(0);
        if (mapListToken.containsKey("command")) {
          String commandValue = mapListToken.getNValue("command");
          if (commandValue.equals("set")) {
            MapListToken newMapListToken = mapListToken
                .removeElement("command");
            SetCommand command = new SetCommand();
            command.setName(node.getNodename());
            CheckResult result = command.execute(new CommandTokens(
                "set "
                    + newMapListToken
                        .getValueAsSantizedString()));
            System.out.println(result.getResult());
            System.out.println(result.getReason());
          } else if (commandValue.equals("kill")) {
            KillNodeCommand command = new KillNodeCommand();
            command.setName(node.getNodename());
            MapListToken newMapListToken = mapListToken
                .removeElement("command");
            CheckResult result = command.execute(new CommandTokens(
                "kill " + node.getNodename()));
            System.out.println(result.getResult());
            System.out.println(result.getReason());
          } else if (commandValue.equals("clone")) {
            CloneNodeCommand command = new CloneNodeCommand();
            command.setName(node.getNodename());
            MapListToken newMapListToken = mapListToken
                .removeElement("command");
            CheckResult result = command.execute(new CommandTokens(
                "clone "
                    + newMapListToken
                        .getValueAsSantizedString()));
            System.out.println(result.getResult());
            System.out.println(result.getReason());
          } else {
            MessageCommand messageCommand = new MessageCommand();
            messageCommand.setName(nodename);
            CommandTokens commandTokens = new CommandTokens(
                "msg master where msg=illegal_command");
            messageCommand.execute(commandTokens);
          }
        } else {
          System.out
              .println(":->"
                  + checkResult
                      .appendReason("The command sent from publisher does not contain 'command' token"));
        }
      } else {
        System.out.println(":->" + checkResult.getReason());
      }
    }
    @Override
    public void onPMessage(String arg0, String arg1, String arg2) {
      System.out.println(arg1);
      System.out.println(arg2);
    }
    @Override
    public void onPSubscribe(String arg0, int arg1) {
    }
    @Override
    public void onPUnsubscribe(String arg0, int arg1) {
    }
    @Override
    public void onSubscribe(String arg0, int arg1) {
    }
    @Override
    public void onUnsubscribe(String arg0, int arg1) {
    }
  }
}

这是主监听器的代码,即MasterEventMessageListener.java

package org.redisch7.gossipserver.shell;
/** omitting the import statements **/
public class MasterEventMessageListener implements Runnable {
  private Subscriber  subscriber  = null;
  private Node    node;
  private Jedis    jedis    = ConnectionManager.get();
  private Validator  validator  = new Validator();
  public MasterEventMessageListener(Node node) {
    this.node = node;
    this.subscriber = new Subscriber(node);
    validator.configureTemplate().add(new MapListToken());
  }
  @Override
  public void run() {
    while (!Thread.currentThread().isInterrupted()) {
      jedis.subscribe(subscriber, node.getNodename());
    }
  }
  public void unsubscribe() {
    subscriber.unsubscribe(node.getNodename());
  }
  public class Subscriber extends JedisPubSub {
    public Subscriber(Node node) {
    }
    @Override
    public void onMessage(String nodename, String readmessage) {
      System.out.println("msg: " + readmessage);
      System.out.println("Not processed further in the current implementation");
    }
    @Override
    public void onPMessage(String arg0, String arg1, String arg2) {
      System.out.println(arg1);
      System.out.println(arg2);
    }
    @Override
    public void onPSubscribe(String arg0, int arg1) {}
    @Override
    public void onPUnsubscribe(String arg0, int arg1) {}
    @Override
    public void onSubscribe(String arg0, int arg1) {}
    @Override
    public void onUnsubscribe(String arg0, int arg1) {}
  }
}

监听器管理器

监听管理器负责维护监听器的生命周期。监听器可以存在于启动模式或停止模式。Gossip 服务器具有面向事件的设计;因此,客户端节点接受的每个事件都有一个相应的命令被执行。

在系统中,有两种类型的监听管理器,一个是针对客户端节点的称为客户端节点监听管理器,另一个是针对主节点的称为主节点监听管理器。

客户端节点监听管理器被编程为在“激活”、“重新激活”和“重新连接”等命令上启动监听器,并在“停用”和“终止”等命令上停止监听器。

主节点监听管理器被编程为在“启动”等命令上启动监听器,并在“停止”等命令上停止监听器。

以下是ClientNodeListenerManager.java的代码:

package org.redisch7.gossipserver.shell;
/** omitting the import statements **/
public class ClientNodeListenerManager implements NodeMessageListenerManager {
  private String            nodename;
  private ClientEventMessageListener  privateEventMessageSubscriber;
  private Thread            commonEventThread;
  private Thread            privateEventThread;
  public ClientNodeListenerManager(ClientNode clientNode) {
    this.nodename = clientNode.getNodename();
    privateEventMessageSubscriber = new ClientEventMessageListener(clientNode);
  }
  @Override
  public void start() {
    System.out.println(" start the client node manager .. ");
    privateEventThread = new Thread(privateEventMessageSubscriber);
    commonEventThread.start();
    privateEventThread.start();
  }
  @Override
  public void stop() {
    System.out.println(" stop the client node manager .. ");
    privateEventMessageSubscriber.unsubscribe();
    commonEventThread.interrupt();
    privateEventThread.interrupt();
  }
  @Override
  public void passCommand(AbstractCommand command) {
    if (command instanceof ActivateCommand || command instanceof ReactivateCommand
        || command instanceof ReConnectCommand) {
      this.start();
    } else if (command instanceof PassivateCommand || command instanceof KillNodeCommand) {
      this.stop();
    }
  }
}

以下是MasterNodeListenerManager.java的代码:

package org.redisch7.gossipserver.shell;
/** omitting the import statements **/
public class MasterNodeListenerManager implements NodeMessageListenerManager {
  private MasterEventMessageListener  masterEventMessageSubscriber;
  private Thread            privateEventThread;
  private MasterNode          masternode;
  public MasterNodeListenerManager(MasterNode masterNode) {
    this.masternode = masterNode;
    masterEventMessageSubscriber = new MasterEventMessageListener(masternode);
  }
  @Override
  public void start() {
    System.out.println(" start the master node manager .. ");
    privateEventThread = new Thread(masterEventMessageSubscriber);
    privateEventThread.start();
  }
  @Override
  public void stop() {
    System.out.println(" stop the master node manager .. ");
    privateEventThread.interrupt();
    masterEventMessageSubscriber.unsubscribe();
  }
  @Override
  public void passCommand(AbstractCommand command) {
    if (command instanceof StartMasterCommand) {
      this.start();
    } else if (command instanceof StopMasterCommand) {
      this.stop();
    }
  }
}

数据处理层

这个层或包在其活动中非常直接,比如与 Redis 服务器交互。这一层负责将 Redis 封装起来,使其与应用程序的其余部分隔离开来。

数据处理层

Gossip 服务器结构层概述

以下是当前应用程序使用的数据结构:

  • 注册持有者:这将作为 Redis 数据存储中的一个集合来实现。这将保存系统中将要注册的所有节点。

  • 激活持有者:这将作为 Redis 数据存储中的一个集合来实现。这将保存所有将处于“激活”状态的节点。

  • 停用持有者:这将作为 Redis 数据存储中的一个集合来实现。这将保存所有将处于停用状态的节点。

  • 配置存储:这将作为 Redis 数据存储中的一个映射来实现。这将保存所有与节点相关的配置数据,以名称-值格式进行存储。

  • 存档存储:这将作为客户端节点本地文件系统中的文件存储来实现。这将保存所有与节点相关的配置数据,以名称-值格式进行存档,并以 JSON 格式存档。

这一层中最重要的类是JedisUtilImpl;让我们花一些时间来了解这个类。这个类的性质使得这个类非常庞大但易于理解。

JedisUtil.java

这个类在与数据存储进行交互时起着关键作用。所有关于管理节点帐户、状态和数据的逻辑都在这里管理。

注意

请注意,我们选择使用jedis_2.1.0作为客户端 API 来连接 Redis。在这个客户端库的这个版本中,使用PIPELINE函数中的MULTI存在一个相关的 bug。

Exception in thread "main" java.lang.ClassCastException: B cannot be cast to java.util.List at redis.clients.jedis.Connection.getBinaryMultiBulkReply(Connection.java:189)
    at redis.clients.jedis.Jedis.hgetAll(Jedis.java:861)
    at com.work.jedisex.JedisFactory.main(JedisFactory.java:59)

由于 Redis 是单线程服务器,我们牺牲了在这个应用程序中使用PIPELINE中的MULTI,因为这不会对 Redis 中的数据完整性产生影响,并且对性能的影响很小。我们继续以单个命令发送命令,而不是批量发送,就像PIPELINE的情况一样。Jedis 的未来 API 可能会对此有解决方案,如果您使用更新版本的 Jedis,您可以根据需要更改类。

在其他语言中实现客户端或在 Java 中实现 Redis 的其他客户端实现,不会有问题,因为这是特定于 Jedis 的。

现在我们对JedisUtil类有了一定的了解,我们也在某种程度上了解了 gossip 服务器的工作原理以及 gossip 服务器所提供的功能。因此,让我们专注于命令以及它们是如何实现的。作为一个经验法则,数据流可以总结如下图所示:

![JedisUtil.java 命令的数据流顺序# 客户端节点命令以下是可以从客户端节点触发的命令列表:+ “注册”命令+ “激活”命令+ “设置”命令+ 获取命令+ 状态命令+ 删除命令+ 停用命令+ reacyivate命令+ 存档命令+ 同步命令+ 重新连接命令让我们从设计和实现的角度来看每个命令。## 注册命令此命令将注册节点进入八卦服务器生态系统。执行此命令的先决条件是节点名称应该是唯一的;否则,将向Shell发送失败响应。节点名称将存储在 Registration holder 中,该 holder 在 Redis 中实现为 Set 数据结构。除此之外,在注册过程发生时,节点的本地机器上将创建一个存档文件。注册命令

注册命令中数据流的顺序

此命令的语法是:register。以下屏幕截图显示了 Shell 控制台中的响应:

注册命令

RegisterCommand 的实现

RegisterCommand 的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class RegisterCommand extends AbstractCommand {
  private Validator validator = new Validator();
  public RegisterCommand() {
    validator.configureTemplate().add((new StringToken("register")));
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new RegisterCommandHandler(this.getName()).process(tokenList);
    }
    if(checkResult.getResult()){
      String path = System.getProperty("user.home") + "\\archive\\";
      File file = new File(path);
      if (!file.exists()) {
        if (file.mkdir()) {
          checkResult.appendReason("Archive folder created!");
        } else {
          checkResult.appendReason("Archive folder exists!");
        }
      }
    }
    return checkResult;
  }
}

RegisterCommandHandler 的实现

RegisterCommandHandler 的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class RegisterCommandHandler extends AbstractCommandHandler {
  public RegisterCommandHandler(String nodename) {
    super(nodename);
  }
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> result = jedisUtil
        .doesExist(this.getNodename(), Arrays
            .asList(ConstUtil.registerationHolder,
                ConstUtil.activationHolder,
                ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    if ((result.get(0) == false) && (result.get(1) == false)
        && (result.get(2) == false)&& (result.get(3) == false)) {
      checkResult = jedisUtil.registerNode(this.getNodename());
    } else {
      checkResult
          .setFalse("Activation Validation :")
          .appendReason(
              ConstUtil.registerationHolder + " = "
                  + ((Boolean) result.get(0)))
          .appendReason(
              ConstUtil.activationHolder + " = "
                  + ((Boolean) result.get(1)))
          .appendReason(
              ConstUtil.passivationHolder + " = "
                  + ((Boolean) result.get(2)));
    }
    return checkResult;
  }
}

激活命令

此命令将激活节点进入八卦服务器生态系统。执行此命令的先决条件是节点应该已注册。激活节点时,将向 ACTIVATION-HOLDER 添加一个条目,该条目在 Redis 中实现为 Set。除此之外,在激活时,客户端节点将生成监听器,这些监听器将准备好监听来自主节点的任何事件。这些监听器基本上将在单独的线程上监听事件。

激活命令

激活命令中数据流的顺序

此命令的语法是:activate。以下屏幕截图显示了 Shell 控制台中的响应:

激活命令

ActivateCommand 的实现

ActivateCommand 的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class ActivateCommand extends AbstractCommand {
  private Validator validator = new Validator();
  public ActivateCommand() {
    validator.configureTemplate().add((new StringToken("activate")));
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new ActivateCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

ActivateCommandHandler 的实现

ActivateCommandHandler 的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public final class ActivateCommandHandler extends AbstractCommandHandler {
  public ActivateCommandHandler(String nodename) {
    super(nodename);
  }
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> result = jedisUtil.doesExist(this.getNodename(), Arrays
        .asList(ConstUtil.registerationHolder,
            ConstUtil.activationHolder,
            ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    if ((result.get(0) == true) && (result.get(1) == false)
        && (result.get(2) == false) && (result.get(3) == false)) {
      checkResult = jedisUtil.activateNode(this.getNodename());
    } else {
      checkResult
          .setFalse("Activation Failed :")
          .appendReason(
              ConstUtil.registerationHolder + " = "
                  + ((Boolean) result.get(0)))
          .appendReason(
              ConstUtil.activationHolder + " = "
                  + ((Boolean) result.get(1)))
          .appendReason(
              ConstUtil.passivationHolder + " = "
                  + ((Boolean) result.get(2)))
          .appendReason(
              ConstUtil.shutdownHolder + " = "
                  + ((Boolean) result.get(3)));
    }
    return checkResult;
  }
}

设置命令

此命令将设置节点中的数据。执行此命令的先决条件是节点应该处于激活状态。该命令将插入名称值到节点的Config-store中。Config store在 Redis 中实现为 Hashes 数据结构。显然,可以在Config store中插入多个名称值对。

设置命令

设置命令中数据流的顺序

此命令的语法是:set <name=value>,<name=value>。以下屏幕截图显示了 Shell 控制台中的响应:

设置命令

SetCommand 的实现

SetCommand 的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
import org.redisch7.gossipserver.util.commandparser.Validator;
public class SetCommand extends AbstractCommand {
  Validator validator = new Validator();
  public SetCommand() {
    validator.configureTemplate().add((new StringToken("set"))).add(new MapListToken());
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new SetCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

SetCommandHandler 的实现

设置命令处理程序的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class SetCommandHandler extends AbstractCommandHandler {
  public SetCommandHandler(String nodename) {
    super(nodename);
  }
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> result = jedisUtil
        .doesExist(this.getNodename(), Arrays
            .asList(ConstUtil.registerationHolder,
                ConstUtil.activationHolder,
                ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    if ((result.get(0) == true) && (result.get(1) == true)
        && (result.get(2) == false)&& (result.get(3) == false)) {
      MapListToken mapListToken = (MapListToken) tokenList.get(1);
      checkResult = jedisUtil.setValuesInNode(this.getNodename(),
          mapListToken.getValueAsMap());
    } else {
      checkResult
          .setFalse("Activation Validation :")
          .appendReason(
              ConstUtil.registerationHolder + " = "
                  + ((Boolean) result.get(0)))
          .appendReason(
              ConstUtil.activationHolder + " = "
                  + ((Boolean) result.get(1)))
          .appendReason(
              ConstUtil.passivationHolder + " = "
                  + ((Boolean) result.get(2)));
    }
    return checkResult;
  }
}

获取命令

此命令将从节点获取数据。执行此命令的先决条件是节点应该处于激活状态。输入将是一个变量列表,数据需要从 Config 存储中获取。每个节点都将有自己的 Config 存储。

获取命令

获取命令中数据流的顺序

此命令的语法是:get。以下屏幕截图显示了 Shell 控制台中的响应:

获取命令

GetCommand 的实现

GetCommand 的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class GetCommand extends AbstractCommand {
  Validator validator = new Validator();
  public GetCommand() {
    validator.configureTemplate().add((new StringToken("get"))).add(new StringListToken());
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new GetCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

GetCommandHandler 的实现

获取命令处理程序的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class GetCommandHandler extends AbstractCommandHandler {
  public GetCommandHandler(String nodename) {
    super(nodename);
  }
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> result = jedisUtil
        .doesExist(this.getNodename(), Arrays
            .asList(ConstUtil.registerationHolder,
                ConstUtil.activationHolder,
                ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    if ((result.get(0) == true) && (result.get(1) == true)
        && (result.get(2) == false)&& (result.get(3) == false)) {
      StringListToken stringList = (StringListToken) tokenList.get(1);
      checkResult = jedisUtil.getValuesFromNode(this.getNodename(),
          stringList.getValueAsList());
    } else {
      checkResult
          .setFalse("Activation Validation :")
          .appendReason(
              ConstUtil.registerationHolder + " = "
                  + ((Boolean) result.get(0)))
          .appendReason(
              ConstUtil.activationHolder + " = "
                  + ((Boolean) result.get(1)))
          .appendReason(
              ConstUtil.passivationHolder + " = "
                  + ((Boolean) result.get(2)));
    }
    return checkResult;
  }
}

删除命令

此命令将删除节点中的数据。执行此命令的先决条件是节点应该处于激活状态。通过传递需要删除的变量的名称来执行命令。

删除命令

Delete 命令中数据流的顺序

该命令的语法是:del <parameter>。以下截图显示了 shell 控制台中的响应:

删除命令

DeleteCommand 的实现

DeleteCommand 的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class DeleteCommand extends AbstractCommand {
  Validator validator = new Validator();
       public DeleteCommand() {
    validator.configureTemplate().add((new StringToken("del"))).add(new StringListToken());
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new DeleteCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

DeleteCommandHandler 的实现

delete命令处理程序的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class DeleteCommandHandler extends AbstractCommandHandler {
  public DeleteCommandHandler(String nodename) {
    super(nodename);
  }
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> result = jedisUtil
        .doesExist(this.getNodename(), Arrays
            .asList(ConstUtil.registerationHolder,
                ConstUtil.activationHolder,
                ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    if ((result.get(0) == true) && (result.get(1) == true)
        && (result.get(2) == false)&& (result.get(3) == false)) {
      StringListToken stringList = (StringListToken) tokenList.get(1);
      checkResult = jedisUtil.deleteValuesFromNode(this.getNodename(),
          stringList.getValueAsList());
    } else {
      checkResult
          .setFalse("Activation Validation :")
          .appendReason(
              ConstUtil.registerationHolder + " = "
                  + ((Boolean) result.get(0)))
          .appendReason(
              ConstUtil.activationHolder + " = "
                  + ((Boolean) result.get(1)))
          .appendReason(
              ConstUtil.passivationHolder + " = "
                  + ((Boolean) result.get(2)));
    }
    return checkResult;
  }
}

状态命令

此命令用于获取节点的当前状态。执行此命令的前提条件是节点应处于某种状态。客户端中的命令关注客户端节点的数据。

状态命令

Passivate 命令中数据流的顺序

该命令的语法是:status。以下截图显示了 shell 控制台中的响应:

状态命令

StatusCommand 的实现

status命令的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class StatusCommand extends AbstractCommand {
  Validator validator = new Validator();
  public StatusCommand() {
    validator.configureTemplate().add((new StringToken("status")));
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new StatusCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

StatusCommandHandler 的实现

passive命令处理程序的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class StatusCommandHandler extends AbstractCommandHandler {
  public StatusCommandHandler(String nodename) {
    super(nodename);
  }
  @Override
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtilImpl();
    if (this.getNodename().equals("master")) {
      List<String> registerednames = jedisUtil.getAllNodesFromRegistrationHolder();
      checkResult.setTrue().appendReason("The following nodes are registered ");
      checkResult.appendReason(registerednames.toString());
      List<String> activenodenames = jedisUtil.getAllNodesFromActivatedHolder();
      checkResult.setTrue().appendReason("The following nodes are activated ");
      checkResult.appendReason(activenodenames.toString());
      List<String> passivenodenames = jedisUtil.getAllNodesFromPassivatedHolder();
      checkResult.setTrue().appendReason("The following nodes are passivated ");
      checkResult.appendReason(passivenodenames.toString());
      List<String> inconsistentState = jedisUtil.getAllNodesInInconsistentState();
      checkResult.setTrue().appendReason("The following nodes are not in consitent state ");
      checkResult.appendReason(inconsistentState.toString());
    } else {
      checkResult = jedisUtil.getStatus(this.getNodename());
    }
    return checkResult;
  }
}

passivate 命令

该命令将节点转为 gossip 服务器生态系统中的被动状态。执行此命令的前提条件是节点应处于激活状态。在被动化时,客户端的事件监听器将被关闭,并且将无法接收来自主节点的事件。由于节点被动化,节点的 Config 存储中的数据将被取出并推送到节点的归档文件中。

passivate 命令

Passivate 命令中数据流的顺序

该命令的语法是:passivate。以下截图显示了 shell 控制台中的响应:

passivate 命令

PassivateCommand 的实现

passivate命令的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class PassivateCommand extends AbstractCommand {
  Validator validator = new Validator();
  public PassivateCommand() {
    validator.configureTemplate().add((new StringToken("passivate")));
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new PassivateCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

PassivateCommandHandler 的实现

passivate命令处理程序的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class PassivateCommandHandler extends AbstractCommandHandler {
  public PassivateCommandHandler(String nodename) {
    super(nodename);
  }
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> result = jedisUtil.doesExist(this.getNodename(), Arrays
        .asList(ConstUtil.registerationHolder,
            ConstUtil.activationHolder,
            ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    if ((result.get(0) == true) && (result.get(1) == true)
        && (result.get(2) == false) && (result.get(3) == false)) {
      checkResult = jedisUtil.passivateNode(this.getNodename());
    } else {
      checkResult
          .setFalse("Passivation Validation :")
          .appendReason(
              ConstUtil.registerationHolder + " = "
                  + ((Boolean) result.get(0)))
          .appendReason(
              ConstUtil.activationHolder + " = "
                  + ((Boolean) result.get(1)))
          .appendReason(
              ConstUtil.passivationHolder + " = "
                  + ((Boolean) result.get(2)));
    }
    return checkResult;
  }
}

reactivate 命令

此命令将重新激活节点。执行此命令的前提条件是节点应处于被动模式。重新激活后,客户端的事件监听器将再次启动。归档文件中的数据将再次被泵回节点的 Config 存储中。

reactivate 命令

Reactivate 命令中数据流的顺序

该命令的语法是:reactivate。以下截图显示了 shell 控制台中的响应:

reactivate 命令

ReactivateCommand 的实现

passivate命令的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class ReactivateCommand extends AbstractCommand {
  Validator validator = new Validator();
  public ReactivateCommand() {
    validator.configureTemplate().add((new StringToken("reactivate")));
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new ReactivateCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

ReactivateCommandHandler 的实现

reactivate命令处理程序的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class ReactivateCommandHandler extends AbstractCommandHandler {
  public ReactivateCommandHandler(String nodename) {
    super(nodename);
  }
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> result = jedisUtil.doesExist(this.getNodename(), Arrays
        .asList(ConstUtil.registerationHolder,
            ConstUtil.activationHolder,
            ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    if ((result.get(0) == true) && (result.get(1) == false)
        && (result.get(2) == true) && (result.get(3) == false)) {
      checkResult = jedisUtil.reactivateNode(this.getNodename());
    } else {
      checkResult
          .setFalse("Passivation Validation :")
          .appendReason(
              ConstUtil.registerationHolder + " = "
                  + ((Boolean) result.get(0)))
          .appendReason(
              ConstUtil.activationHolder + " = "
                  + ((Boolean) result.get(1)))
          .appendReason(
              ConstUtil.passivationHolder + " = "
                  + ((Boolean) result.get(2)));
    }
    return checkResult;
  }
}

归档命令

command将对 gossip 服务器生态系统中的节点数据进行归档。执行此命令的前提条件是节点应处于注册模式。当发出此命令时,节点的 Config 存储中的数据将被刷新并放入客户端节点机器的文件系统中的归档文件中。

archive 命令

Archive 命令中数据流的顺序

该命令的语法是:archive。以下截图显示了 shell 控制台中的响应:

archive 命令

ArchiveCommand 的实现

archive命令的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class ArchiveCommand extends AbstractCommand {
  private Validator validator = new Validator();
  public ArchiveCommand() {
    validator.configureTemplate().add((new StringToken("archive")));
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new ArchiveCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

ArchiveCommandHandler 的实现

reactive命令处理程序的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public final class ArchiveCommandHandler extends AbstractCommandHandler {
  public ArchiveCommandHandler(String nodename) {
    super(nodename);
  }
  @Override
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> result = jedisUtil
        .doesExist(this.getNodename(), Arrays
            .asList(ConstUtil.registerationHolder,
                ConstUtil.activationHolder,
                ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    if ((result.get(0) == true)
        &&  (result.get(3) == false) &&((result.get(1) == true) || (result.get(2) == true))) {
      checkResult = jedisUtil.archiveNode(this.getNodename());
    } else {
      checkResult
          .setFalse("Activation Validation :")
          .appendReason(
              ConstUtil.registerationHolder + " = "
                  + (result.get(0)))
          .appendReason(
              ConstUtil.activationHolder + " = "
                  + (result.get(1)))
          .appendReason(
              ConstUtil.passivationHolder + " = "
                  + (result.get(2)));
    }
    return checkResult;
  }
}

同步命令

sync命令将同步 gossip 服务器生态系统中节点的数据。执行此命令的前提条件是节点应处于注册模式。当发出此命令时,归档文件中的数据将被泵回用户的 Config 存储中。

同步命令

同步命令的数据流序列

该命令的语法是:sync。以下截图显示了 shell 控制台中的响应:

同步命令

SyncCommand 的实现

sync命令的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class SynchCommand extends AbstractCommand {
  Validator validator = new Validator();
  public SynchCommand() {
    validator.configureTemplate().add((new StringToken("sync")));
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new SynchCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

SyncCommandHandler 的实现

sync命令处理程序的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class SynchCommandHandler extends AbstractCommandHandler {
  public SynchCommandHandler(String nodename) {
    super(nodename);
  }
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> result = jedisUtil
        .doesExist(this.getNodename(), Arrays
            .asList(ConstUtil.registerationHolder,
                ConstUtil.activationHolder,
                ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    if (result.get(0) && result.get(1) && (result.get(3)==false)) {
      checkResult = jedisUtil.syncNode(this.getNodename());
    } else {
      checkResult.setFalse("Synch Failed ");
    }
    return checkResult;
  }
}

重新连接命令

reconnect命令将重新连接八卦服务器生态系统中的一个节点。执行此命令的前提是节点应处于激活状态,并且节点应经历了关闭。因此,当节点在关闭后重新启动并触发此命令时,客户端节点的监听器将被生成,并且节点将重新处于激活状态。

重新连接命令

重新连接命令的数据流序列

该命令的语法是:reconnect。以下截图显示了 shell 控制台中的响应:

重新连接命令

ReconnectCommand 的实现

reconnect命令的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class ReConnectCommand extends AbstractCommand {
  Validator validator = new Validator();
  public ReConnectCommand() {
    validator.configureTemplate().add((new StringToken("reconnect")));
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new ReConnectCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

ReconnectCommandHandler 的实现

重新连接命令处理程序的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class ReConnectCommandHandler extends AbstractCommandHandler {
  public ReConnectCommandHandler(String nodename) {
    super(nodename);
  }
  @Override
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> result = jedisUtil.doesExist(this.getNodename(), Arrays
        .asList(ConstUtil.registerationHolder,
            ConstUtil.activationHolder,
            ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    if ((result.get(0) == true)
        && ((result.get(1) == false) || (result.get(2) == false))
        && (result.get(3) == true)) {
      checkResult = jedisUtil.reconnectNode(this.getNodename());
    } else {
      checkResult
          .setFalse("Reconnect Failed :")
          .appendReason(
              ConstUtil.registerationHolder + " = "
                  + (result.get(0)))
          .appendReason(
              ConstUtil.activationHolder + " = "
                  + (result.get(1)))
          .appendReason(
              ConstUtil.passivationHolder + " = "
                  + (result.get(2)));
    }
    return checkResult;
  }
}

主节点命令

以下是可以从主节点触发的命令列表:

  • start命令

  • status命令

  • get命令

  • msg命令

  • kill命令

  • clone命令

  • stop命令

让我们从设计和实现的角度来看每个命令。

开始命令

start命令将启动八卦服务器生态系统中的主节点。执行此命令的前提是节点名称应该是唯一的。

开始命令

开始命令的数据流序列

该命令的语法是:start。以下截图显示了 shell 控制台中的响应:

开始命令

StartMasterCommand 的实现

start命令的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class StartMasterCommand extends AbstractCommand {
  private Validator validator = new Validator();
  public StartMasterCommand() {
    validator.configureTemplate().add((new StringToken("start")));
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    return checkResult.setTrue().appendReason("master started..");
  }
}

停止命令

stop命令将停止八卦服务器生态系统中的主节点。执行此命令的前提是节点应处于启动模式。

停止命令

开始命令的数据流序列

该代码的语法是:stop。以下截图显示了 shell 控制台中的响应:

停止命令

StopMasterCommand 的实现

stop命令的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class StopMasterCommand extends AbstractCommand {
  private Validator validator = new Validator();
  public StartMasterCommand() {
    validator.configureTemplate().add((new StringToken("stop")));
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    return checkResult.setTrue().appendReason("master stoped..");
  }
}

状态命令

status命令将显示八卦服务器生态系统中节点的当前状态。

状态命令

状态命令的数据流序列

该命令的语法是:status。以下截图显示了 shell 控制台中的响应:

状态命令

StatusCommand 的实现

status命令的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class StatusCommand extends AbstractCommand {
  Validator validator = new Validator();
  public StatusCommand() {
    validator.configureTemplate().add((new StringToken("status")));
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new StatusCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

StatusCommandHandler 的实现

status命令处理程序的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class StatusCommandHandler extends AbstractCommandHandler {
  public StatusCommandHandler(String nodename) {
    super(nodename);
  }
  @Override
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    if (this.getNodename().equals("master")) {
      List<String> registerednames = jedisUtil.getAllNodesFromRegistrationHolder();
      checkResult.setTrue().appendReason("The following nodes are registered ");
      checkResult.appendReason(registerednames.toString());
      List<String> activenodenames = jedisUtil.getAllNodesFromActivatedHolder();
      checkResult.setTrue().appendReason("The following nodes are activated ");
      checkResult.appendReason(activenodenames.toString());
      List<String> passivenodenames = jedisUtil.getAllNodesFromPassivatedHolder();
      checkResult.setTrue().appendReason("The following nodes are passivated ");
      checkResult.appendReason(passivenodenames.toString());
      List<String> inconsistentState = jedisUtil.getAllNodesInInconsistentState();
      checkResult.setTrue().appendReason("The following nodes are not in consitent state ");
      checkResult.appendReason(inconsistentState.toString());
    } else {
      checkResult = jedisUtil.getStatus(this.getNodename());
    }
    return checkResult;
  }
}

获取命令

get命令将显示注册在八卦服务器生态系统中的所有节点的状态。

该命令的语法是:get <field1>,<field2> where nodes are <nodename1>,<nodename2>

以下截图显示了 shell 控制台中的响应:

获取命令

GetNodeDataCommand 的实现

get命令的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class GetNodeDataCommand extends AbstractCommand {
  private Validator validator = new Validator();
  public GetNodeDataCommand() {
    validator.configureTemplate().add((new StringToken("get"))).add(new StringListToken()).add(new StringToken("where"))
        .add(new StringToken("nodes")).add(new StringToken("are")).add(new StringListToken());
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new GetNodeDataCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

GetNodeDataCommandHandler 的实现

get命令处理程序的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class GetNodeDataCommandHandler extends AbstractCommandHandler {
  public GetNodeDataCommandHandler(String nodename) {
    super(nodename);
  }
  @Override
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    StringListToken gettersstringListToken = (StringListToken) tokenList
        .get(1);
    StringListToken nodesstringListToken = (StringListToken) tokenList
        .get(5);
    List<String> nodeList = nodesstringListToken.getValueAsList();
    JedisUtil jedisUtil = new JedisUtil();
    for (String nodename : nodeList) {
      List<Boolean> result = jedisUtil.doesExist(nodename, Arrays.asList(
          ConstUtil.registerationHolder, ConstUtil.activationHolder,
          ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
      if ((result.get(0) == true) && (result.get(1) == true)
          && (result.get(2) == false)&& (result.get(3) == false)) {
        CheckResult chkresult = jedisUtil.getValuesFromNode(nodename,
            gettersstringListToken.getValueAsList());
        checkResult.setTrue()
            .appendReason("The results for " + nodename + " :")
            .appendReason(chkresult.getReason());
      } else {
        checkResult
            .appendReason("The node where the GET didn't work is as follows: ");
        checkResult
            .setFalse(
                "Activation Validation for " + nodename + " :")
            .appendReason(
                ConstUtil.registerationHolder + " = "
                    + (result.get(0)))
            .appendReason(
                ConstUtil.activationHolder + " = "
                    + (result.get(1)))
            .appendReason(
                ConstUtil.passivationHolder + " = "
                    + (result.get(2)));
      }
    }
    return checkResult;
  }
}

消息命令

msg命令用于向八卦服务器生态系统中的节点发送消息。执行此命令的前提是主节点应处于启动模式。

消息命令

消息命令的数据流序列

消息命令

主节点和客户端节点之间的消息传递

此命令的语法是:mgs <node name> where command = set, field 1, field 2

以下截图显示了主 shell 控制台中的响应:

消息命令

客户端节点(vinoo)中的响应如下:

消息命令

MessageCommand 的实现

MessageCommand的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class MessageCommand extends AbstractCommand {
  Validator validator = new Validator();
  public MessageCommand() {
    validator.configureTemplate().add((new StringToken("msg"))).add(new StringToken()).add(new StringToken("where"))
        .add(new MapListToken());
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new MessageCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

MessageCommandHandler 的实现

messageCommandHandler的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class MessageCommandHandler extends AbstractCommandHandler {
  public MessageCommandHandler(String nodename) {
    super(nodename);
  }
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> result = jedisUtil.doesExist(this.getNodename(), Arrays
        .asList(ConstUtil.registerationHolder,
            ConstUtil.activationHolder,
            ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    if (this.getNodename().equals("master")
        || ((result.get(0) == true) && (result.get(1) == true) && (result
            .get(2) == false)&& (result.get(3) == false))) {
      StringToken channel = (StringToken) tokenList.get(1);
      MapListToken data = (MapListToken) tokenList.get(3);
      checkResult = jedisUtil.publish(channel.getValue(),
          data.getValueAsMap());
    } else {
      checkResult
          .setFalse("Activation Validation :")
          .appendReason(
              ConstUtil.registerationHolder + " = "
                  + ((Boolean) result.get(0)))
          .appendReason(
              ConstUtil.activationHolder + " = "
                  + ((Boolean) result.get(1)))
          .appendReason(
              ConstUtil.passivationHolder + " = "
                  + ((Boolean) result.get(2)));
    }
    return checkResult;
  }
}

杀死命令

kill命令用于在八卦服务器生态系统中杀死节点。执行此命令的前提条件是主节点应处于启动模式。在这里,我们将通过msg命令执行。

杀死命令

Kill 命令中数据流的顺序

此命令的语法是:mgs <node name> where command = kill

以下截图显示了主 shell 控制台中的响应:

杀死命令

客户端节点(vinoo)中的响应如下:

杀死命令

KillNodeCommand 的实现

kill命令的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class KillNodeCommand extends AbstractCommand {
  private Validator validator = new Validator();
  public KillNodeCommand() {
    validator.configureTemplate().add((new StringToken("kill")))
        .add(new StringToken());
  }
  @Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new KillNodeCommandHandler(this.getName())
          .process(tokenList);
      if (checkResult.getResult()) {
        String path = System.getProperty("user.home") + "\\archive\\"
            + this.getName() + ".json";
        File file = new File(path);
        if (file.exists()) {
          if (file.delete()) {
            System.exit(0);
          } else {
            checkResult.appendReason("Archive file for "
                + this.getName()
                + ".json could not get deleted!");
          }
        }
      }
    }
    return checkResult;
  }
}

KillNodeCommandHandler 的实现

Kill命令处理程序的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class KillNodeCommandHandler extends AbstractCommandHandler {
  public KillNodeCommandHandler(String nodename) {
    super(nodename);
  }
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> result = jedisUtil.doesExist(this.getNodename(),
        Arrays.asList(ConstUtil.registerationHolder,ConstUtil.shutdownHolder));
    if ((result.get(0)) && (result.get(1) == false)) {
      checkResult = jedisUtil.killNode(this.getNodename());
    } else {
      checkResult.setFalse("Kill node failed ");
    }
    return checkResult;
  }
}

克隆命令

clone命令用于在八卦服务器生态系统中克隆节点。执行此命令的前提条件是主节点应处于启动模式,并且至少有两个客户端节点应处于激活模式。

克隆命令

Clone 命令中数据流的顺序

此代码的语法是:mgs <node name> where command = clone, target =<node name>, source=<node name>

以下截图显示了主 shell 控制台中的响应:

克隆命令

这是客户端节点(loki)的响应:

克隆命令

此时,源节点中的所有属性将被复制到目标节点。

CloneNodeCommand 的实现

clone命令的实现如下所示:

package org.redisch7.gossipserver.commands;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class CloneNodeCommand extends AbstractCommand {
       private Validator validator = new Validator();
       public CloneNodeCommand() {
    validator.configureTemplate().add((new StringToken("clone"))).add(new StringToken())
        .add(new StringToken("from")).add(new StringToken());
}
@Override
  public CheckResult execute(CommandTokens commandTokens) {
    CheckResult checkResult = new CheckResult();
    validator.setInput(commandTokens);
    checkResult = validator.validate();
    if (checkResult.getResult()) {
      List<Token> tokenList = validator.getAllTokens();
      checkResult = new CloneNodeCommandHandler(this.getName()).process(tokenList);
    }
    return checkResult;
  }
}

CloneNodeCommandHandler 的实现

cloneCommandHandler的实现如下所示:

package org.redisch7.gossipserver.commandhandlers;
/* OMITTING THE IMPORT STATEMENTS TO SAVE SPACE */
public class CloneNodeCommandHandler extends AbstractCommandHandler {
  public CloneNodeCommandHandler(String nodename) {
    super(nodename);
  }
  public CheckResult process(List<Token> tokenList) {
    CheckResult checkResult = new CheckResult();
    MapListToken maptokens = (MapListToken) tokenList.get(1);
    String target = maptokens.getNValue("target");
    String source = maptokens.getNValue("source");
    JedisUtil jedisUtil = new JedisUtil();
    List<Boolean> target_validity_result = jedisUtil
        .doesExist(target, Arrays
            .asList(ConstUtil.registerationHolder,
                ConstUtil.activationHolder,
                ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    List<Boolean> source_validity_result = jedisUtil
        .doesExist(source, Arrays
            .asList(ConstUtil.registerationHolder,
                ConstUtil.activationHolder,
                ConstUtil.passivationHolder, ConstUtil.shutdownHolder));
    if ((target_validity_result.get(0) == true)
        && (target_validity_result.get(1) == true)
        && (target_validity_result.get(2) == false)&& (target_validity_result.get(3) == false)) {
      if (((Boolean) source_validity_result.get(0) == true)
          && (source_validity_result.get(1) == true)
          && (source_validity_result.get(2) == false)&& (source_validity_result.get(3) == false)) {
        checkResult = jedisUtil.clone(target, source);
      } else {
        checkResult.setFalse("The source =" + source
            + " is not in a proper state to clone");
      }
    } else {
      checkResult.setFalse("The target =" + target
          + " is not in a proper state to clone");
    }
    return checkResult;
  }}

Redis 配置-数据管理

要管理 Redis 中的数据,了解我们试图构建的应用程序是很重要的。由于八卦服务器旨在成为配置服务器,因此读取次数将多于写入次数。Redis 提供了一些数据持久性机制,我们在前几章中已经处理过,当前部分可以作为一个复习。Redis 提供的机制如下:

  • RDB 选项

  • AOF 选项

  • 虚拟机超额内存(仅限 LINUX 环境)

RDB 选项

RDB 选项提供了定期对数据进行快照的机制。由于这是一个周期性活动,将数据转储到dump.rdb文件中,因此它是一个很好的选项来备份数据。对于我们当前的应用程序,RDB 在redis.conf文件中的配置可以是以下之一:

  • save 60 10:如果有 10 个键发生变化,将每 1 分钟保存一次数据

  • save 900 10:如果有 1 个键发生变化,将每 15 分钟保存一次数据

AOF 选项

这适用于所有的写操作。AOF 选项默认将写入数据命令转储到appendonly.aof文件中。可以使用不同的组合将命令写入到这个文件中,但每种策略都会带来性能和数据持久性的权衡。这意味着 Redis 可以配置为每次遇到写命令时都写入到这个文件,但这可能会使整个过程变慢。将持久性留给底层操作系统来刷新缓冲区到这个文件可能会使系统失去控制,但这会使应用程序非常快。对于 gossip 服务器,配置如下:

  • appendonly yes:这将创建一个appendonly.aof文件

  • appendfsync everysec:这将每秒调用fsync()函数。

VM 过度承诺内存

这是通过调整 Linux 系统的/etc/stsctl.conf来实现的。这个命令将处理 Linux 系统内部的虚拟内存管理。当调用BGSAVE函数并且父进程 fork 一个子进程时会出现问题。按照规则,子进程将拥有与父进程一样多的共享内存页。因此,如果父进程中的数据发生变化,子进程也需要具有相同的数据集以刷新到磁盘。如果父进程和子进程的组合内存需求不足以达到共享内存,则BGSAVE将失败。

本书不涉及 VM 内存管理的讨论。然而,缺少这个设置可能会导致 Redis 在写入数据到磁盘时失败。应该对/etc/stsctl.conf进行的更改是:vm.overcommit_memory=1

总结

在本应用程序中,您学习了如何创建一个 Config 服务器,也称为 gossip 服务器,它可以存储属性并将信息传递给其对等节点。在本章中,我们为客户端节点提供了存储和访问信息以及生命周期的规定。此外,我们提供了一个主节点,它可以控制任何客户端节点。

在接下来的章节中,我们将进一步扩展并为服务器增加扩展和容错能力。

第八章:集群

如果您正在阅读本文,这意味着您对 Redis 及其在 Web 和业务应用中的使用有相当多的了解。除此之外,可以合理地假设您对它可以容纳的数据结构以及如何在应用程序中使用它们也有相当多的了解。

在本章中,我们将继续讨论在生产环境中部署 Redis 应用所需采取的步骤。在生产环境中部署总是棘手的,并需要对架构和业务需求有更深入的了解。由于我们无法设想应用程序必须满足的业务需求,但我们总是可以抽象出大多数应用程序具有的非功能需求,并创建可以供读者根据需要使用的模式。

当我们考虑或谈论生产环境时,脑海中浮现的一些最常见的非功能需求列举如下:

  • 性能

  • 可用性

  • 可扩展性

  • 可管理性

  • 安全性

所有提到的非功能需求都总是在我们创建部署架构的蓝图时得到解决。接下来,我们将把这些非功能需求与我们将讨论的集群模式进行映射。

集群

计算机集群由一组松散或紧密连接的计算机组成,它们共同工作,因此在许多方面,它们可以被视为一个单一系统。此信息来源于en.wikipedia.org/wiki/Computer_cluster

我们对系统进行集群有多种原因。企业需要与成本效益和解决方案未来路线图相匹配的增长需求;因此,选择集群解决方案总是有意义的。一个大型机器来处理所有流量总是理想的,但纵向扩展的问题在于芯片的计算能力上限。此外,与一组具有相同计算能力的较小机器相比,更大的机器成本总是更高。除了成本效益之外,集群还可以满足的其他非功能需求包括性能、可用性和可扩展性。然而,拥有集群也增加了可管理性、可维护性和安全性的工作量。

随着现代网站产生的流量,集群不仅是一种低成本选择,而且是唯一的选择。从这个角度来看,让我们来看看各种集群模式,并看看它们如何与 Redis 配合。可以为基于 Redis 的集群开发的两种模式是:

  • 主-主

  • 主-从

集群模式-主-主

这种集群模式是为了应用程序而创建的,其中读取和写入非常频繁,并且节点之间的状态在任何给定时间点都需要保持一致。

从非功能需求的角度来看,在主-主设置中可以看到以下行为:

集群模式-主-主

主-主集群模式中的获取器和设置器

性能

在这种类型的设置中,读取和写入的性能非常高。由于请求在主节点之间进行负载平衡,主节点的个体负载减少,从而导致更好的性能。由于 Redis 本身并没有这种能力,因此必须在外部提供。在主-主集群的前面放置写复制器和读负载均衡器将起到作用。我们在这里所做的是,如果有写入请求,数据将被写入所有主节点,并且所有读取请求可以在任何主节点之间分配,因为所有主节点中的数据处于一致的状态。

我们还需要考虑的另一个方面是数据量非常大的情况。如果数据量非常大,那么我们必须在主节点设置内部创建分片节点)。各个主节点内的这些分片可以根据密钥分发数据。在本章后面,我们将讨论 Redis 中的分片能力。

性能

  • 在分片环境中的读写操作

- 可用性

数据的可用性很高,或者更依赖于用于复制的主节点数量。所有主节点上的数据状态是相同的,因此即使其中一个主节点宕机,其余的主节点也可以处理请求。在这种情况下,应用程序的性能会下降,因为请求必须在剩余的主节点之间共享。如果数据分布在主节点内的分片中,如果其中一个分片宕机,那么其他主节点中的副本分片可以处理该分片的请求。这将使受影响的主节点仍然能够工作(但不是完全)。

- 可扩展性

在这种情况下,可扩展性的问题有点棘手。在提供新的主节点时,必须注意以下事项:

  • 新的主节点必须处于与其他主节点相同的状态。

  • 新的主节点的信息必须添加到客户端 API 中,因为客户端可以在管理数据写入和数据读取时使用这个新的主节点。

  • 这些任务需要一段停机时间,这将影响可用性。此外,在调整主节点或主节点中的分片节点的大小之前,必须考虑数据量,以避免出现这些情况。

可管理性

这种类型的集群模式的可管理性需要在节点级别和客户端级别进行努力。这是因为 Redis 没有为这种模式提供内置机制。由于进行数据复制和数据加载的责任在于客户端适配器,因此需要解决以下观察结果:

    • 客户端适配器必须考虑可服务性,以防节点(主节点或分片)宕机。
    • 客户端适配器必须考虑可服务性,以防添加新的主节点。
  • 在已经配置了分片生态系统的情况下,应该避免添加新的分片,因为分片技术是基于客户端适配器生成的唯一密钥。这是根据应用程序开始配置的分片节点来决定的,添加新的分片将会干扰已经设置的分片和其中的数据。这将使整个应用程序处于不一致的状态。新的分片将从主节点的生态系统中复制一些数据。因此,进行此操作的选择方式将是引入一致性哈希来生成分配主节点的唯一密钥。

安全性

Redis 作为一个非常轻量级的数据存储,从安全性的角度来看提供的内容很少。这里的期望是 Redis 节点将在一个安全的环境中进行配置,责任在于外部。尽管如此,Redis 确实提供了一定形式的安全性,即用户名/密码身份验证连接到节点。这种机制有其局限性,因为密码以明文形式存储在Config文件中。另一种安全性形式可以是混淆命令,以防止意外调用。在我们讨论的集群模式中,这种安全性的用途有限,更多的是从程序的角度来看。

- 此模式的缺点

在决定采用这种模式之前,我们需要注意一些灰色地带。这种模式需要计划的停机时间才能在生产环境中工作。如果一个节点宕机并且向集群添加了一个新节点,则需要这种停机时间。这个新节点必须具有与其他副本主节点相同的状态。另一个需要注意的是数据容量规划,如果低估了,那么在分片环境中进行水平扩展将是一个问题。在下一节中,我们将运行一个示例,添加另一个节点,并查看不同的数据分布,这可以给我们一些问题的提示。数据清理是另一个特性,Redis 服务器没有解决,因为它的目的是将所有数据保存在内存中。

分片

分片是一种水平拆分数据并将其放置在不同的节点(机器)中的机制。在这里,驻留在不同节点或机器中的数据的每个分区形成一个分片。如果正确执行分片技术,可以将数据存储扩展到多个节点或机器。如果正确执行分片,可以提高系统的性能。

它还可以克服需要使用更大的机器,并且可以使用较小的机器完成工作。分片可以提供部分容错能力,因为如果一个节点宕机,那么来到该特定节点的请求将无法得到服务,除非所有其他节点都能满足传入的请求。

Redis 没有提供直接机制来支持数据的内部分片,因此为了实现数据的分区,必须在客户端 API 中应用技术来拆分数据。由于 Redis 是一个键值数据存储,可以基于算法生成唯一 ID,然后将其映射到节点。因此,如果有读取、写入、更新或删除的请求,算法可以生成相同的唯一键,或者将其定向到映射的节点,从而进行操作。

我们在本书中使用的客户端 API Jedis 提供了基于键的数据分片机制。让我们尝试一个示例,并查看数据在节点之间的分布。

从至少两个节点开始。该过程已在前几章中讨论过。在当前示例中,我们将在端口 6379 上启动一个节点,另一个节点在 6380 上启动。第一个分片节点应该类似于以下屏幕截图:

分片

第一个分片节点的屏幕截图

第二个分片节点应该类似于以下屏幕截图:

分片

第二个分片节点的屏幕截图

让我们打开编辑器,输入以下程序:

package org.learningredis.chap8;
import java.util.Arrays;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.jedis.ShardedJedis;
public class Test {
  public static void main(String[] args) {
    Test test = new Test();
    test.evaluateShard();
  }
  private void evaluateShard() {
    // Configure Jedis sharded connection pool.
    JedisShardInfo shard_1 = new JedisShardInfo("localhost", 6379);
    JedisShardInfo shard_2 = new JedisShardInfo("localhost", 6380);
    ShardedJedis shardedJedis = new ShardedJedis(Arrays.asList(shard_1, shard_2));
    // Looping to set values in the shard we have created..
    for (int i = 0; i < 10; i++) {
      shardedJedis.set("KEY-" + i, "myvalue-" + i);
    }
    // Lets try to read all the values from SHARD -1
    for (int i = 0; i < 10; i++) {
      Jedis jedis = new Jedis("localhost", 6379);
      if (jedis.get("KEY-" + i) != null) {
        System.out.println(jedis.get("KEY-" + i) + " : this is stored in SHARD-1");
      }
    }
    // Lets try to read all the values from SHARD -2
    for (int i = 0; i < 10; i++) {
      Jedis jedis = new Jedis("localhost", 6380);
      if (jedis.get("KEY-" + i) != null) {
        System.out.println(jedis.get("KEY-" + i) + " : this is stored in SHARD-2");
      }
    }
    // Lets try to read data from the sharded jedis.
    for (int i = 0; i < 10; i++) {
      if (shardedJedis.get("KEY-" + i) != null) {
        System.out.println(shardedJedis.get("KEY-" + i));
      }
    }
  }
}

控制台输出的响应应该如下所示:

myvalue-1 : this is stored in SHARD-1
myvalue-2 : this is stored in SHARD-1
myvalue-4 : this is stored in SHARD-1
myvalue-6 : this is stored in SHARD-1
myvalue-9 : this is stored in SHARD-1
myvalue-0 : this is stored in SHARD-2
myvalue-3 : this is stored in SHARD-2
myvalue-5 : this is stored in SHARD-2
myvalue-7 : this is stored in SHARD-2
myvalue-8 : this is stored in SHARD-2
myvalue-0
myvalue-1
myvalue-2
myvalue-3
myvalue-4
myvalue-5
myvalue-6
myvalue-7
myvalue-8
myvalue-9

观察

关于样本可以得出以下观察:

  • 数据分布是随机的,基本上取决于用于分发程序或分片的哈希算法

  • 多次执行相同的程序将得到相同的结果。这表明哈希算法对于为键创建哈希是一致的。

  • 如果键发生变化,那么数据的分布将不同,因为对于相同的给定键将生成新的哈希码;因此,会有一个新的目标分片。

在不清理其他分片的情况下添加一个新的分片:

  1. 在 6381 上启动一个新的主节点:观察

  2. 让我们输入一个新的程序,其中添加了客户端的新分片信息:

package org.learningredis.chap8;
import java.util.Arrays;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.jedis.ShardedJedis;
public class Test {
  public static void main(String[] args) {
    Test test = new Test();
    test.evaluateShard();
  }
  private void evaluateShard() {
    // Configure Jedis sharded connection pool.
    JedisShardInfo shard_1 = new JedisShardInfo("localhost", 6379);
    JedisShardInfo shard_2 = new JedisShardInfo("localhost", 6380);
    JedisShardInfo shard_3 = new JedisShardInfo("localhost", 6381);
    ShardedJedis shardedJedis = new ShardedJedis(Arrays.asList(shard_1, shard_2, shard_3));
    // Looping to set values in the shard we have created..
    for (int i = 0; i < 10; i++) {
      shardedJedis.set("KEY-" + i, "myvalue-" + i);
    }
    // Lets try to read all the values from SHARD -1
    for (int i = 0; i < 10; i++) {
      Jedis jedis = new Jedis("localhost", 6379);
      if (jedis.get("KEY-" + i) != null) {
        System.out.println(jedis.get("KEY-" + i) + " : this is stored in SHARD-1");
      }
    }
    // Lets try to read all the values from SHARD -2
    for (int i = 0; i < 10; i++) {
      Jedis jedis = new Jedis("localhost", 6380);
      if (jedis.get("KEY-" + i) != null) {
        System.out.println(jedis.get("KEY-" + i) + " : this is stored in SHARD-2");
      }
    }
    // Lets try to read all the values from SHARD -3
    for (int i = 0; i < 10; i++) {
      Jedis jedis = new Jedis("localhost", 6381);
      if (jedis.get("KEY-" + i) != null) {
        System.out.println(jedis.get("KEY-" + i) + " : this is stored in SHARD-3");
      }
    }
    // Lets try to read data from the sharded jedis.
    for (int i = 0; i < 10; i++) {
      if (shardedJedis.get("KEY-" + i) != null) {
        System.out.println(shardedJedis.get("KEY-" + i));
      }
    }
  }
}
  1. 结果将如下所示,因为我们可以看到来自SHARD_1SHARD_2的数据在SHARD_3中被复制。这复制的数据实际上就是由于先前的执行而存在于SHARD_1SHARD_2中的旧数据。在生产环境中,这可能是危险的,因为它增加了无法核算的死数据:
myvalue-1 : this is stored in SHARD-1
myvalue-2 : this is stored in SHARD-1
myvalue-4 : this is stored in SHARD-1
myvalue-6 : this is stored in SHARD-1
myvalue-9 : this is stored in SHARD-1
myvalue-0 : this is stored in SHARD-2
myvalue-3 : this is stored in SHARD-2
myvalue-5 : this is stored in SHARD-2
myvalue-7 : this is stored in SHARD-2
myvalue-8 : this is stored in SHARD-2
myvalue-4 : this is stored in SHARD-3
myvalue-6 : this is stored in SHARD-3
myvalue-7 : this is stored in SHARD-3
myvalue-8 : this is stored in SHARD-3
myvalue-9 : this is stored in SHARD-3
myvalue-0
myvalue-1
myvalue-2
myvalue-3
myvalue-4
myvalue-5
myvalue-6
myvalue-7
myvalue-8
myvalue-9
  1. 为相同的数据集添加一个新的主节点,并清理SHARD_1SHARD_2节点中的所有先前数据,结果将如下:
The response in the output console should be as follows 
myvalue-1 : this is stored in SHARD-1
myvalue-2 : this is stored in SHARD-1
myvalue-0 : this is stored in SHARD-2
myvalue-3 : this is stored in SHARD-2
myvalue-5 : this is stored in SHARD-2
myvalue-4 : this is stored in SHARD-3
myvalue-6 : this is stored in SHARD-3
myvalue-7 : this is stored in SHARD-3
myvalue-8 : this is stored in SHARD-3
myvalue-9 : this is stored in SHARD-3
myvalue-0
myvalue-1
myvalue-2
myvalue-3
myvalue-4
myvalue-5
myvalue-6
myvalue-7
myvalue-8
myvalue-9

我们可以看到数据在所有分片之间得到了清洁的分布,没有重复,例如旧数据已被清理。

集群模式 - 主从

这种集群模式适用于读取非常频繁而写入不太频繁的应用程序。这种模式能够工作的另一个条件是具有有限的数据大小,或者换句话说,数据容量可以适应为主节点配置的硬件(从节点也需要相同的硬件配置)。由于要满足频繁读取的需求,这种模式还具有水平扩展的能力。我们还必须记住的一点是,从节点中的复制可能会有时间延迟,这可能导致提供陈旧数据。业务需求应该能够接受这种情况。

这种模式的解决方案是将所有写操作都放在主节点上,并让从节点处理所有读操作。需要对从节点的读操作进行负载平衡,以满足性能要求。

Redis 提供了内置的主从配置功能,其中写入可以在主节点上进行,而读取可以在从节点上进行。

集群模式-主从

主-从模式中的获取器和设置器

从非功能需求的角度来看,主-从设置中可以看到的行为在以下部分进行了讨论。

性能

在这种设置中,性能写入非常高。这是因为所有写入都发生在单个主节点上,并且写入的频率较低,如假设中所述。由于读取请求在从节点之间进行负载平衡,从节点上的单个负载减少,从而提高了性能。由于 Redis 本身提供了从从节点读取的功能,除了负载平衡外,无需在外部提供任何内容。放置在从节点前面的读取负载均衡器将起作用。我们在这里所做的是,如果有写入请求,数据将被写入主节点,所有读取请求将分配给所有从节点,因为所有从节点中的数据处于最终一致状态。

在这种情况下需要注意的是,由于主节点推送新更新数据和从节点更新数据之间的时间差,可能会出现从节点继续提供陈旧数据的情况。

可用性

主-从集群模式中的可用性需要不同的方法,一个用于主节点,另一个用于从节点。最容易处理的是从节点的可用性。当涉及到从节点的可用性时,很容易处理,因为从节点比主节点更多,即使其中一个从节点出现问题,也有其他从节点来处理请求。在主节点的情况下,由于只有一个主节点,如果该节点出现问题,我们就有麻烦了。虽然读取将继续进行,但写入将停止。为了消除数据丢失,可以采取以下两种措施:

  • 在主节点前面设置一个消息队列,以便即使主节点出现问题,写入消息仍然存在,可以稍后写入。

  • Redis 提供了一种称为 Sentinel 的机制或观察者。Sentinel 的讨论已在即将到来的某些部分中进行。

可扩展性

在这种情况下,可扩展性的问题有点棘手,因为这里有两种类型的节点,它们都解决不同类型的目的。在这里,可扩展性不在于数据的分布,而更多地在于为了性能而进行的扩展。以下是一些特点:

  • 主节点的大小必须根据需要在 RAM 中保留的数据容量来确定性能

  • 从节点可以在运行时附加到集群,但最终会达到与主节点相同的状态,并且从节点的硬件能力应与主节点相当

  • 新的从节点应该注册到负载均衡器中,以便负载均衡器分发数据

可管理性

这种类型的集群模式的可管理性在主节点和从节点级别以及客户端级别需要付出很少的努力。这是因为 Redis 确实提供了支持这种模式的内置机制。由于数据复制和数据加载的责任属于从节点,所以剩下的就是管理主节点和客户端适配器。

需要解决以下观察结果:

  • 客户端适配器必须考虑服务性,以防从节点宕机。适配器必须足够智能,以避免宕机的从节点。

  • 客户端适配器必须考虑服务性,以防新的从节点被添加。

  • 客户端适配器必须具有临时持久性机制,以防主节点宕机。可管理性

主节点的容错能力

安全性

Redis 作为一个非常轻量级的数据存储,在安全方面提供的内容非常有限。这里的期望是 Redis 节点将在一个受保护的环境中进行配置,责任在于环境之外。尽管如此,Redis 确实提供了一定形式的安全性,例如用户名/密码认证连接到节点。这种机制有其局限性,因为密码是以明文存储在Config文件中的。另一种安全性形式可以是混淆命令,以防止意外调用。

在我们讨论的集群模式中,它的使用有限,更多的是从程序的角度来看。另一个很好的做法是有单独的 API,这样程序就不会意外地写入从节点(尽管这将导致错误)。以下是一些讨论过的 API:

  • 写 API:这个组件应该与与主节点进行交互的程序一起使用,因为主节点可以在主从中进行写入

  • 读 API:这个组件应该与与必须获取记录的从节点进行交互的程序一起使用

这种模式的缺点

在决定采用这种模式之前,这种模式还有一些需要注意的地方。其中最大的问题之一是数据大小。容量大小应该根据主节点的垂直扩展能力来确定。从节点也必须具有相同的硬件能力。另一个问题是主节点将数据复制到从节点时可能出现的延迟。这有时会导致在某些情况下提供过时的数据。另一个需要注意的地方是如果主节点发生故障,Sentinel 选举新的主节点所需的时间。

这种模式最适合用于 Redis 作为缓存引擎的情况。如果它被用作缓存引擎,那么在达到一定大小后清除数据是一个很好的做法。在接下来的部分中,有我们可以在 Redis 中使用的清除策略来管理数据大小。

配置 Redis Sentinel

数据存储提供了处理故障情况的能力。这些能力是内置的,并且不会以处理容错的方式暴露自己。Redis 最初是一个简单的键值数据存储,已经发展成为一个提供独立节点来处理故障管理的系统。这个系统被称为Sentinel

Sentinel 背后的理念是它是一个独立的节点,它跟踪主节点和其他从节点。当主节点宕机时,它会将从节点提升为主节点。正如讨论的那样,在主从场景中,主节点用于写入,从节点用于读取,所以当从节点被提升为主节点时,它具有读写的能力。所有其他从节点将成为这个新的从节点变成的主节点的从节点。下图显示了 Sentinel 的工作原理:

配置 Redis Sentinel

Sentinel 的工作

现在让我们举个例子,演示 Sentinel 在 Redis 2.6 版本中的工作原理。Sentinel 在 Windows 机器上运行时会出现问题,因此最好在*NIX 机器上执行此示例。步骤如下:

  1. 按照所示启动主节点:配置 Redis Sentinel

  2. 按照所示启动从节点。让我们称其为从节点:配置 Redis Sentinel

  3. 按照所示启动 Sentinel:配置 Redis Sentinel

  4. 让我们编写一个程序,我们将在其中执行以下操作:

  5. 写入主节点

  6. 从主节点读取

  7. 写入从节点

  8. 停止主节点

  9. 关闭主节点后从主节点读取

  10. 关闭主节点后从从节点读取

  11. 写入从节点

  12. Sentinel 配置

  13. 让我们输入程序:

package simple.sharded;
import redis.clients.jedis.Jedis;
public class TestSentinel {
      public static void main(String[] args) {
        TestSentinel testSentinel = new TestSentinel();
            testSentinel.evaluate();
      }

      private void evaluate() {
            System.out.println("-- start the test ---------");
            this.writeToMaster("a","apple");
            this.readFromMaster("a");
            this.readFromSlave("a");
            this.writeToSlave("b", "ball");
            this.stopMaster();

            this.sentinelKicks();
            try{
            this.readFromMaster("a");
            }catch(Exception e){
              System.out.println(e.getMessage());
            }
            this.readFromSlave("a");
            this.sentinelKicks();
            this.sentinelKicks();
            this.writeToSlave("b", "ball");
            this.readFromSlave("b");
            System.out.println("-- end of test ------ -----");
     }
     private void sentinelKicks() {
            try {
                   Thread.currentThread().sleep(10000);
            } catch (InterruptedException e) {
                   e.printStackTrace();
            }
     }
     private void stopMaster() {
       Jedis jedis =  ConnectionUtill.getJedisConnection("10.207.78.5", 6381);
         jedis.shutdown();
     }
     private void writeToSlave(String key , String value) {
         Jedis jedis =  ConnectionUtill.getJedisConnection("10.207.78.5", 6382);
         try{
           System.out.println(jedis.set(key, value));
         }catch(Exception e){
           System.out.println(e.getMessage());

         }
     }
     private void readFromSlave(String key) {
         Jedis jedis =  ConnectionUtill.getJedisConnection("10.207.78.5", 6382);
         String value = jedis.get(key);
         System.out.println("reading value of '" + key + "' from slave is :" + value);
     }
     private void readFromMaster(String key) {
         Jedis jedis =  ConnectionUtill.getJedisConnection("10.207.78.5", 6381);
         String value = jedis.get(key);
         System.out.println("reading value of '" + key + "' from master is :" + value);
     }
     private void writeToMaster(String key , String value) {
         Jedis jedis =  ConnectionUtill.getJedisConnection("10.207.78.5", 6381);
         System.out.println(jedis.set(key, value));
     }
}
  1. 您应该能够看到您编写的程序的以下结果:

  2. 写入主节点:配置 Redis Sentinel

  3. 从主节点读取:配置 Redis Sentinel

  4. 写入从节点:配置 Redis Sentinel

  5. 停止主节点:配置 Redis Sentinel

  6. 关闭主节点后从主节点读取:配置 Redis Sentinel

  7. 关闭主节点后从从节点读取:配置 Redis Sentinel

  8. 写入从节点:配置 Redis Sentinel

  9. 将以下文本添加到默认的 Sentinel 配置中:

sentinel monitor slave2master 127.0.0.1 6382 1
sentinel down-after-milliseconds slave2master 10000
sentinel failover-timeout slave2master 900000
sentinel can-failover slave2master yes
sentinel parallel-syncs slave2master 1

让我们理解我们在前面的代码中添加的五行的含义。默认的 Sentinel 将包含运行在默认端口的主节点的信息。如果您在其他主机或端口上启动了主节点,则必须相应地在 Sentinel 文件中进行更改。

  • Sentinel monitor slave2master 127.0.0.1 63682 1:这给出了从节点的主机和端口的信息。除此之外,1表示 Sentinel 之间对于主节点故障达成一致的法定人数。在我们的情况下,由于我们只运行一个 Sentinel,因此提到了1的值。

  • Sentinel down-after-milliseconds slave2master 10000:这是主节点不可达的时间。Sentinel 会不断 ping 主节点,如果主节点不响应或响应错误,那么 Sentinel 会开始其活动。如果 Sentinel 检测到主节点已经宕机,那么它将标记节点为SDOWN。但这本身不能决定主节点是否宕机,所有 Sentinel 之间必须达成一致才能启动故障转移活动。当 Sentinel 达成一致认为主节点已宕机时,就会处于ODOWN状态。可以将其视为 Sentinel 在选举新主节点之前达成一致的民主制度。

  • Sentinel failover-timeout slave2master 900000:这是以毫秒为单位指定的时间,负责整个故障转移过程的时间跨度。当检测到故障转移时,Sentinel 会请求将新主节点的配置写入所有配置的从节点。

  • Sentinel parallel-syncs slave2master 1:此配置指示故障转移事件发生后同时重新配置的从节点数量。如果我们从只读从节点提供读取查询,我们希望将此值保持较低。这是因为在同步发生时,所有从节点将无法访问。

总结

在本章中,我们学习了如何使用集群技术来最大化性能并处理不断增长的数据集。除此之外,我们还对可用性数据处理和故障处理进行了简要介绍。虽然 Redis 提供了一些技术,但我们也看到了如果不需要 Sentinel,我们如何使用其他技术。在下一章中,我们将专注于如何维护 Redis。

第九章:维护 Redis

为了维护数据,了解我们将存储在 Redis 数据存储中的数据是很重要的。数据具有各种属性,我们在第一章中已经涵盖了,NoSQL 简介,我们将再次专注于这些方面之一,以决定本章中我们将采取的数据维护策略。我们将专注于数据的短暂性质。

维护短暂数据

在一定时间内具有重要性的数据,其性质是短暂的,可以被称为短暂数据。这样的数据在预定时间后需要从系统中清除,并且计算机资源必须被释放以便为新的数据集提供空间。在一些数据存储中,没有内置的能力来做到这一点,必须编写脚本和程序来清理它们,换句话说,清理系统的责任在用户身上。在我们深入了解 Redis 提供的机制之前,让我们看看可以被称为短暂的数据类型。属于这一类别的数据类型有以下几种:

  • 事件数据:股票代码在短时间内具有重要性,然后在其被查看的上下文形式中失去价值。假设一个虚拟公司的科技股票价值在 1300 小时是 100 美元,对于所有算法来说,在 1300 小时计算科技股票的某某指数的数据是重要的。然后,比如说,在 1310 小时之后,这个数据的价值就不重要了,因为它是旧数据或者日志数据,因此可以被视为短暂数据。

  • 短暂业务数据:短暂业务数据,例如促销优惠券折扣,是电子商务业务的一个重要特性。它们在一定时间内很重要,时间结束后这些促销优惠就不存在了。同样,这种类型的数据可以被归类为短暂数据。

  • 会话数据:每个电子商务都有一个会话处理组件;基本上是维护记录注册用户与门户互动时生成的数据。

在 Redis 中处理短暂数据的策略很简单。Redis 有一个内置的功能叫做生存时间TTL),或者另一个选项是 P-TTL,它更精确,因为以毫秒为分辨率返回数据。这个功能将数据保留在内存中一段指定的时间,时间结束后,数据被清除。Redis 有一个内置的进程,不断监视具有指定 TTL 的数据,并在持续时间结束后清理数据。

维护短暂数据

Redis 中 TTL 过程的图示表示

如果没有指定 TTL/PTTL,另一个清理数据的机制是使用EXPIREPEXPIRE命令。这些命令在键上设置超时,因为数据是易失性的。在PEXPIRE中一个有趣的事情是,如果一个键已经被赋予一个值和一个EXPIRE时间,如果在这段时间结束之前再次设置了值,那么EXPIRE时间属性就会被移除。

在集群环境中,对于PEXPIRE命令,键的 DEL 命令被发送到所有从节点和节点的追加写入文件AOF)。Redis 确保它从所有位置删除,无论是在内存中(比如从节点)还是在文件系统中(比如 AOF)。

维护短暂数据

在集群环境中 EXPIRE 命令的图示表示

TTL 的行为类似于集群环境中的EXPIRE命令。

维护非短暂数据

非短暂类型的数据不依赖于时间,在系统中存在期间具有用处。由于这种数据是与时间无关的,可能会随着时间的推移而增加。这在 Redis 中可能会有问题,因为 Redis 中的数据存储在内存中。处理和维护这种非短暂数据对于 Redis 的维护至关重要,因为我们必须牢记可用内存和数据的可用性。

Redis 具有一些功能来处理先前讨论的情况,即如果数据存储在惊人的速度增长,可能会超出可用内存。在这种情况下,增加更多的 RAM 可以解决问题,或者我们可以使用一个称为 Sharding 的编程技术来分发数据集。然而,在本章中,我们将讨论一种维护不需要在活动应用程序中的数据但需要存储的机制。

让我们看看 Redis 中管理数据的一些内置技术或机制,以及它在已发布的版本中的演进路线图。

Redis 2.4

Redis 具有一个内置功能(自 2.4 版起已弃用),可以在 RAM 和文件系统(磁盘或 SSD)之间交换数据集。这个 Redis 的功能称为虚拟内存(VM)。可以通过在配置文件中启用它vm-enabled yes来配置这个 Redis 的功能。

为了理解这个特性,让我们想象一下 Redis 中整个数据集是一个按最后访问数据排序的桶。这里,最后访问是指上次修改或访问的实例。最少访问的数据集被推送到磁盘。这样就为频繁访问的数据集保留了空间。如果再次访问被推送的数据集,那么这个数据集将被带回主内存,而倒数第二个最少访问的数据将被推送到磁盘。当 VM 启用时,下图是幕后活动的表示:

Redis 2.4

一个虚拟机启用系统数据集处理的简单表示

注意

请注意,推送到磁盘的是值,而不是键。键始终在内存中。

这个 VM 选项适用于包含大型数据集的业务数据。当存在一种使用模式,随着时间推移,一些数据被较少访问时,这个选项也是有用的。

在这种情况下,我们可以将这些键值对合并在 Hashes 中。例如,假设我们正在维护客户记录如下所示:

  • 客户 1 作为(KEY),一些客户数据“ABC”作为(VALUE)

  • 客户 2 作为(KEY),一些客户数据“XYZ”作为(VALUE)

  • 客户 3 作为(KEY),一些客户数据“123”作为(VALUE)

  • 客户 4 作为(KEY),一些客户数据“AQ@”作为(VALUE)

如果我们继续以这种方式存储数据,那么如果客户数据增长,我们有可能会耗尽空间(内存)(尽管这对业务来说是好事,但对支持它的技术团队来说并不是好事)。更好的存储客户数据的方法是使用 Hash。

客户存储将是(KEY),相应的客户值将是(HASHES),并包含以下数据:

  • 客户 3 作为(KEY),一些客户数据“123”作为(VALUE)

  • 客户 4 作为(KEY),一些客户数据“AQ@”作为(VALUE)

  • 客户 1 作为(KEY),一些客户数据“ABC”作为(VALUE)

  • 客户 2 作为(KEY),一些客户数据“XYZ”作为(VALUE)

如果以这种方式存储值,在最坏的情况下,整个值数据集将被推送到磁盘,并且如果需要,可以再次带回内存。

除了vm-enabled yes之外,要配置 VM 功能,需要查看以下配置:

  • vm-max-threads:此设置提供了在内存和磁盘之间执行 I/O 活动的最大线程数。将值设置为0将使管理客户端请求的单个线程负担过重,导致整个过程停滞,并将数据集重新加载到主内存中。

  • vm-max-memory:此选项告诉 Redis 服务器应保留多少内存来存储数据集。一旦达到此阈值,它就开始将数据集从内存交换到磁盘。

  • vm-swap-file:此设置提供了可以转储数据集的文件系统中的位置。

  • vm-pages:此设置将提示 Redis 服务器需要创建多少页面来交换文件。

  • vm-page-size:此设置将提示 Redis 服务器分配多少磁盘存储空间来存储值数据集。vm-pagesvm-page-size的组合对于从磁盘快速检索数据集的存储非常重要。

在业务案例场景中,性能至关重要,并且有使用 VM 选项的限制时,可以通过使用固态设备SSD)来提高性能。与磁盘相比,这些设备具有更快的读写速度,磁盘受读/写速度的限制。

注意

请注意,从 Redis 2.4 开始,VM 选项将被弃用。

Redis 2.6 到 2.8

与 2.4 版本不同——在该版本中,VM 选项是处理大于内存的数据的方法——在更新的版本中,最好清除数据并将其存储在一个单独的位置(这里,位置可以是不同的实例或文件系统)。在新版本中解决了 VM 选项面临的问题。

转储和恢复

对于 Redis 版本 2.6,其中一种机制是发出Dump键命令,它将返回该键的数据的序列化版本。此数据可以与目标 Redis 实例中的Restore命令一起使用,从而将其转换为可读数据。如前所述,处理大数据的最佳模式是将键值收集到集合中(例如 Hashes),然后对其进行操作以管理数据。

以下图表是处理数据(不再访问但需要保留在系统中)的简单表示:

转储和恢复

DUMP 和 RESTORE 命令的示意图表示

将键和值存储在集合中(例如 Hashes)的好处是,您可以发出一个命令来操作整个集合,然后使用一个命令将其恢复。当您已经有一小部分需要清除的数据时,这种技术非常有用。然而,当您想要存储整个数据集时,您必须研究快照,这将在后面讨论。

这种机制有一个警告;即它记录数据的序列化 RDB 版本,因此这些序列化数据不能用于任何其他 Redis 版本。

快照

处理大型数据集的内置技术称为快照。如前几章所讨论的,这种技术用于在 AOF 中持久化数据。此过程将数据转储到 AOF 中,如配置文件中指定的那样。执行命令将数据转储到文件中的方式和机制可以是在后台(BGSAVE)或前台(SAVE)执行。在高度并发的环境中,如果这些活动对系统性能造成压力,解决此问题的一个聪明方法是使用更大的机器。

将一个更大的机器作为从节点(主节点)引入并在适当的时间压力下提升为主节点的想法,现在整个数据集都在资源更多的更大的机器中。以下图是整个活动的简单表示。在许多生产环境中,由于数据层通常在路由器后面,通常使用路由器来切换流量而不是依赖 Sentinel 来进行切换是一般做法。在没有路由器的环境中,可以使用 Sentinel 来进行切换,如何进行此操作在之前的章节中已经讨论过。

Snapshotting

从小型机器迁移数据的简单表示

Redis 3.0

将 Redis 数据集限制在可用内存范围内的另一个机制是清除旧数据。Redis 没有内置机制来清除数据;相反,它有一个MIGRATERESTORE数据的组合。让我们详细看看这个过程。

假设我们有一个 Hashes 集合,用于维护所有客户在 2012 年的购买历史记录;因此,通常键看起来像PURCHASE_HISTORY_2012,值将是包含客户 ID 作为键和客户购买详情作为值的数据集的 Hash。

Redis 3.0

要迁移的键值数据集的表示

同样,PURCHASE_HISTORY_2013PURCHASE_HISTORY_2014PURCHASE_HISTORY_2015将用于后续年份。任何需要为用户显示过去 4 年的购买数据的业务需求,比如Customer-A,将从 2012、2013、2014 和 2015 的键中获取数据。业务需求将从这些年份追加数据,从而形成一个组合响应。现在,在 2016 年,将创建另一个键,但是为了获取Customer-A的购买历史记录,将从 2013、2014、2015 和 2016 的键中获取数据。在这种情况下,PURCHASE_HISTORY_2012将被留下,但出于法律原因,我们不能删除它。然而,它在在线系统中占用内存空间。在这种情况下,我们可以发出MIGRATE命令,这是DUMPDEL的组合。当我们内部发出MIGRATE命令时,Redis 将发出DUMP键来序列化数据和 I/O 到目标实例。一旦目标实例被恢复,序列化键将发送一个OK命令回到源机器,然后可以删除PURCHASE_HISTORY_2012键。现在我们可以在目标实例中发出SAVE命令并创建一个 AOF 文件,如果需要,可以将其存储在文件系统中以供以后参考。

以下图是给定键的数据迁移的表示:

Redis 3.0

Redis 中迁移过程的表示

注意

请注意,MIGRATE命令将在 Redis 3.0 版本中工作。

摘要

在本章中,您看到了在 Redis 中维护数据的各种机制,可以使用 Redis 中的内置功能,也可以使用巧妙的机制来实现。

posted @ 2024-05-21 12:36  绝不原创的飞龙  阅读(72)  评论(0)    收藏  举报