缓存指南
缓存是一种常见的技术,目标是提高系统的性能和伸缩性。 它通过暂时将经常访问的数据复制到靠近应用程序的快速存储来缓存数据。 如果这种快速数据存储比原始源更靠近应用程序,则缓存可以通过更快速提供数据,大幅改善客户端应用程序的响应时间。
当客户端实例重复读取相同的数据时,缓存最有效,尤其是在以下所有条件都适用于原始数据存储时:
- 它保持相对静态。
- 相对于缓存速度而言较慢时。
- 受限于激烈的资源争用。
- 当网络延迟可能导致访问速度缓慢时,这很遥远。
缓存数据时,分布式应用程序通常实现以下任一或两种策略:
- 它们使用专用缓存,其中数据保存在运行应用程序或服务实例的计算机上本地。
- 它们使用共享缓存,充当可由多个进程和计算机访问的公共源。
在这两种情况下,都可以执行客户端和服务器端缓存。 客户端缓存由为系统(如 Web 浏览器或桌面应用程序)提供用户界面的过程完成。 服务器端缓存由提供远程运行的业务服务的过程完成。
最基本的缓存类型是内存中存储。 它保存在单个进程的地址空间中,并由在该进程中运行的代码直接访问。 这种类型的缓存可以快速访问。 它还可以提供用于存储少量静态数据的有效方法。 缓存的大小通常受托管进程的计算机上的可用内存量的约束。
如果需要在内存中缓存比物理上可能更多的信息,可以将缓存的数据写入本地文件系统。 与内存中保存的数据相比,此过程的访问速度会变慢,但与跨网络检索数据相比,此过程仍应该更快、更可靠。
如果有多个应用程序实例使用此模型同时运行,则每个应用程序实例都有自己的独立缓存,其中包含其自己的数据副本。
将缓存视为过去某个时候原始数据的快照。 如果此数据不是静态的,则可能不同的应用程序实例在其缓存中保存不同版本的数据。 因此,这些实例执行的同一查询可以返回不同的结果,如图 1 所示。
图 1:在不同的应用程序实例中使用内存中缓存。
如果使用共享缓存,则有助于缓解每个缓存中数据可能有所不同的担忧,这可能会导致内存中缓存发生。 共享缓存可确保不同的应用程序实例看到相同的缓存数据视图。 它将缓存定位在单独的位置,该位置通常作为单独服务的一部分进行托管,如图 2 所示。
图 2:使用共享缓存。
共享缓存方法的一个重要好处是它提供的可伸缩性。 许多共享缓存服务是使用服务器群集实现的,并使用软件以透明方式在群集之间分发数据。 应用程序实例只是将请求发送到缓存服务。 底层基础结构确定群集中缓存数据的位置。 可以通过添加更多服务器轻松缩放缓存。
共享缓存方法有两个主要缺点:
- 缓存访问速度较慢,因为它不再保存在每个应用程序实例本地。
- 实现单独的缓存服务的要求可能会增加解决方案的复杂性。
以下部分更详细地介绍了设计和使用缓存的注意事项。
缓存可以显著提高性能、可伸缩性和可用性。 拥有的数据越多,需要访问此数据的用户数量越多,缓存的好处就越大。 缓存可减少与处理原始数据存储中大量并发请求相关的延迟和争用。
例如,数据库可能支持有限数量的并发连接。 但是,从共享缓存检索数据(而不是基础数据库)使客户端应用程序能够访问此数据,即使当前可用连接数已用尽。 此外,如果数据库不可用,客户端应用程序可能能够继续使用缓存中保存的数据。
考虑缓存经常读取但不经常修改的数据(例如,读取作比例高于写入作的数据)。 但是,不建议将缓存用作关键信息的权威存储。 相反,请确保应用程序无法丢失的所有更改始终保存到持久性数据存储。 如果缓存不可用,应用程序仍可使用数据存储继续运行,并且不会丢失重要信息。
有效使用缓存的关键在于确定要缓存的最合适的数据,并在适当的时间缓存它。 数据可以在应用程序首次检索数据时按需添加到缓存中。 应用程序只需从数据存储提取一次数据,并且可以使用缓存满足后续访问。
或者,缓存可以提前部分填充或完全填充数据,通常是应用程序启动时(称为种子设定的方法)。 但是,不建议为大型缓存实现种子设定,因为当应用程序开始运行时,此方法可能会对原始数据存储造成突然的高负载。
通常,使用模式的分析可以帮助你决定是否完全或部分预填充缓存,以及选择要缓存的数据。 例如,可以将缓存与静态用户配置文件数据种子设定为定期使用应用程序(可能每天使用)的客户,但对于每周只使用该应用程序一次的客户,则不能为缓存设定种子。
缓存通常适用于不可变或不经常更改的数据。 示例包括参考信息,例如电子商务应用程序中的产品和定价信息,或构建成本高昂的共享静态资源。 在应用程序启动时,可以将部分或全部数据加载到缓存中,以最大程度地减少对资源的需求并提高性能。 你可能还需要有一个后台进程,定期更新缓存中的引用数据,以确保其 up-to日期。 或者,当引用数据更改时,后台进程可以刷新缓存。
缓存对动态数据不太有用,尽管此注意事项存在一些例外情况(有关详细信息,请参阅本文后面的“缓存高度动态数据”部分)。 当原始数据定期更改时,缓存的信息会很快过时,或者将缓存与原始数据存储同步的开销会降低缓存的有效性。
缓存不必包括实体的完整数据。 例如,如果数据项表示多值对象,例如具有名称、地址和帐户余额的银行客户,则其中一些元素可能保持静态,例如名称和地址。 其他元素(如帐户余额)可能更具动态性。 在这些情况下,缓存数据的静态部分并仅在需要时检索(或计算)剩余信息可能很有用。
建议执行性能测试和使用情况分析,以确定缓存的预填充或按需加载,还是两者的组合都合适。 决策应基于数据的波动性和使用模式。 缓存利用率和性能分析对于遇到大量负载且必须高度可缩放的应用程序非常重要。 例如,在高度可缩放的方案中,可以设定缓存种子,以在高峰时间减少数据存储上的负载。
缓存还可用于避免在应用程序运行时重复计算。 如果作转换数据或执行复杂的计算,它可以在缓存中保存作的结果。 如果之后需要相同的计算,应用程序只需从缓存中检索结果。
应用程序可以修改缓存中保存的数据。 但是,建议将缓存视为随时可能消失的暂时性数据存储。 不要仅将有价值的数据存储在缓存中;请确保还保留原始数据存储中的信息。 这意味着,如果缓存变得不可用,则可以最大程度地减少丢失数据的机会。
在持久性数据存储中快速存储信息时,可能会给系统带来开销。 例如,请考虑持续报告状态或其他度量的设备。 如果应用程序选择不根据缓存的信息几乎始终过时来缓存此数据,那么在存储和从数据存储中检索此信息时,相同的注意事项可能是正确的。 在保存和提取此数据所需的时间中,它可能已更改。
在这种情况下,请考虑将动态信息直接存储在缓存而不是持久数据存储中的好处。 如果数据不关键且不需要审核,则偶尔更改丢失并不重要。
在大多数情况下,缓存中保存的数据是原始数据存储中保存的数据的副本。 原始数据存储中的数据在缓存后可能会更改,从而导致缓存的数据变得过时。 许多缓存系统使你能够将缓存配置为过期数据,并减少数据可能过期的时间段。
缓存数据过期时,会从缓存中删除该数据,应用程序必须从原始数据存储检索数据(它可以将新提取的信息放回缓存中)。 配置缓存时,可以设置默认过期策略。 在许多缓存服务中,还可以在以编程方式将其存储在缓存中时为各个对象规定到期期限。 某些缓存使你可以将过期期限指定为绝对值,或指定一个滑动值,以便在指定时间内未访问该项时从缓存中删除该项。 此设置将覆盖任何缓存范围的过期策略,但仅适用于指定的对象。
备注
请考虑缓存的过期期限及其包含的对象。 如果使其太短,则对象会过期太快,并减少使用缓存的好处。 如果时间过长,则可能会使数据变得过时。
如果允许数据长时间驻留,缓存可能会填满。 在这种情况下,向缓存添加新项的任何请求都可能导致某些项在称为逐出的进程中强行删除。 缓存服务通常以最近使用最少的 (LRU) 方式逐出数据,但通常可以重写此策略并阻止项被逐出。 但是,如果采用此方法,则存在超出缓存中可用内存的风险。 尝试将项添加到缓存的应用程序将失败并出现异常。
某些缓存实现可能会提供其他逐出策略。 有多种类型的逐出策略。 这些包括:
- 最近使用的策略(预期数据不会再次需要)。
- 先出先出策略(最早的数据先逐出)。
- 基于触发的事件(例如修改的数据)的显式删除策略。
客户端缓存中保存的数据通常被视为不在向客户端提供数据的服务的主持之外。 服务无法直接强制客户端添加或删除客户端缓存中的信息。
这意味着,使用配置不佳的缓存继续使用过时信息的客户端是可能的。 例如,如果缓存的过期策略未正确实现,则当原始数据源中的信息发生更改时,客户端可能会使用本地缓存的过时信息。
如果生成通过 HTTP 连接提供数据的 Web 应用程序,则可以隐式强制 Web 客户端(例如浏览器或 Web 代理)提取最新信息。 如果资源是通过该资源的 URI 中的更改更新的,则可以执行此作。 Web 客户端通常使用资源的 URI 作为客户端缓存中的密钥,因此,如果 URI 发生更改,Web 客户端将忽略以前缓存的任何资源版本并提取新版本。
缓存通常设计为由应用程序的多个实例共享。 每个应用程序实例都可以读取和修改缓存中的数据。 因此,任何共享数据存储出现的相同并发问题也适用于缓存。 在应用程序需要修改缓存中保存的数据的情况下,可能需要确保应用程序的一个实例进行的更新不会覆盖另一个实例所做的更改。
根据数据的性质和冲突的可能性,可以采用以下两种方法之一实现并发:
- 乐观。 在更新数据之前,应用程序会检查缓存中的数据自检索以来是否已更改。 如果数据仍然相同,可以进行更改。 否则,应用程序必须决定是否对其进行更新。 (驱动此决策的业务逻辑将特定于应用程序。此方法适用于更新不频繁或冲突不太可能发生的情况。
- 悲观。 检索数据时,应用程序将其锁定在缓存中,以防止另一个实例更改它。 此过程可确保冲突无法发生,但它们还可以阻止需要处理相同数据的其他实例。 悲观并发可能会影响解决方案的可伸缩性,建议仅用于短期作。 此方法可能适用于冲突的可能性更大的情况,尤其是在应用程序更新缓存中的多个项时,必须确保这些更改一致地应用。
避免使用缓存作为数据的主要存储库;这是填充缓存的原始数据存储的角色。 原始数据存储负责确保数据的持久性。
请注意不要将共享缓存服务可用性的关键依赖项引入解决方案。 如果提供共享缓存的服务不可用,应用程序应能够继续运行。 等待缓存服务恢复时,应用程序不应变得无响应或失败。
因此,应用程序必须准备好检测缓存服务的可用性,并在缓存不可访问时回退到原始数据存储。 Circuit-Breaker 模式可用于处理此方案。 提供缓存的服务可以恢复,一旦缓存可用,缓存就可以在从原始数据存储中读取数据时重新填充,并遵循 缓存端模式等策略。
但是,如果应用程序在缓存暂时不可用时回退到原始数据存储,则系统可伸缩性可能会受到影响。 在恢复数据存储时,原始数据存储可能会被数据请求淹没,从而导致超时和连接失败。
请考虑在应用程序的每个实例中实现本地专用缓存,以及所有应用程序实例访问的共享缓存。 当应用程序检索项时,它可以先签入其本地缓存,然后在共享缓存中,最后签入原始数据存储。 如果共享缓存不可用,则可以使用共享缓存中的数据填充本地缓存。
此方法需要仔细配置,以防止本地缓存在共享缓存方面变得太过时。 但是,如果共享缓存无法访问,则本地缓存充当缓冲区。 图 3 显示了此结构。
图 3:将本地专用缓存与共享缓存配合使用。
为了支持保存相对长生存期数据的大型缓存,某些缓存服务提供了一个高可用性选项,当缓存不可用时,该选项可实现自动故障转移。 此方法通常涉及将存储在主缓存服务器上的缓存数据复制到辅助缓存服务器,并在主服务器发生故障或连接丢失时切换到辅助服务器。
为了减少与写入到多个目标相关的延迟,将数据写入主服务器上的缓存时,可能会异步地复制到辅助服务器。 与缓存的总体大小相比,此方法可能导致某些缓存信息丢失(如果发生故障),但此数据的比例应该很小。
如果共享缓存很大,则跨节点对缓存数据进行分区可能会很有帮助,以减少争用和提高可伸缩性的可能性。 许多共享缓存支持动态添加(和删除)节点以及跨分区重新平衡数据的功能。 此方法可能涉及群集,其中节点集合以无缝的单一缓存的形式呈现给客户端应用程序。 但是,在内部,数据分散在节点之间,遵循一个均衡负载的预定义分发策略。 有关可能的分区策略的详细信息,请参阅 数据分区指南。
聚类分析还可以提高缓存的可用性。 如果节点失败,仍可访问缓存的其余部分。 群集经常与复制和故障转移结合使用。 可以复制每个节点,如果节点发生故障,副本可以快速联机。
许多读取和写入作都可能涉及单个数据值或对象。 但是,有时可能需要快速存储或检索大量数据。 例如,对缓存进行种子设定可能涉及将数百或数千个项写入缓存。 应用程序可能还需要从缓存中检索大量相关项作为同一请求的一部分。
许多大型缓存为这些目的提供批处理作。 这样,客户端应用程序就可以将大量项打包到单个请求中,并减少与执行大量小型请求相关的开销。
若要使缓存端模式正常工作,填充缓存的应用程序实例必须有权访问最新且一致的数据版本。 在实现最终一致性(如复制数据存储)的系统中,这种情况可能并非如此。
应用程序的一个实例可以修改数据项并使该项的缓存版本失效。 应用程序的另一个实例可能会尝试从缓存中读取此项,这会导致缓存丢失,因此它会从数据存储读取数据并将其添加到缓存。 但是,如果数据存储尚未与其他副本完全同步,则应用程序实例可以使用旧值读取和填充缓存。
有关处理数据一致性的详细信息,请参阅 数据一致性入门。
无论使用何种缓存服务,请考虑如何保护缓存中保存的数据免受未经授权的访问。 有两个主要问题:
- 缓存中数据的隐私。
- 数据在缓存与使用缓存的应用程序之间流动时数据的隐私。
为了保护缓存中的数据,缓存服务可能实现一种身份验证机制,要求应用程序指定以下内容:
- 哪些标识可以访问缓存中的数据。
- 允许执行这些标识的哪些作(读取和写入)。
为了减少与读取和写入数据相关的开销,在向标识授予对缓存的写入或读取访问权限后,该标识可以使用缓存中的任何数据。
如果需要限制对缓存数据的子集的访问,可以执行下列作之一:
- 将缓存拆分为分区(使用不同的缓存服务器),并仅授予对应允许使用的分区的标识的访问权限。
- 使用不同的密钥对每个子集中的数据进行加密,并向应有权访问每个子集的标识提供加密密钥。 客户端应用程序仍可以检索缓存中的所有数据,但只能解密其具有密钥的数据。
还必须在数据传入和流出缓存时保护数据。 为此,取决于客户端应用程序用来连接到缓存的网络基础结构提供的安全功能。 如果缓存是使用托管客户端应用程序的同一组织中的站点服务器实现的,则网络本身的隔离可能不需要执行其他步骤。 如果缓存位于远程位置,并且需要通过公用网络(如 Internet)建立 TCP 或 HTTP 连接,请考虑实现 SSL。
Azure Redis 缓存 是开源 Redis 缓存的实现,该缓存在 Azure 数据中心作为服务运行。 它提供可从任何 Azure 应用程序访问的缓存服务,无论应用程序是作为云服务、网站或 Azure 虚拟机内部实现的。 缓存可由具有相应访问密钥的客户端应用程序共享。
Azure Redis 缓存是一种高性能缓存解决方案,可提供可用性、可伸缩性和安全性。 它通常作为分散在一台或多台专用计算机的服务上运行。 它尝试将尽可能多的信息存储在内存中,以确保快速访问。 此体系结构旨在通过减少执行慢 I/O作的需求来提供低延迟和高吞吐量。
Azure Redis 缓存与客户端应用程序使用的许多各种 API 兼容。 如果已有使用本地运行的 Azure Redis 缓存的应用程序,则 Azure Redis 缓存提供了在云中缓存的快速迁移路径。
Redis 不仅仅是一个简单的缓存服务器。 它提供一个分布式内存中数据库,其中包含支持许多常见方案的广泛命令集。 本文档稍后将介绍这些内容,请参阅“使用 Redis 缓存”部分。 本部分总结了 Redis 提供的一些关键功能。
Redis 支持读取和写入作。 在 Redis 中,写入可以通过定期存储在本地快照文件或仅追加日志文件中来保护系统故障。 在许多缓存中,这种情况并不是这种情况,应将其视为临时数据存储。
所有写入都是异步的,不会阻止客户端读取和写入数据。 当 Redis 开始运行时,它会从快照或日志文件中读取数据,并使用它来构造内存中缓存。 有关详细信息,请参阅 Redis 网站上的 Redis 持久性 。
备注
Redis 不保证如果发生灾难性故障,所有写入都将保存,但最坏情况下,可能会丢失几秒的数据。 请记住,缓存不打算充当权威数据源,并且应用程序负责使用缓存来确保关键数据已成功保存到适当的数据存储。 有关详细信息,请参阅 缓存端模式。
Redis 是键值存储,其中值可以包含简单类型或复杂的数据结构,例如哈希、列表和集。 它支持对这些数据类型执行一组原子作。 密钥可以永久或标记有有限的生存时间,此时密钥及其相应的值会自动从缓存中删除。 有关 Redis 键和值的详细信息,请访问 Redis 网站上的 Redis 数据类型和抽象简介 页。
Redis 支持主/从属复制,以帮助确保可用性和维护吞吐量。 写入 Redis 主节点的作将复制到一个或多个从属节点。 读取作可由主要或任一从属服务。
如果有网络分区,则从属服务器可以继续提供数据,然后在重新建立连接时以透明方式与主数据库重新同步。 有关更多详细信息,请访问 Redis 网站上的 “复制 ”页。
Redis 还提供聚类分析,使你能够以透明方式将数据分区到服务器之间的分片并分散负载。 此功能可提高可伸缩性,因为可以添加新的 Redis 服务器,并且随着缓存大小增加而重新分区的数据。
此外,可以使用主/从属复制来复制群集中的每个服务器。 这可确保群集中每个节点的可用性。 有关群集和分片的详细信息,请访问 Redis 网站上的 Redis 群集教程页 。
Redis 缓存的大小有限,具体取决于主计算机上可用的资源。 配置 Redis 服务器时,可以指定可以使用的最大内存量。 还可以将 Redis 缓存中的密钥配置为具有过期时间,之后该密钥会自动从缓存中删除。 此功能有助于防止内存中缓存填充旧数据或过时数据。
随着内存的填充,Redis 可以通过遵循多个策略自动逐出密钥及其值。 默认值为 LRU(最近使用最少),但你也可以选择其他策略,例如随机逐出密钥或完全关闭逐出(在这种情况下,如果缓存已满,则尝试将项添加到缓存失败)。 使用 Redis 作为 LRU 缓存的页面提供了更多信息。
Redis 使客户端应用程序能够提交一系列作,以原子事务的形式在缓存中读取和写入数据。 事务中的所有命令都保证按顺序运行,其他并发客户端发出的命令不会在它们之间交织。
但是,这些事务不是真正的事务,因为关系数据库会执行这些事务。 事务处理由两个阶段组成,第一个阶段是命令排队时,第二个阶段是运行命令。 在命令队列阶段,构成事务的命令由客户端提交。 如果此时发生某种错误(例如语法错误或参数数量错误),则 Redis 拒绝处理整个事务并放弃它。
在运行阶段,Redis 按顺序执行每个排队命令。 如果命令在此阶段失败,Redis 将继续执行下一个排队命令,并且不会回滚已运行的任何命令的效果。 这种简化的事务形式有助于维护性能,并避免争用导致的性能问题。
Redis 确实实现了一种乐观锁定形式,以帮助保持一致性。 有关 Redis 的事务和锁定的详细信息,请访问 Redis 网站上的 “事务”页 。
Redis 还支持对请求进行非事务批处理。 客户端用来将命令发送到 Redis 服务器的 Redis 协议使客户端能够将一系列作作为同一请求的一部分发送。 这有助于减少网络上的数据包碎片。 处理批处理时,将执行每个命令。 如果这些命令中的任何一个格式不正确,它们将被拒绝(事务不会发生),但将执行其余命令。 也不能保证批处理中命令的处理顺序。
Redis 纯粹专注于提供对数据的快速访问,旨在在仅可由受信任的客户端访问的受信任环境中运行。 Redis 支持基于密码身份验证的有限安全模型。 (尽管我们不建议这样做,但可以完全删除身份验证。
所有经过身份验证的客户端共享相同的全局密码并有权访问同一资源。 如果需要更全面的登录安全性,则必须在 Redis 服务器前面实现自己的安全层,并且所有客户端请求都应通过此附加层。 Redis 不应直接公开给不受信任的或未经身份验证的客户端。
可以通过禁用命令或重命名命令来限制对命令的访问(并且仅提供具有新名称的特权客户端)。
Redis 不直接支持任何形式的数据加密,因此所有编码都必须由客户端应用程序执行。 此外,Redis 不提供任何形式的传输安全性。 如果需要在数据流经网络时保护数据,建议实现 SSL 代理。
有关详细信息,请访问 Redis 网站上的 Redis 安全 页。
备注
Azure Redis 缓存提供自己的安全层,客户端通过该层进行连接。 基础 Redis 服务器不会向公共网络公开。
Azure Redis 缓存提供对 Azure 数据中心托管的 Redis 服务器的访问权限。 它充当提供访问控制和安全性的外观。 可以使用 Azure 门户预配缓存。
门户提供了许多预定义的配置。 这些范围从作为专用服务运行的 53 GB 缓存(用于隐私)和支持 SSL 通信(隐私)和主/从属复制,服务级别协议(SLA)为 99.9% 可用性,到在共享硬件上运行的 250 MB 缓存(无可用性保证)。
使用 Azure 门户,还可以配置缓存的逐出策略,并通过将用户添加到提供的角色来控制对缓存的访问。 这些角色定义成员可以执行的作,包括所有者、参与者和读取者。 例如,所有者角色的成员完全控制缓存(包括安全性)及其内容,参与者角色的成员可以在缓存中读取和写入信息,读取者角色的成员只能从缓存中检索数据。
大多数管理任务都是通过 Azure 门户执行的。 因此,许多在标准版本的 Redis 中可用的管理命令都不可用,包括能够以编程方式修改配置、关闭 Redis 服务器、配置其他从属服务器或强制将数据保存到磁盘。
Azure 门户包含一个方便的图形显示,可用于监视缓存的性能。 例如,可以查看正在建立的连接数、正在执行的请求数、读取和写入量以及缓存命中次数与缓存未命中数。 使用此信息,可以确定缓存的有效性,如有必要,请切换到其他配置或更改逐出策略。
此外,如果一个或多个关键指标超出预期范围,则可以创建向管理员发送电子邮件的警报。 例如,如果缓存未命中次数超过过去一小时内的指定值,则可能需要向管理员发出警报,因为这意味着缓存可能太小,或者数据可能太快被逐出。
还可以监视缓存的 CPU、内存和网络使用情况。
有关如何创建和配置 Azure Redis 缓存的详细信息和示例,请访问 Azure 博客上有关 Azure Redis 缓存的页面 Lap。
如果生成 ASP.NET 使用 Azure Web 角色运行的 Web 应用程序,则可以在 Azure Redis 缓存中保存会话状态信息和 HTML 输出。 使用 Azure Redis 缓存的会话状态提供程序可以在 ASP.NET Web 应用程序的不同实例之间共享会话信息,并且对于客户端-服务器相关性不可用且缓存内存中的会话数据不适用的 Web 场情况非常有用。
将会话状态提供程序与 Azure Redis 缓存配合使用可提供多项优势,包括:
- 与大量 ASP.NET Web 应用程序的实例共享会话状态。
- 提供改进的可伸缩性。
- 支持对多个读取器和单个编写器对同一会话状态数据的受控并发访问。
- 使用压缩来节省内存并提高网络性能。
有关详细信息,请参阅 适用于 Azure Redis 缓存的 ASP.NET 会话状态提供程序。
备注
不要将 Azure Cache for Redis 的会话状态提供程序与在 Azure 环境外部运行的 ASP.NET 应用程序一起使用。 从 Azure 外部访问缓存的延迟可以消除缓存数据的性能优势。
同样,使用 Azure Redis 缓存的输出缓存提供程序可以保存由 ASP.NET Web 应用程序生成的 HTTP 响应。 将输出缓存提供程序与 Azure Redis 缓存配合使用可以提高呈现复杂 HTML 输出的应用程序的响应时间。 生成类似响应的应用程序实例可以使用缓存中的共享输出片段,而不是重新生成此 HTML 输出。 有关详细信息,请参阅 azure Redis 缓存的 ASP.NET 输出缓存提供程序。
Azure Redis 缓存充当基础 Redis 服务器的外观。 如果需要 Azure Redis 缓存(例如大于 53 GB 的缓存)未涵盖的高级配置,则可以使用 Azure 虚拟机生成和托管自己的 Redis 服务器。
这是一个潜在的复杂过程,因为如果需要实现复制,可能需要创建多个 VM 来充当主节点和从属节点。 此外,如果要创建群集,则需要多个主服务器和从属服务器。 提供高可用性和可伸缩性的最小群集复制拓扑至少包含六个 VM,这些 VM 组织为三对主/从属服务器(群集必须至少包含三个主节点)。
每个主要/从属对应位于一起,以最大程度地减少延迟。 但是,如果希望找到最可能使用它的应用程序附近的缓存数据,则可以在不同的 Azure 数据中心运行每组对。 有关生成和配置作为 Azure VM 运行的 Redis 节点的示例,请参阅 在 Azure 中的 CentOS Linux VM 上运行 Redis。
备注
如果通过这种方式实现自己的 Redis 缓存,则负责监视、管理和保护服务。
对缓存进行分区涉及跨多台计算机拆分缓存。 此结构提供了使用单个缓存服务器的优势,包括:
- 创建比单个服务器上存储的缓存要大得多。
- 跨服务器分发数据,提高可用性。 如果一台服务器发生故障或无法访问,则保存的数据不可用,但仍可以访问剩余服务器上的数据。 对于缓存,这并不重要,因为缓存的数据只是数据库中保存的数据的暂时性副本。 可以在其他服务器上缓存不可访问的服务器上的缓存数据。
- 跨服务器分布负载,从而提高性能和可伸缩性。
- 将数据放置在靠近访问数据的用户的地理位置,从而减少延迟。
对于缓存,最常见的分区形式是分片。 在此策略中,每个分区(或分片)都是自己的 Redis 缓存。 数据通过使用分片逻辑定向到特定分区,该逻辑可以使用各种方法来分发数据。 分片模式提供有关实现分片的详细信息。
若要在 Redis 缓存中实现分区,可以采用以下方法之一:
- 服务器端查询路由。 在此技术中,客户端应用程序向构成缓存的任何 Redis 服务器(可能是最近的服务器)发送请求。 每个 Redis 服务器存储描述其保存的分区的元数据,并包含有关哪些分区位于其他服务器上的信息。 Redis 服务器检查客户端请求。 如果它可以在本地解析,它将执行请求的作。 否则,它会将请求转发到相应的服务器上。 此模型由 Redis 聚类分析实现,并在 Redis 网站上的 Redis 群集教程 页上更详细地介绍。 Redis 群集对客户端应用程序是透明的,可以将其他 Redis 服务器添加到群集(以及重新分区的数据),而无需重新配置客户端。
- 客户端分区。 在此模型中,客户端应用程序包含逻辑(可能以库的形式)将请求路由到相应的 Redis 服务器。 此方法可与 Azure Redis 缓存配合使用。 创建多个 Azure Redis 缓存(每个数据分区一个),并实现将请求路由到正确缓存的客户端逻辑。 如果分区方案发生更改(例如,如果创建了其他 Azure Redis 缓存),则可能需要重新配置客户端应用程序。
- 代理辅助分区。 在此方案中,客户端应用程序将请求发送到中介代理服务,该服务了解数据的分区方式,然后将请求路由到相应的 Redis 服务器。 此方法还可用于 Azure Redis 缓存;代理服务可以作为 Azure 云服务实现。 此方法需要额外的复杂性才能实现服务,并且请求的执行时间可能比使用客户端分区要长。
页面 分区:如何在 Redis 网站上的多个 Redis 实例之间拆分数据 ,进一步了解如何使用 Redis 实现分区。
Redis 支持使用多种编程语言编写的客户端应用程序。 如果使用 .NET Framework 生成新应用程序,建议使用 StackExchange.Redis 客户端库。 此库提供了一个 .NET Framework 对象模型,用于抽象化连接到 Redis 服务器、发送命令和接收响应的详细信息。 它以 NuGet 包的形式在 Visual Studio 中提供。 可以使用同一库连接到 Azure Redis 缓存,或 VM 上托管的自定义 Redis 缓存。
若要连接到 Redis 服务器,请使用类的Connect
静态ConnectionMultiplexer
方法。 此方法创建的连接旨在在整个客户端应用程序的整个生存期内使用,并且多个并发线程可以使用同一连接。 每次执行 Redis作时不要重新连接和断开连接,因为这会降低性能。
可以指定连接参数,例如 Redis 主机的地址和密码。 如果使用 Azure Redis 缓存,则密码是使用 Azure 门户为 Azure Redis 缓存生成的主密钥或辅助密钥。
连接到 Redis 服务器后,可以在充当缓存的 Redis 数据库上获取句柄。 Redis 连接提供 GetDatabase
执行此作的方法。 然后,可以使用和StringGet
方法从缓存中检索项,并将数据存储在缓存StringSet
中。 这些方法需要一个键作为参数,并在缓存中返回具有匹配值的项(StringGet
)或使用此键将项添加到缓存中(StringSet
)。
根据 Redis 服务器的位置,在将请求传输到服务器并将响应返回到客户端时,许多作可能会产生一些延迟。 StackExchange 库提供了它公开的许多方法的异步版本,以帮助客户端应用程序保持响应。 这些方法支持 .NET Framework 中 基于任务的异步模式 。
以下代码片段显示了一个名为 RetrieveItem
.. 它演示了基于 Redis 和 StackExchange 库的缓存端模式的实现。 该方法采用字符串键值,并尝试通过调用 StringGetAsync
方法(异步版本的 StringGet
)从 Redis 缓存中检索相应的项。
如果未找到该项,则会使用 GetItemFromDataSourceAsync
该方法(这是本地方法,而不是 StackExchange 库的一部分)从基础数据源中提取该项。 然后使用该方法将其添加到缓存 StringSetAsync
中,以便下次可以更快地检索它。
// Connect to the Azure Redis cache
ConfigurationOptions config = new ConfigurationOptions();
config.EndPoints.Add("<your DNS name>.redis.cache.windows.net");
config.Password = "<Redis cache key from management portal>";
ConnectionMultiplexer redisHostConnection = ConnectionMultiplexer.Connect(config);
IDatabase cache = redisHostConnection.GetDatabase();
...
private async Task<string> RetrieveItem(string itemKey)
{
// Attempt to retrieve the item from the Redis cache
string itemValue = await cache.StringGetAsync(itemKey);
// If the value returned is null, the item was not found in the cache
// So retrieve the item from the data source and add it to the cache
if (itemValue == null)
{
itemValue = await GetItemFromDataSourceAsync(itemKey);
await cache.StringSetAsync(itemKey, itemValue);
}
// Return the item
return itemValue;
}
和StringGet
StringSet
方法不限于检索或存储字符串值。 它们可将序列化为字节数组的任何项。 如果需要保存 .NET 对象,则可以将其序列化为字节流,并使用 StringSet
该方法将其写入缓存。
同样,可以使用该方法从缓存 StringGet
中读取对象,并将其反序列化为 .NET 对象。 以下代码显示了 IDatabase 接口的一组扩展方法( GetDatabase
Redis 连接的方法返回对象 IDatabase
),以及使用这些方法读取和写入 BlogPost
缓存对象的一些示例代码:
public static class RedisCacheExtensions
{
public static async Task<T> GetAsync<T>(this IDatabase cache, string key)
{
return Deserialize<T>(await cache.StringGetAsync(key));
}
public static async Task<object> GetAsync(this IDatabase cache, string key)
{
return Deserialize<object>(await cache.StringGetAsync(key));
}
public static async Task SetAsync(this IDatabase cache, string key, object value)
{
await cache.StringSetAsync(key, Serialize(value));
}
static byte[] Serialize(object o)
{
byte[] objectDataAsStream = null;
if (o != null)
{
var jsonString = JsonSerializer.Serialize(o);
objectDataAsStream = Encoding.ASCII.GetBytes(jsonString);
}
return objectDataAsStream;
}
static T Deserialize<T>(byte[] stream)
{
T result = default(T);
if (stream != null)
{
var jsonString = Encoding.ASCII.GetString(stream);
result = JsonSerializer.Deserialize<T>(jsonString);
}
return result;
}
}
以下代码演示了一种命名 RetrieveBlogPost
的方法,该方法使用这些扩展方法读取和写入缓存中的可 BlogPost
序列化对象,并遵循缓存端模式:
// The BlogPost type
public class BlogPost
{
private HashSet<string> tags;
public BlogPost(int id, string title, int score, IEnumerable<string> tags)
{
this.Id = id;
this.Title = title;
this.Score = score;
this.tags = new HashSet<string>(tags);
}
public int Id { get; set; }
public string Title { get; set; }
public int Score { get; set; }
public ICollection<string> Tags => this.tags;
}
...
private async Task<BlogPost> RetrieveBlogPost(string blogPostKey)
{
BlogPost blogPost = await cache.GetAsync<BlogPost>(blogPostKey);
if (blogPost == null)
{
blogPost = await GetBlogPostFromDataSourceAsync(blogPostKey);
await cache.SetAsync(blogPostKey, blogPost);
}
return blogPost;
}
如果客户端应用程序发送多个异步请求,Redis 支持命令管道传送。 Redis 可以使用同一连接多路复用请求,而不是按照严格的顺序接收和响应命令。
此方法有助于通过更有效地使用网络来降低延迟。 以下代码片段演示了一个示例,该示例可同时检索两个客户的详细信息。 代码提交两个请求,然后在等待接收结果之前执行一些其他处理(未显示)。
Wait
缓存对象的方法类似于 .NET Framework Task.Wait
方法:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
var task1 = cache.StringGetAsync("customer:1");
var task2 = cache.StringGetAsync("customer:2");
...
var customer1 = cache.Wait(task1);
var customer2 = cache.Wait(task2);
有关编写可以使用 Azure Redis 缓存的客户端应用程序的其他信息,请参阅 Azure Redis 缓存文档。 StackExchange.Redis 中还提供了详细信息。
同一网站上的 管道和多路复用器 页提供有关 Redis 和 StackExchange 库的异步作和管道传送的详细信息。
Redis 缓存问题的最简单用法是键值对,其中该值是一个可以包含任何二进制数据的任意长度的未解释字符串。 (实质上是可以视为字符串的字节数组)。 本文前面的“实现 Redis 缓存客户端应用程序”部分中演示了此方案。
请注意,密钥还包含未解释的数据,因此可以使用任何二进制信息作为密钥。 但是,密钥越长,存储所花费的空间越多,执行查找作所需的时间就越长。 为了方便使用和维护,请仔细设计密钥空间并使用有意义的(但不详细)密钥。
例如,使用结构化密钥(如“customer:100”)来表示 ID 为 100 的客户的密钥,而不仅仅是“100”。 借助此方案,可以轻松区分存储不同数据类型的值。 例如,还可以使用键“orders:100”来表示 ID 为 100 的订单的键。
除了一维二进制字符串外,Redis 键值对中的值还可以保存更多结构化信息,包括列表、集(排序和未排序)和哈希。 Redis 提供了一个可以作这些类型的综合命令集,其中许多命令可通过客户端库(如 StackExchange)提供给 .NET Framework 应用程序。 Redis 网站上的 Redis 数据类型和抽象简介 页面提供了对这些类型的更详细概述以及可用于作它们的命令。
本部分汇总了这些数据类型和命令的一些常见用例。
Redis 支持对字符串值执行一系列原子获取和设置作。 这些作消除了使用单独 GET
命令时 SET
可能发生的争用危险。 可用的作包括:
INCR
、INCRBY
、DECR
和DECRBY
,它们对整数数字数据值执行原子递增和递减运算。 StackExchange 库提供重载版本的IDatabase.StringIncrementAsync
和IDatabase.StringDecrementAsync
方法来执行这些作,并返回缓存中存储的结果值。 以下代码片段演示如何使用这些方法:ConnectionMultiplexer redisHostConnection = ...; IDatabase cache = redisHostConnection.GetDatabase(); ... await cache.StringSetAsync("data:counter", 99); ... long oldValue = await cache.StringIncrementAsync("data:counter"); // Increment by 1 (the default) // oldValue should be 100 long newValue = await cache.StringDecrementAsync("data:counter", 50); // Decrement by 50 // newValue should be 50
GETSET
,它检索与键关联的值,并将其更改为新值。 StackExchange 库通过IDatabase.StringGetSetAsync
该方法提供此作。 下面的代码片段显示了此方法的示例。 此代码返回与上一示例中的键“data:counter”关联的当前值。 然后将此键的值重置回零,全部作为相同作的一部分:ConnectionMultiplexer redisHostConnection = ...; IDatabase cache = redisHostConnection.GetDatabase(); ... string oldValue = await cache.StringGetSetAsync("data:counter", 0);
MGET
,MSET
它可以以单个作的形式返回或更改一组字符串值。IDatabase.StringGetAsync
重IDatabase.StringSetAsync
载和方法以支持此功能,如以下示例所示:ConnectionMultiplexer redisHostConnection = ...; IDatabase cache = redisHostConnection.GetDatabase(); ... // Create a list of key-value pairs var keysAndValues = new List<KeyValuePair<RedisKey, RedisValue>>() { new KeyValuePair<RedisKey, RedisValue>("data:key1", "value1"), new KeyValuePair<RedisKey, RedisValue>("data:key99", "value2"), new KeyValuePair<RedisKey, RedisValue>("data:key322", "value3") }; // Store the list of key-value pairs in the cache cache.StringSet(keysAndValues.ToArray()); ... // Find all values that match a list of keys RedisKey[] keys = { "data:key1", "data:key99", "data:key322"}; // values should contain { "value1", "value2", "value3" } RedisValue[] values = cache.StringGet(keys);
还可以将多个作合并到单个 Redis 事务中,如本文前面的 Redis 事务和批处理部分所述。 StackExchange 库通过 ITransaction
接口提供对事务的支持。
使用ITransaction
该方法创建对象IDatabase.CreateTransaction
。 使用对象提供的方法调用事务的 ITransaction
命令。
该 ITransaction
接口提供对一组方法的访问权限,这些方法与接口访问 IDatabase
的方法类似,但所有方法都是异步方法。 这意味着,它们仅在调用方法时 ITransaction.Execute
执行。 方法返回 ITransaction.Execute
的值指示事务是成功(true)还是失败(false)。
以下代码片段演示了一个示例,该示例将两个计数器递增和递减为同一事务的一部分:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
ITransaction transaction = cache.CreateTransaction();
var tx1 = transaction.StringIncrementAsync("data:counter1");
var tx2 = transaction.StringDecrementAsync("data:counter2");
bool result = transaction.Execute();
Console.WriteLine("Transaction {0}", result ? "succeeded" : "failed");
Console.WriteLine("Result of increment: {0}", tx1.Result);
Console.WriteLine("Result of decrement: {0}", tx2.Result);
请记住,Redis 事务与关系数据库中的事务不同。 该方法 Execute
将构成要运行的事务的所有命令排入队列,如果其中任一命令格式不正确,则停止事务。 如果所有命令都已成功排队,则每个命令都以异步方式运行。
如果任何命令失败,其他命令仍继续处理。 如果需要验证命令是否已成功完成,则必须使用相应任务的 Result 属性提取命令的结果,如上面的示例所示。 读取 Result 属性将阻止调用线程,直到任务完成。
有关详细信息,请参阅 Redis 中的事务。
执行批处理作时,可以使用 IBatch
StackExchange 库的接口。 此接口提供对一组类似于接口访问 IDatabase
的方法的访问,但所有方法都是异步的。
使用IBatch
该方法创建对象IDatabase.CreateBatch
,然后使用该方法运行批处理IBatch.Execute
,如以下示例所示。 此代码只是设置字符串值,递增和递减上一个示例中使用的相同计数器,并显示结果:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
IBatch batch = cache.CreateBatch();
batch.StringSetAsync("data:key1", 11);
var t1 = batch.StringIncrementAsync("data:counter1");
var t2 = batch.StringDecrementAsync("data:counter2");
batch.Execute();
Console.WriteLine("{0}", t1.Result);
Console.WriteLine("{0}", t2.Result);
请务必了解,与事务不同,如果批处理中的命令由于格式不正确而失败,其他命令仍可能运行。 该方法 IBatch.Execute
不返回任何成功或失败的迹象。
Redis 支持使用命令标志触发和忘记作。 在这种情况下,客户端只需启动作,但对结果没有兴趣,并且不会等待命令完成。 下面的示例演示如何将 INCR 命令作为触发和忘记作执行:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
await cache.StringSetAsync("data:key1", 99);
...
cache.StringIncrement("data:key1", flags: CommandFlags.FireAndForget);
将项存储在 Redis 缓存中时,可以指定超时时间,之后该项将自动从缓存中删除。 还可以使用 TTL
命令查询密钥在过期之前的时间多多。 此命令可用于 StackExchange 应用程序,方法是使用 IDatabase.KeyTimeToLive
该方法。
以下代码片段演示如何在密钥上设置 20 秒的过期时间,并查询密钥的剩余生存期:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
// Add a key with an expiration time of 20 seconds
await cache.StringSetAsync("data:key1", 99, TimeSpan.FromSeconds(20));
...
// Query how much time a key has left to live
// If the key has already expired, the KeyTimeToLive function returns a null
TimeSpan? expiry = cache.KeyTimeToLive("data:key1");
还可以使用 STACKExchange 库中提供的 EXPIRE 命令将过期时间设置为特定日期和时间,该方法如下 KeyExpireAsync
:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
// Add a key with an expiration date of midnight on 1st January 2015
await cache.StringSetAsync("data:key1", 99);
await cache.KeyExpireAsync("data:key1",
new DateTime(2015, 1, 1, 0, 0, 0, DateTimeKind.Utc));
...
提示
可以使用 DEL 命令从缓存中手动删除项,该命令可通过 StackExchange 库作为 IDatabase.KeyDeleteAsync
方法使用。
Redis 集是共享单个键的多个项的集合。 可以使用 SADD 命令创建集。 可以使用 SMEMBERS 命令检索集中的项。 StackExchange 库使用 IDatabase.SetAddAsync
该方法实现 SADD 命令,并使用该方法实现 SMEMBERS 命令 IDatabase.SetMembersAsync
。
还可以组合现有集以使用 SDIFF(集差)、SINTER(设置交集)和 SUNION(集联合)命令创建新集。 StackExchange 库在方法中 IDatabase.SetCombineAsync
统一这些作。 此方法的第一个参数指定要执行的设置作。
以下代码片段演示如何对快速存储和检索相关项的集合使用集。 此代码使用 BlogPost
本文前面“实现 Redis 缓存客户端应用程序”部分中介绍的类型。
对象 BlogPost
包含四个字段:ID、标题、排名分数和标记集合。 下面的第一个代码片段显示了用于填充对象 C# 列表 BlogPost
的示例数据:
List<string[]> tags = new List<string[]>
{
new[] { "iot","csharp" },
new[] { "iot","azure","csharp" },
new[] { "csharp","git","big data" },
new[] { "iot","git","database" },
new[] { "database","git" },
new[] { "csharp","database" },
new[] { "iot" },
new[] { "iot","database","git" },
new[] { "azure","database","big data","git","csharp" },
new[] { "azure" }
};
List<BlogPost> posts = new List<BlogPost>();
int blogKey = 0;
int numberOfPosts = 20;
Random random = new Random();
for (int i = 0; i < numberOfPosts; i++)
{
blogKey++;
posts.Add(new BlogPost(
blogKey, // Blog post ID
string.Format(CultureInfo.InvariantCulture, "Blog Post #{0}",
blogKey), // Blog post title
random.Next(100, 10000), // Ranking score
tags[i % tags.Count])); // Tags--assigned from a collection
// in the tags list
}
可以将每个 BlogPost
对象的标记存储为 Redis 缓存中的集,并将每个集与 ID BlogPost
相关联。 这使应用程序能够快速查找属于特定博客文章的所有标记。 若要在相反的方向启用搜索并查找共享特定标记的所有博客文章,可以创建另一组用于保存引用密钥中标记 ID 的博客文章:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
// Tags are easily represented as Redis Sets
foreach (BlogPost post in posts)
{
string redisKey = string.Format(CultureInfo.InvariantCulture,
"blog:posts:{0}:tags", post.Id);
// Add tags to the blog post in Redis
await cache.SetAddAsync(
redisKey, post.Tags.Select(s => (RedisValue)s).ToArray());
// Now do the inverse so we can figure out which blog posts have a given tag
foreach (var tag in post.Tags)
{
await cache.SetAddAsync(string.Format(CultureInfo.InvariantCulture,
"tag:{0}:blog:posts", tag), post.Id);
}
}
通过这些结构,可以非常高效地执行许多常见查询。 例如,你可以查找并显示博客文章 1 的所有标记,如下所示:
// Show the tags for blog post #1
foreach (var value in await cache.SetMembersAsync("blog:posts:1:tags"))
{
Console.WriteLine(value);
}
可以通过执行集交集作来查找博客文章 1 和博客文章 2 通用的所有标记,如下所示:
// Show the tags in common for blog posts #1 and #2
foreach (var value in await cache.SetCombineAsync(SetOperation.Intersect, new RedisKey[]
{ "blog:posts:1:tags", "blog:posts:2:tags" }))
{
Console.WriteLine(value);
}
你可以找到包含特定标记的所有博客文章:
// Show the ids of the blog posts that have the tag "iot".
foreach (var value in await cache.SetMembersAsync("tag:iot:blog:posts"))
{
Console.WriteLine(value);
}
许多应用程序所需的常见任务是查找最近访问的项。 例如,博客网站可能想要显示有关最近阅读的博客文章的信息。
可以使用 Redis 列表实现此功能。 Redis 列表包含多个共享同一键的项。 该列表充当双端队列。 可以使用 LPUSH(左推送)和 RPUSH(右推送)命令将项推送到列表的任一端。 可以使用 LPOP 和 RPOP 命令从列表的任一端检索项。 还可以使用 LRANGE 和 RRANGE 命令返回一组元素。
下面的代码片段演示如何使用 StackExchange 库执行这些作。 此代码使用 BlogPost
前面示例中的类型。 当用户阅读博客文章时,该方法会将 IDatabase.ListLeftPushAsync
博客文章的标题推送到与 Redis 缓存中密钥“blog:recent_posts”关联的列表。
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
string redisKey = "blog:recent_posts";
BlogPost blogPost = ...; // Reference to the blog post that has just been read
await cache.ListLeftPushAsync(
redisKey, blogPost.Title); // Push the blog post onto the list
随着更多博客文章的阅读,他们的标题将推送到同一个列表中。 列表按添加标题的顺序排序。 最近阅读的博客文章位于列表的左端。 (如果多次阅读同一篇博客文章,它将在列表中有多个条目。
可以使用该方法显示最近阅读的帖子 IDatabase.ListRange
的标题。 此方法采用包含列表、起点和终点的键。 以下代码检索列表最左侧的 10 篇博客文章(项目从 0 到 9)的标题:
// Show latest ten posts
foreach (string postTitle in await cache.ListRangeAsync(redisKey, 0, 9))
{
Console.WriteLine(postTitle);
}
请注意,该方法 ListRangeAsync
不会从列表中删除项。 为此,可以使用 IDatabase.ListLeftPopAsync
和 IDatabase.ListRightPopAsync
方法。
若要防止列表无限期增长,可以通过剪裁列表来定期剔除项目。 下面的代码片段演示如何从列表中删除除五个最左侧项外的所有项:
await cache.ListTrimAsync(redisKey, 0, 5);
默认情况下,集中的项不会按任何特定顺序保留。 可以使用 ZADD 命令 IDatabase.SortedSetAdd
(StackExchange 库中的方法)创建有序集。 这些项是使用名为分数的数值进行排序的,该数值作为命令的参数提供。
以下代码片段将博客文章的标题添加到有序列表。 在此示例中,每个博客文章还有一个包含博客文章排名的分数字段。
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
string redisKey = "blog:post_rankings";
BlogPost blogPost = ...; // Reference to a blog post that has just been rated
await cache.SortedSetAddAsync(redisKey, blogPost.Title, blogPost.Score);
可以使用此方法按升序检索博客文章标题和分数 IDatabase.SortedSetRangeByRankWithScores
:
foreach (var post in await cache.SortedSetRangeByRankWithScoresAsync(redisKey))
{
Console.WriteLine(post);
}
备注
StackExchange 库还提供 IDatabase.SortedSetRangeByRankAsync
方法,该方法按评分顺序返回数据,但不返回分数。
还可以按分数降序检索项,并通过向方法提供其他参数 IDatabase.SortedSetRangeByRankWithScoresAsync
来限制返回的项数。 下一个示例显示排名前 10 位的博客文章的标题和分数:
foreach (var post in await cache.SortedSetRangeByRankWithScoresAsync(
redisKey, 0, 9, Order.Descending))
{
Console.WriteLine(post);
}
下一个示例使用 IDatabase.SortedSetRangeByScoreWithScoresAsync
该方法,可用于限制返回到给定分数范围内的项:
// Blog posts with scores between 5000 and 100000
foreach (var post in await cache.SortedSetRangeByScoreWithScoresAsync(
redisKey, 5000, 100000))
{
Console.WriteLine(post);
}
除了充当数据缓存外,Redis 服务器还通过高性能发布者/订阅服务器机制提供消息传送。 客户端应用程序可以订阅通道,其他应用程序或服务可以将消息发布到通道。 然后,订阅应用程序将收到这些消息,并可以处理它们。
Redis 为客户端应用程序提供 SUBSCRIBE 命令,用于订阅通道。 此命令需要应用程序将接受消息的一个或多个通道的名称。 StackExchange 库包括接口 ISubscription
,使 .NET Framework 应用程序能够订阅和发布到通道。
使用ISubscription
与 Redis 服务器的连接方法创建对象GetSubscriber
。 然后使用此对象的方法侦听通道 SubscribeAsync
上的消息。 下面的代码示例演示如何订阅名为“messages:blogPosts”的频道:
ConnectionMultiplexer redisHostConnection = ...;
ISubscriber subscriber = redisHostConnection.GetSubscriber();
...
await subscriber.SubscribeAsync("messages:blogPosts", (channel, message) => Console.WriteLine("Title is: {0}", message));
方法的第一个参数 Subscribe
是通道的名称。 此名称遵循缓存中的键使用的相同约定。 该名称可以包含任何二进制数据,但我们建议使用相对较短、有意义的字符串来帮助确保良好的性能和可维护性。
另请注意,通道使用的命名空间与键使用的命名空间分开。 这意味着可以有具有相同名称的通道和密钥,尽管这可能会使应用程序代码更易于维护。
第二个参数是作委托。 每当通道上显示新消息时,此委托将异步运行。 此示例只是在主机上显示消息(该消息将包含博客文章的标题)。
若要发布到通道,应用程序可以使用 Redis PUBLISH 命令。 StackExchange 库提供 IServer.PublishAsync
执行此作的方法。 下一个代码片段演示如何将消息发布到“messages:blogPosts”频道:
ConnectionMultiplexer redisHostConnection = ...;
ISubscriber subscriber = redisHostConnection.GetSubscriber();
...
BlogPost blogPost = ...;
subscriber.PublishAsync("messages:blogPosts", blogPost.Title);
应了解发布/订阅机制的几个要点:
- 多个订阅者可以订阅同一频道,它们都会收到发布到该通道的消息。
- 订阅者仅接收订阅后发布的消息。 通道不会缓冲,发布消息后,Redis 基础结构会将消息推送到每个订阅服务器,然后将其删除。
- 默认情况下,订阅者按照消息的发送顺序接收消息。 在具有大量消息和许多订阅者和发布者的高度活动系统中,保证消息的顺序传递可能会降低系统的性能。 如果每个消息都是独立的,并且顺序不重要,则可以通过 Redis 系统启用并发处理,这有助于提高响应能力。 可以通过将订阅服务器所使用的连接的 PreserveAsyncOrder 设置为 false,在 StackExchange 客户端中实现此目的:
ConnectionMultiplexer redisHostConnection = ...;
redisHostConnection.PreserveAsyncOrder = false;
ISubscriber subscriber = redisHostConnection.GetSubscriber();
选择序列化格式时,请考虑性能、互作性、版本控制、与现有系统的兼容性、数据压缩和内存开销之间的权衡。 评估性能时,请记住基准高度依赖于上下文。 它们可能不会反映实际工作负荷,并且可能不考虑较新的库或版本。 所有方案都没有单个“最快”序列化程序。
一些可供考虑的选项包括:
协议缓冲区 (也称为 protobuf)是由 Google 开发的序列化格式,用于高效序列化结构化数据。 它使用强类型定义文件来定义消息结构。 然后将这些定义文件编译为特定于语言的代码,以便序列化和反序列化消息。 Protobuf 可用于现有 RPC 机制,也可以生成 RPC 服务。
Apache Thrift 使用类似的方法,其中包含强类型定义文件和编译步骤来生成序列化代码和 RPC 服务。
Apache Avro 提供与协议缓冲区和 Thrift 类似的功能,但没有编译步骤。 相反,序列化的数据始终包含描述结构的架构。
JSON 是一种开放标准,它使用人工可读文本字段。 它具有广泛的跨平台支持。 JSON 不使用消息架构。 作为基于文本的格式,它不是非常高效的通过网络。 但是,在某些情况下,可以通过 HTTP 将缓存项直接返回到客户端,在这种情况下,存储 JSON 可以节省从另一种格式反序列化,然后序列化为 JSON 的成本。
二进制 JSON (BSON) 是使用类似于 JSON 的结构的二进制序列化格式。 BSON 设计为轻量级、易于扫描、快速序列化和反序列化,相对于 JSON。 有效负载的大小与 JSON 相当。 根据数据,BSON 有效负载可能小于或大于 JSON 有效负载。 BSON 具有一些其他数据类型,这些数据类型在 JSON 中不可用,尤其是 BinData(用于字节数组)和日期。
MessagePack 是旨在用于进行压缩以进行网络传输的二进制序列化格式。 没有消息架构或消息类型检查。
Bond 是用于处理架构化数据的跨平台框架。 它支持跨语言序列化和反序列化。 此处列出的其他系统的显著区别在于支持继承、类型别名和泛型。
gRPC 是由 Google 开发的开源 RPC 系统。 默认情况下,它使用协议缓冲区作为其定义语言和基础消息交换格式。
在应用程序中实现缓存时,以下模式也可能与方案相关: