返回
Featured image of post Redis - 面试常见问题

Redis - 面试常见问题

整理一些Redis基础知识,数据结构和使用场景在另一篇笔记中收录.

常见的内存数据库包括 Memcached 和 Redis。后者相较之下在 k/v 类型数据基础上提供了 list, set, zset, hash 等数据结构存储,并且可扩展性强,能够通过插件增加更多;同时具有容灾机制,支持数据持久化,也有原生集群模式,支持发布订阅模型、Lua 脚本、事务;并且支持更多编程语言,单线程模型更加高效。总而言之功能很强,应用很广。

简要介绍

Redis 是用 C 开发的内存数据库,非关系型数据库,读写速度快,广泛应用于缓存,也可以做分布式锁、消息队列。

  • Redis6.0 之前都是单线程处理,仅在4.0增加了对大键值对删除操作的“异步处理”
  • 服务器内存使用完之后,将不用的数据存到磁盘上
  • 过期数据的删除策略包括惰性删除与定期删除

缓存的作用

访问数据库从硬盘中读取,过程较慢。如果用户访问数据为高频数据且不会经常改变,则可以存在缓存中,速度快。


删除策略和内存淘汰机制

  • 惰性删除

    只在取出 key 的时候才对数据进行过期检查。CPU负担小,但会残留很多过期 key

  • 定期删除

    周期性取一批 key 执行删除过期 key 操作,通过限制删除操作执行时长和频率来减少删除操作对 CPU 影响

删除策略并不能清理所有过期 key ,过期 key 还需要内存淘汰机制解决。

除了缓解内存消耗,设置过期时间也可以用于满足业务需要,比如验证码、登录Token的有效时间。

内存淘汰机制跟据从中挑选淘汰数据的数据集不同,分为三大类:

  1. 已设置过期时间的数据集volatile

    1. volatile-lru (least recently used)

      移除最近最少使用key

    2. volatile-ttl

      移除将要过期的数据

    3. volatile-random

      移除随机选择的数据

    4. volatile-lfu

      (4.0新增) 移除最不经常使用的数据

  2. 从**数据集(所有)**中 allkeys

    1. allkeys-lru (least recently used)

      移除最近最少使用key

    2. allkeys-random

      移除随机选择的数据

    3. allkeys-lfu (least frequently used)

      (4.0新增) 移除最不经常使用key

  3. 不进行数据淘汰 no

    1. no-eviction

      内存不足以容纳新写入数据就直接报错

如图所示,Redis通过一个过期字典(类似HashTable)来保存数据过期时间,对应内存淘汰机制中 server.db[i].expires


持久化机制

为了保证Redis挂掉后再重启数据可以进行恢复,需要将内存数据写入硬盘。两种持久化机制分别是快照 (snapshotting, RDB) 和只追加文件 (append-only file, AOF) 。

RDB 记录的是内存快照,AOF 记录的是执行过的所有命令。

快照持久化是 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命令创建快照。
# 大部分情况下,15分钟够用,只保留这一条即可

RDB提供了三种机制

  • save 命令将阻塞服务器主线程直到 RDB 完成 (不推荐)
  • bgsave 命令 fork() 一个子线程在后台异步进行快照操作,同样会阻塞,但只发生在 fork() 阶段,时间较短。RDB 快照持久化期间父进程修改的数据不会被保存。
  • 自动,通过配置完成

AOF 持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:

appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

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

appendfsync everysec 比较好

优点:写入性能非常高;即时日志文件过大出现后台重写也不会影响客户端读写(fork()新线程进行重写);记录方式可读,适合用作紧急恢复

缺点:日志文件更大,且会带来持续IO,对QPS影响更大

总结

Redis 重启时优先载入 AOF,因为 AOF 数据集一般更加完整,但 RDB 更适合用于备份数据库,快速重启,且没有 AOF 潜在 BUG


Redis事务

关系型数据库的事务具备四大特性(ACID),合起来就是:

  1. 原子性

    确保都成功或都失败

    Redis 不具备原子性,因为不支持回滚,当然这也带来部分性能提升和开发便捷性

  2. 隔离性

    并发访问时,单用户事务不被其他事务所干扰,防止数据损坏

    Redis 不具备隔离级别概念,命令在事务中没有被直接执行。只有发起执行命令时才会执行。

  3. 持久性

    事务一旦提交,对数据库中数据的改变是持久的,被持久化写到存储器中,不会被系统其它问题改变

    Redis 同样不具备,但是当 AOF 持久化模式下,并且 appendfsync 选项值为 always 时,事务具有耐久性

  4. 一致性

    执行事务前后数据保持一致,多个事务对同一数据读取的结果相同

Redis 事务实际提供了将多个命令请求打包功能,再按顺序执行打包的所有命令,且不会被中途打断。具备 一次性顺序性排他性,分以下两种情况:

编译型异常中

当命令出现错误,后续命令依旧可以添加到命令队列中,但所有命令都不会被执行

运行时异常中

当命令出现错误,其它命令可以正常执行,只有错误命令抛出异常


缓存穿透攻击

黑客制造大量不存在 key 的请求,导致请求直接落到数据库进行查询,没有经过缓存层。

要解决这一问题,最基本是要做好参数校验,不合法的参数直接抛异常给客户端。

  • 缓存无效的 key

    即时返回的空对象也将其缓存起来,同时设置过期时间

    但在 key 变化频繁的情况下,尤其在恶意攻击中可能产生大量无效的 key

  • 布隆过滤器

    先用布隆过滤器判断请求值是否存在,实际上就是哈希校验

缓存击穿

key 失效的瞬间,大量并发集中访问,直接落在数据库上。

  • 设置热点数据不过期

    可以解决问题,但并不好

  • 加互斥锁

    分布式锁来保证对每个 key 同时只有一个线程查询后端服务,其它线程没有获得分布式锁的权限,只需要等待,从而将高并发压力转移到分布式锁

缓存雪崩

服务器宕机或断网形成缓存雪崩,对数据库造成压力不可预知,很可能瞬间将数据库压垮。

实际上就是压力累积超过临界导致的,

  • 增设缓存集群,异地多活
  • 限流降级,缓存失效后,通过加锁或队列来控制都数据库写缓存的线程数量
  • 数据预热,预访问数据,使得尽可能多的数据被加载到缓存中,但要注意设置不同的过期时间,使缓存失效的时间点尽量均匀

单线程模型

  • 单线程开发、维护容易
  • Redis性能瓶颈在内存和网络,CPU瓶颈不明显
  • 多线程带来了死锁、线程上下文切换等问题,甚至可能影响性能

6.0 后引入多线程也是为了提高网络 IO 读写性能,仅用在网络数据读写这类耗时操作上,无需担心线程安全问题。

Redis 事件处理模型对应其中单线程的文件事件处理器(File Event Handler),因此是单线程模型。通过 IO 多路复用来监听大量连接,跟据套接字执行任务关联不同的事件处理器,不需要创建多余线程来监听连接。

虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。


缓存读写策略

缓存读写策略实际上也就是缓存和数据库间的位置关系,主要有以下三种

旁路缓存模式

适用读请求较多的场景

写:

  • 先更新 DB 中数据
  • 直接删除 cache

之所以先更新 DB ,是因为 cache 的删除操作相对快很多,数据不一致的可能性大大降低。相反,如果先删除 cache,此时如果有并行请求直接从 DB 中读取数据,这一操作很可能在 DB 中数据被更新前完成。

读:

  • 从 cache 中读取数据,读到直接返回
  • 读不到就从 DB 中读取并返回
  • 数据放到 cache 中

缺点一、 首次请求数据一定不在 cache ,但是这一问题可以通过热点数据的提前缓存解决。

缺点二、 写操作如果频繁,则 cache 数据被频繁删除,缓存命中率降低,缓存很大程度上被架空。在强一致场景下需要锁/分布锁保证更新 cache 时不存在线程问题;弱一致场景下可以 cache 和 DB 一起更新,cache 设置较短的过期事件以提高缓存命中率。

读写穿透模式

cache 负责将数据读取和写入 DB,作为服务端和 DB 间的中间件。然而相当难实现,因为 Redis 不提供 DB 读写功能。

写:

  • 查 cache,不存在则直接更新 DB
  • cache 存在,则先更新 cache,cache 服务自己更新 DB(cache 和 DB 同步更新)

读:

  • 从 cache 读数据,读到直接返回
  • 没读到就从 DB 加载到 cache,然后返回响应

由于 Redis 不提供 DB 读写,这一模式实际上只是在旁路模式上进行了封装。同样具有首次请求数据不在 cache 问题。

异步缓存写入

和 读写穿透模式 相似,但只更新缓存,不直接更新 DB,用异步批量的方式来更新 DB。消息队列中消息异步写入磁盘、MySQL 的 InnoDB Buffer Pool 机制都用到这种策略。

DB 的写性能非常高,适合数据频繁变化,数据一致性要求又不高的场景,如浏览量、点赞量。

缺点很明显,数据一致性很难维护,cache 可能在数据异步更新前宕机。

如何保证缓存数据库数据一致

旁路缓存模式下,可以增加 cache 更新重试机制——如果 cache 服务不可用而暂时无法删除缓存,就隔一段时间再试,多次失败就将更新失败 key 存入队列,等缓存恢复后进行删除。


Redis Cluster

Redis 集群主要解决的是性能问题,在缓存数据量过大的情况下将数据分散到各台 Redis 主机上,可以看作是一种负载均衡手段,方便业务进行横向拓展。

Redis Cluster 有多个节点,是去中心化的分布式结构,每个节点都负责数据读写操作,各节点间会进行通信。通过分片 (sharding) 来进行数据管理,提供复制和故障转移功能。

Hash Slot

共 16384 个槽被平均分配给节点进行管理,每个节点对自己负责的槽进行读写操作。各个节点间彼此通信,知晓其它节点负责管理的槽范围。

作为一个分布式系统,各结点需要互相通信来维护一份所有其它示例的状态信息,基于 Gossip 协议实现数据的最终一致性。

访问流程

客户端访问任意节点时,对数据 key 按照 CRC16 进行 Hash 计算,然后对运算结果模 16384 ,判断槽是否在当前节点管理范围内:如果在,则执行命令,返回结果;如果不在,返回moved重定向异常,之后由客户端跟据重定向异常中目标节点信息去发送命令。

迁移

如果节点在迁移过程中收到客户端命令,会返回 ASK 重定向异常。


Redis Replication

Redis 主从主要解决的是可用性问题,读吞吐量过大情况下,可以通过一主多从来提高可用性和读吞吐量,从机多少取决于读吞吐量大小。

从机只能读,不能写。主机断开连接,从机仍然连接到主机,只是没有任何写操作传入,如果主机上线,从机依然可以直接获取。通过指令 SLAVEOF no one 来脱离从机身份。

复制

  • SYNC

    每次执行 SYNC ,主服务器需要 BGSAVE 来生成 RDB,并发送给从服务器;从服务器载入 RDB 期间阻塞进程,无法处理请求。

  • PSYNC

    部分重同步,主服务器收到 PSYNC 后返回 +CONTINUE ,示意准备执行部分重同步,然后继续发送新指令以完成同步。

    主从服务器分别维护“复制偏移量”,记录收到的数据长度(字节数)。通过对比主从复制偏移量可以直到是否处于一致状态。

    主服务器维护一个定长 FIFO 队列,作为复制积压缓冲区。主服务器将写命令发给从机,同时入队到复制积压缓冲区。

  • 如果从机先前没有复制过任何主机,或执行过 SLAVEOF no one ,则为了开始新复制而发送 PSYNC ? -1 ,请求主机进行完整重同步。主机返回 +FULLRESYNC <runid> <offsetid> 示意准备完整重同步。

  • 反之,发送 PSYNC <runid> <offset> ,供主机判断执行哪种同步

哨兵

主从的问题在于一旦主机宕机,从机晋升,将需要人工重新配置其余所有从机,复制新的主机,并改变应用方主机地址,为此需要一个(实际上一般是多个)哨兵来干这件事。

单个哨兵如果检测到主服务器宕机,不会马上进行 failover ,而是认为主服务器“主观下线”。当检测到主服务器不可用的哨兵达到一定数量,则哨兵间进行投票,决定接替的从机,切换成功后,通过发布订阅模式,让各个哨兵把监控的从服务器实现切换主机,称为“客观下线”。

客观下线后,即使原主机重新上线,也只能作为新主机的从机。

缺点:无法在线扩容,集群容量到达上限,不好在线扩容;实现哨兵模式配置有很多选择,较为复杂

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus