Redis
基础
什么是Redis
?
Redis
是一个基于 C 语言开发的开源数据库(BSD 许可),被频繁用于分布式缓存,与传统数据库不同的是 Redis
的数据是存在内存中的(内存数据库),读写速度非常快,被广泛应用于缓存方向。并且,Redis
存储的是 KV 键值对数据。
为了满足不同的业务场景,Redis
内置了多种数据类型实现(比如 String、Hash、Sorted Set、Bitmap)。并且,Redis
还支持事务 、持久化、Lua 脚本、多种开箱即用的集群方案(Redis Sentinel
、Redis Cluster
)。
Redis
没有外部依赖,Linux 和 OS X 是 Redis
开发和测试最多的两个操作系统,官方推荐生产环境使用 Linux 部署 Redis
。
为什么Redis
快?
Redis
基于内存,内存的访问速度是磁盘的上千倍;Redis
基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用;Redis
内置了多种优化过后的数据结构实现,性能非常高。
为什么要用Redis
/缓存?
-
高性能 如果将高频热点数据存入缓存中,在需要读取得时候就可以直接操作缓存而不用访问数据库,操作缓存就是直接操作内存,所以速度相当快
-
高并发 一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用
Redis
缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机Redis
的情况,Redis
集群的话会更高)。由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。 -
此外,如果问到问什么不用本地缓存?可以往微服务项目上代,比如我前台系统和后台系统分别部署在两台服务器上,但是想共用同一套缓存和数据库,那么分布式缓存
redis
就可以轻松满足需求,而本地缓存比如hashMap
显然是无法做到的Redis
除了缓存,还能干啥?分布式锁 : 通过
Redis
来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于Redisson
来实现分布式锁。可限流 :一般是通过
Redis
+ Lua 脚本的方式来实现限流。消息队列 :
Redis
自带的 list 数据结构可以作为一个简单的队列使用。Redis 5.0
中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。复杂业务场景 :通过
Redis
以及Redis
扩展(比如Redisson
)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。
Redis
数据结构
常见的数据结构有哪些?
5 种基础数据结构 :String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset
(有序集合)。
3 种特殊数据结构 :HyperLogLogs
(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。
1.String
String 是 Redis
中最简单同时也是最常用的一个数据结构。
String 是一种二进制安全的数据结构,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
虽然 Redis
是用 C 语言写的,但是 Redis
并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(Simple Dynamic String,SDS)。相比于 C 的原生字符串,Redis
的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis
的 SDS API 是安全的,不会造成缓冲区溢出。
2.List
许多高级编程语言都内置了链表的实现比如 Java 中的 LinkedList
,但是 C 语言并没有实现链表,所以 Redis
实现了自己的链表数据结构。Redis
的 List 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
应用场景:
信息流展示
- 举例 :最新文章、最新动态。
- 相关命令 :
LPUSH
、LRANGE
。
消息队列
Redis
List 数据结构可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。
相对来说,Redis
5.0 新增加的一个数据结构 Stream
更适合做消息队列一些,只是功能依然非常简陋。和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。
3. Hash
Redis
中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接修改这个对象中的某些字段的值。
Hash 类似于 JDK1.8 前的 HashMap
,内部实现也差不多(数组 + 链表)。不过,Redis
的 Hash 做了更多优化
应用场景:
对象数据存储场景
- 举例 :用户信息、商品信息、文章信息、购物车信息。
- 相关命令 :
HSET
(设置单个字段的值)、HMSET
(设置多个字段的值)、HGET
(获取单个字段的值)、HMGET
(获取多个字段的值)。
4. Set(集合)
Redis
中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 HashSet
。当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择,并且 Set 提供了判断某个元素是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。
你可以基于 Set 轻易实现交集、并集、差集的操作,比如你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。这样的话,Set 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程
应用场景:
需要存放的数据不能重复的场景
- 举例:网站 UV 统计(数据量巨大的场景还是
HyperLogLog
更适合一些)、文章点赞、动态点赞等场景。 - 相关命令:
SCARD
(获取集合数量) 。
需要获取多个数据源交集、并集和差集的场景
- 举例 :共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集) 、订阅号推荐(差集+交集) 等场景。
- 相关命令:
SINTER
(交集)、SINTERSTORE
(交集)、SUNION
(并集)、SUNIONSTORE
(并集)、SDIFF
(差集)、SDIFFSTORE
(差集)。
需要随机获取数据源中的元素的场景
- 举例 :抽奖系统、随机。
- 相关命令:
SPOP
(随机获取集合中的元素并移除,适合不允许重复中奖的场景)、SRANDMEMBER
(随机获取集合中的元素,适合允许重复中奖的场景)。
5. Sorted Set(有序集合,也称zset
)
Sorted Set 类似于 Set,但和 Set 相比,Sorted Set 增加了一个权重参数 score
,使得集合中的元素能够按 score
进行有序排列,还可以通过 score
的范围来获取元素的列表。有点像是 Java 中 HashMap
和 TreeSet
的结合体。
应用场景:
需要随机获取数据源中的元素根据某个权重进行排序的场景
- 举例 :各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
- 相关命令 :
ZRANGE
(从小到大排序) 、ZREVRANGE
(从大到小排序)、ZREVRANK
(指定元素排名)。
3种特殊的数据结构
1.Bitmap
Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)
应用场景:
需要保存状态信息(0/1 即可表示)的场景
- 举例 :用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。
- 相关命令 :
SETBIT
、GETBIT
、BITCOUNT
、BITOP
。
2.HyperLogLog
HyperLogLog
是一种有名的基数计数概率算法 ,基于 LogLog Counting(LLC)
优化改进得来,并不是 Redis
特有的,Redis
只是实现了这个算法并提供了一些开箱即用的 API。
Redis
提供的 HyperLogLog
占用空间非常非常小,只需要 12k 的空间就能存储接近2^64
个不同元素。并且,Redis
对 HyperLogLog
的存储结构做了优化,采用两种方式计数:
- 稀疏矩阵 :计数较少的时候,占用空间很小。
- 稠密矩阵 :计数达到某个阈值的时候,占用 12k 的空间。
应用场景:
数量量巨大(百万、千万级别以上)的计数场景
- 举例 :热门网站每日/每周/每月访问
ip
数统计、热门帖子uv
统计、 - 相关命令 :
PFADD
、PFCOUNT
。
3.Geospatial index
Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。
通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。
应用场景:
需要管理使用地理空间数据的场景
- 举例:附近的人。
- 相关命令:
GEOADD
、GEORADIUS
、GEORADIUSBYMEMBER
。
String
vs Hash
- String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
- String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。
在绝大部分情况,我们建议使用 String 来存储对象数据即可!在数据频繁变动,如文章的文章id与浏览量,购物车的商品id与数量,建议使用Hash类型
String的底层实现
Redis
是基于 C 语言编写的,但 Redis
的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 \0
结尾的字符数组),而是自己编写了SDS(Simple Dynamic String,简单动态字符串) 来作为底层实现。
SDS 最早是 Redis
作者为日常 C 语言开发而设计的 C 字符串,后来被应用到了 Redis
上,并经过了大量的修改完善以适合高性能操作。
SDS 相比于 C 语言中的字符串有如下提升:
- 可以避免缓冲区溢出 :C 语言中的字符串被修改(比如拼接)时,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。SDS 被修改时,会先根据
len
属性检查空间大小是否满足要求,如果不满足,则先扩展至所需大小再进行修改操作。 - 获取字符串长度的复杂度较低 : C 语言中的字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。SDS 的长度获取直接读取
len
属性即可,时间复杂度为 O(1)。 - 减少内存分配次数 : 为了避免修改(增加/减少)字符串时,每次都需要重新分配内存(C 语言的字符串是这样的),SDS 实现了空间预分配和惰性空间释放两种优化策略。当 SDS 需要增加字符串时,
Redis
会为 SDS 分配好内存,并且根据特定的算法分配多余的内存,这样可以减少连续执行字符串增长操作所需的内存重分配次数。当 SDS 需要减少字符串时,这部分内存不会立即被回收,会被记录下来,等待后续使用(支持手动释放,有对应的 API)。 - 二进制安全 :C 语言中的字符串以空字符
\0
作为字符串结束的标识,这存在一些问题,像一些二进制文件(比如图片、视频、音频)就可能包括空字符,C 字符串无法正确保存。SDS 使用len
属性判断字符串是否结束,不存在这个问题。
Redsi
线程模型
对于读写命令来说,Redis
一直是单线程模型。不过,在 Redis
4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作, Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
单线程模型
Redis
基于 Reactor 模式设计开发了一套高效的事件处理模型 (Netty
的线程模型也基于 Reactor 模式,Reactor 模式不愧是高性能 IO 的基石),这套事件处理模型对应的是 Redis
中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis
是单线程模型。Redis
通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。这样的好处非常明显: I/O 多路复用技术的使用让 Redis
不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector
组件很像)。
文件事件处理器(file event handler)主要是包含 4 个部分:
-
多个 socket(客户端连接)
-
IO 多路复用程序(支持多个客户端连接的关键)
-
文件事件分派器(将 socket 关联到相应的事件处理器)
-
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
Redis6.0 之后为何引入了多线程?
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis
中的一个性能瓶颈(Redis
的瓶颈主要受限于内存和网络)。
虽然,Redis6.0 引入了多线程,但是 Redis
的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要设置IO线程数 > 1,需要修改 redis
配置文件 redis.conf
:
Redis
内存管理
Redis
给缓存数据设置过期时间有啥用?
一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢?
因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。
Redis
自带了给缓存数据设置过期时间的功能,比如:
127.0.0.1:6379> expire key 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56
注意:Redis
中除了字符串类型有自己独有设置过期时间的命令 setex
外,其他方法都需要依靠 expire
命令来设置过期时间 。另外, persist
命令可以移除一个键的过期时间。
过期时间除了有助于缓解内存的消耗,还有什么其他用么?
很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 token 可能只在 1 天内有效。
如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。
Redis
是如何判断数据是否过期的呢?
Redis
通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis
数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
过期的数据的删除策略?
如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis
是怎么对这批 key 进行删除的呢?
常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西):
- 惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
- 定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,
Redis
底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis
采用的是 定期删除+惰性/懒汉式删除 。
但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。
怎么解决这个问题呢?答案就是:Redis
内存淘汰机制。
Redis
内存淘汰机制?
相关问题:MySQL 里有 2000w 数据,
Redis
中只存 20w 的数据,如何保证Redis
中的数据都是热点数据?
Redis
提供 6 种数据淘汰策略:
- volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
4.0 版本后增加以下两种:
- volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
Redis
持久化机制
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
Redis
不同于 Memcached
的很重要一点就是,Redis
支持持久化,而且支持两种不同的持久化操作。Redis
的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)。
什么是 RDB 持久化?
Redis
可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis
创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis
主从结构,主要用来提高 Redis
性能),还可以将快照留在原地以便重启服务器的时候使用。
什么是 AOF 持久化?
与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis
没有开启 AOF(append only file)方式的持久化
开启 AOF 持久化后每执行一条会更改 Redis
中的数据的命令,Redis
就会将该命令写入到内存缓存 server.aof_buf
中,然后再根据 appendfsync
配置来决定何时将其同步到硬盘中的 AOF 文件。
AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir
参数设置的,默认的文件名是 appendonly.aof
。
在 Redis
的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显式地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec
选项 ,让 Redis
每秒同步一次 AOF 文件,Redis
性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis
还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
对比
RDB 的特点
-
RDB 优点:
- RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低
- RDB 内部存储的是
Redis
在某个时间点的数据快照,非常适合用于数据备份,全量复制、灾难恢复 - RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复
-
RDB 缺点:
- BGSAVE 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能
- RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有丢失数据的可能性,最后一次持久化后的数据可能丢失
Redis
的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容
AOF 特点:
- AOF 的优点:数据持久化有较好的实时性,通过 AOF 重写可以降低文件的体积
- AOF 的缺点:文件较大时恢复较慢
AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失)
应用场景:
-
对数据非常敏感,建议使用默认的 AOF 持久化方案,AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略
Redis
仍可以保持很好的处理性能注意:AOF 文件存储体积较大,恢复速度较慢,因为要执行每条指令
-
数据呈现阶段有效性,建议使用 RDB 持久化方案,可以做到阶段内无丢失,且恢复速度较快
注意:利用 RDB 实现紧凑的数据持久化,存储数据量较大时,存储效率较低
综合对比:
- RDB 与 AOF 的选择实际上是在做一种权衡,每种都有利有弊
- 灾难恢复选用 RDB
- 如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF;如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB
- 双保险策略,同时开启 RDB 和 AOF,重启后
Redis
优先使用 AOF 来恢复数据,降低丢失数据的量 - 不建议单独用 AOF,因为可能会出现 Bug,如果只是做纯内存缓存,可以都不用
Redis
性能优化
Redis bigkey
什么是 bigkey
?
简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey
。具体多大才算大呢?有一个不是特别精确的参考标准:string 类型的 value 超过 10 kb,复合类型的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。
bigkey
有什么危害?
除了会消耗更多的内存空间,bigkey
对性能也会有比较大的影响。因此,我们应该尽量避免写入 bigkey
!
如何发现 bigkey
?
1、使用 Redis
自带的 --bigkeys
参数来查找。
2、分析 RDB 文件
大量 key 集中过期问题
我在上面提到过:对于过期 key,Redis
采用的是 定期删除+惰性/懒汉式删除 策略。
定期删除执行过程中,如果突然遇到大量过期 key 的话,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个这个定期任务线程是在 Redis
主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢。
如何解决呢?下面是两种常见的方法:
- 给 key 设置随机过期时间。
- 开启 lazy-free(惰性删除/延迟释放) 。lazy-free 特性是
Redis
4.0 开始引入的,指的是让Redis
采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间。
Redis
生产问题
缓存预热
场景:宕机,服务器启动后迅速宕机
问题排查:
-
请求数量较高,大量的请求过来之后都需要去从缓存中获取数据,但是缓存中又没有,此时从数据库中查找数据然后将数据再存入缓存,造成了短期内对
redis
的高强度操作从而导致问题 -
主从之间数据吞吐量较大,数据同步操作频度较高
解决方案:
-
前置准备工作:
-
日常例行统计数据访问记录,统计访问频度较高的热点数据
-
利用 LRU 数据删除策略,构建数据留存队列例如:storm 与
kafka
配合
-
-
准备工作:
-
将统计结果中的数据分类,根据级别,redis 优先加载级别较高的热点数据
-
利用分布式多服务器同时进行数据读取,提速数据加载过程
-
热点数据主从同时预热
-
-
实施:
-
使用脚本程序固定触发数据预热过程
-
如果条件允许,使用了 CDN(内容分发网络),效果会更好
-
总的来说:缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据
缓存穿透
问题排查:
-
Redis
中大面积出现未命中 -
出现非正常 URL 访问
问题分析:
-
访问了不存在的数据,跳过了
Redis
缓存,数据库页查询不到对应数据 -
Redis
获取到 null 数据未进行持久化,直接返回 -
出现黑客攻击服务器
解决方案:
-
缓存无效Key:对查询结果为 null 的数据进行缓存,设定短时限,例如 30-60 秒,最高 5 分钟
-
布隆过滤器:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。关于布隆过滤器,总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
缓存击穿
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
![image.png](http://hwfblogoss.hanwangfei.cn//2023/02/21/e64c471c4d1d44ad86b4025c2a9aeb10.png)
解决方案:
- 设置热点数据永不过期或者过期时间比较长。
- 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
- 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。
缓存穿透和缓存击穿有什么区别?
缓存穿透中,请求的 key 既不存在于缓存中,也不存在于数据库中。
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。
缓存雪崩
实际上,缓存雪崩描述的就是这样一个简单的场景:缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
另外,缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。
![image.png](http://hwfblogoss.hanwangfei.cn//2023/02/21/fe5def82c2e647a4adc4e8737975419a.png)
解决方案:
针对 Redis
服务不可用的情况:
- 采用
Redis
集群,避免单机出现问题整个缓存服务都没办法使用。 - 限流,避免同时处理大量的请求。
针对热点缓存失效的情况:
- 设置不同的失效时间比如随机设置缓存的失效时间。
- 缓存永不失效(不太推荐,实用性太差)。
- 设置二级缓存。
缓存雪崩和缓存击穿有什么区别?
缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)。