This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
<audio id="audio" title="12 | 缓存:数据库成为瓶颈后,动态数据的查询要如何加速?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a6/75/a6d9c0ef2a0cd7ecdf8eaf45a05e7a75.mp3"></audio>
你好,我是唐扬。
通过前面数据库篇的学习你已经了解了在高并发大流量下数据库层的演进过程以及库表设计上的考虑点。你的垂直电商系统在完成了对数据库的主从分离和分库分表之后已经可以支撑十几万DAU了整体系统的架构也变成了下面这样
<img src="https://static001.geekbang.org/resource/image/c1/20/c14a816c828434fe1695220b7abdbc20.jpg" alt="">
从整体上看数据库分成了主库和从库数据也被切分到多个数据库节点上。但随着并发的增加存储数据量的增多数据库的磁盘IO逐渐成了系统的瓶颈我们需要一种访问更快的组件来降低请求响应时间提升整体系统性能。这时我们就会使用缓存。**那么什么是缓存,我们又该如何将它的优势最大化呢?**
**本节课是缓存篇的总纲,**我将从缓存定义、缓存分类和缓存优势劣势三个方面全方位带你掌握缓存的设计思想和理念再用剩下4节课的时间带你针对性地掌握使用缓存的正确姿势以便让你在实际工作中能够更好地使用缓存提升整体系统的性能。
接下来,让我们进入今天的课程吧!
## 什么是缓存
缓存,是一种存储数据的组件,它的作用是让对数据的请求更快地返回。
我们经常会把缓存放在内存中来存储, 所以有人就把内存和缓存画上了等号这完全是外行人的见解。作为业内人士你要知道在某些场景下我们可能还会使用SSD作为冷数据的缓存。比如说360开源的Pika就是使用SSD存储数据解决Redis的容量瓶颈的。
实际上,凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为缓存。那么说到这儿我们就需要知道常见硬件组件的延时情况是什么样的了,这样在做方案的时候可以对延迟有更直观的印象。幸运的是,业内已经有人帮我们总结出这些数据了,我将这些数据整理了一下,你可以看一下。
<img src="https://static001.geekbang.org/resource/image/01/ad/0134f4cd9e0d6e8d57ebe35eb28c32ad.jpg" alt="">
从这些数据中你可以看到做一次内存寻址大概需要100ns而做一次磁盘的查找则需要10ms。如果我们将做一次内存寻址的时间类比为一个课间那么做一次磁盘查找相当于度过了大学的一个学期。可见我们使用内存作为缓存的存储介质相比于以磁盘作为主要存储介质的数据库来说性能上会提高多个数量级同时也能够支撑更高的并发量。所以内存是最常见的一种缓存数据的介质。
缓存作为一种常见的空间换时间的性能优化手段,在很多地方都有应用,我们先来看几个例子,相信你一定不会陌生。
### 1.缓存案例
Linux内存管理是通过一个叫做MMUMemory Management Unit的硬件来实现从虚拟地址到物理地址的转换的但是如果每次转换都要做这么复杂计算的话无疑会造成性能的损耗所以我们会借助一个叫做TLBTranslation Lookaside Buffer的组件来缓存最近转换过的虚拟地址和物理地址的映射。TLB就是一种缓存组件缓存复杂运算的结果就好比你做一碗色香味俱全的面条可能比较复杂那么我们把做好的面条油炸处理一下做成方便面你做方便面的话就简单多了也快速多了。这个缓存组件比较底层这里你只需要了解一下就可以了。
在大部分的笔记本桌面电脑和服务器上都会有一个或者多个TLB组件在不经意间帮助我们加快地址转换的速度。
**再想一下你平时经常刷的抖音。**平台上的短视频实际上是使用内置的网络播放器来完成的。网络播放器接收的是数据流,将数据下载下来之后经过分离音视频流,解码等流程后输出到外设设备上播放。
如果我们在打开一个视频的时候才开始下载数据的话,无疑会增加视频的打开速度(我们叫首播时间),并且播放过程中会有卡顿。所以我们的播放器中通常会设计一些缓存的组件,在未打开视频时缓存一部分视频数据,比如我们打开抖音,服务端可能一次会返回三个视频信息,我们在播放第一个视频的时候,播放器已经帮我们缓存了第二、三个视频的部分数据,这样在看第二个视频的时候就可以给用户“秒开”的感觉。
**除此之外我们熟知的HTTP协议也是有缓存机制的。**当我们第一次请求静态的资源时比如一张图片服务端除了返回图片信息在响应头里面还有一个“Etag”的字段。浏览器会缓存图片信息以及这个字段的值。当下一次再请求这个图片的时候浏览器发起的请求头里面会有一个“If-None-Match”的字段并且把缓存的“Etag”的值写进去发给服务端。服务端比对图片信息是否有变化如果没有则返回浏览器一个304的状态码浏览器会继续使用缓存的图片信息。通过这种缓存协商的方式可以减少网络传输的数据大小从而提升页面展示的性能。
<img src="https://static001.geekbang.org/resource/image/7a/81/7a2344bd27535936b4ad4d8519d9fd81.jpg" alt="">
### 2.缓存与缓冲区
讲了这么多缓存案例,想必你对缓存已经有了一个直观并且形象的了解了。除了缓存,我们在日常开发过程中还会经常听见一个相似的名词——缓冲区,那么,什么是缓冲区呢?缓冲和缓存只有一字之差,它们有什么区别呢?
我们知道,缓存可以提高低速设备的访问速度,或者减少复杂耗时的计算带来的性能问题。理论上说,我们可以通过缓存解决所有关于“慢”的问题,比如从磁盘随机读取数据慢,从数据库查询数据慢,只是不同的场景消耗的存储成本不同。
**缓冲区则是一块临时存储数据的区域,这些数据后面会被传输到其他设备上。**缓冲区更像“消息队列篇”中即将提到的消息队列,用以弥补高速设备和低速设备通信时的速度差。比如,我们将数据写入磁盘时并不是直接刷盘,而是写到一块缓冲区里面,内核会标识这个缓冲区为脏。当经过一定时间或者脏缓冲区比例到达一定阈值时,由单独的线程把脏块刷新到硬盘上。这样避免了每次写数据都要刷盘带来的性能问题。
<img src="https://static001.geekbang.org/resource/image/09/d1/09d6e75a62e5cb5b72d45337ca206ad1.jpg" alt="">
以上就是缓冲区和缓存的区别从这个区别来看上面提到的TLB的命名是有问题的它应该是缓存而不是缓冲区。
现在你已经了解了缓存的含义,那么我们经常使用的缓存都有哪些?我们又该如何使用缓存,将它的优势最大化呢?
## 缓存分类
在我们日常开发中,常见的缓存主要就是**静态缓存、分布式缓存和热点本地缓存**这三种。
静态缓存在Web 1.0时期是非常著名的它一般通过生成Velocity模板或者静态HTML文件来实现静态缓存在Nginx上部署静态缓存可以减少对于后台应用服务器的压力。例如我们在做一些内容管理系统的时候后台会录入很多的文章前台在网站上展示文章内容就像新浪网易这种门户网站一样。
当然我们也可以把文章录入到数据库里面然后前端展示的时候穿透查询数据库来获取数据但是这样会对数据库造成很大的压力。虽然我们使用分布式缓存来挡读请求但是对于像日均PV几十亿的大型门户网站来说基于成本考虑仍然是不划算的。
**所以我们的解决思路是**每篇文章在录入的时候渲染成静态页面放置在所有的前端Nginx或者Squid等Web服务器上这样用户在访问的时候会优先访问Web服务器上的静态页面在对旧的文章执行一定的清理策略后依然可以保证99%以上的缓存命中率。
这种缓存只能针对静态数据来缓存,对于动态请求就无能为力了。那么我们如何针对动态请求做缓存呢?**这时你就需要分布式缓存了。**
分布式缓存的大名可谓是如雷贯耳了我们平时耳熟能详的Memcached、Redis就是分布式缓存的典型例子。它们性能强劲通过一些分布式的方案组成集群可以突破单机的限制。所以在整体架构中分布式缓存承担着非常重要的角色接下来的课程我会专门针对分布式缓存带你了解分布式缓存的使用技巧以及高可用的方案让你能在工作中对分布式缓存运用自如
对于静态的资源的缓存你可以选择静态缓存,对于动态的请求你可以选择分布式缓存,**那么什么时候要考虑热点本地缓存呢?**
**答案是当我们遇到极端的热点数据查询的时候。**热点本地缓存主要部署在应用服务器的代码中,用于阻挡热点查询对于分布式缓存节点或者数据库的压力。
比如某一位明星在微博上有了热点话题,“吃瓜群众”会到他(她)的微博首页围观,这就会引发这个用户信息的热点查询。这些查询通常会命中某一个缓存节点或者某一个数据库分区,短时间内会形成极高的热点查询。
那么我们会在代码中使用一些本地缓存方案如HashMapGuava Cache或者是Ehcache等它们和应用程序部署在同一个进程中优势是不需要跨网络调度速度极快所以可以用来阻挡短时间内的热点查询。**来看个例子。**
**比方说**你的垂直电商系统的首页有一些推荐的商品这些商品信息是由编辑在后台录入和变更。你分析编辑录入新的商品或者变更某个商品的信息后在页面的展示是允许有一些延迟的比如说30秒的延迟并且首页请求量最大即使使用分布式缓存也很难抗住所以你决定使用Guava Cache来将所有的推荐商品的信息缓存起来并且设置每隔30秒重新从数据库中加载最新的所有商品。
首先我们初始化Guava 的Loading Cache
```
CacheBuilder&lt;String, List&lt;Product&gt;&gt; cacheBuilder = CacheBuilder.newBuilder().maximumSize(maxSize).recordStats(); //设置缓存最大值
cacheBuilder = cacheBuilder.refreshAfterWrite(30, TimeUnit.Seconds); //设置刷新间隔
LoadingCache&lt;String, List&lt;Product&gt;&gt; cache = cacheBuilder.build(new CacheLoader&lt;String, List&lt;Product&gt;&gt;() {
@Override
public List&lt;Product&gt; load(String k) throws Exception {
return productService.loadAll(); // 获取所有商品
}
});
```
这样你在获取所有商品信息的时候可以调用Loading Cache的get方法就可以优先从本地缓存中获取商品信息如果本地缓存不存在会使用CacheLoader中的逻辑从数据库中加载所有的商品。
由于本地缓存是部署在应用服务器中,而我们应用服务器通常会部署多台,当数据更新时,我们不能确定哪台服务器本地中了缓存,更新或者删除所有服务器的缓存不是一个好的选择,所以我们通常会等待缓存过期。因此,这种缓存的有效期很短,通常为分钟或者秒级别,以避免返回前端脏数据。
## 缓存的不足
通过了解上面的内容,你不难发现,缓存的主要作用是提升访问速度,从而能够抗住更高的并发。那么,缓存是不是能够解决一切问题?显然不是。事物都是具有两面性的,缓存也不例外,我们要了解它的优势的同时也需要了解它有哪些不足,从而扬长避短,将它的作用发挥到最大。
**首先,缓存比较适合于读多写少的业务场景,并且数据最好带有一定的热点属性。**这是因为缓存毕竟会受限于存储介质不可能缓存所有数据那么当数据有热点属性的时候才能保证一定的缓存命中率。比如说类似微博、朋友圈这种20%的内容会占到80%的流量。所以,一旦当业务场景读少写多时或者没有明显热点时,比如在搜索的场景下,每个人搜索的词都会不同,没有明显的热点,那么这时缓存的作用就不明显了。
**其次,缓存会给整体系统带来复杂度,并且会有数据不一致的风险。**当更新数据库成功,更新缓存失败的场景下,缓存中就会存在脏数据。对于这种场景,我们可以考虑使用较短的过期时间或者手动清理的方式来解决。
**再次,之前提到缓存通常使用内存作为存储介质,但是内存并不是无限的。**因此,我们在使用缓存的时候要做数据存储量级的评估,对于可预见的需要消耗极大存储成本的数据,要慎用缓存方案。同时,缓存一定要设置过期时间,这样可以保证缓存中的会是热点数据。
**最后,缓存会给运维也带来一定的成本,**运维需要对缓存组件有一定的了解,在排查问题的时候也多了一个组件需要考虑在内。
虽然有这么多的不足,但是缓存对于性能的提升是毋庸置疑的,我们在做架构设计的时候也需要把它考虑在内,只是在做具体方案的时候需要对缓存的设计有更细致的思考,才能最大化地发挥缓存的优势。
## 课程小结
这节课我带你了解了缓存的定义,常见缓存的分类以及缓存的不足。我想跟你强调的重点有以下几点:
<li>
缓存可以有多层,比如上面提到的静态缓存处在负载均衡层,分布式缓存处在应用层和数据库层之间,本地缓存处在应用层。我们需要将请求尽量挡在上层,因为越往下层,对于并发的承受能力越差;
</li>
<li>
缓存命中率是我们对于缓存最重要的一个监控项,越是热点的数据,缓存的命中率就越高。
</li>
你还需要理解的是,缓存不仅仅是一种组件的名字,更是一种设计思想,你可以认为任何能够加速读请求的组件和设计方案都是缓存思想的体现。而这种加速通常是通过两种方式来实现:
<li>
使用更快的介质,比方说课程中提到的内存;
</li>
<li>
缓存复杂运算的结果比方说前面TLB的例子就是缓存地址转换的结果。
</li>
那么,当你在实际工作中碰到“慢”的问题时,缓存就是你第一时间需要考虑的。
## 一课一思
这节课讲了这么多缓存的例子,你在日常工作中看到了哪些使用了缓存思想的设计呢?欢迎在留言区留言与我一起讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,121 @@
<audio id="audio" title="13 | 缓存的使用姿势(一):如何选择缓存的读写策略?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f8/3c/f8e5c503893cf8db5e10a74c58e8083c.mp3"></audio>
上节课,我带你了解了缓存的定义、分类以及不足,你现在应该对缓存有了初步的认知。从今天开始,我将带你了解一下使用缓存的正确姿势,比如缓存的读写策略是什么样的,如何做到缓存的高可用以及如何应对缓存穿透。通过了解这些内容,你会对缓存的使用有深刻的认识,这样在实际工作中就可以在缓存使用上游刃有余了。
今天,我们先讲讲缓存的读写策略。你可能觉得缓存的读写很简单,只需要优先读缓存,缓存不命中就从数据库查询,查询到了就回种缓存。实际上,针对不同的业务场景,缓存的读写策略也是不同的。
而我们在选择策略时也需要考虑诸多的因素,比如说,缓存中是否有可能被写入脏数据,策略的读写性能如何,是否存在缓存命中率下降的情况等等。接下来,我就以标准的“缓存+数据库”的场景为例,带你剖析经典的缓存读写策略以及它们适用的场景。这样一来,你就可以在日常的工作中根据不同的场景选择不同的读写策略。
## Cache Aside旁路缓存策略
我们来考虑一种最简单的业务场景比方说在你的电商系统中有一个用户表表中只有ID和年龄两个字段缓存中我们以ID为Key存储用户的年龄信息。那么当我们要把ID为1的用户的年龄从19变更为20要如何做呢
**你可能会产生这样的思路:**先更新数据库中ID为1的记录再更新缓存中Key为1的数据。
<img src="https://static001.geekbang.org/resource/image/d3/65/d3389ef91de21e90dec2a9854e26e965.jpg" alt="">
**这个思路会造成缓存和数据库中的数据不一致。**比如A请求将数据库中ID为1的用户年龄从19变更为20与此同时请求B也开始更新ID为1的用户数据它把数据库中记录的年龄变更为21然后变更缓存中的用户年龄为21。紧接着A请求开始更新缓存数据它会把缓存中的年龄变更为20。此时数据库中用户年龄是21而缓存中的用户年龄却是20。
<img src="https://static001.geekbang.org/resource/image/7f/35/7fbf80fb7949939dd5543a8da8181635.jpg" alt="">
**为什么产生这个问题呢?**因为变更数据库和变更缓存是两个独立的操作,而我们并没有对操作做任何的并发控制。那么当两个线程并发更新它们的时候,就会因为写入顺序的不同造成数据的不一致。
另外直接更新缓存还存在另外一个问题就是丢失更新。还是以我们的电商系统为例假如电商系统中的账户表有三个字段ID、户名和金额这个时候缓存中存储的就不只是金额信息而是完整的账户信息了。当更新缓存中账户金额时你需要从缓存中查询完整的账户数据把金额变更后再写入到缓存中。
这个过程中也会有并发的问题比如说原有金额是20A请求从缓存中读到数据并且把金额加1变更成21在未写入缓存之前又有请求B也读到缓存的数据后把金额也加1也变更成21两个请求同时把金额写回缓存这时缓存里面的金额是21但是我们实际上预期是金额数加2这也是一个比较大的问题。
**那我们要如何解决这个问题呢?**其实,我们可以在更新数据时不更新缓存,而是删除缓存中的数据,在读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。
<img src="https://static001.geekbang.org/resource/image/66/c4/661da5a2b55b7d6e1575a3241247eec4.jpg" alt="">
这个策略就是我们使用缓存最常见的策略Cache Aside策略也叫旁路缓存策略这个策略数据以数据库中的数据为准缓存中的数据是按需加载的。它可以分为读策略和写策略**其中读策略的步骤是:**
- 从缓存中读取数据;
- 如果缓存命中,则直接返回数据;
- 如果缓存不命中,则从数据库中查询数据;
- 查询到数据后,将数据写入到缓存中,并且返回给用户。
**写策略的步骤是:**
- 更新数据库中的记录;
- 删除缓存记录。
你也许会问了,在写策略中,能否先删除缓存,后更新数据库呢?**答案是不行的,**因为这样也有可能出现缓存数据不一致的问题,我以用户表的场景为例解释一下。
假设某个用户的年龄是20请求A要更新用户年龄为21所以它会删除缓存中的内容。这时另一个请求B要读取这个用户的年龄它查询缓存发现未命中后会从数据库中读取到年龄为20并且写入到缓存中然后请求A继续更改数据库将用户的年龄更新为21这就造成了缓存和数据库的不一致。
<img src="https://static001.geekbang.org/resource/image/b7/3b/b725cc2c93f31a5d477b6b72fc5add3b.jpg" alt="">
那么像Cache Aside策略这样先更新数据库后删除缓存就没有问题了吗其实在理论上还是有缺陷的。假如某个用户数据在缓存中不存在请求A读取数据时从数据库中查询到年龄为20在未写入缓存中时另一个请求B更新数据。它更新数据库中的年龄为21并且清空缓存。这时请求A把从数据库中读到的年龄为20的数据写入到缓存中造成缓存和数据库数据不一致。
<img src="https://static001.geekbang.org/resource/image/f2/d9/f24f728919216b90e374e33a82ccd5d9.jpg" alt="">
不过这种问题出现的几率并不高原因是缓存的写入通常远远快于数据库的写入所以在实际中很难出现请求B已经更新了数据库并且清空了缓存请求A才更新完缓存的情况。而一旦请求A早于请求B清空缓存之前更新了缓存那么接下来的请求就会因为缓存为空而从数据库中重新加载数据所以不会出现这种不一致的情况。
**Cache Aside策略是我们日常开发中最经常使用的缓存策略不过我们在使用时也要学会依情况而变。**比如说当新注册一个用户,按照这个更新策略,你要写数据库,然后清理缓存(当然缓存中没有数据给你清理)。可当我注册用户后立即读取用户信息,并且数据库主从分离时,会出现因为主从延迟所以读不到用户信息的情况。
**而解决这个问题的办法**恰恰是在插入新数据到数据库之后写入缓存,这样后续的读请求就会从缓存中读到数据了。并且因为是新注册的用户,所以不会出现并发更新用户信息的情况。
Cache Aside存在的最大的问题是当写入比较频繁时缓存中的数据会被频繁地清理这样会对缓存的命中率有一些影响。**如果你的业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:**
1.一种做法是在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个线程更新缓存,就不会产生并发问题了。当然这么做对于写入的性能会有一些影响;
2.另一种做法同样也是在更新数据时更新缓存,只是给缓存加一个较短的过期时间,这样即使出现缓存不一致的情况,缓存的数据也会很快过期,对业务的影响也是可以接受。
当然了,除了这个策略,在计算机领域还有其他几种经典的缓存策略,它们也有各自适用的使用场景。
## Read/Write Through读穿/写穿)策略
这个策略的核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据。这就好比你在汇报工作的时候只对你的直接上级汇报,再由你的直接上级汇报给他的上级,你是不能越级汇报的。
Write Through的策略是这样的先查询要写入的数据在缓存中是否已经存在如果已经存在则更新缓存中的数据并且由缓存组件同步更新到数据库中如果缓存中数据不存在我们把这种情况叫做“Write Miss写失效”。
一般来说我们可以选择两种“Write Miss”方式一个是“Write Allocate按写分配做法是写入缓存相应位置再由缓存组件同步更新到数据库中另一个是“No-write allocate不按写分配做法是不写入缓存中而是直接更新到数据库中。
在Write Through策略中我们一般选择“No-write allocate”方式原因是无论采用哪种“Write Miss”方式我们都需要同步将数据更新到数据库中而“No-write allocate”方式相比“Write Allocate”还减少了一次缓存的写入能够提升写入的性能。
Read Through策略就简单一些它的步骤是这样的先查询缓存中数据是否存在如果存在则直接返回如果不存在则由缓存组件负责从数据库中同步加载数据。
下面是Read Through/Write Through策略的示意图
<img src="https://static001.geekbang.org/resource/image/90/d1/90dc599d4d2604cd5943584c4d755bd1.jpg" alt="">
Read Through/Write Through策略的特点是由缓存节点而非用户来和数据库打交道在我们开发过程中相比Cache Aside策略要少见一些原因是我们经常使用的分布式缓存组件无论是Memcached还是Redis都不提供写入数据库或者自动加载数据库中的数据的功能。而我们在使用本地缓存的时候可以考虑使用这种策略比如说在上一节中提到的本地缓存Guava Cache中的Loading Cache就有Read Through策略的影子。
我们看到Write Through策略中写数据库是同步的这对于性能来说会有比较大的影响因为相比于写缓存同步写数据库的延迟就要高很多了。那么我们可否异步地更新数据库这就是我们接下来要提到的“Write Back”策略。
## Write Back写回策略
这个策略的核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中。
**需要注意的是,**在“Write Miss”的情况下我们采用的是“Write Allocate”的方式也就是在写入后端存储的同时要写入缓存这样我们在之后的写请求中都只需要更新缓存即可而无需更新后端存储了我将Write back策略的示意图放在了下面
<img src="https://static001.geekbang.org/resource/image/59/9f/59f3c4caafd4c3274ddb7e0ac37f429f.jpg" alt="">
**如果使用Write Back策略的话读的策略也有一些变化了。**我们在读取缓存时如果发现缓存命中则直接返回缓存数据。如果缓存不命中则寻找一个可用的缓存块儿,如果这个缓存块儿是“脏”的,就把缓存块儿中之前的数据写入到后端存储中,并且从后端存储加载数据到缓存块儿,如果不是脏的,则由缓存组件将后端存储中的数据加载到缓存中,最后我们将缓存设置为不是脏的,返回数据就好了。
<img src="https://static001.geekbang.org/resource/image/a0/59/a01bbf953088eef6695ffb1dc182b559.jpg" alt="">
**发现了吗?**其实这种策略不能被应用到我们常用的数据库和缓存的场景中它是计算机体系结构中的设计比如我们在向磁盘中写数据时采用的就是这种策略。无论是操作系统层面的Page Cache还是日志的异步刷盘亦或是消息队列中消息的异步写入磁盘大多采用了这种策略。因为这个策略在性能上的优势毋庸置疑它避免了直接写磁盘造成的随机写问题毕竟写内存和写磁盘的随机I/O的延迟相差了几个数量级呢。
但因为缓存一般使用内存而内存是非持久化的所以一旦缓存机器掉电就会造成原本缓存中的脏块儿数据丢失。所以你会发现系统在掉电之后之前写入的文件会有部分丢失就是因为Page Cache还没有来得及刷盘造成的。
**当然,你依然可以在一些场景下使用这个策略,在使用时,我想给你的落地建议是:**你在向低速设备写入数据的时候可以在内存里先暂存一段时间的数据甚至做一些统计汇总然后定时地刷新到低速设备上。比如说你在统计你的接口响应时间的时候需要将每次请求的响应时间打印到日志中然后监控系统收集日志后再做统计。但是如果每次请求都打印日志无疑会增加磁盘I/O那么不如把一段时间的响应时间暂存起来经过简单的统计平均耗时每个耗时区间的请求数量等等然后定时地批量地打印到日志中。
## 课程小结
本节课,我主要带你了解了缓存使用的几种策略,以及每种策略适用的使用场景是怎样的。我想让你掌握的重点是:
1.Cache Aside是我们在使用分布式缓存时最常用的策略你可以在实际工作中直接拿来使用。
2.Read/Write Through和Write Back策略需要缓存组件的支持所以比较适合你在实现本地缓存组件的时候使用
3.Write Back策略是计算机体系结构中的策略不过写入策略中的只写缓存异步写入后端存储的策略倒是有很多的应用场景。
而且,你还需要了解,我们今天提到的策略都是标准的使用姿势,在实际开发过程中需要结合实际的业务特点灵活使用甚至加以改造。这些业务特点包括但不仅限于:整体的数据量级情况,访问的读写比例的情况,对于数据的不一致时间的容忍度,对于缓存命中率的要求等等。理论结合实践,具体情况具体分析,你才能得到更好的解决方案。
## 一课一思
结合今天课程中的内容,你可以思考一下在日常工作中使用缓存时都使用了哪些缓存的读写策略呢?欢迎在留言区和我一起讨论。
最后,感谢你的阅读,如果这篇文章对你有收获,欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,153 @@
<audio id="audio" title="14 | 缓存的使用姿势(二):缓存如何做到高可用?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4a/fd/4a55305ed43275956ca7be80f24e69fd.mp3"></audio>
你好,我是唐扬。
前面几节课,我带你了解了缓存的原理、分类以及常用缓存的使用技巧。我们开始用缓存承担大部分的读压力,从而缓解数据库的查询压力,在提升性能的同时保证系统的稳定性。这时,你的电商系统整体的架构演变成下图的样子:
<img src="https://static001.geekbang.org/resource/image/6c/05/6c860d61a578cde20591968cc2741a05.jpg" alt="">
我们在Web层和数据库层之间增加了缓存层请求会首先查询缓存只有当缓存中没有需要的数据时才会查询数据库。
在这里,你需要关注缓存命中率这个指标(缓存命中率=命中缓存的请求数/总请求数。一般来说在你的电商系统中核心缓存的命中率需要维持在99%甚至是99.9%哪怕下降1%,系统都会遭受毁灭性的打击。
这绝不是危言耸听我们来计算一下。假设系统的QPS是10000/s每次调用会访问10次缓存或者数据库中的数据那么当缓存命中率仅仅减少1%数据库每秒就会增加10000 * 10 * 1% = 1000次请求。而一般来说我们单个MySQL节点的读请求量峰值就在1500/s左右增加的这1000次请求很可能会给数据库造成极大的冲击。
命中率仅仅下降1%造成的影响就如此可怕,更不要说缓存节点故障了。而图中单点部署的缓存节点就成了整体系统中最大的隐患,那我们要如何来解决这个问题,提升缓存的可用性呢?
我们可以通过部署多个节点,同时设计一些方案让这些节点互为备份。这样,当某个节点故障时,它的备份节点可以顶替它继续提供服务。**而这些方案就是我们本节课的重点:分布式缓存的高可用方案。**
在我的项目中,我主要选择的方案有**客户端方案、中间代理层方案和服务端方案**三大类:
<li>
**客户端方案**就是在客户端配置多个缓存的节点,通过缓存写入和读取算法策略来实现分布式,从而提高缓存的可用性。
</li>
<li>
**中间代理层方案**是在应用代码和缓存节点之间增加代理层,客户端所有的写入和读取的请求都通过代理层,而代理层中会内置高可用策略,帮助提升缓存系统的高可用。
</li>
<li>
**服务端方案**就是Redis 2.4版本后提出的Redis Sentinel方案。
</li>
掌握这些方案可以帮助你,抵御部分缓存节点故障导致的,缓存命中率下降的影响,增强你的系统的鲁棒性。
## 客户端方案
在客户端方案中,你需要关注缓存的写和读两个方面:
<li>
写入数据时,需要把被写入缓存的数据分散到多个节点中,即进行数据分片;
</li>
<li>
读数据时,可以利用多组的缓存来做容错,提升缓存系统的可用性。关于读数据,这里可以使用主从和多副本两种策略,两种策略是为了解决不同的问题而提出的。
</li>
下面我就带你一起详细地看一下到底要怎么做。
**1.缓存数据如何分片**
单一的缓存节点受到机器内存、网卡带宽和单节点请求量的限制,不能承担比较高的并发,因此我们考虑将数据分片,依照分片算法将数据打散到多个不同的节点上,每个节点上存储部分数据。
这样在某个节点故障的情况下,其他节点也可以提供服务,保证了一定的可用性。这就好比不要把鸡蛋放在同一个篮子里,这样一旦一个篮子掉在地上,摔碎了,别的篮子里还有没摔碎的鸡蛋,不至于一个不剩。
**一般来讲分片算法常见的就是Hash分片算法和一致性Hash分片算法两种。**
Hash分片的算法就是对缓存的Key做哈希计算然后对总的缓存节点个数取余。你可以这么理解
比如说我们部署了三个缓存节点组成一个缓存的集群当有新的数据要写入时我们先对这个缓存的Key做比如crc32等Hash算法生成Hash值然后对Hash值模3得出的结果就是要存入缓存节点的序号。
<img src="https://static001.geekbang.org/resource/image/72/55/720f7e4543d45fdc71056de280caff55.jpg" alt="">
这个算法最大的优点就是简单易理解,缺点是当增加或者减少缓存节点时,缓存总的节点个数变化造成计算出来的节点发生变化,从而造成缓存失效不可用。**所以我建议你,**如果采用这种方法,最好建立在你对于这组缓存命中率下降不敏感,比如下面还有另外一层缓存来兜底的情况下。<br>
<br>
**当然了用一致性Hash算法可以很好地解决增加和删减节点时命中率下降的问题。**在这个算法中我们将整个Hash值空间组织成一个虚拟的圆环然后将缓存节点的IP地址或者主机名做Hash取值后放置在这个圆环上。当我们需要确定某一个Key需要存取到哪个节点上的时候先对这个Key做同样的Hash取值确定在环上的位置然后按照顺时针方向在环上“行走”遇到的第一个缓存节点就是要访问的节点。比方说下面这张图里面Key 1和Key 2会落入到Node 1中Key 3、Key 4会落入到Node 2中Key 5落入到Node 3中Key 6落入到Node 4中。
<img src="https://static001.geekbang.org/resource/image/f9/fe/f9ea0e201aa954cf46c5762835095efe.jpg" alt="">
这时如果在Node 1和Node 2之间增加一个Node 5你可以看到原本命中Node 2的Key 3现在命中到Node 5而其它的Key都没有变化同样的道理如果我们把Node 3从集群中移除那么只会影响到Key 5 。所以你看,**在增加和删除节点时只有少量的Key会“漂移”到其它节点上**而大部分的Key命中的节点还是会保持不变从而可以保证命中率不会大幅下降。
<img src="https://static001.geekbang.org/resource/image/4c/91/4c13c4fd4278dc97d072afe09a1a1b91.jpg" alt="">
不过,事物总有两面性。虽然这个算法对命中率的影响比较小,但它还是存在问题:
<li>
缓存节点在圆环上分布不平均,会造成部分缓存节点的压力较大;当某个节点故障时,这个节点所要承担的所有访问都会被顺移到另一个节点上,会对后面这个节点造成压力。
</li>
<li>
一致性Hash算法的脏数据问题。
</li>
极端情况下比如一个有三个节点A、B、C承担整体的访问每个节点的访问量平均A故障后B将承担双倍的压力A和B的全部请求当B承担不了流量Crash后C也将因为要承担原先三倍的流量而Crash这就造成了整体缓存系统的雪崩。
说到这儿,你可能觉得很可怕,但也不要太担心,**我们程序员就是要能够创造性地解决各种问题所以你可以在一致性Hash算法中引入虚拟节点的概念。**
它将一个缓存节点计算多个Hash值分散到圆环的不同位置这样既实现了数据的平均而且当某一个节点故障或者退出的时候它原先承担的Key将以更加平均的方式分配到其他节点上从而避免雪崩的发生。
**其次就是一致性Hash算法的脏数据问题。为什么会产生脏数据呢**比方说在集群中有两个节点A和B客户端初始写入一个Key为k值为3的缓存数据到Cache A中。这时如果要更新k的值为4但是缓存A恰好和客户端连接出现了问题那这次写入请求会写入到Cache B中。接下来缓存A和客户端的连接恢复当客户端要获取k的值时就会获取到存在Cache A中的脏数据3而不是Cache B中的4。
**所以在使用一致性Hash算法时一定要设置缓存的过期时间**这样当发生漂移时,之前存储的脏数据可能已经过期,就可以减少存在脏数据的几率。
<img src="https://static001.geekbang.org/resource/image/4c/f8/4c10bb2e9b0f6cb9920d4b1c9418b2f8.jpg" alt="">
很显然,数据分片最大的优势就是缓解缓存节点的存储和访问压力,但同时它也让缓存的使用更加复杂。在MultiGet批量获取场景下单个节点的访问量并没有减少同时节点数太多会造成缓存访问的SLA即“服务等级协议”SLA代表了网站服务可用性得不到很好的保证因为根据木桶原则SLA取决于最慢、最坏的节点的情况节点数过多也会增加出问题的概率**因此我推荐4到6个节点为佳。**
**2.Memcached的主从机制**
Redis本身支持主从的部署方式但是Memcached并不支持所以我们今天主要来了解一下Memcached的主从机制是如何在客户端实现的。
在之前的项目中我就遇到了单个主节点故障导致数据穿透的问题这时我为每一组Master配置一组Slave更新数据时主从同步更新。读取时优先从Slave中读数据如果读取不到数据就穿透到Master读取并且将数据回种到Slave中以保持Slave数据的热度。
主从机制最大的优点就是当某一个Slave宕机时还会有Master作为兜底不会有大量请求穿透到数据库的情况发生提升了缓存系统的高可用性。
<img src="https://static001.geekbang.org/resource/image/54/60/5468eb8779396b38c3731839f3d8d960.jpg" alt="">
**3.多副本**
其实主从方式已经能够解决大部分场景的问题但是对于极端流量的场景下一组Slave通常来说并不能完全承担所有流量Slave网卡带宽可能成为瓶颈。
为了解决这个问题我们考虑在Master/Slave之前增加一层副本层整体架构是这样的
<img src="https://static001.geekbang.org/resource/image/67/03/6779f9b6741b7767068df767218bcd03.jpg" alt="">
在这个方案中当客户端发起查询请求时请求首先会先从多个副本组中选取一个副本组发起查询如果查询失败就继续查询Master/Slave并且将查询的结果回种到所有副本组中避免副本组中脏数据的存在。
基于成本的考虑每一个副本组容量比Master和Slave要小因此它只存储了更加热的数据。在这套架构中Master和Slave的请求量会大大减少为了保证它们存储数据的热度在实践中我们会把Master和Slave作为一组副本组使用。
## 中间代理层方案
虽然客户端方案已经能解决大部分的问题但是只能在单一语言系统之间复用。例如微博使用Java语言实现了这么一套逻辑我使用PHP就难以复用需要重新写一套很麻烦。**而中间代理层的方案就可以解决这个问题。**你可以将客户端解决方案的经验移植到代理层中通过通用的协议如Redis协议来实现在其他语言中的复用。
如果你来自研缓存代理层,你就可以将客户端方案中的高可用逻辑封装在代理层代码里面,这样用户在使用你的代理层的时候就不需要关心缓存的高可用是如何做的,只需要依赖你的代理层就好了。
除此以外业界也有很多中间代理层方案比如Facebook的[Mcrouter](https://github.com/facebook/mcrouter)Twitter的[Twemproxy](https://github.com/twitter/twemproxy),豌豆荚的[Codis](https://github.com/CodisLabs/codis)。它们的原理基本上可以由一张图来概括:
<img src="https://static001.geekbang.org/resource/image/c5/43/c517437faf418e7fa085b1850e3f7343.jpg" alt="">
看这张图你有什么发现吗? 所有缓存的**读写请求**都是经过代理层完成的。代理层是无状态的主要负责读写请求的路由功能并且在其中内置了一些高可用的逻辑不同的开源中间代理层方案中使用的高可用策略各有不同。比如在Twemproxy中Proxy保证在某一个Redis节点挂掉之后会把它从集群中移除后续的请求将由其他节点来完成而Codis的实现略复杂它提供了一个叫Codis Ha的工具来实现自动从节点提主节点在3.2版本之后换做了Redis Sentinel方式从而实现Redis节点的高可用。
## 服务端方案
Redis在2.4版本中提出了Redis Sentinel模式来解决主从Redis部署时的高可用问题它可以在主节点挂了以后自动将从节点提升为主节点保证整体集群的可用性整体的架构如下图所示
<img src="https://static001.geekbang.org/resource/image/94/e1/94ae214f840d2844b7b43751aab6d8e1.jpg" alt="">
Redis Sentinel也是集群部署的这样可以避免Sentinel节点挂掉造成无法自动故障恢复的问题每一个Sentinel节点都是无状态的。在Sentinel中会配置Master的地址Sentinel会时刻监控Master的状态当发现Master在配置的时间间隔内无响应就认为Master已经挂了Sentinel会从从节点中选取一个提升为主节点并且把所有其他的从节点作为新主的从节点。Sentinel集群内部在仲裁的时候会根据配置的值来决定当有几个Sentinel节点认为主挂掉可以做主从切换的操作也就是集群内部需要对缓存节点的状态达成一致才行。
Redis Sentinel不属于代理层模式因为对于缓存的写入和读取请求不会经过Sentinel节点。Sentinel节点在架构上和主从是平级的是作为管理者存在的**所以可以认为是在服务端提供的一种高可用方案。**
## 课程小结
这就是今天分享的全部内容了,我们一起来回顾一下重点:
分布式缓存的高可用方案主要有三种首先是客户端方案一般也称为Smart Client。我们通过制定一些数据分片和数据读写的策略可以实现缓存高可用。这种方案的好处是性能没有损耗缺点是客户端逻辑复杂且在多语言环境下不能复用。
其次中间代理方案在客户端和缓存节点之间增加了中间层在性能上会有一些损耗在代理层会有一些内置的高可用方案比如Codis会使用Codis Ha或者Sentinel。
最后服务端方案依赖于组件的实现Memcached就只支持单机版没有分布式和HA的方案而Redis在2.4版本提供了Sentinel方案可以自动进行主从切换。服务端方案会在运维上增加一些复杂度。
总体而言分布式缓存的三种方案各有所长有些团队可能在开发过程中已经积累了Smart Client上的一些经验而有些团队在Redis运维上经验丰富就可以推进Sentinel方案有些团队在存储研发方面有些积累就可以推进中间代理层方案甚至可以自研适合自己业务场景的代理层组件具体的选择还是要看团队的实际情况而定。
## 一课一思
结合你们过往的经历,我们来聊一聊缓存高可用的重要性吧,比如当缓存可用性下降会造成什么严重问题呢?你们又是如何来保证缓存的高可用的呢?欢迎在留言区与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,161 @@
<audio id="audio" title="15 | 缓存的使用姿势(三):缓存穿透了怎么办?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/dd/c3/dd50de3e145049731e565ffabce74ec3.mp3"></audio>
你好,我是唐扬。
我用三节课的时间带你深入了解了缓存,你应该知道对于缓存来说命中率是它的生命线。
在低缓存命中率的系统中,大量查询商品信息的请求会穿透缓存到数据库,因为数据库对于并发的承受能力是比较脆弱的。一旦数据库承受不了用户大量刷新商品页面、定向搜索衣服信息,查询就会变慢,大量的请求也会阻塞在数据库查询上,造成应用服务器的连接和线程资源被占满,最终导致你的电商系统崩溃。
一般来说我们的核心缓存的命中率要保持在99%以上非核心缓存的命中率也要尽量保证在90%,如果低于这个标准你可能就需要优化缓存的使用方式了。
既然缓存的穿透会带来如此大的影响,那么我们该如何减少它的发生呢?本节课我就带你全面探知面对缓存穿透时,我们到底有哪些应对措施。不过在此之前你需要了解“到底什么是缓存穿透”,只有这样才能更好地考虑如何设计方案解决它。
## 什么是缓存穿透
缓存穿透其实是指从缓存中没有查到数据,而不得不从后端系统(比如数据库)中查询的情况。你可以把数据库比喻为手机,它是经受不了太多的划痕和磕碰的,所以你需要贴个膜再套个保护壳,就能对手机起到一定的保护作用了。
不过少量的缓存穿透不可避免,对系统也是没有损害的,主要有几点原因:
<li>
一方面,互联网系统通常会面临极大数据量的考验,而缓存系统在容量上是有限的,不可能存储系统所有的数据,那么在查询未缓存数据的时候就会发生缓存穿透。
</li>
<li>
另一方面互联网系统的数据访问模型一般会遵从“80/20原则”。“80/20原则”又称为帕累托法则是意大利经济学家帕累托提出的一个经济学的理论。简单来说它是指在一组事物中最重要的部分通常只占20%而其他的80%并没有那么重要。把它应用到数据访问的领域就是我们会经常访问20%的热点数据而另外的80%的数据则不会被经常访问。比如你买了很多衣服,很多书,但是其实经常穿的、经常看的可能也就是其中很小的一部分。
</li>
既然缓存的容量有限并且大部分的访问只会请求20%的热点数据那么理论上说我们只需要在有限的缓存空间里存储20%的热点数据就可以有效地保护脆弱的后端系统了也就可以放弃缓存另外80%的非热点数据了。所以这种少量的缓存穿透是不可避免的,但是对系统是没有损害的。
那么什么样的缓存穿透对系统有害呢?答案是大量的穿透请求超过了后端系统的承受范围造成了后端系统的崩溃。如果把少量的请求比作毛毛细雨,那么一旦变成倾盆大雨,引发洪水,冲倒房屋,肯定就不行了。
产生这种大量穿透请求的场景有很多,接下来我就带你解析这几种场景以及相应的解决方案。
## 缓存穿透的解决方案
先来考虑这样一种场景在你的电商系统的用户表中我们需要通过用户ID查询用户的信息缓存的读写策略采用Cache Aside策略。
那么如果要读取一个用户表中未注册的用户,会发生什么情况呢?按照这个策略,我们会先读缓存再穿透读数据库。由于用户并不存在,所以缓存和数据库中都没有查询到数据,因此也就不会向缓存中回种数据(也就是向缓存中设置值的意思),这样当再次请求这个用户数据的时候还是会再次穿透到数据库。在这种场景下缓存并不能有效地阻挡请求穿透到数据库上,它的作用就微乎其微了。
那如何解决缓存穿透呢?一般来说我们会有两种解决方案:**回种空值以及使用布隆过滤器。**
我们先来看看第一种方案。
### 回种空值
回顾上面提到的场景,你会发现最大的问题在于数据库中并不存在用户的数据,这就造成无论查询多少次数据库中永远都不会存在这个用户的数据,穿透永远都会发生。
**类似的场景还有一些:**比如由于代码的bug导致查询数据库的时候抛出了异常这样可以认为从数据库查询出来的数据为空同样不会回种缓存。
那么,当我们从数据库中查询到空值或者发生异常时,我们可以向缓存中回种一个空值。但是因为空值并不是准确的业务数据,并且会占用缓存的空间,所以我们会给这个空值加一个比较短的过期时间,让空值在短时间之内能够快速过期淘汰。**下面是这个流程的伪代码:**
```
Object nullValue = new Object();
try {
Object valueFromDB = getFromDB(uid); //从数据库中查询数据
if (valueFromDB == null) {
cache.set(uid, nullValue, 10); //如果从数据库中查询到空值,就把空值写入缓存,设置较短的超时时间
} else {
cache.set(uid, valueFromDB, 1000);
}
} catch(Exception e) {
cache.set(uid, nullValue, 10);
}
```
回种空值虽然能够阻挡大量穿透的请求,但如果有大量获取未注册用户信息的请求,缓存内就会有有大量的空值缓存,也就会浪费缓存的存储空间,如果缓存空间被占满了,还会剔除掉一些已经被缓存的用户信息反而会造成缓存命中率的下降。
**所以这个方案,我建议你在使用的时候应该评估一下缓存容量是否能够支撑。**如果需要大量的缓存节点来支持,那么就无法通过通过回种空值的方式来解决,这时你可以考虑使用布隆过滤器。
### 使用布隆过滤器
1970年布隆提出了一种布隆过滤器的算法用来判断一个元素是否在一个集合中。这种算法由一个二进制数组和一个Hash算法组成。**它的基本思路如下:**
我们把集合中的每一个值按照提供的Hash算法算出对应的Hash值然后将Hash值对数组长度取模后得到需要计入数组的索引值并且将数组这个位置的值从0改成1。在判断一个元素是否存在于这个集合中时你只需要将这个元素按照相同的算法计算出索引值如果这个位置的值为1就认为这个元素在集合中否则则认为不在集合中。
下图是布隆过滤器示意图,我来带你分析一下图内的信息。
<img src="https://static001.geekbang.org/resource/image/87/88/873fcbbb19b49a92f490ae2cf3a30e88.jpg" alt="">
A、B、C等元素组成了一个集合元素D计算出的Hash值所对应的的数组中值是1所以可以认为D也在集合中。而F在数组中的值是0所以F不在数组中。
**那么我们如何使用布隆过滤器来解决缓存穿透的问题呢?**
还是以存储用户信息的表为例进行讲解。首先我们初始化一个很大的数组比方说长度为20亿的数组接下来我们选择一个Hash算法然后我们将目前现有的所有用户的ID计算出Hash值并且映射到这个大数组中映射位置的值设置为1其它值设置为0。
新注册的用户除了需要写入到数据库中之外它也需要依照同样的算法更新布隆过滤器的数组中相应位置的值。那么当我们需要查询某一个用户的信息时先查询这个ID在布隆过滤器中是否存在如果不存在就直接返回空值而不需要继续查询数据库和缓存这样就可以极大地减少异常查询带来的缓存穿透。
<img src="https://static001.geekbang.org/resource/image/eb/1a/eb0c5da5deb7e729e719c30fcacd391a.jpg" alt="">
布隆过滤器拥有极高的性能无论是写入操作还是读取操作时间复杂度都是O(1)是常量值。在空间上相对于其他数据结构它也有很大的优势比如20亿的数组需要2000000000/8/1024/1024 = 238M的空间而如果使用数组来存储假设每个用户ID占用4个字节的空间那么存储20亿用户需要2000000000 * 4 / 1024 / 1024 = 7600M的空间是布隆过滤器的32倍。
不过任何事物都有两面性,布隆过滤器也不例外,**它主要有两个缺陷:**
1.它在判断元素是否在集合中时是有一定错误几率的,比如它会把不是集合中的元素判断为处在集合中;
2.不支持删除元素。
**关于第一个缺陷主要是Hash算法的问题。**因为布隆过滤器是由一个二进制数组和一个Hash算法组成的Hash算法存在着一定的碰撞几率。Hash碰撞的含义是不同的输入值经过Hash运算后得到了相同的Hash结果。
本来Hash的含义是不同的输入依据不同的算法映射成独一无二的固定长度的值也就是我输入字符串“1”根据CRC32算法值是2212294583。但是现实中Hash算法的输入值是无限的输出值的值空间却是固定的比如16位的Hash值的值空间是65535那么它的碰撞几率就是1/65535即如果输入值的个数超过65535就一定会发生碰撞。
**你可能会问为什么不映射成更长的Hash值呢**
因为更长的Hash值会带来更高的存储成本和计算成本。即使使用32位的Hash算法它的值空间长度是2的32次幂减一约等于42亿用来映射20亿的用户数据碰撞几率依然有接近50%。
Hash的碰撞就造成了两个用户ID A和B会计算出相同的Hash值那么如果A是注册的用户它的Hash值对应的数组中的值是1那么B即使不是注册用户它在数组中的位置和A是相同的对应的值也是1**这就产生了误判。**
布隆过滤器的误判有一个特点就是它只会出现“false positive”的情况。这是什么意思呢当布隆过滤器判断元素在集合中时这个元素可能不在集合中。但是一旦布隆过滤器判断这个元素不在集合中时它一定不在集合中。**这一点非常适合解决缓存穿透的问题。**为什么呢?
你想,如果布隆过滤器会将集合中的元素判定为不在集合中,那么我们就不确定被布隆过滤器判定为不在集合中的元素是不是在集合中。假设在刚才的场景中,如果有大量查询未注册的用户信息的请求存在,那么这些请求到达布隆过滤器之后,即使布隆过滤器判断为不是注册用户,那么我们也不确定它是不是真的不是注册用户,那么就还是需要去数据库和缓存中查询,这就使布隆过滤器失去了价值。
所以你看,布隆过滤器虽然存在误判的情况,但是还是会减少缓存穿透的情况发生,只是我们需要尽量减少误判的几率,这样布隆过滤器的判断正确的几率更高,对缓存的穿透也更少。**一个解决方案是:**
使用多个Hash算法为元素计算出多个Hash值只有所有Hash值对应的数组中的值都为1时才会认为这个元素在集合中。
**布隆过滤器不支持删除元素的缺陷也和Hash碰撞有关。**给你举一个例子假如两个元素A和B都是集合中的元素它们有相同的Hash值它们就会映射到数组的同一个位置。这时我们删除了A数组中对应位置的值也从1变成0那么在判断B的时候发现值是0也会判断B是不在集合中的元素就会得到错误的结论。
**那么我是怎么解决这个问题的呢?**我会让数组中不再只有0和1两个值而是存储一个计数。比如如果A和B同时命中了一个数组的索引那么这个位置的值就是2如果A被删除了就把这个值从2改为1。这个方案中的数组不再存储bit位而是存储数值也就会增加空间的消耗。**所以,你要依据业务场景来选择是否能够使用布隆过滤器,**比如像是注册用户的场景下,因为用户删除的情况基本不存在,所以还是可以使用布隆过滤器来解决缓存穿透的问题的。
**讲了这么多,关于布隆过滤器的使用上,我也给你几个建议:**
<li>
选择多个Hash函数计算多个Hash值这样可以减少误判的几率
</li>
<li>
布隆过滤器会消耗一定的内存空间,所以在使用时需要评估你的业务场景下需要多大的内存,存储的成本是否可以接受。
</li>
总的来说,**回种空值和布隆过滤器**是解决缓存穿透问题的两种最主要的解决方案但是它们也有各自的适用场景并不能解决所有问题。比方说当有一个极热点的缓存项它一旦失效会有大量请求穿透到数据库这会对数据库造成瞬时极大的压力我们把这个场景叫做“dog-pile effect”狗桩效应
这是典型的缓存并发穿透的问题,**那么,我们如何来解决这个问题呢?**解决狗桩效应的思路是尽量地减少缓存穿透后的并发,方案也比较简单:
<li>
在代码中控制在某一个热点缓存项失效之后启动一个后台线程,穿透到数据库,将数据加载到缓存中,在缓存未加载之前,所有访问这个缓存的请求都不再穿透而直接返回。
</li>
<li>
通过在Memcached或者Redis中设置分布式锁只有获取到锁的请求才能够穿透到数据库。
</li>
分布式锁的方式也比较简单比方说ID为1的用户是一个热点用户当他的用户信息缓存失效后我们需要从数据库中重新加载数据时先向Memcached中写入一个Key为"lock.1"的缓存项然后去数据库里面加载数据当数据加载完成后再把这个Key删掉。这时如果另外一个线程也要请求这个用户的数据它发现缓存中有Key为“lock.1”的缓存,就认为目前已经有线程在加载数据库中的值到缓存中了,它就可以重新去缓存中查询数据,不再穿透数据库了。
## 课程小结
本节课,我带你了解了一些解决缓存穿透的方案,你可以在发现自己的缓存系统命中率下降时从中得到一些借鉴的思路。我想让你明确的重点是:
<li>
回种空值是一种最常见的解决思路,实现起来也最简单,如果评估空值缓存占据的缓存空间可以接受,那么可以优先使用这种方案;
</li>
<li>
布隆过滤器会引入一个新的组件,也会引入一些开发上的复杂度和运维上的成本。所以只有在存在海量查询数据库中,不存在数据的请求时才会使用,在使用时也要关注布隆过滤器对内存空间的消耗;
</li>
<li>
对于极热点缓存数据穿透造成的“狗桩效应”,可以通过设置分布式锁或者后台线程定时加载的方式来解决。
</li>
除此之外,你还需要了解数据库是一个脆弱的资源,它无论是在扩展性、性能还是承担并发的能力上,相比缓存都处于绝对的劣势,所以我们解决缓存穿透问题的**核心目标在于减少对于数据库的并发请求。**了解了这个核心的思想,也许你还会在日常工作中找到其他更好的解决缓存穿透问题的方案。
## 一课一思
在你的日常工作中还会有哪些解决缓存穿透的方案呢?欢迎在留言区和我互动讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,欢迎将它分享给更多的朋友。

View File

@@ -0,0 +1,128 @@
<audio id="audio" title="16 | CDN静态资源如何加速" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/81/50/816e320600717e26e31665eac0c90150.mp3"></audio>
你好,我是唐扬。
前面几节课,我带你了解了缓存的定义以及常用缓存的使用姿势,你应该对包括本地缓存、分布式缓存等缓存组件的适用场景和使用技巧有了一定了解了。结合在[14讲](https://shimo.im/docs/tqDK6cRX9yR8XJyt)中我提到的客户端高可用方案,你会将单个缓存节点扩展为高可用的缓存集群,现在,你的电商系统架构演变成了下面这样:
<img src="https://static001.geekbang.org/resource/image/1a/ba/1aa34cb9f368727399ba32e2891d48ba.jpg" alt="">
在这个架构中我们使用分布式缓存对动态请求数据的读取做了加速,但是在我们的系统中存在着大量的静态资源请求:
<li>
对于移动APP来说这些静态资源主要是图片、视频和流媒体信息
</li>
<li>
对于Web网站来说则包括了JavaScript文件、CSS文件、静态HTML文件等等。
</li>
具体到你的电商系统来说商品的图片、介绍商品使用方法的视频等等静态资源都放在了Nginx等Web服务器上它们的读请求量极大并且对访问速度的要求很高还占据了很高的带宽这时会出现访问速度慢带宽被占满影响动态请求的问题**那么你就需要考虑如何针对这些静态资源进行读加速了。**
## 静态资源加速的考虑点
你可能会问:“我们是否也可以使用分布式缓存来解决这个问题呢?”答案是否定的。一般来说,图片和视频的大小会在几兆到几百兆之间不等,如果我们的应用服务器和分布式缓存都部署在北京的机房里,这时一个杭州的用户要访问缓存中的一个视频,那这个视频文件就需要从北京传输到杭州,期间会经过多个公网骨干网络,延迟很高,会让用户感觉视频打开很慢,严重影响到用户的使用体验。
所以,静态资源访问的关键点是**就近访问,**即北京用户访问北京的数据,杭州用户访问杭州的数据,这样才可以达到性能的最优。
你可能会说:“那我们在杭州也自建一个机房,让用户访问杭州机房的数据就好了呀。”可用户遍布在全国各地,有些应用可能还有国外的用户,我们不可能在每个地域都自建机房,这样成本太高了。
另外,单个视频和图片等静态资源很大,并且访问量又极高,如果使用业务服务器和分布式缓存来承担这些流量,无论是对于内网还是外网的带宽都会是很大的考验。
所以我们考虑在业务服务器的上层增加一层特殊的缓存,用来承担绝大部分对于静态资源的访问,这一层特殊缓存的节点需要遍布在全国各地,这样可以让用户选择最近的节点访问。缓存的命中率也需要一定的保证,尽量减少访问资源存储源站的请求数量(回源请求)。**这一层缓存就是我们这节课的重点CDN。**
## CDN的关键技术
CDNContent Delivery Network/Content Distribution Network内容分发网络。简单来说CDN就是将静态的资源分发到位于多个地理位置机房中的服务器上因此它能很好地解决数据就近访问的问题也就加快了静态资源的访问速度。
在大中型公司里面CDN的应用非常普遍大公司为了提供更稳定的CDN服务会选择自建CDN而大部分公司基于成本的考虑还是会选择专业的CDN厂商网宿、阿里云、腾讯云、蓝汛等等其中网宿和蓝汛是老牌的CDN厂商阿里云和腾讯云是云厂商提供的服务如果你的服务部署在云上可以选择相应云厂商的CDN服务这些CDN厂商都是现今行业内比较主流的。
对于CDN来说你可能已经从运维的口中听说过并且也了解了它的作用。但是当让你来配置CDN或者是排查CDN方面的问题时你就有可能因为不了解它的原理而束手无策了。
所以我先来带你了解一下搭建一个CDN系统需要考虑哪两点
1. 如何将用户的请求映射到CDN节点上
1. 如何根据用户的地理位置信息选择到比较近的节点。
### 如何让用户的请求到达CDN节点
首先我们考虑一下如何让用户的请求到达CDN节点你可能会觉得这很简单啊只需要告诉用户CDN节点的IP地址然后请求这个IP地址上面部署的CDN服务就可以了啊。**但是这样会有一个问题:**就是我们使用的是第三方厂商的CDN服务CDN厂商会给我们一个CDN的节点IP比如说这个IP地址是“111.202.34.130”,那么我们的电商系统中的图片的地址很可能是这样的:“`http://111.202.34.130/1.jpg`”, 这个地址是要存储在数据库中的。
那么如果这个节点IP发生了变更怎么办或者我们如果更改了CDN厂商怎么办是不是要修改所有的商品的url域名呢这就是一个比较大的工作量了。所以我们要做的事情是将第三方厂商提供的IP隐藏起来给到用户的最好是一个本公司域名的子域名。
**那么如何做到这一点呢?**这就需要依靠DNS来帮我们解决域名映射的问题了。
DNSDomain Name System域名系统实际上就是一个存储域名和IP地址对应关系的分布式数据库。而域名解析的结果一般有两种一种叫做“A记录”返回的是域名对应的IP地址另一种是“CNAME记录”返回的是另一个域名也就是说当前域名的解析要跳转到另一个域名的解析上。实际上www.baidu.com 域名的解析结果就是一个CNAME记录域名的解析被跳转到www.a.shifen.com 上了我们正是利用CNAME记录来解决域名映射问题的**具体是怎么解决的呢?我给你举个例子。**
比如你的公司的一级域名叫做example.com那么你可以把你的图片服务的域名定义为“img.example.com”然后将这个域名的解析结果的CNAME配置到CDN提供的域名上比如uclound可能会提供一个域名是“80f21f91.cdn.ucloud.com.cn”这个域名。这样你的电商系统使用的图片地址可以是“`http://img.example.com/1.jpg`”。
用户在请求这个地址时DNS服务器会将域名解析到80f21f91.cdn.ucloud.com.cn域名上然后再将这个域名解析为CDN的节点IP这样就可以得到CDN上面的资源数据了。
**不过这里面有一个问题:**因为域名解析过程是分级的每一级有专门的域名服务器承担解析的职责所以域名的解析过程有可能需要跨越公网做多次DNS查询在性能上是比较差的。
<img src="https://static001.geekbang.org/resource/image/95/96/95d3d6081d8e55860bff6ad0df96c396.jpg" alt="">
从“ 域名分级解析示意图”中你可以看出DNS分为很多种有根DNS顶级DNS等等。除此之外还有两种DNS需要特别留意一种是Local DNS它是由你的运营商提供的DNS一般域名解析的第一站会到这里另一种是权威DNS它的含义是自身数据库中存储了这个域名对应关系的DNS。
下面我以www.baidu.com 这个域名为例给你简单介绍一下域名解析的过程:
<li>
一开始域名解析请求先会检查本机的hosts文件查看是否有www.baidu.com 对应的IP
</li>
<li>
如果没有的话就请求Local DNS是否有域名解析结果的缓存如果有就返回标识是从非权威DNS返回的结果
</li>
<li>
如果没有就开始DNS的迭代查询。先请求根DNS根DNS返回顶级DNS.com的地址再请求.com顶级DNS得到baidu.com的域名服务器地址再从baidu.com的域名服务器中查询到www.baidu.com 对应的IP地址返回这个IP地址的同时标记这个结果是来自于权威DNS的结果同时写入Local DNS的解析结果缓存这样下一次的解析同一个域名就不需要做DNS的迭代查询了。
</li>
经过了向多个DNS服务器做查询之后整个DNS的解析的时间有可能会到秒级别**那么我们如何来解决这个性能问题呢?**
**一个解决的思路是:**在APP启动时对需要解析的域名做预先解析然后把解析的结果缓存到本地的一个LRU缓存里面。这样当我们要使用这个域名的时候只需要从缓存中直接拿到所需要的IP地址就好了如果缓存中不存在才会走整个DNS查询的过程。同时为了避免DNS解析结果的变更造成缓存内数据失效我们可以启动一个定时器定期地更新缓存中的数据。
**我曾经测试过这种方式,**对于HTTP请求的响应时间的提升是很明显的原先DNS解析时间经常会超过1s使用这种方式后DNS解析时间可以控制在200ms之内整个HTTP请求的过程也可以减少大概80ms100ms。
<img src="https://static001.geekbang.org/resource/image/1a/c9/1a692c89b0bcaa8106a8ba045be835c9.jpg" alt="">
**这里总结一下,**将用户的请求映射到CDN服务器上是使用CDN时需要解决的一个核心的问题而CNAME记录在DNS解析过程中可以充当一个中间代理层的角色可以把用户最初使用的域名代理到正确的IP地址上。
<img src="https://static001.geekbang.org/resource/image/4c/59/4c884118fccb7041fdfb4d3e37003f59.jpg" alt="">
现在剩下的一个问题就是如何找到更近的CDN节点了而GSLB承担了这个职责。
### 如何找到离用户最近的CDN节点
GSLBGlobal Server Load Balance全局负载均衡的含义是对于部署在不同地域的服务器之间做负载均衡下面可能管理了很多的本地负载均衡组件。**它有两方面的作用:**
<li>
一方面,它是一种负载均衡服务器,负载均衡,顾名思义嘛,指的是让流量平均分配使得下面管理的服务器的负载更平均;
</li>
<li>
另一方面,它还需要保证流量流经的服务器与流量源头在地缘上是比较接近的。
</li>
GSLB可以通过多种策略来保证返回的CDN节点和用户尽量保证在同一地缘区域比如说可以将用户的IP地址按照地理位置划分为若干个区域然后将CDN节点对应到一个区域上根据用户所在区域来返回合适的节点也可以通过发送数据包测量RTT的方式来决定返回哪一个节点。**不过这些原理不是本节课重点内容,**你了解一下就可以了,我不做详细的介绍。
有了GSLB之后节点的解析过程变成了下图中的样子
<img src="https://static001.geekbang.org/resource/image/fc/01/fcc357ff674b4abdc00dc9c33cbf9a01.jpg" alt="">
**当然是否能够从CDN节点上获取到资源还取决于CDN的同步延时。**一般我们会通过CDN厂商的接口将静态的资源写入到某一个CDN节点上再由CDN内部的同步机制将资源分散同步到每个CDN节点即使CDN内部网络经过了优化这个同步的过程是有延时的一旦我们无法从选定的CDN节点上获取到数据我们就不得不从源站获取数据而用户网络到源站的网络可能会跨越多个主干网这样不仅性能上有损耗也会消耗源站的带宽带来更高的研发成本。所以我们在使用CDN的时候需要关注CDN的命中率和源站的带宽情况。
## 课程小结
本节课我主要带你了解了CDN对静态资源进行加速的原理和使用的核心技术这里你需要了解的重点有以下几点
1.DNS技术是CDN实现中使用的核心技术可以将用户的请求映射到CDN节点上
2.DNS解析结果需要做本地缓存降低DNS解析过程的响应时间
3.GSLB可以给用户返回一个离着他更近的节点加快静态资源的访问速度。
作为一个服务端开发人员你可能会忽略CDN的重要性对于偶尔出现的CDN问题嗤之以鼻觉得这个不是我们应该关心的内容**这种想法是错的。**
CDN是我们系统的门面其缓存的静态数据如图片和视频数据的请求量很可能是接口请求数据的几倍甚至更高一旦发生故障对于整体系统的影响是巨大的。另外CDN的带宽历来是我们研发成本的大头**尤其是目前处于小视频和直播风口上,**大量的小视频和直播研发团队都在绞尽脑汁地减少CDN的成本。由此看出CDN是我们整体系统至关重要的组成部分而它作为一种特殊的缓存其命中率和可用性也是我们服务端开发人员需要重点关注的指标。
## 一课一思
结合今天课程中的内容我们知道CDN的可用性对系统至关重要那么你可以思考一下除了CDN厂商对于SLA的保证之外还有什么方案可以保证CDN的可用性欢迎在留言区和我一起讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,153 @@
<audio id="audio" title="加餐 | 数据的迁移应该如何做?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6a/85/6abe6d8f2261e921f8595a254778b485.mp3"></audio>
你好,我是唐扬。
在“[数据库优化方案(二):写入数据量增加时,如何实现分库分表?](https://time.geekbang.org/column/article/145480)”中我曾经提到由于MySQL不像MongoDB那样支持数据的Auto Sharding自动分片所以无论是将MySQL单库拆分成多个数据库还是由于数据存储的瓶颈不得不将多个数据库拆分成更多的数据库时你都要考虑如何做数据的迁移。
其实在实际工作中,不只是对数据库拆分时会做数据迁移,**很多场景都需要你给出数据迁移的方案,**比如说某一天你的老板想要将应用从自建机房迁移到云上那么你就要考虑将所有自建机房中的数据包括MySQL、Redis、消息队列等组件中的数据全部迁移到云上这无论对哪种规模的公司来说都是一项浩瀚的工程所以你需要在迁移之前准备完善的迁移方案。
“数据的迁移”的问题比较重要和繁琐,也是开发和运维同学关注的重点。在课程更新的过程中,我看到有很多同学,比如@每天晒白牙@枫叶11@撒旦的堕落等等,在留言区询问如何做数据迁移,所以我策划了一期加餐,准备从数据库迁移和缓存迁移两个方面带你掌握数据迁移的方法,也带你了解数据迁移过程中需要注意的关键点,尽量让你避免踩坑。
## 如何平滑地迁移数据库中的数据
你可能会认为数据迁移无非是将数据从一个数据库拷贝到另一个数据库可以通过MySQL 主从同步的方式做到准实时的数据拷贝也可以通过mysqldump工具将源库的数据导出再导入到新库**这有什么复杂的呢?**
其实这两种方式只能支持单库到单库的迁移,无法支持单库到多库多表的场景。而且即便是单库到单库的迁移,迁移过程也需要满足以下几个目标:
<li>
迁移应该是在线的迁移,也就是在迁移的同时还会有数据的写入;
</li>
<li>
数据应该保证完整性,也就是说在迁移之后需要保证新的库和旧的库的数据是一致的;
</li>
<li>
迁移的过程需要做到可以回滚,这样一旦迁移的过程中出现问题,可以立刻回滚到源库不会对系统的可用性造成影响。
</li>
如果你使用Binlog同步的方式在同步完成后再修改代码将主库修改为新的数据库这样就不满足可回滚的要求一旦迁移后发现问题由于已经有增量的数据写入了新库而没有写入旧库不可能再将数据库改成旧库。
一般来说,我们有两种方案可以做数据库的迁移。
#### “双写”方案
第一种方案我称之为双写,其实说起来也很简单,它可以分为以下几个步骤。
<li>
将新的库配置为源库的从库用来同步数据如果需要将数据同步到多库多表那么可以使用一些第三方工具获取Binlog的增量日志比如开源工具Canal在获取增量日志之后就可以按照分库分表的逻辑写入到新的库表中了。
</li>
<li>
同时我们需要改造业务代码,在数据写入的时候不仅要写入旧库也要写入新库。当然,基于性能的考虑,我们可以异步地写入新库,只要保证旧库写入成功即可。**但是我们需要注意的是,**需要将写入新库失败的数据记录在单独的日志中,这样方便后续对这些数据补写,保证新库和旧库的数据一致性。
</li>
<li>
然后我们就可以开始校验数据了。由于数据库中数据量很大,做全量的数据校验不太现实。你可以抽取部分数据,具体数据量依据总体数据量而定,只要保证这些数据是一致的就可以。
</li>
<li>
如果一切顺利,我们就可以将读流量切换到新库了。由于担心一次切换全量读流量可能会对系统产生未知的影响,所以这里**最好采用灰度的方式来切换,**比如开始切换10%的流量如果没有问题再切换到50%的流量最后再切换到100%。
</li>
<li>
由于有双写的存在,所以在切换的过程中出现任何的问题都可以将读写流量随时切换到旧库去,保障系统的性能。
</li>
<li>
在观察了几天发现数据的迁移没有问题之后,就可以将数据库的双写改造成只写新库,数据的迁移也就完成了。
</li>
<img src="https://static001.geekbang.org/resource/image/ad/30/ad9a4aa37afc39ebe0c91144d5ef7630.jpg" alt="">
**其中最容易出问题的步骤就是数据校验的工作,**所以我建议你在未开始迁移数据之前先写好数据校验的工具或者脚本,在测试环境上测试充分之后,再开始正式的数据迁移。
如果是将数据从自建机房迁移到云上,你也可以使用这个方案,**只是你需要考虑的一个重要的因素是:**自建机房到云上的专线的带宽和延迟,你需要尽量减少跨专线的读操作,所以在切换读流量的时候你需要保证自建机房的应用服务器读取本机房的数据库,云上的应用服务器读取云上的数据库。这样在完成迁移之前,只要将自建机房的应用服务器停掉并且将写入流量都切到新库就可以了。
<img src="https://static001.geekbang.org/resource/image/b8/54/b88aefdb07049f2019c922cdb9cb3154.jpg" alt="">
这种方案是一种比较通用的方案无论是迁移MySQL中的数据还是迁移Redis中的数据甚至迁移消息队列都可以使用这种方式**你在实际的工作中可以直接拿来使用。**
这种方式的好处是:迁移的过程可以随时回滚,将迁移的风险降到了最低。劣势是:时间周期比较长,应用有改造的成本。
#### 级联同步方案
这种方案也比较简单,比较适合数据从自建机房向云上迁移的场景。因为迁移上云最担心云上的环境和自建机房的环境不一致,会导致数据库在云上运行时因为参数配置或者硬件环境不同出现问题。
所以我们会在自建机房准备一个备库,在云上环境上准备一个新库,通过级联同步的方式在自建机房留下一个可回滚的数据库,具体的步骤如下:
1. 先将新库配置为旧库的从库,用作数据同步;
1. 再将一个备库配置为新库的从库,用作数据的备份;
1. 等到三个库的写入一致后,将数据库的读流量切换到新库;
1. 然后暂停应用的写入,将业务的写入流量切换到新库(由于这里需要暂停应用的写入,所以需要安排在业务的低峰期)。
<img src="https://static001.geekbang.org/resource/image/3a/2b/3a2e08181177529c3229c789c2081b2b.jpg" alt="">
**这种方案的回滚方案也比较简单,**可以先将读流量切换到备库再暂停应用的写入,将写流量切换到备库,这样所有的流量都切换到了备库,也就是又回到了自建机房的环境,就可以认为已经回滚了。
<img src="https://static001.geekbang.org/resource/image/ad/b9/ada8866fda3c3264f495c97c6214ebb9.jpg" alt="">
上面的级联迁移方案可以应用在将MySQL从自建机房迁移到云上的场景也可以应用在将Redis从自建机房迁移到云上的场景**如果你有类似的需求可以直接拿来应用。**
这种方案**优势是**简单易实施,在业务上基本没有改造的成本;**缺点是**在切写的时候需要短暂的停止写入,对于业务来说是有损的,不过如果在业务低峰期来执行切写,可以将对业务的影响降至最低。
## 数据迁移时如何预热缓存
另外,在从自建机房向云上迁移数据时,我们也需要考虑缓存的迁移方案是怎样的。那么你可能会说:缓存本来就是作为一个中间的存储而存在的,我只需要在云上部署一个空的缓存节点,云上的请求也会穿透到云上的数据库,然后回种缓存,对于业务是没有影响的。
你说得没错,但是你还需要考虑的是缓存的命中率。
如果你部署一个空的缓存,那么所有的请求就都穿透到数据库,数据库可能因为承受不了这么大的压力而宕机,这样你的服务就会不可用了。**所以,缓存迁移的重点是保持缓存的热度。**
刚刚我提到Redis的数据迁移可以使用双写的方案或者级联同步的方案所以在这里我就不考虑Redis缓存的同步了而是以Memcached为例来说明。
#### 使用副本组预热缓存
在“[缓存的使用姿势(二):缓存如何做到高可用?](https://time.geekbang.org/column/article/151949)”中,我曾经提到为了保证缓存的可用性,我们可以部署多个副本组来尽量将请求阻挡在数据库层之上。
数据的写入流程是写入Master、Slave和所有的副本组而在读取数据的时候会先读副本组的数据如果读取不到再到Master和Slave里面加载数据再写入到副本组中。**那么,我们就可以在云上部署一个副本组,**这样,云上的应用服务器读取云上的副本组,如果副本组没有查询到数据,就可以从自建机房部署的主从缓存上加载数据,回种到云上的副本组上。
<img src="https://static001.geekbang.org/resource/image/ab/c6/abc0b5e4c80097d8e02000b30e7ea9c6.jpg" alt="">
当云上部署的副本组足够热之后也就是缓存的命中率达到至少90%,就可以将云机房上的缓存服务器的主从都指向这个副本组,这时迁移也就完成了。
**这种方式足够简单,不过有一个致命的问题是:**如果云上的请求穿透云上的副本组,到达自建机房的主从缓存时,这个过程是需要跨越专线的。
这不仅会占用较多专线的带宽同时专线的延迟相比于缓存的读取时间是比较大的即使是本地的不同机房之间的延迟也会达到2ms3ms那么一次前端请求可能会访问十几次甚至几十次的缓存一次请求就会平白增加几十毫秒甚至过百毫秒的延迟会极大地影响接口的响应时间因此在实际项目中我们很少使用这种方案。
**但是这种方案给了我们思路,**让我们可以通过方案的设计在系统运行中自动完成缓存的预热,所以我们对副本组的方案做了一些改造,以尽量减少对专线带宽的占用。
#### 改造副本组方案预热缓存
改造后的方案对读写缓存的方式进行改造,步骤是这样的:
<li>
在云上部署多组mc的副本组自建机房在接收到写入请求时会优先写入自建机房的缓存节点异步写入云上部署的mc节点
</li>
<li>
在处理自建机房的读请求时会指定一定的流量比如10%)优先走云上的缓存节点,这样虽然也会走专线穿透回自建机房的缓存节点,但是流量是可控的;
</li>
<li>
当云上缓存节点的命中率达到90%以上时,就可以在云上部署应用服务器,让云上的应用服务器完全走云上的缓存节点就可以了。
</li>
<img src="https://static001.geekbang.org/resource/image/7f/f4/7f41a529a322e396232ac7963ec082f4.jpg" alt="">
使用了这种方式,我们可以实现缓存数据的迁移,又可以尽量控制专线的带宽和请求的延迟情况,**你也可以直接在项目中使用。**
## 课程小结
以上我提到的数据迁移的方案,都是我在实际项目中经常用到的、经受过实战考验的方案,希望你能通过这节课的学习,将这些方案运用到你的项目中解决实际的问题。与此同时,我想再次跟你强调一下本节课的重点内容:
<li>
双写的方案是数据库、Redis迁移的通用方案**你可以在实际工作中直接加以使用。**双写方案中最重要的,是通过数据校验来保证数据的一致性,这样就可以在迁移过程中随时回滚;
</li>
<li>
如果你需要将自建机房的数据迁移到云上,那么也可以考虑**使用级联复制的方案,**这种方案会造成数据的短暂停写,需要在业务低峰期执行;
</li>
<li>
缓存的迁移重点是保证云上缓存的命中率,你可以**使用改进版的副本组方式来迁移,**在缓存写入的时候异步写入云上的副本组,在读取时放少量流量到云上副本组,从而又可以迁移部分数据到云上副本组,又能尽量减少穿透给自建机房造成专线延迟的问题。
</li>
**如果你作为项目的负责人,**那么在迁移的过程中,你一定要制定周密的计划:如果是数据库的迁移,那么数据的校验应该是你最需要花费时间来解决的问题。
如果是自建机房迁移到云上,那么专线的带宽一定是你迁移过程中的一个瓶颈点,你需要在迁移之前梳理清楚有哪些调用需要经过专线,占用带宽的情况是怎样的,带宽的延时是否能够满足要求。你的方案中也需要尽量做到在迁移过程中同机房的服务调用同机房的缓存和数据库,尽量减少对于专线带宽资源的占用。
## 一课一思
结合实际工作的经验,你可以和我分享一下在做数据迁移的时候都采用了哪些方案吗?这些方案你觉得它的优势和劣势分别是什么呢?
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。