Java后端面试高频问题:Redis。你会怎么回答?

不同点:

  • 列表基于链表实现,获取两端元素速度快,访问中间元素速度慢;

  • 有序集合基于散列表和跳跃表实现,访问中间元素时间复杂度是OlogN;

  • 列表不能简单的调整某个元素的位置,有序列表可以(更改元素的分数);

  • 有序集合更耗内存。

Redis事务


事务的原理是将一个事务范围内的若干命令发送给Redis,然后再让Redis依次执行这些命令。

事务的生命周期:

  1. 使用MULTI开启一个事务

  2. 在开启事务的时候,每次操作的命令将会被插入到一个队列中,同时这个命令并不会被真的执行

  3. EXEC命令进行提交事务

一个事务范围内某个命令出错不会影响其他命令的执行,不保证原子性:

WATCH命令

WATCH命令可以监控一个或多个键,一旦其中有一个键被修改,之后的事务就不会执行(类似于乐观锁)。执行EXEC命令之后,就会自动取消监控。

比如上面的代码中:

  1. watch name开启了对name这个key的监控

  2. 修改name的值

  3. 开启事务a

  4. 在事务a中设置了name和gender的值

  5. 使用EXEC命令进提交事务

  6. 使用命令get gender发现不存在,即事务a没有执行

使用UNWATCH可以取消WATCH命令对key的监控,所有监控锁将会被取消。

持久化机制


持久化就是把内存的数据写到磁盘中,防止服务宕机导致内存数据丢失。

Redis支持两种方式的持久化,一种是RDB的方式,一种是AOF的方式。前者会根据指定的规则定时将内存中的数据存储在硬盘上,而后者在每次执行完命令后将命令记录下来。一般将两者结合使用。

RDB方式

RDB是 Redis 默认的持久化方案。RDB持久化时会将内存中的数据写入到磁盘中,在指定目录下生成一个dump.rdb文件。Redis 重启会加载dump.rdb文件恢复数据。

bgsave是主流的触发 RDB 持久化的方式,执行过程如下:

  1. 执行BGSAVE命令Redis

  2. 父进程判断当前是否存在正在执行的子进程,如果存在,BGSAVE命令直接返回。

  3. 父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞。

  4. 父进程fork完成后,父进程继续接收并处理客户端的请求,而子进程开始将内存中的数据写进硬盘的临时文件

  5. 当子进程写完所有数据后会用该临时文件替换旧的 RDB 文件

Redis启动时会读取RDB快照文件,将数据从硬盘载入内存。通过 RDB 方式的持久化,一旦Redis异常退出,就会丢失最近一次持久化以后更改的数据。

触发 RDB 持久化的方式:

  1. 手动触发:用户执行SAVE或BGSAVE命令。SAVE命令执行快照的过程会阻塞所有客户端的请求,应避免在生产环境使用此命令。BGSAVE命令可以在后台异步进行快照操作,快照的同时服务器还可以继续响应客户端的请求,因此需要手动执行快照时推荐使用BGSAVE命令。

  2. 被动触发:

  • 根据配置规则进行自动快照,如SAVE 100 10,100秒内至少有10个键被修改则进行快照。如果

  • 从节点执行全量复制操作,主节点会自动执行BGSAVE生成 RDB 文件并发送给从节点。

  • 默认情况下执行shutdown命令时,如果没有开启 AOF 持久化功能则自动执行·BGSAVE·。

优点:

Redis 加载 RDB 恢复数据远远快于 AOF 的方式。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能。

缺点:

  • RDB方式数据无法做到实时持久化。因为BGSAVE每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本比较高。

  • RDB 文件使用特定二进制格式保存,Redis 版本升级过程中有多个格式的 RDB 版本,存在老版本 Redis 无法兼容新版 RDB 格式的问题

AOF方式

AOF(append only file)持久化:以独立日志的方式记录每次写命令,Redis重启时会重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,AOF 是Redis持久化的主流方式。

默认情况下Redis没有开启AOF方式的持久化,可以通过appendonly参数启用:appendonly yes。开启AOF方式持久化后每执行一条写命令,Redis就会将该命令写进aof_buf缓冲区,AOF缓冲区根据对应的策略向硬盘做同步操作。

默认情况下系统每30秒会执行一次同步操作。为了防止缓冲区数据丢失,可以在Redis写入AOF文件后主动要求系统将缓冲区数据同步到硬盘上。可以通过appendfsync参数设置同步的时机。

appendfsync always //每次写入aof文件都会执行同步,最安全最慢,不建议配置

appendfsync everysec //既保证性能也保证安全,建议配置

appendfsync no //由操作系统决定何时进行同步操作

接下来看一下 AOF 持久化执行流程:

  1. 所有的写入命令会追加到 AOP 缓冲区中。

  2. AOF 缓冲区根据对应的策略向硬盘同步。

  3. 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩文件体积的目的。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。

  4. 当 Redis 服务器重启时,可以加载 AOF 文件进行数据恢复。

优点:

  • AOF可以更好的保护数据不丢失,可以配置 AOF 每秒执行一次fsync操作,如果Redis进程挂掉,最多丢失1秒的数据。

  • AOF以append-only的模式写入,所以没有磁盘寻址的开销,写入性能非常高。

缺点:

  • 对于同一份文件AOF文件比RDB数据快照要大。

  • 数据恢复比较慢。

RDB和AOF如何选择?


通常来说,应该同时使用两种持久化方案,以保证数据安全。

  • 如果数据不敏感,且可以从其他地方重新生成,可以关闭持久化。

  • 如果数据比较重要,且能够承受几分钟的数据丢失,比如缓存等,只需要使用RDB即可。

  • 如果是用做内存数据,要使用Redis的持久化,建议是RDB和AOF都开启。

  • 如果只用AOF,优先使用everysec的配置选择,因为它在可靠性和性能之间取了一个平衡。

当RDB与AOF两种方式都开启时,Redis会优先使用AOF恢复数据,因为AOF保存的文件比RDB文件更完整。

Redis常见的部署方式有哪些?


Redis的几种常见使用方式包括:

  • 单机版

  • Redis主从

  • Redis Sentinel(哨兵)

  • Redis Cluster

使用场景:

  • 单机版:很少使用。存在的问题:1、内存容量有限 2、处理能力有限 3、无法高可用。

  • 主从模式:master 节点挂掉后,需要手动指定新的 master,可用性不高,基本不用。

  • 哨兵模式:master 节点挂掉后,哨兵进程会主动选举新的 master,可用性高,但是每个节点存储的数据是一样的,浪费内存空间。数据量不是很多,集群规模不是很大,需要自动容错容灾的时候使用。

  • Redis cluster:主要是针对海量数据+高并发+高可用的场景,如果是海量数据,如果你的数据量很大,那么建议就用Redis cluster,所有主节点的容量总和就是Redis cluster可缓存的数据容量。

主从复制

Redis的复制功能是支持多个数据库之间的数据同步。主数据库可以进行读写操作,当主数据库的数据发生变化时会自动将数据同步到从数据库。从数据库一般是只读的,它会接收主数据库同步过来的数据。一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。

redis-server //启动Redis实例作为主数据库

redis-server --port 6380 --slaveof 127.0.0.1 6379 //启动另一个实例作为从数据库

slaveof 127.0.0.1 6379

SLAVEOF NO ONE //停止接收其他数据库的同步并转化为主数据库。

主从复制的原理?

  1. 当启动一个从节点时,它会发送一个 PSYNC 命令给主节点;

  2. 如果是从节点初次连接到主节点,那么会触发一次全量复制。此时主节点会启动一个后台线程,开始生成一份 RDB 快照文件;

  3. 同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, 主节点会将RDB文件发送给从节点,从节点会先将RDB文件写入本地磁盘,然后再从本地磁盘加载到内存中

  4. 接着主节点会将内存中缓存的写命令发送到从节点,从节点同步这些数据;

  5. 如果从节点跟主节点之间网络出现故障,连接断开了,会自动重连,连接之后主节点仅会将部分缺失的数据同步给从节点。

哨兵Sentinel

主从复制存在不能自动故障转移、达不到高可用的问题。哨兵模式解决了这些问题。通过哨兵机制可以自动切换主从节点。

客户端连接Redis的时候,先连接哨兵,哨兵会告诉客户端Redis主节点的地址,然后客户端连接上Redis并进行后续的操作。当主节点宕机的时候,哨兵监测到主节点宕机,会重新推选出某个表现良好的从节点成为新的主节点,然后通过发布订阅模式通知其他的从服务器,让它们切换主机。

工作原理

  1. 每个Sentinel以每秒钟一次的频率向它所知道的Master,Slave以及其他 Sentinel实例发送一个 PING命令。

  2. 如果一个实例距离最后一次有效回复 PING 命令的时间超过指定值, 则这个实例会被 Sentine 标记为主观下线。

  3. 如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel要以每秒一次的频率确认Master是否真正进入主观下线状态。

  4. 当有足够数量的 Sentinel(大于等于配置文件指定值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。若没有足够数量的 Sentinel同意 Master 已经下线, Master 的客观下线状态就会被解除。 若 Master重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。

  5. 哨兵节点会选举出哨兵 leader,负责故障转移的工作。

  6. 哨兵 leader 会推选出某个表现良好的从节点成为新的主节点,然后通知其他从节点更新主节点信息。

Redis cluster

哨兵模式解决了主从复制不能自动故障转移、达不到高可用的问题,但还是存在主节点的写能力、容量受限于单机配置的问题。而cluster模式实现了Redis的分布式存储,每个节点存储不同的内容,解决主节点的写能力、容量受限于单机配置的问题。

Redis cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。

Redis cluster采用虚拟槽分区,所有的键根据哈希函数映射到0~16383个整数槽内,每个节点负责维护一部分槽以及槽所映射的键值数据。

哈希槽是如何映射到 Redis 实例上的?

  1. 对键值对的key使用 crc16 算法计算一个结果将结果

  2. 对 16384 取余,得到的值表示 key 对应的哈希槽

  3. 根据该槽信息定位到对应的实例

优点:

  • 无中心架构,支持动态扩容;

  • 数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布

  • 高可用性。部分节点不可用时,集群仍可用。集群模式能够实现自动故障转移(failover),节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色转换。

缺点:

  • 不支持批量操作(pipeline)。

  • 数据通过异步复制,不保证数据的强一致性

  • 事务操作支持有限,只支持多key在同一节点上的事务操作,当多个key分布于不同的节点上时无法使用事务功能。

  • key作为数据分区的最小粒度,不能将一个很大的键值对象如hash、list等映射到不同的节点

  • 不支持多数据库空间,单机下的Redis可以支持到16个数据库,集群模式下只能使用1个数据库空间。

哈希分区算法有哪些?

节点取余分区

使用特定的数据,如Redis的键或用户ID,对节点数量N取余:hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上。优点是简单性。扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况。

一致性哈希分区

为系统中每个节点分配一个token,范围一般在0~232,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。

虚拟槽分区

所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。Redis Cluser采用虚拟槽分区算法

过期键的删除策略?


  • 被动删除。在访问key时,如果发现key已经过期,那么会将key删除。

  • 主动删除。定时清理key,每次清理会依次遍历所有DB,从db随机取出20个key,如果过期就删除,如果其中有5个key过期,那么就继续对这个db进行清理,否则开始清理下一个db。

  • 内存不够时清理。Redis有最大内存的限制,通过maxmemory参数可以设置最大内存,当使用的内存超过了设置的最大内存,就要进行内存释放, 在进行内存释放的时候,会按照配置的淘汰策略清理内存。

内存淘汰策略有哪些?


当Redis的内存超过最大允许的内存之后,Redis 会触发内存淘汰策略,删除一些不常用的数据,以保证Redis服务器正常运行。

Redis4.0前提供 6 种数据淘汰策略:

  • volatile-lru:LRU(Least Recently Used),最近使用。利用LRU算法移除设置了过期时间的key

  • allkeys-lru:当内存不足以容纳新写入数据时,从数据集中移除最近最少使用的key

  • volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰

  • volatile-random:从已设置过期时间的数据集中任意选择数据淘汰

  • allkeys-random:从数据集中任意选择数据淘汰

  • no-eviction:禁止删除数据,当内存不足以容纳新写入数据时,新写入操作会报错

Redis4.0后增加以下两种:

  • volatile-lfu:LFU,Least Frequently Used,最少使用,从已设置过期时间的数据集中挑选最不经常使用的数据淘汰。

  • allkeys-lfu:当内存不足以容纳新写入数据时,从数据集中移除最不经常使用的key。

内存淘汰策略可以通过配置文件来修改,相应的配置项是maxmemory-policy,默认配置是noeviction。

如何保证缓存与数据库双写时的数据一致性?


  1. 先删除缓存再更新数据库进行更新操作时,先删除缓存,然后更新数据库,后续的请求再次读取时,会从数据库读取后再将新数据更新到缓存。

存在的问题:删除缓存数据之后,更新数据库完成之前,这个时间段内如果有新的读请求过来,就会从数据库读取旧数据重新写到缓存中,再次造成不一致,并且后续读的都是旧数据。

  1. 先更新数据库再删除缓存进行更新操作时,先更新MySQL,成功之后,删除缓存,后续读取请求时再将新数据回写缓存。

存在的问题:更新MySQL和删除缓存这段时间内,请求读取的还是缓存的旧数据,不过等数据库更新完成,就会恢复一致,影响相对比较小。

  1. 异步更新缓存数据库的更新操作完成后不直接操作缓存,而是把这个操作命令封装成消息扔到消息队列中,然后由Redis自己去消费更新数据,消息队列可以保证数据操作顺序一致性,确保缓存系统的数据正常。

缓存穿透


缓存穿透是指查询一个不存在的数据,由于缓存是不命中时被动写的,如果从DB查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到DB去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了。

  1. 缓存空值,不会查数据库。

  2. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,查询不存在的数据会被这个bitmap拦截掉,从而避免了对DB的查询压力。

布隆过滤器的原理:当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。查询时,将元素通过散列函数映射之后会得到k个点,如果这些点有任何一个0,则被检元素一定不在,直接返回;如果都是1,则查询元素很可能存在,就会去查询Redis和数据库。

缓存雪崩


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

解决方法:在原有的失效时间基础上增加一个随机值,使得过期时间分散一些。

缓存击穿


缓存击穿:大量的请求同时查询一个 key 时,此时这个 key 正好失效了,就会导致大量的请求都落到数据库。缓存击穿是查询缓存中失效的 key,而缓存穿透是查询不存在的 key。

解决方法:加分布式锁,第一个请求的线程可以拿到锁,拿到锁的线程查询到了数据之后设置缓存,其他的线程获取锁失败会等待50ms然后重新到缓存取数据,这样便可以避免大量的请求落到数据库。

public String get(String key) {

String value = redis.get(key);

if (value == null) { //缓存值过期

String unique_key = systemId + “:” + key;

//设置30s的超时

if (redis.set(unique_key, 1, ‘NX’, ‘PX’, 30000) == 1) { //设置成功

value = db.get(key);

redis.set(key, value, expire_secs);

redis.del(unique_key);

} else { //其他线程已经到数据库取值并回写到缓存了,可以重试获取缓存值

sleep(50);

get(key); //重试

}

} else {

return value;

}

}

Redis 怎么实现消息队列?


使用一个列表,让生产者将任务使用LPUSH命令放进列表,消费者不断用RPOP从列表取出任务。

BRPOP和RPOP命令相似,唯一的区别就是当列表没有元素时BRPOP命令会一直阻塞连接,直到有新元素加入。

BRPOP queue 0 //0表示不限制等待时间

优先级队列如果多个键都有元素,则按照从左到右的顺序取元素。

BLPOP queue:1 queue:2 queue:3 0

发布/订阅模式

PSUBSCRIBE channel?* 按照规则订阅。

PUNSUBSCRIBE channel?*

退订通过PSUBSCRIBE命令按照某种规则订阅的频道。其中订阅规则要进行严格的字符串匹配,PUNSUBSCRIBE* 无法退订channel?*规则。

PUBLISH channel1 hi

SUBSCRIBE channel1

UNSUBSCRIBE channel1 //退订通过SUBSCRIBE命令订阅的频道。

缺点:在消费者下线的情况下,生产的消息会丢失。

延时队列

使用sortedset,拿时间戳作为score,消息内容作为key,调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

pipeline的作用?


redis客户端执行一条命令分4个过程: 发送命令、命令排队、命令执行、返回结果。使用pipeline可以批量请求,批量返回结果,执行速度比逐条执行要快。

使用pipeline组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline命令完成。

原生批命令(mset和mget)与pipeline对比:

  • 原生批命令是原子性,pipeline是非原子性。pipeline命令中途异常退出,之前执行成功的命令不会回滚。

  • 原生批命令只有一个命令,但pipeline支持多命令。

最后

整理的这些资料希望对Java开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

image

image

其实面试这一块早在第一个说的25大面试专题就全都有的。以上提及的这些全部的面试+学习的各种笔记资料,我这差不多来回搞了三个多月,收集整理真的很不容易,其中还有很多自己的一些知识总结。正是因为很麻烦,所以对以上这些学习复习资料感兴趣

加入社区:https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
pipeline的作用?


redis客户端执行一条命令分4个过程: 发送命令、命令排队、命令执行、返回结果。使用pipeline可以批量请求,批量返回结果,执行速度比逐条执行要快。

使用pipeline组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline命令完成。

原生批命令(mset和mget)与pipeline对比:

  • 原生批命令是原子性,pipeline是非原子性。pipeline命令中途异常退出,之前执行成功的命令不会回滚。

  • 原生批命令只有一个命令,但pipeline支持多命令。

最后

整理的这些资料希望对Java开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

[外链图片转存中…(img-GERVX6CZ-1725477748675)]

[外链图片转存中…(img-iwVGz4vo-1725477748675)]

其实面试这一块早在第一个说的25大面试专题就全都有的。以上提及的这些全部的面试+学习的各种笔记资料,我这差不多来回搞了三个多月,收集整理真的很不容易,其中还有很多自己的一些知识总结。正是因为很麻烦,所以对以上这些学习复习资料感兴趣

加入社区:https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值