Java八股

Redis篇

redis使用场景

其他相关问题

如何解决缓存穿透,击穿,雪崩问题?

缓存穿透:查询一个不存在的数据,mysql查询不到数据,就会导致每次请求都查询数据库,给数据库带来很大的压力

解决方案:1.缓存空数据,占用内存,并且导致数据不一致。

2.布隆过滤器,主要是用于检索一个元素是否在一个集合中,使用的是redisson实现的布隆过滤器。底层是先去初始化一个比较大数组,里面存放二进制0或1,在一开始都是0,当一个key来了之后经过3次hash计算,根据数据长度取模找到数据的下标,将数组中原来的0改成1,这样的话,三个数组的位置就能标明一个key的存在。布隆过滤器有可能会产生一定的误判,可以设置这个误判率,大概率不会超过5%。

缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发请求可能会瞬间把数据库压垮。

解决方案:1.互斥锁(强一致,性能差),当缓存失效时,先使用Redis的setnx去设置一个互斥锁,再去查找数据库并且重建缓存,释放锁。有可能产生死锁的问题

2.逻辑过期(高可用,性能优),在设置key的时候,设置一个过期时间字段一起存入缓存,不给当前key设置过期时间。

缓存击穿

缓存雪崩:在同一时间段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:1.给不同的key的TTL添加随机值。2.利用Redis集群提高服务的可用性。3.给缓存业务添加降级限流策略。4.给业务添加多级缓存

mysql的数据如何与redis进行同步呢?(强一致性)

数据同步

使用的是Redisson实现的读写锁。在读的时候添加共享锁,其他线程可以共享读操作。当我们更新数据的时候,添加排他锁,阻塞其他线程读写操作,这样就能保证在写数据的同时,是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是,读方法和写方法上需要使用同一把锁才行。

其实排他锁底层使用的也是SETNX,它保证了同时只能有一个线程操作锁住的方法。

延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据。其中,这个延时多久不太好确定。在延时的过程中,可能会出现脏数据,并不能保证强一致性,所以没有采用它。

mysql的数据如何与redis进行同步呢?(延迟一致性)

使用MQ中间件,更新数据之后,通知缓存删除

redis的数据持久化

数据持久化

1.RDB

例子: 想象一下相机的快门点击。每当你点击快门,你都会捕捉到那个特定时刻的场景。RDB的工作方式很相似,只不过它捕捉的是数据的状态。

Redis 主进程(父进程)会执行 fork 操作来创建一个子进程,子进程会获得父进程内存中的数据副本,父进程可以立即返回处理其他客户端请求,而不需要等待 RDB 的生成过程,操作系统使用写时复制(Copy-On-Write, COW)技术,任何在父进程(Redis主进程)上发生的写操作不会影响子进程中的数据。这确保了子进程中的数据是隔离的,不受父进程中数据更改的影响。当 Redis 重新启动时,如果配置为使用 RDB 持久化,它会查找 RDB 文件,并加载它。由于 RDB 文件是一个紧凑的二进制表示形式,数据加载非常快

2.AOF

例子:想象你正在写日记,每次有新的事件,你就写下来。AOF 就像是 Redis 的日记,记录了所有的写操作命令。

AOF 持久化的实现主要是以上三步:命令追加、文件写入、文件同步

  • 命令追加: 将 redis 写操作命令追加到 aof_buf 缓冲区
  • 文件写入: 周期性地将 aof_buf 缓冲区的命令写入 AOF 文件的内核缓冲区。
  • 文件同步:根据配置同步策略,将 AOF 文件缓冲区的内容同步到磁盘。

其中文件同步策略 redis 提供了三种,分别是以下三种:

always:每次有命令写入时都立即同步。这提供了最高的数据安全性,但效率最低。

everysec:每秒同步一次。这是一个权衡安全性和效率的策略。最多只丢失 1 秒 的数据

no:让操作系统决定最佳的同步时间。这可能导致数据丢失,但提供了最高的效率。

这里顺带提个问题:假如 Redis 的 RDB 和 AOF 持久化都启用,redis 在载入数据的时候,是载入 AOF 文件?还是 RDB 文件?

我直接说答案:Redis 会优先载入 AOF 文件来恢复数据,而不是 RDB 文件。这是因为 AOF 文件通常包含了更完整的操作记录,从而能够恢复更完整的数据状态。而 RDB 文件是定时生成的数据快照,所以它可能没有记录到最后一次快照之后发生的所有更改。因此,使用 AOF 文件恢复数据可以提供更高的数据完整性。

数据过期策略

1.惰性删除,在设置该key过期时间后,我们不去管它。当需要该key时,我们检查其是否过期。如果过期,我们就删掉它;反之,返回该key。

2.定期删除,每隔一段时间,我们就对一些key进行检查,并删除里面过期的key。定期清理的两种模式是:1) SLOW模式,是定时任务,执行频率默认为10hz,每次不超过25ms,可以通过修改配置文件redis.conf的hz选项来调整这个次数;2) FAST模式,执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms。

Redis的过期删除策略是:惰性删除 + 定期删除两种策略配合使用。

假如缓存过多,内存被占满了怎么办?

默认是noeviction,不删除任何数据,内部不足时直接报错。这个可以在redis的配置文件中进行设置。

里面有两个非常重要的概念:一个是LRU,另外一个是LFU。LRU的意思就是最少最近使用。它会用当前时间减去最后一次访问时间。这个值越大,则淘汰优先级越高。LFU的意思是最少频率使用。它会统计每个key的访问频率。值越小,淘汰优先级越高。我们在项目中设置的是allkeys-lru,它会挑选最近最少使用的数据进行淘汰,把一些经常访问的key留在redis中。

Redis分布式锁如何实现?

在redis中提供了一个命令SETNX(SET if not exists)。由于redis是单线程的,用了这个命令之后,只能有一个客户端对某一个key设置值。在没有过期或删除key的时候,其他客户端是不能设置这个key的。

redis的SETNX指令不好控制这个问题。我们当时采用的是redis的一个框架Redisson实现的(底层是setnxlua脚本)保证原子性。在Redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间。当锁住的一个业务还没有执行完成的时候,Redisson会引入一个看门狗机制。就是说,每隔一段时间就检查当前业务是否还持有锁。如果持有,就增加加锁的持有时间。当业务执行完成之后,需要使用释放锁就可以了。还有一个好处就是,在高并发下,一个业务有可能会执行很快。客户1持有锁的时候,客户2来了以后并不会马上被拒绝。它会自旋不断尝试获取锁。如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。

是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计数上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数。

这个是不能的。比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时如果当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。

我们可以利用Redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个Redis实例上创建锁,应该是在多个Redis实例上创建锁,并且要求在大多数Redis节点上都成功创建锁,红锁中要求是Redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。

但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变得非常低,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。

Redis本身就是支持高可用的,要做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用ZooKeeper实现的分布式锁,它是可以保证强一致性的。

Redis集群方案?

主从复制

主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中。

主从同步分为了两个阶段,一个是全量同步,一个是增量同步

全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:

第一:从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。

第二:主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。

第三:在同时主节点会执行BGSAVE,生成RDB文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的RDB文件,这样就保持了一致。

当然,如果在RDB生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步。

增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步。

哨兵模式

保证了redis集群的高可用性,实现主从集群的自动故障恢复

首先可以搭建主从集群,再加上使用Redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证Redis的高并发高可用。

由于Redis master节点和Redis slave节点和Sentinel处于不同的网络分区,使得Sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个slave为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,Sentinel会将old master降为slave,这时再从新master同步数据,这会导致old master中的大量数据丢失。

关于解决的话,我记得在Redis的配置中可以设置:第一可以设置最少的slave节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。

Redis分片集群有什么作用,其中如何存储和读取的?

分片集群主要解决的是海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点。

Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围,key通过CRC16校验后对16384取模来决定放置哪个槽,通过槽找到对应的节点进行存储。