Redis面试问题汇总

本文详细介绍了Redis的五种核心数据结构:String、List、Hash、Set和SortSet,及其在缓存、共享、分布式锁等场景的应用。此外,讨论了其内存管理、持久化策略、淘汰机制以及分布式锁的实现原理和挑战。
摘要由CSDN通过智能技术生成

redis的数据结构有哪些,都用于什么场景?

String

字符串,用于数据缓存,常用于:计数器、限流、数据缓存、incr、dcr、分布式锁。

list

列表,用于数据缓存,使用场景:栈、队列、阻塞队列、消息队列、粉丝列表。

Hash

哈希,用于java对象的缓存,使用场景:缓存普通的java对象。

Set

集合,Stirng类型的无序元素合集,但元素唯一,使用场景:用户标签、共同好友。

Sort set

有序结合,元素唯一,同时根据score进行排序,默认升序。使用场景:top

redis的存储结构都有哪些,为什么要设计这么多种,不同的存储对象为什么有多种映射?

String

字符串的存储结构分为:SDS和直接存储。编码方式分为:int、raw、embstr,主要体现在内存结构不同。

SDS

simple dynamic string 简单动态字符串,与普通的C语言数据结构做了优化,主要有一下几个有点:

  1. 预分配内存:
    因为内存的申请是一个比较重的操作,因此sds直接在需求的基础上,多申请些冗余空间,用于数据的修改。例如:字符串修改N次,普通的数据结构至少要N次内存操作,但是sds因为预分配内存,操作至多N次。
  2. 惰性回收内存:
    当进行字符串缩减时,并不会立即释放内存,而是稍后进行回收。
    通过预分配和惰性回收,减少内存分配和数据移动。
  3. 记录字符串长度len。
  4. 二进制安全,因为c的数据结构,\0作为字符串的结束,只能存储文字,而sds是通过长度确认的,因此可以存储多种格式数据,例如:图片,视频等。

int

直接存储可以整数化的字符串,长度小于long,超过后转化为embstr。

embstr

字符串长度小于44字节,使用embstr存储,创建时只需要分配一次内存,内存连续。但是属于只读,一旦修改后默认转化为raw。
44字节也是有考虑的,因此cpu的cache line=64字节,减去数据结构的20字节,因此只能存储44字节的数据。

raw

字符串长度大于44字节,使用raw存储,创建时需要分配两次内存,内存不连续。

list

linkedlist

普通的双向列表,每个节点都有pre和next指针。缺点:当少量的小数据存储时,有可能指针的内存占用大于数据存储,造成了相当大的内存浪费。

ziplist

ziplist:压缩列表,通过内存连续,减少指针,压缩内存占用。每个节点=前节点的长度+数据存储,正常。
优点:1 减少内存占用。2 少量数据,可以利用cpu的缓存行(64byte)。
缺点:1. 列表不能太长,即数据不能太多;2 单节点的数据不能太大。
原因:因为每个节点通过前一节点的长度+当前节点数据,如果节点过多,数据过大,频繁的修改容易触发批量的内存分配和数据移动。

quciklist

在原有列表基础上,内置了ziplist,兼并两者的有点。quicklist整体是普通列表,有pre和next指针,但是列表的节点,存储的ziplist,当ziplist的内存过多,就单独开辟一个新的quicklist节点,保证每个ziplist的数据大小可控。

说明

ziplist选择:当保存的所有键值对字符串长度小于 64 字节,并且键值对数量小于 512 时使用ziplist。

hash

hash采用hashtable存储,也可以认为是字典结构。扩容时:采用渐进式的扩容,因为数据量过大一次性rehash,会造成线程阻塞,渐进式rehash,是持有两个数组,每次对hash的操作,都会操作原数组中的数据到新数组中。

说明

当保存的所有键值对字符串长度小于 64 字节并且键值对数量小于 512 时使用ziplist ,否则使用字典的方式。

set

集合,元素唯一。采用hashtable存储。

sort set

有序集合,元素唯一,按照分数进行升序排序。采用ziplist、HashMap+skiplist两种存储方式。
说明:每个元素小于64字节,同时元素数量小于128时,采用ziplist存储。

说明

之所以设计这么多数据机构,主要是提高访问速度。例如:ziplist减少了内存占用,利用缓存行。但是当数据量过大时,就需要切换成其他数据,例如:hashtable,quicklist,skiplist。

redis的使用场景都有哪些,用到什么存储对象?

缓存

缓存,缓存相关的查询结果,方便第二次查询时,可以快速访问。

共享

因为Redis是独立的部署,因此可以在分布式系统中,作为共享中介,例如:全局session,单点登录。

分布式锁

String配合setNx命令,实现分布式锁。

全局id

通过String存储int id,采用incr批量获取id。

计数器

通过String存储int,使用incr和decr,实现计数的加减。

限流

通过String存储int,使用dcer,针对指定的ip或目标,进行访问限制。

位统计

主要是bitmap数据结构,可以通过bit数组的形式,记录每个id的存在与否。

时间轴

list可以作为栈使用,利用先进后出。例如:用户发完微博后,lpush入队列,然后lrange,可以获取最新的微博。

消息队列

利用List结构,生产者lpush,消费者rpop,即可完成相关的生产和消费。上述只是简单的应用,因为消费者rpop时,存在数据丢失的风险,可以使用rpoplpush:从尾部移除元素,并添加到指定列表的头部,并返回该元素。如果要考虑阻塞问题,可以使用:brpoplpush:从尾部移除元素,并添加到指定列表的头部,并返回该元素,如果尾部没有元素,一直阻塞到可以弹出,当然也支持等待超时。

抽奖

set是无序,但是唯一的集合,可以通过spop移除随机元素,实现抽奖。

点赞、签到、打卡

利用set,使用sadd表示点赞/签到/打卡,srem表示:取消点赞/签到/打卡。sismember 是否点赞/签到/打卡。

标签库

利用Set,通过sadd添加标签。

好友关系、用户关注、推荐模型

利用set,通过SDIFF:差集;SINTER:交集;SUNION :并集实现相关的互相关注,共同好友等。

排行榜/top榜

利用Sort set,通过

倒排索引的存储

利用String,对“你好,北京”,拆分成:“你”,“你好”,“北”,“北京”,上述拆分次作为key,value=“你好北京”,这就是存储了“你好北京”的倒排索引。

显示最新的项目列表

利用list,通过lpush,插入头部,显示最近的内容。

redis为什么使用跳表取代树进行有序集?好处是什么,又有什么缺点?

跳表

普通的有序列表,存在一个问题,如果查找某一个元素,需要全部遍历一遍,时间复杂度O(N),同样添删除元素,也需要从头遍历。降低了访问效率。
跳表:针对普通有序列表,抽出一层来,作为索引列表。
在这里插入图片描述
当查询数字30时,正常遍历,需要遍历7次,从3-30。而有了索引层之后,直接从3-18-30即可,只需要3次。这样就减少了遍历次数。 把索引层和数据层稍微旋转一下,就是个树,第二层索引层的3就是根节点,第二层索引层的3就是左子树,第一层的18就是右子树。
之所以用跳表取代树,主要原因:树的操作太复杂,尤其当添加节点/删除节点时,可能涉及数的旋转等操作。
跳表的缺陷:跳表实际是通过空间换时间,因此会有内存浪费。比如示例图中:第一层索引和第二层索引都是冗余数据。

如何保证redis缓存和数据库的数据一致性,有哪些好的方案?你常用的方案是什么?有哪些需要改进?

数据一致性

主要指缓存数据与数据库数据的一致性,主要发生在数据变更后,如何确保数据库数据与redis中的数据一致。因为更新数据库与更新redis不是原子操作。

旁路缓存

缓存策略中的一种。

  1. 读取操作:先读缓存,缓存未命中,读取数据库,然后缓存数据,返回结果。请注意整个操作流程,都是调用方自己处理。
  2. 写操作时:主要有以下几种。

先写数据库,后写缓存

写操作时:先写入数据库,后写入缓存。
缺点:

  1. 存在数据不一致的风险,因此写入缓存,如果业务复杂,可能设计多张表的查询,那么在数据库更新后,到写入缓存,中间的间隔时间可能会很长,那么就会导致数据不一致性的问题。
  2. 存在写入缓存失败,导致数据库与缓存不一致的问题。

先写缓存,后写数据库

写操作时:先写入缓存,后写入数据库。
缺点:

  1. 存在数据库丢失的风险,写入缓存成功后,有可能写入数据库失败(1 数据库异常,2 系统挂了)。

先写数据库,后删除缓存

写操作时:先写入数据库,后删除缓存。与“先写数据库,后写缓存”缺点一致,但是删除缓存比写入缓存的间隔时间更短,毕竟只有一个删除操作。

其他方式

整体的写操作,都是按照先写入数据库,保证数据不丢失,然后再考虑不一致性问题。因此对唯一性要求更严格的系统,可以考虑其他的策略。例如:

  1. 写数据库+binglog删除缓存:主要读写分离,存在主库和从库数据同步的风险。
  2. 写数据库+删除缓存+异步删除缓存:两次删除。

读/写穿透缓存

缓存策略的一种:调用方只与缓存打交道,如果缓存未命中,由缓存组件负责数据库读写。

先写缓存,后写数据库

写操作时:当缓存中没有数据时,先写入缓存,后写入数据库,属于按写分配。

不写缓存,写数据库

写操作时:当缓存中没有数据时,写入数据库,读取时再重新获取。

后置写缓存

相对写穿透缓存策略而言,后置写是通过异步的形式更新到数据库。写缓存后发送异步通知,异步通知消费后,写数据库。

优点:1. 写缓存与写数据库分离,减少与磁盘的交互,提高效率;2. 可以对写数据库命令进行优化。例如:多个写操作,是否可以批量执行。
缺点:1. 存在数据丢失的风险。2. 有延迟。
适用场景:用于读少写多的场景,Linux系统的页缓存和MySQL InnoDB 引擎的Cache Pool其实就是使用的WriteBack策略。相较于Write through 而言拥有更高的写入性能.

redis的缓存穿透,缓存击穿,缓存雪崩是什么场景?有哪些好的解决方案?

缓存穿透

场景

已缓存了很多数据,但是每次请求都未命中。例如:数据id范围1-1000,但是通过非法伪造id(负数,1000以上),缓存肯定无法未命中,大量的读请求压力到数据库,这就是缓存穿透。

解决方案

需要在读取缓存前,添加一个过滤器,这样通过过滤器的请求,才能读取缓存信息。

缓存key有规律

组织架构id、用户id等作为缓存的key,有一定规律,因此可以直接对id进行校验,例如:组织架构id,从1-8000,那么id不在该范围内,可以直接忽略。

缓存key没有规律

如果缓存的key没有范围规律,可以考虑布隆过滤器。布隆过滤器有两种实现:1. 本地的布隆过滤器;2 redis的布隆过滤器。

缓存击穿

场景

当缓存失效后,瞬时有某个key的大量请求,最终请求落入数据库中。

解决方案

缓存未命中后,请求数据库查询时,可以加锁,这样可以保证只有一个请求能落入数据库中,减轻数据库压力。
或者可以针对一些可能的热点key,走定时刷新机制。

缓存雪崩

场景描述

大量的缓存,同一时刻失效,导致大量的请求落入数据库中。

解决方案

可以封装redis操作,针对指定失效时间的key,添加随机数,是失效时间相对离散。当然上述方案主要是针对,对失效时间要求不严格的相关操作。

redis的分布式锁的使用场景有哪些?

使用场景

redis分布式锁的使用场景有:

  1. 主从结构的系统中,可以通过分布式锁的占用,表明当前服务器的主从属性。
  2. 单机运行的定时任务:有一些定时任务,需要单机运行,因此在任务触发时,需要竞争执行,因此获取分布式锁的机器才能执行,其他机器直接跳过。

使用方式

利用redis提供的setNx命令,该命令只有当key不存在时,才能返回set成功。

分布式锁其他问题

锁释放

  1. 当前线程获取分布式锁后,完成后需要释放,因此释放时,必须在finally中释放。
  2. 极端情况:线程获取分布式锁成功了,但是服务重启了,因此无法finally释放,所以获取分布式锁成功后,需要添加锁的失效时间。

锁续期

上述锁释放时,需要给锁设置失效期,但是设置的失效期内,仍然没有完成业务操作,可能存在因为锁失效,导致其他线程获取锁的情景。

方案:看门狗,即通过定时任务自动给key续期,可以设置续期次数(一般三次左右)。

锁重入

当前线程获取分布式锁后,如果再次获取时,需要支持锁重入。

方案:setNx时,value=线程id,加锁时,先判断线程id是否一致,如果一直说明可以重入,同时用另外的key记录重入次数,用于解锁。

缺陷:判断线程id与当前线程id是否一致,如果一致,设置重入次数,因此该操作并非原子操作,有可能判断一致后,锁被释放了,或者超期了,那么当前线程就不能重入。因此需要将上述命令整合,一次性在redis设置好,而不是通过java代码进行设置。

lua脚本:整合多个redis操作命令,生成一个lua命令(脚本),redis支持lua脚本的原子操作,通过lua脚本,完成多个redis命令的原子操作。

说明

完成上述操作后,一个比较健全的redis分布式锁就算大功告成了,当然上述的能力,redisson框架已经全部实现,并且还提供了更加丰富的功能:可重入锁(Reentrant Lock)公平锁(Fair Lock)联锁(MultiLock)红锁(RedLock)读写锁(ReadWriteLock)信号量(Semaphore)可过期性信号量(PermitExpirableSemaphore)闭锁(CountDownLatch)。

redis是单线程,为什么可以抗住大量流的访问?

  1. Redis是内存操作,数据获取比数据库(磁盘)要快很多,一般数据库查询是100-200毫秒,而redis查询可以认为是1毫秒。
  2. Redis是单线程的,这样不需要考虑多线程并发问题,例如:加锁,解锁等,因为加锁和解锁相对1毫秒的查询来说,也是重操作。
  3. Redis是单线程,cpu执行时,不需要考虑线程切换,可以极高的利用cpu。
  4. Redis数据结构简单,只支持5中数据机构:String、Hash、List、Set、SortSet,同时针对不同的数据机构,设计了不同的存储结构,例如:SDS、skipList,都是以空间换时间。
  5. Redis也可以认为是读写分离,通过集群的模式,解决单机并发(最高10W+)不足的问题。
  6. 非阻塞I/O多路复用机制,非阻塞I/O

redis的持久化策略

主要有两种方式:RDB和AOP

RDB

快照模式:指定时间对整个redis拍照,生成照片,当redis重启后,直接使用之前的照片,恢复数据。
优点:只需要一个快照文件。缺点:存在数据丢失的风险。

AOP

操作日志模式:每次的key的先关修改操作,都追加到日志文件中。
优点:不存在数据丢失的风险。缺点:日志文件会很大(有日志压缩的优化),当日志文件很大时,重启恢复比较慢。
当然AOP不存在数据丢失的风险,是不严格的,因为AOP的redis命令,并不是直接写入日志文件的,而是先写入内存缓存中,定时刷新到日志文件中,所以还是存在一定的数据丢失风险。

RDB+AOP

两者结合,定时快照,然后在快照的基础上进行AOP,结合双方的优点。

redis的淘汰策略

当内存满了之后,继续有新的数据缓存进来,就需要进行缓存淘汰机制。
redis的缓存淘汰机制有两种:volatile(设置过期时间)和allkeys(所有的key)

volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰

allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
allkeys-random:从数据集中任意选择数据淘汰

no-enviction(驱逐):禁止驱逐数据

Redis热点数据问题

热点数据:高频率访问的数据,因为存在缓存淘汰,所以存在热点数据被淘汰的风险。那么如何保证热点数据不被淘汰呢?
热点数据分为:静态热点数据和动态热点数据。针对热点数据就不能走删除缓存实现更新,而是主动更新,组装数据更新缓存。

静态热点数据

可以固定的缓存数据,例如:组织架构信息,秒杀商品等。针对静态热点数据,可以定时刷新。秒杀活动开始前,把相关商品和库存信息刷新到缓存中。

动态热点数据

动态热点数据:因为突发事件,导致某些数据变成热点数据,例如微博的明星热门事件。因为这些热点是不可控,突发的,因此需要动态处理。

解决方案:需要一个热点动态监控系统,针对查询接口,利用AOP进行代理,把相关的查询条件异步通知动态监控系统,动态监控系统分析查询频率,主动刷新某些热点缓存数据,而不是通过删除来实现缓存刷新。

说明

针对动态热点数据,其实仅仅依靠redis缓存,还是有一定的缺陷的,因为Redis的QPS有上限的(单机最高10W+),因此一般热点动态监控系统,是利用本地缓存+Redis缓存二级缓存机制,处理大流量高并发问题。当一个key被识别为热点数据后,数据访问会首先走本地缓存,同时主动刷新本地缓存,主动刷新缓存时,会直接调用redis缓存。

热点监控系统设计

热点监控系统设计

Redis操作延迟的可能原因

复杂度较高的命令

例如:sort、sunion、zunionstore、keys、scan,这些命令都是0(N),或者一次性取出list中的所有数据。
当服务请求量并不大,但Redis实例的CPU使用率很高,很有可能是使用了复杂度高的命令导致的。可以通过Redis提供了慢日志命令的统计功能排查。

尽量不要一次性获取大量数据,而是通过分批获取,减轻cpu的压力。

bigkey

如果查询慢日志发现,并不是复杂度较高的命令导致的,例如都是SET、DELETE操作出现在慢日志记录中,那么你就要怀疑是否存在Redis写入了bigkey的情况。

Redis在写入数据时,需要为新的数据分配内存,当Redis中删除数据时,它会释放对应的内存空间。

如果一个key写入的数据非常大,Redis在分配内存时也会比较耗时。同样的,当删除这个key的数据时,释放内存也会耗时比较久。
redis也提供了lazy-free机制,但是还是尽量减少bigkey。

集中过期

有时你会发现,平时在使用Redis时没有延时比较大的情况,但在某个时间点突然出现一波延时,而且报慢的时间点很有规律,例如某个整点,或者间隔多久就会发生一次。
如果出现这种情况,就需要考虑是否存在大量key集中过期的情况。

如果有大量的key在某个固定时间点集中过期,在这个时间点访问Redis时,就有可能导致延迟增加。
解决方案:针对失效期要求不严格的数据,将失效日期打散。正常设置的失效时间一般为整时,整分,但是可以将上述失效时间添加一个随机数(10以内),把失效时间打散。

内存达到阈值

redis达到缓存最大值后,再写入新的缓存,就要开启缓存淘汰,这样每次写入,都需要根据缓存淘汰策略,淘汰一批缓存,也会导致写入过慢。

此时需要检查redis内存设置是否过小,是否有大量key长期有效。

fork耗时严重

生成RDB和AOF都需要父进程fork出一个子进程进行数据的持久化,在fork执行过程中,父进程需要拷贝内存页表给子进程,如果整个实例内存占用很大,那么需要拷贝的内存页表会比较耗时,此过程会消耗大量的CPU资源,在完成fork之前,整个实例会被阻塞住,无法处理任何请求,如果此时CPU资源紧张,那么fork的时间会更长,甚至达到秒级。这会严重影响Redis的性能。

这种情况需要检查RDB和AOP的频率配置是否过高。

网卡负载过高

特点就是从某个时间点之后就开始变慢,并且一直持续。这时你需要检查一下机器的网卡流量,是否存在网卡流量被跑满的情况。

网卡负载过高,在网络层和TCP层就会出现数据发送延迟、数据丢包等情况。Redis的高性能除了内存之外,就在于网络IO,请求量突增会导致网卡负载变高。

如果出现这种情况,你需要排查这个机器上的哪个Redis实例的流量过大占满了网络带宽,然后确认流量突增是否属于业务正常情况,如果属于那就需要及时扩容或迁移实例,避免这个机器的其他实例受到影响。

Redis的延迟队列

redis实现延迟队列,利用SortSet的有序性,可以直接将到期时间作为score,定时从SortSet中,根据当前时间,获取数据。
Zrangebyscore :返回有序集合中指定分数区间的成员列表。有序集成员按分数值递增(从小到大)次序排列。
Zremrangebyscore:移除有序集中,指定分数(score)区间内的所有成员。

先获取,后移除,不是原子操作,数据重复的问题。所以还是需要通过lua脚本,先获取,存在数据,返回,同时删除。

当然针对大批量数据,使用同一个SortSet,还是存在风险,可以参考时间轮的概念,可以以5分钟作为一个SortSet,这样分散单个key的数量,如果5分钟数据量仍然很大,那么可以再细化,甚至10秒钟。
例如:10秒一个单位,20220909150010、20220909150020。
10秒一个单位,拆成100分:20220909150010_0,20220909150010_1,20220909150010_99。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值