什么是缓存?
在计算中,高速缓存是一种高速数据存储层,用于存储数据的子集(本质上通常是瞬态的),因此对该数据的未来请求的存储速度比访问数据的主存储位置可能的速度更快。缓存使您可以有效地重用以前检索或计算的数据。
缓存系统的数据实际上存储在诸如RAM之类的快速访问硬件中。RAM提供了更快的I / O操作并减少了延迟。缓存用于技术的每一层,例如:操作系统,CDN,DNS,在许多应用程序(如搜索)中,也用于游戏中以提高媒体内容性能。
在实现缓存层时,重要的是要了解所缓存数据的有效性。成功的缓存会导致较高的命中率,这意味着在提取数据时就存在该数据。当高速缓存中不存在所获取的数据时,将发生高速缓存未命中。可以应用诸如TTL(生存时间)之类的控件来相应地使数据过期。
特征估计:
我们要保存大量数据,大小接近TerraByte。我们的缓存应该能够每秒50K到1M的查询。预期延迟大约为1ms。LRU(逐出)100%可用性可扩展性缓存访问模式:
直写:任何“写”请求到达时,都会通过缓存到达数据库。仅当成功将数据写入高速缓存和DB中时,才认为写入成功。写入:写入请求直接绕过缓存到DB,并确认发送回去,数据不发送到缓存。当第一个高速缓存未命中时,数据将写入高速缓存。回写:写更新进入缓存,数据保存在缓存中,并立即发送确认。然后还有一项服务会将数据下沉到DB。用于实现缓存的数据结构是“哈希表”。我们需要的只是散列函数,键和值。
HashTable的工作:
如上图所示,假设我们有3个数据,分别是“ Apple”,“ Boy”和“ Cat”,这些数据需要存储在哈希表中。这些值作为哈希函数(hashCode())的输入给出,在这种情况下,从中我们分别得到的哈希值分别为“ 53,78和76”。然后将这些值除以存储区的大小(即10),并存储在其剩余值的存储区号中。即桶号53。 3,第8个存储区中的78,依此类推。假设我们还有另一个数据“毛毛虫”,其哈希值为66,也需要将其存储在与cat相同的6号存储区中。当两个不同的键给出相同的存储桶值时,即发生碰撞。出于明显的原因,我们不能将两个值都保存在同一位置。
有几种解决冲突的策略,例如单独的链接,开放式寻址,罗宾汉哈希。一种简单的策略是将价值存储在存储桶号上。3,我们将创建一个链表,其中将存储键值对。这被称为使用链表的单独链接。
要检索该值,将哈希函数的键命名为“ Apple”,它将根据存储桶的大小给出一个哈希值take模块,并查看该特定位置。
在分布式系统中缓存:
如图所示,所有橙色块值都存储在节点1中,蓝色存储在节点2中。如果由于某种原因节点2发生故障,则这两个位置(即9号和10号存储桶)将不可用。哈希表保持不变,但存储桶大小现在为8。对于同一示例“ Apple”,当系统关闭时,我们将百分比乘以8,即53%8,而不是3,我们得到5。案件。除了空值,还可能会出现错误值的情况,这是不可接受的。我们需要进行重新映射,这是一项乏味的工作。如果我们有10k个键而不是10个键,该怎么办?要解决此问题而不是使用常规哈希表,我们需要使用一致性哈希或一致性哈希环。
一致性哈希环的工作:
在传统方案中,我们了解可用的存储位置,因为我们使用连续的数字(从1到10)来连续命名哈希表中的键。在这里,我们完全随机地分配密钥。
对于“苹果”,我们将其传递给哈希函数,结果为2394。我们使用此哈希数,直接去查找位置。找到一个大于2394的密钥,在这种情况下为3030。在这里保存我们的值。假设另一个键“ Ball”的值为2567,它也将以链接的方式存储在同一位置。如果我们得到的散列值为11000,则由于没有可用的值,因此我们回到最前面,将其保存为1000。这就像发生了环跳一样。这就是一致性哈希环的工作方式。
缓存逐出策略:
我们如何退出缓存,何时需要退出缓存?
逐出意味着从缓存中删除键和值条目。为什么我们需要这样做?缓存是昂贵的,并且并非总是使用那里存在的所有值。因此,我们需要确定哪个条目没有被使用并且理想地位于该位置。我们需要删除它们,以便为新条目腾出空间。这称为缓存逐出策略。常见策略之一是“最近最少使用”或LRU。
LRU:驱逐最近最少使用的条目。
我们必须以某种方式找出哪个是最近最少使用的存储桶来保存新条目。我们需要释放内存。我们如何实施LRU?
LRU示例代码:
缓存内部:
到目前为止,我们已经处理了哈希表,RAM和LRU。我们需要有一个服务获取/放入/删除请求的服务。我们正在使用RAM,尽管速度很快,但仍会阻止调用。我们需要一种有效的方式来满足这些要求。有一件事我们可以做的是产卵ñ没有线程的,当我们得到的请求,或者我们可以有更大的线程池来处理线程。我们可以做的另一件事是事件驱动的逻辑。
如上图所示,我们有一个事件队列。任何“获取/放入”请求都将首先进入事件队列。我们有一个无限期运行的事件循环,它是一个单线程操作。之后,我们有了一个线程池,仅负责对RAM的输入/输出操作。
每当我们收到“ get / put ”请求时,它就会被放置在事件队列中。事件循环继续读取事件队列,并将请求释放到事件队列中。一旦移交给线程池,它就会回调并再次读取事件队列。这样,事件队列永远不会被阻塞。接收请求的线程池中的线程将执行操作,获取数据,并通过事件循环或其他某种机制通过回调响应将其返回给请求。这就是我们可以更有效地处理请求的方式。
容错:
如何使我们的缓存系统容错?我们知道所有哈希表和数据都存储在RAM中,如果断电会发生什么情况,我们所有的数据都会折腾。这意味着我们的缓存系统不是持久性的,因此要使其具有持久性,我们必须做一些事情。
定期间隔快照:我们在后台运行同步服务“ S ”。它获取哈希表的冻结副本并将其放置在转储文件中,并将其保存在硬盘上。
2.日志重建:负责哈希表读取和写入的服务将不再具有同步服务,而是继续重建日志文件日志。日志文件中的所有操作都会进行此异步调用,从而继续更新日志文件。我们可以将这些请求放在两者之间的队列中,以不影响读取/写入功能。如果我们记录了每个操作,如果我们的计算机宕机了,那么我们可以使用日志文件操作来重建哈希表。
可用性:
如何使我们的系统100%可用?
考虑在您的集群中,我们有两个节点node1和node2,它们都有自己的缓存,例如我们使用的是一致性哈希,节点1具有一些键和值,节点2处理一些键和值。如果节点1出现故障怎么办?到达节点1的所有请求都将具有缓存未命中。现在发生的是,这些即将到来的请求将命中数据库,从而增加对数据库的读/写。为了避免这种情况,我们可以做的是拥有节点1的副本,假设我们有RF =2。因此,无论数据节点1具有什么,相同的数据节点1'也将具有。
优点:减少了负载,因为去往节点1的请求也将去往节点1',即,现在去往节点1的请求也可以与节点1'共享,并以此方式读取请求,并且具有较少的延迟高性能。
缺点:保持同步这两个节点。可能导致不一致。
如何解决呢?除了制作副本,我们还可以拥有节点的从属。在这种情况下,数据更新也将发送到从属设备。但是读/写总是发生在主节点上,直到从节点掉线后才接触从节点。
这就是“分布式缓存系统设计”的全部内容。
如果您要添加某些内容或有任何疑问,请喜欢,分享和评论。请继续更多此类博客。