Skip to content

Latest commit

 

History

History
377 lines (234 loc) · 31.9 KB

File metadata and controls

377 lines (234 loc) · 31.9 KB

三、构建大规模数据库操作

在企业软件开发领域,开发人员总是构建处理大量数据的应用。在早期的计算时代,系统通常跨越比我们现在居住的房间更大的房间,数据以平面文件格式存储,而今天,系统已经缩小到如此之小,以至于在用于存储单个系统的相同大小的房间中,我们现在可以运行数千个系统,每个系统相互协调,为我们提供了能以光速处理数据的机器。随着时间的推移,数据存储方式也从使用平面文件发展到复杂的数据库管理系统。

随着企业规模的不断扩大以及新兴领域带来的业务的不断扩展,企业应用需要处理的数据量也在不断增长,因此了解如何构建应用以处理大规模数据库相关操作非常重要。尽管构建大规模数据库操作永远不是一种一刀切的解决方案,但我们将讨论构建应用的一些共同点,这些应用可以轻松地进行扩展以处理数据的增加、模式修改的要求、应用复杂性的增加等等。

尽管有多种类型的数据库(如 SQL、NoSQL 和 Graph)可用于存储应用数据,但这取决于企业所需的应用类型,本章重点介绍使用 SQL 的关系数据库管理系统的使用,由于它们的广泛流行性和处理大量用例的能力。

在本章结束时,您将了解以下内容:

  • 使用对象关系映射器ORMs)及其带来的好处

  • 构建数据库模型以提高效率和易于修改

  • 专注于维护数据库一致性

  • 即时加载和延迟加载的区别

  • 利用缓存加快查询速度

技术要求

本书中的代码清单可在chapter03目录下找到 https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python.

可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

本章中提供的代码示例要求您在其系统上安装并配置以下系统包:

  • python-devel
  • PostgreSQL
  • Python–virtualenv

除了这三个包之外,您还需要sqlalchemy包,它提供我们将在本章中使用的 ORM,以及psycopg2,它提供postgres数据库绑定以允许sqlalchemy ...

数据库和对象关系映射器

正如我们在前几章中所讨论的,Python 为我们提供了许多面向对象的功能,并允许我们根据类和对象映射用例。现在,当我们可以将问题集映射到一个类及其对象时,为什么不将数据库表映射为对象,其中一个特定的类表示一个表,而它的对象表示表中的行呢。沿着这条路线走下去,不仅可以帮助我们保持代码编写方式的一致性,还可以帮助我们对问题进行建模。

提供将数据库映射到对象的功能的框架称为 ORMs,它们帮助我们将数据库可视化为一组类和对象。

在 Python 环境中,经常看到 ORMs。例如,流行的 PythonWeb 框架 Django 提供了自己的 ORM 解决方案。然后是 SQLAlchemy,它提供了一个成熟的 ORM 解决方案和数据库工具包,支持各种关系数据库。

但要说服开发人员使用 ORM 框架,应该有比仅仅说他们能够将您的数据库映射到类和对象,并为您提供访问数据库的面向对象接口更好的优势。让我们来看一下 ORMS 在表格中使用的一些优点:

  • 对特定于供应商的 SQL的抽象:关系数据库空间充满了选择,有几家公司在销售他们的产品。这些产品中的每一个在如何通过使用 SQL 实现特定功能方面都可能存在差异。有时,一些数据库可能实现一些其他数据库尚不支持的 SQL 关键字。对于开发人员来说,如果他们需要支持多个功能不连贯的数据库,这可能会成为一个问题。由于 ORMs 已经知道如何处理数据库中的这些差异,它们可以帮助开发人员缓解支持多个数据库的问题。大多数情况下,当使用 ORM 时,开发人员所要做的就是修改数据库连接统一资源标识符(URI),然后他们就可以在应用中使用新的数据库了。
  • 减少了重复 SQL 的需要:在编写应用时,有相当多的地方需要使用类似的查询从相同的表中检索数据。这将导致在很多地方编写大量重复的 SQL,不仅会导致大量格式不好的代码,而且还会为错误打开大门,因为 SQL 查询构造不当(人类在进行重复工作时很容易失去注意力,所以这不也适用于开发人员吗?)。ORM 解决方案通过减少编写 SQL 以获得相同结果的需要来提供帮助,通过在 SQL 命令上提供抽象并根据调用不同方法的方式动态生成 SQL。
  • 提高了应用的可维护性:由于 ORM 允许您一次性定义一个数据库模型,并通过实例化类在整个应用中重用它,因此它允许您在一个地方进行更改,然后在整个应用中反映出来。这使得维护应用的任务不那么烦人(至少是与数据库处理相关的部分)。
  • 提高生产率:这本身不是一个特征,而是前面提到的几点的副作用。通过使用 ORM 解决方案,开发人员现在对总是考虑 SQL 查询或尝试遵循特定的设计模式有点放松了。他们现在可以专注于如何最好地构建他们的应用。这大大提高了开发人员的生产率,并允许他们完成更多的工作,提高了时间的利用率。

在本章中,我们将重点介绍如何利用 ORMs 来最佳地开发企业应用,以便它们能够轻松地与数据库交互并高效地处理大规模数据库操作。为了保持本章的简单性,我们将继续使用 SQLAlchemy,它将自身作为 SQL 工具包和 Python 的 ORM 解决方案进行推广,并为 Python 环境中的不同框架提供大量绑定。它正被一些实用的大型项目使用,如 OpenStack、Fedora 项目和 Reddit。

建立炼金术

在深入研究如何为应用创建最佳数据库模型以促进高效的大规模数据库操作之前,我们首先需要设置 ORM 解决方案。因为我们将在这里使用 SQLAlchemy,所以让我们看看如何在开发环境中设置它。

为了让 SQLAlchemy 工作,您应该在您的系统或远程计算机上设置一个数据库管理系统,您可以连接到该系统。一个带有暴露端口的容器也可以为我们完成工作。为了简化示例,我们假设读者在这里使用 PostgreSQL 作为他们的数据库解决方案,并且了解 PostgreSQL 设置的工作原理。现在,让我们看看如何设置 SQLAlchemy:

mkdir ch3 && cd ch3 ...

建立最佳数据库模型

实现对数据库的有效访问的第一步是为数据库构建最佳模型。如果一个模型不是最优的,那么其余的加速访问数据库的技术将不会有什么不同。

但在我们深入研究如何为数据库构建最佳模型之前,让我们先看看如何使用 SQLAlchemy 为数据库构建任何模型。

对于这个例子,让我们假设我们想要构建一个模型来表示 BugZot 应用中的用户。在我们的 BugZot 应用中,用户需要提供以下字段:

  • 名和姓
  • 用户名
  • 电子邮件地址
  • 暗语

此外,我们的 BugZot 应用还需要维护有关用户的更多信息,例如他们在系统中的成员级别、用户有权享有的特权、用户帐户是否处于活动状态以及发送给用户以激活其帐户的激活密钥。

现在,让我们看看如果我们尝试使用 SQLAlchemy 根据这些需求建模用户表会发生什么。下面的代码描述了我们如何在 SQLAlchemy 中构建用户模型:

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Boolean, Date, Integer, String, Column
from datetime import datetime

# Initialize the declarative base model
Base = declarative_base()

# Construct our User model
class User(Base):
 __tablename__ = 'users'

 id = Column(Integer, primary_key=True, autoincrement=True)
 first_name = Column(String, nullable=False)
 last_name = Column(String, nullable=False)
 username = Column(String(length=25), unique=True, nullable=False)
 email = Column(String(length=255), unique=True, nullable=False)
 password = Column(String(length=255), nullable=False)
 date_joined = Column(Date, default=datetime.now())
 user_role = Column(String, nullable=False)
 user_role_permissions = Column(Integer, nullable=False)
 account_active = Column(Boolean, default=False)
 activation_key = Column(String(length=32))

 def __repr__(self):
 return "<User {}>".format(self.username)

这个例子展示了如何使用 SQLAlchemy 构建模型。现在,让我们来看看我们在代码示例中做了什么。

在代码示例的开始部分,我们首先导入了declarative_base方法,该方法负责为我们的模型提供基类

Base = declarative_base()行将基础模型分配给我们的基础变量。

接下来我们要做的是包含 SQLAlchemy 中的不同数据类型,我们将在模型定义中使用这些数据类型。

最后一次导入将导入我们将在数据库模型中使用的 Pythondatetime库。

现在,不考虑我们的代码将如何填充数据库模型的不同字段,让我们来看看我们是如何设计用户模型的。

设计模型的第一步是定义一个类用户作为我们的模型类。这个类派生自我们前面在代码中初始化的基本模型。

__tablename__ = 'users'行定义了在数据库中实现此数据库模型时应为表指定的名称。

接下来,我们开始定义表将包含的列。为了定义列,我们使用 key=value 类型的方法,其中键定义列的名称,值定义列的属性。

例如,要定义列 id,它应该是整数类型,并且应该作为表用户的主键,我们定义它如下:

id = Column(Integer, primary_key=True, autoincrement=True)

我们现在可以看到这是多么简单。我们不必编写任何 SQL 来定义列。类似地,通过向列构造函数传递unique=Truenullable=False参数,可以很容易地强制特定字段应该具有唯一的值,并且不能将 null 作为值,如下行中的示例所示:

username = Column(String(length=25), unique=True, nullable=False)

定义完所有列后,我们提供了__repr__方法的定义。__repr__方法是一种神奇的方法,由内部repr()Python 方法调用,以提供对象的表示,例如当用户发出print(userobj)时。

这就完成了我们使用 SQLAlchemy 对用户模型的定义。这很简单,不是吗?我们不必编写任何 SQL;我们只是快速地将列添加到一个类中,并将所有其他内容留给 SQLAlchemy 处理。现在,虽然所有这些都很有趣,也很容易实现,但我们犯了一些错误,这些错误现在似乎没有造成任何伤害,但随着我们的应用的扩展,成本会越来越高。让我们来看看这些错误。

模型定义的问题

虽然 SQLAlchemy 为我们提供了大量的抽象来轻松定义我们的用户模型,但它也使我们很容易犯一些错误,一旦应用的使用规模扩大,企业发展壮大,这些错误的代价可能会很高。让我们来看看我们在定义这个模型时所犯的一些错误:

  • 易受更改的漏洞:当前用户模型的定义使得一旦应用扩展,很难对模型进行更改。让我们举一个组织决定为用户提供更多关于 bug 报告的权限的例子。在 SQL 方面,为了达到这个效果,我们需要编写一个查询,该查询将遍历所有记录,并将user_role作为用户。。。

优化我们的模型

在讨论如何建立最优模型之前,我们首先需要了解最优模型中需要呈现的特征。让我们来看看下面的内容:

  • 易于适应:随着应用用户群的增长,优化模型应易于根据应用不断变化的需求进行适应。这意味着更改一个特定的模型不需要在整个应用中进行更改,并且应该具有很高的内聚性。
  • 最大化主机上的吞吐量:每个主机都有不同的体系结构,数据模型应该能够以最大化其吞吐量的方式利用底层主机资源。这可以通过为特定的体系结构和用例使用正确的数据存储引擎来实现,或者跨计算机集群运行数据库来提高并行执行能力。
  • 高效存储:数据库模型还应考虑存储在其中的数据增长时可能使用的存储。这可以通过仔细选择数据类型来实现。例如,仅表示一个只能有两个值(true 或 false)的列,随着数据库中记录数的增加,整数类型将被过度使用,从而浪费大量磁盘空间。这样一个列的标称数据类型可以是布尔型的,在内部不会占用太多空间。
  • 易于调整:一个高效的模型会仔细地对列进行索引,以加速对特定表的查询处理。这将提高数据库的响应时间,并让快乐的用户不会因为应用从数据库返回 10000 条记录而感到沮丧。

为了实现这些目标,我们现在需要简化模型,并使用关系数据库提供的关系概念。现在,让我们开始重新分解我们的用户模型,使其更加优化。

要实现这一点,首先我们需要将它从一个大模型分解为多个小模型,这些模型独立地存在于我们的代码库中,并且不需要所有的东西都如此紧密地耦合。让我们开始吧。

我们将移出模型的第一件事是如何处理角色和权限。由于角色及其权限不会因用户而异(当然不是每个用户都有一个唯一的角色,也不是每个角色都有一组不同的权限),因此我们可以将这些字段移动到不同的模型,称为权限。以下代码说明了这一点:

class Role(Base):
 __tablename__ = 'roles'

 id = Column(Integer, primary_key=True, autoincrement=True)
 role_name = Column(String(length=25), nullable=False, unique=True)
 role_permissions = Column(Integer, nullable=False)

 def __repr__(self):
 return "<Role {}>".format(role_name)

现在,我们将角色与用户模型分离。这使得我们可以很容易地修改提供的角色,而不会引起太多问题。这些修改可能包括重命名角色或更改现有角色的权限。我们所做的只是在一个地方进行修改,它可以反映给具有相同角色的所有用户。让我们看看在我们的用户模型中如何借助关系数据库管理系统关系数据库中的关系来实现这一点。

下面的代码示例演示如何实现角色模型和用户模型之间的关系:

class User(Base):
  __tablename__ = 'users'

  id = Column(Integer, primary_key=True, autoincrement=True)
  first_name = Column(String, nullable=False)
  last_name = Column(String, nullable=False)
  username = Column(String(length=25), unique=True, nullable=False)
  email = Column(String(length=255), unique=True, nullable=False)
  password = Column(String(length=255), nullable=False)
  date_joined = Column(Date, default=datetime.now())
  user_role = Column(Integer, ForeignKey("roles.id"))
  account_active = Column(Boolean, default=False)
  activation_key = Column(String(length=32))

  def __repr__(self):
    return "<User {}>".format(self.username) 

在这个代码示例中,我们将user_role修改为整数,并存储了一个存在于roles模型中的值。任何向该字段中插入角色模型中不存在的值的尝试都将引发 SQL 异常,该操作是不允许的。

现在,继续同一个例子,让我们考虑一下用户模型的activation_key列。一旦用户激活了他们的帐户,我们可能就不需要激活密钥。这为我们提供了一个在我们的用户模型中执行更多优化的机会。我们可以将此激活密钥移出用户模型,并将其存储在单独的模型中。一旦用户成功激活了他们的帐户,就可以安全地删除记录,而不会有修改用户模型的风险。那么,让我们开发激活密钥的模型。以下代码示例说明了我们要执行的操作:

class ActivationKey(Base):
  __tablename__ = 'activation_keys'

  id = Column(Integer, primary_key=True, autoincrement=True)
  user_id = Column(Integer, ForeignKey("users.id"))
  activation_key = Column(String(length=32), nullable=False)

  def __repr__(self):
    return "<ActivationKey {}>".format(self.id)

在本例中,我们实现了ActivationKey模型。因为每个激活密钥都属于一个唯一的用户,所以我们需要存储哪个用户拥有哪个激活密钥。我们通过在用户模型的id字段中引入外键来实现这一点。

现在,我们可以安全地从我们的用户模型中删除activation_key列,而不会造成任何问题。

利用索引

索引是一种可以提供大量性能优势的东西,如果在很适合索引的字段上进行索引的话。但是,如果不小心选择要编制索引的列,索引也可能被证明是无用的,甚至会损害数据库性能。例如,索引表中的每一列可能没有任何优势,并且会不必要地占用磁盘空间,同时也会降低数据库操作的速度。

因此,在开始讨论如何使用 ORM 为特定字段编制索引之前,我们这里以 ORM 为例,首先让我们澄清在数据库的上下文中,索引到底是什么(不要深入探讨它们到底是如何工作的)、哪个数据结构。。。

维护数据库一致性

在应用部署后的整个生命周期中,数据库通常有大量并行操作。这些操作可以像从数据库检索信息一样简单,也可以是通过插入新记录、更新现有记录或删除其他记录来修改数据库状态的操作。在处理环境中可能发生的、干扰数据库正常运行的错误和崩溃方面,大型组织目前在生产中使用的大多数数据库都具有相当高的恢复能力。这些方法可以防止数据损坏和停机。

但这并不能完全免除应用开发人员在维护数据库中数据的一致性时仍然需要小心的事实。让我们试着了解一下这种情况。

在企业级应用中,在任何给定的时间点都会有许多数据库查询并行运行。这些查询源于使用来自多个用户的应用或内部应用维护作业。其中一个主要事实是,并非所有查询都能成功执行。这可能是由于几个原因造成的,例如查询中的数据不符合模式、为列值提供的数据类型不正确以及违反了约束。发生这种情况时,数据库引擎只会阻止查询的执行,并为查询返回一个错误。这是绝对正确的,因为我们不正确的查询没有对数据库进行任何不正确的更改。但是,当此查询是在数据库中创建新资源的一组较大操作的一部分时,情况会变得棘手。现在,我们需要确保在恢复失败的查询之前由其他查询所做的更改。

这种行为仍然可以通过应用开发人员的一些努力来修复,方法是跟踪 SQL 查询,并在两者之间发生异常时手动恢复它们的更改。

但是,如果在执行其中一个查询时,数据库引擎由于其间的错误而崩溃,该怎么办。现在我们处于一种无法预测数据库状态的情况下,处理这类情况可能会变得非常烦人,并且可能会使整个组织的操作暂停很长一段时间,直到验证数据库一致性为止。那么,我们能做什么?我们是否有办法防止此类问题的发生?答案是肯定的。让我们看一看。

利用事务保持一致性

关系数据库中的事务为我们提供了解决刚才讨论的问题的能力。就关系数据库而言,事务可以被视为由多个数据库查询组成的信封,这些查询要么作为一个任务执行,要么在其中任何一个查询失败时完全恢复。我们也可以考虑事务的原子单位的数据库操作,即使是一个单一的故障将恢复整个交易。但是,这不正是我们解决数据库一致性问题所需要的吗?

现在,让我们看看 ORM 解决方案如何帮助我们实现事务支持。

为了理解这一点,让我们举个例子。我们的混蛋。。。

了解延迟加载与急切加载

当我们进行查询以从数据库加载数据时,这个操作很可能会定义我们构建的应用的响应时间。这主要发生在需要加载大量数据并且应用等待数据库返回所有这些行和列时。

这些操作可能需要一些时间,从几毫秒到超过 10 秒不等,具体取决于从数据库查询的数据量。这里的问题是,我们是否可以对此进行优化以提高应用的响应时间?

答案在于使用 SQL 关系和 ORM 层加载技术。虽然关系可以帮助我们定义这两个模型如何相互关联,但加载技术定义了 ORM 如何检索关系。当需要加载大量数据时,这可以证明是非常有帮助的,因为它不仅提供了一种机制,通过这种机制,我们可以将关系数据的加载推迟到需要时,还可以节省大量应用的内存占用。那么,让我们来看看这些技术。

使用关系

有了关系数据库管理系统,我们现在可以定义两个模型之间的关系。数据库支持两个模型之间不同类型关系的建模,例如:

  • 一对一关系:一个模型中的一条记录只与另一个模型中的一条记录相关的关系。例如,我们的用户模型中的用户只有一个从 ActivationKey 模型映射到它的激活密钥。这是一对一的关系。
  • 一对多关系:一个模型中的一条记录映射到另一个模型中的多条记录的关系。例如,如果我们有一个名为 Bug 的模型,描述 Bug 条目,那么我们可以说,一个用户。。。

延迟加载

许多 ORM 层以及 SQLAlchemy 都试图尽可能长时间地延迟数据加载。通常,只有当应用实际访问对象时,才会加载数据。这种延迟加载数据直到有人试图访问该数据的技术称为延迟加载。

这种技术确实有助于减少应用的响应时间,因为整个数据不是一次性加载的,而是按需加载的。这种优化是以运行几个 SQL 查询为代价的,这些查询将在发出请求时检索实际数据。但是我们有没有办法对这种技术进行明确的控制?

对于每种 ORM 解决方案,答案都会有所不同,但实际上有相当多的解决方案允许您启用或禁用延迟加载行为。那么,我们如何在炼金术中控制这一点呢?

看看我们在上一节中所做的用户模型修改,我们可以通过在角色字段中添加一个额外的属性,明确地告诉 SQLAlchemy 延迟加载角色模型中的数据,如以下代码片段所示:

role = relationship("Role", lazy_load='select')

这个额外的lazy_load属性定义了 SQLAlchemy 用于从角色模型加载数据的技术。以下示例显示了延迟加载期间的请求流:

>>> Session = sessionmaker(bind=engine)
>>> db_session = Session()
>>> user_record = db_session.query(User).first()
INFO sqlalchemy.engine.base.Engine SELECT users.username AS users_username, users.id AS users_id, users.role_id AS users_role_id 
FROM users 
 LIMIT %(param_1)s
INFO sqlalchemy.engine.base.Engine {'param_1': 1}
>>> role = user_record.role
INFO sqlalchemy.engine.base.Engine SELECT roles.id AS roles_id, roles.role_name AS roles_role_name, roles.role_permissions AS roles_role_permissions 
FROM roles 
WHERE roles.id = %(param_1)s
INFO sqlalchemy.engine.base.Engine {'param_1': 1}

从这个例子中我们可以看到,SQLAlchemy 不会尝试加载角色模型的数据,除非我们尝试访问它。一旦我们尝试从角色模型访问数据,SQLAlchemy 就会对数据库进行SELECT查询,在对象中获取结果,并返回填充的对象,我们现在可以使用它。

与按需加载数据的技术不同,我们还可以要求 SQLAlchemy 在发出第一个请求后立即加载所有数据。这可以节省应用在 ORM 层从数据库中按需获取数据之前额外等待的几毫秒。

这种技术被称为急切加载,我们将在下一节中解释。

急装

有些情况下,我们希望加载对象的数据以及对象映射到的关系。这是一个有效的用例,例如当开发人员确信他们将访问关系的数据时,不管情况如何。

在这些用例中,ORM 层按需加载关系时浪费时间是没有意义的。这种加载对象数据以及与我们的主要对象相关联的对象的数据的技术称为渴望加载。

SQLAlchemy 提供了实现此行为的简单方法。还记得我们在上一节中指定的lazy_load属性吗?是的,这就是从惰性加载行为切换到渴望加载行为所需的全部内容。。。

优化数据加载

我们可以提高应用性能的一个方法是优化它从数据库加载数据的方式。这并不是一件很难实现的事情,而 ORM 解决方案使所有这些都能够启动并运行起来更加简单。

优化数据加载只有几个规则。那么,让我们看看这些是什么,以及它们如何证明是有利的:

  • 延迟加载可以跳过的数据:当我们知道不需要从数据库中获取的所有数据时,我们可以利用延迟加载技术安全地延迟加载该数据。例如,如果我们想向 BugZot 应用中的所有用户发送一封邮件,这些用户有 10 多个待处理的 bug,并且不是管理员,那么我们可以推迟角色关系的加载,这有助于显著减少应用的响应时间及其总体内存占用,而牺牲一些额外的查询,这可能是一个理想的折衷方案。
  • 如果要使用数据,则提前加载数据:与第一点完全相反,如果我们知道应用将使用数据,无论情况如何,那么一次加载数据是完全有意义的,而不是发出额外的查询来按需加载数据。例如,如果我们想将所有管理员提升为超级管理员,我们知道我们将访问所有用户的角色字段。然后,让应用延迟加载角色字段是没有意义的。我们可以简单地要求应用立即加载所需的数据,这样应用就不会等待数据按需加载。这种类型的优化以增加内存使用和缓慢的初始响应时间为代价,但在加载所有数据后提供了快速执行的优势。
  • 不要加载不需要的数据:有时对象映射到的某些关系在处理过程中根本不需要。在这种情况下,我们可以通过根本不加载这些关系对象来节省大量内存和时间。在 SQLAlchemy 中,只需设置lazy_load='noload'即可轻松实现这一点。这种用例的一个例子是,当我们只想更新数据库中用户的last_active时间时,不需要加载关系。在这种情况下,我们知道不需要验证与用户角色相关的任何内容,因此我们可以完全跳过角色的加载。

如果加载技术完全嵌入到模型定义中,显然无法实现这些效果。因此,SQLAlchemy 确实通过使用不同的方法提供了实现这些效果的另一种方法,根据他们用于从数据库加载数据的技术,恰当地命名为lazyload(),例如,joinedload()用于延迟加载,subqueryload()用于子查询急切加载,以及noload()用于不加载,我们将在后面的章节中解释,包括如何在实际应用中使用它们。

现在我们熟悉了加载技术以及如何使用它们来实现我们的优势,现在让我们来看看本章的最后一个主题,在这里我们将看到如何利用缓存来加快我们的应用响应时间,以及节省我们一次又一次地查询数据库的努力。当应用执行大量数据密集型操作时,这确实会帮助我们。

利用缓存

在大多数企业应用中,一次访问过的数据会被反复使用。这可能是在不同的请求中,也可能是因为请求在同一组数据上运行。

在这类场景中,如果我们尝试一次又一次地从数据库访问相同的数据,会造成巨大的资源浪费,导致应用对数据库进行大量查询,从而导致高数据库负载和低响应时间。

我们使用的 ORM 层为已访问的数据提供了一定程度的缓存,但大多数控制权仍掌握在应用开发人员手中,他们可以利用自己的智慧,通过反复分析将使用哪些数据来提高应用的性能。。。

数据库级别的缓存

数据库是一个相当复杂的软件。它们不仅能有效地存储数据,还为我们提供了以同样的效率检索数据的机制。这涉及到很多在幕后进行的复杂逻辑。

使用 ORM 的优点之一是数据库可以在查询级别执行缓存。由于数据库应该以尽可能快的方式返回数据,数据库系统通常会缓存一次又一次执行的查询。这种缓存发生在查询解析级别,因此当在数据库上执行相同的查询时,不必一次又一次地解析相同的查询,就可以节省一些时间。

这种缓存提高了响应时间,因为解析查询节省了大量工作。

块级缓存

现在,让我们看看在应用级别上可以使用的缓存,这可以证明是一个很大的帮助。

为了理解应用块级别的缓存概念,我们来看看下面的简单代码片段:

for name in ['super_admin', 'admin', 'user']:  if db_session.query(User).first().role.role_name == name:    print("True")

从我们可以假设的情况来看,这可以执行一次查询,然后从数据库中检索数据,然后反复使用它与 name 变量进行比较。但是让我们看一下前面代码的输出:

INFO sqlalchemy.engine.base.Engine SELECT users.username AS users_username, users.id AS users_id, users.role_id AS ...

使用用户级缓存

用户级缓存是另一种缓存级别,可以证明它非常有用。想象一下,每当用户从一个页面移动到另一个页面时,从数据库查询用户的个人详细信息。这不仅效率低下,而且在高负载情况下也会受到惩罚,因为数据库的响应时间可能非常长,以至于请求可能会超时,并且在总体负载降低之前,用户将无法登录到应用。

那么,这里有什么可以帮忙的吗?

答案是用户级缓存。当我们知道某些数据是特定于用户的,并且对安全性并不重要时,我们只需从数据库中加载一次数据并将其保存在用户端。这可以通过在客户端实现 cookie 或创建临时文件来实现。这些 cookie 或临时文件存储有关用户的非机密数据,如用户 ID 或用户名,或其他非重要数据,如用户名称。

每当应用希望加载此数据时,而不是直接访问数据库,它首先检查用户端是否有此数据可用。如果找到数据,则从那里加载数据。如果在用户端找不到数据,则向数据库发出请求,并从那里加载数据,然后最终在客户端缓存数据。

当试图减少特定于用户的数据加载的影响时,这种技术有很大帮助,并且不需要经常从数据库中刷新数据。

我们将在后面的章节中看到,通过使用键值缓存机制来缓存数据的技术要复杂得多,例如使用 memcached 等工具实现内存缓存,这在处理大量数据时会非常有帮助。然而,这超出了本书的范围,因为涉及的主题非常复杂,可以跨越数百页。

总结

在本章中,我们学习了如何构建数据库模型,以帮助我们在处理大规模数据时提高应用的性能。我们了解了优化模型如何成为优化的第一阶段,以及它如何通过减少数据库模型之间的耦合,帮助我们使应用更易于维护。然后,我们继续讨论索引如何通过索引访问频率更高的列来帮助更快地访问数据库中的数据。

我们后来讨论了通过使用事务来维护数据库一致性的一个重要方面。

本章的最后一部分介绍了数据加载技术,如延迟加载、即时加载和无加载。。。

问题

  1. 规范化数据库表的好处是什么?
  2. 通过select延迟加载与通过joined延迟加载有什么区别?
  3. 我们如何在运行数据库更新查询时保持数据的完整性?
  4. 从数据库缓存数据的不同级别是什么?