跟随黑马面Java-2,Redis篇

第二篇:Redis篇

“求其上,得其中;求其中,得其下,求其下,必败”

新版Java面试专题视频教程,java八股文面试全套真题+深度详解(含大厂高频面试真题)_哔哩哔哩_bilibili

开篇


引言

Redis作为目前后端开发中最常用的缓存中间件,几乎每个项目都会使用到Redis。因此,Redis在面试中占比是非常高的。

注意:此篇需要重点准备!


提问方式
  • Redis使用场景

    image-20240505210512416
    问题
    Redis的数据持久化策略有哪些
    什么是缓存穿透,怎么解决
    什么是布隆过滤器
    什么是缓存击穿,怎么解决
    什么是缓存雪崩,怎么解决
    Redis双写问题
    Redis分布式锁如何实现
    Redis实现分布式锁如何合理的控制锁的有效时长
    Redis的数据过期策略有哪些
    Redis的数据淘汰策略有哪些
  • 其他面试

    这些一般不会和业务捆绑提问,而是会单独提问

    image-20240505210613791
    问题
    Redis集群有哪些方案,知道嘛
    什么是Redis主从同步
    你们使用Redis是单点还是集群?哪种集群
    Redis分片集群中数据是怎么存储和读取的
    redis集群脑裂
    怎么保证redis的高并发高可用
    你们用过Redis的事务吗?事务的命令有哪些
    Redis是单线程的,但是为什么还那么快?

Redis使用场景面试题


注意点

使用场景需要结合实际项目


问题及回答
什么是缓存穿透,怎么解决
  • 正常的缓存操作:image-20240505211522875

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

  • 解决方案:

    • 方案1:缓存空数据,查询返回的数据为空,仍将这个空结果进行缓存。

      优点:简单

      缺点:消耗内存,可能会发生缓存不一致的问题。

    • 方案2:布隆过滤器

      image-20240505212027805

      如果布隆过滤器中查询的到数据,就允许继续查询,否则直接返回。在缓存预热的时候,需要预热布隆过滤器。

      优点:内存占用较少,没有多余的key

      缺点:实现复杂,存在误判

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

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

    image-20240505212603453
  • 布隆过滤器误判:

    数组越小越容易误判,越大越消耗内存

    image-20240505213057256
  • 布隆过滤器设置:

    现在已经有了很多布隆过滤器的实现方案,比如Redission/Guava等,在初始化布隆过滤器时,能够对其进行误判率的设置。通常一般设置在5%以内,保证数据误判率的同时,不至于在高并发场景下压倒数据库。

    image-20240505213320536 image-20240505213707065
什么是缓存击穿,怎么解决
  • 缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮

    image-20240505214303658
  • 解决方案:

    • 方案一:互斥锁

      即全部等待,直到第一个进程查询完成数据并将数据写入缓存

      image-20240505215015859
    • 方案二:逻辑过期

      热点数据不设置过期时间(这里的不设置过期时间不是指【数据在缓存中永远不会过期】,而是指定期刷新数据,让此数据的过期时间反复重置。如果设置永久过期,你不知道现在缓存数据是不是最新的,你得有一个过期时间来触发去数据库刷新最新数据到缓存中)。

      要实现此逻辑过期,存入Redis的数据还需要另外设置一个字段,这里以expire为例。

      image-20240505215637332

      expire到期后,将触发刷新,重新从数据库中获取数据,重置缓存时间。

      image-20240505215809679
  • 总结:

    如果选择方案一,能够保证数据的强一致性,也就是保证缓存中数据总是最新的。同时,此方案性能较差,因为高并发下,另外的所有线程都需要等待缓存建立。

    方案二能够保证数据的高可用性,能够使每次访问缓存都能立即返回结果。同时,性能较好

什么是缓存雪崩,怎么解决
  • 缓存雪崩:是指在同一时段大量的缓存key同时失效或者Redis服务岩机,导致大量请求到达数据库,带来巨大压力。

    image-20240505220915237
  • 同时有大量缓存过期:

    • 解决方案:给不同的key设置不同的过期时间(给不同的key的TTL添加随机值)
  • Redis宕机:

    • 解决方案:搭建Redis高可用的集群来预防(哨兵模式/集群模式)

      或者给缓存业务添加降级限流策略(nginx或者SpringCloudGateway)

      或者给业务添加多级缓存(Guava或Caffeine作为一级缓存,Redis作为二级缓存)

Redis双写问题
  • redis做为缓存,mysql的数据如何与redis进行同步呢?

    • 回答时一定要先设置前提,介绍自己的业务。
    • 介绍自己简历上的业务,我们当时是把文章的热点数据存入到了缓存中,虽然是热点数据,但是实时要求性并没有那么高,所以,我们当时采用的是异步的方案同步的数据。
    • 我们当时是把抢券的库存存入到了缓存中,这个需要实时的进行数据同步,为了保证数据的强一致,我们当时采用的是redisson提供的读写锁来保证数据的同步
  • 什么是双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据应当要保持一致。

  • 业务要求:一致性要求高

    1. 方案一:延迟双删:就是在修改数据时,先删除缓存,然后修改数据库,然后过一段时间再删除缓存。

      image-20240510213918271
      • 一般的业务逻辑中,我们是删除缓存,然后修改数据库。那么就有下面一个问题:是先删除缓存还是先修改数据库?

      • 先删除缓存还是先修改数据库?都可能出现脏数据

        1. 先删缓存:可能会出现脏数据。

          • 正常情况是:线程1删除缓存,然后立即更新数据库。线程2在数据库更新完成后进行查询,未命中缓存,才查询数据库并重建缓存。

            image-20240510221154716
          • 脏数据情况:线程1删除缓存,没来得及更新数据库,线程2就进行查询、缓存重建。这会导致后续进程读到缓存的脏数据。

            image-20240510214132681
        2. 先修改数据库:也可能导致脏数据

          • 正常情况是:线程1查询缓存,未命中(可能是已经过期),于是查询数据库并重建缓存。线程2在缓存重建后命中缓存。
          • 脏数据情况:线程1查询缓存,未命中,还没来得及重建缓存。线程2就进行更新数据同时删除缓存(缓存在查询时建立)。线程1重新切换回来并继续重建缓存。此时缓存中的数据不是经过2更新后的数据,而是脏数据。
          image-20240510215509316
      • 方案二:互斥锁

        1. 分布式锁:

          在每次读写数据的时候都加锁,防止其他线程读取。(性能低)

          image-20240510221818107
        2. 读写锁:

          在读数据的时候加共享锁,允许其他线程读但不允许其写。在写数据的时候加排他锁,不允许其他线程读写。

          image-20240510222006016
          5ad680c6894e60e791a2cdb2d026218a
          8edb7692f700872341f664689ee8f694
  • 业务要求:允许延迟

    方案:异步通知

    1. 基于MQ的异步通知:使用MQ中间件,更新数据之后,通知删除缓存。

      在进行Mysql的修改时,发一条消息给MQ,MQ将监听消息,并更新缓存。这种方案肯定是有延迟的,但是能够保证最终的一致性,这取决于MQ的可靠性。

      image-20240510222619339
    2. 基于的Canal异步通知:利用Canal中间件,不需要删除业务代码,伪装成Mysql的一个从节点,Canel通过读取BINLOG数据更新缓存。

      Canal是通过监听数据库的变化来监听数据操作的。Mysql有个BINLOG,记录所有DDL和DML语句。当我们的表数据发生变化之后,Canal会检测到,并通知缓存服务以更新缓存。这种方式是0侵入的,因为它不需要修改业务代码。这种方案也是有延时的。

      image-20240510222709871
Redis的数据持久化策略有哪些

在Redis中提供了两种数据持久化的方式:RDB/AOF

  • RDB:Redis Database Backup File(Redis 数据备份文件),所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。

    1
    2
    3
    4
    5
    [root@localhost ~]# redis-cli
    127.0.0.1:6379> save #由Redis主进程来执行RDB,会阻塞所有命令
    ok
    127.0.0.1:6379> bgsave #开启子进程执行RDB,避免主进程受到影响
    Background saving started

    Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:

    1
    2
    3
    4
    #900秒内,如果至少有1个key被修改,则执行bgsave
    save 900 1
    save 300 10
    save 60 10000
    • 执行原理

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

      在fork时,子进程会复制主进程的页表而不是整个内存单元。这样可以加快读取操作。

      image-20240511142402938

      在bgsave备份数据的时候,主线程可以进行写操作,那么是否会冲突呢?

      • 不会,因为fork采用的是copy-on-write技术。

        • 当主进程执行读操作的时候,访问共享内存。
        • 当主进程执行写操作的时候,子进程会拷贝一份此数据的块,主进程在副本上执行写操作。写完后,之后的读操作也会在此块上读。
      image-20240511142746772
  • AOF:Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。

    image-20240511143306793

    AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF。

    1
    2
    3
    4
    #是否开启AOF功能,默认是no
    appendonly yes
    #AOF文件的名称
    appendfilename "appendonly.aof"
    • 当然,你也可以配置其命令记录频率。

      配置项 刷盘时机 优点 缺点
      Always 同步刷盘 可靠性高,几乎不丢失数据 性能影响大
      everysec 每秒刷盘 性能适中 最多丢失1秒数据
      no 操作系统控制 性能最好 可靠性较差,可能丢失大量数据
      1
      2
      3
      4
      5
      6
      #表每执行一次写命令,立即记录到AOF文件
      appendfsync always
      #写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
      appendfsync everysec
      #写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
      appendfsync no
    • 注意:因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。

      image-20240511144025001

      Redis也会在触发阈值的时候自动重写AOF文件。阈值可以在redis中配置

      1
      2
      3
      4
      #AOF文件比上次文件 增长超过多少百分比则触发重写
      auto-aof-rewrite-percentage 100
      #AOF文件体积最小多大以上才触发重写
      auto-aof-rewrite-min-size 64mb
  • RDB和AOF对比

    类型 RDB AOF
    持久化方式 定时对整个内存做快照 记录每一次执行的命令
    数据完整性 不完整,两次备份之间会丢失 相对完整,取决于刷盘策略
    文件大小 会有压缩,文件体积小 记录命令,文件体积很大
    宕机恢复速度 很快
    数据恢复优先级 低,因为数据完整性不如AOF 高,因为数据完整性更高
    系统资源占用 高,大量CPU和内存消耗 低,主要是磁盘I0资源。但AOF重写时会占用大量CPU和内存资源
    使用场景 可以容忍数分钟的数据丢失,追求更快的启动速度 对数据安全性要求较高常见
Redis的数据过期策略有哪些

Redis的key过期之后,会被立刻删除吗?

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

  • 在Redis中提供了两种过期策略,惰性删除/定期删除。它们配合使用

  • 惰性删除

    • 设置该key过期时间之后,我们不去管他,当需要该key时,我们再去检查其是否过期,如果过期,删除之;否则返回此key。

      image-20240511150027258
    • 优点:对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。缺点:对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放。

  • 定期删除

    • 每隔一段时间,我们就对一定量的key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。

    • 定期删除有两种模式:

      • SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf的hz选项来调整这个次数
      • FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
    • 优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期删除,也能有效释放过期键占用的内存。

      缺点:难以确定删除操作执行的时长和频率。

Redis的数据淘汰策略有哪些

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

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

  • Redis支持8种不同策略来选择要删除的key:

    策略 说明
    noeviction 不淘汰任何key,但是内存满时不允许写入新数据。默认
    volatile-ttl 对设置了TTL的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算法进行淘汰

    LRU(Least Recently Used)最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
    LFU(Least Frequently Used)最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。

  • 使用建议

    1. 优先使用allkeys-lru策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。
    2. 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用allkeys-random,随机选择淘汰。
    3. 如果业务中有置顶的需求,可以使用volatile-lru策略,同时置顶数据不设置过期时间,这些数据就一直不被删除会淘汰其他设置过期时间的数据。
    4. 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu或volatile-lfu 策略。
  • 其他问题

    1. 数据库有1000万数据,Redis只能缓存20w数据,如何保证Redis中的数据都是热点数据?

      用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据。

    2. Redis的内存用完了会发生什么?

      主要看数据淘汰策略是什么?如果是默认的配置(noeviction),会直接报错。

Redis分布式锁如何实现/Redis实现分布式锁如何合理的控制锁的有效时长

需要结合项目中的业务进行回答,通常情况下,分布式锁使用的场景:集群情况下的定时任务、抢单、幂等性场景。

  • 抢票-加锁超卖

    image-20240511152833097

    这种情况,如果是最后一张票(票:1)就可能出现超卖的情况(票:-1)

    我们可以为其加锁来防止超卖,如下:

    image-20240512164745078

    但是我们在项目中,一般会用到Nginx做反向代理,来实现服务的负载均衡:
    image-20240512164935272

    这种情况下,为其加synchronize锁就无效了,因为此锁是JVM本地的锁,只对JVM本地有效,而多个微服务是处在不同的JVM中。

  • 分布式锁

    image-20240512165346157

    所有微服务的所有线程想要获取票,都需要先获取分布式互斥锁。

  • Redis分布式锁

    Redis实现分布式锁主要利用Redis的setnx命令。setnx是SET if not exists(如果不存在,则 SET)的简写。

    获取锁:

    1
    2
    #添加锁,NX是互斥,EX是设置超时时间
    SET key value NX EX 10 # 一定要加上这个失效时间,以免服务宕机导致死锁
    • “set key value nx ex 10”,它是在设置键值对的同时设置了一个锁,如果锁已存在则不执行任何操作,同时设置了过期时间为 10 秒。

    • “setnx key value ex 10”,它是尝试设置一个带有过期时间的键值对,如果该键不存在则设置成功,否则不执行任何操作。

    释放锁:

    1
    2
    # 释放锁,删除即可
    DEL key
    • 如何合理的控制有效时长?

      如果业务执行时间过长,那么锁可能自己释放。一般来说有两种方案:根据业务执行时间预估/给锁续期。但是第一种方案缺乏可靠性,我们一般采用第二种方案。另开一个线程,使其监督业务运行状体和锁的状态,及时给锁续期。

      但是我们手工实现此方案过于复杂 ,其实市面上已经有基于redisson实现的分布式锁。

  • Redisson实现的分布式锁-执行流程

    1. 线程加锁成功后,将能够直接操作Redis。

    2. 加锁成功后,将会另开一个线程进行监控,一般称之为watch dog(看门狗)。watchdog将不断监听持有锁的线程,为其增加锁的持有时间(续期)。

    3. 续期是每隔releaseTime/3的时间做一次续期。这个releaseTime就是锁的过期时间,默认是30s。那么也就是每隔10s就会给持有锁的线程进行一次续期 ,将会重置锁的过期时间。比如锁的过期时间是30s,那么每隔10s就会重置锁的过期时间。

    4. 何时释放锁?手动释放锁(代码执行)。在手动释放后,需要通知watchdog停止对锁的监听,因为其key已经失效删除。

      image-20240514215050710
    5. 如果此时来了一个新的线程,它也会尝试去加锁,看看是否能够加锁成功。如果加锁成功,那么和上述流程一样。假如没有,它将循环尝试获取锁,在一定时间后,要么成功获取到锁,要么放弃循环。

    6. 代码如下:

      image-20240514215456995
      • 首先获取到了Redis客户端,然后通过Redis客户端获取锁。

      • 然后尝试上锁,需要注意的是

        1
        2
        3
        4
        5
        // 第一个参数:循环等待锁的最大等待时间。第二个参数:时间单位
        boolean isLock = lock.tryLock(10,TimeUnit.SECONDS);
        // 第一个参数:循环等待锁的最大等待时间。第二个参数:这个锁的失效时间。第三个参数:时间单位
        boolean isLock = lock.tryLock(10,30,TimeUnit.SECONDS);
        // 设置了锁的失效时间时,将没有watchdog监听了,因为redisson认为你能够自己控制锁的失效时间,也就不自动续期了。如果你将其写为-1,那么watchdog仍能生效。

        这些加锁、设置过期时间等操作都是基于lua脚本完成的,其能够保证命令的原子性。lua脚本最大的作用是能够调用redis命令,能保证多条命令的原子性。

  • Redisson实现的分布式锁-可重入

    image-20240514221845649
    • 我们在add1()方法中获取了一个锁,add2()方法也获取此锁。在add1()方法中调用add2()add2()是可以成功执行的,这说明Redisson实现的分布式锁是可重入的。因为他们位于同一个线程。

    • 在Redisson中,其实现如下:

      image-20240515151024060

      使用Hash结构记录线程id和重入次数,其中,key可以通过业务进行命名,field表示当前持有锁的线程标识,value表示重入次数。

      当执行add1()时,线程在Redisson中获取的结构如上所示。而当add2()调用后,value将会增加1,如下:

      image-20240515151622283

      如果此线程有调用lock.unlock(),那么value将会-1。当value到0时,Redisson将会删除此锁信息。

  • Redisson实现的分布式锁-主从一致性

    • 背景:

      在使用Redis搭建的集群架构中,会有一个主节点和若干个从节点,如下:

      image-20240515152006043

      主节点主要负责写操作,从节点主要负责读操作。当主节点发生了写操作之后,就要将数据同步给从节点,以保证数据一致性。

      此时有一Java应用创建了一分布式锁,因为是获取锁,是写数据,所以需要先将数据写入到主节点中。而此时主节点尚未来得及将数据同步给从给点,便宕机了。依据Redis提供的哨兵模式,将会从剩下的从节点中选举一个节点作为主节点(这里以下方节点为例)。

      若此时又有一Java应用创建相同分布式锁,由于新的主节点未从旧的主节点上同步数据,那么,此Java应用也会加锁成功。

      image-20240515161803933

      这样,两个线程将同时拥有同一把锁,将会丧失锁的基本性质-互斥性,这样是万万不可的。

    • RedLock

      在Redis中,提供了另一种分布式锁RedLock,其特性是:不只在一个Redis实例上创建锁,而是在多(n/2+1)个实例上创建锁。其中n为redis节点数量。

      image-20240515162257713

      但是RedLock的缺点也很明显,实现复杂,性能差,运维繁琐

      Redis的思想是AP思想,优先保证可用性;如果实在要保证一致性,可以使用基于CP思想的Zookepper。

      CAP:C(一致性),A(可用性),P(分区容错)

image-20240515163125142
Redis其他面试问题
  • Redis集群有哪些方案

    • 主从复制:主节点负责写操作,从节点复制主节点的数据,提供读操作。
    • 哨兵模式:在主从复制基础上,增加哨兵节点监控主从节点状态,自动进行故障转移和通知。
    • 分片集群:数据分布在多个节点上,每个节点负责一部分数据,通过哈希槽进行数据分片。
  • 主从复制

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

    image-20240515200520865
    • 主从全量同步

      1. 从节点执行replicaof命令,与主节点建立连接。
      2. 从节点请求数据同步。
      3. 主节点判断是否为首次同步。
      4. 若是首次,同步数据版本信息。
      5. 从节点保存版本信息。
      6. 主节点执行bgsave,生成RDB文件。
      7. 主节点发送RDB文件。
      8. 从节点清空本地数据并加载RDB文件。
      9. 主节点记录RDB期间的所有命令。
      10. 主节点发送repl_backlog中的命令。
      11. 从节点执行接收到的命令。
      image-20240515201004269
      • Replicationld:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid

      • offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。

      现在便有了两个问题:1,主节点是如何判断是否第一次同步呢?2,主节点和从节点之间是如何确保传输的就是需要的那部分数据呢?

      1. 在从节点发起数据同步的时候,就会把自己的replid发送给主节点。主节点将会根据从节点发送的replid与自己的进行比对,如果不一致,说明是第一次进行数据同步。然后,主节点将自己的replid发送给从节点,从节点就将此replid保存,此时,主从节点的replid一致。主节点于是开始执行bgsave,生成RDB。

      2. 主从节点的replid一致,主节点将会发送repl_baklog日志,这样比发送RDB文件要快。但是如何判断需要发送多少repl_baklog呢?这由offset偏移量决定。

        假如主节点中的offset为80,从节点中的offset为50,说明相差的30数据没有同步完成,Redis就会对这30数据进行同步。

      image-20240515214152277
      • 总结-3个大步骤

        1. 从节点给主节点发送请求想要同步数据,主节点master判断是否为第一次请求。如果是第一次请求,则全量同步。
        2. 主节点将会执行bgsave,生成一个RDB文件发送给从节点执行。
        3. 如果主节点在执行bgsave过程中执行了其他命令,将能够在repl_baklog中获取到,然后将此日志发送给从节点去执行。整个流程结束。
    • 主从增量同步

      主从增量同步(slave重启或主从数据变化)

      1. 从节点重启后,将会给主节点发送一个psync请求,其带了两个值,replid/offset

      2. 主节点Master判断其replied是否一致(不一致将会全量同步)

      3. 主节点会从repl_baklog中获取offset之后的数据,发送给从节点同步数据

      image-20240515214634939
  • 哨兵模式

    主从复制不能保证Redis的高可用,因为只要主节点宕机 ,那么redis就只能读不能写了。

    • Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下:

      • 监控:Sentinel 会不断检查您的master和slave是否按预期工作

      • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。 当故障实例恢复后也以新的master为主

      • 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端

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

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

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

      image-20240515215951059
    • 哨兵选主规则:

      • 首先判断主与从节点断开时间长短,如超过指定值就排该从节点
      • 然后判断从节点的slave-priority值,越小优先级越高
      • 如果slave-priority一样,则判断slave节点的offset值,越大优先级越高
      • 最后是判断slave节点的运行id大小,越小优先级越高
    • Redis集群(哨兵模式)脑裂

      如果因为网络原因,内网中,主节点与所有从节点&哨兵都断开连接(或处于不同的网络分区),那么哨兵就会在从节点中选举一个主节点出来。而此时老的主节点并没有宕机,还和客户端保持着连接,客户端也在向老主节点写入数据。而内网的网络一旦恢复,哨兵会将老的主节点强制降为从节点,那么这个老的主节点(现在是从节点)将会把自己的数据全部清空,然后从新的主节点中复制数据。这样,这段脑裂过程中客户端写入的数据于是丢失了。

      image-20240515221235812 image-20240515221304237

      为防止集群脑裂,我们可以修改以下配置:

      1
      2
      min-replicas-to-write 1 # 表示最少的salve节点为1个,如果主节点连接的slave少于1,主节点将拒绝写入数据。
      min-replicas-max-lag 5 # 表示数据复制和同步的延迟不能超过5秒,如果从节点的数据复制和同步延迟超过5秒,主节点将拒绝继续写入操作。
      image-20240515221914981
  • 分片集群模式

    • 主从和哨兵模式可以解决高可用、高并发读的问题,但是依然有两个问题:

      • 海量数据存储问题

      • 高并发写的问题

    • 使用分片集群可以解决上述问题,分片集群特征:

      • 集群中有多个Master,每个master保存不同的数据
      • 每个master都可以有多个slave节点
      • master之间通过ping检测彼此健康状态(哨兵模式)
      • 客户端请求可以访问集群中任意一节点,最终都会被转发到正确节点(因为自动路由)
      image-20240515222446143
    • 分片集群结构-数据读写

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

      如果我们set name itheima,集群将会对其做CRC16计算name的hash,然后通过hash后的值,将name存到指定的节点中;如果要指定有效值,你可以set {valid}name key

      image-20240515222927350
image-20240515223114981
Redis是单线程的,但是为什么还是这么快?
  • 一般答到这里就可以了

    • Redis是纯内存操作,执行速度非常快

    • 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题

    • 使用I/O多路复用模型,非阻塞IO

  • I/O多路复用

    Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,I/O多路复用模型主要就是实现了高效的网络请求。

    • 用户空间和内核空间

      • Linux系统中一个进程使用的内存情况划分两部分:内核空间、用户空间
      • 用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问
      • 内核空间可以执行特权命令(Ring0),调用一切系统资源
      image-20240515231434673

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

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

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

    • 阻塞IO/非阻塞IO/多路IO复用

      • 阻塞IO:

        image-20240515231906460
      • 非阻塞IO:
        image-20240515232120025

      • IO多路复用

        image-20240515232324486
      • 总结

        IO 模型 工作原理 优点 缺点
        阻塞 IO IO 操作(读或写)会阻塞当前进程,直到操作完成 实现简单,编程模型直观 效率低下,无法处理高并发连接,每个 IO 操作都要等待
        非阻塞 IO IO 操作立即返回,不会阻塞进程;如果操作未完成,会返回一个错误,程序继续执行其他任务,并轮询检查 可以同时处理多个 IO 请求,提高并发性能 需要不断轮询,编程复杂度高,效率低于 IO 多路复用
        IO 多路复用 使用系统调用(如 selectpollepoll)同时监控多个文件描述符,当任何一个文件描述符准备好读或写时,通知程序进行操作 高效处理大量并发连接,减少 CPU 资源浪费,编程模型相对简单 在某些场景下可能不如多线程或多进程模型高效,但总体性能最佳之一
        image-20240515232810880
    • Redis网络模型

      image-20240515233129691
image-20240515233424981