Java-应用持久化最佳实践-全-
Java 应用持久化最佳实践(全)
原文:
zh.annas-archive.org/md5/6c26fb5fbcefd96b3eb3d19a5f4f8cca
译者:飞龙
前言
一个稳固的软件架构结合了将技术解决方案付诸实施所需的每一个构建块。在应用程序生命的早期阶段,设计和实践就已经确立:微服务或单体架构、事件驱动方法、集成和交付应用程序生命周期、容器化等。在应用程序方面,特别是在 Java 环境中,框架和执行运行时也被定义。像那些古老的遗留系统一样,大多数现代云原生解决方案依赖于数据,通常在数据存储(例如,数据库)中。不幸的是,持久层往往被忽视,没有得到与其他主题相同的重要性。对于依赖于有状态服务的场景,这是走向终结的开始——全新的解决方案注定要经历性能挣扎和维护性降低,甚至更糟,可能会植入导致数据不一致的杂草。维护性降低是持久层设计和定义忽视的结果,因为方案和数据模型在信息不足的基础上实施得不好。在这种情况下,即使是微不足道的数据库重构也是残酷且耗时的。
对持久层、技术和现有方法的挑战、解决方案和最佳实践的全面理解,是走出这些棘手场景以及许多与应用程序和数据存储相关的场景的出路。
本书介绍了在 Java 解决方案中可以使用的成熟模式和标准,并详细阐述了在面临良好的 Java 编码实践时,用于云原生微服务的流行技术和框架的优缺点。由于 Java 技术已经广泛使用超过二十年了,云的采用带来了额外的挑战,例如通过堆栈现代化降低成本的需求日益增长。因此,你将学习关于应用现代化策略的知识,并深入了解企业数据集成模式和事件驱动架构如何使现代化过程平稳进行,对现有遗留堆栈的影响降至最低或为零。
阅读本书后,你的下一个架构决策将更加稳固,并基于对数据存储选项及其相应推荐使用场景的全面解释,涵盖了 SQL、NoSQL 和新 SQL 等技术。当在 Java 生态系统背景下讨论与数据相关的内容时,关于 MicroProfile 和 Jakarta EE 如何工作;数据库模式(如 Active Record 和数据访问对象(DAO);命令和查询责任分离(CQRS);内存持久化;以及对象映射框架等主题,都有大量信息可供参考。
如果您此时已经理解了为什么谨慎处理数据存储对于系统和架构至关重要,并相信它对整个组织有直接影响,以至于到了通过采用以数据为中心的战略来击败竞争对手的程度,那么这本书就是为您准备的。准备好加入我们在这段激动人心的旅程中,探索数据、其奥秘及其在云驱动时代中的宝藏。
本书面向的对象
本书主要面向具有强大 Java 背景、专注于构建 Java 解决方案的资深开发者、工程师和软件架构师。它旨在满足那些已经对 Java 开发有扎实理解并希望增强其在持久化方面的知识和技能的个人。
本书的内容假设读者对 Java 编程概念、面向对象设计原则和企业级应用开发有一定的熟悉程度。它深入探讨高级主题,并探索现代 Java 解决方案中持久化的各个方面。
无论您是一位希望深化对持久化技术理解的资深 Java 开发者,还是一位寻求优化 Java 应用程序性能和可扩展性的工程师,亦或是一位负责设计稳健持久层的软件架构师,本书都提供了有价值的见解、最佳实践和实用指导,以满足您的需求。
通过利用本书中分享的专业知识和经验,您可以提升在 Java 项目中设计、实施和优化持久化解决方案的能力,最终使您能够开发出高性能、可扩展和易于维护的 Java 应用程序。
本书涵盖的内容
第一章,从洞穴到云端的数据存储,您将获得为书中后续内容所需的基础知识,所以请系好安全带。您将了解存储数据的挑战,这些挑战催生了最初的数据存储解决方案。随着技术的进步,数据库演变成了稳健和可靠的解决方案。Java 生态系统对此做出了良好的反应,并与数据生态系统同步增长,为用户提供框架、应用服务器等,以简化开发者的体验并实现高性能的数据库集成。
第二章,提炼多种数据库风味,讨论了随着解耦和独立服务的增长和个体需求,多语言持久化策略如何自然地显现。您将探索不同的数据存储方式,市场数据管理系统(例如,关系型、NoSQL 和新 SQL),它们各自的语言,以及最重要的是,每个系统的用例场景。过度设计是系统设计中的恶棍,所以这一章将为您提供知识,以使它远离您的持久化层。
第三章,探索架构策略和云使用,将使你熟悉并帮助你回忆起关于多种架构解决方案的概念。你将了解单体、微服务和事件驱动解决方案之间的关系,以及这些方法如何推动不同云服务提供的日益普及。你将学习如何识别使用本地和云解决方案混合使用的利弊,这种混合使用导致组织解决方案建立在混合和/或多云模型之上。
第四章,在云原生应用中充分利用数据管理设计模式,深入探讨了云原生应用中的数据管理领域,并探讨了如何有效地利用设计模式。随着云技术的日益普及,开发者优化数据管理策略以最大化云原生架构的益处变得至关重要。
第五章,雅加达 EE 和 JPA:现状,提供了对雅加达 EE 和 MicroProfile 生态系统中持久性的全面概述。持久性是企业应用开发的基本方面,了解这些框架如何处理持久性对于开发者来说是至关重要的。
第六章,Java 中 NoSQL 的神秘面纱:一个 API 统治一切,讨论了 NoSQL 数据库如何为企业应用和系统打开各种能力的大门。如今,甚至更加保守的市场,如金融业,也开始考虑非关系型数据库解决方案。是时候熟悉 NoSQL 数据库及其类型了,了解如何将它们与 Java 服务集成,以及它们可能适合数据存储的用例。
第七章,jOOQ 采用指南的缺失部分,讨论了面向对象查询,通常称为 jOOQ,这是一个在 Java 中实现 Active Record 模式的轻量级数据库映射软件库。其目的是通过提供基于数据库模式自动生成的类来构建查询的领域特定语言(DSL),实现关系型和面向对象。
第八章,使用 Eclipse Store 实现超快内存持久性,探讨了 Eclipse Store,它通过纯 Java 提供超快的内存数据处理。它提供微秒级查询时间、低延迟数据访问、巨大的数据吞吐量和工作负载。因此,它节省了大量 CPU 功率、CO2 排放和数据中心成本。
第九章,持久化实践:探索多语言持久化,深入探讨了在 Jakarta 数据生态系统中的多语言持久化概念。多语言持久化指的是在应用程序中使用多种数据存储技术以优化不同的数据需求。
第十章,构建分布式系统:挑战和反模式,探讨了构建分布式系统的复杂性,并检查了在过程中可能出现的挑战和反模式。分布式系统在现代软件架构中越来越普遍,但它们也带来了一组自己的复杂性。
第十一章,现代化策略和数据集成,探讨了现代化策略和数据集成技术,以帮助组织适应其现有系统以满足现代技术景观的需求。随着技术的快速发展,企业现代化其遗留系统并将它们无缝集成到新技术中变得至关重要。
第十二章,现代 Java 解决方案中持久化的最终考虑,是最后一章,我们提供了关于现代 Java 解决方案中持久化的重要考虑和见解。随着 Java 开发领域的演变,了解持久化的最佳实践和新兴趋势至关重要。
为了充分利用这本书
在您开始阅读这本书并深入研究软件需求之前,了解以下技术至关重要:Java 17、Maven、Git 和 Docker。假设您熟悉 Java 17,包括其语法和面向对象编程概念,以及核心库和框架的熟悉度。了解 Maven 将有益,因为它是一个流行的构建自动化工具,用于管理依赖关系和构建 Java 项目。Git,一个版本控制系统,对于有效地跟踪和管理源代码更改是必要的。最后,了解 Docker,一个容器化平台,将帮助您理解如何在隔离环境中打包和部署软件应用程序。
本书涵盖的软件/硬件 | 操作系统要求 |
---|---|
Java 17 | Windows、macOS 或 Linux |
Maven | |
Git | |
Docker |
如果您正在使用这本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件github.com/PacktPublishing/Persistence-Best-Practices-for-Java-Applications/
。如果代码有更新,它将在 GitHub 仓库中更新。
使用的约定
本书使用了多种文本约定。
文本中的代码
:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“在这个领域,Book
实体属性应该是title
、author
、publisher
和genre
。”
代码块设置如下:
public class Book { private final String title;
private final String author;
private final String publisher;
private final String genre;
// constructor method
// builder inner class
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
public class Book { private final String title;
private final String author;
private final String publisher;
private final String genre;
// constructor method
// builder inner class
}
任何命令行输入或输出应如下编写:
$ mkdir css$ cd css
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“从管理面板中选择系统信息。”
小贴士或重要注意事项
它看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们非常感谢您能向我们报告。请访问www.packtpub.com/support/errata并填写表格。
盗版:如果您在互联网上发现我们作品的任何非法副本,我们将非常感谢您能提供位置地址或网站名称。请通过 copyright@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《Java 应用程序持久性最佳实践》,我们很乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都非常重要,它将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
您喜欢在路上阅读,但又无法携带您的印刷书籍到处走?
您的电子书购买是否与您选择的设备不兼容?
请放心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止这些,您还可以获得独家折扣、时事通讯和丰富的免费内容,每天直接发送到您的邮箱
按照以下简单步骤获取这些好处:
- 扫描下面的二维码或访问以下链接
packt.link/free-ebook/9781837631278
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的邮箱
第一部分:云计算中的持久性 – 在现代软件架构中存储和管理数据
在本书的这一部分,我们深入探讨了云计算环境中持久性的基本方面。随着云解决方案在现代软件架构中越来越普遍,了解如何有效地存储和管理数据在这个环境中至关重要。
本部分包括以下章节:
-
第一章**,存储数据:从洞穴到云端
-
第二章**,提炼多种数据库风味
-
第三章**,探索架构策略和云使用
-
第四章**,在云原生应用中充分利用数据管理设计模式
第一章:数据存储的历史——从洞穴到云端
数据:一个关键、改变生活、根本的资产,它支持人类的生存和进化。数千年来(是的,数千年!),数据存储解决方案已经演变并支持人类,使我们能够以简单、易于维护和可搜索的方式“记住”和分享知识。数据转化为信息,进而转化为知识。从过去学习并规划未来的能力高度受到我们今天在系统中管理数据的方式的影响。
软件工程师是这一过程的催化剂:我们的责任是通过软件工程定义和交付解决方案,以解决人们的问题——这些解决方案大多围绕大规模或小规模的数据操作。在理解了持久性在软件工程中的重要性之后,你就可以将你解决方案的持久性提升到下一个层次。
在本章中,我们将探讨现代时代,数据库已经成为我们应用程序和整个地球的支柱。我们将涵盖以下主题:
-
数据库为何存在?数据库的历史
-
Java 持久性框架的特点
-
云对有状态解决方案的影响
-
探索分布式数据库系统的权衡——对 CAP 定理及其超越的探讨
本章首先为你提供对数据存储技术过去和当前状态的理解,然后转向更高级的主题。这将为你提供一个更好的工作基础。你将学习数据存储技术如何应对市场的云迁移心态。最后,你将熟悉诸如领域驱动设计(DDD)等实践,这些实践与良好的持久性开发实践完美结合,以及分布式世界中分布式数据系统面临的挑战,例如 CAP 定理。
数据库为何存在?
没有深入研究人类历史,就不可能全面理解数据库。人类渴望在时间流逝中保存知识,这使得写作成为最持久的技术之一。回顾过去,它最初被用于寺庙和洞穴,这可以被认为是人类第一个非计算数据库。
今天,行业强调准确和详尽的记录信息。事实上,越来越多的人获得技术访问权限并加入全球信息网络的结果反映在研究上,表明数据量每两年翻一番。
现代数据库的历史始于 1960 年,当时查尔斯·巴科曼为计算机设计了第一个数据库,即集成数据存储,或IDS,它是 IBM 的信息管理系统(IMS)的前身。
在那之后的十年,大约在 1970 年,数据库历史上最重大的事件之一发生,当时 E. F. Codd 发表了其论文《大型共享数据库的数据关系模型》,创造了关系数据库这一术语。
最后,在数据存储方面的下一个,也许是最新的突破是 NoSQL,它指的是任何非关系型数据库。有人说NoSQL代表Non-SQL,而另一些人则说它代表Not Only SQL。
NoSQL 数据库为一些最受欢迎的在线应用程序提供动力。以下是一些:
-
谷歌:谷歌使用 NoSQL Bigtable 为 Google Mail、Google Maps、Google Earth 和 Google Finance 提供服务
-
Netflix:Netflix 喜欢 NoSQL 数据库的高可用性,并使用 SimpleDB、HBase 和 Cassandra 的组合
-
优步:优步使用 Riak,这是一个具有灵活键值存储模型的分布式 NoSQL 数据库
-
领英:领英构建了自己的 NoSQL 数据库,名为 Espresso,这是一个面向文档的数据库
处理数据的挑战
数据库系统的演变在几十年中由关键里程碑所标记。在早期,当存储成本高昂时,挑战在于找到减少信息浪费的方法。减少价值一百万美元的信息就是一个重大的成就。
你知道吗?
在数据库时代的黎明时期,兆字节的成本曾经高达约 500 万美元!
ourworldindata.org/grapher/historical-cost-of-computer-memory-and-storage
现在,兆字节的成本不再是挑战,因为我们生活在一个 0.001 美元/MB 的成本时代。随着时间的推移和存储成本的降低,减少重复数据的手段开始对应用程序的响应时间产生负面影响。规范化以及减少数据重复、多个连接查询和大量数据尝试并没有起到太大的帮助。
对这一模型提出挑战并不令人惊讶。正如备受尊敬和尊敬的书籍《软件架构基础》的作者所指出的(www.amazon.com/dp/1492043451/
),没有明确的解决方案;相反,我们面临的是许多解决方案,每个解决方案都伴随着其自身的优点和缺点。
显然,这一点也适用于数据库。
在数据存储解决方案方面,没有一种适合所有情况的解决方案。
在 2000 年代,新的存储解决方案,如 NoSQL 数据库,开始变得流行,架构师有更多的选择。这并不意味着 SQL 不再相关,而是说架构师现在必须应对选择每个问题的正确范例的复杂性。
随着数据库领域经历了这些阶段,应用程序的场景也发生了变化。讨论转向了采用微服务架构风格的动机和挑战,这使我们回到了多种持久性策略。传统上,架构包括关系型数据库解决方案,通常有一个或两个实例(考虑到其增加的成本)。现在,随着新的存储解决方案的成熟,架构解决方案开始包括基于 NoSQL 数据库的持久性,扩展到多个运行实例。在多个服务中存储数据,这些服务组成一个更广泛解决方案的可能性,为具有多语言持久性的潜在新解决方案提供了一个良好的环境。
多语言持久性是指计算机应用程序可以使用不同的数据库类型,利用各种引擎系统更适合处理不同问题的这一事实。复杂的应用程序通常涉及不同类型的问题,因此为每一项工作选择合适的工具可能比试图使用单一解决方案解决问题的所有方面更有效率。
在分析最近时期的大部分解决方案时,现实情况使我们开发者、架构师面临选择的复杂性。我们如何处理数据,必须考虑一个具有多种数据类型的场景?为了清楚起见,我们正在谈论混合和匹配数百种可能的解决方案。最佳途径是通过学习持久性基础、最佳实践和范式来做好准备。最后,意识到无论我们多么渴望一个快速、可扩展、高度可用、精确且一致的解决方案——我们现在知道,根据 CAP 定理,一个在本书后面章节讨论的概念,这可能是不可能的。
接下来,我们将我们的关注点具体缩小到 Java 应用程序中的持久性。
Java 持久性框架的特点
让我们掌握 Java 语言和可用的多个数据库之间的差异概念。Java 是一种面向对象编程(OOP)语言,自然提供了诸如继承、封装和类型等特性,这些特性支持创建良好的代码。不幸的是,并非所有这些特性都得到数据库系统的支持。
因此,当整合语言和数据库范式时,它们的一些独特优势可能会丢失。当我们观察到在内存对象和数据库模式之间的所有数据操作中,都应该有一些数据映射和转换时,这种复杂性变得明显。定义一个首选方法或提供一个隔离层是至关重要的。在 Java 中,通过使用框架来整合这两个世界的最系统化方式是使用框架。框架的类型和类别由它们的通信级别和提供的 API 动态性决定。在图 1.1中,观察这两个概念的关键方面:
图 1.1 – 关于 Java 持久性框架不同特性的考虑
-
通信级别:定义代码与数据库或面向对象范式之间的无关性。代码可以被设计得与两个领域中的一个更相似。为了澄清,考虑两种常见的将 Java 应用程序与数据库集成的途径 – 直接使用数据库驱动程序或依赖映射模式:
-
直接采用驱动程序(例如,JDBC 驱动程序)意味着更接近数据库领域空间的工作。一个易于工作的数据库驱动程序通常是面向数据的。一个缺点是需要更多的样板代码,以便能够将数据库模型和 Java 领域对象之间所有操作的数据进行映射和转换。
-
映射模式提供了使用完全相反的方法将数据库结构映射到 Java 对象的可能性。在诸如 Hibernate 和 Panache 这样的映射框架的上下文中,主要目标是更紧密地与面向对象范式对齐,而不是主要关注数据库。虽然提供了减少样板代码的好处,但它作为权衡,需要与持续存在的对象关系阻抗不匹配及其随之而来的性能影响共存。这个主题将在后续章节中更详细地介绍。
-
-
API 抽象级别:在数据操作和其他数据库交互期间,为了在 Java 和数据库之间进行某些级别的翻译,开发者依赖于特定的 Java API。为了阐明 API 的抽象级别,例如,你可以问:“一个特定的数据库 API 支持多少种不同的数据库类型?”当使用 SQL 作为关系数据库集成的标准时,开发者可以使用一个 单一 API 并将其与 所有 关系数据库版本集成。有两种类型的 API:
-
特定 API 可能会提供来自供应商的更准确的更新,但这同时也意味着,如果你想要切换到不同的数据库(例如,Morphia 或 Neo4j-OGM – OGM 代表 对象 图 映射器),任何依赖于该 API 的解决方案都需要进行更改。
-
一个无差别的 API 更灵活,可以与许多不同类型的数据库一起使用,但管理每个数据库的更新或特定行为可能更具挑战性
-
代码设计– DDD 与面向数据
在著名的书籍《代码整洁之道》中,作者被称为 Uncle Bob,他提出面向对象编程语言具有隐藏数据以展示其行为的优势。沿着同样的思路,我们看到领域驱动设计(DDD),它提出了在整个领域代码和相关通信中使用通用语言的建议。实现这一建议可以通过使用面向对象的概念来完成。在《面向数据编程》中,Yehonathan Sharvit 建议通过赋予数据相关性并将其视为“一等公民”来简化复杂性。
幸运的是,有几个框架可以帮助我们应对提供高性能持久层所面临的挑战。尽管我们理解更多的选择会带来选择的悖论,但无需担心——这本书是软件工程师可以用来学习如何在软件架构中评估多个视角的有用资源,特别是数据存储集成和数据操作空间中的细节。
到目前为止,我们已经探讨了人类为解决一个基本问题而设计的各种方法:以确保长期性和作为支持我们进化的知识库的方式高效地存储数据。随着技术的进步,多种持久策略已经提供给软件架构师和开发者,包括关系型和无结构的方法,如 NoSQL。持久性选项的多样性导致了软件设计中的新挑战;毕竟,检索、存储和使数据可用也经历了应用层的创新。自那时起,至今,持久性框架为架构师提供了不同的策略,使得设计与底层数据库技术紧密相关或更加动态和无关紧要。
我们这次数据库历史之旅的下一站是云时代。让我们探讨云服务如何影响应用程序以及数据现在可以存储的方式和位置。
云对有状态解决方案的影响
当涉及到数据库时,专业人士除了需要基础设施和软件架构的视角外,还需要具备运营视角。在解决方案的架构和所需合规性方面,有几个因素需要考虑,例如网络、安全、云备份和升级。
幸运的是,我们可以利用云服务。作为一个与技术相关的概念,云已经被国家标准与技术研究院(NIST)定义为一种模型,它通过网络按需提供共享的计算资源,这些资源可以快速提供。
你可能已经在技术社区中听到过一个笑话,说“云只是别人的电脑”。然而,我们认为云不仅仅是这样;我们更喜欢以下这样的看法:
云是别人的问题。
采用云服务的主要目标是外包非核心业务功能给其他人。这样,我们可以专注于我们的核心竞争力。
小贴士
随着你阅读这本书,你会注意到使用了几个缩写。在本章中,我们主要指的是以下云服务提供类型:基础设施即服务(IaaS)、平台即服务(PaaS)和软件即服务(SaaS)。
尽管你可能觉得云服务最终可能是解决你经历过的许多技术问题的解决方案,但请记住,委托的责任和任务也有可能与你预期的截然不同——例如,服务崩溃或成本激增。由于我们正在讨论“将问题委托给他人”的行为,以下是三种类型的云服务(三种“委托”方式)及其相应的目标受众:
-
IaaS:基础设施不是你的问题。目标受众是从事运营方面工作的人,例如 SREs。
-
PaaS:基础设施和运营不是你的问题。主要的目标受众是软件工程师。
-
SaaS:基础设施、运营和软件都不是你的问题。在这种情况下,目标受众是最终用户,他们不一定知道如何编码。
正如我们在这章中之前指出的,每个解决方案的权衡都必须考虑。以选择 PaaS 云服务为例:这种模型以更高的抽象层次为代价,换取了更高的价格标签。
那么,关于数据存储的云服务怎么办呢?正如 Dan More 在《97 Things Every Cloud Engineer Should Know》一书中所指出的(www.amazon.com/dp/1492076732
),数据库也可以用作托管云服务。查看托管数据库服务时,你可以考虑其他人(供应商)将提供一项服务,以抽象化大部分(在某些情况下,全部)数据库基础设施和管理任务。
数据库即服务(DBaaS)是一种流行的云服务类型,允许用户从多个地区运行的不同数据库类型中选择。
当我们需要探索各种架构持久化解决方案并委托复杂性时,云服务可以很有帮助。它们已被广泛采用,并证明在满足这一目的方面是有效的。
随着云服务和微服务架构的采用,分布式解决方案变得越来越普遍。因此,架构师必须处理与数据完整性相关的新挑战,以及必须满足此类要求的应用程序中数据不一致的意外发生。
探索分布式数据库系统的权衡——对 CAP 定理及其超越的探讨
如果要描述完美的分布式数据库系统(DDBS),它肯定是一个高度可扩展、提供完美一致数据且在管理方面不需要过多关注的数据库(例如备份、迁移和网络管理)。不幸的是,埃里克·布赖尔提出的 CAP 定理表明这是不可能的。
注意
到目前为止,还没有数据库解决方案能够提供总数据一致性、高可用性和可扩展性等特性的理想组合。
有关详细信息,请参阅:迈向健壮的分布式系统。PODC. 7. 10.1145/343477.343502 (https://www.researchgate.net/publication/221343719_Towards_robust_distributed_systems)。
CAP 定理是理解 DDBS 不同属性之间权衡的一种方式。埃里克·布赖尔在 2000 年分布式计算原理(PODC)研讨会上推测,在创建 DDBS 时,“对于任何共享数据系统,你最多只能拥有这些属性中的两个,”指的是属性一致性、可用性和对网络分区的容错性。
图 1.2 – 受埃里克·布赖尔主题演讲启发的表示
注意
迈向健壮的分布式系统。有关埃里克·布赖尔工作的更多信息,请参阅布赖尔,埃里克。(2000),演讲:people.eecs.berkeley.edu/~brewer/cs262b-2004/PODC-keynote.pdf
。
CAP 定理中描述的三个特性可以描述如下:
-
一致性:保证分布式集群中的每个节点返回相同的、最新的、成功的写入。
-
可用性:每个非失败节点在合理的时间内对所有的读取和写入请求返回响应。
-
分区容错性:系统即使在网络分区的情况下也能继续运行并保持其一致性保证。换句话说,即使在崩溃、磁盘故障、数据库、软件和操作系统升级、停电和其他因素下,服务仍在运行。
换句话说,我们可以选择和选择的 DDBS 只会是CA(一致且高度可用)、CP(一致且容错)或AP(高度可用且容错)。
小贴士
正如书中《软件架构基础:工程方法》所强调的,良好的软件架构需要处理权衡。这又是需要考虑的一个权衡点(www.amazon.com/Fundamentals-Software-Architecture-Engineering-Approach-ebook/dp/B0849MPK73/
)。
通过考虑 CAP 定理,我们就可以将这一新知识应用于决策过程中,帮助我们选择 SQL 和 NoSQL。例如,传统的数据库管理系统(DBMS)在(主要)提供原子性、一致性、隔离性和持久性(ACID)属性时表现良好;然而,在分布式系统中,为了实现更高的可用性和更好的性能,可能需要放弃一致性和隔离性。这通常被称为为了可用性牺牲一致性。
在 CAP 理念提出后的近 12 年后,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 发表了一些研究,对布赖尔定理进行了形式化的证明。然而,另一位数据库系统架构和实现方面的专家也对可扩展和分布式系统进行了一些研究,为现有的定理增加了对一致性和延迟权衡的考虑。
2012 年,Daniel Abadi 教授发表了一项研究,指出 CAP 已被“越来越误解和误用,造成了重大损害”,导致不必要的分布式数据库管理系统(DDBMS)创建受限,因为 CAP 只针对某些类型的故障提出了限制——而不是在正常操作期间。
Abadi 的论文《现代分布式数据库系统设计中的一致性权衡》提出了一种新的公式,性能和一致性弹性能力(PACELC),认为可以通过使用弹性来管理一致性和性能之间的权衡。论文中引用的以下问题阐明了主要思想:“如果存在分区(P),系统如何权衡可用性和一致性(A 和 C);否则(E),当系统在没有分区的情况下正常运行时,系统如何权衡延迟(L)和一致性(C)?”
根据 Abadi 的说法,分布式数据库可以在某些条件下既高度一致又高度性能,但只有当系统能够通过使用弹性根据网络条件调整其一致性级别时。
到这一点,构建数据库系统的复杂性,尤其是分布式数据库,已经变得非常清晰。作为负责评估和选择 DDBS 以及在其之上设计解决方案的专业人士,对这些研究中讨论的概念有基本理解,对于做出明智的决策是一个宝贵的基石。
摘要
任何软件应用程序都高度依赖于其数据库,因此给它应有的关注是很重要的。在本章中,我们探讨了数据存储的有趣历史,从其早期到云计算的现代化时代。在整个旅程中,我们见证了数据存储演变对软件工程领域的影响,以及 Java 框架是如何演变以支持多语言解决方案的。作为经验丰富的软件工程师,了解数据及其有效管理和操作的重要性对我们至关重要。
此外,我们还讨论了关系型数据库的挑战,例如数据冗余和规范化,以及如何出现 NoSQL 数据库来处理非结构化数据需求。我们介绍了 CAP 定理,并提到了额外的研究,如 PACELC,来解释实现分布式数据存储解决方案的挑战。
随着我们继续阅读本书,我们将深入了解高级架构和开发实践、挑战以及权衡,这些是你必须了解的,以便为现在及以后你将与之合作的每个解决方案提供最佳持久层,相关于数据持久性。在查看数据库与 Java 之间的历史、动机和关系之后,准备好在下一章中探索不同类型的数据库及其优缺点。
第二章:探索多种数据库风味
随着系统的演变,尤其是在微服务架构中,实施一种多语言持久化策略来满足解耦和独立服务的个别需求变得必要。这涉及到检查存储数据的各种选项,包括关系型、NoSQL 和新 SQL 数据库在内的数据库管理系统(DBMSs)。为了防止过度设计架构设计,考虑每种数据库类型的应用用例场景是很重要的。
在本章中,我们将深入研究传统关系型数据库和较新的非关系型数据库的特点和优势。我们还将更详细地探讨 NewSQL 数据库及其在市场中的位置。
在深入研究应用程序细节之前,我们将首先熟悉我们可以采用作为解决方案持久化策略的多种存储解决方案。以下主题将涵盖:
-
回顾关系型数据库
-
深入了解非关系型数据库
-
NewSQL 数据库 – 努力从两个世界中获取最佳
回顾关系型数据库
关系型数据库已经是一种超过 50 年的数据存储信任解决方案,在全球范围内被广泛采用。用户从使用关系型数据库中获得的最优点之一是能够使用结构化查询语言(SQL)。
SQL 是一种由多个供应商支持的标准化查询语言,这意味着 SQL 代码是可移植的,相同的 SQL 代码在许多数据库系统上只需稍作修改或无需修改即可工作。这是一种确保供应商锁定的方式。除此之外,SQL 还有助于减少学习新语言或 API(如 Java 的 JDBC 或 JPA)的认知负担。
现在,当我们提到 DBMS 时,除了大量工具和资源外,关系型数据库还遵循ACID原则(原子性、一致性、隔离性和持久性),确保数据事务的可靠性和完整性。这些特性使关系型数据库成为大量用例的可靠选择。关系型数据库已经显示出极高的成熟度,带来了几个成功案例,这些案例不仅涵盖了基础知识,还包括提供备份工具、数据可视化等其他功能。事实上,当习惯于使用 SQL 数据库的人转向关注 NoSQL 存储解决方案时,他们会错过所有支持他们日常任务的各种辅助工具和数千种工具。
在 Java 中,我们有 JDBC,这是一种我们可以学习一次并在任何地方编写/应用的语言。关系型数据库引擎是透明的;因此,JDBC 和 JPA 将是相同的。
这些持久化技术的本质特征是与数据事务相关的属性:原子性、一致性、隔离性和持久性(ACID)。符合 ACID 属性的事务具有以下方面:
-
原子性:事务是一个原子单元。要么所有数据库操作作为一个单元发生,要么不发生;这是一个“全有或全无”的操作。这导致防止了部分数据更新和潜在的数据不一致。
-
一致性:当事务开始和结束时,数据库应处于一致状态。事务应遵循每个数据库约束和规则,以确保充分的一致性。
-
隔离性:一个事务不应不利或意外地影响另一个操作。例如,一个表插入将生成一个表行 ID,该 ID 被第二个操作使用。然而,我们不希望两个操作同时更改相同的行。
-
持久性:事务完成后,一旦提交,更改将永久保留。这确保了即使在意外故障的情况下,数据的一致性。
正如我们将在接下来的章节中学习的,Java 应用程序有多种不同的策略与数据库集成。存在几种与数据集成相关的设计模式,可用于设计应用程序,从底层数据库的低度解耦到高度解耦。我们应该担心抽象能力和在切换到另一个数据存储解决方案时的低努力程度的原因是,尽管关系数据库非常成熟,但它们并不适合每个用例。例如,数据模式灵活性、面对大量读写操作时的可扩展性、大型数据集上的查询性能,以及在数据建模期间处理层次结构和其他复杂关系等特点,通常在 NoSQL 数据库上比在关系数据库上更强。现在,我们应该更好地了解多种类型的 NoSQL 数据库及其特性。
深入了解非关系型数据库(NoSQL)
NoSQL数据库提供了存储和检索非结构化数据(非关系型)的机制,与关系数据库中使用的表格关系形成鲜明对比。与关系数据库相比,NoSQL 数据库具有更好的性能和高度的可扩展性。它们在金融和流媒体等几个行业中越来越受欢迎。由于这种使用量的增加,用户和数据库供应商的数量正在增长。
与关系数据库的 ACID 原则相对比,在 NoSQL 世界中,关键特性是BASE(基本可用性、软状态和最终一致性)。每个细节如下:
-
基本可用性:高程度的复制确保即使发生多次故障,数据仍然可用。
-
软状态:没有要求具有写入一致性,也没有保证复制的数据将在节点之间保持一致性。与传统的关系数据库不同,更改可以在没有直接用户输入的情况下发生。
-
最终一致性:当检索数据(读取时间)时,一致性可以延迟处理。换句话说,数据最终将是一致的,这样所有节点都将具有相同的数据,但不必同时具有。
有许多 NoSQL 数据库类型,每种类型都是为处理特定的工作负载和数据建模需求而设计的。为了最好地定义要使用哪种 NoSQL 存储类型,我们现在将深入探讨,以获得对键值、文档、列族和图数据库类型的更清晰的认识。
在对多种 NoSQL 风味有了广泛了解之后,您可以参考本节末尾提供的图 2.5,以了解如何在关系数据库和某些 NoSQL 存储风味之间比较概念。
NoSQL 数据库类型 – 键值
这些是 NoSQL 世界中最简单的存储类型。数据以键值对集合的形式存储,这种方式优化了存储大量数据并有效地通过键进行数据搜索。这种数据库类型具有类似于java.util.Map
API 的结构,其中值映射到键。
例如,如果使用这种范例来存储关于希腊神话人物的信息并将它们与它们的特征关联起来,数据关联将表示如下:
图 2.1 – 键值存储数据库表示
在前面的图中,表示了三个希腊神话人物以及它们与特征的关联。在这个例子中,值太阳有一个键阿波罗,而键阿芙罗狄蒂可以用来指代爱情和美丽。
目前市场上,这种方法的流行实现包括Amazon DynamoDB、Hazelcast和Redis数据库,后两者是开源技术。每个供应商都带来了自己独特的优势;DynamoDB 可以作为一项完全托管的服务使用,这意味着亚马逊负责运行该服务所需的所有基础设施和维护。Redis 是一种支持 pub/sub 消息和缓存功能的内存数据库解决方案。最后,Hazelcast 支持 MapReduce 编程模型以执行分布式数据处理任务,以及跨语言支持,包括 Java、.NET 和 Python。
在这种数据库类型中,有一些新的概念需要了解,例如桶和键值对。尽管不是每个方面都可行,但对于那些习惯于传统 SQL 世界的人来说,两个世界的概念之间有相关性,这有助于他们理解。
总结一下,键值 NoSQL 是一种可以将数据作为键值对集合存储的数据库,并且优化了存储大量数据以及通过键高效检索数据。它以其易于使用和理解而闻名,以及其水平可扩展性,这使得它成为需要高读写吞吐量的应用程序的良好选择。
即使有多个好处,在数据建模和查询方面,键值数据库可能不如其他类型的 NoSQL 数据库灵活。它们不支持复杂查询,并且没有丰富的数据模型,因此可能不适合需要复杂数据操作的应用程序。此外,键值数据库不支持事务,这可能会限制某些用例。
现在,让我们来看看文档数据库类型及其特征。
NoSQL 数据库类型 – 文档
NoSQL 文档存储类型旨在以最小定义的结构存储、检索和管理文档,例如 XML 和 JSON 格式。换句话说,没有预定义结构的文档是一种可能由多种不同类型的数据字段组成的模型,包括其他文档内的文档。数据结构看起来像以下代码结构中的 JSON:
{ "name":"Diana",
"duty":["Hunt","Moon","Nature"],
"age":1000,
"siblings":{
"Apollo":"brother"
}
}
上述 JSON 结构显示了一个存储有关名为 Diana 的神话人物数据的文档。这个相同的结构可以存储不同类型的数据,如字符串、数字、列表和其他复杂对象。与其他类型一样,这是一个灵活的选项,可以以分层格式存储数据,无需事先指定模式。具体来说,文档 NoSQL 数据库选项易于使用且需要最少的设置,这使得它成为快速原型设计和快速开发应用程序的良好选择。另一方面,它通常缺乏事务支持,并且不像传统关系数据库提供的复杂多表连接功能那样提供复杂的查询功能。
Amazon SimpleDB、Apache CouchDB和MongoDB都是流行的 NoSQL 文档类型存储解决方案。前者是亚马逊网络服务提供的一项完全管理的数据库服务,而后者都是开源解决方案。所有三种选项都提供了使用 Java 与数据库交互的 API。
在了解了更多关于键值和文档类型之后,让我们继续了解下一个:广度列数据库。
NoSQL 数据库类型 – 广度列/列族
广度列(也称为列族)模型因 Google 的 BigTable 论文而流行,它是一种用于结构化数据的分布式存储系统,并且具有高可扩展性和大存储容量。这些数据库针对存储大量具有灵活模式的结构化、半结构化和非结构化数据进行了优化,并支持高并发级别。
与其他类型不同,此类数据库中的数据是按列而不是按行存储的,这允许更灵活和可扩展的数据模型。单个列族中存储的数据可以是不同类型和结构,如图 2.2 所示:
图 2.2 – NoSQL 列族类型表示
与其他 NoSQL 类型相比,这些数据由于不是存储在传统的基于行的格式中,因此查询可能更加困难。此外,增加的模式灵活性也代表了数据模型设计和数据管理等任务复杂性的增加。
在引擎选项方面,HBase 和 Cassandra 都是开源的、分布式、宽列 NoSQL 数据库,设计重点是处理大量数据。Scylla 也是一个分布式宽列数据库,但设计为 Cassandra 的直接替代品,并针对性能进行了优化。
总之,宽列 NoSQL 数据库是存储和管理大量数据、具有灵活模式的强大工具,非常适合需要高可用性和水平扩展存储的分布式应用程序。然而,与其他 NoSQL 数据库相比,它们可能更难查询。
在我们进入下一节之前,我们将讨论最后一种 NoSQL 数据库类型,这在某些场景下可以特别有用,以补充宽列数据库:图数据库。
NoSQL 数据库类型 – 图
图 NoSQL 数据库类型针对存储和查询具有复杂关系的数据进行了优化。在此方法中,数据表示为图,其中节点表示实体,边表示这些实体之间的关系。观察 图 2.3 中的图结构,用于语义查询,以及通过节点、边和属性进行的数据表示:
图 2.3 – NoSQL 图类型表示
在开发将与图数据库一起工作的应用程序时,需要注意的关键概念如下:
-
顶点/Vertice:也称为图中的节点。它存储实体或对象数据,就像传统的关系数据库中的表一样,或者像文档型 NoSQL 数据库中的文档一样。
-
边缘:用于建立两个顶点之间关系的一个元素。
-
属性:一个键值对,用于存储关于图中的边或顶点元素的元数据。
-
图:表示实体之间关系的顶点和边的集合。
在图中,边可以是定向的或非定向的,节点之间的关系可以存在方向,实际上,这是图结构中的一个基本概念。如果考虑现实世界,我们可以将其与好莱坞明星进行比较,例如,有些人认识一个演员,但演员并不了解所有他们的粉丝。这种关联的元数据作为图边方向(关系)的一部分存储。在图 2.4中,请注意关联方向和类型被明确定义:
图 2.4 – NoSQL 图类型数据模型方向表示
图 2.4显示了从顶点Poliana到顶点Hephaestus的方向关联。关联还有自己的数据,例如在这种情况下的时间when和地点where。在查询图时,方向尤其相关,因为你不能从Hephaestus查询到Poliana – 只能是相反的方向。
图 NoSQL 数据库解决方案非常适合需要快速查询高度互联数据的场景,例如社交网络、推荐引擎和欺诈检测系统。尽管它们可以存储和检索大量数据,但在大量具有灵活模式的结构化和非结构化数据的情况下,列族类型可能更适合。此外,复杂的查询可能需要遍历图以找到所需的数据片段。
有几个图数据库引擎可供选择,包括Neo4j、InfoGrid、Sones和HyperGraphDB。这些引擎各自提供自己独特的一套特性和功能,正确的选择将取决于应用程序的具体需求。
我们已经探讨了关系数据库和 NoSQL 数据库,这是今天用于使用固定模式存储和查询结构化数据以及存储和查询大量结构化/半结构化/非结构化数据的两种主要数据库存储范式。
在进入下一节之前,这里有一个最后的提示,帮助你将你已熟悉的概念与你迄今为止所展示的概念联系起来:
图 2.5 – 如何在不同数据库风味之间建立概念关系以用于学习目的
在本章的下最后一个部分,我们将检查一类较新的数据库:NewSQL 数据库。
NewSQL 数据库 – 努力从两个世界中获取最佳
NewSQL 数据库是一种混合数据库类型,结合了关系型和 NoSQL 世界的最佳特性,能够在具有固定模式的结构化数据存储和查询的同时,也提供 NoSQL 数据库的可扩展性和灵活性特性。NewSQL 被视为解决关系型和 NoSQL 范式局限性的方法,为现代应用提供更灵活和可扩展的解决方案。NewSQL 旨在统一 SQL 和 NoSQL 世界的最佳特性。我们已经学习了两种一致性模型:关系数据库提供的 ACID(原子性、一致性、隔离性和持久性)和 NoSQL 的 BASE。NewSQL 试图在保持 ACID 原则保证的同时提供横向可扩展性。换句话说,它试图在 SQL 的保证下提供 NoSQL 的高可扩展性、灵活性和性能。另一个积极方面是能够使用 SQL 作为查询语言。
NewSQL 看起来是一个有希望的解决方案,我们可以在撰写本文时观察到几家相关公司向市场提供企业级解决方案。值得一提的是,这些公司拥有大量关于开发和运营需求的专业知识。
以下是一些 NewSQL 数据库的例子:
-
VoltDB
-
ClustrixDB
-
CockroachDB
注意到 NewSQL 技术领域并非同质化,每种解决方案都带来自己的优势和劣势。
重要提示
NewSQL 使用 SQL,但通常不支持 100%的 SQL。
尽管这个范式给人一种印象,似乎可以一次性解决 CAP 定理提出的问题,但我们应该警告您,它并不能。此外,通常,混合选项会带来两者的最佳和最坏方面。
摘要
数据库选项有多种类型,理解每种类型的权衡至关重要。每个数据目标都有特定的行为,例如 SQL 拥有标准、成熟度、多种工具和专业知识等优势。然而,实现横向扩展相当困难。
NoSQL 旨在提高横向可扩展性;然而,这以牺牲比关系型数据库所知更少的致性为代价。
最后,NewSQL 试图将两个世界融合在一起,带来两者的好处,但在两个领域都存在不足。
在下一章中,我们将介绍更多关于技术和它们的架构以及策略,以及如何处理它们。
第三章:探索架构策略和云使用
在本章中,我们将从服务的角度深入探讨架构的话题。具体来说,我们将探讨单体架构和微服务架构之间的关系,并考虑每种方法的优缺点。我们还将检查使用事件驱动架构作为整合这些服务的方法。除了提供技术基础外,我们还将旨在提供战略性和情境性的见解,了解这些概念是如何相互结合的,以及为什么它们推动了各种云服务提供的采用。
在这本书中,我们的目标不仅是提供技术 Java 持久化概念的坚实基础,还要提供战略性和情境性的见解,了解这些想法是如何相互关联的,以及为什么它们有助于各种云服务提供的日益普及。
本章将在以下部分涵盖上述主题:
-
云对软件架构设计的影响
-
设计模式 – 软件架构师的基本构建块
-
单体架构
-
微服务架构
到本章结束时,你将更深入地理解整体解决方案架构如何影响数据集成设计,以及使用本地和云解决方案组合的优缺点,从而产生混合和/或多云模型。
云对软件架构设计的影响
虽然我们可以从深入研究多个架构主题的具体细节开始——包括单体、微服务、SOA、事件驱动和事件溯源——但我们将采取不同的方法。我们将首先为您提供对这些模式及其在软件设计中的重要性有更深入的理解。有了这个背景,将有助于拓宽您的视野。让我们更详细地探讨一些设计模式。
设计模式 – 软件架构师的基本构建块
在过去的几十年里,正如 Martin Fowler 明智地所说:“在一种实际情境中有用的想法,可能会在其他情境中也有用”,我们已经识别并分享了“在一种实际情境中有用的想法,可能会在其他情境中也有用”:
全世界技术爱好者分享的想法、经验和解决方案的持续流动,汇聚成一个丰富的知识库,推动和加速技术进化。
模式描述了不同级别的解决方案,从代码级别的实践到应用级别的实践。在数百种模式中,我们将突出设计模式、企业应用模式和软件架构模式的实践,以帮助我们构建一个坚实的持久化层。
四人帮(GoF)设计模式和面向服务的架构(SOA)模式是最近出现的微服务架构和事件驱动架构模式的重要基础。
近年来广受欢迎的微服务架构是一种将软件系统设计为一系列小型、独立可部署服务的架构方法。这种架构模式基于 SOA 的核心思想——模块化和关注点分离,但将其推向了更深的层次。
人们常常采用他们实际上并不需要的解决方案,因为他们缺乏分析趋势和应对技术炒作的能力。重要的是要记住,目标应该是使用可用的技术来识别解决特定问题的最佳方案,而不是简单地针对云原生微服务或其他流行解决方案的交付。关键在于理解如何使用适当的技术来解决一系列商业问题。
考虑技术趋势——一些需要注意的事项
判断特定趋势是否适合您场景的常见方法是参考其技术采用生命周期。它提供了市场采用洞察,有助于您了解该主题当前的成熟度;换句话说,采用特定解决方案的人越多,出现的成功案例就越多。不仅如此,还有恐怖故事、采用挑战、优缺点、推荐等等也会出现。从更广阔的角度来看,不同的成熟度群体提供了更多关于接受该技术的市场细分领域的理解。
我们现在明白,模式是一组构建块,可以用来实现特定的商业目标。有数百种模式覆盖了应用解决方案的多个层次和方面,并且可以从先前模式的概念中派生出新的模式。重要的是要记住,模式可以组合并用于不同的方式来应对不同的目标。例如,一个 Java 服务可以为它的持久层采用存储库模式,基于微服务架构的最佳实践进行构建,使用企业集成模式与其他服务通信,遵循 12 因子应用的推荐进行云原生应用的构建,并采用设计模式以实现自动化管道交付。
考虑到这一点,让我们深入探讨不同架构选项(如微服务和单体应用)的优缺点,同时考虑基本需求和特性。
单体架构
建立解决方案的传统方式是使用单体应用,这些是大型、独立的软件系统,作为单一、统一的单元构建,所有组件都包含在单个包中,并一起编译、管理和部署:
图 3.1 – 单体应用特性
这意味着前端和后端都包含在同一个工件中,必须一起编译、管理和部署。虽然这种方法可以使得最初开发和维护应用程序变得更容易,但随着团队的扩大,代码的维护变得更加复杂,部署更新变得更加具有挑战性和耗时。
在性能方面,可扩展性受到影响,因为很难升级或降级特定的功能或组件。
定义数据库和单体之间的关系并不复杂。有些人选择开发存储和消费来自多个数据库的数据的单体,这进一步增加了维护的复杂性。
惊讶的是,使用单体架构可以创建模块化应用程序。这些应用程序可以以模块化的方式设计,每个模块负责一组特定的功能,并且独立于其他模块开发。
接下来,为了验证其成熟度,让我们参考广泛的市场采用和反馈。根据 2022 年的趋势报告[2],模块化单体架构方法已经跨越了鸿沟,在早期多数群体中得到了广泛采用。
与每个架构设计一样,这种方法有其优点和缺点。我们可以从多个角度分析其好处,包括但不限于可维护性、部署流程和频率、验证流程、自动化管道等。图 3.2 显示了在设计应用程序时需要分析的关键主题,这些主题可能导致在应用程序生命周期的每个阶段都需要不同水平和成本的努力。有些人优先考虑长期利益,如易于维护。其他人可能更喜欢采用更容易、更快的启动策略:
图 3.2 – 应用设计过程中的决策点
最佳选择将取决于每个业务需求。就单体架构风格而言,其特性已被证明可能成为组织的一个重大障碍,尤其是在应用程序随着多个开发团队和众多功能的增长而变得更加复杂时。在这种环境下,对应用程序的更改和添加变得成本高昂,扩展变得困难。
面对由 SOA 方法启发的单体设计的缺点,微服务概念应运而生。微服务提出将组件/模块解耦成更小的服务,每个服务都有其独特的职责。
尽管微服务涉及管理更多的故障点,但成功的实施允许获得以下好处:独立团队、变更、部署以及生态系统中每个服务的扩展,而不会影响其他微服务。这是在维护每个个体服务完整性的原则下实现的。让我们进一步探讨这个主题,并更仔细地检查细节。
微服务架构
微服务导向架构引入了创建彼此解耦且根据其业务领域建模的应用程序的想法。这些应用程序通过不同的协议和多种通信模式(如 REST、GRPC 和异步事件等)以及集成模式进行集成。使用微服务导向架构可以促进更快、更频繁的交付,并引入一种语言无关的生态系统。
微服务架构具有解耦且独立于构成更广泛解决方案的其他微服务的服务。正如 Sam Newman 在他的书《Building Microservices》中所言,从图 3.3中可以隐含地了解微服务的概念和行为:
图 3.3 – 微服务特征
架构师和开发者不仅应该考虑微服务架构的核心特征,还应该考虑可能导致项目风险极大的关键要素,如下所述:
-
集成延迟:在单体应用中,组件通过内存直接通信,而不是通过网络。与微服务场景相比,结果是通信更快。然而,随着服务数量和架构复杂性的增加,性能问题和延迟的风险也会增加——可能成为一个灾难性的问题。为了减轻这种风险,将适当的监控和管理服务调用响应时间的阈值作为良好实践,包括客户端服务自行处理此类问题的能力。一个建议和良好实践是在客户端微服务中具备容错能力。例如,客户端应该能够重试之前失败的调用,有适当的回退机制(而不是因为错误而关闭),并且一旦请求的服务恢复正常运行,能够重新建立自身。
-
职责分离:注意后端和前端组件的职责分离。一个前端可能依赖于多个后端服务。前端解决方案必须实施,以便在请求的后端服务之一失败的情况下,只有特定的相关功能会中断其正常功能——所有其他前端组件应继续正常工作,确保为最终用户提供最佳可能体验。
-
多语言环境陷阱: 微服务架构对语言和框架是中立的。因此,您的环境可能成为多语言的。从架构和技术领导的角度来看,在评估要使用的科技时要节俭,以免最终使用缺乏所需人员维护技术的服务。范围定义和微服务大小应被视为此类定义的措施。
-
服务大小和责任: 确定一个微服务的大小和范围可能需要在解耦之旅开始时投入时间和精力。记住,在衡量服务范围和大小时要仔细考虑单一责任(SOLID)原则。
-
遗忘的服务: 管理挑战之一是避免在生产环境中存在孤儿应用程序。通过为每个服务建立一支团队,包括在生产阶段,尽量减少没有所有者的服务。这样,面对意外问题或新的变更请求时,将更容易确定和定义谁应该负责这些任务。
-
细粒度仓库: 避免将项目拆分成过多的仓库,因为这种过度粒度化可能会在仓库数量超过公司合作者数量时变得难以管理。
微服务采用常见陷阱
微服务架构的采用带来了多重影响和挑战,正如我们现在所知道的,正如在《软件架构基础:工程方法》一书中所述(www.amazon.com/dp/B08X8H15BW
),一切都有权衡——而且微服务也不例外。
虽然最初被认为是一种有希望的方法,但微服务之旅已被证明比更广泛的科技行业估计的要复杂,尤其是对于小型团队。随着其采用的增加,我们也观察到更多关于各种设计问题和失误的报告。为了避免常见陷阱,重要的是要注意以下错误。
不当地将领域拆分成微服务
将业务问题映射到领域,以及将领域映射到微服务时,很容易出错,尤其是在开始采用微服务方法时。这种领域导致需要向多个服务发出请求才能检索到相关的一组业务数据,而不是通过单个请求有效地提供。换句话说,在数据检索和查询方面,不正确的范围定义可能导致复杂的代码实现和性能不佳的解决方案。
以下是一些有助于您找到正确路径的指南:
-
利益相关者和业务专家参与领域定义的过程,因为他们可以就领域边界提供有价值的输入
-
微服务应该有明确的范围;负责一个“事物”并能做好;具有易于组合的功能(换句话说,就是具有内聚性);能够独立部署和扩展;并且记住,单体架构由于所有处理都在内存中进行,没有在微服务集成期间增加额外的网络延迟,因此更有可能表现出更高的性能。
通常来说,跨域服务集成可以依赖于多种策略,例如以下几种:
-
使用 API 网关来回路由请求,并从多个源中过滤、转换和聚合一个客户端请求中的请求数据
-
在服务之间进行数据去规范化,这可能导致数据重复,但以更高效的数据检索和查询为代价,依赖于事件驱动架构等技术来减少检索数据所需的请求数量,或者拥有能够异步过滤、聚合、丰富并提供访问相关数据的基于事件驱动的服务
自动化差距
随着开发团队被拆分成更小、数量更多的组,它们开始更频繁地交付更多服务。这些服务的生命周期操作不应阻碍它们的快速演变潜力。“持续集成和持续部署”(CI/CD)是微服务领域的最佳实践,对于管理跨多个部署环境部署的多个服务至关重要,这些环境从本地机器到云服务不等。
采用团队希望尽可能多的语言和技术
决定编程语言无疑是众多引人入胜的话题之一。尽管程序员喜欢炫耀那些能让他们写出超短“Hello World”示例的编程语言,并以此为基础做出决定,但时至今日,我们还没有遇到过任何一个项目是以将文本输出到某种控制台、终端或甚至编写 HTML 作为核心业务目标的。
像服务编程语言这样的关键决策不应仅基于示例代码的行数或行简单性。
一个应用程序必须成为微服务,因为它很大
请认识到,并非每个大型应用程序都需要成为微服务。这里有一个我们希望您熟悉的有趣指标:每行代码的成本。
(medium.com/swlh/stop-you-dont-need-microservices-dc732d70b3e0
).
链接中提到的成本包括计算资源和人力,包括组织流程可能经历的潜在变化,以及潜在的新的软件解决方案,如容器和容器编排器。
与单体架构(monolith)的对应物不同,在微服务架构(microservices architecture)中,代码规模越小,每行代码的成本就越高,因为服务存在所涉及的一切和每个人仍然都是必需的。遗憾的是,一个成功交付的微服务只是解决实际业务问题所需的一部分。
没有充分利用独立微服务的扩展性
可扩展性是微服务的一个关键优势。然而,考虑是否单独扩展组件是有意义的很重要。在某些情况下,一起扩展整个系统可能更有效。想想看:是否只扩展更广泛解决方案中独特的、较小的部分是有意义的?
数据不一致
微服务依赖于数据,就像任何其他分布式数据库一样,它们都受到 CAP 定理的影响。这意味着每次你必须更新多个服务时,你将在应用程序中增加一层复杂性。
一种处理这个问题的方法是通过采用 SAGA 模式。然而,这个额外的复杂性层可能会对你的数据整体一致性产生负面影响。
从微服务开始
通常,假设你的项目将基于微服务是不明智的。这可能导致未来出现大问题,尤其是在领域定义方面。小错误可能导致服务之间出现多个错误的相互依赖和紧密耦合。这就是为什么许多专家建议在处理关系型数据时使用连接,在处理如 MongoDB 这样的 NoSQL 数据库时使用子文档。
虽然连接(joins)是关系型数据库中一个强大的功能,它允许我们通过外键(foreign keys)将不同表中的数据结合起来,但在 NoSQL 数据库中,尤其是在处理大数据集时,连接可能会变得低效且耗时。这是因为连接需要执行多个查询,可能会导致大量的网络流量和资源消耗。
此外,NoSQL 数据库根据应用程序的访问模式和用法进行了查询性能优化。
因此,通常建议对数据进行建模,以最小化连接的需求,并使用反规范化(denormalization)和嵌入(embedding)技术将相关数据组合成单个文档。
然而,在某些情况下,对于 NoSQL 数据库来说,连接可能是必要的。在这些情况下,NoSQL 数据库提供了不同的方法来执行连接,例如在 MongoDB 中使用$lookup
或 MapReduce,这些方法旨在更有效地与 NoSQL 数据模型协同工作。
小贴士 - 采用微服务时常见的错误参考
没有必要因为这里提出的挑战而感到气馁;当架构被正确使用并在有利的情况下使用时,它非常适合。关键是,没有圣杯或银弹。
如果你想继续了解采用微服务时的常见错误,请参考以下阅读推荐:Ebin John 的《停止,你不需要微服务》(medium.com/swlh/stop-you-dont-need-microservices-dc732d70b3e0
)和 Sam Newman 的《我应该使用微服务吗?》(www.oreilly.com/content/should-i-use-microservices/
)。
我们迄今为止概述了单体和微服务架构的概念,探讨了三种主要的云交付模型:IaaS、PaaS 和 SaaS,并了解到它们可以组合起来以最佳满足组织的需要。
接下来,让我们进一步探讨云部署模型,以及拥有多个云部署选项如何帮助团队缩短开发周期、填补知识空白,并使团队能够更有效地应用他们的知识和努力。
倾向于现代有状态解决方案的云部署策略
云部署模型允许应用程序依赖于具有按需使用、弹性、容错性、可测量访问和其他基本方面的基础设施。让我们来看看部署模型策略,例如公有云和私有云,如何通过混合和多云模型推导出两种组合,以及如何最佳地利用可用的云部署策略来高效地交付有状态应用程序。
为什么混合和多云模型很重要
在寻求更好的灵活性、访问供应商特定功能、集成选项和降低成本的过程中,云部署模型的组合已经开始被更频繁地使用。组织开始结合公有和私有部署模型,并从私有云和公有云服务中受益,采用混合云模型。另一种策略是多云模型,当需要从不同供应商运行或消费相同类型的服务时使用。
当你结合公有云和私有云,并利用多个供应商提供的类似云服务时,你正在使用一个混合多云****部署模型。
注意
注意,最佳的部署模型并不是名字中单词数量最多的那个——最佳的模型是解决你组织现有问题的那个。
由于现有技术和解决方案的种类繁多,团队不可能在每种技术上都具备专业知识。难以组建一个在专业知识方面足够多元化的团队,这导致两种可能性:一个管理不善和维护不佳的基础持久化基础设施,或者开发者可用的选项受限。
由于数据管理是如此关键的业务组件,因此不应被忽视。这就是我们可以与我们云故事联系起来之处:我们的业务是否可以将数据存储管理的责任委托给他人? 在这一点上,我们理解混合和多云模型可以提供几种类型云计算资源的轻松扩展和缩减。如果我们有一个具有 这样能力的数据库…
结果我们发现确实有一个——它被称为数据库即服务(DBaaS)。
除了能够快速启动和运行一切之外,使用 DBaaS,还可能将复杂的任务如监控、版本维护、安全补丁维护、灾难恢复和备份委托出去。除此之外,它还使得采用团队中尚无专业人员的存储技术成为可能,从而便于为每种场景选择最佳解决方案。然而,如果需要直接访问运行数据库的服务器或对传输和存储的敏感数据进行完全控制,DBaaS 则不是一个可行的解决方案。目前市场上可用的 DBaaS 提供示例包括 Amazon RDS、AWS Aurora MySQL、Microsoft Azure SQL Database、ScyllaDB 和 MongoDB Atlas。
在架构解决方案时,这正是你得到两者之最佳之处:通过使用解耦和独立的服务,你可以在适合的地方依赖公共云服务,如 DBaaS 提供的服务,专门用于特定需要的服务,而对于无法处理公共云提供服务的服务的,则依赖本地数据存储解决方案。
在这些不同的部署模型中,分布式服务,服务集成是一个需要考虑的关键架构方面。
分布式系统及其对数据系统的影响
微服务是大拼图中的一小部分:每个组件只有在整个拼图拼好之后才能发挥其真正的价值。可靠性、弹性和可伸缩性这些品质不应在每个单独的服务级别上被考虑,而实际上,对于所提出的集成解决方案;毕竟,我们同意马丁·福勒的观点,即集成应被视为对业务具有战略意义。
“基于微服务的解决方案的性能仅与其各个组件高效沟通的能力相当。”
示例 - 架构食品配送解决方案
在分布式架构中,处理跨服务的数据集成可能会很困难。因此,我们将通过一个简单的例子来探讨围绕集成的架构概念和错误——一个基于微服务的食品配送网站解决方案。缩小范围,讨论考虑以下内容:
-
微服务后端层
-
微服务数据存储
-
跨服务集成
接下来,让我们看看这个解决方案最初是如何作为一个微服务架构起草的,以及这些服务的集成如何极大地影响数据管理和一致性。
基本场景
在早期开发阶段,它可能看起来像是一个非问题场景。对于食品配送的例子,想象一个解决方案,如图图 3**.4所示,由四个微服务组成,每个微服务都有自己的持久性和数据存储策略:订单服务、支付服务、厨房服务和配送服务。该图表示了微服务,每个微服务都有自己的持久存储。
图 3.4 – 食品配送服务的表示
这个示例的“快乐路径”是这样的:每当客户创建并支付新的订单时,厨房就会烹饪并把它送到配送团队,配送团队随后将订单送到客户手中。图 3**.5展示了从创建到配送的新订单流程,其中业务流程在四个独立的微服务之间处理。
图 3.5 – 食品配送业务的需求
从技术角度来看,这个业务需求可以这样描述:
-
订单服务:记录新的订单 0001
-
支付服务:
-
记录订单 0001 所需的付款
-
记录订单 0001 的成功付款
-
-
厨房服务:
-
通知订单 0001 的到来
-
记录订单 0001 正在准备中
-
记录订单 0001 已准备好配送
-
-
配送服务:
-
通知订单 0001 已准备好发送给客户
-
记录 0001 的配送已完成
-
要掌握这个看似简单的业务需求的细微差别,我们必须深入了解技术细节,并探索各种障碍和潜在解决方案。
这个解决方案是否可以是单体架构?是的,可以。然而,配送服务,尤其是那些跨越多个客户/订单提供者/配送提供者的服务,是基于一个广泛的业务需求列表构建的,这些需求在用于学习目的的简单示例中没有涵盖。现实世界中的配送服务,如 Uber Eats 和 DoorDash 的架构解决方案和业务需求,是复杂、现实场景的好例子。
这个解决方案的微服务有一个独立的数据库,这不仅符合微服务的理念,而且也带来了良好的封装水平,减少了因变更(例如,模式变更)引起的错误数量。
围绕中心数据集成的挑战
尽管所有四个服务都被设计成独立的,但它们都围绕一个关键功能运作:订单。并且出现了关于这个数据的问题:如何在四个服务之间管理和处理订单数据?
微服务的共享数据库
一些服务可以利用数据存储作为这些服务的集成层,拥有一个单一的模式,不仅包含订单详情,还包括支付、厨房和配送信息。不幸的是,这是一个不可取的解决方案,被称为共享数据库(也称为集成数据库)。图 3.6显示,在这种情况下,所有服务都依赖于一个单一的模式来维护订单信息:
图 3.6 – 共享数据库反模式
在先前的例子中,服务实现可能看起来很简单,因为它不需要处理集成方面。然而,解决方案中增加了多个问题,并破坏了设计原则:
-
微服务应该是解耦和独立的。
-
瓶颈和性能影响,以及意外的异常,如锁异常。
-
由于边界上下文不受尊重,涉及多个业务领域。
-
数据库的更改可能需要更改所有服务。
-
对同一数据进行操作的多项服务可能会导致不一致。
-
更高的错误和故障风险。例如,对某个服务所做的更改,其他所有服务都没有预料到。
考虑到上述问题以及更多问题,很容易看出这不是一个好的选择。
双写反模式
为了避免上述问题,我们可能会考虑拥有独立的服务,每个服务都有自己的数据库。然而,在这个解决方案中,服务在其数据库中不保留订单的副本,但它们也应该在它们的数据库和订单服务数据库中更新订单状态。
在图 3.6中,观察订单服务是独立的,并在其数据存储中维护订单数据。然而,其他服务依赖于在其自己的数据库中复制订单数据,并在两个数据库中维护订单状态——它们自己的和订单服务的:
图 3.7 – 双写反模式
亲爱的读者,这又是一个反模式:双写反模式。它带来了很高的可能性导致数据不一致和完整性问题,因为它无法确保两次写入都能成功完成,或者都不完成,就像在单一事务中一样。这在处理分布式数据系统、使用专用于分析的独占数据存储、实施专用的搜索索引工具和设计事件驱动解决方案(例如,将相同的数据写入数据库和 Kafka)等场景中是一个常见的错误。
首先明确,我们在分布式架构的数据访问和管理方面指出了两个红旗:
-
第一,一个服务不应直接更改任何由其他服务拥有和消费的数据,如图 3.7 中的箭头所示,其中所有服务都在修改订单服务的数据库中的数据
-
第二点是,一个服务不应负责或对多个数据存储解决方案(包括但不限于图 3.5中所示的服务间,以及图 3.6中所示的不同数据存储类型)的数据持久性和一致性进行操作和维护。
在第十章和第十一章中,深入探讨了反模式、用例以及数据集成的潜在解决方案和策略。目前,只需意识到在分布式数据服务集成中存在可能导致性能瓶颈、数据不一致和可靠性损失的反模式就足够了。
到目前为止,我们更好地理解了为什么集成是提供基于现代微服务解决方案的关键架构方面。接下来,让我们看看另一种通过依赖事件驱动架构进行异步数据集成的方式来整合分布式服务。
揭示数据集成中的变更数据捕获
服务集成可以是同步的或异步的,可以使用不同的机制和形式,例如基于文件、共享数据库、基于消息、基于 API(例如 REST、SOAP)、事件驱动等。为了本书的目的,我们将考虑事件驱动架构(EDA)的方面,因为它使得使用如变更数据捕获这样的数据集成模式成为可能。
事件驱动模式相关的技术被创建出来,以便数据库——即使是传统的数据库——能够拥有新的能力:发出事件。您读对了;传统的关系型数据库(以及其他数据库)可以超越基础功能,并允许开发者依赖变更数据捕获。
使用变更数据捕获,数据库操作可以被捕获并由解决方案的数据库和微服务之外的组件作为事件发出。有了这个,开发者可以创建能够对上述数据事件或“通知”做出反应和响应的事件驱动服务。
如您所预期的那样,EDA 并非全是阳光和玫瑰。当涉及多个服务和大量事件时,在这种架构风格中理解整个业务流程中发生的事情可能会相当令人不知所措。故障排除也可能极其复杂,因为跟踪过程不是线性的,并且不会在唯一的交易中发生。当与 EDA 一起工作时,请忘记自动回滚。
尽管每个提到的挑战都可以被解决或减轻,但请注意,这些只是 EDA 潜在缺点列表中的一部分;因此,不要忘记对特定场景进行评估,并验证 EDA 是否是最佳解决方案。
了解 EDA 提供的集成优势对于在不破坏模式、最佳实践和推荐的情况下集成您的服务至关重要,并且对于确保您能够获得异步、高度可扩展集成的益处至关重要。
摘要
在这一点上,我们已经探讨了云计算技术对软件架构设计的影响以及设计模式作为软件架构师构建块的重要性。我们比较了单体架构和微服务架构,揭示了它们的优缺点。
我们还探讨了云部署策略,如混合云和多云模型,以及这些策略如何与如 DbaaS 之类的托管服务相结合,以加快有状态解决方案的开发和交付。另一方面,我们也发现了我们在分布式系统中如何集成数据可以直接影响数据管理和使用。当集成分布式有状态服务时,我们现在知道我们必须谨慎使用如共享数据库和“双重写入”之类的反模式。
在本章末尾,我们揭示了 Change Data Capture 在 EDA 中用于数据集成的潜力,这通过增加架构复杂性(更多组件和技术)来换取完全解耦和异步的集成。
在讨论了架构和部署模型选择之后,我们将进一步深入探讨利用设计模式进行云原生应用数据管理的策略,基于本章奠定的基础。
第四章:云原生应用程序中的数据管理设计模式
无论选择单体架构还是微服务架构的原则,我们当然应该期望通过引入另一种软件设计模式——分层架构软件设计模式来提高每个服务的质量。最近,云原生这个词变得相当流行,并且讨论很多,它描述了一组通过使用容器、编排和自动化来优化云应用程序的最佳实践。
这种方法建议在独立的层中设计和服务组织,每一层拥有特定的责任和明确的接口。更好的抽象和隔离特性的潜在保证是所需额外源代码及其聚合代码设计复杂性的回报。
在探索分层架构模式对健康应用程序至关重要的原因时,特别是关于持久性集成和数据操作,本章将准备并引导你完成服务设计转型的旅程。你将从熟悉一组关键的应用层设计策略开始,这些策略将技术性地解释和演示一个无结构的应用程序,没有任何抽象级别,如何被转换成一个优雅设计的由适当层组成的服务,这些层能够提供良好的分离和隔离,以区分持久性实现的技术细节和业务环境。
在对每一层设计策略的比较分析中,我们将讨论错误地将核心理念推向极端所带来的利弊。在上述坚实的背景之上,你将通过详细的代码示例了解每一层存在的原因,并能够确定何时利用它们提供的绝佳机会。
持久性解决方案的质量正是本章动机的核心。由于前几章侧重于更广泛的解决方案架构、集成和部署模型,我们应该更深入地研究实现单个服务。我们需要考虑将数据相关模式与其他流行实践(如领域驱动设计(DDD))相结合的强大成果。最后,但同样重要的是,我们必须讨论框架的质量;毕竟,大多数 Java 解决方案都强烈依赖于框架。我们必须,并且在本章中,我们将揭示实际的框架实现策略,直至评估某些框架特性(如作为反射或无反射技术构建)的影响。
内容将在以下部分进行分解和讨论:
-
将设计模式应用于 Java 持久层
-
探索 Java 映射景观——评估框架权衡
-
视图层与底层之间的数据传输
技术要求
-
Java 17
-
Maven
-
Git 客户端
-
一个 GitHub 账户
展示的代码示例可在github.com/PacktPublishing/Persistence-Best-Practices-for-Java-Applications/
找到。
应用到 Java 持久化层的设计模式
我们作为软件工程师,经常讨论和采用分层架构解决方案,但为什么?为什么我们应该考虑使用这种代码风格?它有哪些相关的权衡?为了更好地理解代码设计模式,我们将通过一个简单任务的场景来展示:从数据库中存储和检索数据——更具体地说,是一个管理书籍及其相关数据的图书馆系统。乍一看,我们的任务看起来相当直接,对吧?让我们开始吧。
首先,我们看到需要创建一个实体,一个Book
类,我们可以用它来处理图书馆的领域——我们的业务领域。我们可以假设的第一个特征是,我们的Book
实体应该具有title
(标题)、author
(作者)、publisher
(出版社)和genre
(类型)这些属性。
以下代码示例表示所描述的Book
类。请注意,所有字段都被设置为final
以实现不可变性的假设。为了使开发者能够创建此类实例,Book
类提供了一个constructor
方法和一个builder
类(为了简洁已省略):
public class Book { private final String title;
private final String author;
private final String publisher;
private final String genre;
// constructor method
// builder inner class
}
第一个实体Book
被实现为一个不可变类。
实例变量被设置为final
。因此,在对象初始化后无法更改它们的值。请注意,也没有 setter 方法。如果您对内部类的详细实现感兴趣,请参阅Book
类的实现(github.com/architects4j/mastering-java-persistence-book-samples/blob/e594bb17eab3dc97665b495b4245312bfd0f421b/chapter-04/src/main/java/dev/a4j/mastering/data/Book.java#L14-L66
)。
为了模拟从数据库到数据库的序列化,我们将使用类型为Map
的Map
内存对象db
:Map<String, Map<String, Object>> db
:
import java.util.HashMap;import java.util.Map;
import java.util.Objects;
import java.util.Optional;
public enum Database {
INSTANCE;
private Map<String, Map<String, Object>> db = new
HashMap<>();
public Optional<Map<String, Object>> findById(String id) {
Objects.requireNonNull(id, "id is required");
return Optional.ofNullable(db.get(id));
}
public Map<String, Object> insert(String id,
Map<String, Object> entry) {
Objects.requireNonNull(id, "id is required");
Objects.requireNonNull(entry, "entry is required");
db.put(id, entry);
return entry;
}
public void delete(String id) {
Objects.requireNonNull(id, "id is required");
db.remove(id);
}
public Map<String, Object> update(String id,
Map<String, Object> entry) {
Objects.requireNonNull(id, "id is required");
Objects.requireNonNull(entry, "entry is required");
if (findById(id).isEmpty()) {
throw new IllegalArgumentException("The
database cannot be updated");
}
return entry;
}
}
内存数据库并不复杂,也不涵盖任何并发情况,但它简单,可以更多地关注层。
注意
核心示例的目标是评估数据库层,如 JDBC,因此我们不会涵盖竞争条件和其他现实生活中的挑战。
为了保持我们对实体映射和代码设计的关注,我们的模拟内存数据库
仅支持四个创建、读取、更新和删除(CRUD)操作。
在继续实现的过程中,下一步将是实现每个 CRUD 数据库操作。记住,在我们场景的起点,我们目前是没有分层的生活;因此,我们所有的方法都应该位于同一个类中。
接下来,我们将查看我们提到的非结构化方法,然后是它与使用数据映射器、数据访问对象(DAO)、仓库和活动****记录模式实现的相同解决方案的比较。
非结构化代码
我们场景的旅程始于设计一个单层应用程序。这个层是应用程序将依赖它来使用插入书籍、将书籍的底层表示作为数据库模型从/转换为 Java 域对象以及允许查询书籍实例的操作来操作书籍数据。嗯,有一个好消息:我们所有需要的都在一个集中的地方/文件中。当最终需要维护请求来定位和修改数据库模型的字段或更新实体的方法逻辑时,不应该有任何惊喜或痛苦——它们位于同一个地方。
随着这个应用程序的功能增长和类的长度增加,越来越难以识别哪些代码在做什么。正如我们在现实世界应用程序中反复注意到的那样,不幸的是,这种复杂性最终肯定会导致不必要的代码重复。这对于具有许多实体类的应用程序尤其如此。
回到代码,接下来是一个代码实现,它实例化了一个新的书籍对象,并使用我们自制的数据库客户端来操作书籍数据:
-
使用 CDI 机制和其构造方法实例化 Java 的域对象
Book
。 -
对象的属性映射到它们各自的数据库模型属性。
-
使用 CDI 创建或检索
database
客户端实例。 -
使用
database
客户端的 API 保存书籍;持久化的信息由实际的 Java 模型属性引用加上手动设置的数据库表示entry
组成。 -
通过其 ID -
title
- 从数据库检索书籍信息,并存储在类型为Map
的数据库模型表示中 - 不是类类型Book
。 -
使用构建器,从检索到的数据中创建了一个
Book
对象实例:
Book book = BookSupplier.INSTANCE.get(); // 1// – - 2 – -
Map<String, Object> entry = new HashMap<>();
entry.put("title", book.getTitle());
entry.put("author", book.getAuthor());
entry.put("publisher", book.getPublisher());
entry.put("genre", book.getGenre());
// - - - -
Database database = Database.INSTANCE; // 3
database.insert(book.getTitle(), entry); //4
Map<String, Object> map = database.findById(book.getTitle())
.orElseThrow(); // 5
Book entity = Book.builder()
.title((String) map.get("title"))
.author((String) map.get("author"))
.publisher((String) map.get("publisher"))
.genre((String) map.get("genre"))
.build(); // 6
System.out.println("the entity result: " + entity);
有些人可能会觉得这段代码很容易处理。然而,也容易预测其对长期支持的影响。更多的代码使得维护更容易出错,结果是现在代表风险的应用程序,不仅对组织的正常运作,而且对业务,更不用说多方面的技术影响。
作为软件开发者,我们可能都遇到过(甚至是我们自己设计的)由于设计选择不当而变得越来越难以维护和修改的系统。罗伯特·马丁(又称 Uncle Bob),在他的一个演讲中,将软件“腐烂设计”的四个迹象命名为:刚性、脆弱性、不可移动性和粘滞性。这四个迹象的解释如下:
-
刚性:软件改变的倾向
-
脆弱性:软件每次更改时在许多地方崩溃的趋势
-
不可移动性:无法从其他项目中重用软件
-
粘滞性:当我们需要修改代码时,API 使得代码更难以破解
记得我们提到过,在之前的库示例中可能会出现重复吗?这是因为改变代码比复制它更困难。可预测的结果违反了单一职责原则(SOLID设计原则之一)和复杂的测试场景。毕竟,你怎么能坚持测试金字塔的测试实践(见图 4.1)?
图 4.1 – 测试金字塔
注意
我们可以在讨论中的代码设计和无结构的单体(见构建进化式架构)之间绘制一条比较线;两者都有向增加复杂性和难以移动的架构发展的趋势——就像一个“大泥球”。
当谈到持久性时,还有一些其他的事情需要考虑,我们想强调:
-
你选择的设计将影响在数据库范式之间更改时所需的努力程度。例如,更改持久性提供者(如从 SQL 切换到 NoSQL)可能是一项艰巨的任务。
-
如果你寻求采用金字塔测试方法的好习惯,层与层之间的高耦合使得与集成测试相比,准确编写适当数量的单元测试变得困难。记住,使用脚本或小型工具进行持久性存储在短期内可能是值得的;问题是,它也可能在长期内变成一场噩梦。
-
使用更多层可能会有优势。例如,你将能够将业务逻辑从技术特定性中抽象出来。除了常见的模型-视图-控制器(MVC)基本层之外,还可以考虑在模型和数据库之间添加一个额外的抽象层,尤其是在使用三层架构时。
与具有三个独立层的 MVC 不同,在不规则的代码设计中,客户端可以直接访问数据库。这并不是关于这是一个好方案还是坏方案的问题,而是关于突出权衡。这种方法在创建简单/快速的迁移脚本或其他不会长期存在或预期不会增长的代码时可能是有用的。以下图表说明了这种设计:
图 4.2 – 非结构化代码设计中的客户端-数据库集成
如前所述,这个模型很简单,但随着解决方案规模的扩大,我们可能会遇到包括在数据库和业务实体之间转换的样板代码在内的重复代码。为了解决这些问题,我们将创建一个第一层来集中映射转换在一个地方,并在客户端和数据库之间建立边界。
数据映射模式
下一步是创建客户端应用程序和数据库之间的第一层。这个层是一个很好的机会来减少样板代码,从而最小化错误——代码越少,错误越少。在先前的示例应用程序中,你可能已经注意到映射域和操作数据的整个操作是单个块的一部分,这可能会使其难以阅读、维护和测试。
在书籍《Just Enough Software Architecture: A Risk-Driven Approach》中,我们了解到考虑这些威胁对设计能力的重要性,并使用三种武器来对抗复杂性和风险:分区、知识和抽象。
在这个例子中,我们将使用抽象来隐藏技术细节,并将它们集中在单个位置。以下是我们可以这样做的方法:让我们引入我们的危险层。虽然一层可以帮助隔离和抽象功能,但它也增加了更多的代码。这是我们做出的权衡。
数据库和 Java 领域模型之间的转换也应该发生,并且随着实体的增加,它将变得更加频繁。在这个第一步中,让我们使用数据映射模式在抽象层中抽象这个转换过程。
BookMapper
类将集中化转换行为在单个位置:层。从现在起,如果转换中存在错误,这是检查任何实体或数据库相关代码更改的类:
class BookMapper { private Database database = Database.INSTANCE;
public Optional<Book> findById(String id) {
Objects.requireNonNull(id, "id is required");
return database.findById(id)
.map(entity());
}
private Function<Map<String, Object>, Book> entity() {
return (map) ->
Book.builder()
.title((String) map.get("title"))
.author((String) map.get("author"))
.publisher((String)
map.get("publisher"))
.genre((String) map.get("genre"))
.build();
}
private Function<Book, Map<String, Object>> database() {
return (book) -> {
Map<String, Object> entry = new HashMap<>();
entry.put("title", book.getTitle());
entry.put("author", book.getAuthor());
entry.put("publisher", book.getPublisher());
entry.put("genre", book.getGenre());
return entry;
};
}
}
如前所述,BookMapper
集中化数据库模型和应用实体模型的映射操作。市场上存在几个有效的框架可以执行此类映射任务,例如流行的选项BookMapper
有一个更直接的方法:它使用 Java 函数来封装和执行这些转换。
信息 - Java 函数
Java 函数是一种封装代码片段的方法,可以在整个应用程序中重复使用。它们使用public static
关键字定义,后跟返回类型、函数名和括号内的参数列表。函数可以使代码更加组织化,更容易阅读,并通过消除重复编写相同代码的需要来节省时间。
看看我们如何使用BookMapper
操作:
Book book = BookSupplier.INSTANCE.get();BookMapper mapper = new BookMapper();
mapper.insert(book);
Book entity =
mapper.findById(book.getTitle()).orElseThrow();
System.out.println("the entity result: " + entity);
之前的示例代码通过使用 Mapper
类引入了转换过程。通过这样做,我们将转换操作从该方法中抽象出来,移动到 BookMapper
类。由于封装,客户端不知道翻译过程是如何进行的 – 太棒了!
虽然这是一个积极的步骤,但仍然需要改进,因为客户端仍然负责调用转换操作。虽然我们可以测试转换过程,但客户端与技术之间的高度耦合仍然是一个担忧。
为了解决这些问题,我们的下一个设计包括添加一个 映射层,这将减少客户端和数据库之间的摩擦。这个映射器将被反复使用,使其成为 JPA 或 Hibernate 等框架操作的好候选。
总体而言,引入这个映射层将帮助我们提高解决方案的灵活性和可维护性,同时减少复杂性(见 图 4**.3):
图 4.3 – 映射层 – 代码设计现在有一个额外的抽象层
虽然映射层确实使客户端的工作变得更简单,但它仍然要求客户端对数据库细节有所了解。这可能是个问题,因为它在实现映射器和其操作时可能会引发错误。如果我们能找到一种方法来降低这种风险怎么办?关于创建一个新层,但这次让它作用于整个数据库操作呢?
让我们介绍 DAO 模式!它将使我们能够减轻客户端的负担,并最大限度地减少实现错误的可能性。
DAO 模式
DAO 模式是一种将应用/业务层与持久化层分离的结构方式。其主要目标是抽象整个数据库操作从 API 中。
通过将所有操作封装在一个类或接口中,API 可以在需要时随时更新,而不会影响持久化数据实现。这在长期系统中特别有用,因为 DAO 实现可能需要更改。
BookDAO
引入了插入和检索 Book
的合约。作为此接口的客户,你不需要了解其内部工作方式。这使得代码更安全,因为将数据库过程集中在一个地方。现在 BookDAO
将是处理数据库映射器的人:
public interface BookDAO { Optional<Book> findById(String id);
void insert(Book book);
void update(Book book);
void deleteByTitle(String title);
}
DAO 具有命令式风格,这意味着具体操作由客户端定义。例如,如果你正在使用 API 并想更新一本书,你必须确保这本书存在;否则,你会抛出异常。如果你熟悉从之前的 Java EE 中的 JPA,你可能会考虑在这个项目中将 EntityManager
抽象化。在这个例子中,我们将在 DAO 层中使用映射操作:
public class BookMemory implements BookDAO {//..
@Override
public void update(Book book) {
mapper.update(book);
}
//…
}
DAO 模式由微软在 Visual Basic 中推广,后来通过 Sun 组织在 Java 中推广。它也在早期的《核心 J2EE 模式》一书中提到。它包括方法的名称,但目标是使用抽象来隔离数据库,所以无论你使用 SQL、NoSQL 还是任何服务,都无关紧要。
从权衡的角度来看,我们得到了隔离和更好的可维护性,并且如果需要,我们可以通过模拟 DAO 来测试服务单元。但是,请记住,因为它通常是一个命令式 API,所以确保客户端在使用正确的方法(例如更新或插入)在正确的情况下取决于客户端:
Book book = BookSupplier.INSTANCE.get();BookDAO dao = new BookMemory();
dao.insert(book);
Book entity = dao.findById(book.getTitle()) .orElseThrow();
System.out.println("the entity result: " + entity);
使用 DAO 模式,从现在开始,一个消费BookDAO
的图书客户端与图书交互时,无需意识到数据库转换过程。
通过抽象数据库操作,我们的客户甚至不需要了解映射操作,我们可以在持久化方面隔离一些事情。然而,客户仍然需要意识到数据操作。图 4.4显示了客户端被移动或进一步抽象,远离数据库的新层:
图 4.4 – 使用 DAO 模式进行的前置设计为数据库集成带来了更多的抽象
从客户端的角度来看,与客户端必须处理整个流程(包括数据库和实体模型转换,以及数据操作本身)的初始阶段相比,这是一个改进。但是,如果客户端尝试插入两次或更新不存在的信息,我们仍然会得到抛出的异常。这可能是一些情况下没有意义的数据库细节。那么,我们如何去除这些细节并更多地关注业务呢?这就是我们在下一节中通过存储库模式和领域驱动设计(DDD)实践要探讨的内容。
由 DDD 推动的存储库模式
存储库是 DDD 中的一个模式,它侧重于业务视角,并抽象出存储和基础设施细节。作为使用此 API 的客户端,我们不需要担心任何实现细节。主要关注的是通用语言。
DDD 和通用语言
在 DDD 中,“通用语言”的概念指的是一个共享语言,开发团队的所有成员都使用它来沟通领域模型。这种语言通过确保每个人使用相同的术语来指代相同的概念,有助于提高沟通并减少误解。它是 DDD 过程中的一个重要部分,应该在软件项目的开发过程中得到培养和改进。
回到我们的图书示例,让我们首先创建一个接口来处理Library
图书集合。Library
应该能够保存图书,通过标题查找图书,并在适当的时候注销图书。
Library
合约将完成这项工作,客户端甚至不知道实现是否会实际插入或更新一本书。客户端的需求是保存一本书;从技术角度来看,如果它是一本书,则进行插入,如果它已经存在,则进行更新。Library
接口将如下所示:
public interface Library { Book register(Book book);
Optional<Book> findByTitle(String title);
void unregister(Book book);
}
接口合约使用一种通用的语言,这种语言更接近业务语言,并包含与其操作相关的方 法。作为一个客户端,我不想关心数据是如何存储的或它从哪里来。如果你是 Java 开发者,你可能熟悉实现仓库模式的框架,例如使用save
方法来执行数据库操作。
这个框架允许使用 DDD 实践吗?
一些框架使用仓库接口方法,但并非所有框架都遵循 DDD 实践。你可以很容易地检查一个框架是否遵循 DDD 实践:查找插入和更新方法,例如在 Quarkus 框架和 JPA 与 PanacheRepository 中。
DAO 模式和仓库模式实现之间的主要区别是客户端和数据库之间的距离,通常称为邻近性。虽然 DAO 暴露了持久层的功能,但仓库倾向于具有面向业务的功能暴露。
我们的Library
实现将使用在BookDAO
类上实现的 DAO 层。我们的DAO
已经准备好了映射转换操作和数据库操作。以下代码通过register
方法展示了如何使用 DAO 的insert
和update
方法:
public class LibraryMemory implements Library { private final BookDAO dao;
public LibraryMemory(BookDAO dao) {
this.dao = dao;
}
@Override
public Book register(Book book) {
Objects.requireNonNull(book, "book is required");
if(dao.findByTitle(book.getTitle()).isPresent()) {
dao.update(book);
} else {
dao.insert(book);
}
return book;
}
@Override
public Book unregister(Book book) {
Objects.requireNonNull(book, "book is required");
dao.deleteByTitle(book.getTitle());
return book;
}
@Override
public Optional<Book> findByTitle(String title) {
Objects.requireNonNull(title, "title is required");
return dao.findByTitle(title);
}
}
现在,让我们看看客户端代码。从客户端的角度来看,我们可以在注册一本书时注意到主要的抽象——名为register
的业务导向操作通过将更新或插入的技术决策委托给底层实现而简化。
框架和映射模式
有几个框架可供使用,以帮助简化 Java 开发者实现映射层的工作。一些例子包括 Spring Data、Micronaut、Quarkus 以及 Jakarta Data 规范。
以下展示了仓库客户端实现注册一本书的过程:
Book book = BookSupplier.INSTANCE.get();Library library = new LibraryMemory(new BookMemory());
library.register(book);
Optional<Book> entity =
library.findByTitle(book.getTitle());
System.out.println("the entity result: " + entity);
通过将前面的仓库作为客户端,无需实现任何有关从何处获取这些数据的细节。这简化了并专注于业务需求——注册一本书和通过其标题查找它。然而,这也带来了一定的代价。即使在使用框架的情况下,增加更多层也有其权衡,例如增加 CPU 消耗和更多位置,这些都可能是最终出现 bug 的潜在原因。以下图示显示我们在数据库和业务域之间添加了另一个层:
图 4.5 – 使用仓库模式的预先设计
再次,我们必须面对软件设计的困境——在这里没有正确或错误答案,只有权衡。一方面,我们可以尽可能地将数据库移开,简化客户端实现。另一方面,我们可能走得太远,在试图简化事情的同时,最终导致实体和数据库操作紧密集成。
在这次旅程的下一站和最后一站,我们将讨论活动记录模式。
活动记录模式
活动记录是一种减少在模型中使用数据库操作复杂性的方法。马丁·福勒在他的 2003 年著作《企业应用架构模式》中定义了它。接下来是我们的下一个目的地——我们将结合实体及其数据库操作。
这种模式背后的想法是通过在 Java 中使用继承来拥有一个扩展Model
类的实体。这给实体带来了像拥有超能力的模型一样的数据库能力:
public class Book extends Model { private final String title;
private final String author;
private final String publisher;
private final String genre;
}
但是,权力越大,责任越大。这种模式的主要好处之一是简单性。如果你从 MVC 的角度来看,模型将同时持有与业务相关的逻辑和数据操作逻辑。在我们的代码示例中,Book
类能够执行多个数据库操作,例如插入、更新、删除和按 ID 查找。以下代码显示了客户端的实现代码,它可以创建书籍并使用insert
方法:
Book book = ...;book.insert();
Book model = Book.findById(book.getId());
这种模式在某些情况下是有意义的,尤其是在简单的应用程序中。但就像其他任何解决方案一样,这并不是万能的。这种模式有其自己的担忧,比如违反 SOLID 的单一职责原则。一些 Java 框架依赖于这种模式,例如与 Quarkus 一起使用的 Panache、ActiveJDBC 和 ActiveJPA。
讨论层和抽象可能是一个相当大的话题,因为你的决定可能会产生积极和消极的后果。
现在我们已经看到了设计持久性集成层的不同方法,我们将继续分析框架在底层是如何工作的,并了解在选择持久性框架技术时,哪些特性可以被赋予更高的权重。
探索 Java 映射领域——评估框架权衡
你现在可以理解使用层的动机了。我们有一个成熟的 Java 生态系统,不需要手动做所有事情,这真是太好了——多亏了框架。由于框架众多,我们可以根据 API 可用性、邻近性和运行时对它们进行分类。
-
可用性:在查看框架时,评估其 API 的可用性是一个需要考虑的项目。例如,你可以问这样一个问题:“我们能在多少种不同的数据库中使用相同的 API?这是否 可能?”
-
无偏见 API: 一个 API 可以与多个数据库供应商、类型或范式一起使用。这种积极的一面是,无偏见 API 减少了认知负荷,因为您不需要为每个不同的数据库集成学习新的 API。然而,您可能会失去特定的数据库行为或需要更长的时间才能收到功能更新和错误修复。
-
特定 API: 无偏见 API 的对立面是每个数据库都需要一个专门的 API——换句话说,每个数据库一个 API。提供不断更新的版本以支持用户与目标数据库提供商的最新版本集成。幸运的是,它可能具有更少的层和更好的性能;不幸的是,当处理多语言持久性时,认知负荷可能更难管理。
-
-
邻近性: 框架与数据库存储引擎有多接近?
-
通信: 更接近数据库,远离领域模型;这使数据驱动设计成为可能,但可能会有更多的样板代码。
-
映射: 更接近模型,远离数据库;这使 DDD 成为可能并减少了样板代码,但远离数据库可能会导致忽略数据库侧的最佳实践。
-
-
运行时: 这主要影响依赖于注解使用的映射框架。
-
反射: 这个框架探索 Java 中的反射,这允许有更多的灵活性和运行时插件的多样性。然而,启动时间较慢,应用程序消耗大量内存来执行读取元数据的进程。
-
无反射: 这种类型的框架避免了反射,使启动更快、更经济。然而,元数据处理发生在构建时间而不是运行时,导致构建和打包过程更长,框架在实时探索时灵活性较低。
-
总之,有各种各样的 Java 映射框架可供选择,每个框架在 API 可用性、与数据库实现细节的邻近性和运行时能力方面都有自己的权衡。考虑您项目的具体需求并选择最适合这些需求的框架是很重要的。
现在我们已经将我们的“一站式”类拆分,简化了客户端实现,减少了开发错误的可能性,并认可了从市场上众多选项中我们可以选择的框架类型,我们如果不讨论从数据角度出发的视图和控制器层(MVC 的视图和控制器层),就无法继续前进。在下一节中,我们将探讨在使用数据传输对象(DTOs)时,如何处理视图层和底层之间传输的数据。
视图层和底层之间的数据传输
在本章中,我们讨论了应用层对于开发的重要性以及它们如何影响项目的可维护性和复杂性。我们还探讨了应用程序的模型及其在 MVC 架构中与数据库的关系。但是等等……当涉及到视图和控制器(MVC)时,对数据库集成及其性能是否有潜在的影响?
答案是是的。让我们更深入地看看从表示层到底层的数据传输如何对你的解决方案产生益处或影响。
大多数时候,当开发人员决定在客户端使用数据模型时,可能会出现以下挑战:
-
以
Book
为例——可以直接影响视图层并需要对其进行修改。 -
Library
示例,暴露敏感数据,如书籍的价格,将不是一件好事。在更具体的场景中,假设你正在开发一个社交媒体 API 的客户端消费者——通过 ID 查找用户并暴露所有非敏感和敏感信息,包括用户的密码等,将是不被接受的!强烈建议只分享必要的信息——不是所有信息都应该对客户端*可见。 -
代码演变和版本控制:在典型场景中,代码的一部分是不断演变的,而另一部分,即遗留部分,必须得到维护。在这种情况下,如果新的功能需要修改视图层内部使用的模型,可能会破坏这个集成中的遗留模型部分。
-
为了处理旧代码和当前代码之间的模型差异,一种方法是用版本控制。通过为视图(即客户端)中使用的模型类进行版本控制,使得通过不同的类提供相同的模型成为可能,并能够创建不同的视图,每个视图都有其相应的适配器。
考虑到这种方法中存在的问题,结论是,通过表示层传递信息的解决方案是将模型与视图和控制器分离。这时,DTO 模式就派上用场了。
回顾 DTOs
DTO 是一种设计模式,它促进了系统层或组件之间的数据传输。它可以用来解耦表示层和业务逻辑,增加应用程序的灵活性和可维护性。这些简单的对象包含数据但没有关联的业务逻辑——它们是数据在视图中显示的简单表示。
DTOs 代表实际领域模型的不同视图。例如,一个 DTO 可以只包含需要展示的信息所需的基本信息子集。总之,DTO 模式具有以下优点:由于业务逻辑和数据库逻辑之间的分离,实现了模型简化;由于数据库调用次数减少,提高了性能;通过防止通过暴露敏感属性泄露数据,增强了安全性。
然而,也可能看到潜在的缺点,例如随着层数和类数量的增加导致的更高复杂性,由于对模型信息的访问受限导致的降低灵活性(可能需要但未公开),以及由于在 DTO 和模型之间的映射上增加的处理导致的降低性能。
必须牢记,隔离是关键,过多的代码会增加复杂性并影响性能。
创建 DTO 可能意味着大量的工作,尤其是在手动实现时。幸运的是,如果您认为 DTO 模式适合您的项目,市场上有一些框架可以使您的生活更加轻松。例如,模型映射器(modelmapper.org/
)和 MapStruct(mapstruct.org/
)可以促进并加快实现过程。
我们不会深入探讨表示层和 DTO 模式。不过,我们想提醒您,也要对视图空间保持谨慎,因为关注的点不仅仅是持久性——一个例子就是可视化。
摘要
层层叠叠,更多层——有时,它们是出色的盟友,帮助分担责任,减少和集中开发错误风险,并促进采用 SOLID 的单职责原则。然而,过多的层可能会适得其反,增加代码设计的复杂性。何时应该添加或移除新层?答案将隐藏在每个应用程序的上下文挑战、技术需求和业务需求中。
通过代码演示的旅程,我们探索了多种模式,从无结构、零层应用设计到多种多层级设计采用和面向业务简化技术。在这段旅程中,我们了解了在软件应用程序中将数据库从客户端抽象出来的层的使用优势和劣势。
此外,我们明确指出,对于开发人员和架构师来说,持久层还有更多需要关注的地方,并且我们将在层视图上可视化和交互数据的方式也应被视为一个可能受到我们如何设计持久性解决方案影响的层。
理解应用程序的需求和上下文是确定应用于数据库集成最佳模式的关键,以及最佳的成本效益抽象和隔离水平。有了这些,我们就准备好了解和探索通过 Jakarta EE 和 MicroProfile 规范提供的 Java 企业标准。在下一章中,我们将向您介绍两个与持久性相关的规范,这些规范可以解决迄今为止提到的多个挑战,并深入探讨探索企业级和微服务 Java 应用程序空间的力量。
第二部分:Jakarta EE、MicroProfile、现代持久化技术及其权衡
在本书的这一部分,我们探讨了 Jakarta EE、MicroProfile 和现代持久化技术的交汇点。我们深入分析了不同持久化方法所涉及的权衡,为在 Java 持久化动态环境中航行的开发者提供了宝贵的见解和实用的指导。
本部分包含以下章节:
-
第五章**,Jakarta EE 和 JPA:现状分析
-
第六章**,Java 中的 NoSQL 揭秘:一统天下的 API
-
第七章**,jOOQ 采纳指南之缺失部分
-
第八章**,使用 Eclipse Store 的超快内存持久化
第五章:Jakarta EE 和 JPA – 状况
Java,无论是语言还是平台,在提供有效的开发体验和创建高性能的持久化应用方面都取得了显著进步。当检查 Java 数据库集成能力和开发体验的演变时,这些显著的改进变得显而易见:回顾一下 1.1
的引入,并将其与 Jakarta 企业版(Jakarta EE)、MicroProfile 提供的最现代体验进行比较。
本章介绍了 Java 进化和持续增长的主要推动者——Jakarta EE(以前称为 Java EE)和 Eclipse MicroProfile 的现在和未来。一方面,Eclipse 基金会和 Jakarta EE 针对企业组织对稳定性和可靠性的需求。另一方面,有 Eclipse MicroProfile,它具有快速交互和持续创新。同时,Jakarta EE 平台也在不断发展和采用全面的 MicroProfile 技术。所有这些都在你阅读的同时发生,因此现在是时候最终理解开放社区正在发生的事情,以及从数据解决方案的角度可以期待什么了。
寻求提供可扩展企业解决方案的 Java 工程师,这些解决方案能够平滑地启用分布式架构,通常依赖于一个能够支持云原生和传统解决方案的平台。在商业世界中,鉴于 Java 的长期采用,经常需要灵活的技术,这些技术能够在不放弃提供新的云原生解决方案机会的情况下,最大限度地发挥现有技术和基础设施的潜力。在这种情况下,Jakarta EE 平台是一个很好的选择。
Jakarta EE 的规范非常庞大,影响了整个 Java 社区;重要的是要强调,如果你在使用 Spring、Micronaut 或 Quarkus,即使间接地,你也在使用 Jakarta EE。在本章中,我们将检查 Jakarta EE 覆盖的规范。
本章我们将涵盖以下主题:
-
Jakarta EE 概述
-
框架揭晓——反射与非反射解决方案
-
Java 持久化 API(JPA)状况
-
Quarkus 和 Panache 云原生运行时带来的 JPA 力量
-
一般 JPA 相关性能考虑
技术要求
对于本章,你需要以下内容:
-
Java 17
-
Git
-
Maven
-
任何首选的集成开发环境(IDE)
本章的代码可以在以下 GitHub 仓库中找到:
github.com/PacktPublishing/Persistence-Best-Practices-for-Java-Applications/tree/main/chapter-05
Jakarta EE 概述
Jakarta EE 的核心是其一系列规范,每个规范都针对企业架构的特定方面。这些通常被称为“EE 规范”的规范旨在涵盖企业应用程序开发中遇到的各种用例。它们提供了实施关键功能的标准化方法和指南,确保在不同实现之间具有互操作性和可移植性。
Jakarta EE 规范满足企业架构的广泛需求,包括以下方面:
-
Web 应用程序: Jakarta Servlet 规范提供了一个平台无关的 API,用于构建 Web 应用程序。它定义了如何处理 Web 请求和响应,允许开发者创建动态、交互式、安全的基于 Web 的解决方案。
-
企业集成: Jakarta 消息(JMS)规范提供了一个消息系统,使分布式应用程序组件之间的通信无缝。它确保了可靠和异步的信息交换,促进了不同系统之间的集成。
-
持久性: JPA 规范通过提供一个 对象关系映射(ORM)框架简化了数据库访问和操作。它允许开发者使用 Java 对象与关系数据库进行交互,抽象了底层的 SQL 操作。我们还可以包括 Jakarta Bean Validation 规范来定义由注解驱动的 Java 约束;此外,新的规范正在出现以支持 NoSQL 和 领域驱动设计(DDD)存储库。
-
依赖注入(DI): Jakarta 上下文依赖注入(CDI)规范通过管理对象创建、连接和生命周期管理来促进松散耦合并推动模块化开发。它使得在应用程序中轻松集成不同的组件成为可能,增强了可维护性和可测试性。
-
安全: Jakarta 安全规范提供了一套全面的 API 和服务,用于保护企业应用程序。它提供了身份验证、授权和数据保护机制,帮助开发者构建安全的应用程序并保护敏感信息。
-
RESTful 网络服务: Jakarta RESTful 网络服务(JAX-RS)规范简化了使用 表示状态转换(REST)架构风格开发网络服务的过程。它提供了一套注解和 API,用于构建可扩展、轻量级和互操作的网络 API。
下面的图表展示了 Jakarta EE 10 API 的概述,其中你可以看到大量可以帮助你作为软件工程师的规范;图表的另一个区域与配置文件相关。目前,根据你的需求,你可以使用三个配置文件:
图 5.1 – Jakarta EE 10 规范
这些只是 Jakarta EE 中广泛规范的一小部分示例。每个规范都针对特定需求,确保开发者拥有解决各种企业架构挑战的工具和指南。
通过遵循 Jakarta EE 规范,开发者可以在不同的应用程序服务器和供应商之间创建可移植的应用程序,从而实现灵活性和可扩展性。这些规范促进了互操作性和兼容性,使得与其他系统和服务的无缝集成成为可能。
这些规范将帮助您了解现代概念、方法和架构模型。在 Jakarta EE 10 API 中,我们有 CDI Lite 规范,其目标是减少反射,但反射有什么问题呢?在下一节中,我们将更详细地讨论这个问题。
框架揭晓——反射与非反射解决方案
Java 框架对于简化并加速应用程序开发至关重要,它们通过提供可重用组件、预定义结构和标准方法来实现这一点。这些框架封装了常见功能性和设计模式,使开发者能够专注于业务逻辑,而不是底层实现细节。
Java 编程和许多 Java 框架中的一个基本概念是反射。反射允许程序在运行时动态地检查和修改其结构和行为。它提供了一种检查和操作类、接口、方法和字段的方法,即使它们在编译时未知。
反射对开发者来说至关重要,以下列出了一些原因:
-
动态代码执行:反射允许开发者动态地实例化类、调用方法和访问字段。这种灵活性使得创建灵活、可扩展和可定制的应用程序成为可能。例如,Spring 和 Hibernate 等框架严重依赖反射来动态创建和连接依赖项、执行数据映射和处理应用程序行为的各个方面。
-
元数据提取:反射允许提取与类、方法和字段关联的元数据。这些元数据可能包括诸如注解、修饰符、泛型类型和方法签名等信息。通过分析这些元数据,开发者可以实现高级应用程序功能和行为。例如,JUnit 等框架使用反射根据注解发现和执行测试用例。
-
框架和注解:Java 框架通常利用注解,即在类、方法或字段上添加的标记,以提供额外信息或配置特定行为。例如,Spring、JPA 和 Java Servlet 等框架广泛使用注解和反射来简化配置和定制。反射允许框架在运行时扫描和处理这些注解,实现自动配置、依赖注入(DI)和面向切面编程(AOP)。
然而,尽管反射提供了卓越的灵活性和功能,但它可能会影响 Java 应用程序在启动时的性能。检查类和动态加载元数据的过程可能会引入显著的开销,尤其是在快速启动时间至关重要的无服务器或云原生环境中。
这为什么很重要?Java 应用的原生编译
一个很好的例子是创建原生可执行 Java 应用程序,其中开发者使用 Java 虚拟机(JVM)如 GraalVM(Oracle)和 Mandrel(Red Hat)来编译这些应用程序并生成原生二进制文件。这个过程基于即时(AOT)编译,导致在运行时无法使用某些行为——包括反射。AOT 编译器在构建时进行静态代码分析以创建原生可执行文件,这意味着通过动态加载(如反射、Java 原生接口(JNI)或代理)完成的任何处理都代表了这个用例的潜在问题。
为了解决这个问题,Quarkus 和 Micronaut 等框架采用了另一种被称为构建时或编译时的方法。这些框架不是依赖于运行时反射,而是利用注解在构建过程中捕获必要的元数据。这样做消除了运行时昂贵的反射操作,并提供了更快的启动时间和改进的性能。
下一个图表说明了这两种方法是如何工作的,其中使用反射时,Java 会实时读取注解和任何元数据,从而在读取时提供更多的灵活性和可插拔性;这需要更多的内存和预热时间。我们可以在构建时读取这些信息,这样在启动时可以获得更好的预热并节省更多内存;然而,我们失去了反射的灵活性。通常,这是一个权衡分析的点:
图 5.2 – 运行时与构建时读取 Java 注解
反射是 Java 编程和框架中的一个强大机制。它允许动态代码执行、元数据提取以及利用注解进行配置和定制。虽然反射可能在特定场景中影响启动性能,但 Quarkus 和 Micronaut 等框架已经引入了构建时反射作为解决方案,允许开发者利用注解的好处而不牺牲性能。这种由 CDI Lite 启用的方法,促进了在无服务器和云原生环境中高效使用 Java。
Jakarta EE 平台持续进化 – CDI Lite
基于迄今为止突出显示的需求和影响,作为版本 10 发布的雅加达 EE 平台对 CDI 规范进行了更改,以适应许多有助于此场景的行为。CDI Lite 规范提供了这些框架所需的行为,旨在提供 CDI 的轻量级版本。CDI Lite 利用 编译时反射 来消除与完整 CDI 实现相关的运行时开销,使其适用于资源受限的环境和无服务器架构。
开发者在开发 Java 应用程序时可以选择使用反射或采用无反射方法的框架。此比较表将探讨这两个 Java 框架的关键方面,如注解读取、预热灵活性和封装。
反射 | 无反射 | |
---|---|---|
读取 Java 注解 | 实时 | 构建 |
预热(框架启动时所需额外时间) | 启动速度慢 | 启动速度快 |
灵活性 | 实时插拔性 | 构建时限制 |
封装 | 强封装 | Java 封装的限制更多 |
表 5.1 – 反射与无反射解决方案比较
当我们谈论应用程序时,我们不确定架构风格,如微服务或单体,或者我们将使用实时或构建时 Java 应用程序;然而,对于大多数解决方案,我们将使用任何持久化引擎。现在让我们更详细地讨论最成熟的雅加达持久化规范:JPA。
JPA 状态
JPA 是一个关键的雅加达 EE 规范,也是企业应用中最成熟的数据规范。它为 Java 中的 ORM 提供了一种标准化且稳健的方法,使开发者能够无缝地与关系数据库交互。
当与 Java 应用程序和关系数据库之间的集成工作时,需要考虑以下几个方面:
-
配置管理:如何将配置外部化,以便根据部署的环境(开发、生产等)轻松且安全地更改。
-
连接处理:不正确处理与数据库的连接可能导致额外的处理时间,因为这是昂贵的。这一需求与有效管理数据库的打开、关闭和跟踪连接以使用资源并避免有太多打开和空闲连接或应用程序可用连接不足的要求相关。
-
将类映射到数据库表:正如我们在前面的章节中看到的,映射对象可以以多种方式实现,并提供更高或更低级别的灵活性和抽象。
-
映射类之间的关系:面向对象编程(OOP)引入了层次等概念,这在关系数据库模式中是不可用的。根据这些类配置的方式,数据管理可能会具有更高的复杂性和维护成本。
-
事务管理:在应用层管理事务,并确保原子性和回滚。
-
代码生成:开发者可以编写纯 SQL 查询或依赖抽象来加快开发速度。目前,一些框架可以抽象出大多数基本的 CRUD 查询。不幸的是,如果误用,代码生成可能会导致查询缓慢和对私有方法正确使用的限制。
-
获取策略:允许以最佳方式检索数据,以充分利用内存消耗,并且当正确使用时,可以带来性能提升,因为数据仅在需要时才会从数据库中检索。这与 Hibernate 上可用的众所周知的延迟/预取模式相关。
-
解耦业务逻辑和技术方面:根据他们的目标,开发者可以创建极其灵活和定制的代码(例如,使用 JDBC),以换取对数据持久化层和业务逻辑层之间代码耦合的负面影响。
考虑到 Java 开发者的这些反复需求以及创建可重复的、易于广泛采用的优秀实践的可能性,JPA 规范自其创建以来已经发展。
以下图表显示了 JPA 作为 Jakarta EE 世界中最成熟的持久化规范;多个供应商和框架使用它,我们还可以应用多种持久化模式,如 Active Record、Repository 和 Mapper:
图 5.3 – JPA 时间线和景观
当与 Spring 和 Quarkus 等框架结合使用时,JPA 提供了实现不同设计方法(包括 Active Record、Mapper 和 Repository 模式)的灵活性。让我们深入了解这些设计方法,并探讨 JPA 如何通过反射或构建时读取注解来操作。
JPA 和数据库映射模式
当使用 JPA 进行开发时,开发者通常采用三种设计选项:Active Record、Mapper 和 Repository。请注意,由于 JPA 具有映射实体及其相互关系、抽象基本数据库操作和异常处理机制等功能,采用这些模式变得更加简单。让我们更深入地了解一下:
- 使用 JPA 的 Active Record:在这种方法中,领域模型类封装了持久化逻辑,遵循 Active Record 模式。它简化了数据库操作,因为领域类是活跃的参与者,并直接负责处理 CRUD 操作和关系。
当依赖 JPA 时,可以通过使用 JPA 注解如@Entity
来注解领域类,将其标记为持久化实体。领域类还可以注解为@Table
,这将定义与该实体对应的数据库表,该表应映射到该实体。这些注解的元数据使 JPA 能够将对象属性映射到相应的数据库列。
- Mapper:根据 Mapper 模式,领域模型和持久化逻辑应该通过新的专用 mapper 类进行分离。
JPA 与 Spring 和 Quarkus 等框架结合使用,允许开发者配置和管理这些 mapper。mapper 处理领域对象和数据库表之间的转换,从领域模型中抽象出持久化细节。JPA 的EntityManager
和EntityManagerFactory
类提供了执行数据库操作所需的 API,而 mapper 类则促进了数据库和领域模型之间的映射。
- Repositories:Repository 模式建议在应用程序领域层和数据访问层之间引入一层抽象。
在使用 JPA 进行开发时,开发者可以定义充当合同并指定可用 CRUD 操作和查询的仓库接口。JPA 的EntityManager
类是执行查询和管理事务的底层机制,它使数据访问高效且可扩展。
例如,Spring Data JPA 和 Quarkus 等框架支持仓库,并且可以根据定义的接口自动生成必要的实现代码。
当考虑使用框架来实现设计模式时,我们应该意识到其优缺点。我们将深入探讨一个详细的代码示例,但在那之前,让我们检查需要注意的事项。
根据应用用例和需求,了解底层发生的事情以及你的应用程序将从你选择的框架中继承的限制是很重要的。例如,当使用 Active Record 与 Panache 和 Quarkus 一起使用时,你的实体可能扩展了PanacheEntity
类。使用 Repository 时,可能扩展了JpaRepository
,这是一个通用的 Spring Data JPA 接口。通过了解所选框架的实现细节,你可以更好地识别你选择将应用程序代码与框架紧密耦合的地方,通过使用专用的注解或依赖项。你会知道是否以及到什么程度会违反关注点分离(SoC)的原则,或者例如,在需要迁移到不同的持久化框架时所需的额外工作量。
在第四章中学到的优缺点在这里同样适用:Active Record 将比 Repository 更简单,而采用 Repository 可以比 Active Record 带来更好的 SoC,从而提高可维护性和可测试性。
我们将深入一个全面的代码示例,以阐明在框架提供的便利性和遵循众所周知的编码最佳实践之间权衡的细节。
使用 Quarkus 和 Panache 云原生运行时的 JPA 力量
为了展示现代持久化框架如何使开发者能够依赖他们对 JPA 的了解,让我们来看看 Quarkus 和 Panache,以及使用加速开发速度开发云原生 Java 服务的体验。在这个背景下,我们将评估设计模式实现的关键方面、自动生成的持久化代码,以及在设计解决方案时需要考虑的一些潜在缺点。
您可以跟随操作或创建一个全新的项目来尝试以下代码。如果您还没有使用过 Quarkus 和 Panache,您可能会注意到与传统的应用服务器相比,轻量级运行时的开发体验有相当大的差异,以及使用 Panache 编写简单的 CRUD 场景的简单性。
关于如何创建项目的详细信息可以在项目的仓库中找到:github.com/architects4j/mastering-java-persistence-book-samples/edit/main/chapter-05/README.md
。现在,让我们深入探讨。
我们将要看到的微服务将用于管理书籍和杂志,我们将使用 JPA 探索两种不同的数据库设计模式:仓库模式和活动记录模式。
设置新服务
由于我们将依赖持久化和 REST 端点(通过 Quarkus 启动页面轻松生成)的功能,项目需要依赖项来处理这些功能。有趣的是,大部分艰苦的工作将由框架自动生成,而这些框架实际上基于众所周知的规范和技术,如 RESTEasy、JSON-B、Hibernate ORM、Hibernate Validator、Panache 和 JDBC。
基础存储将由 H2 内存数据存储处理,这对于学习目的应该是很有用的,因为它不需要安装外部数据库或使用 Docker 来启动一个数据库实例。然而,请记住,H2 不建议用于生产环境。
第一个差异出现在 Quarkus 项目的配置(src/main/resources/application.properties
)中,因为开发者可以依赖一个单一的属性配置文件来将h2
作为数据库类型,将memory
作为 JDBC URL。这种方法使得在不修改任何代码的情况下(例如,从 H2 到 PostgreSQL、MariaDB 或其他数据库)更改底层数据库技术。
另一个积极方面是,这种配置风格依赖于 Eclipse MicroProfile Configuration 规范,该规范提供了对基于应用程序运行环境的属性覆盖的即插即用支持——换句话说,这就是如何在生产环境中的敏感数据(如用户名和密码)保持机密性,并且不在应用程序级别直接配置。
属性配置可以设置如下:
quarkus.datasource.db-kind=h2quarkus.datasource.username=username-default
quarkus.datasource.jdbc.url=jdbc:h2:mem:default
quarkus.datasource.jdbc.max-size=13
quarkus.hibernate-
orm.dialect=org.hibernate.dialect.H2Dialect
quarkus.hibernate-orm.database.generation=create
quarkus.hibernate-orm.log.sql=true
持久化实体和数据库操作
基础设施准备就绪后,接下来创建项目实体。从现在开始,我们将检查两个模式,你可以观察到Book
实体使用 Active Record 实现,而Magazine
使用 Repository 模式。
Book
类如下所示。请注意,尽管它带来了@Entity
注解,但没有额外的属性级注解。此外,Book
实体“知道”其数据库操作,例如如何通过名称和书籍发布搜索书籍:
@Entitypublic class Book extends PanacheEntity {
public String name;
public int release;
public int edition;
public static List<Book> findByName(String name) {
return list("name", name);
}
public static List<Book> findByRelease(int year) {
return list("release", year);
}
}
正如你接下来会看到的,Magazine
类使用经典的 JPA 注解,如@Entity
和@id
(到目前为止,太阳下无新事)。Book
实体不需要@id
注解的原因是它从它扩展的类PanacheEntity
继承了这种能力。PanacheEntity
通过继承处理多个操作,包括id
属性:
@Entitypublic class Magazine {
@Id
@GeneratedValue
public Long id;
public String name;
public int release;
public int edition;
}
与使用 Active Record 实现的类不同,其中数据库操作将在实体本身进行,Magazine
类需要额外的类来进行此类数据操作——一个Repository
类。MagazineRepository
类必须实现基本数据库过程,以及查询(如Book
类中可用的find by release and name
)。由于我们使用PanacheRepository
类,我们可以在基本操作上节省一些时间,因为 Panache 稍后会自动生成它们。
这里展示了MagazineRepository
代码:
@ApplicationScopedpublic class MagazineRepository implements
PanacheRepository<Magazine> {
public List<Magazine> findByName(String name) {
return list("name", name);
}
public List<Magazine> findByRelease(int year) {
return list("release", year);
}
}
暴露 REST 端点以进行数据操作
最后,为了通过我们迄今为止检查的类来操作数据,应用程序暴露了 REST API。端点是BookResource
和MagazineResource
,它们应该暴露与Book
和Magazine
相同的数据库操作,以便我们可以评估每种方法使用的差异。可以提到的第一个差异是,虽然我们不需要注入任何内容就可以使用BookResource
端点,但要操作Magazine
实体,开发者必须注入相应的repository
类。
首先,观察BookResource
端点如何允许与使用 Active Record 实现的实体Book
进行交互。你会注意到一个负面方面是端点与 Active Record 之间存在更紧密的耦合。作为一个积极点,注意它如何使应用程序更简单,层次更少。
BookResource
类包括以下内容:
-
三个
GET
端点:findAll
、findByName
和findByYear
-
一个
POST
和一个DELETE
方法
代码如下所示:
@Path("/library")@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class BookResource {
@GET
public List<Book> findAll() {
return Book.listAll();
}
@GET
@Path("name/{name}")
public List<Book> findByName(@PathParam("name") String
name) {
return Book.findByName(name);
}
@GET
@Path("release/{year}")
public List<Book> findByYear(@PathParam("year") int
year) {
return Book.findByRelease(year);
}
@POST
@Transactional
public Book insert(Book book) {
book.persist();
return book;
}
@DELETE
@Path("{id}")
@Transactional
public void delete(@PathParam("id") Long id) {
Book.deleteById(id);
}
}
在前面的代码中,请注意 Book
实体已经提供了执行数据库操作的方法。
现在,让我们转向 MagazineResource
端点,它涵盖了仓库模式。请注意,尽管这是一个简单的示例项目,但在现实生活中,随着架构的侵蚀,它将增加业务需求的复杂性和时间。这让我们想起了第四章,在那里我们讨论了更多关于层及其权衡的内容,所以那个可以单独帮助我们分解问题的层可能会对更复杂的代码产生更大的影响。随着应用程序的扩展和采用额外的层,如服务层,或者采用六边形模型,仔细分析权衡并密切关注持久化层的设计变得至关重要。
这里是 MagazineResource
端点的实现:
@Path("/magazines")@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class MagazineResource {
@Inject
MagazineRepository repository;
@GET
public List<Magazine> findAll() {
return repository.listAll();
}
@GET
@Path("name/{name}")
public List<Magazine> findByName(@PathParam("name")
String name) {
return repository.findByName(name);
}
@GET
@Path("release/{year}")
public List<Magazine> findByYear(@PathParam("year") int
year) {
return repository.findByRelease(year);
}
@POST
@Transactional
public Magazine insert(Magazine magazine) {
this.repository.persist(magazine);
return magazine;
}
@DELETE
@Path("{id}")
@Transactional
public void delete(@PathParam("id") Long id) {
repository.deleteById(id);
}
}
在前面的类中需要注意的关键点如下:
-
开发者需要注入
MagazineRepository
端点的一个实例。 -
开发者必须实现所需的类和方法,从而获得对底层实现的更高控制度和定制度,以及更好的 SoC(分离关注点)代码,在领域实体和数据库集成之间。
到目前为止,应用程序已准备就绪,所有操作都可通过 REST 访问,并且可以通过开发者定义的方法和 Panache 提供的内置方法正确地操作数据。
更快的开发速度——自动端点生成
Panache 允许在标准场景下实现更快的开发速度,结合了我们之前看到的 Active Record 的好处以及基于 Panache 实体的自动生成 REST 端点。quarkus-hibernate-orm-rest-data-panache
Quarkus 扩展提供了以下功能,而不是之前使用的 quarkus-hibernate-orm-panache
扩展。
与之前的方法相比,开发者交付一个完全可用的 CRUD 服务的速度非常明显,与传统的 EE 应用服务器相比更是如此。通过以下步骤,开发者应该能够在几分钟内创建一个完整的 通讯录 CRUD。
考虑到现有项目,可以创建一个新的 Newsletter
类,如下所示:
@Entitypublic class Newsletter extends PanacheEntity {
public String author;
public String headline;
}
它依赖于 Active Record 实现。在此基础上,它结合了 Quarkus 和 Panache 的能力,基于 Panache 实体自动生成 REST 端点。
要达到之前示例中提到的相同结果,以下 REST 操作应该是可用的:
-
三个
GET
资源:findAll
、findById
和getCount
-
POST
、PUT
和DELETE
方法,分别用于插入、更新和删除通讯录
要实现这个目标,所需的就是一个新的接口,该接口扩展了 PanacheEntityResource
接口。该接口指示 Panache 实体是 id
属性类型:
import io.quarkus.hibernate.orm.rest.data.panache.PanacheEntityResource;public interface NewsletterResource extends PanacheEntityResource<Newsletter, Long> {
}
就这样!如果使用 dev 模式运行 Quarkus,开发者应该已经能够通过刷新页面并检查 swagger-ui
页面和新端点来简单地验证结果,如图所示:
图 5.4 – 由 Panache 自动生成的新的端点
现在,请注意,当选择走这条路时,所有属性都被配置为公共属性。使用这种方法时,你需要权衡的是:除非你添加额外的代码来处理私有属性的使用,否则你将选择开发速度,完全放弃封装、没有访问控制、增加代码耦合(因为类别的更改可能会导致其他类别的潜在更改),以及有限的控制和数据完整性(属性可以被直接修改)。
你可能会认为这就像将属性配置为私有并添加公共的 getter 和 setter 一样简单。确实如此——这基本上是相同的。但你会以相同的方式(因为 setter 仍然是公共的)缺乏封装,使用“愚蠢”的 getter 和 setter。此外,这正是 Panache(在撰写本文时的当前版本)在幕后所做的事情:它生成 getter
和 setter
属性,并将这些属性的每个使用都重写为相应的 getter
和 setter
属性。
Panache 非常强大,并允许开发者在编写查询时也变得更加高效,例如可以使用如下代码:Newsletter.find("order by author")
,或者Newsletter.find("author = ?1 and headline = ?2", "karina", "Java lives!")
,或者更好的是,Newsletter.find("author", "karina")
。
你已经看到了 Java 开发者可以从现代运行时技术中获得惊人的体验,以及从头开始创建一个全新的有状态服务是多么有效,同时依靠现有的 JPA 知识。接下来,我们将稍微转向另一个话题,强调大多数曾经使用过 JPA 的开发者和架构师通常面临的问题的考虑因素:性能和可伸缩性。
通用 JPA 相关性能考虑因素
以下考虑不仅适用于 Panache,也适用于基于 JPA 的应用程序。为了帮助识别或进行性能调整过程,你始终可以依赖框架的能力来输出正在执行的 DDL(数据库 SQL 操作)和数据库操作统计信息。例如,Hibernate 提供了多个配置参数,如show_sql
、generate_statistics
、jdbc.batch_size
、default_batch_fetch_size
和cache.use_query_cache
。在接下来的段落中,你将找到围绕此类配置的考虑。现在,检查这里如何将一些配置应用于我们刚刚创建的示例 Quarkus 应用程序。这些属性允许记录 DDL 和统计信息:
quarkus.hibernate-orm.log.sql=truequarkus.hibernate-orm.statistics=true
quarkus.hibernate-orm.metrics.enabled=true
quarkus.log.level=DEBUG
注意,在生产环境中不应使用详尽的日志配置,因为它会直接影响应用程序性能;此外,可以单独配置应用程序日志类别,仅输出所需的内容。例如,前面的统计配置可以帮助你识别慢速执行的 DDL。以下是一个你可以为每个数据库操作获取的信息示例:
2023-06-19 02:10:25,402 DEBUG [org.hib.sta.int.StatisticsImpl] (executor-thread-1) HHH000117: HQL: SELECT COUNT(*) FROM dev.a4j.mastering.data.Newsletter, time: 1ms,
rows: 1
如果你担心性能,请确保你的代码(无论是由于映射还是查询解析)在幕后不会自动生成性能低下的 SQL 查询,在不需要时检索不必要的信息,或者自动生成过多的查询而不是运行一个更适合的单个查询。
除了与持久性相关的 Java 代码本身之外,还可以通过设置应用程序启动时打开的连接数、连接池大小(以便可以重用打开的连接)以及应用程序(通过你选择的框架和类)如何识别和清理空闲或未关闭的连接来微调你的 JPA 数据源连接。
另一个需要考虑的项目是批量操作。假设每份通讯可以包含几篇文章,作者可以一次性创建一份新的通讯和 50 篇文章。在这种情况下,而不是在应用程序和数据库之间来回 51 次以创建所有文章和通讯,只需执行一次操作即可完成所有操作。同样适用于查询数据。
对于查询密集型应用程序,专注于创建可以更好地执行的特定 SQL 查询,如果应用程序需要多次查询执行,建议在应用程序配置中微调批量获取大小。JDBC 批量操作是定义单次数据库往返中可以执行多少操作的好方法。
对于具有大量插入操作的应用程序,也可以使用批量插入,确保避免长时间运行的事务或每次“flush”操作时花费额外的时间(因为EntityManager
将不得不一次性处理大量对象的插入)。对于大多数微调配置,评估每个应用程序上最佳配置的最佳方式是执行负载测试并比较结果。然而,在查询数据的上下文中,请记住,缓存常用查询有助于减少数据库访问次数并提高性能。
在 JPA 上下文中,关于缓存,有两种类型的缓存:一级缓存和二级缓存。一级缓存与EntityManager
缓存(会话缓存)中包含的对象相关。它允许应用程序在访问会话中最近访问或操作的对象时节省时间。
当与扩展到许多运行实例的分布式应用程序一起工作时,考虑使用允许使用共享缓存的二级缓存可能是有益的。请记住,缓存功能并不适用于所有场景,尽管它可能导致显著更好的性能,但它将要求对如何微调缓存解决方案有良好的理解。
最后,微调缓存解决方案意味着提供适当的缓存失效(以确保缓存数据与底层数据库的当前数据保持一致),适当的缓存同步(因为可能有多个缓存提供程序实例),驱逐策略等。在存在实时或最新数据的场景中,考虑缓存使用带来的挑战以及引入的数据过时可能性。
这就带我们结束了 Quarkus 和 JPA 的旅程,在这个过程中,我们看到了 JPA 中的 Active Record 和 Repository 模式。我们可以看到 Active Record 是多么简单,但与此同时,我的实体知道并执行数据库操作。因此,它有两个职责。当我们谈论重定向或任何不需要巨大业务复杂性的集成函数时,这是可以的。
摘要
总之,Jakarta EE 是一个强大的平台,它提供了一套全面的规范、API 和工具,用于开发企业应用程序。在持久化层,Jakarta EE 凭借其成熟的 JPA 规范而闪耀,该规范提供了一种标准化的 ORM 方法。使用 JPA,开发者可以利用 Active Record 和 Repository 等设计模式来简化并优化他们的数据访问操作。
当与 Quarkus 框架结合使用时,Jakarta EE 中的 JPA 在实践上展示了其能力。以其快速的启动时间和高效的资源利用而闻名的 Quarkus,通过无缝集成 JPA 来提升开发体验。开发者可以利用 Active Record 模式,使他们的领域模型类直接处理持久化操作。或者,他们可以采用 Repository 模式,该模式引入了一个抽象层,以实现灵活和可扩展的数据访问。通过在 Quarkus 中利用 JPA,开发者可以高效地与关系型数据库交互,确保数据完整性,并在他们的 Jakarta EE 应用程序中实现最佳性能。
总体而言,凭借其成熟的 JPA 规范,结合 Quarkus 框架的 Jakarta EE,赋予了开发者构建强大且高效的持久化层的力量。Jakarta EE 对持久化的标准化方法与 Quarkus 的简化开发体验相结合,为创建可扩展且高性能的企业应用程序开辟了无限可能。但 NoSQL 呢?Jakarta EE 是否支持它?是的,它支持;接下来的章节将介绍如何使用 Java 处理几种 NoSQL 数据库类型,如键值、文档和图。
第六章:Java 中的 NoSQL 揭秘 – 一个 API 统治一切
最近 NoSQL 数据库获得了显著的关注度,本章将探讨它们为什么值得更多的关注。随着软件的发展和多样化的需求增加,NoSQL 数据库提供了一条更容易成功的途径。使用 Jakarta 标准,这种持久化类型在各个领域都有帮助,包括更传统的领域如金融。NoSQL 数据库提供灵活的数据建模、水平扩展和更好的性能,以及其他优势。因此,它们适合管理大量结构化或非结构化数据,并已成为现代应用的热门选择。本章将指导我们如何使用 Java 操作 NoSQL 数据库,帮助开发者利用其功能和能力。
本章我们将涵盖以下主题:
-
理解 NoSQL 数据库的权衡
-
使用Jakarta NoSQL (JNoSQL)消费 NoSQL 数据库
-
图形数据库
技术要求
以下为本章所需内容:
-
Java 17
-
Git
-
Maven
-
Docker
-
任何首选的 IDE
-
本章的代码可以在
github.com/PacktPublishing/Persistence-Best-Practices-for-Java-Applications/tree/main/chapter-06
找到。
理解 NoSQL 数据库的权衡
NoSQL 数据库很受欢迎,包括顶级数据库引擎提供的几个持久化解决方案。我们必须记住,NoSQL 数据库并没有消除对关系数据库的需求。
SQL 数据库对于大多数企业解决方案来说仍然至关重要。人们通常从这里开始学习编程,关于这个主题有大量的文章和书籍。
此外,使用 SQL 的产品成熟度非常广泛!这些产品可以帮助你完成关键任务,例如备份、迁移和查询分析。
目标不是让你对使用 NoSQL 失去动力。然而,一旦你成为高级工程师,请记住 Neal Ford 在《软件架构基础:工程方法》中提到的软件架构的第二定律:一切皆有权衡!
考虑这一点,让我们继续讨论 NoSQL 数据库。
使用 JNoSQL 消费 NoSQL 数据库
我们很幸运在 Java 平台上拥有几个解决方案和成功案例。因此,下一步是尽快在技术成熟后创建一个标准 API。
JNoSQL 规范旨在简化 Java 和 NoSQL 数据库之间的通信。
标准化多个 NoSQL 数据库的行为和接口的好处是代码的可移植性和易于集成。我们通常谈论切换数据库,这是真的。然而,最大的优势是使每个人都能更容易地参与项目。当需要时,可以自然地切换数据库。
图 6.1:NoSQL 数据库 – 文档类型
使用标准 API 的好处很大;此外,您可以使用特定的行为,例如 Cassandra 的CQL(Cassandra 查询语言)和 ArangoDB 的AQL(ArangoDB 查询语言)。
图 6.2:NoSQL 数据库 – 单 API 的文档类型
这是 JNoSQL 的主要原则,即在 Java 和 NoSQL 数据库方面简化并使您和您组织的日常生活更加轻松。在规范方面,您可以探索您的实体;例如,使用 JPA,您可以使用注解来操作多种 NoSQL 数据库类型,如文档、列、图和键值。看看相同的注解如何在多个文档数据库中工作:
图 6.3:JNoSQL 的多数据库集成抽象
该规范支持最流行的 NoSQL 类型:键值、文档、宽列或其他列类型,以及图。
键值数据库
从最简单的一个开始:键值。这种 NoSQL 解决方案的味道有一个类似于映射的结构。因此,您通常可以从键中找到信息,而值是一个 blob。每个供应商都有不同的序列化和存储值的方式,例如文本、JSON 或二进制 JSON。
使用图书馆系统,我们可以使用这个数据库来保存用户设置信息;因此,我们将创建一个User
实体来保存语言和分类。
为了提供一个此模型的示例,我们将遵循一个简单的 Java SE 应用程序与 JNoSQL。我们将使用最流行的键值数据库解决方案:Redis。
定义生产中的 Redis 配置需要一本书;我们将本地安装此示例,但请记住,在生产环境中工作,请查阅 Redis 文档以获取更多详细信息。现在,一旦您已配置 Docker,请运行以下命令:
docker run --name redis-instance -p 6379:6379 -d redis
服务器正在运行;下一步是向我们的项目中添加依赖项。此示例使用 Maven 项目,因此我们将添加映射依赖项和 Redis 驱动程序:
<dependency> <groupId>org.eclipse.jnosql.mapping</groupId>
<artifactId>jnosql-mapping-key-value</artifactId>
<version>${jnosql.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jnosql.communication</groupId>
<artifactId>jnosql-redis-driver</artifactId>
<version>${jnosql.version}</version>
</dependency>
准备好依赖项后,下一步是使用注解创建User
实体,以映射到键值数据库。它需要一个注解来定义为一个 JNoSQL 实体和键,您将在其中设置Entity
和Id
注解,分别:
@Entitypublic class User {
@Id
private String userName;
private String name;
private Set<String> languages;
private Set<String> categories;
//...
}
在定义User
实体类和userName
字段时,分别使用Entity
和Id
注解。
让我们执行它。KeyValueTemplate
是我们用于操作键值数据库的实例;这是映射通信的最低级别:
public static void main(String[] args) { User otavio = User.builder().userName("otaviojava")
.name("Otavio Santana")
.category("Technology")
.category("Philosophy")
.category("History")
.language("English")
.language("Portuguese")
.language("French").build();
try (SeContainer container =
SeContainerInitializer.newInstance().initialize()) {
KeyValueTemplate template =
container.select(KeyValueTemplate.class).get();
User userSaved = template.put(otavio);
System.out.println("User saved: " + userSaved);
Optional<User> user = template.get("otaviojava",
User.class);
System.out.println("Entity found: " + user);
template.delete("otaviojava");
}
}
这种数据库类型的查询有限制,但功能强大。生存时间(TTL)是用于定义数据库中信息过期时间的功能:
public static void main(String[] args) throws InterruptedException {
User poliana = User.builder()
.userName("poly")
.name("Poliana Santana")
.category("Philosophy")
.category("History")
.language("English")
.language("Portuguese")
.build();
try (SeContainer container = SeContainerInitializer
.newInstance().initialize()) {
KeyValueTemplate template = container
.select(KeyValueTemplate.class).get();
template.put(poliana, Duration.ofSeconds(1));
System.out.println("The key return: " +
template.get("poly", User.class));
TimeUnit.SECONDS.sleep(2L);
System.out.println("Entity after expired: " +
template.get("poly", User.class));
template.delete("poly");
}
}
但等等,配置在哪里?JNoSQL 实现使用 Eclipse MicroProfile 配置来保留良好的软件实践,例如十二因素应用。
在这个示例中,我们将属性放在property
文件中,但我们可以覆盖系统环境或包含更多配置,例如用户名和密码:
jnosql.keyvalue.database=developersjnosql.redis.port=6379
jnosql.redis.host=localhost
当你想快速读写实体时,键值是一个强大的盟友。这些解决方案通常在内存中工作,并使用快照来避免服务器宕机时的数据丢失。
就像任何技术解决方案一样,需要考虑权衡。例如,虽然可以使用 ID 检索信息并将值作为唯一的 blob 返回,但这种方法在所有情况下可能并不理想。因此,让我们探索下一类解决方案来解决这个问题。
列数据库
以下数据库类型是宽列类型,它遵循与键值相同的原理,但与唯一的 blob 不同,你可以将信息拆分为小的列。
这个 NoSQL 数据库也被称为二维键值存储。最流行的实现是 Apache Cassandra;本节将介绍 Java 和 Apache Cassandra 之间的集成。
如前所述,我们不会涵盖在生产环境中运行的建议;目前,我们将运行单个实例以进行测试目的:
docker run -d --name cassandra-instance -p 9042:9042 cassandra
小贴士
当使用 Docker 运行 Cassandra 实例时,请不要在生产环境中以这种方式运行。此配置最适合您的测试环境。对于生产使用,请访问 Apache 网站上的 Apache Cassandra 文档。
我们将遵循相同的配置理念,因此我们将使用 Java 和 Maven 项目。在 Java 方面,第一步是为 Maven 项目添加依赖项:
<dependency> <groupId>org.eclipse.jnosql.mapping</groupId>
<artifactId>jnosql-cassandra-extension</artifactId>
<version>${jnosql.version}</version>
</dependency>
这个依赖项看起来不同,因为它是一个 Cassandra 扩展;它是列 API 加上特定于 Cassandra 的行为,例如 CQL。如果你愿意,你可以像我们使用 Redis 一样使用它,但不能轻松地使用 Cassandra 特定的行为:
<dependency> <groupId>org.eclipse.jnosql.communication</groupId>
<artifactId>jnosql-cassandra-driver</artifactId>
<version>${jnosql.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jnosql.mapping</groupId>
<artifactId>jnosql-mapping-column</artifactId>
<version>${project.version}</version>
</dependency>
这个 NoSQL 数据库的工作方式与 SQL 不同。实际上,反规范化是你的最佳朋友。
首先,可视化模型。然后,创建它。我们希望跟踪和查看具有特定 ID 的用户租借书籍的记录:
@Entity("rental")public class RentalBook {
@Id("id")
private UUID id;
@Column
private LocalDate date;
@Column
@UDT("user")
private User user;
@Column
@UDT("book")
private Set<Book> books = new HashSet<>();
}
@Entity
public class User {
@Column
private String username;
@Column
private String name;
}
@Entity
public class Book {
@Column
private UUID id;
@Column
private String title;
}
模型就到这里;从 ID,我们可以返回书籍租借的记录。我们正在复制诸如书籍标题和用户姓名等信息,以避免任何连接或更多过程,但一旦字段被更新,我们需要在后台运行一个事件来更新它。
User
和Book
实体是用户定义的类型,其中我们可以向单个列添加多个值。
尽管有 JPA,JNoSQL 必须使用Column
或Id
注解定义每个要存储的字段。
让我们执行代码,因为我们本质上可以使用与键值相同的原理和行为。我们还可以在查询中选择要返回的字段,而不是总是返回所有内容:
try(SeContainer container = SeContainerInitializer.newInstance().initialize()) {
RentalBook otavio = RentalBook.builder()
.id(UUID.randomUUID())
.date(LocalDate.now())
.user(User.of("otaviojava", "Otavio
Santana"))
.book(Book.of(UUID.randomUUID(), "Clean
Code"))
.book(Book.of(UUID.randomUUID(), "Effective
Java"))
.build();
RentalBook karina = RentalBook.builder()
.id(UUID.randomUUID())
.date(LocalDate.now())
.user(User.of("kvarel4", "Karina Varela"))
.book(Book.of(UUID.randomUUID(), "Clean
Arch"))
.build();
ColumnTemplate template = container
.select(CassandraTemplate.class).get();
template.insert(List.of(otavio, karina),
Duration.ofDays(600L));
ColumnQuery query = ColumnQuery.select("id",
"date").from("rental")
.where("id").eq(karina.getId()).build();
System.out.println("Executing query using API: ");
template.select(query).forEach(System.out::println);
System.out.println("Executing query using text: ");
template.query("select * from rental")
.forEach(System.out::println);
}
Cassandra 不是无模式的,尽管在使用它之前你需要创建模式。在本地运行查询是可以的,但在生产环境中不要使用它。这是因为它在生产环境中启动和运行需要时间。以下代码显示了使用 Cassandra 的配置:
jnosql.column.database=libraryjnosql.cassandra.query.1=CREATE KEYSPACE IF NOT EXISTS library WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
jnosql.cassandra.query.2=CREATE TYPE IF NOT EXISTS library.user (username text, name text);
jnosql.cassandra.query.3=CREATE TYPE IF NOT EXISTS library.book (id uuid, title text );
jnosql.cassandra.query.4=CREATE COLUMNFAMILY IF NOT EXISTS library.rental (id uuid PRIMARY KEY, date text, user user, books frozen<set<book>>);
与键值对相比,宽列模型在模型中具有更多的灵活性。但我们仍然有搜索非 ID 字段的问题;我们如何解决这个问题?让我们继续到下一个数据库类型来回答这个问题。
小贴士
Cassandra 有一个二级索引,允许在键之外进行查询。请注意,使用它有几个影响。
文档数据库
我们第三种 NoSQL 类型可以搜索 ID 之外的字段;好消息!文档 NoSQL 类型具有 XML 或 JSON 结构。搜索 ID 仍然是更有效的方式,但能够通过其他字段搜索信息使模型更具灵活性,并使探索数据库中的信息更容易。
对于这个示例,我们将使用 MongoDB 进行实现。我们将本地运行单个节点。请注意,在生产环境中运行时;但现在,我们将从 Docker 镜像中运行它:
docker run -d --name mongodb-instance -p 27017:27017 mongo
作为 Maven 依赖项,我们将添加 MongoDB 扩展:
<dependency> <groupId>org.eclipse.jnosql.mapping</groupId>
<artifactId>jnosql-mongodb-extension</artifactId>
<version>${jnosql.version}</version>
</dependency>
在这个示例中,我们将展示商店内的图书项。该模型与宽列类似,因为它是由查询驱动的,但这次我们有更多的灵活性来搜索。该模型遵循 DDD 原则,其中Book
作为实体,Author
作为值对象:
@Entitypublic class Book {
@Id
private String id;
@Column
private String title;
@Column
private List<String> categories;
@Column
private Set<String> languages;
@Column
private Author author;
}
@Entity
public record Author(@Column("nickname") String nickname,
@Column("name") String name, @Column("profile") String
profile) {
public static AuthorBuilder builder() {
return new AuthorBuilder();
}
}
小贴士
如果你正在使用不可变值对象,它是一个使用 Java 的最新功能:记录的绝佳候选者。
模型已经准备好探索;因此,我们将它在 Java SE 上运行,并探索DocumentTemplate
,它遵循与之前数据库版本相同的原理——作为 Java 和数据库之间的桥梁:
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
Author otavio = Author.builder()
.nickname("otaviojava").name("Otavio Santana")
.profile("@otaviojava").build();
Book cassandra = Book.builder()
.title("Apache Cassandra Horizontal scalability
for Java applications")
.category("database").category("technology")
.language("Portuguese").language("English")
.author(otavio).build();
DocumentTemplate template = container
.select(DocumentTemplate.class).get();
template.insert(cassandra);
System.out.println("The database found: " +
template.find(Book.class, cassandra.getId()));
template.delete(Book.class, cassandra.getId());
}
文档的力量与关系数据库相似,但我们没有像 SQL 和 JOIN 那样的强大事务。即使有这种限制,我们也可以从任何字段对元素进行排序:
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
Author neal = Author.builder()
.nickname("neal").name("Neal Ford")
.profile("@neal4d").build();
Book evolutionary = Book.builder()
.title("Building Evolutionary Architectures:
Support Constant Change")
.category("architecture")
.category("technology")
.language("Portuguese").language("English")
.author(neal).build();
//...
DocumentTemplate template = container
.select(DocumentTemplate.class).get();
template.insert(evolutionary);
DocumentQuery query = DocumentQuery
.select().from("Book")
.where("author.nickname").eq("neal")
.orderBy("title").asc().build();
System.out.println("The query by API");
template.select(query).forEach(System.out::println);
}
运行示例的属性将遵循相同的核心思想,以利用十二因素应用:
jnosql.document.database=libraryjnosql.mongodb.host=localhost:27017
文档 NoSQL 类型在查询中的灵活性非常好!但实体之间的关系呢?这种查询在某些时候是需要的,那么我们如何解决这个问题?让我们看看最后一种 NoSQL 类型并找出答案。
图数据库
如果你正在寻找关系,你来到了正确的位置!让我们来谈谈图数据库。图数据库是一个具有图结构的强大引擎,它根据顶点和边保存信息,其中边是一个用于保存关系信息的对象。
使用边,你可以定义关系的方向和属性;它甚至比关系数据库更强大。
让我们创建一个简单的推荐引擎,其中包含一个可以读写并能遇见人的角色。
图 6.4:人与书之间的关系
首先要确保我们至少有一个实例正在运行;记住,这并不是在生产环境中运行的正确方式:
docker run --publish=7474:7474 --publish=7687:7687 --env NEO4J_AUTH=neo4j/admin neo4j
我们将有两个实体:Book
和Person
。一个人可以写 N 本书,读 N 本书,遇到 N 个人。当我们有多个 N-to-N 关系时,树形层次结构和元关系表明这是一个图数据库:
@Entitypublic class Book {
@Id
private Long id;
@Column
private String name;
}
@Entity
public class Person {
@Id
private Long id;
@Column
private String name;
@Column
private String profile;
}
@Entity
public class Category {
@Id
private Long id;
@Column
private String name;
}
该图还有一个GraphTemplate
实例,您可以使用它来操作图数据库。
此示例将使用LibraryGraph
来操作该系统上的所有操作。准备好随着它变大而重构它,主要是因为它打破了单一责任SOLID原则。
需要记住的主要点是开发者的核心原则,即使代码易于维护和阅读;不幸的是,我们还没有像 SQL 那样拥有一套完整的已建立的最佳实践来处理 NoSQL:
@ApplicationScopedclass LibraryGraph {
@Inject
private GraphTemplate template;
public Book save(Book book) {
Objects.requireNonNull(book, "book is required");
return template.getTraversalVertex()
.hasLabel(Book.class)
.has("name", book.getName())
.<Book>next()
.orElseGet(() -> template.insert(book));
}
public Category save(Category category) {
Objects.requireNonNull(category, "category is
required");
return template.getTraversalVertex()
.hasLabel(Category.class)
.has("name", category.getName())
.<Category>next()
.orElseGet(() ->
template.insert(category));
}
//...
}
最后一步是运行它。在插入实体和关系时,查询和操作之间存在微小的差异。我们可以使用 Neo4j 实现以下图。
JNoSQL 使用 Apache TinkerPop 作为通信层,我们可以通过 Gremlin 进行查询搜索。这打开了一个无限可能的世界:
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
LibraryGraph graph = container
.select(LibraryGraph.class).get();
Category software = graph
.save(Category.of("Software"));
Category java = graph.save(Category.of("Java"));
Person joshua = graph.save(Person.of("Joshua Bloch",
"@joshbloch"));
graph.is(java, software);
graph.write(joshua, effectiveJava);
List<String> softwareCategories =
graph.getSubCategories();
List<String> softwareBooks = graph.getSoftwareBooks();
List<String> softwareNoSQLBooks =
graph.getSoftwareNoSQL();
Set<Category> categories = graph.getCategories(otavio);
Set<String> suggestions = graph.getFollow(otavio);
}
图数据库具有利用关系的广泛能力,但这也带来了性能上的代价。数据库扩展很困难,并且比键值数据库慢。
摘要
我们已经完成了对 NoSQL 类型的旅程,我们查看的是从最不灵活到最不可扩展的类型。注意建模至关重要,因为它与 SQL 数据库不同,并且是 NoSQL 数据库初学者的常见陷阱。
我们向您介绍了 JNoSQL Java API 标准,它简化了 Java 应用程序与 NoSQL 数据库的集成。我们将在关于多语言持久性的章节中讨论 Jakarta 和数据持久层。在下一章中,我们将介绍使用 jOOQ 的关系数据库。
第七章:jOOQ 采用指南的缺失部分
面向对象编程(OOP)是讨论企业架构时最流行的方法;然而,还有更多,例如数据驱动。在当今数据驱动的世界中,jOOQ 已成为开发者用来与数据库交互的强大工具,提供了一种无缝且高效的 SQL 工作方式。
首先,让我们解决基本问题:什么是 jOOQ?jOOQ,即Java 面向对象查询,是一个轻量级但强大的 Java 库,使开发者能够流畅且直观地编写类型安全的 SQL 查询。它提供了一个领域特定语言(DSL),封装了 SQL 的复杂性,使开发者能够专注于编写简洁且易于阅读的代码。
现在,你可能想知道为什么 jOOQ 在开发者中获得了显著的吸引力。答案在于其能够弥合数据库的关联世界与现代应用程序开发面向对象范式之间的差距。jOOQ 使开发者能够在 Java 代码中充分利用 SQL 的全部功能,提供传统对象关系映射(ORM)框架往往难以实现的灵活性、性能和可维护性。
当我们深入到 jOOQ 的世界时,我们将探讨数据驱动设计的概念及其影响。与主要围绕操作对象及其行为的传统面向对象不同,数据驱动设计强调底层数据结构和它们之间的关系。我们将研究 jOOQ 如何采用这种方法,赋予开发者高效处理复杂数据库交互的能力,同时保持强类型和编译时安全性的好处。
在本章中,我们将探讨 jOOQ 框架以及如何在具有 Jakarta EE 和 MicroProfile 的企业架构中使用它:
-
Java 中的数据驱动和面向对象编程
-
什么是 jOOQ?
-
使用 jOOQ 与 Jakarta/MicroProfile
因此,让我们开始这段旅程,去发现 jOOQ 的力量,并理解它是如何革命性地改变我们与数据库交互的方式,弥合 SQL 世界和面向对象世界之间的差距。
技术要求
本章需要以下内容:
-
Java 17
-
Git
-
Maven
-
任何首选的 IDE
-
本章的代码可以在以下位置找到:
github.com/PacktPublishing/Persistence-Best-Practices-for-Java-Applications/tree/main/chapter-07
Java 中的数据驱动和面向对象编程
在 Java 中,数据驱动编程指的是一种方法,其中底层数据和其结构主要驱动程序的设计和功能。它侧重于以允许灵活性、可扩展性和易于修改的方式操作和处理数据,而不太依赖于对象的行为。
相比之下,面向对象编程是一种围绕对象旋转的编程范式,对象是类的实例。面向对象编程强调在对象内部封装数据和相关的行为,促进诸如继承、多态和抽象等概念。它侧重于将现实世界实体建模为对象,并通过方法和关系定义其行为。
数据驱动编程与面向对象编程之间的关键区别在于它们对程序设计的处理方式。在面向对象编程中,重点是建模实体及其行为,围绕对象及其交互组织代码。当对象的行为复杂或需要表示系统中的现实世界实体时,这种方法效果很好。
另一方面,数据驱动编程优先考虑操作和处理数据结构。当处理大量数据,如数据库或以数据为中心的应用程序时,这很有益。数据驱动编程允许高效地查询、过滤和转换数据,通常利用如 SQL 或其他查询语言之类的声明式方法。
在某些情况下,数据驱动方法可能比面向对象方法更适合。以下是一些例子:
-
数据处理和分析:在处理大量数据或执行复杂分析任务时,使用专用库或框架的数据驱动方法可以提供更好的性能和灵活性。
-
数据库驱动应用程序:在开发与数据库交互频繁或依赖于外部数据的应用程序时,如 jOOQ 这样的数据驱动方法可以简化数据库交互并优化查询执行。
-
配置驱动系统:在行为主要由配置文件或外部数据决定的系统中,数据驱动方法允许轻松修改和定制,而无需更改代码。
-
基于规则的系统:在涉及复杂规则评估或基于数据做出决策的应用程序中,数据驱动方法可以提供一种透明且易于管理的表达和处理规则的方式。
重要的是要注意,面向对象编程和数据驱动编程不是相互排斥的,它们通常可以结合使用,以在 Java 应用程序中实现所需的功能和可维护性。两种方法之间的选择取决于系统的具体要求和要解决的问题的性质。
虽然数据驱动编程提供了一些优势,但也伴随着不可避免的权衡。以下是与数据驱动编程相关的某些权衡:
-
复杂性增加:数据驱动编程可能会引入额外的复杂性,尤其是在处理大型和复杂的数据结构时。在细粒度级别管理和操作数据可能需要复杂的代码和逻辑,使系统更难以理解和维护。
-
减少封装:在数据驱动编程中,重点主要在于数据和它的操作,而不是在对象内封装行为。这可能导致封装减少和数据暴露增加,可能损害系统的安全和完整性。
-
有限的表达性:虽然数据驱动编程提供了强大的数据操作和查询机制,但在表达复杂业务逻辑或数据之间的关系时可能存在限制。面向对象,强调行为和封装,通常可以提供更具有表达性和直观的解决方案。
尽管存在这些权衡,但在高效的数据操作、查询和灵活性至关重要的情况下,数据驱动编程可以非常有用。通过理解这些权衡,开发者在选择面向对象和数据驱动方法时可以做出明智的决定,考虑到他们应用程序的具体需求和限制。
面向对象是讨论企业应用时最流行的范式;然而,我们可以探索更多范式,例如数据驱动设计。
注意
本章简要概述了这一主题,但如果你想要深入研究,这里有两份推荐的资料。
第一份资料是 Yehonathan Sharvit 所著的《数据驱动编程》一书,其中讨论了这一模式,我们可以总结出三个原则:
-
代码是数据分离的
-
数据是不可变的
-
数据具有灵活的访问
第二份资料是一篇名为《数据驱动编程》的文章,作者是 Brian Goetz,他解释了 Java 的新特性,主要是记录,以及如何利用 Java 的优势。
在对数据驱动编程有一个概述之后,让我们深入探讨一个最受欢迎的框架,它可以帮助你设计和创建数据驱动应用程序:jOOQ。
什么是 jOOQ?
jOOQ 是一个强大的 Java 库,在企业应用场景中架起了面向对象和数据驱动编程之间的桥梁。虽然面向对象长期以来一直是开发企业应用的主导范式,但在某些情况下,数据驱动方法可以提供独特的优势。jOOQ 为开发者提供了一个优雅的解决方案,使他们能够利用 SQL 的力量,并在他们的 Java 代码中利用数据驱动设计原则。
面向对象因其能够通过封装数据和行为在对象内来模拟复杂系统而得到广泛采用。它强调代码组织、可重用性和模块化。然而,随着企业应用处理大量数据和复杂的数据库交互,纯面向对象的方法有时可能受到限制。
正是在这里,jOOQ 发挥了作用。jOOQ 使开发者能够无缝地将 SQL 和关系数据库操作集成到他们的 Java 代码中。它提供了一个流畅、类型安全且直观的 DSL,用于构建 SQL 查询和与数据库交互。通过拥抱数据导向的方法,jOOQ 赋予开发者直接与数据结构工作并利用 SQL 的全部力量进行查询、聚合和转换数据的能力。
使用 jOOQ,开发者可以摆脱传统 ORM 框架的限制,并对其数据库交互获得更细粒度的控制。通过拥抱数据导向的思维模式,他们可以优化性能,处理复杂的数据操作,并利用底层数据库系统提供的特性和优化。
通过使用 jOOQ,开发者可以充分利用面向对象和数据导向编程范式的优势。他们可以继续利用面向对象设计的成熟原则来封装对象内的行为,同时也能从数据导向编程的效率和灵活性中受益,以处理大量数据集和复杂的数据库操作。
在以下章节中,我们将更深入地探讨 jOOQ 的功能和特性。我们将深入研究 jOOQ 提供的用于构建 SQL 查询的 DSL,讨论其与 Java 代码的集成,并展示其在数据驱动设计中的优势。我们将共同发现 jOOQ 如何彻底改变我们与数据库的交互方式,并使面向对象编程和数据导向编程在企业应用程序中实现无缝融合。
虽然 jOOQ 提供了许多优势和好处,但也存在不可避免的权衡。以下是使用 jOOQ 可能带来的权衡之一:
-
学习曲线:jOOQ 引入了一种新的用于构建 SQL 查询的 DSL,这要求开发者熟悉其语法和概念。理解 jOOQ 的复杂性和有效利用它需要一定的学习曲线。
-
代码复杂性增加:与传统的 ORM 框架或直接 SQL 查询相比,使用 jOOQ 可能会引入额外的代码复杂性。DSL 语法以及 Java 对象与数据库记录之间的映射需求可能会导致代码量增加和潜在的复杂性,尤其是在处理复杂的数据库交互时。
-
数据库可移植性有限:jOOQ 根据底层数据库方言及其特定功能生成 SQL 查询。虽然 jOOQ 旨在为不同数据库提供统一的 API,但支持的特性和行为之间可能仍存在一些差异。这可能会限制代码在其他数据库系统之间的可移植性。
-
性能考虑:虽然 jOOQ 提供了高效的查询构建和执行,但性能可能仍然受到数据库模式设计、索引和查询优化等因素的影响。考虑 jOOQ 生成的查询的性能影响并相应优化数据库模式至关重要。
-
维护和升级:与任何第三方库一样,使用 jOOQ 引入了一个需要管理和维护的依赖项。跟上新版本、与不同 Java 版本的兼容性以及解决潜在的问题或错误可能需要在维护和升级期间付出额外的努力。
-
对底层数据库的抽象有限:与提供更高层次抽象的 ORM 框架不同,jOOQ 要求开发者深入了解 SQL 和底层数据库模式。如果你更喜欢更抽象的方法并隐藏数据库特定的细节,这可能是一个缺点。
-
潜在的阻抗不匹配:可能存在应用程序的面向对象特性与 jOOQ 的数据导向方法相冲突的情况。平衡这两种范式并在对象模型和数据库模式之间保持一致性可能具有挑战性,可能需要仔细的设计考虑。
虽然 jOOQ 为 Java 中的数据驱动编程提供了强大的功能,但在某些情况下可能存在更好的选择。权衡这些权衡与你的项目具体需求和限制至关重要。在决定 jOOQ 是否是你应用程序的正确工具时,考虑项目复杂性、团队经验、性能需求和数据库要求。
当我们谈论一个新的工具时,我们会将其与我们已知的东西进行比较;因此,让我们更深入地讨论 jOOQ 与 Java 持久化 API(JPA)之间的差异以及何时选择其中一个而不是另一个。
JPA 与 jOOQ 的比较
jOOQ 和 JPA 都是 Java 应用程序中数据库访问的流行选择,但它们有不同的方法和用例。以下是两者的比较以及何时选择其中一个而不是另一个:
jOOQ
-
以 SQL 为中心的方法:jOOQ 提供了一个流畅的 DSL,允许开发者以类型安全和直观的方式构建 SQL 查询。它提供了对 SQL 语句的细粒度控制,并允许利用 SQL 的全部功能。jOOQ 非常适合需要复杂查询、数据库特定功能和性能优化的场景。
-
数据驱动设计:jOOQ 拥抱数据导向编程范式,使其适合处理大型数据集和复杂的数据库操作。它提供了高效的数据操作能力,并允许开发者与底层数据结构紧密工作。jOOQ 非常适合具有中心数据处理和分析的应用程序。
-
数据库特定功能:jOOQ 支持各种数据库特定功能和函数,使开发者能够利用不同数据库系统提供的特定功能。当与特定数据库紧密合作并使用其独特功能时,它成为一个合适的选择。
JPA
-
ORM:JPA 专注于将 Java 对象映射到关系数据库表,提供更高层次的抽象。它允许开发者使用持久化实体,并自动将对象映射到数据库记录。对于高度依赖面向对象设计且需要对象与数据库无缝集成的应用程序,JPA 是一个很好的选择。
-
跨数据库可移植性:JPA 旨在提供一个可移植的 API,可以在不同的数据库上工作。它抽象了数据库特定的细节,允许应用程序在数据库系统之间切换,而代码更改最小。当您需要关于数据库后端的灵活性并希望避免供应商锁定时,JPA 是一个合适的选择。
-
快速应用开发:JPA 提供了自动 CRUD 操作、缓存和事务管理等功能,简化并加速了应用开发。它提供更高层次的抽象,减少了编写低级 SQL 查询的需求。当您优先考虑快速原型设计、生产力和关注业务逻辑而非数据库特定优化时,JPA 是有益的。
在 jOOQ 和 JPA 之间进行选择取决于您特定的项目需求。如果您的应用程序是数据密集型,需要复杂的查询,并且需要精细控制 SQL,那么 jOOQ 可能是一个更好的选择。另一方面,如果您优先考虑面向对象的设计、跨不同数据库的可移植性以及快速应用开发,那么 JPA 可能更适合。还值得考虑混合方法,您可以在应用程序的不同部分同时使用 jOOQ 和 JPA,根据需要利用每个库的优势。
在介绍 jOOQ 之后,让我们将其应用到实践中,这次与 Jakarta EE 结合。本书展示了 Jakarta EE 在几个持久化框架中的应用;在本章中,我们将向您展示如何使用 jOOQ 结合 Jakarta EE。
使用 jOOQ 与 Jakarta/MicroProfile
在本节中,我们将探讨 jOOQ 与 Jakarta EE 和 MicroProfile 的集成,这两个 Java 生态系统中的强大框架。jOOQ 以其数据驱动的方法和以 SQL 为中心的能力,可以无缝地补充 Jakarta EE 的企业级功能和 MicroProfile 面向微服务的实践。通过结合这些技术,开发者可以解锁一个强大的工具集,用于构建健壮、可扩展且数据驱动的 Java 应用程序。
Jakarta EE,以前称为 Java EE,是一套规范和 API,为在 Java 中构建企业应用程序提供了一个标准化的平台。它提供了一系列功能,包括 servlets、JavaServer Faces(JSF)、Enterprise JavaBeans(EJB)和 JPA。开发者可以利用 Jakarta EE 的成熟生态系统和行业标准来创建可伸缩和可维护的应用程序。
另一方面,MicroProfile 是一个由社区驱动的倡议,专注于在 Java 中构建基于微服务的应用程序。它提供了一套轻量级和模块化的规范和 API,专为微服务架构量身定制。MicroProfile 允许开发者利用 JAX-RS、JSON-P 和 CDI 等技术,在微服务中实现更大的灵活性和敏捷性。
将 jOOQ 与 Jakarta EE 和 MicroProfile 结合使用,可以为您的 Java 应用程序带来两者的最佳特性。以下是这种组合的一些优势和用例:
-
增强的数据库交互:jOOQ 以 SQL 为中心的理念允许您直接在 Java 代码中编写复杂和优化的 SQL 查询。它使您能够高效且细致地控制数据库交互,从而实现优化的数据检索、更新和分析。将 jOOQ 与 Jakarta EE 和 MicroProfile 集成,将使您能够在企业或微服务应用程序中无缝利用 jOOQ 强大的查询构建功能。
-
数据驱动的微服务:架构通常需要在多个服务之间进行高效的数据访问和处理。将 jOOQ 与 MicroProfile 结合使用,允许您设计利用 jOOQ 数据驱动方法的微服务,以实现无缝的数据库集成。它使每个微服务能够独立处理其数据操作,并从 jOOQ 的 DSL 提供的性能和灵活性中受益。
-
与 JPA 和 ORM 的集成:Jakarta EE 应用程序通常利用 JPA 和 ORM 框架进行数据库交互。通过将 jOOQ 与 Jakarta EE 及其持久化能力集成,您可以利用 jOOQ 以 SQL 为中心的理念以及 JPA 面向对象的设计的优势。它允许您高效地处理复杂查询,并利用 JPA 的实体管理、事务和缓存功能,从而实现强大且灵活的数据访问层。
-
横切关注点和可伸缩性:Jakarta EE 和 MicroProfile 提供了大量针对横切关注点(如安全、日志记录和监控)的功能。通过将 jOOQ 与这些框架集成,您可以利用它们的能力来确保一致的安全策略、高效的日志记录和监控数据库交互,贯穿您的应用程序或微服务架构。
在本节中,我们将探讨实际示例,并展示如何有效地将 jOOQ 与 Jakarta EE 和 MicroProfile 结合使用。我们将展示 jOOQ 与 Jakarta EE 持久化 API 的集成,说明在 MicroProfile 微服务架构中使用 jOOQ 的方法,并讨论利用这些技术结合力量的最佳实践。
到本节结束时,你将牢固地理解如何一起使用 jOOQ、Jakarta EE 和 MicroProfile,这将使你能够构建健壮且数据驱动的 Java 应用程序,适用于企业和微服务环境。让我们深入探讨这个强大组合的可能性。
为了展示组合潜力,我们将创建一个简单的 Java SE 项目,使用 Maven,但作为一个亮点,我们可以将此代码顺利转换为微服务。该项目是一个包含单个表Book
的 CRUD,我们将在其中执行操作,就像在一个可执行类中一样。
我们仍然会使用一个简单的数据库项目,即 H2,以降低我们项目的要求。但你可以将其在生产环境中替换为 PostgreSQL、MariaDB 等。实际上,这正是关系型数据库的美丽之处;与 NoSQL 数据库相比,我们可以更容易地在数据库之间进行切换,影响不大:
-
让我们从 Maven 项目的配置开始,我们将包括依赖项:
<dependency> <groupId>org.jboss.weld.se</groupId> <artifactId>weld-se-shaded</artifactId> <version>${weld.se.core.version}</version></dependency><dependency> <groupId>io.smallrye.config</groupId> <artifactId>smallrye-config-core</artifactId> <version>2.13.0</version></dependency><dependency> <groupId>org.jooq</groupId> <artifactId>jooq</artifactId> <version>3.18.4</version></dependency><dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.1.214</version></dependency><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.9.0</version></dependency>
-
在 Maven 依赖项之后,下一步是包括生成数据库结构的插件,然后基于此表创建 jOOQ。我们将开始数据结构,并使用插件执行以下查询;正如你将看到的,我们将创建模式并包含一些书籍。我们不会展示插件源代码;更多详细信息请参阅存储库源代码:
DROP TABLE IF EXISTS book;CREATE TABLE book ( id INT NOT NULL, title VARCHAR(400) NOT NULL, author VARCHAR(400) NOT NULL, release INT, CONSTRAINT pk_t_book PRIMARY KEY (id));INSERT INTO book VALUES (1, 'Fundamentals of Software Architecture', 'Neal Ford' , 2020);INSERT INTO book VALUES (2, 'Staff Engineer: Leadership beyond the management track', 'Will Larson' , 2021);INSERT INTO book VALUES (3, 'Building Evolutionary Architectures', 'Neal Ford' , 2017);INSERT INTO book VALUES (4, 'Clean Code', 'Robert Cecil Martin' , 2008);INSERT INTO book VALUES (5, 'Patterns of Enterprise Application Architecture', 'Martin Fowler' , 2002);
-
Maven 基础设施已经就绪,下一步是定义配置以获取数据库连接并将其提供给 CDI 上下文。我们将结合 Jakarta CDI 和 Eclipse MicroProfile Config,提取如 JDBC URL 和凭证等属性。
-
我们将把这些凭证信息,如用户名和密码,放入
microprofile-config.properties
文件中;然而,请记住,你不应该用生产凭证这样做。我做的事情之一是通过环境覆盖这些配置。因此,开发者会在生产环境中理解这一点,而无需了解它;开发者知道这些属性,而无需理解生产属性。这是将实现推向 Twelve-Factor App(12factor.net
)边缘的一个优点:@ApplicationScopedclass ConnectionSupplier { private static final Logger LOGGER = Logger .getLogger(ConnectionSupplier.class.getName()); private static final String URL= "db.url"; private static final String USER = "db.username"; private static final String PASSWORD = "db.password"; private static final Config CONFIG = ConfigProvider.getConfig(); @ApplicationScoped @Produces public Connection get() throws SQLException { LOGGER.fine("Starting the database connection"); var url = CONFIG.getValue(URL, String.class); var password = CONFIG.getOptionalValue(PASSWORD, String.class).orElse(""); var user = CONFIG.getValue(USER, String.class); return DriverManager.getConnection( url, user, password); } public void close(@Disposes Connection connection) throws SQLException { connection.close(); LOGGER.fine("closing the database connection"); }}
-
CDI 可以在你的容器上下文中创建和销毁 bean 实例。我们将使用这一点来开发和关闭连接,避免在我们的应用程序中出现任何连接泄漏。一旦我们有了连接,就让我们创建
DSLContext
实例——这是我们的数据和 Java 之间的桥梁,提供了一个通过fluent-API
的简单且安全的方式:@ApplicationScopedclass ContextSupplier implements Supplier<DSLContext> { private final Connection connection; @Inject ContextSupplier(Connection connection) { this.connection = connection; } @Override @Produces public DSLContext get() { return using(connection, SQLDialect.H2); }}
-
我们可以使
Connection
和DSLContext
都可用并由 CDI 处理;下一步是使用它们来与关系数据库交互。你可以将DSLContext
注入为一个字段,但由于我们是用 Java SE 创建的,我们将创建一个SeContainer
并选择它,如下面的代码所示:try (SeContainer container = SeContainerInitializer.newInstance().initialize()) { DSLContext context = container.select(DSLContext.class).get();//...}
-
你准备好行动了吗?让我们利用 jOOQ 执行一个无需创建实体的 CRUD 操作,它基于数据库模式,将生成我们可以工作的数据结构。操作的第一步是插入。代码显示了记录创建,我们可以设置属性并根据 setter 方法存储它们:
BookRecord record = context.newRecord(BOOK);record.setId(random.nextInt(0, 100));record.setRelease(2022);record.setAuthor("Otavio Santana");record.setTitle("Apache Cassandra Horizontal scalability for Java applications");record.store();
-
有数据,我们可以从数据库中读取这些信息;使用流畅的 API 和
select
方法以及DSLContext
类,我们可以执行多个选择查询操作。查询将按标题顺序选择书籍。这种方法的优点是,我们大多数时候会在应用级别看到查询是否兼容,因为如果你进行任何不规则操作,它将不会编译:Result<Record> books = context.select() .from(BOOK) .orderBy(BOOK.TITLE) .fetch();books.forEach(book -> { var id = book.getValue(BOOK.ID); var author = book.getValue(BOOK.AUTHOR); var title = book.getValue(BOOK.TITLE); var release = book.getValue(BOOK.RELEASE); System.out.printf("Book %s by %s has id: %d and release: %d%n", title, author, id, release);});
-
最后两个步骤是
更新
和删除
;你可以执行其他操作,探索流畅的 API 功能。我们可以定义尽可能多的参数和条件。我们正在使用的示例将设置where
条件在ID
值上:context.update(BOOK) .set(BOOK.TITLE, "Cassandra Horizontal scalability for Java applications") .where(BOOK.ID.eq(randomId)) .execute();context.delete(BOOK) .where(BOOK.ID.eq(randomId)) .execute();
我们可以利用 jOOQ API 探索整个 CRUD 操作,而不需要创建实体。数据方法允许从模式生成结构。我们可以保证我的应用程序将能够与最后一个实体一起工作,而无需任何工作。这就结束了我们今天的 jOOQ 之旅。
摘要
本章深入探讨了数据驱动编程及其与面向对象方法的权衡。我们探讨了拥抱数据驱动思维的好处和挑战,理解在某些场景下,以数据为导向的方法可以提供比传统面向对象范式独特的优势。然后我们见证了 jOOQ,一个强大的 Java 库,如何弥合面向对象编程和数据驱动编程之间的差距,使开发者能够在 Java 代码中充分利用 SQL 和数据操作的全部功能。
我们还检查了 jOOQ 与 Jakarta EE 和 MicroProfile 的集成,这两个框架在开发企业级和微服务应用中广泛使用。通过结合这些技术,开发者可以同时利用 jOOQ 的数据驱动能力以及 Jakarta EE 提供的企业级功能和 MicroProfile 面向微服务的架构方法。这种集成使得高效的数据库交互、对 SQL 查询的精细控制,以及在统一架构中利用面向对象和数据导向的设计原则成为可能。
通过结合 jOOQ 所提供的数据驱动方法、Jakarta EE 和 MicroProfile 的企业级特性,以及探索 MicroStream 的开创性功能,我们可以将我们的应用程序提升到新的性能、可扩展性和效率高度。我们正站在数据库驱动应用程序开发新时代的边缘,在这里,数据的力量与执行速度相得益彰。
那么,让我们开始我们旅程的下一章,在这里我们将深入探索 MicroStream 的世界,并释放我们持久化层、Jakarta EE 和 MicroProfile 驱动的应用程序的真正潜力。随着我们拥抱这一前沿技术并见证它对我们开发过程和应用程序性能带来的变革,前方将充满激动人心的时刻。
第八章:使用 Eclipse Store 的超快内存持久化
NoSQL 和 SQL 数据库在处理其目标用例时可以非常出色且强大。然而,寻求最佳性能的用户需要意识到其他可能影响应用程序在处理效率、速度甚至代码设计方面的因素。在这方面,可以提前提到一个例子:这些数据库解决方案中的大多数将需要在数据库模式与应用程序数据模型之间进行某种类型的映射。正如你可以想象的那样,映射需要在应用程序和数据库之间数据流动的每次都发生。这种被称为对象关系阻抗不匹配的特性,有很高的潜力影响我们之前提到的几乎所有数据库类型——SQL 和 NoSQL。
在本章中,我们将讨论另一种数据库范式,即内存数据库。除了显著的性能提升外,这绝对是在处理数据处理、Web 和移动应用、缓存和实时分析等用例时可以利用的数据库类型。对于此类场景,高性能的数据存储解决方案、低延迟的数据访问和实时数据处理似乎是有希望的替代方案,因为它们允许提供超快的持久化解决方案。
我们将使用Eclipse Store,一个高性能、轻量级的内存持久化解决方案,来探索上述概念。这个数据库的一个要点是更快,消除额外处理,减少代码大小和复杂性,尤其是在与 SQL 数据库和 Hibernate/JPA 集成相比时。
在本章中,我们将涵盖以下主要内容:
-
为什么每个数据库操作都会秘密地增加延迟?我们将了解对象关系阻抗不匹配是什么以及它如何影响持久化性能。
-
什么是内存持久化存储,它与其他数据库类型有何不同?
-
探索 Eclipse Store。
-
Eclipse Store 与 Jakarta/MicroProfile。
技术要求
以下为本章的技术要求:
-
Java 17
-
Git
-
Maven
-
任何首选 IDE
本章的源代码可在github.com/PacktPublishing/Persistence-Best-Practices-for-Java-Applications/tree/main/chapter-08
找到。
对象关系阻抗不匹配解释
作为 Java 开发者,我们知道面向对象编程(OOP)范式的力量——它允许我们根据多态性、封装、继承、接口、创建自定义类型等探索多个模式。我们非常喜欢它!主要是因为我们可以将这些方法与设计模式结合起来,创建干净且易于阅读的代码。
不幸的是,许多这些面向对象的概念和行为在数据库端不可用,这种特性被称为阻抗不匹配。
对象-关系映射(ORM)阻抗不匹配是在将数据在面向对象语言和关系数据库管理系统(RDBMS)之间映射时发生的一种特定类型的阻抗不匹配。
如 Java、Python 和 C#这样的面向对象(OOP)语言使用对象来表示和操作数据,而关系数据库使用表来存储和管理数据。ORM 是一种技术,通过将对象映射到数据库表以及反向映射来弥合这两种不同范式之间的差距。
图 8.1 – 数据库模式上 Java 对象模型等价映射的示例
ORM 阻抗不匹配发生的原因是对象和表具有不同的属性和结构。例如,对象可以具有复杂的数据类型、继承和多态,而表由简单的行和列组成。此外,对象可以与其他实体有关系,而表则行与行之间存在关系。
为了减轻这种阻抗不匹配并提高开发效率,ORM 工具提供了映射策略,允许开发人员将对象映射到表以及反向映射。这些策略可以包括 ORM 模式,如表继承、关联映射和延迟加载。
尽管有这些策略,但由于查询语言、性能问题和可扩展性问题,ORM 阻抗不匹配仍然可能发生。因此,开发人员需要了解使用 ORM 工具所涉及的限制和权衡,并在必要时考虑替代解决方案。
在映射处理方面,另一个需要强调的事项是它使用映射器。这个映射器在每次应用程序-数据库交互中使用,负责将实体转换为/从实体转换,并且需要大量的 CPU 资源,这可能会导致比执行的查询本身更重。
这个映射器具有一种在范式之间通信的明亮机制。即使有缓存和最先进的性能提升技术,这个过程在许多应用程序中也可能是一个噩梦。
我们可以采用的一种技术来克服这个挑战,并避免在每次数据库操作上都进行额外的 Java 处理,就是 Eclipse Store。让我们深入了解这个内存数据库是什么,它是如何工作的,以及如何开始使用它。
Eclipse Store 是一种基于 Java 的开源内存数据存储技术,它提供了一种新的对象持久化方法。
与依赖于 ORM 将对象映射到关系表的传统数据库不同,Eclipse Store 的内部机制定位并使用堆上的 Java 对象。它可以直接从内存中获取信息,消除映射或序列化的需要。这种方法由于避免了 ORM 阻抗不匹配并减少了昂贵的数据库访问需求,从而实现了更快的应用程序性能。
Eclipse Store 10 年前作为 MicroStream 的一个闭源项目开始。最近,MicroStream 开源了,并成为两个 Eclipse 项目,其中一个是 Eclipse Store。
Eclipse Store 提供了一个 Java API,允许开发者直接在内存中存储、加载和操作 Java 对象,无需访问单独的数据库。数据可以选择外部持久化,在这种情况下,它以压缩的二进制格式存储,从而允许高效地使用内存资源。这种方法消除了 ORM 的需求,ORM 可能耗时且资源密集,尤其是对于复杂的对象层次结构。
Eclipse Store 主要在内存中运行;因此,它可以提供对数据的超快读写访问,使其非常适合高性能数据处理应用,如实时分析、金融交易和游戏。
除了其速度和性能优势外,Eclipse Store 还提供了高度的可灵活性和可扩展性。它支持分布式数据结构,允许数据跨多个节点分布,并与其他数据库或数据源集成。
总体而言,Eclipse Store 为传统基于 ORM 的数据库提供了一个有吸引力的替代方案,为需要超快数据处理的程序提供了更快的性能和更低的复杂性。
图 8.2 – Eclipse Store 架构概述
使用 Eclipse Store 作为内存数据存储解决方案的应用程序可以依赖以下功能:
-
快速性能:快速高效地集成,依赖于快速读写操作,无需 ORM 工具的额外开销。
-
内存存储:快速访问数据,因为它直接从内存堆中获取。
-
易于使用:由于该技术旨在简单易用,具有熟悉的 Java 语法和可选的注解,因此开发者可以快速上手,轻松定义和持久化数据。
-
无外部依赖:使用它非常简单,因为您唯一需要的依赖项就是 Eclipse Store(它基本上依赖于一个日志库)。您不应担心库冲突或兼容性问题。
-
轻量级:一种不需要大量资源或配置的数据存储解决方案,易于设置和部署。
-
灵活性:从所有数据类型(非常少数例外)中选择,并在各种应用规模中使用它——从小型项目到企业级系统。
-
开源:Eclipse Store 提供多种类型,其中之一是免费开源项目,这意味着无限使用和定制,以满足您的特定需求。
-
高可用性:使用时,它提供内置的高可用性和冗余功能,确保您的数据始终可用且受保护。
-
可伸缩性:轻松添加更多节点或资源来处理不断增长的数据量,因为数据库是从零开始设计的,以满足这些目标。
在以下章节中,我们将深入探讨这个强大且灵活的内存数据管理和持久化解决方案,它可以帮助开发者构建快速高效的应用程序。让我们了解 Eclipse Store 的基本知识,通过代码示例,并理解如何使用现代、云原生、内存中、开源的解决方案创建一个超快的应用程序。
内存持久化存储 – Eclipse Store
Eclipse Store 是一种数据存储解决方案,由于去除了映射过程、查询的解析操作,避免了传统查询执行的缺点,并使用独特且先进的序列化过程,因此运行速度快。Eclipse Store 估计 90%的查询时间基于这些操作。
基准测试(eclipsestore.io/
)显示的结果可以比使用 JPA 的 SQL 数据库快1,000 倍。从开发者的角度来看,积极的一面是学习曲线短,安装和使用简单。
要开始,第一步是安装 Eclipse Store,这就像在应用程序的 Maven 配置中添加一个依赖项一样简单。
该解决方案的一些关键点包括使用纯 Java 实现闪电般的内存数据处理,具有微秒级查询时间、低延迟数据访问和处理海量数据负载的能力。这种方法可以实现显著的 CPU 功率节省,减少二氧化碳排放,并降低数据中心成本。
内存是易失的;因此,为了作为持久存储,数据必须存储在其他地方。Eclipse Store 的默认存储目标是文件系统,在本地文件夹中。这是一个不错的起点,但考虑到生产需求,你可能希望将数据保存在不同的位置。好消息是你可以从超过 15 个不同的选项中进行选择:存储目标(docs.microstream.one/manual/storage/storage-targets/index.html
)从关系型数据库到 NoSQL 数据库,以及 blob 服务。例如,MariaDB、PostgreSQL、Redis 和 Amazon S3。
使用这项技术解锁的另一个可能性,你可能也会喜欢,那就是你现在可以按照业务需求创建自定义的图结构,并使用纯 Java 进行查询(无需使用 SQL 等!),这降低了开发者的认知负荷。
您可以使用 Eclipse Store 与多种运行时技术一起使用,例如 Helidon、Spring 和 Quarkus。在这本书中,我们解释了如何仅依靠 CDI 来使用它;换句话说,您将学习如何在不考虑它将要集成的供应商或平台的情况下使用这项技术。一旦我们仅使用 Java 标准 API 掌握基础知识,我们就应该能够开始尝试不同的 Jakarta EE 和 MicroProfile 供应商,例如 Helidon、Wildfly 和 Payara。
在我们的上下文中,CDI 充当我们企业架构组件之间的粘合剂。因此,它是使您能够将 Eclipse Store 作为库、组件、模块等注入的机制。现在让我们开始了解如何使用内存数据库存储和 CDI 持久化和管理数据。
存储和管理内存中数据的基本方法
为了进一步解释 Eclipse Store,让我们看看它是如何工作的:我们将使用 Java SE 和 CDI 创建我们的第一个示例。这个示例的目标是展示如何为汽车创建一个平滑的 CRUD 流程,其中每辆汽车都应该将其型号、制造商和年份作为属性。
首先,使用maven-archetype-quickstart
创建一个简单的 Maven 项目。安装 Eclipse Store 很简单;您只需要将其依赖项添加到 Maven 项目中。以下是一个pom.xml
的示例:
<dependency> <groupId>one.microstream</groupId>
<artifactId>eclipse-store-integrations-cdi</artifactId>
<version>07.00.00-MS-GA</version>
</dependency>
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-shaded</artifactId>
<version>3.1.9.Final</version>
</dependency>
<dependency>
<groupId>io.smallrye.config</groupId>
<artifactId>smallrye-config</artifactId>
<version>2.7.0</version>
</dependency>
依赖项设置完成后,我们就可以开始编码了。以下 Java 类,Car
实体,是我们的数据模型。根据 Eclipse Store 的建议,属性应该定义为final
,从而产生一个不可变类:
public class Car { private final String plate;
private final Year year;
private final String make;
private final String model;
// add getters and setters
// they are removed here for brevity
}
下一步是创建一个图形或结构来存储数据并提供给我们。为了表示汽车集合,我们将创建一个Garage
存储库,所有数据操作都应该在这里发生。
您可以自由地操作汽车的数据或创建任何其他新的数据结构;您使用纯 Java 编写代码,并将其留给 Eclipse Store 来处理其余部分。我们唯一必须使用的组件是识别这个Garage
作为一个结构。为此,使用@Storage
注解对其进行注解。
@Storage
注解表示将由 Eclipse Store 处理的图形的根对象。在这种情况下,Garage
是我们的根对象:
@Storagepublic class Garage {
private List<Car> cars;
public Garage() {
this.cars = new ArrayList<>();
}
public void add(Car car) {
this.cars.add(car);
}
public List<Car> getCars() {
return this.cars.stream()
.collect(Collectors.toUnmodifiableList());
}
public Optional<Car> findByModel(String model) {
return this.cars.stream().filter(c ->
c.getModel().equals(model))
.findFirst();
}
}
这个示例涵盖了所有必要的代码和依赖项,使我们能够使用Garage
通过型号添加和查找汽车。而且它只使用 Java SE,没有特定的运行时!
接下来,我们将介绍第二个示例,重点关注服务层,在那里我们将实现实体数据在移动到存储之前的验证。这种验证相当直接;我们将检查car
是否为null
。
为了操作cars
数据,我们需要在CarService
中有一个Garage
的实例。为了使用 CDI 为我们提供这个类的实例,我们可以使用 CDI 的@Inject
注解。
当执行数据库操作时,我们可能希望它在事务中发生,对吧?是的,在关系型数据库中可能如此。在这里,我们依赖于@Store
注解来配置哪些方法应该允许更改数据结构。观察以下public
void add
(Car car) 方法及其注解:
@ApplicationScopedpublic class CarService {
@Inject
private Garage garage;
@Store
public void add(Car car) {
Objects.requireNonNull(car, "car is required");
this.garage.add(car);
}
public List<Car> getCars() {
return this.garage.getCars();
}
public Optional<Car> findByModel(String model) {
Objects.requireNonNull(model, "model is required");
return this.garage.findByModel(model);
}
}
太好了,到目前为止我们已经有足够的代码来测试它并享受乐趣了,所以让我们执行它!为了消费我们的CarService
API,我们需要一个新的类,我们可以称之为App
,以及一个public static void main(final String[] args)
方法。在下面展示的代码的前几行中,请注意以下内容:
-
获取所有车辆列表的服务 API,
service.getCars()
-
由服务 API 调用的搜索操作,
service.findByModel("Corolla")
当第一次运行代码时,你会在输出日志中观察到检索到的车辆列表将是空的;然而,当你再次运行它时,你可以看到数据:
public static void main(final String[] args) { try (SeContainer container =
SeContainerInitializer.newInstance().initialize()) {
final CarService service =
container.select(CarService.class).get();
System.out.println("The current car list: " +
service.getCars());
Optional<Car> model =
service.findByModel("Corolla");
System.out.println("Entity found: " + model);
Car dodge = Car.builder()
.make("Dodge")
.model("Wagon")
.year(Year.of(1993))
.plate("JN8AE2KP7D9956349").build();
Car ford = Car.builder()
.make("Ford")
.model("F250")
.year(Year.of(2005))
.plate("WBANE73577B200053").build();
Car honda = Car.builder()
.make("Honda")
.model("S2000")
.year(Year.of(2005))
.plate("WBANE73577B200053").build();
Car toyota = Car.builder()
.make("Toyota")
.model("Corolla")
.year(Year.of(2005))
.plate("WBANE73577B200053").build();
service.add(ford);
service.add(honda);
service.add(toyota);
service.add(dodge);
}
System.exit(0);
}
如果你尝试运行此代码几次,你可能会注意到在第三次尝试左右,看起来项目正在重复!这是我们的list
对象的行为,这可以通过将其结构更新为Set
而不是List
并确保Car
实体适当地实现了 equals 和 hashcode 来解决。
应用程序的properties
文件保存了引擎中使用的目录和线程数的设置配置。通过与Eclipse MicroProfile Configuration(download.eclipse.org/microprofile/microprofile-config-3.0/microprofile-config-spec-3.0.html
)的集成,可以简化外部化配置。我们在上一章关于 JNoSQL 的章节中看到了类似的配置方法,因为它依赖于相同的底层配置机制:
one.Eclipse Store.storage.directory=target/dataone.Eclipse Store.channel.count=4
很简单,对吧?在我们继续前进之前,让我们了解之前列出的第二个设置的重要性,one.Eclipse Store.channel.count
。这个内存解决方案可以通过多种方式微调,其中之一是调整引擎可以用来执行 I/O 操作的数量(线程)。这个配置应该始终配置为等于 2n 的值。
使用基于规范的策略外部化配置简化了服务维护。在更改应用程序实现时,这应该需要非常少的努力,正如我们在创建基于此示例代码的微服务时将在下一节中看到的那样。简化维护的原因是所选的内存数据库存储,Eclipse Store,使用 CDI,这恰好也是 MicroProfile 和 Jakarta EE 的核心引擎。
在探索如何配置和实现依赖于内存数据存储的服务之后,我们将接下来看到如何将代码示例作为微服务的一部分进行迁移。
使用 Jakarta EE 和 MicroProfile 的内存数据存储
多亏了 MicroProfile 和 Jakarta EE 规范中的引擎,我们可以非常容易地选择最适合应用程序目标的一个。在 第五章 中,我们讨论了这两个规范以及为什么它们对 Java 社区至关重要:
-
要开始,您可以访问 MicroProfile 网站 (
start.microprofile.io/
) 和入门项目。它就像 Spring 初始化器一样,用于基于 Spring 的应用程序。 -
一旦进入页面,请确认 MicroProfile 版本 3.3 可用,并选择一个选项。确保勾选 Config 复选框以节省时间并获得一些自动生成的基本文件。
-
对于这个例子,我们将使用 Helidon 运行时。
图 8.3 – MicroProfile 入门网站
-
接下来,我们只需将 Eclipse Store 依赖项添加到
pom.xml
应用程序中,因为 Eclipse MicroProfile 实现已经提供了 config 和 CDI:<dependency> <groupId>one.microstream</groupId> <artifactId>eclipse-store-integrations-cdi</artifactId> <version>07.00.00-MS-GA</version></dependency>
-
接下来,使用古老的程序员技术…复制和粘贴!您可以将前一个项目的依赖项配置复制到您的新基于 MicroProfile 的项目中。
现在,我们需要能够修改一个实体,例如 Car
实体。由于实体是不可变的,创建新实例必须通过其构造方法来完成。这种良好实践不是数据存储解决方案 Eclipse Store 所要求的,但它是用于 REST 端点的实体的一种好方法。
在 Car
类中,识别并使用来自 JSON 绑定规范的 @JsonCreator
和 @JsonProperty
注解来注释其构造方法,这些规范可以在 jakarta.ee/specifications/jsonb/2.0/
找到。请注意,这些不是 Eclipse Store 所要求的注解。
-
将
Year
类型更改为Integer
,以避免创建一个用于序列化和反序列化数据的自定义接口:public class Car { private final String plate; private final Integer year; private final String make; private final String model; @JsonbCreator public Car(@JsonbProperty("plate") String plate, @JsonbProperty("year") Integer year, @JsonbProperty("make") String make, @JsonbProperty("model") String model) { this.plate = plate; this.year = year; this.make = make; this.model = model; }}
我们正在将 Car
实体构建为一个不可变类;因此,其字段是最终的,并且可以通过构造方法上的注入来设置。为了帮助我们实现这个目标,我们将使用一个与 JSONB 兼容的实现。
- 添加
@JsonbCreator
注解,将这个类转换为 API 的合格 Bean,并且使得@JsonProperty
注解可以将相应的参数与定义的 JSON 属性链接起来。
注意
一旦通过 JSON 完成创建过程,我们就可以删除 CarBuilder。
我们将创建一个资源,我们将看到路径和 URL。我们将通过 URL 暴露我们创建的所有服务,因此我们需要通过查找型号并插入一辆车来列出汽车:
@ApplicationScoped@Path("garage")
public class GarageResource {
@Inject
private CarService service;
@GET
public List<Car> getCars() {
return this.service.getCars();
}
@Path("{model}")
@GET
public Car findByModel(@PathParam("model") String
model) {
return this.service.findByModel(model)
.orElseThrow(() -> new
WebApplicationException(NOT_FOUND));
}
@POST
public Car add(Car car) {
this.service.add(car);
return car;
}
}
我们的资源类已经准备好在我们的微服务中使用。正如你所看到的,我们正在注入CarService
并使用这个集成来连接到这个GarageResource
,我们可以通过 HTTP 请求来探索它。
我们已经准备好了所有代码;让我们构建并执行应用程序:
mvn clean packagejava -jar target/garage.jar
当服务启动后,我们可以通过创建一个消耗此服务的客户端前端或使用 HTTP 客户端 UI 来探索它。我们将使用curl
运行我们的示例。我们将创建三辆车,然后从服务中返回它们:
curl --location --request POST 'http://localhost:8080/garage' \--header 'Content-Type: application/json' \
--data-raw '{"make": "Dodge", "model": "Wagon", "year": 1993, "plate": "JN8AE2KP7D9956349"}'
curl --location --request POST 'http://localhost:8080/garage' \
--header 'Content-Type: application/json' \
--data-raw '{"make": "Ford", "model": "F250", "year": 2005, "plate": "WBANE73577B200053"}'
curl --location --request POST 'http://localhost:8080/garage' \
--header 'Content-Type: application/json' \
--data-raw '{"make": "Honda", "model": "S2000", "year": 2005, "plate": "WBANE73577B200053"}'
curl --location --request POST 'http://localhost:8080/garage' \
--header 'Content-Type: application/json' \
--data-raw '{"make": "Toyota", "model": "Corolla", "year": 2005, "plate": "WBANE73577B200053"}'
curl --location --request GET 'http://localhost:8080/garage/Corolla'
curl --location --request GET 'http://localhost:8080/garage'
这是一个使用 curl 程序发出的示例 HTTP 请求;请随意使用你想要的任何 HTTP 客户端,例如 Postman。
我们还需要将 Eclipse Store 设置附加到这个应用程序中。另一个要点是我们更新了ApplicationPath
注解为"/"
。此外,我们添加了Garage
资源,但在这里不会提供全部细节;请查看仓库以获取所有详细信息。
摘要
Eclipse Store 带来了新的持久化视角;你可以通过减少映射器进程来提高性能。它不仅影响应用程序的响应时间,还影响云成本,因为它需要更少的机器,从而降低了基础设施成本。
本章探讨了 Java SE 和微服务中 CDI 的集成,以及使用 MicroProfile。我们看到了几个数据库和持久化解决方案的力量,但我们如何将它们合并?你将在下一章中找到答案,关于多语言持久化。
第三部分:持久化的架构视角
在本书的这一部分,我们从架构的角度探讨了持久化,探讨了与设计和实现健壮和可扩展持久化解决方案相关的各种主题。本节深入探讨了与现代 Java 解决方案中持久化相关的架构考虑和挑战,从多语言持久化到现代化策略。
这部分包含以下章节:
-
第九章**,持久化实践:探索多语言持久化
-
第十章**,构建分布式系统:挑战和反模式
-
第十一章**,现代化策略和数据集成
-
第十二章**,现代 Java 解决方案中持久化的最终考虑
第九章:持久化实践 – 探索多语言持久化
软件开发已经变得更加复杂,需要更多的集成,我们需要同时创新来使我们的生活更轻松。一个好的选择是利用多语言持久化来利用几个数据库。
当我们谈论持久化解决方案时,大约有 400 种,它们具有不同的类型、结构和特定行为,这些行为在特定情况下是有意义的。多语言持久化的哲学是使用工具来找到正确的解决方案。
本章将介绍多语言持久化的原理以及如何使用 Java 来实现它。
我们将讨论以下主题:
-
多语言持久化的权衡
-
理解领域驱动设计(DDD)和 Jakarta
-
Jakarta Data
技术要求
本章的技术要求如下:
-
Java 17
-
Git
-
Maven
-
任何首选的 IDE
本章的源代码可在github.com/PacktPublishing/Persistence-Best-Practices-for-Java-Applications/tree/main/chapter-09
找到。
多语言持久化的权衡
多语言持久化是一种数据存储方法,其中使用多种类型的数据库来满足应用程序内的不同需求。术语多语言指的是使用各种语言或工具,在这个上下文中,它指的是使用多种类型的数据库。
在传统的单体应用程序中,通常使用单个数据库来存储所有数据类型。然而,随着应用程序变得更加复杂,这种方法可能会变得不那么有效。然而,多语言持久化允许开发者根据可扩展性、数据结构和查询需求等因素为每个用例选择最佳的数据库。
例如,一个社交媒体平台可能会使用文档数据库 MongoDB 来存储用户资料和活动流,使用图数据库 Neo4j 来分析社交关系,以及使用关系数据库 MySQL 来管理交易和支付。
通过利用多个数据库,多语言持久化可以帮助提高应用程序的性能、可扩展性和灵活性。然而,它也带来了在多个系统之间管理数据一致性、迁移和备份的额外复杂性。
多语言的理念总是好的,并为应用程序提供了几个机会。核心思想是合理的:在最佳场景下利用数据库是极好的。但是,即使有多语言持久化,也存在权衡,就像任何软件架构决策一样。
更多的数据库也意味着更大的成本和基础设施知识来处理特定的持久化解决方案。请留意这一点。
在 Java 中,更多的数据库意味着应用程序中更多的依赖项,这可能会加剧 jar-hell 的头痛。微服务方法将帮助您在这种情况下,其中您环境中的每个数据库都有自己的接口;它还有助于将技术从业务中隔离出来。
从代码设计角度来看,有端口和适配器模式,或者六边形模型,您可以在其中将应用程序的核心逻辑与持久化层隔离开来。然而,正如提到的,更多的层意味着更多的代码,这意味着对可维护性和错误的担忧。
从简单的三层开始,如模型-视图-控制器(MVC)架构模式,并将它们隔离开来是一个良好的开始,例如从单体而不是微服务开始。当需要时,就去做,并重构代码。
有时,我们只需要这些层中的某些层来满足我们的应用程序;从足够的架构开始是一个管理架构风险的绝佳方式。
识别从/到业务层的抽象,并尽可能避免将其与持久化层耦合,对于进化式架构至关重要。
常识和实用主义是定义每个场景最佳模式的最佳公式。作为建议,将软件视为一个长期项目;我们不需要在第一天就设计一个复杂的 Netflix 风格的架构。
基于最佳实践,享受、利用和探索数据库以充分利用您的系统是可能的。在以下图中,基于 James Serra 的文章《什么是多语言持久性?》(www.jamesserra.com/archive/2015/07/what-is-polyglot-persistence/
),您可以获得更多关于哪种数据库最适合或是一个给定场景的良好候选者的上下文。它描述了推荐使用哪种类型的数据库来满足列出的用例的常见和关键需求:
图 9.1 – 用例和数据库类型
这些是基于类型的数据库使用的一些可能性;当我们谈论 NoSQL 时,请记住,在某些类别中,有一些有意义的特定行为,值得使用。
对于长期应用程序,迁移是可能的。隔离可以帮助您在多语言之旅中。下一节将介绍DDD,它对持久化层的影响以及 Jakarta 如何帮助我们在这段旅程中。
理解 DDD 和 Jakarta
DDD 是一种软件开发方法,它侧重于理解问题域并在代码中对它进行建模。DDD 基于这样的理念,即问题域应该是开发的主要焦点,并且软件应该被设计成反映底层域概念和流程。
DDD 区分了战略设计和战术设计。战略设计指的是软件的整体架构和组织,而战术设计指的是单个组件和模块的详细设计。
在战略设计中,DDD 强调定义一个清晰且一致的领域模型的重要性,该模型代表问题域中的业务概念和流程。此模型应独立于任何特定技术或实现,并基于对领域深入的理解。战略设计还涉及定义边界上下文和具有明确边界的特定领域区域,这些区域与其他领域部分分别建模。
另一方面,战术设计专注于单个组件和模块的设计和实现细节。DDD 使用诸如聚合、实体、值对象和存储库等模式和技巧来在战术设计中建模和操作领域对象。
DDD 可以显著影响软件应用的各个层次,包括表示层、应用层、领域层和持久层。以下是 DDD 如何应用于并影响每一层的简要概述:
-
表示层:DDD 通过提供清晰且一致的领域模型来影响表示层,该模型可用于指导用户界面和用户交互的设计。表示层应反映领域模型。它应提供一个用户友好的界面,使用户能够从领域角度以有意义的方式与应用程序交互。
-
应用层:DDD 通过提供一组清晰且一致的服务和操作,这些服务和操作反映了领域中的业务流程和工作流,从而影响应用层。应用层的设计应支持领域模型,并提供一层抽象,使领域层能够专注于业务逻辑而不是实现细节。
-
领域层:DDD 对领域层的影响最为显著,领域层是应用的核心。在领域层,DDD 强调使用丰富且表达力强的语言来建模领域的重要性,这种语言反映了业务概念和流程。领域层的设计应独立于任何特定技术或实现,并专注于封装业务逻辑和领域知识。
-
持久层:DDD 还可以通过提供将领域对象映射到数据库的清晰且一致的方式来影响持久层。DDD 强调存储库,它为领域层和持久层之间提供了一层抽象。存储库使领域层能够专注于业务逻辑而不是数据库访问,并提供了一种确保领域对象一致且可靠地持久化和检索的方法。
总体而言,领域驱动设计(DDD)可以显著影响软件应用的设计和架构,并有助于确保应用专注于问题域而不是实现细节。DDD 可以通过提供清晰和一致的领域模型以及一系列设计模式和技巧,帮助创建更易于维护和可扩展的软件,使其能够适应不断变化的企业需求。
存储库模式是一种设计模式,它为域层和持久层之间提供了一个抽象层。存储库模式封装了访问和持久化领域对象的逻辑。它提供了一种确保领域对象一致和可靠地存储和检索的方法。
使用存储库,域层可以被设计成独立于持久层。它可以专注于使用丰富和表达性强的语言来建模业务流程和工作流。存储库模式可以通过使域层专注于业务逻辑和领域知识,而不是数据库访问和查询等实现细节,从而对持久层产生重大影响。
存储库模式通常在域层实现为一个接口,在持久层有一个具体的实现。存储库接口定义了一组用于存储、检索和查询领域对象的方法。具体的实现提供了使用所选持久化技术(如关系型或 NoSQL 数据库)的实际方法实现。
存储库模式的一个关键好处是它使域层能够与持久层解耦,使应用更加模块化且易于维护。通过分离关注点和封装逻辑,存储库模式可以确保应用更加灵活且能够适应不断变化的需求。
存储库模式通常与数据访问对象(DAO)模式相比较,这是另一种用于访问和持久化数据的模式。存储库与 DAO 之间的主要区别在于,存储库被设计用来封装访问和持久化领域对象的逻辑。相比之下,DAO 被设计用来封装访问和持久化数据的通用逻辑。以下图显示了从控制器开始到数据库,然后返回控制器的序列。
图 9.2 – 控制器到数据库的序列
换句话说,DAO 通常关注底层细节,如数据库连接、事务和 SQL 语句。相比之下,仓库关注领域模型的更高层次关注点。虽然这两种模式都可以用于持久化,但仓库模式通常被认为更符合 DDD 的原则,因为它提供了一种确保持久化层设计以支持领域模型和业务逻辑的方法。
数据从哪里来?我们不需要知道数据库的来源,无论是来自 SQL、NoSQL 还是 Web 服务。客户端需要知道。
下图展示了这个想法,其中我们有一个业务层注入持久化层,并且数据源来自哪里很重要;它可能来自所有这些来源同时。
图 9.3 – DDD 仓库表示
让我们看看一些代码,以探索 Jakarta Data 在发布仓库功能的第一个版本中的功能。与 MicroStream 一样,我们将从 Java SE 开始,然后将应用程序迁移到 Jakarta EE。我们将使用Developer
和Airplane
实体创建两个 CRUD 操作,其中第二个将处理简单的分页代码。
Jakarta Data
在软件世界中,设计一个具有多个数据库系统的应用程序是其中最难的事情之一。幸运的是,有一个规范使得在 Java 中实现多语言持久化变得更加容易。这是一个持久化无关的 API,可以无缝连接到不同类型的数据库和存储源。Jakarta Data 提供的 API 使得方便访问数据技术成为可能,使得 Java 开发者能够将持久化和模型关注点划分为不同的特性。例如,可以创建一个具有查询方法的仓库接口,该接口将由框架实现。
探索无知的(agnostic)设计模式是 Jakarta Data 的目标之一;本规范的第一个特性是 DDD 仓库模式。仓库的目标是便于多语言持久化而不影响业务。
从依赖关系开始,从现在起,我们只会添加 API;然后,我们将开始解释实现。所以,我们将包含 Jakarta Data 依赖项:
<dependency> <groupId>jakarta.data</groupId>
<artifactId>jakarta-data-api</artifactId>
<version>${data.version}</version>
</dependency>
我们将创建Developer
和Airline
实体。在 Jakarta Data API 中,我们必须使用@Entity
和@Id
分别定义实体及其 ID。嘿,要不要添加其他字段?这取决于实现方式;例如,在 JPA 中,这已经足够了,而在 Jakarta NoSQL 中,我们需要使用@Column
注解来标识其他字段:
@Entitypublic class Developer {
@Id
private String nickname;
private String name;
private String city;
private String language;
}
@Entity
public class Airplane {
@Id
private String model;
private String manufacturer;
private String nationality;
private int seats;
}
我们有了实体;下一步是使用仓库的持久化层。一旦我们有两个不同的实体,我们将创建两个独立的仓库。
此接口提供了用户不需要实现的方法;供应商将实现它们。第一个与 Developer
实体相关,并使用最基本的仓库:CrudDataRepository
。此外,还有一个使用查询的方法,你可以使用约定创建不需要实现查询;供应商将实现它们。
DeveloperRepository
是开发者仓库,一个扩展了 CrudDataRepository
的接口。现在我们将介绍几个方法;此外,我们将创建一个查询,使用 findByLanguage
方法按语言查找:
@Repositorypublic interface DeveloperRepository extends
CrudRepository<Developer, String> {
List<Developer> findByLanguage(String language);
}
下一步是 Airplane
仓库,所以我们有了我们的 Hangar
,但是等等!它应该是 AirplaneRepository
吗?我们有一个使用 Repository
后缀的约定。然而,你可以使用实体的集合,例如 Garage
用于汽车集合或 Team
用于球员集合。
Hangar
接口扩展了不同的接口,这次是 PageableRepository
。这是一个专门化的接口,它使得分页资源成为可能。它使用查询方法,并返回一个 Page
接口来处理飞机信息的一小部分:
@Repositorypublic interface Hangar extends
PageableRepository<Airplane, String> {
Page<Hangar> findByManufacturer(String manufacturer,
Pageable pageable);
}
最后,我们有两个代码仓库都准备好了执行。从 Developer
仓库开始,我们将创建开发者,通过 ID 查找他们,通过 ID 删除他们,并使用我们创建的方法,即按语言查询:
public class App { public static void main(String[] args) {
try (SeContainer container =
SeContainerInitializer.newInstance().initialize()) {
DeveloperRepository repository = container
.select(DeveloperRepository.class).get();
Developer otavio = Developer.builder()
.name("Otavio Santana")
.city("Salvador")
.nickname("ptavopkava")
.language("Java")
.build();
Developer kvarel4 = Developer.builder()
.name("Karina Varela")
.city("Brasília")
.nickname("kvarel4")
.language("Java")
.build();
repository.save(otavio);
repository.save(kvarel4);
Optional<Developer> developer = repository
.findById(otavio.getNickname());
List<Developer> java = repository
.findByLanguage("Java");
System.out.println("Java developers: " + java);
repository.delete(otavio);
}
}
下一步是在我们的 Hangar 中执行分页资源,并使用分页。一旦我们添加了几架飞机,我们将包含大小为两个元素的分页。在现实世界中,这个数字更为可观。它将根据区域和上下文而变化;通常在 10 到 100 之间:
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
Hangar hangar = container
.select(Hangar.class).get();
Airplane freighters = Airplane.builder()
.model("Freighters")
.manufacturer("Boeing")
.nationality("United States")
.seats(149)
.build();
Airplane max = Airplane.builder().model("Max")
.manufacturer("Boeing").nationality("United
States")
.seats(149)
.build();
Airplane nextGeneration = Airplane.builder()
.model("Next-Generation 737")
.manufacturer("Boeing").nationality("United
States")
.seats(149)
.build();
Airplane dreamliner = Airplane.builder()
.model("Dreamliner")
.manufacturer("Boeing").nationality("United
States")
.seats(248)
.build();
hangar.saveAll(List.of(freighters, max,
nextGeneration));
Pageable pageable = Pageable.ofSize(1)
.sortBy(Sort.asc("manufacturer"));
Page<Airplane> page = hangar.findAll(pageable);
System.out.println("The first page: " +
page.content());
Pageable nextPageable = page.nextPageable();
Page<Airplane> page2 =
hangar.findAll(nextPageable);
System.out.println("The second page: " +
page2.content());
}
我们都在 Java SE 上运行;让我们继续到下一个阶段,即将相同的代码推送到 MicroProfile
以创建一个微服务。在第八章第八章中,我们解释了 CDI 引擎/核心;我们将遵循相同的原理——复制/粘贴相同的代码,并更改访问权限,使其成为一个 REST 资源而不是 Java SE 应用程序:
@ApplicationScoped@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("developers")
public class DeveloperResource {
private final DeveloperRepository repository;
@Inject
public DeveloperResource(DeveloperRepository
repository) {
this.repository = repository;
}
@GET
public List<Developer> getDevelopers() {
return this.repository.findAll()
.collect(Collectors.toUnmodifiableList());
}
@GET
@Path("{id}")
public Developer findById(@PathParam("id") String id) {
return this.repository.findById(id)
.orElseThrow(() -> new WebApplicationException
(Response.Status.NOT_FOUND));
}
@PUT
public Developer insert(Developer developer) {
return this.repository.save(developer);
}
@DELETE
@Path("{id}")
public void deleteById(@PathParam("id") String id) {
this.repository.deleteById(id);
}
}
我们展示了纯 API,但关于实现呢?为了展示选项的数量,我们有一个仓库,它展示了在我们的 git
远程中对每个示例的特定行为的实现。你可以尝试,运行,并感受供应商和持久化解决方案之间的差异。
摘要
多语言持久化是大多数企业应用进步的好路径。使用这种方法,可以探索 SQL、NoSQL 或任何持久化解决方案。然而,与任何架构决策一样,要注意权衡;一个抽象可以确保数据库的选择不会影响业务视角。
雅加达数据(Jakarta Data)有助于标准化行为和代码模式。它帮助我们利用多种持久化解决方案构建一个功能宇宙。它是提高 Java 数据持久化模式能力的一个有前景的解决方案,并且欢迎帮助和反馈;加入我们,让使用这个工具的工作生活变得更加便捷。
现在是时候在架构层面探索那些允许我们探索现代云导向解决方案中最佳数据集成模式的集成实践了。
第十章:架构分布式系统 – 挑战和反模式
在今天的数字景观中,对可扩展和可靠系统的需求导致了分布式系统的广泛应用。这些由相互连接的组件组成的复杂网络被设计用来处理跨多台机器或节点的海量数据处理、存储和通信。然而,设计分布式系统伴随着独特的挑战和陷阱。
构建分布式系统的目标是实现高可用性、容错性、更好的性能和可扩展性,同时将工作负载分布在多个节点上。然而,这些系统的复杂性往往会产生各种挑战,架构师和开发者必须克服。从确保数据一致性和同步到管理网络延迟和优化性能,在设计分布式系统时应该考虑众多因素。
在设计分布式系统时,一个关键的挑战是实现适当的数据一致性。维护不同节点间数据的完整性和一致性至关重要,但随着系统规模的扩大,这变得越来越具有挑战性。确保给定数据块的副本都被正确且同时更新是一项重大挑战,通常需要实施复杂的同步机制。
另一个挑战在于管理网络延迟和通信开销。在分布式系统中,节点通过网络相互通信,消息穿越网络所需的时间可能会引入延迟和瓶颈。架构师必须仔细设计通信协议并选择合适的网络技术以最小化延迟并最大化系统性能。
可扩展性是设计分布式系统时必须考虑的关键因素。随着对资源和处理能力的需求增长,系统应通过无缝添加更多节点来实现水平扩展。在保持性能和避免瓶颈的同时实现这种可扩展性是一项复杂的任务,需要周密的规划和架构决策。
尽管存在这些挑战,架构师还必须意识到可能损害分布式系统有效性和可靠性的常见反模式。反模式是反复出现的、被认为是不太理想或反生产力的设计或实现实践。这些可能包括网络拥塞、单点故障、不适当的负载均衡或过度依赖中央协调器。识别和避免这些反模式对于确保分布式系统成功运行至关重要。
在本章中,我们将探讨在讨论分布式系统时现代架构的陷阱:
-
数据集成和分布式事务
-
双写反模式
-
微服务和共享数据库
-
最终一致性问题的挑战
我们将深入探讨架构师在设计分布式系统时面临的挑战,并探讨在过程中可能出现的常见反模式。通过理解这些挑战并避免陷阱,架构师和开发者可以创建出既稳健又高效的分布式系统,以满足现代应用的需求。通过最佳实践和实际见解,我们旨在为您提供构建分布式系统和有效减轻潜在风险的知识和工具。
数据集成和分布式事务
数据集成对于构建分布式系统至关重要,在这些系统中,不同的数据源必须被协调并可供各种系统组件访问。随着数据和分布式节点规模的增加,与数据集成相关的挑战变得更加复杂。在此背景下,一个关键考虑因素是分布式事务的协调。
维护数据完整性对于分布式事务至关重要,并确保系统表现得好像它在一个集中式数据库上执行单个事务。分布式事务是指必须在多个节点上原子执行的相关的数据库操作。在数据分布在不同的节点上的分布式系统中,确保这些操作的一致性和隔离性变得复杂。
下图展示了数据被整合到两个服务中,每个服务都有一个数据库。在这个阶段,需要协调以保证数据一致性和安全性:
图 10.1:分布式系统中的事务工作流程
然而,在规模上实现分布式事务一致性带来了重大挑战。在集中式数据库中通常保证的原子性、一致性、隔离性和持久性(ACID)属性,由于网络延迟、节点故障和并发问题,在分布式节点上执行变得更加困难。
解决这些挑战的一种方法是通过使用分布式事务协议,如两阶段提交(2PC)或三阶段提交(3PC)。这些协议协调分布式事务中多个节点之间的提交或回滚决策。然而,这些协议存在局限性,包括如果协调节点不可用,会增加延迟和故障脆弱性。以下图示了 2PC 的序列:
图 10.2:2PC 示意图
另一种方法是采用更宽松的一致性模型,如最终一致性或乐观并发控制。这些模型以牺牲严格的一致性保证为代价,换取更高的可扩展性和可用性。当实时一致性不是严格必需时,这些模型可以通过允许暂时不一致和异步解决冲突来表现更好。
此外,分布式数据集成通常涉及处理具有不同模式和格式的异构数据源。数据转换和映射变得至关重要,以确保来自不同来源的数据可以有效地组合和处理,这通常伴随着性能成本。为了创建分布式系统的一致视图,可以使用诸如提取、转换和加载(ETL)或数据虚拟化等方法来组合来自各种来源的数据。
分布式事务系统需要谨慎的设计决策来平衡一致性、可扩展性和性能之间的权衡。在设计大规模数据集成时,考虑数据一致性需求、延迟、容错性和性能因素是至关重要的。理解不同事务模型的特点和限制,并采用适当的数据集成技术,可以帮助架构师和开发者解决与分布式数据集成相关的复杂性,并确保系统的可靠性和效率。
总结来说,在分布式系统中进行大规模数据集成需要解决分布式事务的挑战以及维护多个节点之间的一致性。架构师和开发者在设计分布式事务系统时必须考虑一致性保证、可扩展性和性能之间的权衡。组织可以通过采用适当的交易协议、一致性模型和数据集成技术,有效地管理和集成大规模数据到他们的分布式系统中。
分布式数据库具有挑战性,因此我们应该利用最佳架构来最小化陷阱。接下来,我们将讨论在管理一个不信任的系统时记录的错误,这个错误特别与双写过程相关,以及为什么应该避免它。
双写反模式
双写是一种软件开发模式或方法,其中数据在实时同时写入两个或更多独立的系统或数据库。双写的目的是确保在不同目的或需要额外数据的多个系统中保持数据的一致性和同步。以下图示展示了这一操作,其中单个 Web 应用多次写入数据库、缓存和第二个应用程序。
图 10.3:双写操作
虽然双写对于数据集成和同步可能看起来很方便,但它通常被认为是一种反模式。但如果一个更新成功而另一个失败会发生什么?以下是一些为什么双写可能成为问题的原因:
-
复杂性和耦合: 实施双向写入会在不同系统之间引入复杂性和紧密耦合。它增加了维护开销,并使系统更加脆弱,容易出错。任何一个系统的任何更改或更新都可能需要在双向写入过程中涉及的所有其他系统中进行相应的更改。
-
性能开销: 双向写入会对系统产生显著的性能影响。在实时同步写入多个系统可能会引入延迟并降低整体系统性能。随着涉及系统的数量增加,性能影响变得更加明显,可能导致用户体验下降。
-
不一致性和故障: 双向写入不能保证所有系统之间的一致性。在写入过程中出现的故障,如网络问题或系统故障,可能导致不同系统之间数据状态不一致。处理这些故障和解决不一致性可能具有挑战性且耗时。
-
数据完整性挑战: 使用双向写入维护数据完整性变得更加复杂。确保所有涉及的系统都正确且同时更新,没有任何数据丢失或损坏,需要实施复杂的机制,如分布式事务。这些机制增加了复杂性,并可能进一步影响性能。
-
可扩展性限制: 随着系统的增长,双向写入的可扩展性变得越来越具有挑战性。随着设计和数据量的增加,同步所有系统写入的开销变得更加难以有效管理。将双向写入扩展到处理高吞吐量场景可能需要额外的基础设施和优化努力。
不要仅仅依赖双向写入,让我们探索其他集成和同步数据的选择。以下是一些推荐的替代方案:
-
ETL: 使用 ETL 流程,可以从源系统中提取数据,将其转换为适当的格式,然后加载到目标系统中。这种方法允许系统之间有更多的灵活性和解耦,可以根据需要执行数据转换和映射。
-
事件驱动架构: 采用事件驱动架构可以帮助异步地在系统之间传播数据更改或事件。它解耦了系统,并允许更灵活和可扩展的数据集成。当数据发生变化时,会发布事件,已订阅的感兴趣系统可以相应地做出反应。
-
消息队列: 利用消息队列可以提供可靠和可扩展的数据集成和同步机制。系统可以向队列发布消息,而订阅系统可以以自己的节奏消费它们,确保异步和解耦的通信。
通过采用这些替代方法,组织可以在避免双重写入陷阱的同时实现数据集成和同步。这些方法提供了更多的灵活性、可扩展性和可维护性,从而能够更好地管理分布式数据系统。
不幸的是,双重写入是我们作为分布式架构师面临的最常见的反模式,这是一个错误。现在,让我们转到第二个主题:微服务与共享数据库。
微服务与共享数据库
微服务架构的使用越来越受欢迎,因为它允许你创建可扩展和灵活的系统。这种方法涉及将应用程序分解成更小、独立的微服务,这些服务可以单独开发、部署和扩展。然而,尽管它有诸多优点,但在多个服务之间共享数据库可能会带来挑战和不利因素。
下图展示了一个示例,其中三个应用程序共享同一个数据库。从短期来看,我们可以想象这将节省我们一些电力资源,但从长远来看,我们开始质疑代价。如果我们创建了一个不一致的数据事件,我们如何知道哪个应用程序包含错误?我们可能还会遇到安全问题,如未经授权的数据访问:
图 10.4:微服务中的共享数据库
多个微服务共享数据库可以引入几个挑战和不利因素。这包括数据耦合和依赖、性能瓶颈、缺乏自主权和所有权、数据完整性和一致性问题和可扩展性和部署灵活性限制。由于共享数据而导致的紧密耦合可能会减慢开发速度并阻碍单个服务的灵活性和自主性。对数据库资源的竞争可能导致性能下降,尤其是在多个服务同时访问同一数据库时。共享数据库也模糊了所有权界限,使得难以确定负责数据相关问题的服务。确保数据完整性和一致性在多个服务向同一数据库写入时变得复杂,并且可能会出现冲突和不一致。将数据库扩展以适应来自众多服务的负载变得具有挑战性,并且由于必要的模式更改和迁移影响其他服务,部署新服务或进行更改可能会变得复杂。
-
数据耦合和依赖:在多个微服务之间共享数据库会在服务之间引入紧密耦合。数据库模式或数据模型的变化可能会影响多个服务,需要协调和同步的努力。这可能会减慢开发速度并阻碍单个服务的灵活性和自主性。
-
性能瓶颈:当多个服务访问相同的共享数据库时,对数据库资源的竞争可能会成为瓶颈。来自各种服务的增加流量和并发请求可能导致性能下降,因为数据库成为争用点。随着来自多个服务的负载必须得到适应,扩展数据库变得更加具有挑战性。
-
缺乏自主权和所有权:微服务架构强调单个服务的自主权和所有权。共享数据库模糊了所有权的界限,因为多个服务都可以访问并修改相同的数据。这可能会造成混淆,并使确定负责数据相关问题或错误的负责服务变得更加容易。
-
数据完整性和一致性:当多个服务写入同一数据库时,维护数据完整性变得更加复杂。涉及多个服务时,协调事务和管理并发变得更加复杂。确保一致性和在服务之间执行业务规则可能具有挑战性,因为可能会出现冲突和数据不一致。
-
可扩展性和部署灵活性:共享数据库可能会限制微服务的可扩展性和部署灵活性。随着系统的增长,由于多个服务带来的负载增加,扩展数据库变得更加具有挑战性。此外,部署新服务或更改现有服务变得更加复杂,因为它们可能需要数据库模式更改或影响其他服务的数据迁移。
以下图表展示了几个服务之间的隔离情况,其中每个服务都有一个专用的数据库,并对其负责。所有应用程序之间的通信将通过 API 进行;没有任何应用程序会直接与另一个应用程序的数据库进行通信:
图 10.5:每个微服务都有自己的数据库
为了应对这些挑战,为每个微服务使用一个数据库是可取的。这种方法提供了许多优势,如下所述:
-
服务自主权和隔离:每个微服务都有一个专用的数据库,提供独立性和隔离性。每个服务可以选择最适合其需求的数据库技术或模式。服务可以独立发展,而不会影响其他服务,从而实现更快的开发、部署和可扩展性。
-
简化数据管理:每个微服务使用单个数据库,使数据管理变得更加简单。它减少了协调工作,并允许服务选择最合适的数据存储技术或方法。服务完全控制其数据,包括模式更改、迁移和优化。
-
改进的性能和可伸缩性:专用数据库使服务能够水平扩展和独立扩展。服务可以选择针对其特定工作负载优化的数据库,确保高效的数据访问和处理。每个服务可以处理其数据库负载,提高性能和可伸缩性。
-
清晰的拥有权和责任:每个微服务拥有单独的数据库确保了清晰的拥有感和责任感。每个服务对其数据负责,这使得故障排除和问题解决变得更加容易。此外,它还增强了系统的可维护性和可支持性。
-
简化的数据一致性和完整性:使用专用数据库维护数据一致性和完整性变得更加容易管理。服务可以在其数据库内强制执行自己的业务规则和事务边界。这减少了管理分布式事务的复杂性,并减轻了数据一致性问题。
在微服务架构中,服务之间的集成应理想地通过事件进行,并且通常被认为是一种安全最佳实践,避免直接访问或修改其他服务的数据库。通过依赖事件进行通信并在每个服务的数据库周围保持严格的边界,您可以增强安全性并保护系统中的敏感数据。
以下是为什么事件和避免直接数据库访问可以促进安全性的原因:
-
有限的攻击面:访问其他服务的数据库会增加攻击面。将服务的数据库上下文暴露给其他服务引入了潜在的安全漏洞,如注入攻击或未经授权访问敏感数据。使用事件作为通信机制,您可以限制服务数据的暴露,并降低未经授权访问的风险。
-
数据隔离:微服务架构中的每个服务都有其特定的上下文和边界。通过避免直接访问其他服务的数据库,您可以保持数据隔离,并防止对数据库进行未经授权的读取或写入操作。这种隔离确保只有负责特定数据上下文的服务才能操纵或访问该数据,增强安全性和数据隐私。
-
关注点的分离:微服务架构强调关注点的分离,其中每个服务专注于其特定的领域。允许服务访问彼此的数据库可能会模糊这些边界,并引入潜在的数据不一致或未经授权的修改。通过依赖事件,服务可以在不破坏各自数据库封装和所有权的情况下进行通信和交换相关数据。
-
审计和合规性:为每个服务维护独立的数据库上下文简化了审计和合规性要求。有了专用数据库,跟踪和监控特定服务上下文内的数据访问和修改变得更加容易。它支持符合监管标准,并简化了识别和调查与安全相关的问题或违规行为。
** Saga 设计模式**用于长时间运行和分布式事务。它允许一系列本地事务,每个事务都在特定服务的上下文中,参与跨多个服务的协调和一致操作。Saga 模式允许在无需直接数据库访问的情况下,在服务之间进行通信并维护数据一致性。
在 Saga 模式下,参与事务的每个服务执行其部分并发出事件以指示其任务的完成或进度。其他对事务感兴趣的服务会监听这些事件并相应地继续其任务。Saga 模式通过依赖事件和协调的本地事务序列,确保数据一致性,而无需直接暴露或修改其他服务的数据库。
通过采用事件驱动架构并利用 Saga 模式,微服务可以安全地通信并保持数据一致性,同时坚持隔离、有限接触面和关注点分离的原则。这种方法增强了安全性并最小化了与直接访问其他服务数据库相关的风险,从而实现了一个更稳健、更安全的微服务生态系统。
在分布式架构中使用一些良好的实践可以减少陷阱和挑战的数量,但并不能消除它们。在持久化系统中保持一致性是一个永恒的挑战。然而,有一点我们需要理解和接受:最终一致性。在下一节中,我们将更详细地讨论这一点。
最终一致性问题
在分布式系统中,最终一致性是一个模型,其中数据更新不会立即同步到所有节点。相反,允许存在暂时的不一致性,并且更新会逐渐传播,直到系统收敛到一个一致的状态。
在最终一致性中,系统中的不同节点可能在任何给定时间点对数据有不同的看法。这主要是由于网络延迟、通信延迟和并发更新。然而,最终一致性确保系统达到一个一致的状态,所有节点都收敛到相同的数据。
为了解决与最终一致性相关的挑战和潜在问题,可以采用几种技术和机制:
-
当同时对同一数据进行多个更新时,可能会发生冲突。为了确保一致性,使用冲突解决机制来确定如何解决这些冲突。不同的技术,包括最后写入者胜出和应用定义的冲突解决策略,可以解决冲突更新。
-
读取修复:读取修复是一种在读取操作期间更新或同步数据以修复不一致性的技术。当读取操作遇到不一致或过时的数据时,它会触发一个修复过程,从其他节点检索数据的最新版本并更新本地副本,确保最终一致性。
-
反熵机制:反熵机制积极检测和解决分布式系统中的不一致性。这些机制定期比较节点间的数据,并启动同步过程以确保一致性。反熵工具的例子包括默克尔树、八卦协议和向量时钟。
-
法定多数系统:法定多数系统确定在分布式系统中达到一致性所需的协议级别。通过定义法定多数和法定多数的大小,系统可以确保在更新或操作被认为是一致性之前,必须有一定数量的节点同意。这有助于防止由于部分更新或故障导致的不一致性。
-
补偿操作:在冲突或不一致的更新无法自动解决的情况下,可以采用补偿操作。补偿操作是逆转或补偿错误或不一致更新的操作或过程。这些操作有助于恢复系统的一致性。
-
幂等性:设计幂等的操作可以帮助减轻不一致性。在编程和数学中,幂等性是某些操作的性质,即无论你执行多少次,你都会得到相同的结果。它确保即使由于通信延迟或重试而多次使用操作,结果仍然相同,从而防止不一致性。
如果你熟悉 NoSQL 数据库,你会记得BASE代表基本上可用,其中数据值可能会随时间变化,但最终会达到一致性。这种最终一致性是我们必须考虑的数据建模概念,以满足多个水平扩展性,并且我们可以利用从 NoSQL 数据库中学到的知识。我们可以看到一些之前提到的技术被用于这个数据库引擎上,例如 Cassandra 作为读取修复。
需要注意的是,最终一致性并不适用于所有场景。需要严格实时一致性的系统或处理关键数据的系统可能需要更关键的致性模型。然而,对于许多分布式系统来说,最终一致性在可用性、性能和数据完整性之间找到了一个平衡点。
实施和管理最终一致性需要仔细考虑系统的需求,使用适当的冲突解决策略,并选择反熵机制。通过采用这些技术,分布式系统可以有效地处理临时不一致性,并在一段时间内逐渐收敛到一致状态。
摘要
总之,设计分布式系统提出了独特的挑战,必须仔细解决以确保系统的成功和有效性。在本章中,我们探讨了某些挑战,例如双写和具有共享数据库的微服务,并讨论了为什么它们可能成为问题。
尽管最初对数据一致性有吸引力,但双写可能会引入复杂性、性能开销和数据完整性挑战。同样,微服务之间共享数据库可能会导致数据耦合、性能瓶颈和自主性受损。这些陷阱强调了仔细考虑替代方案的重要性,例如事件驱动架构和每个微服务使用单个数据库,以促进可扩展性、独立性和可维护性。
我们还强调了最终一致性作为分布式系统模型的重要性。虽然它允许临时数据不一致,但最终一致性平衡了可用性、性能和数据完整性。诸如冲突解决、读取修复、反熵机制、法定人数系统、补偿操作和幂等性等技术有助于解决任何挑战并确保最终一致性。
此外,文档成为分布式架构的一个关键方面。良好的文档提供了对系统、其组件及其交互的全面概述。它使开发、维护和现代化过程中的理解、协作和决策更加顺畅。
下一章将深入探讨现代化策略和数据集成。我们将探讨现有系统的现代化方法,利用数据集成技术,并深入研究促进平稳过渡和有效利用分布式架构的各种模式和技术的各种方法。
第十一章:现代化策略和数据集成
在当今快节奏和以数据驱动型的世界中,企业不断努力跟上不断发展的技术景观。现代化已成为各行业组织的重点,旨在提高效率、敏捷性和竞争力。现代化的一个关键方面是数据集成,它在利用数据的力量进行明智决策中发挥着关键作用。通过采用现代化策略、避免反模式和利用现代云服务,企业可以释放其数据的全部潜力,并在市场上获得竞争优势。
现代化策略包括一系列旨在将遗留系统、流程和基础设施升级以适应当代技术进步的方法。这些策略涉及将传统的本地系统转变为基于云的架构,利用微服务和容器以增加可扩展性和敏捷性,并采用 DevOps 实践以简化开发和部署流程。最终目标是使整个 IT 景观现代化,确保其能够跟上数字时代的需求。
然而,现代化努力可能会面临挑战,组织必须警惕可能阻碍进步的反模式。反模式是常见的陷阱或无效实践,可能会妨碍现代化项目的成功。一个值得注意的反模式是缺乏适当的数据集成,孤岛化的数据源和不同的系统阻碍了获取有价值见解的能力。企业越来越多地采用变更数据捕获(CDC)技术来克服这一挑战。CDC 允许组织捕获和传播实时数据变化,实现不同系统之间近乎瞬时的更新和同步。通过实施 CDC,组织可以确保其数据集成工作高效、准确且及时。
反模式是一种反复出现的解决方案或方法,它最初看起来是解决问题的正确方式,但最终会导致负面后果或次优结果。
云计算已经彻底改变了 IT 景观,为组织提供了前所未有的可扩展性、灵活性和成本效益。云原生技术,如无服务器计算和容器化,使组织能够构建高度可扩展和具有弹性的应用程序,以适应波动的工作负载和不断变化的企业需求。通过将遗留系统迁移到云,企业可以利用云提供商提供的强大基础设施、托管服务和高级分析能力。此外,现代化策略可以从利用现代云服务中获得显著的好处。
在本章中,我们将探讨更多关于以下主题的内容:
-
应用现代化策略
-
避免与数据存储相关的反模式和不良实践
-
CDC 模式简介
-
采用云技术和云服务
现代化策略和数据集成在现代商业景观中至关重要。通过拥抱现代化,避免诸如数据集成不良等反模式,并利用现代云服务的力量,组织可以释放其数据的真正潜力,推动创新,并保持竞争优势。现代化之旅需要周密的规划,深入了解组织的目标,并致力于利用尖端技术。有了正确的方法,企业可以应对现代化的复杂性,为成功的数字化转型铺平道路。
应用程序现代化策略
应用程序现代化策略包括更新和转换现有的遗留应用程序,以满足现代数字景观的需求。遗留系统通常以过时的技术和僵化的工作流程为特征,可能会阻碍组织进行创新、快速响应市场需求以及充分利用新兴技术的潜力。通过实施应用程序现代化策略,企业可以重振其软件资产,增强可扩展性,提高性能,并增加敏捷性。
过早优化总是危险的;认为单体等同于遗留是一种错误。作为软件工程师,我们需要了解业务需求和上下文。记住,任何解决方案中都不包括单体和微服务架构风格。
应用程序现代化有几种方法,每种方法都有其优势和考虑因素。让我们探讨一些常见的策略以及如何有效地应用它们:
-
重宿主或迁移,涉及在不进行重大代码更改的情况下将现有应用程序迁移到现代基础设施。这种策略提供了更快的迁移速度,最小化中断。重平台化通过利用云原生功能或服务,如可扩展性和托管数据库,来优化应用程序的性能。关键是确保在迁移到新基础设施时保持兼容性和配置调整——例如,云平台如亚马逊网络服务(AWS)、微软 Azure和谷歌云平台(GCP)。
-
重构主要关注改进现有应用程序的代码库、结构和架构。这种策略涉及进行重大的代码更改,优化性能,增强可扩展性,并采用模块化或微服务架构。目标是使应用程序与现代化开发实践保持一致,例如采用容器化、解耦组件以及利用新的框架或库。
-
重建,也称为重写,涉及从头开始,同时保留原始应用程序的功能和业务逻辑。这种策略允许利用现代开发框架、工具和架构模式。然而,它需要仔细规划,这可能耗时且资源密集。分析现有应用程序的优势和劣势,以确保新应用程序能够有效满足业务需求至关重要。
-
替换策略涉及完全用现成的商业软件包或软件即服务(SaaS)解决方案来替换遗留应用程序。当现有应用程序不再满足业务需求,且采用预构建解决方案比投资于现代化遗留系统更经济时,这种方法是合适的。
当出现特定的组织触发因素或挑战时,实施遗留现代化策略至关重要。让我们检查考虑现代化的常见原因,作为遗留技术堆栈:
-
通常运行在过时的技术之上,这些技术不再受支持或与现代软件组件不兼容。这可能导致安全漏洞、维护成本增加和有限的集成能力。现代化有助于缓解这些风险,并确保应用程序保持可行和安全。
-
可能需要帮助来处理不断增长的工作负载并交付最佳性能。现代化使应用程序能够水平或垂直扩展,利用基于云的资源,并采用现代架构模式,从而提高性能和可伸缩性。
-
由于其单体结构和僵化的工作流程,常常阻碍敏捷开发方法和 DevOps 实践的采用。应用现代化推崇模块化设计、微服务和容器化,使组织能够拥抱敏捷方法,快速迭代,并更频繁地部署变更。
-
可能无法提供现代化的用户体验或跟上行业标准,因为用户期望不断演变,竞争者持续创新。现代化策略可以提升应用程序的用户界面,引入新功能,并利用如人工智能(AI)、机器学习(ML)或移动平台等新兴技术。
对于希望适应、创新并在数字时代保持竞争力的组织来说,应用现代化策略至关重要。选择适当的现代化方法可以最小化对您的业务/组织的影响。但在开始这一现代化过程之前,审查需求和目标,以了解是否必要。特别是当我们谈论持久层时,重构可能是一个风险和相当大的成本;它比在集成开发环境(IDE)中进行代码重构更为简单。那么,让我们谈谈数据中的那些反模式。
避免与数据存储相关的反模式和不良实践
几种常见的反模式和不良实践可能会阻碍应用程序持久层的性能、可扩展性和可维护性。理解反模式和有害实践之间的区别对于准确识别和缓解这些问题至关重要。
反模式在软件开发中很常见,可能源于设计决策不佳、理解不足或遵循过时的实践。持久层中的反模式可能包括以下内容:
-
对象关系阻抗不匹配:当应用程序代码中使用的面向对象(OO)模型与数据库中使用的关联模型之间存在显著脱节时,就会发生这种反模式。它可能导致过度映射和转换逻辑、性能下降以及维护数据一致性复杂性增加。为了避免这种反模式,考虑使用提供应用程序代码和数据库之间无缝集成的对象关系映射(ORM)框架,以减少阻抗不匹配。
-
在表示层中进行数据访问:这种反模式涉及在表示层直接执行数据访问操作,例如在用户界面组件中。它违反了关注点分离(SoC)的原则,导致代码紧密耦合、维护和测试困难以及可重用性降低。虽然很少推荐,但直接从表示层检索数据有一些好的用途。为了解决这个问题,遵循分层架构模式(如模型-视图-控制器(MVC)或模型-视图-视图模型(MVVM)),其中数据访问操作在单独的数据访问层中执行。
-
在循环中查询数据库:这种反模式发生在应用程序在循环中执行单个数据库查询而不是使用批量操作时。它导致数据库往返次数过多、网络开销增加和性能下降。为了避免这种情况,使用批量处理、批量插入或更新以及缓存机制来优化查询,以最小化数据库交互次数。
另一方面,不良实践指的是通常被认为效率低下、次优或损害软件整体质量的行动或习惯。与反模式不同,不良实践可能不是重复的解决方案,而是应该避免的具体行动或选择。持久层中不良实践的例子包括以下内容:
-
缺乏连接池:未能利用连接池可能导致性能问题,尤其是在高流量应用程序中。为每个请求或操作打开和关闭数据库连接可能会导致资源争用、增加开销和降低可扩展性。实现数据库驱动程序或框架提供的连接池技术来高效管理连接是至关重要的。
-
未使用预编译语句或参数化查询:通过直接连接用户输入或动态值来构建 SQL 查询可能会使应用程序面临 SQL 注入攻击。使用预编译语句或参数化查询至关重要,这可以确保用户输入被视为数据而不是可执行代码,从而降低安全风险。
为了避免持久层中的反模式和不良实践,请考虑以下方法:
-
教育和培训开发者:确保开发者对最佳实践、设计模式和现代持久化方法有扎实的理解。提供培训课程、研讨会或资源,以更新他们对行业标准和新兴技术的了解。
-
遵循设计原则和模式:应用设计原则,如 SOLID(代表 单一职责、开闭原则、里氏替换原则、接口隔离原则、依赖倒置),并使用适当的设计模式,如 数据访问对象(DAO)、仓库或 ORM 模式。这些原则和模式促进了 SoC、模块化和可维护性。
-
使用 ORM 或查询构建器:采用提供抽象层以处理数据库交互的 ORM 框架或查询构建器。如 Hibernate、Entity Framework(EF)或 Sequelize 等 ORM 工具可以帮助减少对象关系阻抗不匹配,并高效处理数据访问操作。
-
实现连接池:利用数据库驱动程序或框架提供的连接池技术来高效管理和重用数据库连接。连接池有助于避免为每个请求建立新连接的开销,提高性能和可扩展性。
-
清理用户输入并使用预编译语句:始终过滤和清理用户输入,并避免直接将动态值连接到 SQL 查询中。相反,利用数据库 API 提供的预编译语句或参数化查询。这种方法通过将用户输入视为漏洞而不是可信输入,防止 SQL 注入攻击和许多用户输入错误。
-
执行代码审查和重构:定期进行代码审查,以识别反模式、不良实践和改进领域。鼓励持续改进的文化,让开发者能够提供反馈、提出改进建议并对代码进行重构,以符合最佳实践。
-
测试和基准性能:实施彻底的单元测试和集成测试以验证数据访问操作的正确性。进行性能测试和基准测试以识别瓶颈并优化查询执行时间。例如,JMeter 或 Gatling 等工具可以帮助模拟负载并测量性能指标。
-
保持更新并参与社区:了解持久技术及其框架的最新进展、更新和最佳实践。通过论坛、会议或在线社区与开发社区互动,分享经验,向他人学习,并发现新技术。
采用这些实践并保持对代码质量和性能优化的主动方法可以显著减少持久层中反模式和不良做法的发生,从而实现更健壮、可维护和可扩展的应用程序。谈到良好的做法,在接下来的会话中,我们将探讨最现代的一个,即 CDC,以及它如何帮助你在持久层之旅中。
CDC 模式简介
变更数据捕获(CDC)是一种用于跟踪和捕获数据库中数据变更的技术。它使组织能够识别、捕获和传播数据变更,几乎在实时进行,提供了一种可靠且高效的数据集成和同步方法,跨越不同的系统。
下图展示了使用 CDC 模式的一个示例,其中我们有一个触发事件的源,基于此事件,每个订阅者导致两个数据库目标:
图 11.1 – CDC 架构表示
CDC 的过程涉及监控和捕获在数据库级别发生的变化,如插入、更新和删除,并将这些作为单独的事件发出。而不是不断轮询整个数据库以查找变更,CDC 机制仅跟踪和捕获修改后的数据,减少不必要的开销并提高性能。
让我们来看看 CDC 的一些其他优势:
-
实时数据集成:CDC 使组织能够几乎在实时捕获和传播数据变更,确保集成系统可以访问最新的信息。这种实时数据集成允许更准确的报告、分析和决策。
-
提高数据一致性:通过在不同系统间捕获和同步数据变更,CDC 有助于保持数据一致性和完整性。在一个设计中进行的更新可以自动反映在其他系统中,消除了手动数据输入或批量处理的需求。
-
降低延迟:CDC 显著减少了数据变化与在其他系统中可用性之间的延迟。这在需要及时访问最新数据的场景中尤为重要,例如在金融交易、库存管理或实时分析中。
-
最小化对源系统的影响:与传统基于批处理的数据集成方法不同,CDC 通过增量捕获变更而不是提取和加载大型数据集来减少对源系统的影响。它减少了源系统的负载并避免了性能下降。
-
高效的数据复制:CDC 使数据库或系统之间的数据复制变得高效。它只捕获和传输变更的数据,减少网络带宽需求并提高复制性能。
CDC 在以下场景中具有优势:
-
数据仓库和商业智能(BI):CDC 促进了操作数据库与数据仓库或数据湖的集成,确保分析、交易处理和报告系统可以访问最新的数据。它使组织能够根据最新的信息做出数据驱动的决策。
-
微服务和事件驱动架构(EDA):在 EDA 中,一个微服务的变更会触发其他微服务的操作。通过实时捕获数据变更,CDC 允许微服务对最新的数据更新做出反应和处理,确保系统的一致性。
-
数据同步和复制:当多个数据库或系统需要相互同步并保持最新状态时,CDC 提供了一个高效机制来捕获和传播变更。这在涉及分布式系统、多站点部署或为灾难恢复(DR)目的进行数据复制的情况下尤其相关。
-
遗留系统集成:CDC 可用于将遗留系统与现代应用程序或数据库集成。通过从遗留系统捕获变更并将它们传播到现代系统,组织可以利用新技术的能力,同时保持现有系统的功能。
虽然 CDC 在许多场景中可能非常有用,但也有一些情况可能不适合使用 CDC。以下是一些 CDC 可能不是最佳选择的情况:
-
不频繁或影响低的数据变更:如果您的系统中的数据变更不规律或对下游系统的影响很小,实施 CDC 可能会引入不必要的复杂性。在这种情况下,传统的基于批处理的数据提取和加载过程可能就足够了。
-
小型或简单应用程序:对于数据源有限且集成要求简单的应用程序,实施 CDC 的开销可能超过了其带来的好处。CDC 在具有多个系统和数据库的复杂、大规模环境中具有优势。
-
严格的实时要求:尽管 CDC 提供了近实时数据集成,但它可能不适合需要立即或亚秒级数据传播的场景。其他方法,如事件源或流平台,可能更合适。
-
高频和高量数据变更:如果你的系统经历极高频或大量的数据变更,实施 CDC 可能会给源数据库和基础设施带来负担。在这种情况下,考虑其他能够有效处理规模的数据集成技术可能更有效率。
-
数据安全和合规问题:当数据安全或合规法规严格禁止或限制数据复制或移动时,CDC 可能不推荐使用。在实施 CDC 之前,评估和遵守数据治理和合规要求至关重要。
-
成本和资源限制:CDC 的实施通常需要额外的基础设施、监控和维护开销。如果你有预算限制或有限的资源来管理和支持 CDC,其他数据集成方法可能更可行。
-
功能有限的遗留系统:一些遗留系统可能需要更多的功能或能力来支持 CDC。在这种情况下,将这些系统的 CDC 机制进行改造可能具有挑战性或不切实际。考虑替代集成方法或探索现代化遗留系统的选项。
-
缺乏集成需求:如果你的系统不需要与其他系统或数据库集成,并且作为一个独立应用程序运行,没有数据同步,那么 CDC 可能不是必需的。评估集成需求并评估 CDC 是否为你的用例增加价值。
记住——是否使用 CDC 取决于你的系统需求、复杂性和特性。在实施 CDC 之前,彻底分析你的用例,考虑其优缺点,并评估替代数据集成技术是至关重要的。
总结来说,CDC 是一种强大的技术,用于在近实时捕获和传播数据变更。其好处包括以下内容:
-
实时数据集成
-
提高数据一致性
-
降低延迟
-
最小化对源系统的影响
-
高效的数据复制
CDC 在数据仓库、微服务、EDA、数据同步、复制和遗留系统集成中特别有价值。
这是一项巨大的工作,好消息是我们可以与他人一起完成;公共云服务提供的选择增多,可以为我们提供很多帮助,尤其是在更多地关注业务和委托非核心任务方面。当我们谈论云中的服务时,有一个是隐含的:DBaaS,我们不需要成为专家或在我们身边有专家;让我们在下一节中更深入地探讨它。
采用云技术和云服务
云服务为应用程序的持久层提供了众多优势,提供了增强的数据库体验,并减轻了组织在管理和维护方面的各种任务。在此背景下,一个特定的服务是数据库即服务(DBaaS),它允许用户利用数据库的力量,而无需广泛的专家知识或基础设施管理。
DBaaS 是传统的;设置和管理数据库涉及大量工作,包括硬件配置、软件安装、配置和持续维护。然而,DBaaS 将这些责任转移到了云服务提供商(CSP),使用户能够更多地关注其应用程序开发和业务逻辑。
这里有一些云服务,尤其是 DBaaS,如何使持久层受益的方法:
-
简化数据库管理:DBaaS 抽象了管理数据库的复杂性,使得开发者和团队能够更容易地处理持久层。服务提供商(SPs)处理数据库安装、打补丁和升级等任务,减轻了用户这些耗时且有时容易出错的活动。
-
可扩展性和性能:云服务提供垂直扩展(增加单个实例的资源)或水平扩展(添加更多模型以分散负载)的能力。这种可扩展性确保数据库能够处理不断增长的工作负载,并提供最佳性能以满足应用程序的需求。
-
自动备份和恢复:云服务提供商(CSPs)通常提供自动数据库备份和恢复机制。这确保了定期进行备份,降低了数据丢失的风险。此外,在灾难或故障发生时,云提供商可以促进快速有效的恢复,最小化停机时间并确保数据可用性。
-
高可用性(HA)和容错性(FT):云服务通常提供内置机制以在数据库系统中实现高可用性和容错性。这些机制包括自动故障转移、复制和地理分布式的数据中心。这些功能有助于确保数据库即使在硬件故障或网络中断的情况下也能保持可访问性和弹性。
-
安全和合规性:云服务提供商(CSPs)优先考虑安全性,并投资于强大的基础设施和数据保护措施。他们实施行业标准的安全实践、加密机制和合规性认证。这使得组织能够从提供商的专业知识中受益,并专注于确保其数据的安全性和合规性,而无需自己构建和维护这些措施。
-
成本效益:使用云服务作为持久层可以具有成本效益,消除了投资昂贵硬件基础设施的需求,并减少了持续维护和运营成本。云服务提供商通常提供与实际使用相匹配的定价模式,允许组织根据其消耗的资源付费,而不是进行重大前期投资。
通过利用云服务,组织可以将管理数据库的责任转移出去,专注于其核心业务目标。“别人的电脑”这个笑话突出了将数据库相关问题委托给云服务提供商的优势。SP 负责升级数据库、备份和恢复、数据分区、确保可伸缩性、释放资源以及简化内部管理这些方面的复杂性。
云服务,尤其是 DBaaS,使组织能够利用强大、可伸缩和高度可用的数据库,而无需广泛的专家知识或基础设施管理。通过简化管理、增强可伸缩性、自动备份和恢复、高可用性、安全措施和成本效益,云服务为现代应用程序的持久层提供了一个有价值的解决方案。
摘要
在这本书中,我们探讨了应用程序现代化的各个方面,重点关注策略、反模式和利用现代云服务来增强应用程序持久层的方法。我们强调了采用现代化策略的重要性,以跟上技术发展的步伐,并满足用户和业务不断变化的需求。
我们讨论了避免持久层中的反模式和不良做法的重要性,因为这些做法可能会阻碍应用程序的性能、可维护性和可伸缩性。开发者可以通过了解这些反模式和它们的影响,以及通过实施最佳实践,如适当的设计原则、ORM 框架和连接池,来确保持久层的健壮和高效。
我们还探讨了 CDC 概念及其在跨系统捕获和传播数据变化方面的好处。CDC 实现了实时数据集成、改进数据一致性和高效的数据复制,使其在各种场景中成为一种有价值的技巧,例如数据仓库、微服务架构和数据同步。
此外,我们深入探讨了云服务的优势,特别是 DBaaS 在简化数据库管理、增强可伸缩性、提供自动备份和恢复、确保高可用性和容错性以及解决安全和合规性问题。通过利用 DBaaS,组织可以将数据库相关任务委托给云服务提供商,并专注于其核心目标。
随着本书的结束,我们已经涵盖了与应用程序现代化、持久层优化以及利用云服务相关的根本概念和实践。以下章节总结了我们的讨论,提供了关键要点和最终考虑事项,以指导你的应用程序现代化之旅。
记住——紧跟新兴技术,遵循最佳实践,以及拥抱云服务,这些都能赋予你构建现代、高效和可扩展的应用程序的能力,以满足当今动态数字景观的需求。通过采取主动的现代化方法并利用云服务的力量,你可以在技术不断演变的世界上为你的应用程序的成功定位。
第十二章:最终考虑事项
我们已经探讨了持久性的架构视角,深入研究了 Jakarta EE 和 MicroProfile,检查了现代持久性技术及其权衡,并讨论了云时代持久性的基本方面。这一章将反思我们在持久性旅程中收集到的关键见解和考虑因素。现在,让我们结合所学到的经验,得出一些结论:
-
拥抱架构视角:理解架构视角对于设计健壮和可伸缩的持久性解决方案至关重要。将持久性与整体系统架构相一致的整体方法确保我们可以有效地管理复杂性,并随着时间的推移发展我们的应用程序。通过考虑数据建模、事务管理、缓存和可伸缩性等因素,我们可以构建满足现代应用程序需求的系统。
-
Jakarta EE 和 MicroProfile:Jakarta EE 和 MicroProfile 规范为构建企业 Java 应用程序提供了一个标准化的基础。这些框架提供了许多与持久性相关的 API 和功能,简化了开发过程。通过遵循这些标准,我们可以从可移植性、互操作性和充满活力的兼容库和工具生态系统中获得益处。
-
现代持久性技术和权衡:持久性技术的领域已经发生了显著变化,为开发者提供了多样化的选择。我们探讨了与不同方法相关的权衡,例如关系数据库、NoSQL 数据库和对象关系映射框架。每种技术都有其优势和劣势,选择取决于具体的项目需求。了解权衡有助于我们做出明智的决定,并优化应用程序的持久性层。
-
云时代持久性关键要素:云计算的兴起在持久性领域带来了新的挑战和机遇。云原生持久性解决方案,如托管数据库服务、分布式缓存和事件驱动架构,使我们能够构建弹性、可伸缩和成本效益的应用程序。我们讨论了在云中实现持久性的基本考虑因素,包括可伸缩性、数据一致性、多区域部署和无服务器架构。
-
持续学习的重要性:持久性是动态的,新技术和方法不断涌现。作为开发者,培养持续学习的思维并跟上最新趋势至关重要。这包括监控 Jakarta EE 和 MicroProfile 的进步,探索新的数据库技术,以及了解云原生持久性的最佳实践。通过拥抱学习心态,我们可以适应不断变化的需求,并充分利用我们在应用程序中持久性的全部潜力。
在本章中,我们将进一步探讨以下主题:
-
测试的力量以及如何通过数据域测试进行领导
-
不要低估文档;这有助于可扩展性
-
软件架构无论是否有架构师都在那里
测试的力量——如何通过数据域测试进行领导
确保数据和行为的致性是构建稳健和可靠应用程序的关键方面。应用程序中的错误可能会在数据中引入不一致性,导致意外的行为和错误的结果。实施有效的测试策略可以帮助识别和预防此类问题。集成测试和数据驱动测试是验证应用程序行为正确性和一致性的有效方法。
集成测试涉及测试应用程序不同组件之间的交互,以确保它们按预期协同工作。当尝试持久化层时,它尤其相关,因为它允许您验证应用程序与底层数据存储系统之间的集成。
数据驱动测试通过不同的输入数据集验证应用程序的行为。通过系统地改变输入数据,并将预期结果与实际输出进行比较,您可以识别不一致性并检测可能影响数据一致性的潜在错误。
Java 拥有多个测试框架和工具,以促进集成和数据驱动测试。JUnit Jupiter 是一个流行的测试框架,它提供了一个强大且灵活的平台,用于编写和执行测试。它提供了各种注解、断言和测试执行生命周期回调,以支持集成测试场景。
AssertJ 是另一个强大的库,它增强了测试中断言的可读性和表达性。它提供了一个流畅的 API,用于对各种数据类型执行断言,使验证预期结果更加容易,并确保数据一致性。
Test Container 是一个 Java 库,它简化了依赖于外部资源(如数据库、消息代理或其他容器)的应用程序的测试。它允许您为集成测试定义和管理轻量级、隔离的容器,为与外部系统一起工作时确保一致行为提供了一种方便的方法。
记住,测试是开发过程中的关键部分,投资于坚实的测试实践将帮助您在早期识别和解决数据一致性问题时,从而构建更稳健和值得信赖的应用程序。测试之后,让我们转向软件开发中一个被低估的话题:文档。它减少了会议次数,打破了孤岛,并可以帮助您处理分布式系统。
不要低估文档的重要性
文档在软件开发中至关重要,它使团队能够实现可扩展性,简化入职流程,打破知识孤岛,并确保每个人都朝着正确的方向前进。
随着项目和团队的增长,可扩展性的需求变得明显。文档是一个知识库,使团队能够有效地共享信息和最佳实践。开发者可以通过记录架构决策、设计模式和编码约定,轻松理解系统的结构并高效协作。这种可扩展性确保了随着团队的扩大或新成员的加入,集体知识得到保留,入职过程更加顺畅。
招募新团队成员可能是一个耗时且具有挑战性的过程。文档为新开发者提供了一个资源,使他们能够快速熟悉项目的架构、设计原则和编码标准。它降低了学习曲线,使新来者能够快速学习。良好的文档系统也有助于在员工更替期间的知识转移,最小化人员变动对项目连续性的影响。
在许多组织中,知识孤岛会阻碍协作和生产力,并可能导致错误。当知识只存在于特定个人手中时,其他人难以访问并从中受益。文档通过在团队中捕捉和共享专业知识来帮助打破这些孤岛。通过记录架构决策、集成模式和实施细节,团队可以民主化知识,并使每个人都能够为项目的成功做出贡献。
文档就像是一张指南针,引导团队走向正确的方向。它捕捉了架构选择、设计决策和编码实践背后的“为什么”。通过记录这些理由,团队建立了对项目的共同理解和愿景。它确保每个人都与系统的目的、目标和期望结果保持一致。文档是一个参考点,使开发者能够做出明智的决定,避免走弯路。
文档在分布式系统的背景下发挥着至关重要的作用,它通过提供对系统架构、集成点和通信协议的清晰性和理解来发挥作用。它作为沟通系统结构、行为和依赖关系给所有利益相关者的手段,确保了有一个共同的语言和理解。
文档定义了集成点、数据格式和通信协议,促进了无缝互操作性。它捕捉了容错、弹性和可扩展性策略,使团队能够设计和实施能够优雅处理故障并优化性能的系统。详细的文档概述了部署架构、配置参数和故障排除步骤,有助于分布式系统的顺利设置、管理和维护。总的来说,分布式系统中的文档增强了理解、协作和有效的领导,从而提高了可靠性、性能和系统质量。
由西蒙·布朗普及的 C4 模型为记录软件架构提供了一个稳健的框架。它采用分层结构,允许团队放大和缩小,提供系统组件及其交互的高级概述和详细视图。C4 模型充当架构的“谷歌地图”,使团队能够有效地沟通和可视化复杂系统。
除了架构文档之外,关注代码级别的战术文档也非常关键。清晰的代码注释、描述性的函数和变量名提高了代码的可读性和可维护性。它包括良好记录的代码、信息丰富的 README 文件和全面的变更日志。README 文件提供了项目的概述、安装说明和使用示例,促进了与其他开发者的协作。变更日志跟踪版本历史,记录功能添加、错误修复和其他显著变化。
文档在软件开发中是一种强大的工具,有助于团队的可扩展性,促进入职,打破知识孤岛,并确保从记录架构决策到在代码级别提供战术文档,投入时间和精力创建全面且易于访问的文档。
如果在讨论文档和测试之后你仍然在这里,让我们以一个我们也不太喜欢的话题结束这一章,或者至少因为我们在过去遇到的不良实践而成为一个红旗:架构。
没有架构师的架构
在过去,公司常常将软件架构与一个集中的指挥和控制区域联系在一起,这可能并没有为工程师带来更好的体验。然而,认识到软件架构不仅仅是一个部门或团队的建设至关重要。它在整个组织的成功中发挥着重要作用。
软件架构包括系统的基本结构和设计,包括其组件、交互和整体组织。它是构建稳健、可扩展和可维护系统的蓝图。尽管有些人认为架构是一个可选的关注点,但架构始终存在,无论我们是否注意到它。
一个精心设计的软件架构提供了许多好处,尤其是在分布式系统中。好的架构在考虑分布式系统时特别有用,特别是考虑到分布式系统:
-
经常需要处理增加的负载并适应不断增长的用户群体。一个经过深思熟虑的架构考虑了可扩展性,使系统能够处理更高的流量并适应不断变化的需求。它通过在多个节点上分配组件并利用负载均衡技术实现水平扩展,从而提高性能和响应速度。
-
容易出现故障和网络中断。一个健壮的架构可以集成容错和弹性策略。这包括冗余、复制、错误处理机制以及从故障中优雅恢复的能力。通过设计弹性,你的系统可以在单个组件故障的情况下保持可用性并继续运行。
-
通常涉及多个组件和服务,这些组件和服务必须无缝协作。一个设计良好的架构促进了组件的模块化、封装和松散耦合。这种模块化方法使得单个组件的开发、测试、部署和演进更加容易,从而提高了灵活性并适应不断变化的企业需求。
-
经常与外部服务、API 和数据源交互。一个定义良好的架构促进了与这些外部系统的无缝集成和互操作性。通过明确定义通信协议、API 契约和集成模式,架构使得平滑交互成为可能,从而使得消费或暴露服务以及与外部实体交换数据变得更加容易。
-
必须设计成能够处理大规模数据处理和通信的需求。一个架构良好的系统可以通过考虑数据局部性、缓存策略、负载均衡和高效的通信协议来优化性能。通过仔细的架构选择,你可以最小化延迟、带宽使用和资源竞争,从而最终提高系统的整体性能。
总之,软件架构不仅仅是某个部门或团队,而是整个组织成功的关键方面。良好的架构有助于构建可扩展、弹性、灵活和性能优异的分布式系统。通过考虑可扩展性、弹性、灵活性、互操作性和性能优化等因素,一个设计良好的架构为构建满足现代软件开发挑战和复杂性的分布式系统奠定了基础。
摘要
总结来说,我们衷心感谢您,亲爱的读者,陪伴我们走完了这本书的最后一程。我们希望这些篇章中分享的知识和洞察对您来说,就像对我们一样有价值。在需要的时候,请将这本书作为理解软件开发复杂性的有益资源使用。请记住,这本书只是您作为软件工程师的持久之旅的开始。持久性领域持续发展,新技术、模式和权衡不断涌现。抓住这个学习和成长的机会,保持对持久性领域最新进展的好奇心和开放心态。通过应用本书中涵盖的原则和概念,并保持对进一步探索的热情,您将充分准备应对作为软件工程师旅程中的挑战和机遇。再次感谢,并祝愿您在持之以恒的努力中取得巨大成功。
进一步阅读
-
《有效软件测试:开发者指南》(Effective Software Testing: A Developer’s Guide)由Maurizio Aniche所著,旨在深入探讨有效的软件测试实践。本书提供了宝贵的见解和技术,用于提高您的测试技能,包括集成测试、数据驱动测试和其他基本测试概念。通过利用本书中分享的知识,您可以通过全面和有效的测试来提高软件的质量和可靠性。
-
《软件架构基础:工程方法》(Fundamentals of Software Architecture an Engineering Approach)由Neal Ford所著,是一本强烈推荐的书,提供了对软件架构原则、模式和最佳实践的宝贵见解。它涵盖了诸如架构风格、设计原则、可伸缩性、模块化等基本主题。通过学习这本书,您可以提高对软件架构的理解,并将其有效地应用于分布式系统。
-
《开发者文档:工程师技术写作指南》(Docs for Developers: An Engineer’s Field Guide to Technical Writing)由Jared Bhatti、Sarah Corleissen、Jen Lambourne和David Nunez合著,是一本全面且实用的书籍,对希望提高技术写作技能的开发者来说是无价之宝。这本书由该领域的专家撰写,深入探讨了实用文档的细微差别,提供了针对开发者需求的见解、策略和最佳实践。
-
《C4 模型可视化软件架构》(The C4 Model for Visualising Software Architecture)由Simon Brown所著,是一本变革性的书籍,为开发者提供了一个全面框架,用于有效地可视化和传达软件架构。本书由经验丰富的实践者撰写,介绍了 C4 模型——一种将复杂的架构设计简化为一系列分层图表的实用方法。通过提供一种清晰和一致的语言来表示软件系统、组件、容器和代码,C4 模型促进了开发者、架构师和利益相关者之间的有效沟通和协作。