缓存相关 学习笔记

原文地址如下:
缓存:数据库成为瓶颈后,动态数据的查询要如何加速?
缓存的使用姿势(一):如何选择缓存的读写策略?
缓存的使用姿势(二):缓存如何做到高可用?

本文知识用自己的思路顺利一遍,仅供自己学习笔记使用;
在这里插入图片描述

什么是缓存 ?

缓存,是一种存储数据的组件,它的作用是让对数据的请求更快地返回。

缓存不仅仅是一种组件的名字,更是一种设计思想,你可以认为任何能够加速读请求的组件和设计方案都是缓存思想的体现。而这种加速通常是通过两种方式来实现:

使用更快的介质,比方说存;
缓存复杂运算的结果。

缓存作为一种常见的空间换时间的性能优化手段。
在这里插入图片描述

什么是缓冲区呢?

缓冲区则是一块临时存储数据的区域,这些数据后面会被传输到其他设备上。

在这里插入图片描述

缓存分类

1. 静态缓存:

静态缓存在 Web 1.0 时期是非常著名的,它一般通过生成静态 HTML文件来实现静态缓存,在 Nginx 上部署静态缓存可以减少对于后台应用服务器的压力;

1. 分布式缓存:

静态缓存只能针对静态数据来缓存,对于动态请求就无能为力了。那么我们如何针对动态请求做缓存呢?这时你就需要分布式缓存了。

3. 热点本地缓存:

对于静态的资源的缓存你可以选择静态缓存,
对于动态的请求你可以选择分布式缓存,

那么什么时候要考虑热点本地缓存呢?
答案是当我们遇到极端的热点数据查询的时候

热点本地缓存主要部署在应用服务器的代码中,用于阻挡热点查询对于 分布式缓存节点 或者 数据库 的压力。

那么我们会在代码中使用一些本地缓存方案,如 HashMap,Guava Cache 或者是Ehcache 等,它们和应用程序部署在同一个进程中,优势是不需要跨网络调度,速度极快,所以可以来阻挡短时间内的热点查询。
由于本地缓存是部署在应用服务器中,而我们应用服务器通常会部署多台,当数据更新时,我们不能确定哪台服务器本地中了缓存,更新或者删除所有服务器的缓存不是一个好的选择,所以我们通常会等待缓存过期。因此,这种缓存的有效期很短,通常为分钟或者秒级别,以避免返回前端脏数据。

缓存可以有多层,比如上面提到的
静态缓存处在负载均衡层,
分布式缓存处在应用层和数据库层之间,
本地缓存处在应用层。
我们需要将请求尽量挡在上层,因为越往下层,对于并发的承受能力越差;

缓存命中率是我们对于缓存最重要的一个监控项,越是热点的数据,缓存的命中率就越高。

在这里插入图片描述

缓存的不足

**首先,缓存比较适合于读多写少的业务场景,并且数据最好带有一定的热点属性。**所以比如在搜索的场景下,每个人搜索的词都会不同,没有明显的热点,那么这时缓存的作用就不明显了。

其次,缓存会给整体系统带来复杂度,并且会有数据不一致的风险。 当更新数据库成功,更新缓存失败的场景下,缓存中就会存在脏数据。对于这种场景,我们可以考虑使用较短的过期时间或者手动清理的方式来解决。

再次,之前提到缓存通常使用内存作为存储介质,但是内存并不是无限的。
因此,我们在使用缓存的时候要做数据存储量级的评估,对于可预见的需要消耗极大存储成本的数据,要慎用缓存方案。同时,缓存一定要设置过期时间,这样可以保证缓存中的会是热点数据。

最后,缓存会给运维也带来一定的成本, 运维需要对缓存组件有一定的了解,在排查问题的时候也多了一个组件需要考虑在内。虽然有这么多的不足,但是缓存对于性能的提升是毋庸置疑的,我们在做架构设计的时候也需要把它考虑在内,只是在做具体方案的时候需要对缓存的设计有更细致的思考,才能最大
化的发挥缓存的优势。
在这里插入图片描述

缓存策略

数据变更如何操作数据库和缓存?

常规错误思路:

第1步 更新数据库
第2步 更新缓存

在这里插入图片描述在这里插入图片描述
更新完数据库再更新缓存,会造成缓存和数据库中的数据不一致
如下演示:
在这里插入图片描述
为什么产生这个问题呢?
变更数据库和变更缓存是两个独立的操作,非原子操作;当两个线程并发更新它们的时候,就会因为写入顺序的不同造成数据的不一致;

在这里插入图片描述
直接更新缓存还存在另外一个问题就是丢失更新

-------------------------------------------缓存-----------------------
                                          | 20|

请求A 线程1 从缓存中读到20 执行+121        | 20|                   

请求B 线程2 从缓存中读到20 执行+121        | 20|      

请求A 线程121写回缓存                    | 21|                  

请求B 线程221写回缓存                    | 21|                  

业务预期是金额数是22,这时缓存里面的金额是 21,这是个很严重的问题。

那我们要如何解决这个问题呢?
更新数据库后直接删除缓存中的数据;
在这里插入图片描述这个策略就是Cache Aside 策略(旁路缓存策略)

Cache Aside 策略(旁路缓存策略)

以数据库中的数据为准,缓存中的数据是按需加载。它可以分为读策略和写策
略。

读策略的步骤:

从缓存中读取数据;
如果缓存命中,则直接返回数据;
如果缓存不命中,则从数据库中查询数据;
查询到数据后,将数据写入到缓存中,并且返回给用户。

写策略的步骤是:

更新数据库中的记录;
删除缓存记录。

在写策略中,能否先删除缓存,后更新数据库呢?
答案是不行的,这样有可能出现缓存数据不一致的问题;
在这里插入图片描述先更新数据库,后删除缓存就没有问题了吗?
其实在理论上还是有缺陷的
在这里插入图片描述
参考下图,根据业内有人总结的数据,我们发现做一次内存寻址大概需要 100ns,而做一次磁盘的查找则需要 10ms,相差 100倍。缓存的写入远远快于数据库的写入。

大体上
99%的执行步骤是:
第1步,第2步,第5步,第3步,第4步
1%的执行步骤是:
第1步,第2步,第3步,第4步,第5步

只要请求 A 先更新缓存,请求 B再清空缓存,那么接下来的请求就会因为缓存为空而从数据库中重新加载数据,就不会出现这种不一致的情况。
在这里插入图片描述在这里插入图片描述
Cache Aside 存在的最大的问题是当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响

如果你的业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:

1 . 一种做法是在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个线程更新缓存,就不会产生并发问题了。当然这么做对于写入的性能会有一些影响;

2 . 另一种做法同样也是在更新数据时更新缓存,只是给缓存加一个较短的过期时间,这样即使出现缓存不一致的情况,缓存的数据也会很快地过期,对业务的影响也是可以接受。

在这里插入图片描述

Read/Write Through(读穿 / 写穿)策略

这个策略的核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据。

Write Through 的策略步骤:

先查询要写入的数据在缓存中是否已经存在,
如果已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,
如果缓存中数据不存在,我们把这种情况叫做“Write Miss(写失效)”。

一般可以选择两种“Write Miss”方式:

“Write Allocate(按写分配)”,做法是写入缓存相应位置,再由缓存组件同步更新到数据库中;

“No Wite Allocate(不按写分配)”,做法是不写入缓存中,而是直接更新到数据库中。

在 Write Through 策略中选择“No-write allocate”方式,原因是无论采用哪种“Write Miss”方式,我们都需要同步将数据更新到数据库中,而“No-write
allocate”方式相比“Write Allocate”还减少了一次缓存的写入,能够提升写入的性能。

Read Through 策略步骤:

先查询缓存中数据是否存在,
如果存在则直接返回,
如果不存在,则由缓存组件负责从数据库中同步加载数据。

Read Through/Write Through 策略的示意图:
在这里插入图片描述
Read Through/Write Through 策略的特点是由缓存节点来和数据库打交道。
在开发过程中相比 Cache Aside 策略要少见一些,原因是我们经常使用的分布式缓存组件,无论是 Memcached 还是 Redis 都不提供写入数据库,或者自动加载数据库中的数据的功能。而我们在使用本地缓存的时候可以考虑使用这种策略。

我们看到 Write Through 策略中写数据库是同步的,这对于性能来说会有比较大的影响,因为相比于写缓存,同步写数据库的延迟就要高很多了。那么我们可否异步地更新数据库?

“Write Back”策略
在这里插入图片描述

Write Back(写回)策略

这个策略的核心思想:在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块只有被再次使用时才会将其中的数据写入到后端存储中。
在这里插入图片描述
如果使用 Write Back 策略的话,读的策略如下:
在这里插入图片描述
这种策略不能被应用到我们常用的数据库和缓存的场景中,它是计算机体系
结构中的设计,比如我们在向磁盘中写数据时采用的就是这种策略。
无论是操作系统层面的Page Cache,还是日志的异步刷盘,亦或是消息队列中消息的异步写入磁盘,大多采用了这种策略。因为这个策略在性能上的优势毋庸置疑,它避免了直接写磁盘造成的随机写问题,毕竟写内存和写磁盘的随机 I/O 的延迟相差了几个数量级呢。

但因为缓存一般使用内存,而内存是非持久化的,所以一旦缓存机器掉电,就会造成原本缓存中的脏块儿数据丢失。所以你会发现系统在掉电之后,之前写入的文件会有部分丢失,就是因为 Page Cache 还没有来得及刷盘造成的。

可以在一些场景下依然可以使用这个策略,在使用时,我想给你的落地建议是:你在向低速设备写入数据的时候,可以在内存里先暂存一段时间的数据,甚至做一些统计汇总,然后定时地刷新到低速设备上。比如说,你在统计你的接口响应时间的时候,需要将每次请求的响应时间打印到日志中,然后监控系统收集日志后再做统计。但是如果每次请求都打印日志无疑会增加磁盘 I/O,那么不如把一段时间的响应时间暂存起来,经过简单的统计平均耗时,每个耗时区间的请求数量等等,然后定时地,批量地打印到日志中。
在这里插入图片描述在这里插入图片描述

缓存如何做到高可用?

一般来说,电商系统中,核心缓存的命中率需要维持在 99% 甚至是 99.9%,哪怕下降 1%,系统都会遭受毁灭性的打击。

这绝不是危言耸听,我们来计算一下。
假设系统的 QPS 是 10000/s,每次调用会访问 10次缓存或者数据库中的数据,那么当缓存命中率仅仅减少 1%,数据库每秒就会增加 10000* 10 * 1% = 1000 次请求。而一般来说我们单个 MySQL 节点的读请求量峰值就在 1500/s左右,增加的这 1000 次请求很可能会给数据库造成极大的冲击。命中率仅仅下降 1% 造成的影响就如此可怕,更不要说缓存节点故障了。

那我们要如何来解决这个问题,提升缓存的可用性呢?
可以通过部署多个节点,同时设计一些方案让这些节点互为备份。这样,当某个节点故障时,它的备份节点可以顶替它继续提供服务。这就是 分布式缓存的高可用方案。

分布式缓存的高可用方案

在我的项目中,我主要选择的方案有客户端方案、中间代理层方案和服务端方案三大类:

客户端方案就是在客户端配置多个缓存的节点,通过缓存写入和读取算法策略来实现分布式,从而提高缓存的可用性。

中间代理层方案是在应用代码和缓存节点之间增加代理层,客户端所有的写入和读取的请求都通过代理层,而代理层中会内置高可用策略,帮助提升缓存系统的高可用。

服务端方案就是 Redis 2.4 版本后提出的 Redis Sentinel 方案。掌握这些方案可以帮助你,抵御部分缓存节点故障导致的,缓存命中率下降的影响,增强你的系统的健壮性。

客户端方案

在客户端方案中,你需要关注缓存的写和读两个方面:
写入数据时,需要把被写入缓存的数据分散到多个节点中,即进行数据分片;
读数据时,可以利用多组的缓存来做容错,提升缓存系统的可用性。关于读数据,这里可以使用主从和多副本两种策略,两种策略是为了解决不同的问题而提出的。下面我就带你一起详细地看一下到底要怎么做。

未完待续…

猜你喜欢

转载自blog.csdn.net/weixin_37646636/article/details/120427314