java面试题--Redis

一、说一下redis的持久化机制原理?

  1. RDB文件: redis database。存储的是某个时间点的数据库内容的快照,是结果。
    1. redis默认的持久化策略。
    2. 落盘策略:使用SAVE或者BGSAVE命令。(1)SAVE: 有主线程执行,会阻塞客户端。(2)BGSAVE:会fork出一个子进程,不会出现阻塞问题。子进程使用写时拷贝的策略,子进程页表项指向与父进程页表项相同的物理内存页,所以不会占用大量的内存。 可以使用命令配置自动执行落盘的条件。例如:save 900 2  900秒之内至少有2次修改就执行BGSAVE。
    3. 恢复策略:自动加载。redis在启动时检测是否存在RDB文件,存在的话,就载入。在载入完成之前,服务对外不可用。
  2. AOF文件:AppendOnlyFile。存储的执行成功过的命令、参数。是过程。
    1. 落盘策略:分为三个阶段。(1)命令传播:命令、参数传播到AOF程序。(2)缓存追加:协议文本追加到aof_buf末尾。(3)文件写入和保存:调用faof.c/flushAppendOnlyFile 函数。这个函数执行以下两个工作:1、WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。2、SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
    2. 三种保存模式:(1)AOF_FSYNC_NO:不保存。WRITE 都会被执行, 但 SAVE 会被略过。(2)AOF_FSYNC_EVERYSEC:每一秒钟保存一次(默认)。(3)AOF_FSYNC_ALWAYS:每执行一个命令保存一次。(不推荐)
    3. AOF重写:名称BGREWRITEAOF
      1. Redis可以在 AOF体积变得过大时,自动地在后台(Fork子进程)对 AOF进行重写。
      2. Redis 增加了一个 AOF 重写缓存, 这个缓存在 fork 出子进程之后开始启用,Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 文件之外,还会追加到这个缓存中。
      3. 当子进程在执行 AOF 重写时, 主进程需要执行以下三个工作:

        1. 处理命令请求。
        2. 将写命令追加到现有的 AOF 文件中。
        3. 将写命令追加到 AOF 重写缓存中。
      4. 当子进程完成 AOF 重写之后, 它会向父进程发送一个完成信号, 父进程在接到完成信号之后, 会调用一个信号处理函数, 并完成以下工作:

        1. 将 AOF 重写缓存中的内容全部写入到新 AOF 文件中。

        2. 对新的 AOF 文件进行改名,覆盖原有的 AOF 文件。

        3. Redis数据库里的+AOF重写过程中的命令------->新的AOF文件---->覆盖老的。

        4. 当步骤 1 执行完毕之后, 现有 AOF 文件、新 AOF 文件和数据库三者的状态就完全一致了。
          当步骤 2 执行完毕之后, 程序就完成了新旧两个 AOF 文件的交替。

    4. AOF恢复策略:
      1. 创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服 务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令 的效果和带网络连接的客户端执行命令的效果完全一样。

      2. 从AOF文件中分析并读取出一条写命令。

      3. 使用伪客户端执行被读出的写命令。

      4. 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止。

  3. RDB和AOF混合
    1. 使用RDB头+AOF部分。如果AOF文件以REDIS开头,则先执行RDB的数据快照。再执行AOF命令。

二、说一下对redis缓存雪崩、穿透、击穿的理解?

  1. (1)缓存穿透:大量请求根本不存在的key。(2)缓存击穿:redis中一个热点key过期。(3)缓存雪崩:redis中大量key集体过期。
  2. 缓存穿透解决方案:
    1. 对空值进行缓存,但是过期时间很短。最长不超过5分钟。
      1. 优点:处理简单
      2. 缺点:(1)缓存大量不存在的key, 会消耗redis内存。(2)key之前不存在,后来存在了,可能会发生数据不一致问题。
    2. 使用布隆过滤器:将所有可能存在的数据哈希到一个足够大的BITMAP中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
      1. 做法:缓存预热时,同步预热布隆过滤器。
      2. 优点:内存占用较少,没有多余key。
      3. 缺点:实现复杂,存在误判。
    3. 网警。
  3. 缓存击穿解决方案:
    1. 加互斥锁:在未命中缓存时,先使用Redis的setNx去设置一个互斥锁,通过加锁避免大量请求访问数据库(只有一个线程可以进行热点数据的重构)。
      1. 这种方法能保证数据强一致性,但是性能差
    2. 逻辑过期:物理不过期,也就是不设置过期时间。而是在redis的value对象中定义一个过期时间字段。查线程1查询的时候,从Redis中取出判断时间是否过期。如果过期则开通另一个线程2进行数据同步,当前线程1正常返回已过期的数据。
      1. 这种方法是高可用的,性能优,但是数据一致性差些
  4. 缓存雪崩解决方案:
    1. 给不同key的TTL添加随机值,将缓存失效时间分散开。比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机。
    2. 事前:这种方案就是在发生雪崩前对缓存集群实现高可用。如果是使用redis,可以使用主从+哨兵、Redis Cluster来避免redis全盘崩溃的情况。
    3. 事中:给缓存业务添加降级限流策略。nginx或者spring cloud gateway。比如设置允许一秒2000个请求通过该组件,那么过来5000个请求时,有3000个请求会走限流逻辑,然后去调用我们自己开发的降级组件,比如设置一些默认值,以此来保护最后的MySQL不会被大量请求打死。
    4. 事后:开启redis持久化机制,尽快恢复缓存集群。
    5. 给业务添加多级缓存。使用Guava或者Caffeine作为一级缓存,Redis作为二级缓存。

三、redis的集群模式?

  1. 主从复制模式。
    1. 一主多从。
    2. 无法实现故障转移。  
  2. 哨兵模式。
    1. 一主多从。
    2. 故障转移步骤:
      1. master主观下线;
      2. master客观下线;
      3. Sentinel集群选举出Leader;
      4. Leader指定一个新的master。
  3. Jedis Cluster模式。
    1. 多主多从。

 四、redis与数据库的双写一致性问题

1、概念:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致。但它不是强一致的。

2、延迟双删办法解决双写一致性问题

读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间

写操作:延迟双删

 

public static void main(String[] args) {
        String key = "key";
        //删除缓存
        redisTemplate.delete(key);
        //更新数据库
        updateDb(item);
        //延迟N秒删除缓存
        Thread.sleep(N);
        redisTemplate.delete(key);
    }

 

1、为什么要删除两次缓存?

无论是先删除缓存再删除数据库,还是先删除数据库再删除缓存,多线程并发下,都会造成缓存与数据库双写不一致问题。所以需要删除两次缓存。

2、为什么要延时删除?

因为数据库大都是主从分离的,数据从主库复制到从库需要等一会,等数据都复制到从库后,再执行删除。所以要延时删除,但是延时多长时间是不好控制的。

3、延迟的时间如何确定?

延迟的时间要大于一次写操作的时间。因为如果延迟时间小于写入redis的时间,会导致请求1删除了缓存,请求2还未写入缓存。

在业务程序运行时,统计业务逻辑执行读数据和写缓存的操作时间,以此为基础来进行估算延迟删除的时间。一般都会小于5秒。

3、加锁解决双写一致性问题

缓存中的数据具有读多写少的特点。根据这个特点,添加读写锁,能有效保证强一致性。

  • 共享锁:读锁ReadLock,加锁之后,其他线程可以共享读操作。
  • 排他锁:独占锁WriteLock,加锁之后,阻塞其他的读写操作。
  • 这种方法可以保证数据的强一致性,但是性能低,在写锁时,其他线程都会被阻塞。
  • /**
         * 读数据
         * @param name
         * @return
         */
        public Item getByName(String name) {
            //读写锁
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ITEM_READ_WRITE_LOCK");
            //读之前加读锁,读锁的作用就是等待该lock key释放写锁之后再读
            RLock readLock = readWriteLock.readLock();
            try {
                //加锁
                readLock.lock();
                Item item = (Item) redisTemplate.opsForValue().get("item" + name);
                if (null != item) {
                    return item;
                }
    
                //查询业务数据
                item = new Item("item1", 1);
                //写入缓存
                redisTemplate.opsForValue().set("item" + name, item);
                return item;
            } finally {
                readLock.unlock();
            }
        }
    
    
    /**
         * 更新数据时,调用该接口。1、更新数据库。2、删除缓存
         * @param name
         */
        public void updateByName(String name) {
            //拿到跟读接口相同的锁
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ITEM_READ_WRITE_LOCK");
            //写之前加写锁,写锁加锁成功,读锁只能等待
            RLock writeLock = readWriteLock.writeLock();
            try {
                //加锁
                writeLock.lock();
                //更新业务数据
                Item item = new Item("item1", 2);
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //删除缓存
                redisTemplate.delete("item" + name);
            } finally {
                writeLock.unlock();
            }
        }

     

4、异步通知保证数据的最终一致性。

适用于对数据实时一致性要求不高的业务。

 

5、基于Canal的异步通知。

适用于对数据实时一致性要求不高的业务。

Canal是基于mysql的主从同步来实现的。

二进制日志(binlog)记录了所有的DDL(数据定义语言)语句和DML(数据操纵语言)语句,但不包括数据查询语句(select,show)。

 

posted @ 2023-05-18 22:07  翊梦  阅读(42)  评论(0编辑  收藏  举报