9
系列综述:
💞目的:本系列是个人整理为了
秋招面试
的,整理期间苛求每个知识点,平衡理解简易度与深入程度。🥰来源:材料主要源于拓跋阿秀、小林coding等大佬博客进行的,每个知识点的修正和深入主要参考各平台大佬的文章,其中也可能含有少量的个人实验自证。
🤭结语:如果有帮到你的地方,就点个赞和关注一下呗,谢谢🎈🎄🌷!!!
文章目录
基本篇
- Redis是什么
- 定义:是一个
内存数据库
,提供多种高效的数据结构
来支持不同的业务场景
,从而实现高并发和高性能。 - 内存存储:内存存储读写快,能提供高并发和低时延的数据访问
- 数据结构:String类型、List类型、Hash类型、Set类型、Zset类型、Bitmap类型、HyperLogLog类型、Geo类型、Stream类型
- 业务场景:缓存、消息队列、点赞系统、共同关注、抽奖活动、排行榜系统、连续签到天数等
- 定义:是一个
- 使用Redis的好处有哪些?
- 高并发:Redis基于内存存储,读写速度快,具有好的并发性能
- 数据结构丰富、应用场景广泛
- 持久化支持:Redis支持多种持久化方式,可以将数据存储到磁盘中,从而避免数据丢失。
- 分布式支持:Redis支持分布式部署,可以通过Redis Cluster或Sentinel等机制,扩展应用程序以提供更好的性能和可用性。
- 简单易用:Redis的命令简单易用,易于上手,而且支持多种编程语言和操作系统。
- Redis解决了MySQL那些问题 / Redis相比MySQL的优点
- 高性能
- MySQL具有磁盘IO的速度瓶颈,但Redis
基于内存存储
,读写速度快 - Redis具有
丰富的数据结构
能适合不同的业务场景。
- MySQL具有磁盘IO的速度瓶颈,但Redis
- 高拓展性:Redis
支持分布式部署
,支持集群间的数据迁移。
- 高性能
数据结构篇
String类型
- String的数据结构
- 总体结构:String 数据类型是一种
KV结构
,其中key是字符串类型
,而value是redisObject结构体类型
。 - redisObject 结构体:包括
对象类型(type)
、编码格式(encoding)
、SDS指针
等字段
- SDS结构(Simple Dynamic String)的组成
- len:标识SDS当前保存字符串的实际长度,是一个无符号整数,通过该字段O(1)时间内获得字符串长度
- free:标识SDS中未使用的空间长度,
- buf:定义为
char buf[ ]
,是c99引入的柔性数组,实际是一个指针 - 拓展方式类似于vector,通常每次进行翻倍扩容增大free,但SDS最大容量为512MB。
- 编码格式
- int:不使用SDS进行存储,因为int类型的值大小固定
- raw:redisObject和其ptr指向的SDS分别进行内存空间的分配
- embstr:SDS内嵌到redisObject结构体中,申请和释放一次内存处理。内存连续有利于CPU缓存,提高性能。
- 总体结构:String 数据类型是一种
- SDS的优点
- 保存多种数据:可以保存文本或二进制数据,即也能保存音视频和图片等任意数据类型
- API接口安全:字符串拼接会进行边界安全检查
List类型
- List的数据结构
- 前述:在 Redis 3.2 版本之前,由压缩列表或双向链表实现,之后均由quicklist实现。
- 结构(类似deque)
- 控制器:链表头指针head、链表尾指针tail、quicklist属性信息(节点数量、总元素数量等)
- 双向链表:quickList链表结点间通过双指针连接,quickList内部有指向zipList的指针,zipList是一个键值对数组。
- 插入
- 头尾插入:zipList中有空闲则直接插入,没有则创建新结点再插入
- 中间插入:有空则插入,没空则查找相邻,相邻没空则分裂被插入结点
- 常用于消息队列
- 优点
- 消息保存:建立生产者消费者模型,通过
LPUSH+RPOP
实现消息操作 - 阻塞自旋:使用
BRPOP
将获取到空消息的消费者阻塞,避免其自旋空转 - 重复消息处理:
生产者自行实现全局唯一 ID
,消费者记录已经处理过的消息的 ID,每次处理消息前判断消息是否已经处理过
- 消息保存:建立生产者消费者模型,通过
- 问题:消费者读出消息后,消息队列将删除被读出的消息。
- 消息可靠性问题:如果消费者进行消息处理失败,可通过
BRPOPLPUSH
,从备份List读取过去的消息 - 多消费者消费同一消息:从消息队列读出的一个消息可通过消费者组分发到多个消费者,但是 List 类型并不支持
- 消息可靠性问题:如果消费者进行消息处理失败,可通过
- 优点
Set类型
- 定义:String类型的无序且唯一的集合,key是set名,value为set中的元素
- 特点
- 用途:可以对集合进行增删改查,还可以求多个集合的并交差(主库会导致事务阻塞,通常通过从库进行)
- 当元素小于512个并全为整数时,底层会使用数组进行存储,否则使用哈希表
- 常见应用场景
- 点赞系统
- 示例:用户A对一个视频B点赞
- 原理:视频B为set名称,用户A为set中的一个元素。
- 共同关注
- 示例:A和B各自关注了很多公众号
- A和B作为set名,关注公众号作为各自set中的元素,可以通过交集计算
- 抽奖活动
- key为抽奖活动名,value为员工名称,把所有员工名称放入抽奖箱,可以保证同一个用户不会中奖两次,也可使用
SRANDMEMBER
命令允许重复中奖
- key为抽奖活动名,value为员工名称,把所有员工名称放入抽奖箱,可以保证同一个用户不会中奖两次,也可使用
- 点赞系统
Zset类型
- 定义:是一种有序集合类型,在Value中增加了一个排序属性
- 原理:使用
跳表
实现的 - 业务场景
- 排行榜系统,如销量排名、视频播放排名等
BitMap类型
- 定义:Bitmap位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素
- 适合
数据量大
的二值统计
场景- 适合数据量大:bit 是计算机中最小的单位
- 二值统计:一个bit位只能表示两种状态
- 应用场景
- 连续签到天数:用户为key,value为每天签到状态的bit数组
- 用户登录状态:将用户 ID 作为 offset,在线就设置为 1,下线设置 0
- 连续签到用户总数:把每天的日期作为 Bitmap 的 key,userId 作为 offset,对每天的Bitmap 的对应的用户bit位做 『与』运算
HyperLogLog介绍类型
- 概述
- 作用:进行快速基数统计,可以快速计算
集合中不重复元素的个数
- 原理:统计规则是基于
概率
完成的,标准误算率是 0.81%
。 - 优点:能够使用较小空间开销进行快速的基数统计,每个 HyperLogLog 键只需要花费 12 KB 内存,即可计算接近 2^64 个不同元素的基数。HyperLogLog 就非常节省空间。
- 作用:进行快速基数统计,可以快速计算
- 应用场景
- 百万级以上的网页用户访问量计数
Stream介绍类型
- 概述:Redis 专门为消息队列设计的数据类型。
- 原理:新的消息被追加到Stream的尾部,消费者从Stream的头部开始消费消息。这种方式可以实现高性能的写入和读取操作,并且保证消息的顺序性
- 优点:
- 消息持久化:会将消息持久化到磁盘中,避免redis崩溃导致消息丢失
- 自动生成全局唯一 ID:保证每个消息的唯一性
- 消息的ack 确认:消费者处理完消息会向服务器发送ack确认
- 支持消费组模式:消费者消费消息后,通过ack告知,服务器会将消息标注为已消费状态,同组的其他消费者将无法消费,解决多个消费者消费同一条消息问题
- 消息索引:维护一个消息索引实现高效的消息查询和消费
- 消息留存:Streams 会自动使用内部队列留存消费者读取的消息,直到消费者使用 XACK 命令通知 Streams"消息已经处理完成
- 专业的消息队列
- 消息不可丢
请求响应机制
保证生产者
的消息不可丢AOF持久化
保证仅丢失刷盘间隔内的中间队列
消息丢失内部队列的消息留存
保证消费者
正消费的消息不会丢失
- 消息可堆积。
- 持续的消息积压可能导致内存溢出
- Kafka、RabbitMQ 专业的消息队列通过磁盘存储解决
- 消息不可丢
- 消息队列中间件应用场景
- 业务场景足够简单,对于数据丢失不敏感,而且消息积压概率比较小的情况下,把 Redis 当作队列是完全可以的。
- 如果你的业务有海量消息,消息积压的概率比较大,并且不能接受数据丢失,那么还是用专业的消息队列中间件吧。
- 发布/订阅机制只适合即时通讯的场景,当 Redis 宕机重启,发布/订阅机制的数据也会全部丢失。
GEO介绍类型
- 定义:用于地理位置信息的处理
- 原理:
- 根本:进行降维处理,将二维坐标换成一维的字符串,实现快速的精确定位
- 对二维地图按比例进行区间划分
- 使用GeoHash对每个区间进行编码,将编码值保存到sorted Set中进行排序
- 搜索某个地理位置附近的点,可以通过GeoHash编码后查找Sorted Set中相邻的值。
- 对于 Sorted Set 中的元素,可以保存更多的信息,如点的名称、详细地址、照片等
- 业务场景
- 滴滴打车:GEO 集合保存所有车辆的经纬度信息,用户可以快速找到自己附近的车辆
Hash类型
- Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
- 在redis的Hash类型是KV结构,其中VALUE也是一个kv结构
持久化篇
AOF日志(存储操作)
- AOF(Append Only File) 持久化
- 原理:将Redis的写操作命令完成后再按序追加到文件中,宕机重启则读取执行文件中的命令进行恢复
- 配置:在 Redis 中 AOF 持久化功能
默认是不开启
的,需要修改 redis.conf 配置文件
- 为什么将命令先执行,后写入文件中?
- 优点
- 避免额外的检查开销:不需要回滚,保证记录在 AOF 日志里的命令都是正确执行完成的
- 不会阻塞当前写操作命令的执行:不用写操作命令执行成功后,再持久化到AOF日志
- 缺点:
- 丢失的风险:
写操作命令的执行
和日志的追加
过程中,服务器宕机,会有数据丢失风险 - 阻塞风险:不会阻塞当前写操作命令的执行,但是可能会
给下一个命令带来阻塞风险
。
- 丢失的风险:
- 优点
- Redis写操作命令追加到AOF日志的过程
- 追加到
server.aof_buf 缓冲区
:写操作命令执行完成后,会将命令追加到 server.aof_buf 缓冲区 - 拷贝到
内核缓冲区
:通过 write() 系统调用,将 aof_buf 缓冲区的数据拷贝到了内核缓冲区 page cache - 内核刷盘到
AOF文件
:具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。
- 追加到
- 刷盘策略
- 本质:控制磁盘同步函数 fsync() 的调用时机,需要进行安全性和性能的平衡
- 本质:控制磁盘同步函数 fsync() 的调用时机,需要进行安全性和性能的平衡
- AOF(Append Only File) 重写
- 作用:避免AOF日志过大,导致的恢复过慢问题
- 原理
- 触发:当 AOF 文件的大小
超过所设定的阈值
后,Redis 就会启用 AOF 重写机制 - 压缩:存在多条命令修改某个键值对,则
只记录该键值对的最新状态
,代替之前多条修改命令 - 覆盖:最后在重写工作完成后,将新的 AOF 文件
覆盖
现有的 AOF 文件。
- 触发:当 AOF 文件的大小
- 重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的好处
- 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程
- 子进程以只读的形式共享父进程的数据,通过写时复制可以不加锁进行数据的修改,避免物理内存数据的复制时间过长而导致父进程长时间阻塞的问题
RDB 快照(存储数据)
- 原理(拷贝语义)
- 拷贝:Redis会fork子进程将
全部
的内存实际数据
快照拷贝到临时文件 - 替换:使用临时文件
替换
RDB文件
- 拷贝:Redis会fork子进程将
- 特点:
- 全量快照:每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。频率太频繁,会造成性能负担,Redis默认为5分钟,即如果 Redis 出现宕机等情况,则意味着最多可能丢失 5 分钟数据(比AOF多)。
- 恢复速度快:由于记录的是实际数据,所以可以直接加载
- Redis生成 RDB 文件的方式
- 阻塞主线程:执行save命令,由
主线程负责生成
RDB文件 - 异步生成:执行bgsave 命令,子进程通过
写时复制COW
生成RDB 文件,这样可以避免主线程的阻塞;
- 阻塞主线程:执行save命令,由
- cow中子进程拷贝的父进程数据页正在被修改,如何处理
- 竞态条件:操作系统会在该数据页上额外创建一个新的物理页,用于存储父进程的修改。这样,子进程所拷贝的数据页和父进程的原始版本仍旧保持一致,而父进程的修改则被保存在新的物理页中。这个新的物理页只会被父进程所使用。
- 保存过去:Redis 在使用 bgsave 快照过程中,如果主线程修改了内存数据,子进程写入到 RDB 文件的内存数据只能是原本父进程未修改前的内存数据。
- 混合持久化
- RDB基础+修改追加:AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据:在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
- 优点
- 全量快照部分增加了加载速度
- 追加修改部分缩小了数据丢失的范围
大 Key 对 AOF 日志的影响
- 不同刷盘策略带来的影响
- Always 策略:如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。
- Everysec策略:由于是异步执行 fsync() 函数,所以大 Key 持久化的过程不会影响主线程。
- No策略:由操作系统控制写回时间,所以大 Key 持久化的过程不会影响主线程。
- 对于持久化的影响
- 重写频繁:当AOF日志写入了很多的大 Key,AOF 日志文件的大小会很大,那么很快就会触发 AOF 重写机制。
- 阻塞主线程:fork函数由主线程调用,若fork的页表很大,会导致主线程被阻塞,无法响应后续客户端发来的命令
- fork大页表带来的性能瓶颈,比如超过1秒,则需要做出优化调整:
- 避免大页表:一般将单个实例的内存占用控制在 10 GB 以下
- 关闭关闭 AOF 和 AOF 重写:不会调用fork导致大页表阻塞瓶颈,适合数据安全性要求不高的场景,如单纯做缓存
- 在主从架构中,要适当调大主从同步缓冲区repl-backlog-size,避免主节点频繁地使用全量同步的方式,创建 RDB 文件时调用 fork 函数导致的页表阻塞瓶颈
- 写时复制阻塞主线程问题
- 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
- 创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝父进程物理内存,如果内存越大,自然阻塞的时间也越长;
- Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配。这会导致COW复制瓶颈,所以要关闭内存大页(默认是关闭的)
功能篇
过期删除策略
- 概述
- 基本原理
- 保存过期时间:通过
过期字典
以哈希结构(key : 过期时间)的形式保存所有key的过期时间 - 命中查询:先以O(1)时间查询过期字典,未命中则直接处理数据,命中则将该 key 的过期时间与当前系统时间进行比对,判定该 key是否已过期。
- 保存过期时间:通过
- 策略分类
- 定时删除;
- 惰性删除;
- 定期删除;
- 实际策略: Redis 选择「惰性删除+定期删除」这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。
- 基本原理
- 定时删除策略
- 原理:在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。
- 优点:可以保证过期key的快速删除,对内存友好
- 缺点:删除过期的key会占用相当一部分的cpu时间,对CPU不友好
- 惰性删除策略
- 原理:不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
- 优点:是正常访问的附属操作,开销最小,对cpu最友好
- 缺点:如果一个过期的key一直没有被访问就会一直留在内存,对内存不友好
- 定期删除策略
- 原理:每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。如果超过1/4立即进行下一轮删除,如果没有超过会在10s后进行。(这个思路不错)
- 特点:通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的;无效占用。
内存淘汰策略
- 概述
- 触发条件:redis.conf 中,可以通过参数 maxmemory 来设定最大运行内存,运行内存达到了设置的最大运行内存,会触发内存淘汰策略。
- 策略分类:
- 不淘汰数据策略(Redis3.0之后,默认的内存淘汰策略)
- 进程数据淘汰策略
- 不淘汰数据策略
- noeviction:当运行内存超过最大设置内存时,不淘汰任何数据,如果有新的数据写入,会报错通知禁止写入3. 进行数据淘汰策略
- 在设置过期时间的数据范围进行淘汰
- volatile-random:
随机淘汰
设置了过期时间的任意键值 - volatile-ttl:优先淘汰
更早过期
的键值,多个相同使用lfu - volatile-lru:淘汰
最近最久未使用
的键值 - volatile-lfu:淘汰
最少使用
的键值
- volatile-random:
- 在所有数据范围内进行淘汰
- allkeys-random:
随机淘汰
任意键值; - allkeys-lru:淘汰整个键值中
最久未使用
的键值; - allkeys-lfu:淘汰整个键值中
最少使用
的键值。
- allkeys-random:
- 淘汰策略的设置
- 通过
config set maxmemory-policy <策略>
设置:设置后立即生效,重启Redis后失效 - 通过
maxmemory-policy <策略>
设置:设置后需要重启,但是之后一直有效
- 通过
- Redis中的近似LRU算法
- 传统的 LRU 算法存在两个问题
- 空间上:链表指针带来额外开销
- 时间上:链表移动操作开销大
- 原理:
最后访问时间字段
:在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。- 随机采样方式:随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
- 缺点:无法解决大量访问新数据导致的
缓存污染问题
,可以使用young和old分区进行处理
- 传统的 LRU 算法存在两个问题
- LFU算法
- 原理:基于访问时间和访问次数的淘汰策略,优先淘汰
访问间隔最大和
和数据访问次数最少
的数据 - 实现
- Redis对象头的 24 bits 的 lru 字段被分成两段来存储
- 高 16bit 存储 ldt(Last Decrement Time):用来记录key的访问时间戳
- 低 8bit 存储 logc(Logistic Counter):用来记录key的访问频次
- 每次访问:计算相邻两次访问时间差,时间差越大,那么logc衰减值就越大,然后 logc 越大的 key增加概率越小
- Redis对象头的 24 bits 的 lru 字段被分成两段来存储
- 配置文件
- lfu-decay-time 用于调整 logc 的衰减速度,它是一个以分钟为单位的数值,默认值为1,lfu-decay-time 值越大,衰减越慢;
- lfu-log-factor 用于调整 logc 的增长速度,lfu-log-factor 值越大,logc 增长越慢
- 原理:基于访问时间和访问次数的淘汰策略,优先淘汰
高可用篇
主从复制模式
- 概述
- 定义:将主服务器的实例数据同步到从服务器的过程
- 原理:主服务器可以进行读写操作,当发生写操作时会同步给从服务器保证数据一致性,但是从服务器只允许进行读操作。
- 作用
- 实现数据备份,避免单点故障
- 进行负载均衡,读写分离
- 故障恢复
- 分类
- 全量复制
- 增量复制
- 基于长连接的命令传播
- 执行流程
- 第一阶段:建立链接、协商同步(确定服务器的主从关系)
- 本服务器执行replicaof命令确定自己的主服务器,并向其发送psync命令,表示要进行数据同步
- 主服务器收到 psync 命令后,产生
FULLRESYNC响应
(全量同步,主服务器将所有数据同步给从服务器),其中runID表示主服务器的唯一标识,offset表示复制进度
- 第二阶段:主服务器同步数据给从服务器
- 主服务器会执行 bgsave 命令来生成 RDB 文件,然后把文件发送给从服务器。从服务器收到 RDB 文件后,会先清空当前的数据,然后载入 RDB 文件
- 问题:bgsave 执行中,子进程异步生成并发送的RDB的期间,如果主服务器中仍执行写操作,可能导致主从数据的不一致
- 解决:将同步期间对RDB的生成、传输和加载期间,主服务器收到的写操作命令写入到 replication buffer 缓冲区里
- 第三阶段:主服务器发送新写操作命令给从服务器
- 加载完成主服务器发送的RDB后,回复一个确认消息
- 主服务器将 replication buffer 缓冲区里所记录的写操作命令发送给从服务器,从服务器执行来自主服务器 replication buffer 缓冲区里发来的命令,这时主从服务器的数据就一致了
- 第一阶段:建立链接、协商同步(确定服务器的主从关系)
- 命令传播
- 主从服务器在完成第一次同步后,双方之间就会维护一个 TCP长连接。主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 连接。
- 长连接的好处:避免频繁的 TCP 连接和断开带来的性能开销
- 分摊主服务器的压力
- 瓶颈:如果大量的从服务器要和主服务器进行全量同步,会造成两个问题
- 阻塞主服务器CPU:由于是通过 bgsave 命令来生成 RDB 文件的,那么主服务器就会忙于使用 fork() 创建子进程,如果主服务器的内存数据非大,在执行 fork() 函数时是会阻塞主线程的,从而使得 Redis 无法正常处理请求
- 占用主服务器带宽:传输 RDB 文件会占用主服务器的网络带宽,会对主服务器响应命令请求产生影响。
- 解决方式
- 主服务器生成 RDB 和传输 RDB 的压力可以分摊到充当经理角色的从服务器。(树状结构?)
- 主服务器生成 RDB 和传输 RDB 的压力可以分摊到充当经理角色的从服务器。(树状结构?)
- 瓶颈:如果大量的从服务器要和主服务器进行全量同步,会造成两个问题
- 增量复制
- 从 Redis 2.8 开始,网络断开又恢复后,从主从服务器会采用增量复制的方式继续同步,也就是只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。
- 如何知道增量复制的起始点
- repl_backlog_buffer,是一个「环形」缓冲区,用于主从服务器断连后,从中找到差异的数据;
- replication offset,标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用 master_repl_offset 来记录自己「写」到的位置,从服务器使用 slave_repl_offset 来记录自己「读」到的位置。
- 网络断开后,当从服务器重新连上主服务器时,从服务器会通过 psync 命令将自己的复制偏移量 slave_repl_offset 发送给主服务器,主服务器根据自己的 master_repl_offset 和 slave_repl_offset 之间的差距,然后来决定对从服务器执行哪种同步操作:repl_backlog_buffer 缓行缓冲区的默认大小是 1M,并且由于它是一个环形缓冲区,所以当缓冲区写满后,主服务器继续写入的话,就会覆盖之前的数据。因此,当主服务器的写入速度远超于从服务器的读取速度,缓冲区的数据一下就会被覆盖。那么在网络恢复时,如果从服务器想读的数据已经被覆盖了,主服务器就会采用全量同步,这个方式比增量同步的性能损耗要大很多。
- 为了避免在网络恢复时,主服务器频繁地使用全量同步的方式,我们应该调整下 repl_backlog_buffer 缓冲区大小,尽可能的大一些
- 过期的key如何处理
- 主节点:通过过期删除策略进行淘汰
- 从节点:执行主节点模拟和发送来的del指令
- 怎么判断 Redis 某个节点是否正常工作?
- 原理:通过互相的 ping-pong 心态检测机制,如果有一半以上的节点去 ping 一个节点的时候没有 pong 回应,集群就会认为这个节点挂掉了,会断开与这个节点的连接。
- 主从节点发送的心态间隔
- Redis 主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态,可通过参数repl-ping-slave-period控制发送频率。
- Redis 从节点每隔 1 秒发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量,目的是为实时监测主从节点网络状态;上报自身复制偏移量, 检查复制数据是否丢失, 如果从节点数据丢失, 再从主节点的复制缓冲区中拉取丢失数据。
- Redis 是同步复制还是异步复制?
- Redis 主节点每次收到写命令之后,先写到内部的缓冲区,然后异步发送给从节点。
- 主从复制中两个 Buffer(replication buffer 、repl backlog buffer)有什么区别?
- 出现的阶段不一样:
- repl backlog buffer用于增量复制阶段,一个主节点只有一个repl backlog buffer;
- replication buffer 在全量和增量中都会使用,主节点会给每个新连接的从节点,分配一个 replication buffer;
- 这两个 Buffer 都有大小限制的,当缓冲区满了之后,发生的事情不一样:
- 当 repl backlog buffer 满了,因为是环形结构,会直接覆盖起始位置数据;
- 当 replication buffer 满了,会导致连接断开,删除缓存,从节点重新连接,重新开始全量复制。
- 出现的阶段不一样:
- 如何应对主从数据不一致?
- 原因:主从节点间的命令复制是异步进行的,无法实现强一致性(时时刻刻保持一致)
- 具体:在主从节点命令传播阶段,主节点收到新的写命令后,会发送给从节点。但是,主节点并不会等到从节点实际执行完命令后,再把结果返回给客户端,而是主节点自己在本地执行完命令后,就会向客户端返回结果了。如果从节点还没有执行主节点同步过来的命令,主从节点间的数据就不一致了
- 解决:通过监控程序,先用 INFO replication 命令查到主、从节点的进度。如果某个从节点的进度差值大于我们预设的阈值,我们可以让客户端不再和这个从节点连接进行数据读取,但是阈值过小可能导致所有从节点不能连接的情况
- 主从切换的问题和解决方式
- 异步复制同步丢失
- 原因:对于 Redis 主节点与从节点之间的数据复制,是异步复制的,当客户端发送写请求给主节点的时候,客户端会返回 ok,接着主节点将写请求异步同步给各个从节点,但是如果此时主节点还没来得及同步给从节点时发生了断电,那么主节点内存中的数据会丢失
- 解决:配置min-slaves-max-lag,将 master 和 slave 数据差控制在10s内,即使 master 宕机也只是这未复制的 10s 数据。
- 集群产生脑裂数据丢失
- 原因:由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了
- 异步复制同步丢失
哨兵机制Sentinel
- 概述:
- 作用
- 监控:监测主节点是否发生故障
- 选主:选举一个从节点切换为主节点
- 通知:将新主节点的相关信息通知给从节点和客户端
- 运行:哨兵进程可以运行在Redis集群中的任何节点,尽量分布在多个服务器,避免单点故障
- 作用
- 如何判断一个结点是否故障
- 心跳检测:哨兵会每隔 1 秒给所有主从节点发送 PING 命令,当主从节点收到 PING 命令后,会发送一个响应命令给哨兵,这样就可以判断它们是否在正常运行。
- 主观下线:若主从结点
没有在规定的时间响应哨兵的PING命令
(由down-after-milliseconds 参数配置),哨兵就会将其标记为「主观下线」 - 客观下线:当一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。当这个哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」。
- 主客观判断下线的目的:避免主节点因为网络拥塞造成的误判
- 候选者:哨兵判断完主节点客观下线后,哨兵就要开始在多个「从节点」中,选出一个从节点来做新主节点
- 若主结点下线后,如何选出新的主节点?
- 选Leader:判断主节点为客观下线的哨兵向其他哨兵发送选举请求(回应超时的视为无效哨兵),根据其他哨兵结点的回应选取票数最高的哨兵结点成为Leader,向其他哨兵广播并负责协调故障转移过程
- 选主节点:Leader 发送命令让所有从节点对候选主节点进行投票,选择票数最高的从节点作为新的主节点
- 主从切换:Leader发送
SLAVEOF no one
命令告知被选从结点切换为主结点,Leader再向所有从节点发送 SLAVEOF ,让它们成为新主节点的从节点。 - 后续处理:将新主节点的 IP 地址和信息,通过「发布者/订阅者机制」通知给客户端,继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点。
- 「候选者」成为Leader的条件
- 条件:
获得半数以上的赞成票 && 赞成票≥配置文件的quorum值
- 作用:在一个有 n 个节点的主从集群中,quorum 值通常设置为 (n/2)+1。这是因为只有在超过半数的节点达成一致的情况下,才能确保系统的稳定性和可用性。如果 quorum 值设置得太低,可能会导致系统发生分裂,造成数据不一致或数据丢失的问题。
- 条件:
- 如何组成哨兵集群
- 哨兵节点之间是通过 Redis 的发布者/订阅者机制来相互发现的。
- 在主从集群中,主节点上有一个名为__sentinel__:hello的频道,不同哨兵就是通过它来相互发现连接,实现互相通信的。
- 哨兵集群如何知道从节点信息:主节点知道所有「从节点」的信息,所以哨兵会每 10 秒一次的频率向主节点发送 INFO 命令来获取所有「从节点」的信息。
缓存篇
- 原因
- 概述
缓存雪崩
- 概述
- 前提
使用Redis作为缓存层
:减少磁盘数据库的访问,提高访问速度。设置数据过期时间
保证缓存中与数据库中的数据一致性。请求调入
:当用户访问的数据不在缓存或者过期时,系统会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。
- 问题:当
大量缓存数据同时失效
或者Redis 故障宕机
时,如果此时有大量的用户请求会直接访问数据库,从而导致数据库的压力骤增,严重的会造成整个系统崩溃,这就是缓存雪崩的问题
- 前提
- 大量数据同时过期的解决
过期时间随机化
:同时设置的缓存数据过期时间都加上一个随机数构建缓存互斥锁
:当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),但最好给互斥锁设置超时时间,避免阻塞时间太长造成饥饿后台更新缓存
:不设置缓存数据有效时间,由后台进行定时更新缓存
- Redis 故障宕机问题的解决
请求限流机制
- 启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。
服务熔断
- 暂停业务应用对缓存服务的访问,直接返回错误,不用再继续访问数据库,从而降低对数据库的访问压力,保证数据库系统的正常运行,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。
构建 Redis 缓存高可靠集群
- 主从节点的方式构建 Redis 缓存高可靠集群
缓存击穿
- 原因:如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮
- 解决方式
互斥锁方案
:保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。异步更新
:不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
缓存穿透
- 原理:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题
- 原因
- 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
- 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
- 解决方式
限制非法请求
:在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。缓存空值或者默认值
:线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,避免大量请求查询数据库布隆过滤器
:快速判断数据是否存在,避免通过查询数据库来判断数据是否存在。
布隆过滤器
- 组成
- Boolean:初始值都为 0 的位图
- 过滤:N 个哈希函数
- 作用:当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
- 标记过程
- 第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值;
- 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。
- 第三步,将每个哈希值在位图数组的对应位置的值设置为 1;
- 查询过程
- 当应用要查询数据 x 是否数据库时,通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1,只要有一个为 0,就认为数据 x 不在数据库中。
- 布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时存在哈希冲突的可能性,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。
- 结果:布隆过滤器查询数据存在不一定是真存在,但是不在是真不在
缓存和数据库的一致性
- 无论缓存和数据库的更新先后(都可能出现并发导致的数据不一致问题)
- Cache Aside 策略
- 写策略:更新数据库中的数据,并删除缓存中的数据。
- 读策略:先读取缓存,若没命中,再读取数据库中的数据并回写到缓存中
- 问题:先删除缓存,再更新数据库,在「读 + 写」并发的时候,还是会出现缓存和数据库的数据不一致的问题
- 解决方式:先更新数据库,再删除缓存。因为缓存的写入通常要远远快于数据库的写入,所以可以保证弱一致性
- 强一致性
- 在更新缓存前先加个
分布式锁
,保证同一时间只运行一个请求更新缓存,就会不会产生并发问题了,当然引入了锁后,对于写入的性能就会带来影响。 - 在更新完缓存时,给缓存加上较短的
过期时间
,这样即使出现缓存不一致的情况,缓存的数据也会很快过期,对业务还是能接受的。
- 在更新缓存前先加个
- 如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存
应用场景篇
消息队列
- 作用:消息队列是基础架构中常见的一个中间件,常应用于
- 系统解耦:使用消息队列可以实现不同系统的异步通信,每个系统只需要关注自己负责的部分即可。
- 异步处理:通过队列进行消息的缓冲,实现双方的异步处理,从而提高系统的吞吐量和相应速度
- 削峰填谷:通过限制队列长度或调整消费者数量使用消息队列可以缓解并发系统压力
- 业务拆分:每个消费者只关注自己订阅的消息,实现同一消息队列上复杂业务的拆分
- 异地灾备:使用消息队列可以在不同的地理位置上备份数据,从而实现系统的异地灾备。
- 实现方式
- Kafka:具有高吞吐量和分区特性,适合大数据量场景
- RabbitMQ:具有更高的可靠性和灵活性,适合业务需求变化频繁的场景
- 三个需求
- 生产者-消费者模型
- 处理重复的消息
- 保证消息可靠性
生产者消费者模型
少年,我观你骨骼清奇,颖悟绝伦,必成人中龙凤。 不如点赞·收藏·关注一波
参考博客
- 拓跋阿秀------逆袭进大厂
- 小林coding
- 快速链表quicklist
- 《深入理解计算机系统》
- 侯捷C++全系列视频
- 待定引用
- 待定引用
- 待定引用