面经-redis

210 阅读12分钟

什么是redis

redis基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景

redis高性能,高并发

假如用户第一次访问 MySQL 中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据缓存在 Redis 中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作 Redis 缓存就是直接操作内存,所以速度相当快。

单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。

所以,直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库

5种数据结构:字符串,哈希,列表,集合,有序集合

  • String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
  • List 类型的应用场景:消息队列
  • Hash 类型:缓存对象、购物车等。
  • Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
  • Zset 类型:排序场景,比如排行榜、电话和姓名排序等。

string

  • 内部实现:int和sds
  • sds:动态字符串;优点:1.可以保存文本和二进制 2.获取长度时间复杂度为(1)3.拼接字符串不会造成缓冲区溢出
# 设置 key-value 类型的值
> SET name lin
OK
# 根据 key 获得对应的 value
> GET name
"lin"
# 判断某个 key 是否存在
> EXISTS name
(integer) 1
# 返回 key 所储存的字符串值的长度
> STRLEN name
(integer) 3
# 删除某个 key 对应的值
> DEL name
(integer) 1

list列表

  • 字符串列表,按照出入顺序排序
  • 内部实现:双向链表或压缩列表
  • 如果列表元素个数和元素大小都小于设定值,就用压缩列表,否则用双向链表
  • 3.2之后用quicklist代替前两个
  • 命令:LPUSH; RPUSH; LPOP; RPOP; LRANG key start stop;
  • BLPOP key [key ...] timeout(从对头pop,没有就堵塞timeout) 称为阻塞式读取

hash--是一个键值对(key - value)集合

  • hash,string区别:

hash.webp

  • Hash 类型的底层数据结构是由压缩列表或哈希表
  • 如果哈希元素个数或者元素大小喜爱与设定值用压缩列表,否则用哈希表
  • 7.0之后用listpack
  • 命令:HSET,HGET,
  • 购物车

set

  • Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序

Set 类型和 List 类型的区别如下:

  • List 可以存储重复元素,Set 只能存储非重复元素;
  • List 是按照元素的先后顺序存储元素的,而 Set 则是无序方式存储元素的。

内部实现:哈希表或整数集合

  • 命令:
# 往集合key中存入元素,元素存在则忽略,若key不存在则新建
SADD key member [member ...]
# 从集合key中删除元素
SREM key member [member ...] 
# 获取集合key中所有元素
SMEMBERS key
# 获取集合key中的元素个数
SCARD key

# 判断member元素是否存在于集合key中
SISMEMBER key member

# 从集合key中随机选出count个元素,元素不从key中删除
SRANDMEMBER key [count]
# 从集合key中随机选出count个元素,元素从key中删除
SPOP key [count]

zset

  • Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值)
  • 有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。
  • 内部实现:压缩列表或跳表
  • 命令:
# 往有序集合key中加入带分值元素
ZADD key score member [[score member]...]   
# 往有序集合key中删除元素
ZREM key member [member...]                 
# 返回有序集合key中元素member的分值
ZSCORE key member
# 返回有序集合key中元素个数
ZCARD key 

# 为有序集合key中元素member的分值加上increment
ZINCRBY key increment member 

# 正序获取有序集合key从start下标到stop下标的元素
ZRANGE key start stop [WITHSCORES]
# 倒序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES]

# 返回有序集合中指定分数区间内的成员,分数由低到高排序。
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

# 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。
ZRANGEBYLEX key min max [LIMIT offset count]
# 返回指定成员区间内的成员,按字典倒序排列, 分数必须相同
ZREVRANGEBYLEX key max min [LIMIT offset count]

redis线程模型

  • Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发生数据给客户端」这个过程是由一个线程(主线程)来完成的

  • 但是Redis 程序并不是单线程的,Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理。是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。

后台线程.webp

redis单线程模型.drawio.webp

redis单线程为什么还这么快?

  • reids大部分操作在内存上完成,并且采用了高效的数据结构
  • 单线程避免了多线程竞争
  • 采用了io多路复用机制。(IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。)

官方解释为什么使用单线程?

image.png

Redis 6.0 对于网络请求采用多线程来处理。但是对于读写命令,Redis 仍然使用单线程来处理,

redis持久化

为了保证重启后内存数据不丢失,实现了持久化机制。。。这个机制把数据储存到磁盘,重启后从磁盘中恢复数据

三种持久化方式:

  • AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里; 6f0ab40396b7fc2c15e6f4487d3a0ad7.png
  • RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
  • 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RDB 的优点

RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。

AOF 优点是丢失数据少,但是数据恢复不快。

混合持久化既保证了redis重启速度,有降低了数据丢失风险。

f67379b60d151262753fec3b817b8617.jpg Redis 常见面试题 | 小林coding (xiaolincoding.com)

  • 混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。

redis缓存设计

image.png 缓存雪崩的解决方案有以下几种

  1. 设置缓存失效时间随机性。在设置缓存失效时间时,可以增加一定的随机性,即在缓存失效时间上加上一个随机的时间值。这样可以避免大量的缓存同时失效,减小缓存失效的概率,分散缓存失效的时间。

  2. 数据预热。在系统启动前,可以预先加载缓存中的数据,使得缓存中的数据可以尽早地准备好,避免在系统运行过程中缓存失效导致大量请求落到后端服务上,减小后端服务器的压力。

  3. 分布式锁。在缓存失效的同时,可以使用分布式锁的机制,保证只有一个线程去请求数据库或后端服务,避免大量的请求落到后端服务器上,导致负载过大。

  4. 数据缓存策略优化。可以通过优化数据缓存策略,例如将热点数据缓存在多级缓存中,降低对后端服务的请求次数,从而减小后端服务的负载压力。

  5. 服务降级。在缓存雪崩发生时,可以使用服务降级的方式,暂时关闭一些非核心功能,以避免服务崩溃。可以在系统设计时,将一些非核心功能拆分出去,以避免整个系统崩溃。 解决方案:

  6. 将缓存失效时间随机打散

  7. 设置缓存不过期

juejin.cn/post/711753… 缓存击穿

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。

acb5f4e7ef24a524a53c39eb016f63d4.png 缓存击穿是指在高并发场景下,一个热点数据的缓存失效,导致大量请求直接访问数据库,从而对数据库造成了巨大的压力。为了解决缓存击穿问题,可以考虑以下几种方法:

  1. 设置合理的缓存过期时间:通过设置适当的缓存过期时间,确保缓存的有效期能够覆盖大部分请求。可以根据业务需求和数据更新频率来设置缓存过期时间,避免热点数据的频繁失效。
  2. 使用互斥锁或分布式锁:当缓存失效时,可以通过加锁机制来保证只有一个请求能够访问数据库进行数据查询和缓存的重建。其他请求在等待期间可以从缓存中获取数据,而不会直接访问数据库。一旦缓存被重建,其他请求再次访问缓存即可。
  3. 延迟缓存加载:在缓存失效时,不立即去查询数据库重建缓存,而是等待一段时间,以期待其他请求可以将数据重新缓存。这样可以避免大量的请求同时访问数据库,减轻数据库的压力。可以通过设置短暂的延迟时间,然后再去查询数据库。
  4. 异步缓存更新:在缓存失效时,将缓存的重建操作放入异步任务队列中进行处理,让请求立即返回。后台任务队列负责重新查询数据并更新缓存,这样可以减少对数据库的直接访问压力,提高系统的响应速度。
  5. 热点数据预加载:通过预先加载热点数据到缓存中,避免热点数据失效时的缓存击穿问题。可以在系统启动时或者低峰期通过定时任务或其他方式将热点数据加载到缓存中,提前准备好缓存数据,以应对高并发请求。

以上是一些常见的解决缓存击穿问题的方法,根据具体的业务场景和系统需求,可以选择合适的策略或组合多种策略来应对缓存击穿的挑战。

缓存穿透:当用户访问的数据,既不在缓存中,也不在数据库中,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

b7031182f770a7a5b3c82eaf749f53b0.png

解决方法:

  • 非法请求的限制:在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在
  • 设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在

如何设计一个缓存策略,可以动态缓存热点数据呢?

通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。

Redis 和 MySQL 的更新策略用的是 Cache Aside,

(旁路缓存)策略 6e3db3ba2f829ddc14237f5c7c00e7ce.png

先更新数据库在删除缓存

image.png

image.png

如何实现延迟队列

在 Redis 可以使用有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。

延迟队列.webp

redis管道

用于一次处理多个 Redis 命令,从而提高整个交互的性能。

使用管道技术可以解决多个命令执行时的网络等待,它是把多个命令整合到一起发送给服务器端处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序的执行效率。 管道模式.webp 客户端命令,非服务器功能