这篇文章将介绍缓存的概念。在一个系统中,通常会有一个数据库来存储大量的用户需要访问的数据。然而,当我们执行复杂查询时,或者当我们知道某些数据需要频繁访问时,数据库可能会变得较慢。解决这些问题的一个好方法是引入缓存,缓存可以存储最常用的数据,并能快速执行相应的操作。
如果用户能访问缓存且数据已经存储在其中,响应会非常快。如果缓存中没有数据,系统则需要访问数据库,数据库是基于磁盘存储的,因此查询会稍微慢一些。
需要注意的是,数据库通常有自带的缓存机制,许多数据库会尝试识别最常用的数据,并将这些数据存储在内存中,而其他数据则存储在磁盘上。然而,有时数据库在这方面的表现并不好。举个例子,假设我们有一个特定的查询,我们希望能够缓存这个查询的结果。执行这个查询可能涉及分析大量数据,如果我们只是执行一次查询并缓存结果,就能大大提高性能。另一个可能需要引入缓存的情况是,如果我们知道某些数据在初期会被频繁访问,而之后很少被访问。例如,如果我们知道某些数据在被导入数据库后的第一天会频繁使用,之后几乎不再使用,那么一个有帮助的优化策略是将数据添加到缓存中,并在一天后将其移除。这样,我们的主数据库可以存储大部分数据,而缓存只存储最新一天的数据。最后,如果我们有大量的数据正在导入到数据库中,而我们对数据被完全写入数据库的延迟并不敏感,那么引入缓存可以加速写入操作。
当我们使用缓存时,通常会使用内存数据库,它是我们常规数据库的简化版,所有操作的数据都仅存在内存中,从不同步写入磁盘。正因为如此,缓存中的数据通常被视为易失性的,这意味着它随时可能被删除。
首先,让我们看看将数据写入缓存的几种方式。我们首先要看的是“写穿”策略。
这里的箭头上的数字非常重要,我们可以看到用户首先向服务发送请求,服务的第一个操作是将数据写入数据库。当数据库返回数据后,服务将数据写入缓存,并在从缓存获取到数据后向用户确认数据处理成功。由于数据首先写入数据库,只有在数据库和缓存都更新后,系统才会确认请求成功。使用“写穿”方式意味着我们在写入时无法从缓存获得任何性能提升,因为我们需要等待数据先写入数据库,写入操作才能完成。但我们能利用缓存来提高读取性能。
另一种方式是“写回”策略。
在这种方式下,用户首先向服务发送请求,服务将数据写入缓存,然后开始将数据写入数据库。数据库写入过程中,服务会立即向用户确认请求已完成,并且数据库最终会确认数据已完全写入。这意味着,如果我们在请求之后立刻查询数据库,数据库中的数据可能不是最新的,而缓存中则有更新后的数据。因此,如果我们总是先从缓存中读取数据,可以避免这个问题。然而,这种方式的一个问题是,如果在将数据写入数据库时服务或数据库发生故障,数据可能仅存在缓存中,而没有写入数据库。虽然有方法可以解决这个问题,但在使用“写回”策略时,我们需要考虑这个风险。
第三种方式是“绕写”策略。
这意味着数据只写入数据库,而不会写入缓存。
第一次请求数据时,服务首先检查缓存,发现数据不在缓存中,然后会从数据库中读取数据,并将数据写入缓存,之后才能返回数据给用户。这样,数据就会异步写入缓存。
下一次读取时,服务可以直接从缓存中返回数据,而无需再次访问数据库。“绕写”策略的优点是我们每次都将数据写入数据库,减少了写入延迟,但首次读取时需要从数据库加载数据并写入缓存。这对于某些场景很有用,特别是当我们知道数据在写入数据库后不再被频繁读取,或者在稍后才会被访问时。
接下来,我们讨论如何从缓存中删除数据。因为内存存储比磁盘存储昂贵,所以缓存会快速填满,产生高昂的成本。为了避免缓存占用过多内存,我们需要有策略在缓存满时清理数据。
第一种方法是“生存时间”策略,即给每条缓存数据设定一个存活时间,超过这个时间后,数据会被自动删除。这对那些我们知道会在一段时间后不再被频繁访问的数据非常有效。但即使设置了生存时间,如果缓存中积累了太多数据,它还是可能会满。为此,我们可以设置缓存当内存开始满时优先清除剩余时间最短的数据。
第二种策略是“最近最少使用”(LRU)。当缓存达到内存限制时,缓存会自动移除最久未访问的数据。大多数缓存服务会大致估算最久未使用的数据,一些不常使用的数据会被删除。总体而言,这种方法有助于提高缓存命中率,避免每次都需要访问数据库。
最后一种策略是“最不频繁使用”(LFU)。这与“最近最少使用”类似,不同的是它跟踪的是数据的访问频率,而不是访问时间。LFU 会删除那些访问频率较低的数据。尽管实现上会有一些近似,但这种方法对于优化缓存命中率非常有效。
总结一下,缓存的目标不是缓存所有数据,而是优化命中率,确保在有限的内存和资源下,尽可能多的查询能够从缓存中得到结果,而无需访问数据库。
最后,我们要讨论的是实际的数据库。这其实很简单。我们用于缓存的内存数据库与普通数据库基本相同,唯一不同的是它使用了更简单的内部数据模型,就像常规数据库一样。我们可以对内存数据库进行分片,将数据分布到多个节点上,同时也可以复制我们的数据库,以确保在某个节点故障时不会丢失数据。