整理下部分基础的Redis面试题

- Redis

Redis数据类型

  • String,一般常用在需要计数的场景,比如:用户的访问次数、点赞、转发数量。
  • Hash,类似于JDK1.8之前的HashMap(数组 + 链表)的实现,用来存储对象信息。
  • List,实现为一个双向链表。常用在布与订阅或者说消息队列、慢查询。
  • Set,类似于Java中的HashSet,无序不可重复。可实现共同关注、共同喜好等求交集的需求上。
  • Zset,在Set基础上加了参数 score,可以让元素按照 score 排序。适合实现用户列表、排行榜、弹幕。

Redis单线程模型

Redis4.0之前一直都是单线程模式,4.0之后才开始有多线程的概念,但默认还是单线程模式。

Redis 基于Reactor 模式设计开发了自己的一套高效的事件处理模型,这套事件处理模型在Redis中对应的是文件事件处理器 (file event handler)

由于文件事件处理器是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。

Redis如何监听大量的客户端连接?

通过IO 多路复用程序 监听来自客户端的大量连接(或者说是监听多个 socket)。Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的Selector组件很像)。

Redis的文件事件处理器通过IO多路复用程序监听多个套接字socket,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

文件事件处理器主要是包含 4 个部分:

  • 多个 socket(客户端连接)
  • IO 多路复用程序(支持多个客户端连接的关键)
  • 文件事件分派器(将 socket 关联到相应的事件处理器)
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

Redis6.0的多线程

Redis6.0 引入多线程主要是为了提高网络 IO 读写性能

Redis 的多线程只是在网络数据的读写这类耗时操作上使用了, 执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。

Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改配置文件:redis.conf
io-threads-do-reads yes
还需要设置线程数,否则不会生效:io-threads 4 
//官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

Redis是如何判断过期数据的?

expire 关键字设置过期时间,可以释放内存,防止OOM

Redis 通过一个叫做过期字典(类似 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个key(键),过期字典的值是一个 longlong 类型的64位整数,这个整数保存了 key 所指向的 Redis 键的过期时间。

过期数据的删除策略

  • 惰性删除

    只会在取出 key 时才对数据进行过期检查。这样对 CPU 最友好,但可能会造成太多过期 的key 没有被删除。

  • 定期删除

    每隔一段时间抽取一批 key 执行删除过期 key 的操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。

Redis 采用的是 定期删除 + 惰性(懒汉式) 删除

但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。

怎么解决这个问题呢?答案就是: Redis 内存淘汰机制

Redis内存淘汰机制

MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?

Redis 提供 6 种数据淘汰策略:

  1. volatile-lru(Least Recently Used 最近最少使用):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
  2. volatile-ttl(Time To Live 生存时间):从已设置过期时间的数据集中挑选将要过期的数据淘汰。
  3. volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
  4. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。几乎不用。

4.0 版本后增加以下两种:

  1. volatile-lfu(least frequently used):从已设置过期时间的数据集中挑选最不经常使用的数据淘汰。
  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

Redis持久化:RDB 和 AOF

为什么持久化到硬盘?如果发生了系统故障,可以利用备份恢复数据,减少了重建缓存的开销。

Redis支持两种持久化方式:快照持久化 RDB只追加文件 AOF

RDB

Redis 可以通过创建快照,得到存储在内存里的数据在某个时间点上的副本。

fork一个子进程,遍历内存中数据,以二进制的格式,把数据一条一条放在一起,生成了一个RDB文件。

当需要备份的数据量大,只有读取操作,而很少写操作时,Redis提供了配置参数,支持周期性备份,在redis.conf 中通过以下参数控制:

save 900 1		#在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10		#在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000	#在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

AOF

Redis与MySQL都是基于命令式的,AOF持久化记录所有写操作的命令,通过重新执行这些命令来还原数据集。AOF 文件中的命令全部以 Redis 协议规范来保存,新命令会被追加到文件的末尾。AOF默认关闭,通过配置文件中的 appendonly yes 开启。默认文件名为 appendonly.aof

Redis肯定不能每执行一条写入命令就记录到文件中,所以创建了一个缓冲区,然后把要记录的命令先临时保存在缓冲区中,这个缓冲区就是 aof_buf,由于操作系统也有一个缓冲区,为了将命令直接从 aof_buf 写入文件中,Redis提供了一个参数,主动控制从缓存刷新到文件中。

appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no        #让操作系统决定何时进行同步

AOF重写

随着不断的追加写操作的命令,aof文件越来越大,所以有必要进行压缩,这个过程称之为AOF重写。

fork出一个子进程去重写.aof 文件,同时创建一个重写缓冲区,存放开始重写之后的命令。等到子进程重写.aof 文件结束后,再将重写缓冲区的命令追加到新的.aof 文件中。最后再重命名新的.aof文件,替换掉原来的那个臃肿的文件。

如何选择 ?

Redis默认开启 RDB 持久化方式,Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,通过配置项 aof-use-rdb-preamble 开启)

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点,快速加载同时避免丢失过多的数据。当然缺点也是有的,AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

Redis事务

因为Redis 事务不支持回滚操作,所以不支持 ACID 原则中的原子性、持久性,只支持隔离性、一致性。

Redis 开发者们认为没必要支持回滚,这样更简单便捷并且性能更好,即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。

你可以将 Redis 中的事务就理解为 :Redis 事务提供了一种将多个命令请求打包的功能。然后按顺序执行打包的所有命令,并且不会被中途打断。

缓存穿透

可以理解为:大量请求的 key 根本不存在于缓存中,导致直接请求到了数据库,根本没有经过缓存这一层。

举个例子:某黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。

如何解决?

  • 缓存无效的key

    如果缓存、数据库都查不到某个 key 对应的数据,就写一个到 Redis 中去并设置过期时间。这种方式可以解决请求的 key 变化不频繁的缓存穿透问题。为防止黑客攻击,应尽量将过期时间设的更短,实际上该方案并不能根本上解决问题。

    public Object getObjByNullKey(Integer id) {
        // 从缓存中获取数据
        Object cacheValue = cache.get(id);
        // 缓存为空
        if (cacheValue == null) {
            // 从数据库中获取
            Object storageValue = storage.get(key);
            // 缓存空对象
            cache.set(key, storageValue);
            // 如果存储数据为空,需要设置一个过期时间(300秒)
            if (storageValue == null) {
                // 必须设置过期时间,否则有被攻击的风险
                cache.expire(key, 60 * 5);
            }
            return storageValue;
        }
        return cacheValue;
    }
    
  • 布隆过滤器

    布隆过滤器是一个非常神奇的数据结构,通过它可以非常方便地判断一个给定数据是否存在于海量数据中。

    具体实现:把所有可能存在的请求的值都存放在布隆过滤器中,当请求过来时,先判断用户发来的请求的值是否存在于布隆过滤器中,不存在的话,直接返回请求参数错误信息给客户端;存在的话,才会去执行 Redis 的查询流程。

需要注意:布隆过滤器可能会存在误判的情况。但总结来说: 布隆过滤器说某个元素存在,小概率会不存在。布隆过滤器说某个元素不存在,那么这个元素一定不在。

为什么会存在误判?

当一个元素加入布隆过滤器中的时候会进行哪些操作

  1. 布隆过滤器中的多个哈希函数对元素值进行计算,得到多个不同的哈希值。
  2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。

当判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:

  1. 对这个元素再次进行相同的哈希运算;
  2. 得到值之后判断位数组中的对应位置的值都为 1,如果值都为 1,说明这个元素在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

然后,一定会出现这样一种情况:不同元素进行哈希运算后的值可能相同 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)

缓存雪崩

可以理解为:缓存在同一时间大面积的失效,导致请求都直接到了数据库上,造成数据库短时间内承受大量请求。

在短时间内给数据库造成巨大压力,好比雪崩一样。

如何解决?

  1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
  2. 限流,避免同时处理大量的请求。

如何保证数据、缓存一致性?

旁路缓存模式Cache Aside Pattern

Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。

如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:

  1. 缓存失效时间变短(不推荐,治标不治本)

    我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。

  2. 增加 cache 更新重试机制(常用)

    如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将 缓存中对应的 key 删除即可。

上一篇:从零开始搭建python+selenium+pytest+allure


下一篇:Redis持久化机制