总结不易,看完请点个赞
杂谈
本文是笔者这十年的被面与面试的真实经历总结,本篇主要总结Redis相关高频知识点, 内容总结主要来自于微博、蚂蚁金服、头条、网易、京东、拼多多、MOKA、兴盛优选、瑞幸咖啡、OPPO等厂的面试问题,废话就不多说了,直接上正文。
大纲
- 数据结构
- 应用场景
- 缓存相关
- 过期策略
- 数据持久化
- 淘汰机制
- 主从同步
- 主从同步
- 哨兵机制
- 拓展
数据结构
基础数据结构
String
-
<key,value>形式
-
当字符串长度小于1M前,成倍扩容,大于1M后每次只扩容1M最大为512M
-
每次修改后会使设置的过期时间失效
List
-
List类型是一个链表结构的集合,并且是有序的集合,其值是可以重复的
-
List类型有点类似数组的概念,所以具有下标,可以针对指定下标进行操作
-
既可以做【栈-先进后出】使用,也可以做【队列-先进先出】使用,所以常用来做异步队列使用
这是一个微博场景用的非常频繁的数据结构,有非常多的应用场景,需要特别注意
Hash
- Hash类型是String类型的field和value的映射表
- hash 可以对用户结构中的每个字段单独存储。这样当我们需要获取用户信息时可以进行部分获取
- 而以整个字符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量
- 但是hash 结构的存储消耗要高于单个字符串
京东阿里等平台购物车场景实现用的就是这种数据结构,当然需要配合持久化使用,为啥购物车有添加上限,看到这里你想明白没?
Set
-
相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的,即可以用来做去重的处理
-
以对Set实施交集(Inter--InterStore)、并集(Union--UnionStore)和差集(Diff--DiffStore)的操作
这个数据结构与Zset在社交场景里也是应用非常广泛,后续写个专篇来总结应用场景,感兴趣的读友可关注笔者公众号:[奇客时间]
Zset
-
zset是一个有序集合,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重
-
根据score,可以排序分值(学生成绩),时间(心跳在线时间或者用户关注时间),作为延时队列,限流
经典应用场景:排行榜
容器型数据结构的通用规则
list/set/hash/zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:
- 如果容器不存在,那就创建一个,再进行操作
- 如果容器里元素没有了,那么立即删除元素,释放内存
HyperLogLog
HyperLogLog 提供不精确的去重计数方案(经典场景:统计UV),标准误差为0.81%
主要指令
pfmerge指令:用于将多个 pf 计数值累加在一起形成一个新的 pf 值(合并多个页面的UV值)
缺点
无法知道某一个值是不是已经在 HyperLogLog 结构里面(仅提供添加,统计,合并三个操作)
布隆过滤器
其本质就是一个巨型的BitMap, 可以理解为一个不怎么精确的 set 结构,通过在add前用bf.reserve指令显式创建,设定参数提高准确率 , initial_size参数表示预计放入的元素数量,initial_size参数设置错误率,错误率越低,需要的空间越大 。
特性
当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在
处理场景-{比如 :缓存穿透 }
解决去重的问题。比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重
GeoHash
实现附近的人的功能 参考地址:juejin.cn/post/684790…
Stream
redis 5.0后版本出现的数据结构 可以支持广播模式的可持久化的消息队列
应用场景
分布式锁
-
先用setnx ex来获取锁,并设置过期时间,过期时间主要用来防止业务代码异常,锁没有释放的情况导致死锁的情况 。
-
redis分布式锁过期时间到了业务没执行完问题
- redssion 中的 watch dog自动延期机制
- 默认设置过期时间为30秒,只要客户端成功获得锁,就会启动watch dog。每10秒检查一次客户端是否还持有锁,如果有,则延长过期时间
-
主从异步复制导致的问题
- 客户端1对某个redis master实例,写入了锁,此时会异步复制给对应的master slave实例
- 但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master
- 接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。
- 此时就会导致多个客户端对一个分布式锁完成了加锁
- RedLock算法:www.jianshu.com/p/fba7dd6dc…
计数器
- 分库分表的自增id
- INCR指令实现简单限流
缓存
缓存热点数据,扛读压力
异步队列
- list结构作为队列,使用rpush/lpush操作入队列,使用lpop 和 rpop来出队列。缺点:没有等待队列里有值就直接消费,可以通过在应用层引入sleep机制来降低请求频率
- list中还有另一个指令,BLPOP key [keys ...] timeout:阻塞直到队列有消息或者超时,当 timeout 为 0 是表示阻塞时间无限制,会导致空闲连接问题,服务器会主动断开,抛出异常,客户端需要捕获处理,或重试,并且只能供一个消费者消费
- pub/sub:主题订阅者模式 实现1:N的消息队列,但是消费者下线的时候会导致消息丢失,所以当redis宕机的时候,pub/sub的数据不会持久化
- Redis5.0后提供 Stream 解决了pub/sub消息不能持久化的问题。blog.csdn.net/enmotech/ar…
延时队列
- 使用zset结构,将消息序列化成一个字符串作为 zset 的value,这个消息的到期处理时间作为score
- 用多个线程轮询 zset 获取到期的任务进行处理,多个线程是为了保障可用性,万一挂了一个线程还有其它线程可以继续处理
其它场景
解决服务器分布式部署时,由于负载均衡策略,分发用户请求到不同的服务器,导致session不一致的问题,即分布式会话问题。
缓存相关
缓存雪崩
概念理解
同一时间大面积的缓存失效,或者redis宕机,瞬间所有查询打到DB上,导致DB奔溃。
解决方案
- 分散key的过期时间
- 通过搭建缓存的高可用,避免缓存挂掉导致无法提供服务的情况
- 使用本地缓存时,即使分布式缓存挂了,也可以将 DB 查询到的结果缓存到本地,避免后续请求全部到达 DB 中
缓存穿透
概念理解
指查询一个一定不存在的数据,由于缓存是不命中时被动写,并且处于容错考虑,如果从 DB 查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,失去了缓存的意义,如果请求过大的话,很有可能直接把数据库打垮了。
形成原因
被动写:当从缓存中查不到数据时,然后从数据库查询到该数据,写入该数据到缓存中
解决方法
-
网关层(比如说nginx)做限制,对每秒请求超出一定阈值的ip做拉黑处理
-
缓存空对象。当从 DB 查询数据为空,我们仍然将这个空结果进行缓存,并设置过期时间
-
使用布隆过滤器。在缓存服务的基础上,构建 BloomFilter 数据结构,在 BloomFilter 中存储对应的 KEY 是否存在,如果存在,说明该 KEY 对应的值不为空。需要提前将已存在的数据放在布隆过滤器里面
-
1、根据 KEY 查询【BloomFilter 缓存】。如果不存在对应的值,直接返回;如果存在,继续向下执行。
-
2、根据 KEY 查询在【数据缓存】的值。如果存在值,直接返回;如果不存在值,继续向下执行。
-
3、查询 DB 对应的值,如果存在,则更新到缓存,并返回该值。
-
缓存击穿
概念理解
某个极度“热点”数据在某个时间点过期时,恰好在这个时间点对这个 KEY 有大量的并发请求过来,导致DB奔溃
解决方案
- 使用互斥锁。请求发现缓存不存在后,去查询 DB 前,使用分布式锁,保证有且只有一个线程去查询 DB ,并更新到缓存。
- 1、获取分布式锁,直到成功或超时。如果超时,则抛出异常,返回。如果成功,继续向下执行。
- 2、获取缓存。如果存在值,则直接返回;如果不存在,则继续往下执行。因为,获得到锁,可能已经被“那个”线程去查询过 DB ,并更新到缓存中了
- 3、查询 DB ,并更新到缓存中,返回值。
- 手动过期。缓存上从不设置过期时间,功能上将过期时间存在 KEY 对应的 VALUE 里
- 1、获取缓存。通过 VALUE 的过期时间,判断是否过期。如果未过期,则直接返回;如果已过期,继续往下执行。
- 2、通过一个后台的异步线程进行缓存的构建,也就是“手动”过期。通过后台的异步线程,保证有且只有一个线程去查询 DB。
- 3、同时,虽然 VALUE 已经过期,还是直接返回。通过这样的方式,保证服务的可用性,虽然损失了一定的时效性。
双写数据不一致
产生原因
- 并发的场景下,导致读取老的 DB 数据,更新到缓存中
- 缓存和 DB 的操作,不在一个事务中,可能只有一个 DB 操作成功,而另一个 Cache 操作失败,导致不一致。
解决方法
- 1、将缓存可能存在的并行写,实现串行写,对性能影响较大。
- 在写请求时,在淘汰缓存之前,先获取该分布式锁。
- 在读请求时,发现缓存不存在时,先获取分布式锁。
- 2、延时双删:先淘汰缓存,再写数据库,(线程sleep一段时间)然后再删缓存;无法完全保证双写一致,任然有一定几率出现数据不一致。参考阅读:www.w3cschool.cn/architectro…
数据持久化
持久化的意义:故障恢复
数据持久化主要有两种数据格式:
- RDB ,一份数据文件,恢复的时候,直接加载到内存中即可
- AOF ,存放的指令日志,做数据恢复的时候,其实是要回放和执行所有的指令日志,来恢复出来内存中的所有数据的
RDB
简要
RDB 快照,对redis中的数据执行周期性的全量持久化到磁盘,恢复快
快照时间点
RDB生成快照可自动触发,也可以使用命令手动触发,以下是redis触发执行快照条件
- 客户端执行命令save和bgsave会生成快照
- save操作阻塞主进程,会导致服务一段时间不可用
- bgsave 是fork子进程来执行save操作,仅在fork子进程的时候发生阻塞
- 根据配置文件save m n规则进行自动快照
- 主从复制时,从库全量复制同步主库数据,此时主库会执行bgsave命令进行快照
- 客户端执行数据库清空命令FLUSHALL时候,触发快照
- 客户端执行shutdown关闭redis时,触发快照
持久化流程
- 1、fork一个子进程出来,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来
- 2、子进程尝试将数据dump到临时的rdb快照文件中
- 3、完成rdb快照文件的生成之后,就替换之前的旧的快照文件
优点
- 灵活设置备份频率和周期
- 非常适合冷备份,对于灾难恢复来说,RDB 是非常不错的选择。我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上
- 性能最大化。对于 Redis 的服务进程而言,在开始持久化时,它唯一需要做的只是 fork 出子进程,极大避免了服务进程进行IO操作
- 相比于 AOF 机制,RDB 的恢复速度更更快
缺点
- 如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔一段时间生成一次
- RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒
AOF
简要
AOF 追加更新指令 到本地文件,达到一定的量后会对文件进行瘦身
AOF持久化配置
- always: 每次写入一条数据,同步追加到aof文件,性能非常非常差,吞吐量很低; 确保说redis里的数据一条都不丢失
- everysec: 每秒将os cache中的数据追加到aof文件。默认的配置
- no: 仅仅redis负责将数据写入os cache就撒手不管了,然后后面os自己会时不时有自己的策略将数据刷入磁盘,不可控制(即redis不负责更新aof文件,何时更新由宿主机自己决定,Linux64位大概是30秒一次)
AOF 重写
- aof文件是追加的形式增量递增的 ,因为内存的容量是有限的,且redis的有些数据会有过期的概念,和淘汰机制的存在,会导致一开始存储的指令没有实际对应的数据。
- 当aof文件增长到一定量(auto-aof-rewrite-percentage 100 增长的比例 ,auto-aof-rewrite-min-size 64mb 重写的最小阈值 )的时候会对其进行重写
- 重写工作流程
- redis fork一个子进程
- 子进程基于当前内存中的数据,构建日志,开始往一个新的临时的AOF文件中写入日志
- redis主进程,接收到client新的写操作之后,在内存中写入日志,同时新的日志也继续写入旧的AOF文件
- 子进程写完新的日志文件之后,redis主进程将内存中的新日志再次追加到新的AOF文件中,最后替换旧的文件
优点
- 该机制可以带来更高的数据安全性,即数据持久性。Redis 中提供了 3 种同步策略,即每秒同步、每修改(执行一个命令)同步和不同步
- 日志文件的写入操作采用的是 append 模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容
- AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据
缺点
- 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
- 根据同步策略的不同,AOF 在运行效率上往往会慢于 RDB
持久化方案
redis主从的时候,主节点做aof (避免aof重写?) 从节点使用aof+rdb混合持久化
过期策略
主动过期
redis会将所有设置过期时间的key放在一个独立的字典里面,以后会定时遍历这个字典来删除到期的 key
- 1、默认会每秒进行十次过期扫描
- 2、从过期字典中随机 20 个 key,删除这 20 个 key 中已经过期的 key
- 3、如果过期的 key 比率超过 1/4,那就重复步骤2
- 4、同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms
惰性删除
就是指在客户端访问这个key的时候,redis对key的过期时间进行检查,如果过期了就立即删除。
淘汰机制
当Redis内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap) , 交换会让 Redis 的性能急剧下降,对于访问量比较频繁的 Redis 来说,这样龟速的存取效率基本上等于不可用。为了限制最大使用内存,Redis 提供了配置参数 maxmemory 来限制内存超出期望大小 ,超出后执行淘汰策略。
淘汰策略
- noeviction:不淘汰key(默认)
- volatile-lru : 淘汰过期集合的key中最少使用
- volatile-ttl : 淘汰过期集合的key中,剩余寿命最短
- volatile-random : 随机淘汰过期集合的key
- allkeys-lru : 淘汰全部key中最少使用
- allkeys-random 随机淘汰全部key
主从架构-主从同步
增量同步
Redis 同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存 buffer 中,然后异步将 buffer中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一样的状态,一边向主节点反馈自己同步到哪里了 (偏移量) 因为内存的 buffer 是有限的,所以 Redis 主库不能将所有的指令都记录在内存 buffer 中 ,Redis 的复制内存 buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。
快照同步
快照同步是一个非常耗费资源的操作,它首先需要在主库上进行一次 bgsave 将当前内存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点 ,从节点将快照文件接受完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。加载完毕后通知主节点继续进行增量同步在整个快照同步进行的过程中,主节点的复制 buffer 还在不停的往前移动,如果快照同步的时间过长或者复制 buffer 太小,都会导致同步期间的增量指令在复制 buffer 中被覆盖 。
特别说明
当从节点刚刚加入到集群时,它必须先要进行一次快照同步,同步完成后再继续进行增量同步。
哨兵机制
- 哨兵+主从 实现redis 集群高可用
- 集群监控:负责监控Redis master和slave是否征正常进行工作
- 消息通知:如果某个Redis实例挂了,那么哨兵会发送通知给系统管理员
- 故障转移:如果master node挂了,会自动转移到slave node上
- 配置中心:如果故障转移发生,通知客户端最新的master node的地址
Sentinel 无法保证消息完全不丢失,但是也尽可能保证消息少丢失。它有两个选项可以限制主从延迟过大
- min-replicas-to-write 1 表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务,丧失可用性
- min-replicas-max-lag 10 表示如果 10s 没有收到从节点的反馈,就意味着从节点同步不正常
拓展
keys 和 scan
redis是单线程的,在数据量大的时候,keys * 操作获取全部的key就会导致 Redis 服务卡顿,其余的指令延后或超时报错
scan 是通过游标分步进行的,不会阻塞线程
- 提供 limit 参数,可以控制每次返回结果的最大条数
- 返回的结果可能会有重复,需要客户端去重复
- 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的
redis线程模型
- Redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。
- 它采用 IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理
- 多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket 会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理
雕爷写到这里,Redis所涉及到的应用场景跟原理基本总结完了,唯一有Redis-Cluster涉及完全,后续雕爷将会在另一篇博文当中专门总结。
文章原创不易,看完动动手指点个赞,感谢!
若要转载请先联系笔者,谢谢。