通过前面数据库篇的学习,你已经了解了在高并发大流量下,数据库层的演进过程以及库表

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

这个思路会造成缓存和数据库中的数据不一致。比如,A 请求将数据库中 ID 为 1 的用户年
龄从 19 变更为 20,与此同时,请求 B 也开始更新 ID 为 1 的用户数据,它把数据库中记
录的年龄变更为 21,然后变更缓存中的用户年龄为 21。紧接着,A 请求开始更新缓存数
据,它会把缓存中的年龄变更为 20。此时,数据库中用户年龄是 21,而缓存中的用户年龄
却是 20。
为什么产生这个问题呢?因为变更数据库和变更缓存是两个独立的操作,而我们并没有对操
作做任何的并发控制。那么当两个线程并发更新它们的时候,就会因为写入顺序的不同造成
数据的不一致。
另外,直接更新缓存还存在另外一个问题就是丢失更新。还是以我们的电商系统为例,假如
电商系统中的账户表有三个字段:ID、户名和金额,这个时候缓存中存储的就不只是金额
信息,而是完整的账户信息了。当更新缓存中账户金额时,你需要从缓存中查询完整的账户
数据,把金额变更后再写入到缓存中。
这个过程中也会有并发的问题,比如说原有金额是 20,A 请求从缓存中读到数据,并且把
金额加 1,变更成 21,在未写入缓存之前又有请求 B 也读到缓存的数据后把金额也加 1,
也变更成 21,两个请求同时把金额写回缓存,这时缓存里面的金额是 21,但是我们实际上
预期是金额数加 2,这也是一个比较大的问题。
那我们要如何解决这个问题呢?其实,我们可以在更新数据时不更新缓存,而是删除缓存中
的数据,在读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存
中。

这个策略就是我们使用缓存最常见的策略,Cache Aside 策略(也叫旁路缓存策略),这
个策略数据以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读策略和写策
略,其中读策略的步骤是:
从缓存中读取数据;
如果缓存命中,则直接返回数据;
如果缓存不命中,则从数据库中查询数据;
查询到数据后,将数据写入到缓存中,并且返回给用户。
写策略的步骤是:
更新数据库中的记录;
删除缓存记录。
你也许会问了,在写策略中,能否先删除缓存,后更新数据库呢?答案是不行的,因为这样
也有可能出现缓存数据不一致的问题,我以用户表的场景为例解释一下。
假设某个用户的年龄是 20,请求 A 要更新用户年龄为 21,所以它会删除缓存中的内容。
这时,另一个请求 B 要读取这个用户的年龄,它查询缓存发现未命中后,会从数据库中读取到年龄为 20,并且写入到缓存中,然后请求 A 继续更改数据库,将用户的年龄更新为
21,这就造成了缓存和数据库的不一致。

不过这种问题出现的几率并不高,原因是缓存的写入通常远远快于数据库的写入,所以在实
际中很难出现请求 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 策略的示意图:
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 策略的示意图放在了下面:
如果使用 Write Back 策略的话,读的策略也有一些变化了。我们在读取缓存时如果发现
缓存命中则直接返回缓存数据。如果缓存不命中则寻找一个可用的缓存块儿,如果这个缓存
块儿是“脏”的,就把缓存块儿中之前的数据写入到后端存储中,并且从后端存储加载数据
到缓存块儿,如果不是脏的,则由缓存组件将后端存储中的数据加载到缓存中,最后我们将
缓存设置为不是脏的,返回数据就好了。
发现了吗?其实这种策略不能被应用到我们常用的数据库和缓存的场景中,它是计算机体系
结构中的设计,比如我们在向磁盘中写数据时采用的就是这种策略。无论是操作系统层面的
Page Cache,还是日志的异步刷盘,亦或是消息队列中消息的异步写入磁盘,大多采用了
这种策略。因为这个策略在性能上的优势毋庸置疑,它避免了直接写磁盘造成的随机写问
题,毕竟写内存和写磁盘的随机 I/O 的延迟相差了几个数量级呢。
但因为缓存一般使用内存,而内存是非持久化的,所以一旦缓存机器掉电,就会造成原本缓
存中的脏块儿数据丢失。所以你会发现系统在掉电之后,之前写入的文件会有部分丢失,就
是因为 Page Cache 还没有来得及刷盘造成的。
当然,你依然可以在一些场景下使用这个策略,在使用时,我想给你的落地建议是:你在向
低速设备写入数据的时候,可以在内存里先暂存一段时间的数据,甚至做一些统计汇总,然
后定时地刷新到低速设备上。比如说,你在统计你的接口响应时间的时候,需要将每次请求的响应时间打印到日志中,然后监控系统收集日志后再做统计。但是如果每次请求都打印日
志无疑会增加磁盘 I/O,那么不如把一段时间的响应时间暂存起来,经过简单的统计平均耗
时,每个耗时区间的请求数量等等,然后定时地,批量地打印到日志中。
课程小结
本节课,我主要带你了解了缓存使用的几种策略,以及每种策略适用的使用场景是怎样的。
我想让你掌握的重点是:
1.Cache Aside 是我们在使用分布式缓存时最常用的策略,你可以在实际工作中直接拿来使
用。
2.Read/Write Through 和 Write Back 策略需要缓存组件的支持,所以比较适合你在实现
本地缓存组件的时候使用;
3.Write Back 策略是计算机体系结构中的策略,不过写入策略中的只写缓存,异步写入后
端存储的策略倒是有很多的应用场景。
而且,你还需要了解,我们今天提到的策略都是标准的使用姿势,在实际开发过程中需要结
合实际的业务特点灵活使用甚至加以改造。这些业务特点包括但不仅限于:整体的数据量级
情况,访问的读写比例的情况,对于数据的不一致时间的容忍度,对于缓存命中率的要求等
等。理论结合实践,具体情况具体分析,你才能得到更好的解决方案。
14 | 缓存的使用姿势(二):缓存如何做到高可用?
前面几节课,我带你了解了缓存的原理、分类以及常用缓存的使用技巧。我们开始用缓存承
担大部分的读压力,从而缓解数据库的查询压力,在提升性能的同时保证系统的稳定性。这
时,你的电商系统整体的架构演变成下图的样子:

我们在 Web 层和数据库层之间增加了缓存层,请求会首先查询缓存,只有当缓存中没有需
要的数据时才会查询数据库。
在这里,你需要关注缓存命中率这个指标(缓存命中率 = 命中缓存的请求数 / 总请求
数)。一般来说,在你的电商系统中,核心缓存的命中率需要维持在 99% 甚至是 99.9%,
哪怕下降 1%,系统都会遭受毁灭性的打击。
这绝不是危言耸听,我们来计算一下。假设系统的 QPS 是 10000/s,每次调用会访问 10
次缓存或者数据库中的数据,那么当缓存命中率仅仅减少 1%,数据库每秒就会增加 10000
* 10 * 1% = 1000 次请求。而一般来说我们单个 MySQL 节点的读请求量峰值就在 1500/s
左右,增加的这 1000 次请求很可能会给数据库造成极大的冲击。
命中率仅仅下降 1% 造成的影响就如此可怕,更不要说缓存节点故障了。而图中单点部署
的缓存节点就成了整体系统中最大的隐患,那我们要如何来解决这个问题,提升缓存的可用
性呢?
我们可以通过部署多个节点,同时设计一些方案让这些节点互为备份。这样,当某个节点故
障时,它的备份节点可以顶替它继续提供服务。而这些方案就是我们本节课的重点:分布式
缓存的高可用方案。
在我的项目中,我主要选择的方案有客户端方案、中间代理层方案和服务端方案三大类:客户端方案就是在客户端配置多个缓存的节点,通过缓存写入和读取算法策略来实现分布
式,从而提高缓存的可用性。
中间代理层方案是在应用代码和缓存节点之间增加代理层,客户端所有的写入和读取的请
求都通过代理层,而代理层中会内置高可用策略,帮助提升缓存系统的高可用。
服务端方案就是 Redis 2.4 版本后提出的 Redis Sentinel 方案。
掌握这些方案可以帮助你,抵御部分缓存节点故障导致的,缓存命中率下降的影响,增强你
的系统的鲁棒性。
客户端方案
在客户端方案中,你需要关注缓存的写和读两个方面:
写入数据时,需要把被写入缓存的数据分散到多个节点中,即进行数据分片;
读数据时,可以利用多组的缓存来做容错,提升缓存系统的可用性。关于读数据,这里可
以使用主从和多副本两种策略,两种策略是为了解决不同的问题而提出的。
下面我就带你一起详细地看一下到底要怎么做。
1. 缓存数据如何分片
单一的缓存节点受到机器内存、网卡带宽和单节点请求量的限制,不能承担比较高的并发,
因此我们考虑将数据分片,依照分片算法将数据打散到多个不同的节点上,每个节点上存储
部分数据。
这样在某个节点故障的情况下,其他节点也可以提供服务,保证了一定的可用性。这就好比
不要把鸡蛋放在同一个篮子里,这样一旦一个篮子掉在地上,摔碎了,别的篮子里还有没摔
碎的鸡蛋,不至于一个不剩。
一般来讲,分片算法常见的就是 Hash 分片算法和一致性 Hash 分片算法两种。
Hash 分片的算法就是对缓存的 Key 做哈希计算,然后对总的缓存节点个数取余。你可以
这么理解:比如说,我们部署了三个缓存节点组成一个缓存的集群,当有新的数据要写入时,我们先对
这个缓存的 Key 做比如 crc32 等 Hash 算法生成 Hash 值,然后对 Hash 值模 3,得出的
结果就是要存入缓存节点的序号。

这个算法最大的优点就是简单易理解,缺点是当增加或者减少缓存节点时,缓存总的节点
个数变化造成计算出来的节点发生变化,从而造成缓存失效不可用。所以我建议你,如果采
用这种方法,最好建立在你对于这组缓存命中率下降不敏感,比如下面还有另外一层缓存来
兜底的情况下。
当然了,用一致性 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 中。
这时如果在 Node 1 和 Node 2 之间增加一个 Node 5,你可以看到原本命中 Node 2 的
Key 3 现在命中到 Node 5,而其它的 Key 都没有变化;同样的道理,如果我们把 Node 3
从集群中移除,那么只会影响到 Key 5 。所以你看,在增加和删除节点时,只有少量的
Key 会“漂移”到其它节点上,而大部分的 Key 命中的节点还是会保持不变,从而可以保
证命中率不会大幅下降。
不过,事物总有两面性。虽然这个算法对命中率的影响比较小,但它还是存在问题:
缓存节点在圆环上分布不平均,会造成部分缓存节点的压力较大;当某个节点故障时,这
个节点所要承担的所有访问都会被顺移到另一个节点上,会对后面这个节点造成压力。
一致性 Hash 算法的脏数据问题。
极端情况下,比如一个有三个节点 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 算法时一定要设置缓存的过期时间,这样当发生漂移时,之前
存储的脏数据可能已经过期,就可以减少存在脏数据的几率。
很显然,数据分片最大的优势就是缓解缓存节点的存储和访问压力,但同时它 也让缓存的
使用 更加复杂。在 MultiGet(批量获取)场景下,单个节点的访问量并没有减少,同时
节点数太多会造成缓存访问的 SLA(即“服务等级协议”,SLA 代表了网站服务可用性)
得不到很好的保证,因为根据木桶原则,SLA 取决于最慢、最坏的节点的情况,节点数过
多也会增加出问题的概率,因此我推荐 4 到 6 个节点为佳。
2.Memcached 的主从机制
Redis 本身支持主从的部署方式,但是 Memcached 并不支持,所以我们今天主要来了解
一下 Memcached 的主从机制是如何在客户端实现的。
在之前的项目中,我就遇到了单个主节点故障导致数据穿透的问题,这时我为每一组
Master 配置一组 Slave,更新数据时主从同步更新。读取时,优先从 Slave 中读数据,如果读取不到数据就穿透到 Master 读取,并且将数据回种到 Slave 中以保持 Slave 数据的
热度。
主从机制最大的优点就是当某一个 Slave 宕机时,还会有 Master 作为兜底,不会有大量请
求穿透到数据库的情况发生,提升了缓存系统的高可用性。
3. 多副本
其实,主从方式已经能够解决大部分场景的问题,但是对于极端流量的场景下,一组 Slave
通常来说并不能完全承担所有流量,Slave 网卡带宽可能成为瓶颈。
为了解决这个问题,我们考虑在 Master/Slave 之前增加一层副本层,整体架构是这样的:
在这个方案中,当客户端发起查询请求时,请求首先会先从多个副本组中选取一个副本组发
起查询,如果查询失败,就继续查询 Master/Slave,并且将查询的结果回种到所有副本组
中,避免副本组中脏数据的存在。
基于成本的考虑,每一个副本组容量比 Master 和 Slave 要小,因此它只存储了更加热的数
据。在这套架构中,Master 和 Slave 的请求量会大大减少,为了保证它们存储数据的热
度,在实践中我们会把 Master 和 Slave 作为一组副本组使用。
中间代理层方案
虽然客户端方案已经能解决大部分的问题,但是只能在单一语言系统之间复用。例如微博使
用 Java 语言实现了这么一套逻辑,我使用 PHP 就难以复用,需要重新写一套,很麻烦。
而中间代理层的方案就可以解决这个问题。你可以将客户端解决方案的经验移植到代理层
中,通过通用的协议(如 Redis 协议)来实现在其他语言中的复用。
如果你来自研缓存代理层,你就可以将客户端方案中的高可用逻辑封装在代理层代码里面,
这样用户在使用你的代理层的时候就不需要关心缓存的高可用是如何做的,只需要依赖你的
代理层就好了。
除此以外,业界也有很多中间代理层方案,比如 Facebook 的Mcrouter,Twitter 的
Twemproxy,豌豆荚的Codis。它们的原理基本上可以由一张图来概括:
看这张图你有什么发现吗? 所有缓存的读写请求都是经过代理层完成的。代理层是无状态
的,主要负责读写请求的路由功能,并且在其中内置了一些高可用的逻辑,不同的开源中间
代理层方案中使用的高可用策略各有不同。比如在 Twemproxy 中,Proxy 保证在某一个
Redis 节点挂掉之后会把它从集群中移除,后续的请求将由其他节点来完成;而 Codis 的
实现略复杂,它提供了一个叫 Codis Ha 的工具来实现自动从节点提主节点,在 3.2 版本之
后换做了 Redis Sentinel 方式,从而实现 Redis 节点的高可用。
服务端方案
Redis 在 2.4 版本中提出了 Redis Sentinel 模式来解决主从 Redis 部署时的高可用问题,
它可以在主节点挂了以后自动将从节点提升为主节点,保证整体集群的可用性,整体的架构
如下图所示:
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
方案;有些团队在存储研发方面有些积累,就可以推进中间代理层方案,甚至可以自研适合
自己业务场景的代理层组件,具体的选择还是要看团队的实际情况而定。
15 | 缓存的使用姿势(三):缓存穿透了怎么办?
我用三节课的时间,带你深入了解了缓存,你应该知道,对于缓存来说,命中率是它的生命
线。
在低缓存命中率的系统中,大量查询商品信息的请求会穿透缓存到数据库,因为数据库对于
并发的承受能力是比较脆弱的。一旦数据库承受不了用户大量刷新商品页面、定向搜索衣服
信息,就会导致查询变慢,导致大量的请求阻塞在数据库查询上,造成应用服务器的连接和
线程资源被占满,最终导致你的电商系统崩溃。
一般来说,我们的核心缓存的命中率要保持在 99% 以上,非核心缓存的命中率也要尽量保
证在 90%,如果低于这个标准,那么你可能就需要优化缓存的使用方式了。既然缓存的穿透会带来如此大的影响,那么我们该如何减少它的发生呢?本节课,我就带你
全面探知,面对缓存穿透时,我们到底有哪些应对措施。不过在此之前,你需要了解“到底
什么是缓存穿透”,只有这样,才能更好地考虑如何设计方案解决它。
什么是缓存穿透
缓存穿透其实是指从缓存中没有查到数据,而不得不从后端系统(比如数据库)中查询的情
况。你可以把数据库比喻为手机,它是经受不了太多的划痕和磕碰的,所以你需要给它贴个
膜再套个保护壳,就能对手机起到一定的保护作用了。
不过,少量的缓存穿透不可避免,对系统也是没有损害的,主要有几点原因:
一方面,互联网系统通常会面临极大数据量的考验,而缓存系统在容量上是有限的,不可
能存储系统所有的数据,那么在查询未缓存数据的时候就会发生缓存穿透。
另一方面,互联网系统的数据访问模型一般会遵从“80/20 原则”。“80/20 原则”又
称为帕累托法则,是意大利经济学家帕累托提出的一个经济学的理论。它是指在一组事物
中,最重要的事物通常只占 20%,而剩余的 80% 的事物确实不重要的。把它应用到数
据访问的领域,就是我们会经常访问 20% 的热点数据,而另外的 80% 的数据则不会被
经常访问。比如你买了很多衣服,很多书,但是其实经常穿的,经常看的,可能也就是其
中很小的一部分。
既然缓存的容量有限,并且大部分的访问只会请求 20% 的热点数据,那么理论上说,我们
只需要在有限的缓存空间里存储 20% 的热点数据就可以有效地保护脆弱的后端系统了,也
就可以放弃缓存另外 80% 的非热点数据了。所以,这种少量的缓存穿透是不可避免的,但
是对系统是没有损害的。
那么什么样的缓存穿透对系统有害呢?答案是大量的穿透请求超过了后端系统的承受范围,
造成了后端系统的崩溃。如果把少量的请求比作毛毛细雨,那么一旦变成倾盆大雨,引发洪
水,冲倒房屋,肯定就不行了。
产生这种大量穿透请求的场景有很多,接下来,我就带你解析这几种场景以及相应的解决方
案。
缓存穿透的解决方案先来考虑这样一种场景:在你的电商系统的用户表中,我们需要通过用户 ID 查询用户的信
息,缓存的读写策略采用 Cache Aside 策略。
那么,如果要读取一个用户表中未注册的用户,会发生什么情况呢?按照这个策略,我们会
先读缓存,再穿透读数据库。由于用户并不存在,所以缓存和数据库中都没有查询到数据,
因此也就不会向缓存中回种数据(也就是向缓存中设置值的意思),这样当再次请求这个用
户数据的时候还是会再次穿透到数据库。在这种场景下,缓存并不能有效地阻挡请求穿透到
数据库上,它的作用就微乎其微了。
那如何解决缓存穿透呢?一般来说我们会有两种解决方案:回种空值以及使用布隆过滤器。
我们先来看看第一种方案。
回种空值
回顾上面提到的场景,你会发现最大的问题在于数据库中并不存在用户的数据,这就造成无
论查询多少次,数据库中永远都不会存在这个用户的数据,穿透永远都会发生。
类似的场景还有一些:比如由于代码的 bug 导致查询数据库的时候抛出了异常,这样可以
认为从数据库查询出来的数据为空,同样不会回种缓存。
那么,当我们从数据库中查询到空值或者发生异常时,我们可以向缓存中回种一个空值。但
是因为空值并不是准确的业务数据,并且会占用缓存的空间,所以我们会给这个空值加一个
比较短的过期时间,让空值在短时间之内能够快速过期淘汰。下面是这个流程的伪代码:
 复制代码
1 Object nullValue = new Object();
2 try {
3 Object valueFromDB = getFromDB(uid); // 从数据库中查询数据
4 if (valueFromDB == null) {
5 cache.set(uid, nullValue, 10); // 如果从数据库中查询到空值,就把空值写入缓存,设置较短
6 } else {
7 cache.set(uid, valueFromDB, 1000);
8 }
9 } catch(Exception e) {
10 cache.set(uid, nullValue, 10);
11 }回种空值虽然能够阻挡大量穿透的请求,但如果有大量获取未注册用户信息的请求,缓存内
就会有有大量的空值缓存,也就会浪费缓存的存储空间,如果缓存空间被占满了,还会剔除
掉一些已经被缓存的用户信息反而会造成缓存命中率的下降。
所以这个方案,我建议你在使用的时候应该评估一下缓存容量是否能够支撑。如果需要大量
的缓存节点来支持,那么就无法通过通过回种空值的方式来解决,这时你可以考虑使用布隆
过滤器。
使用布隆过滤器
1970 年布隆提出了一种布隆过滤器的算法,用来判断一个元素是否在一个集合中。这种算
法由一个二进制数组和一个 Hash 算法组成。它的基本思路如下:
我们把集合中的每一个值按照提供的 Hash 算法算出对应的 Hash 值,然后将 Hash 值对数
组长度取模后得到需要计入数组的索引值,并且将数组这个位置的值从 0 改成 1。在判断
一个元素是否存在于这个集合中时,你只需要将这个元素按照相同的算法计算出索引值,如
果这个位置的值为 1 就认为这个元素在集合中,否则则认为不在集合中。
下图是布隆过滤器示意图,我来带你分析一下图内的信息。

A、B、C 等元素组成了一个集合,元素 D 计算出的 Hash 值所对应的的数组中值是 1,所
以可以认为 D 也在集合中。而 F 在数组中的值是 0,所以 F 不在数组中。
那么我们如何使用布隆过滤器来解决缓存穿透的问题呢?还是以存储用户信息的表为例进行讲解。首先,我们初始化一个很大的数组,比方说长度为
20 亿的数组,接下来我们选择一个 Hash 算法,然后我们将目前现有的所有用户的 ID 计
算出 Hash 值并且映射到这个大数组中,映射位置的值设置为 1,其它值设置为 0。
新注册的用户除了需要写入到数据库中之外,它也需要依照同样的算法更新布隆过滤器的数
组中,相应位置的值。那么当我们需要查询某一个用户的信息时,我们首先查询这个 ID 在
布隆过滤器中是否存在,如果不存在就直接返回空值,而不需要继续查询数据库和缓存,这
样就可以极大地减少异常查询带来的缓存穿透。

Version:0.9 StartHTML:0000000105 EndHTML:0000019465 StartFragment:0000000141 EndFragment:0000019425

布隆过滤器拥有极高的性能,无论是写入操作还是读取操作,时间复杂度都是 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 位,而是存储数值,也就会增
加空间的消耗。所以,你要依据业务场景来选择是否能够使用布隆过滤器,比如像是注册用
户的场景下,因为用户删除的情况基本不存在,所以还是可以使用布隆过滤器来解决缓存穿
透的问题的。
讲了这么多,关于布隆过滤器的使用上,我也给你几个建议:
1. 选择多个 Hash 函数计算多个 Hash 值,这样可以减少误判的几率;
2. 布隆过滤器会消耗一定的内存空间,所以在使用时需要评估你的业务场景下需要多大的
内存,存储的成本是否可以接受。
总的来说,回种空值和布隆过滤器是解决缓存穿透问题的两种最主要的解决方案,但是它们
也有各自的适用场景,并不能解决所有问题。比方说当有一个极热点的缓存项,它一旦失效
会有大量请求穿透到数据库,这会对数据库造成瞬时极大的压力,我们把这个场景叫
做“dog-pile effect”(狗桩效应),
这是典型的缓存并发穿透的问题,那么,我们如何来解决这个问题呢?解决狗桩效应的思路
是尽量地减少缓存穿透后的并发,方案也比较简单:
1. 在代码中,控制在某一个热点缓存项失效之后启动一个后台线程,穿透到数据库,将数
据加载到缓存中,在缓存未加载之前,所有访问这个缓存的请求都不再穿透而直接返回。
2. 通过在 Memcached 或者 Redis 中设置分布式锁,只有获取到锁的请求才能够穿透到数
据库。分布式锁的方式也比较简单,比方说 ID 为 1 的用户是一个热点用户,当他的用户信息缓存
失效后,我们需要从数据库中重新加载数据时,先向 Memcached 中写入一个 Key
为"lock.1"的缓存项,然后去数据库里面加载数据,当数据加载完成后再把这个 Key 删掉。
这时,如果另外一个线程也要请求这个用户的数据,它发现缓存中有 Key 为“lock.1”的
缓存,就认为目前已经有线程在加载数据库中的值到缓存中了,它就可以重新去缓存中查询
数据,不再穿透数据库了。
课程小结
本节课,我带你了解了一些解决缓存穿透的方案,你可以在发现自己的缓存系统命中率下降
时,从中得到一些借鉴的思路。我想让你明确的重点是:
1. 回种空值是一种最常见的解决思路,实现起来也最简单,如果评估空值缓存占据的缓存
空间可以接受,那么可以优先使用这种方案;
2. 布隆过滤器会引入一个新的组件,也会引入一些开发上的复杂度和运维上的成本。所以
只有在存在海量查询数据库中,不存在数据的请求时才会使用,在使用时也要关注布隆过滤
器对内存空间的消耗;
3. 对于极热点缓存数据穿透造成的“狗桩效应”,可以通过设置分布式锁或者后台线程定
时加载的方式来解决。
除此之外,你还需要了解的是,数据库是一个脆弱的资源,它无论是在扩展性、性能还是承
担并发的能力上,相比缓存都处于绝对的劣势,所以我们解决缓存穿透问题的核心目标在于
减少对于数据库的并发请求。了解了这个核心的思想,也许你还会在日常工作中找到其他更
好的解决缓存穿透问题的方案。

12 | 缓存:数据库成为瓶颈后,动态数据的查询要如何加速?-学习笔记相关推荐

  1. 『ML』利用K-Means聚类算法对未标注数据分组——《机器学习实战》学习笔记(Ch10)

    本节用Python实现K-Means算法,对未标注的数据进行聚类. 在做完聚类后,如何对于聚类结果进行评估?请看 用Python实现聚类效果的评估(轮廓系数.互信息) 导航 K-Means简介 代码实 ...

  2. mysql执行动态说起来_MySQL实战45讲学习笔记:第十四讲

    一.引子 在开发系统的时候,你可能经常需要计算一个表的行数,比如一个交易系统的所有变更记录总数.这时候你可能会想,一条 select count(*) from t 语句不就解决了吗? 但是,你会发现 ...

  3. 支付卡行业数据安全标准(PCI DSS)学习笔记

    PCI DSS 支付卡行业数据安全标准共有6个部分,分别是建立并维护安全的网络和系统(为数据构建安全的数据环境),保护持卡人数据(通过数据加密提高数据存储和传输安全性),维护漏洞管理计划(开发安全的软 ...

  4. 数据库查询求小于_SQL学习笔记(二)简单查询

    本篇主要学习如何通过简单查询获取想要的数据. 从零学会SQL:简单查询​www.zhihu.com 基本的查询语句 从数据库中查找数据时要使用select子句,select子句是sql中使用最多.最基 ...

  5. python爬取万方数据库,爬虫获取 js 动态数据 (万方数据库文献下载)

    今天讲讲用爬虫下载万方数据库文献. 右键那个下载按钮 -> 检查,我们可以看见,按钮的点击事件是一个 js 函数 upload(). 在网页源代码中,寻找这个 upload() 函数,发现它只在 ...

  6. sql 数据库中只靠一个数据,查询到所在表和列名

    有时候我们想通过一个值知道这个值来自数据库的哪个表以及哪个字段,在网上搜了一下,找到一个比较好的方法,通过一个存储过程实现的.只需要传入一个想要查找的值,即可查询出这个值所在的表和字段名. 前提是要将 ...

  7. 在类中用class时数据是共有还是私有_jvm学习笔记之class文件的加载、初始化

    编写的java文件在要真正运行时,会首先被编译成 ".class"结尾的二进制文件,然后被虚拟机加载.那么在虚拟机中一个class文件要成为java实例,需要经历好几个步骤: 1. ...

  8. lisp 线性标注自动避让_《数据标注工程》第一章学习笔记及作业:数据标注概述...

    第一章:数据标注概述 一.数据标注的起源与发展 1.人工智能行业的发展 AI的概念:意指让机器具有像人一般的智能行为(1956年,达特茅斯会议,由约翰·麦卡锡提出) AI的发展:60多年来,浮浮沉沉. ...

  9. 《Hadoop 权威指南 - 大数据的存储与分析》学习笔记

    第一章 初识Hadoop 1.2 数据的存储与分析 对多个硬盘中的数据并行进行读/写数据,有以下两个重要问题: 硬件故障问题.解决方案:复制(replication),系统保存数据的副本(replic ...

最新文章

  1. 【转载】写博意味着什么?
  2. ibatis源码浅析- 初探
  3. python web-Python的哪个Web框架学习周期短,学习成本低?
  4. 面试题总结(21-40)
  5. Android开发之设置Dialog外部页透明的方法亲测可用
  6. MFC略缩图控件实现
  7. 《世界已无法阻挡Python入侵!》(附学习资源)
  8. llvm安装方式_安装 LLVM 和 Clang
  9. Android Fragment (一)
  10. (转)C#读写共享文件
  11. 资源 | 没有数学和编程基础,这几个数据科学项目了解一下
  12. 学习Scala:孤立对象的实现原理
  13. 怎样快速生成一个动态二维码?动态个性二维码怎么做?
  14. sap税码配置_SAP那些事-技巧篇-42-Billing plan预收款税码问题
  15. Turtle库学习--TurtleScreen/Screen 方法及对应函数
  16. 给中国学生的第二封信:从优秀到卓越
  17. 51 PWM调速蓝牙小车
  18. 手机上有哪些可以记录生日倒计时天数的软件?
  19. 红黑树 -- 增删查改
  20. 如何快速一次性卸载所有python包(第三方库)呢?

热门文章

  1. 机器学习案例之:甲状腺射频消融数据模型
  2. 右键快捷菜单添加WinRAR
  3. U盘字符质量视觉检测视觉系统-东莞康耐德
  4. 如何在 Python 中删除文件或文件夹?
  5. win2003 iis mysql_win2003 IIS6.0+PHP+MySQL的安?配置
  6. 查找oracle系统密码,oracle密码忘记怎么办?
  7. JavaScript打印所有被执行函数堆栈
  8. 【办公类-16-06】“校历(月日版)”(python 排班表系列)
  9. Js关闭当前页面/窗口(兼容浏览器问题)
  10. wordpress建站(1) wordpress简介