数据库缓存策略
大多数(即使不是全部)开发人员都对缓存的概念有所了解。毕竟,这项技术如今无处不在,从 CPU 到浏览器缓存,所有软件都在一定程度上依赖缓存技术来提供快速响应。仅仅几毫秒的延迟就可能造成数百万美元的收入损失,因此亚毫秒级的响应速度正在成为常态。市面上有大量的缓存解决方案,因此选择合适的方案本身就是一场冒险。
在本文中,我们将讨论什么是缓存以及缓存的优势。接下来,我们将讨论不同的缓存策略和缓存驱逐策略。最后,我们将回顾一些现有的缓存解决方案。
缓存基础知识
什么是缓存?
在软件开发中,缓存是一种组件,用于存储数据集中计算时间过长或来自其他底层系统的部分数据。通过减少不必要的计算并避免频繁使用数据的额外请求往返,缓存可以提高应用程序性能或降低响应延迟。
缓存旨在近乎实时地响应缓存请求,因此被实现为简单的键值存储。然而,其内部工作原理仍然可能非常不同,并且依赖于后端存储。
典型的用例是数据库的内存缓存或可缓慢检索的基于磁盘的数据、远程存储在慢速网络连接后面的数据或先前计算的结果。
什么是缓存命中和缓存未命中?
当请求的数据已经在缓存中并且无需任何其他操作或处理即可返回时,就会发生缓存命中。
当请求的数据尚不可用并且必须从底层系统检索数据或计算数据才能返回时,就会发生缓存未命中。
应用程序挑战和缓存优势
现代系统必须能够处理海量流量,并需要极快的响应速度。此外,随着流量和数据量的增加,应用程序需要具备扩展能力才能成功。另一方面,大多数基础设施都直接或间接地依赖于基于磁盘的数据库。
对于需要低延迟和可扩展性的分布式应用程序来说,基于磁盘的数据库可能会带来诸多挑战。一些最常见的挑战包括:
- 查询处理速度慢:磁盘数据检索速度加上额外的查询处理时间,通常会导致响应时间相对较长。虽然有很多优化技术和设计可以提升查询性能,但实际性能提升幅度有限,最终会达到介质的物理极限。毕竟,数据库查询的延迟很大程度上是由磁盘数据检索的物理机制决定的。
- 可扩展性成本:数据库可以水平或垂直扩展。这两种扩展技术都有各自的缺点。垂直扩展成本高昂,而且可能会达到可添加到机器的组件的物理极限。水平扩展允许对数据库进行分片,以实现更高的吞吐量。然而,为了实现更高的读取速度而进行扩展可能成本高昂,并且可能需要大量的副本才能实现。此外,在尝试实现更快的响应时间时,我们必须非常小心,以免出现数据不平衡的情况。
- 可用性:有时,与数据库服务器或数据库分片的连接可能会中断。如果没有缓存,系统将无响应,直到连接恢复。
缓存的总体优势在于能够同时帮助内容消费者和内容提供者。良好的缓存策略可以带来诸多优势。
- 提升响应速度:缓存可以更快地检索内容,并避免额外的网络往返。靠近用户维护的缓存(例如浏览器缓存)可以使检索几乎即时完成。
- 在相同硬件上提升性能:对于内容来源服务器,可以从相同硬件中获取主动缓存。内容所有者可以利用交付路径上更强大的服务器来承担大部分内容加载任务。
- 降低网络成本:根据缓存策略,内容可以在网络路径的多个区域可用。这样,内容可以更接近用户,并减少缓存之外的网络活动。
- 内容交付的稳健性:通过某些策略,即使由于网络短缺或服务器故障而导致内容不可用,也可以使用缓存向最终用户提供内容。
- 消除数据库热点:在许多应用程序中,一小部分数据可能会比其他数据更频繁地被访问。这可能会导致数据库中出现热点。这些热点可能需要根据最常用数据的吞吐量需求来过度配置数据库资源。将常用键存储在内存缓存中可以减少过度配置的需求,同时提供快速且可预测的性能。
缓存什么
判断需要缓存哪些内容的一个好方法是找到任何多次执行相同请求都会产生相同结果的元素。这包括数据库查询、HTML 片段或大量计算的输出。
一般来说,只有一条规则适用。数据不应该更改太频繁,但应该被非常频繁地读取。
哪些内容不宜缓存
一个常见的误解,尤其是在技术相关岗位中,就是认为只要缓存所有内容,就能自动受益。这种想法乍一看似乎不错,但在数据高峰期却会带来另一个问题。
易失性数据通常不太适合缓存。每当数据发生变化时,缓存就必须失效,而且根据我们使用的缓存策略,这可能是一项代价高昂的操作。
另一种无法从缓存中获益的数据类型是那些检索速度很快的数据。缓存这些元素会在填充缓存时引入额外的往返操作,并且不可避免地会增加所需的内存。缓存这些元素的好处甚至可能无法达到预期的效果,因此不值得付出如此大的代价。
缓存类型
内存缓存
内存缓存是一块用于临时存储数据的 RAM。由于访问 RAM 的速度明显快于访问硬盘或网络等其他介质,因此缓存可以通过更快的数据访问速度帮助应用程序更快地运行。
内存缓存的工作原理是首先留出一部分 RAM 作为缓存。当应用程序尝试读取数据时(通常是从数据库等数据存储系统读取),它会检查所需记录是否已存在于缓存中。如果存在,则应用程序将从缓存中读取数据,从而避免数据库访问速度变慢。如果所需记录不在缓存中,则应用程序将从源读取记录。检索该数据时,它还会将数据写入缓存,以便应用程序将来需要相同数据时,可以快速从缓存中获取。
内存缓存的一个广泛用例是加速数据库应用程序,尤其是那些执行大量数据库读取操作的应用程序。通过将部分数据库读取操作替换为缓存读取操作,应用程序可以消除频繁数据库往返操作带来的延迟。这种用例通常出现在数据访问量很大的环境中,例如,在高流量且包含来自数据库的动态内容的网站中。
另一个用例涉及查询加速,即将数据库的复杂查询结果存储在缓存中。执行分组和排序等操作的复杂查询可能需要大量时间才能完成。如果查询重复运行,例如在众多用户访问的商业智能 (BI) 仪表板中,将结果存储在缓存中可以提高这些仪表板的响应速度。
分布式缓存
分布式缓存是一种将多台联网计算机的 RAM 集中到单个内存数据存储中的系统,该数据存储用作缓存以提供对数据的快速访问。
虽然大多数缓存传统上存在于一个物理组件中,无论是服务器还是硬件组件,但分布式缓存可以通过连接多台计算机来获得更大的容量和处理能力,从而超越单台机器的物理限制。
分布式缓存将多台计算机的 RAM 集中到单个内存数据存储中,用作数据缓存,以提供快速的数据访问。
分布式缓存在数据量大、负载高的环境中尤其有用。分布式架构允许通过向集群添加更多硬件来实现增量扩展,从而使缓存容量能够与数据同步增长。
有几种用例可以将分布式缓存作为应用程序体系结构的一部分:
- 应用程序加速:大多数应用程序都直接或间接地依赖于基于磁盘的数据库,而这往往无法满足当今日益增长的需求。通过将最常访问的数据缓存在分布式缓存中,我们可以显著减少基于磁盘的系统的瓶颈。
- 存储会话数据:网站可能会将用户会话数据存储在缓存中,作为各种操作(例如购物车和推荐)的输入。借助分布式缓存,我们可以拥有大量并发的 Web 会话,这些会话可以被系统中的任何服务器访问。这使我们能够将 Web 流量负载均衡到多台服务器,即使任何应用服务器发生故障,也不会丢失会话。
- 极致扩展:某些应用程序会请求大量数据。分布式缓存可以通过跨多台机器调配更多资源来响应这些请求。
缓存数据访问策略
缓存数据时,我们可以从多种缓存策略中进行选择,包括主动和被动两种方式。我们选择实施的模式应该与我们的缓存和应用程序目标直接相关。
缓存预留(延迟加载)
Cache Aside 可能是最常用的缓存方法。此策略规定缓存必须位于缓存侧,应用程序将直接与缓存和数据库通信。
在这种策略中,当应用程序需要某些数据时,它会首先查询缓存。如果缓存中包含该元素,则会发生缓存命中,缓存会将数据返回给应用程序。如果缓存中不存在该数据,则会发生缓存未命中。应用程序现在必须做一些额外的工作。应用程序首先必须查询数据库以获取所需数据。然后,它将数据返回给客户端,最后使用检索到的数据更新缓存。现在,对同一数据的任何后续读取都将导致缓存命中。
缓存旁路缓存通常是通用的,最适合读取繁重的工作负载。
使用“Cache Side”的系统能够有效应对缓存故障。如果存在多个缓存节点,并且某个节点发生故障,虽然不会导致连接中断,但应用程序可能会面临延迟增加的问题。随着新的缓存节点上线,越来越多的请求被重定向到这些节点,每次缓存未命中时,该节点都会被填充所需的数据。即使发生缓存故障,应用程序仍然可以通过数据库请求访问数据。
此策略的缺点是,缓存未命中后需要进行三次网络往返。首先,应用程序需要检查缓存。接下来,应用程序需要从数据库检索数据。最后,应用程序需要更新缓存。这些往返可能会导致响应明显延迟。
通读
与“Cache Aside”相比,“Read Through”将从数据存储获取值的责任转移到缓存提供程序。此策略规定缓存必须位于应用程序和数据库之间。
在这种策略中,当应用程序需要某些数据时,它会查询缓存。如果缓存包含该元素,则称为缓存命中,缓存会将数据返回给应用程序。如果数据不在缓存中,则称为缓存未命中。缓存首先必须查询数据库以获取所需数据。之后,缓存将使用所需数据进行自我更新。最后,缓存将检索到的数据返回给应用程序。现在,对同一数据的任何后续读取都将导致缓存命中。
当我们拥有需要保存在缓存中以便频繁读取的数据(即使这些数据会定期更改)时,读取缓存非常适合与写入策略结合使用。
此策略不适用于应用程序中的所有数据访问。如果系统将缓存用于数据库和应用程序之间的所有数据访问,则缓存故障可能会导致应用程序性能瓶颈,甚至由于应用程序无法访问数据库而直接导致应用程序崩溃。
直写
与“直读”类似,但针对的是写入操作,“直写”将写入任务转移给了缓存提供程序。此策略规定缓存必须位于应用程序和数据库之间。此策略不提供从主数据源读取数据的任何功能,但处理应用程序发出新数据或更新时发生的情况。
在此策略中,当应用程序尝试更新现有数据或向数据存储区添加新数据时,它将命中缓存。此操作始终会导致缓存命中,并且缓存将更新其条目或为数据创建新条目。然后,缓存将更新主数据存储。最后,缓存将确认数据已成功存储。
直写策略本身似乎作用不大,事实上,由于数据先写入缓存,然后再写入数据库,因此会引入额外的写入延迟。但当此策略与直读策略结合使用时,可以确保数据的一致性。
与“直读”策略类似,此策略不适用于应用程序中的所有数据访问。如果系统将缓存用于数据库和应用程序之间的所有数据访问,则缓存故障可能会导致应用程序性能瓶颈,甚至由于应用程序无法访问数据库而直接导致应用程序崩溃。
四处写写
绕写式缓存 (Write Around cache) 的功能与直写式缓存类似。在绕写式缓存中,只有当缓存数据已经映射到缓存中时,才会更新缓存数据,同时将数据“直写”到后端存储。
在此策略中,当应用程序尝试添加或更新某些数据时,它将查询缓存。缓存将使用更新后的数据更新后端存储。然后,如果缓存中包含更新数据的条目,它将自行更新,否则,它将完全跳过该数据。
此策略可防止缓存被不常读取的数据淹没,同时最大程度地减少写入操作的延迟。它还能确保缓存与主数据存储之间的数据一致性。因此,它非常适合一次性数据写入量非常大的系统,例如来自聊天应用程序的消息。
这种策略的缺点是,最近写入的数据总是会导致缓存未命中(从而导致更高的延迟),因为数据只能在较慢的后端存储中找到。
回写
回写式策略与直写式策略非常相似。主要区别在于,缓存不会同步更新主数据存储,而是按照预先定义的时间间隔分批更新。
在此策略中,当应用程序添加或更新数据时,它会与案例进行通信。案例将向应用程序发送确认。在定义的时间间隔后,缓存将对数据存储执行批量查询并更新所有相关数据。
写回缓存可提高写入性能,非常适合读取密集型和写入密集型工作负载。
由于应用程序仅向缓存服务写入数据,因此无需等到数据写入底层数据源,从而提升了性能。此外,由于所有读写操作均在缓存上执行,因此应用程序可以免受数据库故障的影响。即使数据库发生故障,仍然可以访问队列中的项目。
此策略也引入了一些需要解决的问题。考虑到此策略首先读写缓存,因此缓存和主数据存储之间只能实现最终一致性。如果主数据存储与其他应用程序共享,则如果其他应用程序的读取操作发生在批处理操作之间,则始终存在获取过时数据的风险。此外,无法知道缓存更新是否会与其他外部更新冲突。这必须手动或启发式处理。
驱逐政策
驱逐策略允许缓存确保其大小不超过最大限制。为了实现这一点,现有元素会根据驱逐策略从缓存中移除,但可以根据应用程序需求进行自定义。
缓存解决方案可能与不同的驱逐策略兼容,但在选择缓存策略之前,最好先了解应用程序可能需要哪些驱逐策略。
最近最少使用(LRU)
最常用的策略之一是“最近最少使用”策略。“最近最少使用”策略会移除最近使用时间最久的值。为了进行分析,缓存中的每条记录都会跟踪其上次访问的时间戳,以便与其他记录进行比较,从而找到最近最少使用的项目。
最不常用(LFU)
最不常用的驱逐策略会移除访问次数最少的值。为了进行分析,每条记录都会使用一个只增不减的计数器来跟踪其访问情况。然后,可以将该计数器与其他记录的计数器进行比较,以找到最不常用的元素。
最近使用(MRU)
“最近使用”驱逐策略会移除最近使用的值(按时间排序)。为了进行分析,每条记录都会跟踪其上次访问的时间戳,以便与其他记录进行比较,从而找到最近使用的元素。
最常用(MFU)
最常使用的驱逐策略会移除访问次数最多的值。为了进行分析,每条记录都会使用一个只增不减的计数器来跟踪其访问情况。然后,可以将该计数器与这些记录进行比较,以找到最常使用的元素。
最短生存时间(LTTL)
最短生存时间驱逐策略会移除 TTL 字段中生存时间最短的值。为了进行此分析,每条记录都会跟踪其 TTL(在添加到缓存时分配),并按特定时间间隔递减 TTL。然后,可以将该字段与其他记录的 TTL 字段进行比较,以找到在缓存中生存时间最长的项目。
随机的
随机驱逐策略会随机移除值。此策略不考虑缓存中项目的插入顺序,也不考虑项目被访问的频率。当存在循环访问且所有元素都被连续扫描,或者我们期望分布均匀时,可以使用此策略。
现实生活中的缓存解决方案
Memcached
Memcached是一个通用的分布式内存缓存系统。它通常用于动态数据库驱动的网站,以减少读取外部数据源的次数。Memcached 是免费的开源系统,遵循修订版 BSD 许可证,因此是一个非常低成本的解决方案。
该系统设计为一个简单的键值存储。Memcached 无法理解应用程序正在保存什么——它可以将字符串和对象存储为值,但键必须始终存储为字符串。
在分布式环境中,Memcached 节点之间互不交互,因为系统不提供任何同步或复制功能。因此,客户端必须自行决定哪个节点访问特定的数据集。
著名的 Memcached 用户包括 YouTube、Reddit、Twitter 和 Wikipedia。
限制
Memcached不为缓存条目提供任何持久性,因此每次崩溃或重启时,缓存都需要再次预热。
另一个限制是值的大小限制。每个键的值必须最大为 1 MB。这意味着大型对象或数据集可能占用更多空间,并且必须将其分片到不同的缓存槽中。此外,对象在存储到缓存之前必须进行序列化,这会增加读写操作的延迟。
Redis
Redis(远程字典服务器)是一种内存数据结构存储,用作分布式内存键值数据库、缓存和消息代理。
Redis 原生支持多种数据结构,包括列表、集合、有序集合和字符串。它还支持范围查询、超日志和地理空间索引。
Redis 通常将整个数据集保存在内存中,但可以配置为通过两种不同的方法持久化其数据。第一种方法是通过快照,其中数据集以二进制转储的形式定期从内存异步传输到磁盘。第二种方法是日志记录,其中每个修改数据集的操作的记录都会在后台进程中添加到仅追加文件中。
默认情况下,Redis 至少每 2 秒将数据写入文件系统一次,并可根据需要提供更多或更少的健壮选项。在默认设置下,即使系统完全崩溃,也只会丢失几秒钟的数据。
Redis 还支持主从复制。任何 Redis 服务器的数据都可以复制到任意数量的从服务器。Redis 还提供发布-订阅功能,因此从服务器的客户端可以订阅某个频道并接收发布到主服务器的完整消息源。
限制
在分布式环境中,Redis 根据分配给每个主服务器的哈希槽对数据进行分片。如果任何主服务器发生故障,则写入该槽的数据将丢失。此外,除非主服务器至少有一个从服务器,否则不支持故障转移。
由于 Redis 将数据存储在内存中的大型哈希表中,因此需要大量的 RAM。
气动尖峰
Aerospike是一个基于闪存和内存的开源分布式键值 NoSQL 数据库管理系统。Aerospike 最重要的卖点之一是支持混合内存模型——这意味着如果 RAM 已满,可以使用其他合适的闪存驱动器(例如 SSD、NVMe 等)作为替代方案。
Aerospike 使用闪存驱动器进行垂直扩展。通常,IOPS(每秒输入输出)会持续增长。SSD 在每个节点上存储的数据量比 DRAM 多几个数量级,而 NVMe 驱动器现在每个驱动器的 IOPS 高达 10 万次。Aerospike 利用这些功能,可以每秒执行数百万次操作,并且始终保持亚毫秒级的延迟。
榛树
Hazelcast是一个基于 Java 的开源内存数据网格。在 Hazelcast 网格中,数据均匀分布在集群的各个节点上,从而支持处理能力和可用存储空间的水平扩展。备份也分布在各个节点上,以防止单个节点发生故障。
Hazelcast 可以在本地、云端和 Docker 容器中运行。Hazelcast 提供多种云配置和部署技术的集成,包括 Apache jclouds、Consul、Eureka、Kubernetes 和 Zookeeper。Hazelcast 还可以使云端或本地节点相互自动发现。
最后的想法
在本文中,我们讨论了什么是缓存以及缓存的优势。我们还研究了什么是内存缓存和分布式缓存。我们回顾了最常见的缓存策略,并讨论了最流行的驱逐策略。最后,我们回顾了一些可用的缓存提供商。
现在应该更清楚一点,选择缓存服务不仅要考虑大牌或熟悉度,还要考虑应用程序的用例、数据访问模式、基础设施要求、可用性和数据量等。
工程师在选择某项技术之前需要考虑多少因素——人们选择自己熟悉的技术是正常行为,但了解所有这些参数肯定有助于我们做出更好的设计决策。
文章来源:https://dev.to/kalkwst/database-caching-strategies-16in