一、缓存穿透

缓存穿透是指客户端请求的数据在缓存中数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

有些人可能在短时间内恶意发送大量这样的请求,导致数据库崩溃。

常见解决方案:

image-20260411151451665

1、缓存空对象

优点:实现简单,维护方便

缺点:1、额外的内存消耗 2、可能造成短期的不一致

2、布隆过滤

优点:内存占用较少,没有多余key

缺点:1、实现复杂 2、存在误判可能(查不到->数据库是真的没有,查到了->不一定真的有)

3、主动防范

  • 增强id的复杂度,避免被猜测id规律

  • 做好数据的基础格式校验

  • 加强用户权限校验

  • 热点参数限流

二、缓存雪崩(面、所有点)

同一时段大量的缓存key同时失效或者Redis服务宕机(缓存的所有key失效),导致大量请求到达数据库,带来巨大压力。

image-20260411152943410

解决方案:

  • 给不同的key的TTL添加随机值
  • 利用Redis集群(主-从,一个坏了另一个顶上来)提高服务的可用性
  • 给缓存业务添加降级限流策略(快速失败,拒绝服务)
  • 给业务添加多级缓存

三、缓存击穿(点)

也叫热点Key问题,就是一个高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

image-20260411153825539

解决方案:

  • 互斥锁
  • 逻辑过期

image-20260411154443382

互斥锁

这里的互斥锁,如果一个线程发现锁被占用了,需要休眠一会再重试,官方实现的锁如果拿不到都会阻塞,不能实现我们想要的功能,所以这里需要自己设计锁(通过redis)。

使用setnx功能

1
2
setnx lock 1 // 添加锁,后续别的线程无法再修改这个锁
del lock // 任务完成,释放锁

image-20260422103857037

逻辑过期

由于缓存击穿是针对高并发访问的数据的,所以逻辑过期方案选择提前主动把相关数据放到redis,防止大量请求到达数据库。如果在redis中查询不到这些内容时,就直接返回null,不再去数据库查询了。

四、Redis持久化

RDB

Redis Database Backup file(Redis数据备份文件),也叫做Redis数据快照。把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。默认是保存在当前运行目录。

image-20260411155330255

停机会自动执行一次RDB。

配置

image-20260411155804468

bgsave底层原理

image-20260411161025897

AOF

Append Only File(追加文件)。Redis处理的每一个写命令都记录在AOF文件中,可以看作是命令日志文件。

默认不开启,需要在redis.config中禁用RDB,并开启AOF。

save ""禁用RDB

appendonly yes开启AOF

image-20260411161238090

配置

image-20260411161457812

因为是记录命令,AOF文件会比RDB文件大很多。并且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。

通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。

image-20260411162247531

配置自动重写:

image-20260411162639054

总结

image-20260411163112567

五、数据结构

1、String

最常见的数据存储类型。

image-20260411164606173

  • 基本编码方式是RAW,基于简单动态字符串(SDS)实现,存储上限为512mb。
  • 如果存储的SDS长度小于44字节,则会采用EMBSTR编码,此时object head与SDS是一段连续的空间。申请内存时只需要调用一次内存分配函数,效率更高。(为什么是44字节?因为字符串为44字节时,SDS和redisobject的总长度为64字节,是2^n的,在分配内存的时候不会产生内存碎片。)
  • 如果存储的字符串是整数值,并且大小在LONG_MAX范围内,则会采用INT编码:直接将数据保存在RedisObject的ptr指针位置(刚好8字节),不再需要SDS。

SDS结构:

image-20260411164859567

2、List

image-20260411200649827

Redis的List结构类似一个双端链表,可以从首、尾操作列表中的元素:

  • 在3.2版本之前,Redis采用ZipList和LinkedList来实现List,当元素数量小于512并且元素大小小于64字节时采用ZipList编码,超过则采用LinkedList编码。

  • 在3.2版本之后,Redis统一采用QuickList来实现List。

image-20260411201600707

3、Set

Redis的单列集合,满足下列特点:

  • 不保证有序性
  • 元素唯一(可以判断元素是否存在)
  • 求交并差集

存储流程

使用HashTable编码(Dict)。Dict中的key用来存储元素,value统一为null。

当存储的所有数据都是整数,并且元素数量不超过set-max-intset-entries时,Set会采用IntSet编码,以节省内存。

image-20260412152711281

4、ZSet

也就是SortedSet,其中每一个元素都需要指定一个score值和member值:

  • 可以根据score值排序(值越小排在越前面)
  • member必须唯一
  • 可以根据member查询分数

实现方式一:

SkipList:可以排序,并且可以同时存储score和ele值(member)

HT(Dict):可以键值存储,并且可以根据有key找value

image-20260412153722336

SkipList工作原理:

查找:总是从最高层的最左侧开始

  1. 观察当前层右侧的下一个节点的值
  2. 如果右侧节点的值 小于 目标值,说明还没到,继续 向右 走。
  3. 如果右侧节点的值 大于 目标值,或者右侧是空(链表尾部),说明目标值必然在这两个节点之间,于是向下降一层,重复步骤 1 和 2。
  4. 知道降到第0层并找到目标,或者确认目标不存在

(这个过程很像先坐只停大站的快车逼近目的地,再换慢车精准到达)

插入:

  1. 先走一遍查找流程,找到新元素在第0层的插入位置,并把它插进去

  2. 随机函数确定是否要把新节点提拔一层(在第一层也建立这个节点并链接)

  3. 继续随机函数,判断是否要提拔到第2层,直到不提拔为止

实现方式二:

当元素数量不多时,HT和SkipList的优势不明显,而且更耗内存。因此zset会使用ZipList结构来节省内存,不过需要同时满足两个条件:

  1. 元素数量小于zset_max_ziplist_entries,默认值128
  2. 每个元素都小于zset_max_ziplist_value字节,默认64

Ziplist本身没有排序功能,且没有键值对的概念,因此需要有zset通过编码实现:

  • ZipList是连续内存,因此score和element可以紧挨在一起,存在两个entry中,element在前,score在后
  • score越小越接近队首,按照score值升序排列

image-20260412162353252

5、Hash

与Zset非常类似:

  • 都是键值存储
  • 都需要根据键获取值
  • 键必须唯一

区别:

  • zset的键是member,值是score;hash的键和值都是任意值
  • zset要根据score排序;hash无需排序

因此,hash底层采用的编码与Zset也基本一致,只需要把排序有关的SkipList去掉

  • Hash结构默认采用ZipList编码,以节省内存。相邻两个entry分别保存field和value

  • 当数量较大时,Hash结构会转为HT编码,触发条件有两个:

    • ZipList中的元素数量超过hash_max_ziplist_entries,默认512
    • 任意entry大小超过hash_max_ziplist_value字节,默认64

image-20260412163052990