Redis的缓存、持久化,哨兵集群,切片集群面试题,拳拳到肉

157 阅读16分钟

持久化

Redis的持久化机制

Redis可以利用快照RDB,和只追加文件AOF进行持久化存储到磁盘中

AOF日志

AOF是只追加文件,每次处理完请求命令后,将该命令追加到aof文件末尾。

AOF是写后日志,先执行命令,把数据写入内存,然后再记录日志

AOF为什么要先执行命令再记日志

  • 记录日志时,是不对命令进行语法检查的。
  • 如果先记录日志的话,日志中可能记录了错误的命令,导致恢复数据时可能会出错

AOF的好处

  • 先执行命令,再记录日志,可以避免记录错误命令的情况
  • 由于在记录执行后才记录日志,不会阻塞当前的操作

AOF的缺陷

  • 如果执行完命令后,没有来得及写入日志就宕机了,就有数据丢失的风险
  • AOF虽然不会阻塞当前的操作,但是可能会阻塞下一条的操作。AOF日志是在主线程中执行的,如果日志文件写入磁盘时太慢,就会导致后序的操作无法执行

AOF三种写回策略

  • Always:每个命令执行完,立马同步将日志写回磁盘
  • Everysec:每秒写回:每个写命令执行完,只是把日志写道AOF文件的内存缓冲区中,每隔一秒把缓存区的内容写入磁盘。这种如果发生宕机,可能会丢失1s的数据
  • NO:操作系统控制写回磁盘,如果宕机,那么丢失的数据就较多

AOF的重写机制

  • AOF重写机制解决日志文件过大的问题
  • 重写的时候,会根据该键值的当前最新状态,为它生成对应的写入命令。而对中间的修改之类的操作全部忽略

AOF重写会阻塞吗

和AOF日志不同,重写是由后台线程完成的。

AOF重写的具体过程

一个拷贝,两处日志

  • 每次执行重写时,主进程会fork一个子进程,由子进程进行重写,这个将不会阻塞主线程
  • 第一部分的日志是正在使用的AOF日志,第二处是新的AOF重写日志。当新的操作来临时,Redis会将其写入到主线程AOF缓冲区和新的AOF缓冲区。
    • 当拷贝数据的所有操作重写记录完成后,重写日志的记录了也会写入到新的AOF文件。
    • 然后使用新的AOF文件替代旧的文件

RDB快照

RDB类似于一种照片记录效果的方式,把某一时刻的状态写入到文件中

RDB是Redis的默认持久化方案

给那些内存数据做快照

会把内存的所有数据记录到磁盘中,就是全量快照

  • save:主线程中执行,导致主线程阻塞
  • bgsave:创建一个新的子进程,专门写入RDB文件,不会导致阻塞,默认。

bgsave指令工作管理

  • 执行bgsave命令,然后fork一个bgsave子线程
  • bgsave子进程运行后,读取内存数据,写入RDB文件
  • 如果主线程都是读操作,那么主线程和bgsave子线程互不影响
  • 如果主线程要修改一块数据,那么就会这块数据就会复制一份,生成该数据的副本。然后bgsave会把副本数据写入到RDB文件。在这个过程中主线程仍然可以修改原来的数据

RDB缺点

  • fork创建本身会阻塞线程,而且主线程的内存越大,阻塞时间越长
  • 每隔一段时间执行快照,若间隔时间宕机会导致数据丢失

RDB优点

  • RDB是一种压缩的二进制文件,存储效率高
  • RDB存储的时时间点的数据快照,适合数据备份,全量复制等
  • RDB恢复数据的速度AOF快很多。

应用场景

  • 数据不能丢失,可以两者混用
  • 允许分钟级别的丢失,kyRDB
  • aof推荐使用everysec的配置选项。可以从可靠性和性能做一个平衡

主从复制

什么是Redis的主从复制

  • 将一台Redis服务器的数据,复制到其他的Redis服务器。前者为主节点(master)后者为从节点(slave)
  • 数据的复制是单向的,只能由主节点到从节点

主从复制的好处

  • 读写分离:master写,slave读,提高服务器的负载的能力
  • 可以实现负载均衡:slave分担master的分离,并且根据需求的变化,改变slave的数量,可以分担数据的读取负载,提高Redis服务器的整体并发量和数据吞吐量
  • 故障恢复:当master出现问题时,可以由slave提供服务
  • 高可用的基石:基于主从复制,可以构建哨兵模式和集群,实现Reids的高可用方案

读写分离

  • 主库和从库采用读写分离的方式
  • 读操作:主库从库都可以接受
  • 写操作:首先到主库执行,然后主库将写操作同步到从库

为什么要采用读写分离呢

  • 如果每个节点都可以接受写请求:那么会导致数据不一致的问题,如果要保证一致性,需要加锁,节点之间的协商,这样就会造成额外的开销
  • 读写分离可以保证数据的一致性,所有的修改只会在主库,然后将数据同步到从库,效率高

从库和主库如何第一次同步

主要分为三个阶段

  • 主从库之间建立连接后,从库向主库发送psync命令,表示要开始进行数据同步。
  • 主库收到psync的命令后,使用fullresync响应命令,表示主库第一次复制采用全量复制。
    • 主库执行bgsave命令,生成rdb文件,发给从库
    • 从库接收到rdb文件后,先清空当前的数据库,然后加载rdb文件
  • 在发送RDB过程时会产生新的数据,因此主库会把第二阶段执行过程中新的写命令在发送到从库中

主从级联模式下分担全量复制时的主库压力

  • 通过主-从-从的方式将主库生成RDB和传输RDB的压力,以级联的方式分散到从库上。
  • 具体如下
    • 选择一个从库,用来级联其他的从库。

增量复制

如果主从网络断了之后,主从库会采用增量复制的方式继续同步。

  • 增量复制,只会把主从库网络断连期间主库收到的命令,同步给从库。

主从库之间的复制为什么不使用AOF呢

  • AOF虽然比RDB操作命令更全,丢失的数据更小,但是AOF的文件比RDB文件大,网络传输更加耗时
  • 初始化数据时,RDB文件执行比AOF快很多

哨兵机制

什么是哨兵,作用是什么

哨兵实际上就是一个Redis进程,主要负责自动在主从库下的:监控、选主和通知。

  • 监控:监控主库运行状态,并判断主库是否客观下线
  • 在从库客观下线后,选择新主库
  • 选出新主库后,通知从库和客户端

哨兵如何判断主库的下线状态

哨兵对主库的下线判断有主观下线和客观下线两种

主观下线

哨兵进程通过ping命令来检测主库和从库的网络连接情况。

  • 如果检测到从库ping命令超时,那么哨兵就会标记为主观下线
  • 如果检测到主库,为了防止误判,需要通过哨兵集群,少数服从多数判断为客观下线才能判断为下线状态

客观下线

使用奇数个哨兵集群,当有一半以上的哨兵判断主库为客观下线后,就可以判断为客观下线。

如何选定新的主库

  • 哨兵根据在线状态、网络状态筛选出不符合要求的从库,然后根据优先级,复制进度,ID号大小对从库进行打分。得分最高的,就选为新主库

哨兵集群

哨兵实例在运行时发生了故障,主从库还能正常切换吗

一般情况下,哨兵集群是有多个实例组成的,即使有哨兵实例出现故障挂掉,其他的哨兵还可以举行顶上

基于pub/sub机制的哨兵集群的组成

  • 哨兵实例的相互发现,是归功于发布订阅机制,哨兵只要和主库发生了连接,就可以在主库上发布消息了,例如自己的IP和端口。同时也可以再主库订阅消息,获得其他哨兵发布的信息

哨兵如何和从库建立连接

哨兵的监控任务中,需要对主从库进行心跳判断,并且主从库切换完成后,需要通知从库,让它们和新主库进行同步。

  • 哨兵是通过向主库发送INFO命令,主库收到这个命令后,把库列表发送给哨兵,哨兵根据列表中的信息和每个从库建立连接。

哨兵是否需要和客户端建立连接

  • 需要,哨兵是负责主从切换的,如果发送了主从切换,客户端需要和哨兵进行连接得到相关的信息。

切片集群

为什么要集群

因为如果有大量的数据来到,需要进行扩展

  • 纵向扩展:升级单个Redis实例的资源配置,例如内存
  • 横向扩展:增加Redis的实例个数

切片集群

  • 多个Redis实例组成一个集群,然后按照一定的规则,把收到的数据分成多份,每一份用一个实例来保存。
  • 但是这样就需要解决两个问题
    • 数据切片后,多个实例之间如何分布
    • 客户端怎么确定访问的数据在那个实例上

数据切片和实例的对应分布关系

  • 切片集群是一种保存大量数据的通用机制,官方提供了Redis Cluster的方案。
  • Redis Cluster采用了哈希槽。来处理数据和实例的关系。每个键值对根据key,来映射到哈希槽中
  • 客户端定位数据时,在创建连接后,客户端和集群实例连接后,Redis实例会把自己哈希槽信息发送给它相连的其他实例,完成哈希槽分配信息的扩散。这样客户端就可以得到这些哈希槽的信息。

缓存

缓存穿透

缓存穿透是指缓存和数据库中都没有数据,用户不断的发起请求。例如请求id为-1的数据。用户可能是攻击者

接口校验

例如商品的ID是正整数,直接对非整数的内容进行过滤。

缓存空指

如果缓存和DB都没有查询到值,可以将空值写入到缓存中。设置一定的过期时间。

布隆过滤器

  • 使用布隆过滤器可以快速判断数据是否存在,如果不存在则不需要进入到数据库中进行查询
  • 可以将常用的数据请求内容放入到布隆过滤器中

缓存击穿

缓存击穿是指缓存的热点数据时间到期,大量的请求一直打到数据库中。造成数据库压力激增

加互斥锁

并发请求中,第一个请求的线程可以拿到锁并执行数据库的查询操作,其他的线程等待第一个线程将数据写入缓存后,直接走缓存

热点数据不过期

直接设置缓存不过期,然后由定时任务异步加载数据,更新缓存。

缓存雪崩

缓存雪崩就是缓存中数据大批量的过期时间,导致查询请求都到了数据库

  • 过期时间打散:给缓存的过期时间加上一个随机值,让每个key的过期时间分开来
  • 热点数据不过期和加互斥锁。

缓存预热

  • 系统上线后,提前将相关的缓存数据加载到缓存系统。
  • 如果不进行预热,系统上线初期,面对高并发的流量,都会达到数据库中

解决方案

  • 数据量不大时,可以在工程启动时进行加载
  • 数据量大,就设置一个定时任务脚本,进行缓存的刷新
  • 若太大,则优先保证热点数据进入缓存

缓存降级

缓存降级是,缓存失效或者缓存服务器挂掉的情况下,不去访问数据库,而是直接返回默认数据。

缓存一致性

  • 缓存数据的一致性
    • 缓存中有数据,那么数据需要和数据库中的值相同
    • 缓存中没有数据,那么数据库的值必须是最新值

非并发下,数据不一致的发生和解决

主要为更新数据库和删除缓存值。中间有一个失败了,就导致客户端读取到旧值

先删除缓存,再更新数据库的值

若删除删除成功,而数据库更新失败。请求访问缓存时,发现缓存丢失,再访问数据库得到旧值

先更新数据库,后删除缓存

若更新数据库成功,但是缓存删除失效,导致请求访问缓存时,发现缓存命中,从而从缓存中读取旧值

解决方法:重试机制

可以把要删除的缓存值,或者要更新的数据库的值,暂存到消息队列中(例如Kafka消息队列),如果没有更新成功,那么就从消息队列中重新读取该值,然后再次尝试。

并发状态下,数据不一致如何解决。

先删除缓存值,后更新数据库的值

缓存删除后,尚未更新数据库,此时另外一个并发请求,从数据库读取旧值,然后更新到缓存,导致后续请求得到的都是旧值

  • 延时双删。第一个线程更新完数据库值后,sleep一段时间,再进行一次缓存删除操作

先更新数据库,后删除缓存值

优先使用这个方案,因为延迟双删的时间不好估算。 数据库更新成功后,还没有删除缓存,此时有并发请求,从缓存中读取旧值

  • 等待删除缓存期间,会有不一致数据短暂存在

image.png

Redis缓存延迟的淘汰策略

  • 不进行数据淘汰策略:noeviction
  • 会进行淘汰的策略
    • 设置过期时间的数据集中选择性删除
      • volatile-ttl:根据过期时间进行删除,越早过期删除
      • volatile-random:随机删除
      • volatile-lru:使用lru算法进行筛选
      • volatile-lfu:使用lfu算法进行筛选
    • 从全局数据集中选择性删除
      • allkeys-random:所有的键值对中随机删除
        • 如果没有冷热数据,可以使用这个
      • allkeys-lru:使用lru在所有数据中进行筛选
        • 优先使用,这个,因为把最近最长访问的数据留在内存中
      • allkeys-lfu:使用lfu在所有数据中进行筛选

过期键删除策略

Redis使用惰性删除和定期删除

  • 惰性删除:当使用key时,会检查是否过期,如果过期则删除。
    • 对cpu友好,但是对内存不友好
  • 定时删除:设置好一个过期时间的时候,创建一个定时器,让定时器在该过期时间到来时,立刻执行删除操作
    • 对内存友好,对cpu不友好
  • 定期删除:每隔一段时间都对key进行检查,删除key
    • 到可能会造成业务错误

并发

如何保证并发的正确性

redis提供了加锁和原子操作

  • 加锁:读取数据钱,需要先获得锁,更新完成后,在释放
    • 但是加锁的操作多的时候,会降低系统的并发性能
    • redis客户端要加锁的时候,可能使用分布式锁,比较复杂
  • 原子操作:执行过程中保证了原子性
    • 可以保证并发,不需要进行加锁
      • 多个操作在Redis是实同一个个操作
      • 多个操作卸载一个Lua脚本中,以原子的方式执行Lua脚本

事务

Redis能实现ACID的属性吗

  • Redis只能保证一致性和隔离性,对持久性无法保证,因为Redis本身时内存数据库
  • 原子性需要分类,如果事务的执行没有错误,是可以进行保证的,如果事务中的语句执行错误了,是无法保证原子性的。

Redis如何实现事务

  • multi显示的开启一个事务
  • 添加相应的操作,redis实例把这些命令放到一个队列,并不会立刻执行
  • 客户端向服务端发送提交事务的exec命令,执行第二步的操作

Redis的事务可以保证原子性吗

  • 命令入队时如果报错了,放弃事务的执行,就可以保证原子性
  • 如果没有报错,实际执行时报错,那么就不保证原子性
  • 如果执行的时候实例故障,开启了aof日志,可以恢复,那么就可以保证原子性

Redis的事务可以保证一致性吗

  • 在命令执行错误或者Redis发送故障的情况下,Redis事务机制对一致性属性是由保证的

Redis的事务可以保证隔离性吗

  • 事务的隔离性保证,会和事务的执行的并发操作有关
    • 并发操作在EXEC命令执行之前保证,此时隔离性需要开启WATCH机制来实现,否则隔离性无法保证
      • WATCH机制可以检查监控的键是否被其他的客户端修改了,如果修改了,那么直接报错
    • 并发操作在EXEC命令执行之后,隔离性可以保证

Redis的食物可以保证持久性吗

  • Redis不管采用持久化形式,数据的持久性得不到保障。
    • RDB快照,在一个事务执行完后,下一次的RDB快照还没有执行,此时宕机。
    • AOF,no,evverysec和always都会发生数据丢失的情况

Redis为什么不支持事务回滚

  • 因为Redis的错误,只会因为语法而失败,应该在开发过程中发现