typora-copy-images-to: imgs

了解 redis 中的大key吗?多大算是大key呢?如何解决?

答:

redis 的大 key 指的是 key 对应的 value 所占用的内存比较大。

对于 string 类型来说,一般情况下超过 10KB 则认为是大 key;对于set、zset、hash 等类型来说,一般数据超过5000条即认为是大 key

bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。应该避免系统中出现 bigkey。

查找大key:

  1. 使用命令 redis-cli -p 6379 --bigkeys -i 3 在 redis 中查找大 key,通过 -i 控制扫描频率,表示扫描过程中每次扫描后休息的时间间隔为 3 秒。
  2. 分析 RDB 文件来找出大 key。

解决大key:

  1. 将大key切割为多个小key。(不推荐,这样需要修改业务代码)
  2. 如果大key不是热点key,手动删除。redis4.0+ 使用 unlink 命令异步删除传入的key

了解 redis 中的热 key 吗?

答:

热 key 指的是在 redis 接收的读写请求中,某个 key 就占了一般甚至更多的请求,称为热 key。

处理热 key 会占用大量的 CPU 和带宽,如果在某一时间内大量访问热key,请求数量超过 redis 的处理能力或者热key正好在缓存中过期,就会导致线上服务崩溃。

需要对系统中的热 key 进行优化,确保系统的高可用和稳定性。

查找热key:

  1. 使用 redis-cli --hotkeys 来查找热key,使用该命令时,需要设置 maxmemory-policy 为 LRU 算法,否则会报错。
  2. 使用 monitor 命令来实时查看 redis 实例的操作情况,包括读写删除等操作。
    该命令对 redis 性能影响较大,谨慎使用。
    使用 monitor 命令并输出重定向至文件,在关闭 monitor 命令后对文件进行分析,即可找到这段时间内的热 key。

解决热key:

  1. 使用二级缓存。将热 key 存储一份到 JVM 本地内存中(可以使用Caffeine)
  2. 使用 redis 集群:在多个 redis 实例上都存储一份热 key,分散热 key 的访问请求
  3. 读写分离:主节点处理写请求,从节点处理读请求

redis用的什么版本,6.0多线程体现在哪里?redis的性能瓶颈在哪里?

答:自己项目中的 redis 使用的 6.2.6版本。redis的性能瓶颈在于内存、网络IO处理速度。

redis6.0 引入了多线程,引入多线程的目的是为了提高网络IO的处理性能.

redis 的多线程只是用来处理网络请求,对于读写命令,还是使用单线程进行处理,因此并不存在线程安全的问题。

6.0多线程默认关闭,在 redis.conf 中设置 io-threads-do-reads yes 进行开启,并且设置线程个数io-threads 6

主线程与IO线程的协作流程:

  1. 服务端和客户端建立Socket连接,并分配处理线程
    主线程负责接收建立连接请求。当有客户端请求和实例建立Socket连接时,主线程会创建和客户端的连接,并把 Socket 放入全局等待队列中。 紧接着,主线程通过轮询方法把Socket连接分配给IO线程。
  2. IO线程读取并解析请求
    主线程一旦把Socket分配给IO线程,就会进入阻塞状态,等待IO线程完成客户端请求读取和解析。因为有多个IO线程在并行处理,所以,这个过程很快就可以完成。
  3. 主线程执行请求操作
    等到IO线程解析完请求,主线程还是会以单线程的方式执行这些命令操作

常见面试题-Redis专栏(一)_redis

谈谈Redis 的持久化策略?

Redis的确是将数据存储在内存的,但是也会有相关的持久化机制将内存持久化备份到磁盘,以便于重启时数据能够重新恢复到内存中,避免数据丢失的风险。而Redis持久化机制有三种:AOFRDB混合型持久化(4.x版本后提供)

  • RDB持久化
    关闭 RDB 持久化只需要将 save 保存策略注释掉即可RDB持久化的方式有两种:
  • 手动save:阻塞当前 Redis,直到持久化完成,可能造成长时间阻塞,线上不建议使用。
    Redis 中 save 操作的配置:从右向左条件主键变弱,如果60s发生了10000次写操作,就进行持久化,如果没有达到,在300s时,如果有100次写操作就会持久化,如果没有达到在3600s,如果有一次写操作就会持久化
  • 手动bgsave:Redis 进程执行 fork 创建子进程进行持久化,阻塞事件很短。在执行Redis-cli shutdown关闭Redis服务时或执行flushall命令时,如果没有开启AOF持久化,自动执行bgsave

bgsave子进程工作原理:

由子进程继承父进程所有资源,且父进程不能拒绝子进程继承,bgsave子进程先将内存中的全量数据copy到磁盘的一个RDB临时文件,持久化完成后将该临时文件替换原来的dump.rdb文件。

如果持久化过程中出现了新的写请求,则系统会将内存中发生数据修改的物理块copy出一个副本,bgsave 子进程会把这个副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据,fork 使用了 写时复制技术(Copy-On-Write)

写时复制技术:

目的是避免不必要的内存拷贝。

在Linux系统中,调用 fork 系统调用创建子进程时,并不会把父进程所有占用的内存页复制一份,而是与父进程共用相同的内存页,而当子进程或者父进程对内存页进行修改时才会进行复制 —— 这就是著名的 写时复制 机制。

那么bgsave中的写时复制技术即如果在持久化过程中,写入了新的数据,此时再去将元数据重新拷贝一份进行修改。

优点:

  • 使用单独子进程持久化,保证 redis 高性能。
  • RDB 持久化存储压缩的二进制文件,适用于备份、全量复制,可用于灾难备份,同时RDB文件的加载速度远超于AOF文件。

缺点:

  • 没有实时持久化,可能造成数据丢失。
  • 备份时占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(需要的内存是原本的两倍)
  • RDB文件保存的二进制文件存在新老版本不兼容的问题。
  • AOF持久化
    默认AOF没有开启,可在redis.conf中配置

Redis7发生了重大变化,原来只有一个appendonly.aof文件,现在具有了三类多个文件:

  • 基本文件:RDB格式或AOF格式。存放RDB转为AOF当时内存的快照数据。该文件可以有多个。
  • 增量文件:以操作日志形式记录转为AOF后的写入操作。该文件可以有多个。
  • 清单文件:维护AOF文件的创建顺序,保证激活时的应用顺序。该文件只可以有1个。

aof 文件中存储的 resp 协议数据格式,如果执行命令set a hello,aof文件内容如下:(*3代表有3条命令,$5代表有5个字符)

*3
$3
set
$1
a
$5
hello

AOF持久化时,其实是先写入缓存中,之后再同步到磁盘中,同步策略有三种:

  • appendfsync always:每次写入都同步到磁盘,最安全,但影响性能。
  • appendfsync everysec(推荐、默认配置):每秒同步一次,最多丢失1秒的数据。
  • `appendfsync noRedis并不直接调用文件同步,而是交给操作系统来处理,操作系统可以根据buffer填充情况/通道空闲时间等择机触发同步;这是一种普通的文件操作方式。性能较好,在物理服务器故障时,数据丢失量会因OS配置有关。

优点:

  • 数据丢失风险较低,后台线程处理持久化,不影响客户端请求处理的线程。

缺点:

  • 文件体积由于保存的是所有命令会比RDB大上很多,而且数据恢复时也需要重新执行指令,在重启时恢复数据的时间往往会慢很多。

AOF的重写(Rewrite)机制:

  • 为了防止AOF文件太大占用大量磁盘空间,降低性能,Redis引入了Rewrite机制对AOF文件进行压缩
    Rewrite就是对AOF文件进行重写整理。当开启Rewrite,主进程redis-server创建出一个子进程bgrewriteaof,由该子进程完成rewrite过程。
    首先会对现有aof文件进行重写,将计算结果写到一个临时文件,写入完毕后,再重命名为原aof文件,进行覆盖。

配置AOF重写频率

# auto‐aof‐rewrite‐min‐size 64mb //aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就很快,重写的意义不大
# auto‐aof‐rewrite‐percentage 100 //aof文件自上一次重写后文件大小增长了100%则再次触发重写

AOF的持久化流程图:

常见面试题-Redis专栏(一)_数据_02

  • 混合持久化开启
    默认开启,即AOF持久化的基本文件时的基本文件是RDB格式的。(必须先开启aof)

混合持久化重写aof文件流程:aof 在重写时,不再将内存数据转为 resp 数据写入 aof 文件,而是将之前的内存数据做 RDB 快照处理,将 RDB快照+AOF增量数据 存在一起写入新的 AOF 文件,完成后覆盖原有的 AOF 文件。

Redis重启加载数据流程:

  1. 先加载 RDB 数据到内存中
  2. 再重放增量 AOF 日志,加载 AOF 增量数据

优点:

  • 结合了 RDB 和 AOF,既保证了重启 Redis 的性能,又降低数据丢失风险

缺点:

  • AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;

Redis 的数据备份策略

  1. 写crontab定时调度脚本,每小时都copy一份rdb或aof的备份到一个目录中去,仅仅保留最近48小时的备份
  2. 每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份
  3. 每次copy备份的时候,都把太旧的备份给删了
  4. 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏

Redis内存过期策略

答:

Redis中提供了8种内存淘汰策略:

volatile-lru:针对设置了过期时间的key,使用LRU算法进行淘汰 allkeys-lru:针对所有key使用LRU算法进行淘汰 volatile-lfu:针对设置了过期时间的key,使用LFU算法进行淘汰 allkeys-lfu:针对所有key使用LFU算法进行淘汰 volatile-random:从设置了过期时间的key中随机删除

allkeys-random:从所有key中随机删除 volatile-ttl:删除生存时间最近的一个键 noeviction(默认策略):不删除键,返回错误OOM,只能读取不能写入

总结一下:也就是从过期的key或者所有的key中使用 LRU或者LFU或者随机 删除策略进行淘汰

缓存雪崩是什么?如何解决?

答:缓存雪崩造成的原因是:大量缓存数据在同一时间过期或者Redis宕机,此时如果有大量的请求无法在Redis中处理,会直接访问数据库,从而导致数据库的压力骤增,甚至数据库宕机

出现原因:缓存过期、Redis故障

缓存过期解决:

  1. 给过期时间加上一个随机数
  2. 互斥锁,当缓存失效时,加互斥锁,保证同一时间只有一个请求来构建缓存
  3. 缓存预热,在系统启动前,提前将热点数据加载到缓存中,避免大量请求同时访问数据库

Redis故障解决:

  1. 服务熔断或请求限流
  2. 构建Redis缓存高可靠集群

缓存穿透是什么?如何解决?

答:缓存穿透造成的原因是访问数据库中不存在的数据,即数据库和缓存都不命中

缓存穿透就是访问大量数据库中不存在的设备,每次都需要去数据库中查询,失去了缓存保护后端存储的意义。

造成原因:

  • 自身代码问题
  • 恶意攻击

解决方案有两种:

  1. 如果访问数据库中不存在的数据,则将该数据设置为字符串{}并且放入缓存,避免访问不存在的数据而大量请求打到数据库,缓存 kv 格式为:empty_cache_key: {}
// 设置key为空缓存
redisUtil.set(productCacheKey, "{}", 60 + new Random().nextInt(30), TimeUnit.SECONDS);
// 如果访问到空缓存,重新刷新空缓存的过期时间
redisUtil.expire(productCacheKey, 60 + new Random().nextInt(30), TimeUnit.SECONDS);
  1. 布隆过滤器:在使用布隆过滤器时,先将所有数据hash到一个位图中,之后接收客户端请求时,先去布隆过滤器中判断数据是否存在,如果不存在,则直接返回空,不会请求数据库。
    在SpringBoot中,我们可以使用Guava提供的布隆过滤器实现缓存穿透的解决方案。

缓存击穿是什么?如何解决?

答:缓存击穿造成的原因是:热点key失效

同一时间批量添加数据,并且数据的过期时间相同,大量数据同一时间缓存失效可能导致大量请求直达数据库,如果请求过多,数据库会挂掉。

解决:

批量添加数据的话,在设置的过期时间上再加上一个随机时间即可。

// 过期时间 = cache_timeout + new Random().nextInt(5) * 60 * 60;

还可通过互斥锁解决

Redis 的 key 删除策略了解吗?

答:

  • 惰性删除:key过期后任然留在内存中不做处理,当有请求操作这个key的时候,会检查这个key是否过期,如果过期则删除,否则返回key对应的数据信息。(惰性删除对CPU是友好的,因为只有在读取的时候检测到过期了才会将其删除。但对内存是不友好,如果过期键后续不被访问,那么这些过期键将积累在缓存中,对内存消耗是比较大的。)
  • 定期删除:Redis数据库默认每隔100ms就会进行随机抽取一些设置过期时间的key进行检测,过期则删除。(定期删除是定时删除和惰性删除的一个折中方案。可以根据实际场景自定义这个间隔时间,在CPU资源和内存资源上作出权衡。)
  • Redis默认采用定期+惰性删除策略。

Redis 的主从机制有哪些好处?哪里存在问题?

答:

主从机制好处:

  • 主从机制为后续的高可用机制打下了基础,可以将数据同步到多个从节点,做到灾备的效果
  • 通过主写从读的形式实现读写分离,提高 Redis 吞吐量。

主从机制同样存在一些问题:

  • 如果主节点宕机,需手动从 slave 选择一个新的 master,同时需修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点。
  • 写入性能受主节点限制
  • 木桶效应:整个Redis节点群能够存储的数据容量受到所有节点中内存最小的那台限制,比如一主两从架构:master=32GB、slave1=32GB、slave2=16GB,那么整个Redis节点群能够存储的最大容量为16GB

Redis Sentinele 有什么作用?

答:

在主从集群基础上,使用 Sentinel(哨兵) 角色来帮我们监控 Redis 节点运行状态,并自动实现故障转移,当 master 节点出现故障时,Sentinel 根据规则选一个 slave 升级为 master,确保集群可用性,在这个过程中不需要人工介入。

Redis Sentinel的主要功能是:

  • 监控 redis 节点状态是否正常
  • 故障转移,确保 redis 系统的可用性

一般主从复制+哨兵一起使用,使用3台哨兵+1个主从集群(1master,2slave)

Redis Sentinel + 主从模式 存在的问题:

  • Redis Sentinel 在主节点挂了之后,选举主节点中断时间达几秒甚至十几秒,期间无法进行写入操作
  • 只有一个主节点对外提供服务,无法提供很高的并发,并且单个节点内存不宜设置过大,导致持久化文件过大,影响数据恢复和主从同步效率

Redis 切片集群了解吗?

答:

Redis 切片集群是目前使用比较多的方案,Redis 切面集群支持多个主从集群进行横向扩容,架构如下:

常见面试题-Redis专栏(一)_Redis_03

使用切片集群有什么好处?

  • 提升 Redis 读写性能,之前的主从模式中,只有 1 个 master 可以进行写操作,在切片集群中,多个 master 承担写操作压力
  • 多个主从集群进行存储数据,比单个主从集群存储数据更多
    比如10G数据,1个主从集群的话,1个master需要存储10G,对有3个主从集群的切片集群来说,只需要master1存储3G,master2存储3G,master3存储4G即可
  • 具备主从复制、故障转移
    切片集群中的每一个主从集群中,slave 节点不支持读,只做数据备份,因为已经有其他master节点分担压力

切片集群支持水平扩容,可以无限扩容吗?

不可以,官方推荐不要超过1000个,因为各个小集群之间需要互相进行通信,如果水平节点过多,会影响通信效率。

切片集群中插入数据时,数据被放在哪个master中?

Redis 切片集群中,数据是通过 哈希槽分区 来存储的,Redis 切片集群中有 16384 个哈希槽,每一个 master 会拿到一些槽位,在向切片集群中插入数据时,会根据 key 计算对应的哈希槽,插入到对应的哈希槽中,那么计算出来的哈希槽在哪个master中,数据当然也就被存放在对应的master上。(在切片集群中,只有master节点才有插槽,slave节点没有插槽)

切片集群中如果一个master挂了,如何选举主节点?

当 master 挂了之后,该 master 下的所有 slave 会向所有节点广播 FAILOVER_AUTH_REQUEST 信息,其他节点收到后,只有 master 响应,master 会相应第一个收到的 FAILOVER_AUTH_REQUEST 信息,并返回一个 ack,尝试发送 failover 的 slave 会收集 master 返回的 FAILOVER_AUTH_ACK,当 slave 收到超过半数的 master 的 ack 后,就会变成新的 master。

这样会导致 slave 一直没收到超过半数的 master 的 ack,难道要一直选举吗?

其实不会导致 slave 一直选举的,因为在 slave 知道 master 挂了之后,会经过一个延时时间 delay 之后再去给所有节点发送选举消息

延迟时间计算:delay = 500ms + random(0~500ms) + slave_rank * 1000ms (版本不同可能不一样,原理大致相同)

slave\_rank表示slave已经从master复制数据的总量的rank,rank越小,表示复制的数据越新,该slave节点也就越先发起选举。

因此数据量越多的 slave 就越早发送选举消息,也就越早得到 master 的 ack,成为新的 master。

集群切片中的脑裂问题了解吗?如何解决?

比如一个主从集群:master1 对应两个从节点 slave1、slave2,如果master1和两个从节点出现网络分区,两个slave会选举出来一个新的master节点,客户端是可以感知到两个master,但是两个master之间因为网络分区无法感知,客户端会向这两个master都写入数据,之后如果网络分区恢复,其中一个master变为slave,就会导致数据丢失问题。

如何解决?添加redis配置:

min-slave-to-write 1

表示写数据时,写入master之后,不立即返回客户端写成功,而是去slave同步数据,取值为1表示最少同步1个slave之后,才算数据写成功,如果同步的slave节点数量没有达到我们配置的值,就算数据写失败,取值建议:集群总共3个节点,可以取1,这样集群中超过半数(1个master + 1个slave)都写入数据成功,才算写成功。

但是这个配置为了数据的一致性,牺牲了一定的集群可用性,如果一个master的所有slave都挂了,这个小集群就不可用了,master无法写入数据。

一般不使用,redis丢一点数据也影响不大,所以主要还是保证redis的可用性

集群是否完整才能对外提供服务?

当redis.conf配置 cluster-require-full-coverage no 时,表示如果一个主从集群全部挂掉之后,集群仍然可用,如果为 yes,表示不可用

比如有3个主从集群,其中一个主从集群全部瘫痪,配置为no,则整个集群仍然可以正常工作

Redis集群如何对批量操作命令的支持?

对于 mset、mget 这样的多个 key 的原生批量操作命令,redis 集群只支持所有key落在同一个slot的情况,如果多个key一定要用mset在redis集群上操作,则可以在key之前加上{XX},这样就会根据大括号中的内容计算分片hash,确保不同的key落在同一slot内,例如:

mset {user1}: name zhuge {user1}:age 18

虽然name和age计算出来的 hash slot 值不同,但是前边加上大括号{user1},redis集群就会根据 user1 计算插槽值,最后name和age可以放入同一插槽。