面试-redis

详情直达:视频

1. 缓存

1.1 缓存穿透

查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库

image-20240502211237567

  • 解决方案:
    • 缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存
      • 优点:简单
      • 缺点:消耗内存,可能会发生不一致的问题
    • 布隆过滤器

1.1.1 布隆过滤器

1.1.1.1 介绍

  • bitmap:相当于是一个以(bit)位为单位的数组,数组中每个单元只能存储二进制数0或1

  • 布隆过滤器可以用于检索一个元素是否在一个集合中。

  • image-20240502110122091

  • 使用的是redisson实现的布隆过滤器。

1.1.1.2 底层原理

  • 初始化一个比较大数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。
  • 误判率:数组越小误判率就越大,数组越大误判率就越小,但是同时带来了更多的内存消耗。通常误判率在5%。

1.1.1.2 优点

  • 内存占用少
  • 没有多余Key

1.1.1.3 缺点

  • 实现复杂
  • 存在误判

1.2 缓存击穿

给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮

image-20240502211423139

1.2.1 互斥锁

image-20240502211512487

  • 当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法
  • 优点:数据一致性
  • 缺点:性能差

1.2.2 逻辑过期

image-20240502211533351

  • 在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间
  • 当查询的时候,从redis取出数据后判断时间是否过期
  • 如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新
  • 优点:高可用,性能较好
  • 缺点:数据不能达到强一致。

1.3 缓存雪崩

  • 指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库。

  • 解决:

    • 给不同的Key的TTL添加随机值
    • 给缓存业务添加降级限流策略
    • 搭建redis集群。

1.4 缓存预热

  • 指系统上线后,提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。

  • 解决:

    • 数据量不大的时候,工程启动的时候进行加载缓存动作;
    • 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
    • 数据量太大的时候,优先保证热点数据进行提前加载到缓存。

1.5 双写一致

即:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致

1.5.1 数据强一致

  • 对于这类问题,因为要求时效性比较高,我们当时采用的读写锁保证的强一致性。
  • 可以采用redisson实现的读写锁,在读的时候添加共享锁(readLock),可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁(writeLock),它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。
  • 排他锁底层使用setnx,这样就保证了当前只有一个线程操作锁住的方法。
  • 延迟双删:如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性。

1.5.2 时效性高

  • 对于这类问题,因为需要满足高并发,因此允许有一定的延时。
  • 可以采用MQ的方式,当有修改数据操作的时候,同时发布消息,在缓存监听服务中监听消息,一旦收到消息就立刻更新缓存。
  • 还可以采用阿里的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。

1.6 数据持久化

1.6.1 RDB

  • RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。

  • 在redis客户端使用bgsave就可以开启备份

  • 可以在redis的配置文件redis.conf文件中找到

    image-20240502114447746

  • 执行原理:

    • bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。

    • 在linux中所有进程不能直接操作物理内存的数据,os会给每个进程一个虚拟内存,os会维护一个虚拟内存与物理内存的一个关系表(页表)。

    • 主进程操作虚拟内存,通过页表,关联到真正的物理内存,就完成了对数据的读写操作。

    • 开启bgsave的时候fork出一个子进程,同时也复制了页表,那么在子进程与主进程的内存空间共享。

      image-20240502115210482

    • 子进程在写RDB文件的时候,主进程接收用户请求实现修改内存数据。此时就形成主进程修改数据,但是子进程在备份数据,有脏数据风险。

    • 避免脏数据发生,使用copy-on-write的技术:

      • 当主进程执行读操作的时候,访问共享内存

      • 当主进程执行写操作的时候,会拷贝一份数据,专门用来进行写操作。

        image-20240502115510980

1.6.2 AOF

  • AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。

  • 默认是关闭的,需要修改redis.conf文件下的appenonly配置由no改为yes。

  • AOF的命令记录的频率也可以通过redis.conf文件来配:

    image-20240502115706409

  • image-20240502115738891

1.6.3 区别

通常对数据安全性较高的都会使用RDB+AOF

image-20240502115759983

1.7 数据过期策略

  • Redis对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉。可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)。
  • 通常使用惰性+定期删除

1.7.1 惰性删除

  • 惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key
  • 优点:对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查
  • 缺点:对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放

1.7.2 定期删除

  • 每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。
  • slow模式:定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的hz 选项来调整这个次数
  • fast模式:执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
  • 优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。
  • 缺点:难以确定删除操作执行的时长和频率。

1.8 数据淘汰策略

当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

有8种模式:

  • noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
  • volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰。
  • allkeys-random:对全体key ,随机进行淘汰。(数据访问频率不大使用)
  • volatile-random:对设置了TTL的key ,随机进行淘汰。
  • allkeys-lru: 对全体key,基于LRU算法进行淘汰。(优先使用)
  • volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰。(业务指定需求使用)
  • allkeys-lfu: 对全体key,基于LFU算法进行淘汰。(短时高频访问)
  • volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰。(短时高频访问)

LRULeast Recently Used)最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。

LFULeast Frequently Used)最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。

2. 分布式锁

使用场景:集群情况下的定时任务、抢单、幂等性场景

2.1 超卖问题

image-20240502120918214

在这种情况下,一定会出现超卖的。在单系统下,使用synchronized或者reentrantlock加锁可以放置超卖,但是在集群模式下,每个JVM的锁是不一样的,因此还会出现超卖问题。

如果使用redis作为分布式锁,使用setnx就可以完成,当然最后一定要手动的删除锁。

image-20240502121322214

2.2 锁时长

2.2.1 预估

根据业务的平时耗时来给出一个预估加锁时长,但是如果出现网络抖动或者IO异常时,此方法不可靠。

2.2.2 看门狗

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程一还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题。

1、如果我们指定了锁的超时时间,就发送给Redis执行脚本,进行占锁,默认超时就是我们制定的时间,不会自动续期;
2、如果我们未指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】

2.3 可重入

redission实现的分布式锁是可重入的,使用Hash结构记录线程Id和重入次数

image-20240502122445085

2.4 主从一致

即:当线程1获得锁操作master节点后,master发生了宕机,此时在从节点中选举出新的master节点,线程2获取锁在新的master上,这样就造成两个线程同时获得锁

2.4.1 红锁

RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n / 2 + 1),避免在一个redis实例上加锁。

但是:红锁实现复杂,性能差,运维繁琐。

3. 集群

3.1 主从复制

单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。

image-20240502123443764

3.1.1 主从数据同步

  • 全量同步(首次):

    • 从节点与主节点建立连接。
    • 发起请求数据同步,会携带自己的repId和offest。
    • 主节点接收到之后,根据repid判断是否是首次,repid不一致即是首次同步。
    • 首次同步是将自己的repid和offset返回给从节点。
    • 从节点保存主节点数据信息。
    • 主节点执行bgsave。
    • 将生成的rdb文件发送给从节点。
    • 从节点接收后清空自己本地的rdb文件,加载主节点发送的rdb文件。
    • 如果主节点在执行bgsave的时候,接收到用户请求,将会记录下这段备份时间所有的指令至repl_baklog中。
    • 最后将repl_baklog发送给从节点,从节点执行该文件中所有的命令。
    • 完成数据同步。

    image-20240502124016215

  • 增量同步(非首次):发生在salve重启或者数据变化时

    • 从节点发生宕机重启。
    • 从节点发起请求,携带repid和offset。
    • 主节点接收请求,根据repid判断是自己的,进行增量同步。
    • 去repl_baklog中寻找offset后的执行命令。
    • 主节点将这些命令发送给从节点。
    • 从节点执行命令。
    • 完成同步

    image-20240502124318531

3.2 哨兵模式

Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。

image-20240502125331993

  • 监控:Sentinel 会不断检查您的master和slave是否按预期工作。
  • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主。
  • 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端

3.2.1 检测

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:

  • 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线

  • 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。

3.2.2 选举规则

  • 首先判断主与从节点断开时间长短,如超过指定值就排该从节点。
  • 然后判断从节点的slave-priority值,越小优先级越高。
  • 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高。

3.2.3 脑裂

  • 正常的哨兵模式:image-20240502125609906

  • 如果此时出现某一个主节点与哨兵由于网络问题出现不通信,被哨兵检测为下线,那么哨兵会在接下的从节点中重新选举出一个新的master节点。由于服务状态变更通知不能立刻通知到client,用户还在向原master写操作。image-20240502125750134

  • 最终原master会被降为从节点,就进行了重新的主从数据同步操作,因此用户的写操作执行命令就造成丢失。

  • 因此可以通过修改redis的两个配置参数防止出现该问题

    • min-replicas-to-write 1 表示最少的salve节点为1个。即:在执行写操作的时候,必须要有一个从节点。
    • min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒。即:如果主从数据同步操作5秒,达不到要求就拒绝请求,这样就可以防止丢失数据。

3.3 分片集群

分片集群用来解决:

  • 海量数据存储问题
  • 高并发写的问题

image-20240502130411000

3.3.1 特征

  • 集群中有多个master,每个master保存不同数据。
  • 每个master都可以有多个slave节点。
  • master之间通过ping监测彼此健康状态。
  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点。

3.3.2 数据读写

Redis 分片集群引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。

image-20240502130516284

4. Redis事务

4.1 事务基本概念

  • Redis事务中如果有某一条命令执行失败,之前的命令不会回滚,其后的命令仍然会被继续执行。鉴于这个原因,所以说Redis的事务严格意义上来说是不具备原子性的。
  • Redis事务中所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。

4.2 Redis事务三个阶段

  1. multi 开启事务
  2. 大量指令入队
  3. exec执行事务块内命令,截止此处一个事务已经结束。
  4. discard 取消事务
  5. watch 监视一个或多个key,如果事务执行前key被改动,事务将打断。unwatch 取消监视。

事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队.

4.3 Redis事务相关命令

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的

4.3.1 WATCH

  • WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。
  • 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

4.3.2 MULTI

  • MULTI命令用于开启一个事务,它总是返回OK。
  • MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。

4.3.3 EXEC

  • 执行所有事务块内的命令。
  • 返回事务块内所有命令的返回值,按命令执行的先后顺序排列。
  • 当操作被打断时,返回空值 nil 。
  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。

4.3.4 UNWATCH

  • UNWATCH命令可以取消watch对所有key的监控。

4.4 Redis事务支持隔离性吗?

  • Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的

4.5 Redis为什么不支持事务回滚?

  • Redis 命令只会因为错误的语法而失败,或是命令用在了错误类型的键上面,这些问题不能在入队时发现,这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  • 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

4.6 Redis事务其他实现

  • 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行,
    其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完。

5. 其他

5.1 redis是单线程会什么这么快?

  • Redis是纯内存操作,执行速度非常快
  • 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
  • 使用I/O多路复用模型,非阻塞IO
  • Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度

5.2 解释一下IO多路复用

IO多路复用就是实现了高效的网络请求。

5.2.1 IO模型

  • Linux系统中一个进程使用的内存情况划分两部分:内核空间、用户空间

    • 用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源必须通过内核提供的接口来访问。

    • 内核空间可以执行特权命令(Ring0),调用一切系统资源。

  • Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:

    • 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
    • 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区

image-20240502131005074

4.2.1.1 阻塞IO

两个阶段都必须阻塞等待。

  • 阶段一:
    • 用户进程尝试读取数据(比如网卡数据)
    • 此时数据尚未到达,内核需要等待数据
    • 此时用户进程也处于阻塞状态
  • 阶段二:
    • 数据到达并拷贝到内核缓冲区,代表已就绪
    • 将内核数据拷贝到用户缓冲区
    • 拷贝过程中,用户进程依然阻塞等待
    • 拷贝完成,用户进程解除阻塞,处理数据

image-20240502131130565

5.2.1.2 非阻塞IO

  • 阶段一:
    • 用户进程尝试读取数据(比如网卡数据)
    • 此时数据尚未到达,内核需要等待数据
    • 返回异常给用户进程
    • 用户进程拿到error后,再次尝试读取
    • 循环往复,直到数据就绪
  • 阶段二:
    • 将内核数据拷贝到用户缓冲区
    • 拷贝过程中,用户进程依然阻塞等待
    • 拷贝完成,用户进程解除阻塞,处理数据

image-20240502131145658

  • 非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。
  • 虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致CPU空转,CPU使用率暴增

5.2.1.3 IO多路复用

利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。

  • 阶段一:

    • 用户进程调用select,指定要监听的Socket集合
    • 内核监听对应的多个socket
    • 任意一个或多个socket数据就绪则返回readable
    • 此过程中用户进程阻塞
  • 阶段二:

    • 用户进程找到就绪的socket
    • 依次调用recvfrom读取数据
    • 内核将数据拷贝到用户空间
    • 用户进程处理数据

image-20240502131316993

由于在监听某个socket数据就绪的时候,会去寻找对应的socket,因此衍生出poll和epoll

select+poll

select和poll只会通知用户进程有Socket就绪,但不确定具体是哪个Socket ,需要用户进程逐个遍历Socket来确认

select+epoll

epoll则会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间

5.3 redis网络模型

Redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装, 提供了统一的高性能事件库

image-20240502131707906

5.4 redis消息队列

在消息队列中包含3个角色:

  • 消息队列:存储和管理消息,也被称为消息代理。
  • 生产者:发送消息到消息队列。
  • 消费者:从消息队列获取消息并处理消息。

5.4.1 List

  • redis的list结构是一个双向链表,很容易模拟出队列效果。
  • 队列是入口和出口不在一边,因此可以用LPUSH结合RPOP、或者RPUSH结合LPOP实现
  • 但是,当队列没有消息时pop就会返回null,并不会jvm堵塞队列那样堵塞并等待消息,因此这里应该使用BRPOP或者BLPOP来实现堵塞队列。
  • img
  • 优点:
    • 利用Redis存储,不受限于JVM内存上限
    • 基于Redis的持久化机制,数据安全性有保证
    • 可以满足消息有序性
  • 缺点:
    • 无法避免消息丢失。从消息队列取到消息,还没来得及处理就挂掉了,这个消息就消失了。
    • 只支持单消费者。

5.4.2 PubSub

  • PubSub(发布订阅)是redis2.0版本引入的消息传递模型,消费者可以订阅一个或多个channel(频道),生产者向对应channel发送消息后,所有订阅者都能收到相关消息。
  • image-20240502223358110
  • 相关命令:
    • SUBSCRIBE channel [channel] :订阅一个或多个频道
    • PUBLISH channel msg :向一个频道发送消息
    • PSUBSCRIBE pattern[pattern] :订阅与pattern格式匹配的所有频道
  • 优点:
    • 采用发布订阅模型,支持多生产、多消费
  • 缺点:
    • 不支持数据持久化。
    • 无法避免消息丢失。
    • 消息堆积有上限,超出时数据丢失。(缓存空间是有上限的)

5.4.3 Stream

  • Stream是redis5.0引入的一种新数据类型,可以实现一个功能非常完善的消息队列。

5.4.3.1 单消费模式

  • 发送命令:img

  • 使用XREAD读取

    img

    读取最新消息:

    img

  • 注意:当我们指定起始ID为$时,代表读取最新的消息,如果我们处理一条消息的过程中,又有超过1条以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题。

  • 特点:

    • 消息可回溯。不消失永久保存在队列里。
    • 一个消息可以被多个消费者读取。读完不消失的,可以多个读
    • 可以堵塞读取
    • 有消息漏读的风险

5.4.3.2 消费者组

  • 消费者组(Consumer Group):将多个消费者划分到一个组,监听同一个队列。

    image-20240502223632816

  • img

  • img

  • 消费者监听的思路:

    img

  • 特点:

    • 消息可回溯
    • 可以多消费者争抢消息,加快消费速度
    • 可以阻塞读取
    • 没有消息漏读的风险
    • 有消息确认机制,保证消息至少被消费一次

5.4.4 比较

img


面试-redis
https://baijianglai.cn/面试-redis/e3a811c2f1b6/
作者
Lai Baijiang
发布于
2024年5月2日
许可协议