2023春招redis圣经,看完面试嘎嘎能吹

目录

单线程的Redis为什么这么快?

1、Redis是基于内存的

2、Redis使用单线程

3、Redis底层高效的数据结构

Redis的数据类型,以及各自使用场景

1、String

2、List

3、Set

4、Sorted Set(Zset)

5、Hash

6、Bitmaps

7、HyperLoglog

8、Geospatial

Redis的持久化机制

AOF

后写入日志的好处是:

AOF弊端:

三种写回策略:

AOF重写机制

(了解即可)重写阻塞问题

(了解即可)AOF重写不复用AOF本身的日志

RDB

#### (了解)快照时数据能修改吗?

RDB & AOF

AOF 和 RDB 的选择问题

Redis的应用场景

缓存穿透、缓存击穿、缓存雪崩

缓存穿透

缓存雪崩

缓存击穿

如何解决缓存和数据库的数据不一致问题?

问题说明

解决办法

1、重试机制。

2、延迟双删

3、其它方法

缓存满了怎么办?

设定缓存最大容量

缓存淘汰策略

策略说明:

使用建议:

如何处理被淘汰的数据?


    相信我,看完这个,面试官越问redis你越兴奋,1个问题随随便便跟他吹个10分钟不是事!    

​    本文学习参考了极客时间的《Redis核心技术与实战》,如果想更深入了解,可以去他们官网购买专栏观看(真的很顶)    

单线程的Redis为什么这么快?

1、Redis是基于内存的

​    redis是使用内存存储,键值对读写没有磁盘IO上的开销。数据存在内存中,读写速度快。 

2、Redis使用单线程

​    首先要明确,redis的单线程最主要指的是redis的网络io和键值对读写是由一个线程来完成的,但是redis的其他功能,比如持久化,集群数据同步等等是由额外的线程来完成的。

​    但是平常我们不是一般都说多线程可以更快吗?

​    使用多线程我们不得不考虑共享资源的问题。多个线程要对这个共享资源进行修改的时候,为了保证资源的正确性,我们就要用到额外的机制,比如锁。但是锁的精度是比较难把握的,需要良好的设计锁粒度,就像mysql里的表锁和行锁一样,表锁会导致其他人都无法操作这张表。再者,如果线程过多,设置不合理,频繁的线程上下文切换导致的时间花销也不小。

​    而我们使用单线程的时候就不必要考虑这个问题了。

​    并且,redis还采用了多路复用机制,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 redis 线程处理多个IO流的效果。

3、Redis底层高效的数据结构

​    redis有一个全局哈希表,用来保存所有的键值对。

​    一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶,每个哈希桶中保存了键值对数据。哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。

​    使用哈希表最大的优势就是让我们可以用O(1)的时间复杂度来快速查找到键值对——我们只需要计算键的哈希值,就可以知道它所对应的哈希桶位置。

​    另外,还运用了简单动态字符串,双向链表,压缩列表,哈希表,跳表,整数数组这几个高效的数据结构组成了redis的几种数据类型。

Redis的数据类型,以及各自使用场景

1、String

​    当我们保存64位以内的有符号整数时,String 类型会把它保存为一个8 字节的 Long 类型整数。

​    当我们保存的数据中包含字符时,String 类型就会用简单动态字符串 (SimpleDynamic String,SDS) 结构体来保存(后边的面试可以不说),SDS结构体如下:

  • buf: 字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“0”,这就会额外占用1 个字节的开销
  • len:占4个字节,表示 buf 的已用长度
  • alloc: 也占个4字节,表示 buf 的实际分配长度,一般大于len。

​    最常见的使用:保存用户的登录信息——我们以sessionID或者token作为String的key,value保存User对象信息(转换为json字符串保存,不要把密码放进去)

2、List

​    List底层实现有三种数据结构:压缩列表、双向链表、快速列表。其中快速列表是3.2版本后引入的,quicklist是由ziplist组成的双向链表,链表中的每一个节点都以压缩列表ziplist的结构保存着数据,而ziplist有多个entry节点,保存着数据。相当于一个quicklist节点保存的是一片数据,而不再是一个数据。

三种结构对比:(了解即可)

A、双端链表

1.双端链表便于在表的两端进行 push 和 pop 操作,但是它的内存开销比较大;

2.双端链表每个节点上除了要保存数据之外,还要额外保存两个指针;

3.双端链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片;

B、压缩列表

1.ziplist 由于是一整块连续内存,所以存储效率很高;

2.ziplist 不利于修改操作,每次数据变动都会引发一次内存的 realloc;

3.当 ziplist 长度很长的时候,一次 realloc 可能会导致大批量的数据拷贝,进一步降低性能;

C、quickList

1.空间效率和时间效率的折中;

2.结合了双端链表和压缩列表的优点;

​    总的来说,list这一类型具有链表的特质,这就注定了它不适合作为随机读写的集合,因为它的查询时间复杂度是O(N)。所以我们可以将他模拟作为FIFO(先进先出)队列;还有就是我们的关注列表,按照时间排序的时候,可以用list实现,因为它的元素是按照自然插入顺序排序的

3、Set

​    set的底层数据结构是整数数组和哈希表。

 (了解即可)set的底层存储intset和hashtable是存在编码转换的,使用**intset**存储必须满足下面两个条件,否则使用hashtable,条件如下:

  • 结合对象保存的所有元素都是整数值
  • 集合对象保存的元素数量不超过512个

​    我们可以把它当作java的HashSet使用,特点是存放不重复的元素。所以,我们可以用set来精确统计网页或者帖子的浏览次数、点赞数,set的key为帖子id,然后把用户id作为值。另外,set还支持差集,并集,交集操作,可以计算共同爱好,爱好差异等等。

​    当你需要对多个集合进行聚合计算时,Set 类型会是一个非常不错的选择。但是这里有一个潜在的风险:

​    Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻。所以,一种可行的做法是:你可以从主从集群中选择一个从库,让它专门负责聚合计算,或者是把数据读取到客户端,在客户端来完成聚合统计,这样就可以规避阻塞主库实例和其他从库实例的风险了

4、Sorted Set(Zset)

​    Sorted可以按照权重值来进行排序,可以理解它是一个有序的set。

​    所以,它可以用来做一些排行榜,或者是加载帖子的最新评论。

5、Hash

​    类似于Java的Hashmap使用,特点是双列集合,而且查询快。

​    我们可以用于存放购物车内容,比如userid作为hash的集合名,然后把(‘商品id’,‘选购数量’)存到hash中。

​    还有秒杀时,以活动id作为hash的集合名,把(‘商品id’,‘秒杀商品对象(包含数量等信息)‘)存到hash中等等……

​    总之适用于双列集合的应用场景基本都可以用hash。value存储结构化对象值。

6、Bitmaps

​    位图,可以认为是一个bit数组,数组中的每个单元只能存0或者1,数组的下标在 Bitmap 中叫做偏移量。Bitmap的长度与集合中元素个数无关,而是与基数的上限有关。 

​    比如,我们可以判断商品有没有,用户在不在线,用户是否今日打卡等等。

​    例子:在统计 1 亿个用户连续 10 天的签到情况时,你可以把每天的日期作为 key,每个 key 对应一个 1 亿位的 Bitmap,每一个 bit对应一个用户当天的签到情况。
接下来,我们对10个 Bitmap做“与”操作,得到的结果也是一个 Bitmap。在这个 Bitmap 中,只有 10 天都签到的用户对应的 bit 位上的值才会是1。最后,我们可以用 BITCOUNT 统计下 Bitmap 中的1的个数,这就是连续签到10 天的用户总数了。

​    我们可以计算一下记录了10 天签到情况后的内存开销。每天使用1个1亿位的 Bitmap,大约占12MB 的内存(10^8/8/1024/1024),10 天的 Bitmap 的内存开销约为 120MB,内存压力比较小,不过,在实际应用时,最好对 Bitmap 设置过期间,让 Redis 自动删除不再需要的签到记录,以节省内存开销。

7、HyperLoglog

​    Hyperloglog 是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小。    

​    基数统计就是指统计一个集合中不重复的元素个数。对应到我们刚才介绍的场景中,就是统计网页的UV(unique visitor)。

​    网页 UV 的统计有个独特的地方,就是需要去重,一个用户一天内的多次访问只能算作一次在 Redis 的集合类型中,Set 类型默认支持去重所以看到有去重需求时,我们可能第一时间就会想到用 Set 类型。但是,如果该网页非常火爆,UV达到了千万,这个时候,一个 Set 就要记录万个用户ID。对于一个搞大促的电商网站而言,这样的页面可能有成千上万个,如果每个页面都用这样的一个 Set,就会消耗很大的内存空间。

​    在 Redis 中,每个 HyperLogLog 只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数。和元素越多就越耗费内存的 Set和Hash 类型相比,HyperLogLog 就非常节省空间。

​    在统计 UV 时,你可以用 PFADD 命令(用于向 HyperLogLog 中添加新元素)把访问页面的每个用户都添加到 HyperLogLog 中。

​    接下来,就可以用 PFCOUNT 命令直接获得 网页的 UV 值了,这个命令的作用就是返回 HyperLogLog 的统计结果。

​    不过,有一点需要你注意一下,HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。这也就意味着,你使用 HyperLogLog 统计的 UV 是 100 万,但实际的 UV 可能是 101 万。虽然误差率不算大,但是,如果你需要精确统计结果的话,最好还是继续用 Set 或 Hash 类型。

8、Geospatial

​    主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如定位、附近的人等。

Redis的持久化机制

AOF

​    AOF(append only file)是通过保存服务器执行的命令来记录数据库状态的。

​    注意AOF是先执行命令,再把命令写进日志。

后写入日志的好处是:

​    1.避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错;而后写日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。所以,Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。

​    2.不会阻塞当前的写操作。

AOF弊端:

​    1.首先,如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。

​    2.其次,AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。仔细分析的话,你就会发现,这两个风险都是和 AOF 写回磁盘的时机相关的。这也就意味着,如果我们能够控制一个写命令执行完后 AOF 日志写回磁盘的时机,这两个风险就解除了。

三种写回策略:

​    其实,对于这个问题,AOF 机制给我们提供了三个选择,也就是 AOF 配置项 appendfsync 的三个可选值。

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

针对避免主线程阻塞和减少数据丢失问题,这三种写回策略都无法做到两全其美。我们来分析下其中的原因。

  • “同步写回”可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能;
  • 虽然“操作系统控制的写回”在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不在 Redis 手中了,只要 AOF 记录没有写回磁盘,一旦宕机对应的数据就丢失了;
  • “每秒写回”采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。所以,这只能算是,在避免影响主线程性能和避免数据丢失两者间取了个折中。

AOF重写机制

​    按照系统的性能需求选定了写回策略,并不是高枕无忧了。毕竟,AOF 是以文件的形式在记录接收到的所有写命令。随着接收的写命令越来越多,AOF 文件会越来越大。这也就意味着,我们一定要小心 AOF 文件过大带来的性能问题。

​    这里的“性能问题”,主要在于以下三个方面:一是,文件系统本身对文件大小有限制,无法保存过大的文件;二是,如果文件太大,之后再往里面追加命令记录的话,效率也会变低;三是,如果发生宕机,AOF 中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到 Redis 的正常使用。

​    简单来说,AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件,也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。比如说,当读取了键值对“testkey”: “testvalue”之后,重写机制会记录 set testkey testvalue 这条命令。这样,当需要恢复时,可以重新执行该命令,实现“testkey”: “testvalue”的写入。

​    为什么重写机制可以把日志文件变小呢? 实际上,重写机制具有“多变一”功能。所谓的“多变一”,也就是说,旧日志文件中的多条命令,在重写后的新日志中变成了一条命令。我们知道,AOF 文件是以追加的方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令。但是,在重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样一来,一个键值对在重写日志中只用一条命令就行了,而且,在日志恢复时,只用执行这条命令,就可以直接完成这个键值对的写入了。

​    每次 AOF 重写时,Redis 采用fork子进程重写AOF文件;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。而且,因为 Redis 采用额外的线程进行数据重写,所以,这个过程并不会阻塞主线程。

(了解即可)重写阻塞问题

​    Redis采用fork子进程重写AOF文件时,潜在的阻塞风险包括:fork子进程 和 AOF重写过程中父进程产生写入的场景,下面依次介绍。

​    a、fork子进程,fork这个瞬间一定是会阻塞主线程的(注意,fork时并不会一次性拷贝所有内存数据给子进程),fork采用操作系统提供的写实复制(Copy On Write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成的长时间阻塞问题,但fork子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork阻塞时间越久。拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。那什么时候父子进程才会真正内存分离呢?“写实复制”顾名思义,就是在写发生时,才真正拷贝内存真正的数据,这个过程中,父进程也可能会产生阻塞的风险,就是下面介绍的场景。 

​    b、fork出的子进程指向与父进程相同的内存地址空间,此时子进程就可以执行AOF重写,把内存中的所有数据写入到AOF文件中。但是此时父进程依旧是会有流量写入的,如果父进程操作的是一个已经存在的key,那么这个时候父进程就会真正拷贝这个key对应的内存数据,申请新的内存空间,这样逐渐地,父子进程内存数据开始分离,父子进程逐渐拥有各自独立的内存空间。因为内存分配是以页为单位进行分配的,默认4k,如果父进程此时操作的是一个bigkey,重新申请大块内存耗时会变长,可能会产阻塞风险。另外,如果操作系统开启了内存大页机制(Huge Page,页面大小2M),那么父进程申请内存时阻塞的概率将会大大提高,所以在Redis机器上需要关闭Huge Page机制。Redis每次fork生成RDB或AOF重写完成后,都可以在Redis log中看到父进程重新申请了多大的内存空间。 

(了解即可)AOF重写不复用AOF本身的日志

​    一个原因是父子进程写同一个文件必然会产生竞争问题,控制竞争就意味着会影响父进程的性能。二是如果AOF重写过程中失败了,那么原本的AOF文件相当于被污染了,无法做恢复使用。所以Redis AOF重写一个新文件,重写失败的话,直接删除这个文件就好了,不会对原先的AOF文件产生影响。等重写完成之后,直接替换旧文件即可。

RDB

​    对 Redis 来说,它实现类似照片记录效果的方式,就是把某一时刻的状态以文件的形式写到磁盘上,也就是内存快照。这样一来,即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。这个快照文件就称为 RDB 文件,其中,RDB 就是 Redis DataBase 的缩写。

​    Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,给内存的全量数据做快照,把它们全部写入磁盘也会花费很多时间。而且,全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大。对于 Redis 而言,它的单线程模型就决定了,我们要尽量避免所有会阻塞主线程的操作,所以,针对任何操作,我们都会提一个灵魂之问:“它会阻塞主线程吗?”RDB 文件的生成是否会阻塞主线程,这就关系到是否会降低 Redis 的性能。

​    Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave:

  • save:在主线程中执行,会导致阻塞;
  • bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。

​    好了,这个时候,我们就可以通过 bgsave 命令来执行全量快照,这既提供了数据的可靠性保证,也避免了对 Redis 的性能影响。

#### (了解)快照时数据能修改吗?

​    举个例子。我们在时刻 t 给内存做快照,假设内存数据量是 4GB,磁盘的写入带宽是 0.2GB/s,简单来说,至少需要 20s(4/0.2 = 20)才能做完。如果在时刻 t+5s 时,一个还没有被写入磁盘的内存数据 A,被修改成了 A’,那么就会破坏快照的完整性,因为 A’不是时刻 t 时的状态。因此,和拍照类似,我们在做快照时也不希望数据“动”,也就是不能被修改。

​    但是,如果快照执行期间数据不能被修改,是会有潜在问题的。对于刚刚的例子来说,在做快照的 20s 时间里,如果这 4GB 的数据都不能被修改,Redis 就不能处理对这些数据的写操作,那无疑就会给业务服务造成巨大的影响。你可能会想到,可以用 bgsave 避免阻塞啊。这里我就要说到一个常见的误区了,避免阻塞和正常处理写操作并不是一回事。此时,主线程的确没有阻塞,可以正常接收请求,但是,为了保证快照完整性,它只能处理读操作,因为不能修改正在执行快照的数据。为了快照而暂停写操作,肯定是不能接受的。

​    所以这个时候,Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,那么,这块数据就会被复制一份,生成该数据的副本。然后,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据写入 RDB 文件。写时复制机制保证快照期间数据可修改这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。

RDB & AOF

​    到这里,你可以发现,虽然跟 AOF 相比,快照的恢复速度快,但是,快照的频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失。如果频率太高,又会产生额外开销,那么,还有什么方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?

​    Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。

AOF 和 RDB 的选择问题

  • 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
  • 如果允许分钟级别的数据丢失,可以只使用 RDB;
  • 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。

Redis的应用场景

1、缓存热点数据,缓解数据库的压力。 

2、利用 Redis 原子性的自增操作,可以实现计数器的功能,比如统计用户点赞数(Set)、用户访问数等。 

​    用set做点赞功能:因为set是不重复的,而且可以根据元素去快速查找。点赞时,把userId放入set中;下一次再点击时就会判断到set中已有该userId,就把它删掉,从而实现取消点赞。统计点赞个数直接统计set.size()即可。

3、简单的消息队列,可以使用Redis自身的发布/订阅模式或者List来实现简单的消息队列,实现异步操作。 

4、限速器,可用于限制某个用户访问某个接口的频率,比如秒杀场景用于防止用户快速点击带来不必要的压力。 

5、好友关系,利用集合的一些命令,比如交集、并集、差集等,实现共同好友、共同爱好之类的功能。

6、……还有很多情况,这个问到的话,尽量说自己的项目怎么怎么用,解决了什么业务问题

缓存穿透、缓存击穿、缓存雪崩

缓存穿透

​    指查询一个不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都是缓存不命中然后查询数据库然后数据库也没有。
​解决:

​    从数据库查询到null以后写入缓存,以后凡是请求这个资源的让缓存直接返回null,别再查询数据了。(当然缓存中存放的这个null肯定得有过期时间)

缓存雪崩

​    缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
​解决:

1、原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

2、除了微调过期时间,我们还可以通过服务降级,来应对缓存雪崩。所谓的服务降级,是指发生缓存雪崩时,针对不同的数据采取不同的处理方式。

​    当业务应用访问的是非核心数据(例如电商商品属性)时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或是错误信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。这样一来,只有部分过期数据的请求会发送到数据库,数据库的压力就没有那么大了。

​    服务熔断虽然可以保证数据库的正常运行,但是暂停了整个缓存系统的访问,对业务应用的影响范围大。为了尽可能减少这种影响,我们还可以进行请求限流。这里说的请求限流,就是指,我们在业务系统的请求入口前端控制每秒进入系统的请求数,避免过多的请求被发送到数据库。

缓存击穿

​    对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常"热点"的数据。如果这个key在100万请求同时进来前一秒正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

解决:

1、为了避免缓存击穿给数据库带来的激增压力,对于访问特别频繁的热点数据,我们可以不设置过期时间。这样一来,对热点数据的访问请求,都可以在缓存中进行处理,而 Redis 数万级别的高吞吐量可以很好地应对大量的并发请求访问。

2、加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db。如使用SpringCache:使用@Cacheable (sync = true)加锁来解决击穿问题。

如何解决缓存和数据库的数据不一致问题?

问题说明

​    当需要更新数据时,无论是先写到Redis里再写到MySQL,还是先写MySQL再写Redis,这两步写操作不能保证原子性,所以会出现Redis和MySQL里的数据不一致。

​    1、先删除缓存,再更新数据库,如果缓存删除成功,但是数据库更新失败,那么,应用再访问数据时,缓存中没有数据,就会发生缓存缺失。然后,应用再访问数据库,但是数据库中的值为旧值,应用就访问到旧值了。

​    2、先更新数据库,后更新缓存,但是,更新数据库成功而删除缓存时失败了,那么,数据库中的值是新值,而缓存中的是旧值,这肯定是不一致的。这个时候,如果有其他的并发请求来访问数据,按照正常的缓存访问流程,就会先在缓存中查询,但此时,就会读到旧值了。

解决办法

1、重试机制。

​    具体来说,可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用 Kafka 消息队列)。当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。

​    如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了。否则的话,我们还需要再次进行重试。如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。

2、延迟双删

​    假设线程 A 删除缓存值后,还没有来得及更新数据库(比如说有网络延迟),线程 B 就开始读取数据了,那么这个时候,线程 B 会发现缓存缺失,就只能去数据库读取。

​    这会带来两个问题:

  • 线程 B 读取到了旧值;
  • 线程 B 是在缓存缺失的情况下读取的数据库,所以,它还会把旧值写入缓存,这可能会导致其他线程从缓存中读到旧值。

​    等到线程 B 从数据库读取完数据、更新了缓存后,线程 A 才开始更新数据库,此时,缓存中的数据是旧值,而数据库中的是最新值,两者就不一致了。

​    解决方案:在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作。

​    之所以要加上 sleep 的这段时间,就是为了让线程 B 能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程 A 再进行删除。所以,线程 A sleep 的时间,就需要大于线程 B 读取数据再写入缓存的时间。这个时间怎么确定呢?建议你在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,以此为基础来进行估算。这样一来,其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫做“延迟双删”。

3、其它方法

​    这一块其实是比较复杂的,方法根据业务而定,有了解其他的也可以说。

缓存满了怎么办?

​    内存大小毕竟有限,随着要缓存的数据量越来越大,有限的缓存空间不可避免地会被写满。此时,该怎么办呢?

​    解决这个问题就涉及到缓存系统的一个重要机制,即缓存数据的淘汰机制。简单来说,数据淘汰机制包括两步:

  • 第一,根据一定的策略,筛选出对应用访问来说“不重要”的数据;
  • 第二,将这些数据从缓存中删除,为新来的数据腾出空间

设定缓存最大容量

​    对于 Redis 来说,一旦确定了缓存最大容量,比如 4GB,你就可以使用下面这个命令来设定缓存的大小了:

CONFIG SET maxmemory 4gb

缓存淘汰策略

​    Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略。我们可以按照是否会进行数据淘汰把它们分成两类:不进行数据淘汰的策略,只有 noeviction 这一种。会进行淘汰的 7 种其他策略。会进行淘汰的 7 种策略,

​    我们可以再进一步根据淘汰候选数据集的范围把它们分成两类:

  • 在设置了过期时间的数据中进行淘汰,包括 volatile-random、volatile-ttl、volatile-lru、volatile-lfu(Redis  4.0 后新增)四种。
  • 在所有数据范围内进行淘汰,包括 allkeys-lru、allkeys-random、allkeys-lfu(Redis 4.0 后新增)三种。

策略说明:

  • 默认情况下,Redis 在使用的内存空间超过 maxmemory 值时,并不会淘汰数据,也就是设定的 noeviction 策略。对应到 Redis 缓存,也就是指,一旦缓存被写满了,再有写请求来时,Redis 不再提供服务,而是直接返回错误。
  • volatile-ttl 在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。
  • volatile-random 就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
  • volatile-lru 会使用 LRU(最近最少使用)算法筛选设置了过期时间的键值对。
  • volatile-lfu 会使用 LFU 算法选择设置了过期时间的键值对。
  • allkeys-random 策略,从所有键值对中随机选择并删除数据;
  • allkeys-lru 策略,使用 LRU 算法在所有数据中进行筛选。
  • allkeys-lfu 策略,使用 LFU 算法在所有数据中进行筛选。

​    在 Redis 中,LRU 算法被做了简化,以减轻数据淘汰对缓存性能的影响。具体来说,Redis 默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构 RedisObject 中的 lru 字段记录)。然后,Redis 在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。

​    当需要再次淘汰数据时,Redis 需要挑选数据进入第一次淘汰时创建的候选集合。这儿的挑选标准是:能进入候选集合的数据的 lru 字段值必须小于候选集合中最小的 lru 值。当有新数据进入候选数据集后,如果候选数据集中的数据个数达到了 maxmemory-samples,Redis 就把候选数据集中 lru 字段值最小的数据淘汰出去。

使用建议:

  • 优先使用 allkeys-lru 策略。这样,可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。如果你的业务数据中有明显的冷热数据区分,建议你使用 allkeys-lru 策略。
  • 如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用 allkeys-random 策略,随机选择淘汰的数据就行。
  • 如果你的业务中有置顶的需求,比如置顶新闻、置顶视频,那么,可以使用 volatile-lru 策略,同时不给这些置顶数据设置过期时间。这样一来,这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据 LRU 规则进行筛选。

如何处理被淘汰的数据?

​    一般来说,一旦被淘汰的数据选定后,如果这个数据是干净数据,那么我们就直接删除;如果这个数据是脏数据,我们需要把它写回数据库。

​    干净数据和脏数据的区别就在于,和最初从后端数据库里读取时的值相比,有没有被修改过。干净数据一直没有被修改,所以后端数据库里的数据也是最新值。在替换时,它可以被直接删除。而脏数据就是曾经被修改过的,已经和后端数据库中保存的数据不一致了。此时,如果不把脏数据写回到数据库中,这个数据的最新值就丢失了,就会影响应用的正常使用。

​    这么一来,缓存替换既腾出了缓存空间,用来缓存新的数据,同时,将脏数据写回数据库,也保证了最新数据不会丢失。不过,对于 Redis 来说,它决定了被淘汰的数据后,会把它们删除。即使淘汰的数据是脏数据,Redis 也不会把它们写回数据库。所以,我们在使用 Redis 缓存时,如果数据被修改了,需要在数据修改时就将它写回数据库。否则,这个脏数据被淘汰时,会被 Redis 删除,而数据库里也没有最新的数据了。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值