Redis 原理详解(详解在注释里面,图片加载需要时间)
2023-07-04 08:38:48 0 举报
AI智能生成
Redis知识要点和原理解析,详细信息和图片均在黄色图标的注释中,鼠标移动到黄色图标上即会显示,图片加载有时较慢。
作者其他创作
大纲/内容
redisObject
两层结构
使用者的角度
暴露给外部的调用接口:string,list,hash,set,sorted set
内部实现的角度
底层的实现:dict,sds,ziplist,quicklist,skiplist,intset
字段
type
OBJ_STRING
OBJ_LIST
OBJ_SET
OBJ_ZSET
OBJ_HASH
encoding
lru
refcount
ptr
作用
redisObjec是联结两个层面的数据结构的桥梁
为多种数据类型提供一种统一的表示方式
允许同一类型的数据采用不同的内部表示,从而在某些情况下尽量节省内存
支持对象共享和引用计数。当对象被共享的时候,只占用一份内存拷贝,进一步节省内存
内部数据结构
简单动态字符串SDS
C++实现及缺点
SDS结构优势
SDS内部结构
结构
len:数组已使用长度
alloc:数组分配空间总长度
alloc - len:数组未使用空间长度
flags
5 种类型
sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,区别在于数组的 len 长度和分配空间长度 alloc
目的:节省内存
字符串对象的内部编码(encoding)类型
int:用于整数类型
embstr:用于短字符串
raw:保存长度超过 44 的字符串
目的
为了针对不同大小的字符串,使用不同的 SDS 类型保存,从而节省内存占用
应用场景
String底层实现之一
整数集合 intset
定义
字段
encoding
length
contents
可用保存的数据类型
int16_t
int32_t
int64_t
特性
整数集合中的元素有序
方便使用二分法进行查找
集合升级
1、分配空间
2、调整位置和类型
3、添加元素
升级后不会降级
保证集合中不会出现重复元素
复杂度
查找是 O(log n)
插入和删除都是 O(n)
存储元素相对较少的时候,O(log n) 和 O(n) 差距不是很大
优点
灵活
节省内存空间
应用场景
set 的底层实现之一
压缩列表 ziplist
ziplist 的结构
entry 节点
prevlen
encoding
content
字符串(字节数组)或者整数
为什么要这样设计呢
连锁更新
当添加或删除节点时,可能就会因为previous_entry_length的变化导致发生连锁的更新操作
常见操作
获取
先从 ziplist 中拿到 field 的指针,然后向后一个节点就是 value
删除
删除其实就是先查找,后删除
插入 / 更新
每次更新都要重新申请空间
特征
结构紧凑,访问效率高
一整块连续内存,没有多余的内存碎片,更新会导致内存 realloc 与内存复制,平均时间复杂度为 O(N)
逆向遍历
从表尾开始向表头进行遍历(注意了!,但是插入还是在表头)
连锁更新
压缩列表是一种为节约内存而开发的顺序型数据结构,是 ZSET、HASH 和 LIST 的底层实现之一
ziplist 是一个双向链表,可以在时间复杂度为 O(1) 从头部、尾部进行 pop 或 push。
应用场景
ZSET、HASH 和 LIST 的底层实现之一
快速列表 quicklist
quicklist
quicklistNode
没压缩:ziplist
压缩:quicklistLZF
布局结构
特征
双向链表的复合结构体
底层实现是quicklist
需要合理的配置节点中 ziplist 的 entry 个数
元素可以重复(即在List实现中,每个节点的zipList中的元素也可以重复),按插入顺序排序
应用场景
List底层实现
跳跃表 skiplist
为什么使用跳跃表(类比红黑树/ 平衡树)
性能考虑
实现考虑
特征
有序
链表
创建过程
skiplist 与平衡树、哈希表的比较
应用场景
ZSet底层实现
字典 dict
Redis 的字典用哈希表(dictht)的方式实现
哈希节点dictEntry
哈希算法
哈希冲突
拉链法
rehash
渐进式 rehash
五大类型
String
介绍
内部实现
int
SDS
应用场景
缓存对象
常规计数
分布式锁
List
列表对象有 3 种编码
ziplist
quicklist
linkedlist
3.2 版本及后续版本将不再使用
配置
fill (控制 ziplist 大小)
compress (控制压缩程度)
特征
双向链表的内存开销很大,每个节点的地址不连续,容易产生内存碎片
quicklist 利用 ziplist减少节点数量
但 ziplist 插入和删除数麻烦,复杂度高
为避免长度较长的 ziplist修改时带来的内存拷贝开销,通过配置项配置合理的 ziplist长度
应用场景
消息队列
必须要满足三个需求
消息保序
使用 LPUSH + RPOP
问题及解决
性能消耗
解决方案
BRPOP
处理重复的消息
生产者自行实现全局唯一 ID
保证消息可靠性
使用 BRPOPLPUSH
Hash
哈希对象的编码有两种
ziplist
hashtable
编码转换
应用场景
缓存对象
Set
集合对象的编码有两种
intset
hashtable
特征
不要求排序(intset本身是有序的)
不重复
应用场景
点赞
共同关注
抽奖活动
ZSet
有序集合有两种编码方式
压缩列表 ziplist
跳表 skiplist
图示
编码转换
应用场景
排行榜
电话、姓名排序
内存模型
内存统计
info memory
Redis内存划分
进程本身运行内存
对象内存
缓冲内存
内存碎片
内存回收
定期删除+惰性删除
定时删除
惰性删除
内存淘汰机制
noeviction(默认)
allkeys-random
allkeys-lru
allkeys-lfu
volatile-random
volatile-lru
volatile-lfu
volatile-ttl
lru - 最近最少使用
传统lru
问题
维护前后指针及哈希表,空间消耗大
redis lur - 近似lru
增加24bit长度时间属性
随机取5个,淘汰最晚时间的那个
lru存在问题
lfu - 最近最不经常使用
目的:淘汰内存中不经常使用的数据
字段
time
counter
增长
衰减
工作过程
主从同步
分类
1、从节点与主节点建立连接时进行全量同步
通过RDB + replication_buffer
异常情况
2、主节点与从节点正常运行时的同步
3、主节点与从节点连接断开后又重连时会进行增量同步或全量同步
增量同步
实现方式
环形数组repl-backlog-buffer
master-repl-offset
slave-repl-offset
特别注意
repl_backlog_buffer:复制积压缓冲区
默认 1MB
心跳
注意问题
数据延迟
读到过期数据
规避全量复制
规避复制风暴
单一主节点
单一机器
总结
持久化
RDB
触发过程
手动
save 命令
阻塞 Redis 服务,直到整个 RDB 持久化完成,不建议
bgsave 命令
该模式下的 RDB 持久化由子进程完成,非阻塞
自动
save m n
RDB 数据恢复
优缺点
优点
由于 RDB 文件是一个非常紧凑的二进制文件,所以加载的速度快于 AOF 方式
fork 子进程方式,不会阻塞
RDB 文件代表着 Redis 服务器的某一个时刻的全量数据,所以它非常适合做冷备份和全量复制的场景
缺点
没办法做到实时持久化,会存在丢数据的风险
AOF(默认关闭)
目的
解决数据持久化的实时性
执行流程
命令写入
文件同步
文件重写
手动触发
自动触发
优缺点
优点
相比于 RDB,AOF 更加安全,默认同步策略为 everysec 即每秒同步一次,所以顶多我们就失去一秒的数据
根据关注点不同,AOF 提供了不同的同步策略,我们可以根据自己的需求来选择
AOF 文件是以 append-only 方式写入,相比如 RDB 全量写入的方式,它没有任何磁盘寻址的开销,写入性能非常高
缺点
由于 AOF 日志文件是命令级别的,所以相比于 RDB 紧致的二进制文件而言它的加载速度会慢些
AOF 开启后,支持的写 QPS 会比 RDB 支持的写 QPS 低
RDB 和 AOF 混合模式
RDB-AOF 混合持久化
1、在 AOF 重写阶段创建一个同时包含 RDB 数据和 AOF 数据的 AOF 文件,其中 RDB 数据位于 AOF 文件的开头,RDB存储了服务器开始执行重写操作时 Redis 服务器的数据状态(RDB 快照方案)。
2、重写操作执行之后的 Redis 命令(即已存储当前RDB之后的命令),则会继续 append 在 AOF 文件末尾,即 RDB 数据之后(AOF 日志追加方案),一般这部分数据都会比较小
1. Redis 重启的时候,则可以先加载 RDB 的内容,然后再加载 AOF 的日志内容。
哨兵模式
功能描述
监控(Monitoring)
自动故障转移(Automatic failover)
配置提供者(Configuration provider)
通知(Notification)
组成
哨兵节点
数据节点
工作机制
心跳检查
每隔 10 秒,每个 Sentinel 节点会向已知的主从节点发送 info 命令获取最新的主从架构。
每隔 2 秒,Sentinel 节点都会向主从节点的 _sentinel_:hello 频道发送自己的信息。
每隔一秒,哨兵会每个主从节点、Sentinel 节点发送 PING 命令。该定时任务是哨兵心跳机制中的核心,它涉及到 Redis 数据节点的运行状态监控,哨兵领导者的选举等细节操作。当哨兵节点发送 PING 命令后,若超过 down-after-milliseconds 后,当前哨兵节点会认为该节点主观下线。
主观下线
客观下线
注意
Sentinel 选举
故障转移
哨兵机制下的数据丢失
异步复制同步丢失
集群产生脑裂数据丢失
如何保证尽量少的数据丢失
min-slaves-to-write:从节点最小数量,默认0
min-slaves-max-lag:从节点最大延迟,默认10
解释
集群Cluster模式
Cluster 实现原理
集群的组群过程
集群数据分片原理
哈希槽(slots)的划分
均匀分配
使用 cluster addslots 命令来指定
哈希槽(slots)的映射
哈希槽(slots)用来划分数据存储区域,即通过slots定位目标node,真正存储数据的还是redis node
数据复制过程和故障转移
数据复制
故障检测
主从故障转移
1、如果只有一个slave节点
2、多个slave节点
3、新的主节点会撤销所有对已下线主节点的slots指派,并将这些slots全部指派给自己
4、新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
5、新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成
client 访问 数据集群的过程
定位数据所在节点
1、客户端连接任一实例,获取到slots与实例节点的映射关系,并将该映射关系的信息缓存在本地
2、将需要访问的redis信息的key,经过CRC16计算后,再对16384 取模得到对应的 Slot 索引
3、通过slot的位置进一步定位到具体所在的实例,再将请求发送到对应的实例上
Redis Cluster集群扩缩容
扩容
扩容原理
迁移slot和数据
slot迁移的其他说明
缩容
缩容原理
忘记节点
主从、哨兵、集群优缺点
单机模式
优点
架构简单,部署方便
高性价比
高性能
缺点
不保证数据的可靠性
高性能受限于单核 CPU 的处理能力
主从模式
优点
读写分离,提高效率
数据热备份,提供多个副本
缺点
无法自动故障转移
Master的写的压力难以降低
主节点存储能力受到单机限制
主从数据同步,可能产生部分的性能影响甚至同步风暴。
哨兵模式
优点
对节点进行监控,来完成自动的故障发现与转移
缺点
特别是在主从切换的瞬间存在访问瞬断的情况,等待时间比较长,至少十来秒不可用
哨兵模式只有一个主节点对外提供服务,没法支持很高的并发
单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率。
与主从相比,哨兵仅解决了手动切换主从节点问题,至于其他的问题,基本上仍然存在
集群模式
优点
无中心架构
数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
可扩展性:可线性扩展到 1000 多个节点,节点可动态添加或删除
高可用性
缺点
如果主节点A和它的从节点A1都宕机了,那么该集群就无法再提供服务了
发布订阅
组成
频道(channel)
发送者(publisher)
订阅者(subscriber)
实现场景
两种发布/订阅模式
基于频道(Channel)的发布/订阅
SUBSCRIBE:频道订阅
UNSUBSCRIBE:退订频道
PUBLISH:向频道发送消息
基于模式(pattern)的发布/订阅
图解
通配
PSUBSCRIBE:模式订阅
PUBLISH:模式发布
PUNSUBSCRIBE:退订模式
PUBSUB NUMSUB:查看频道的订阅者数量
注意事项
客户端需要及时消费和处理消息
客户端需要支持重连
不建议用于消息可靠性要求高的场景中
布隆过滤器
特性
Bloom命令
底层原理
最佳hash函数数量与错误率的关系
所需存储空间与错误率及容量关系
扩容
扩容逻辑
优缺点
优点
适合大数据场景
节省空间
数据保密
缺点
误判
不可删除
空间利用率不高
容量满时误报率增加
不支持计数
查询速度受错误率影响
分布式锁
分布式锁需要考虑的问题
可重入
锁超时 - 防死锁
防误删
可续期
常见分布式锁方案对比
分布式锁需满足四个条件
Redis分布式锁实现
setnx和expire实现
问题
会出现死锁
锁过期、释放别人的锁
锁被别人释放怎么办?
锁过期时间不好评估怎么办? Redisson
解决方案
死锁:设置过期时间
过期时间评估不好,锁提前过期:守护线程,自动续期
锁被别人释放:锁写入唯一标识,释放锁先检查标识,再释放;
Redlock(红锁)
「主从发生切换」时,这个分布锁会依旧安全吗?
红锁实现
总结
答疑
为什么要在多个实例上加锁?
为什么大多数加锁成功,才算成功?
为什么步骤 3 加锁成功后,还要计算加锁的累计耗时?
为什么释放锁,要操作所有节点?
基于 Zookeeper 的锁安全吗?
Redisson分布式锁
加锁机制
加锁流程
加锁 Lua
其中lock()默认是30秒的生存时间
锁互斥
watch dog:看门狗自动延期机制
官方介绍
lockWatchdogTimeout(监控锁的看门狗超时,单位:毫秒),默认值:30000
看门狗的时间可以自定义设置
释放锁机制
解锁流程图
广播解锁消息有什么用?
缺点
主从节点下异步同步数据导致锁丢失问题
解决方案:redLock
如何使用
特别注意
线程模型及框架原理
Redis启动流程
阶段
阶段一:基本初始化
阶段二:检查哨兵模式,并检查是否要执行 RDB 检测或 AOF 检测
阶段三:运行参数解析
阶段四:初始化 server
阶段五:执行事件驱动框架
Reactor 模型
两个“三”
三类事件类型
连接事件
写事件
读事件
三个关键角色
reactor
acceptor
handler
事件类型与关键角色 交互关系
客户端和服务器端在交互过程中,不同类请求和 Reactor 模型事件的对应关系
事件驱动框架
事件驱动框架包括了两部分
1、事件初始化
2、事件捕获、分发和处理主循环
事件驱动框架的基本执行过程
Reactor 模型的基本工作机制
Redis 对 Reactor 模型的实现
事件的数据结构定义
以 aeFileEvent 为例
负责事件和 handler 注册的 aeCreateFileEvent 函数
initServer过程中调用aeCreateFileEvent
aeCreateFileEvent 如何实现事件和处理函数的注册?
主循环aeMain函数
负责事件捕获与分发的 aeProcessEvents 函数
情况一:既没有时间事件,也没有网络事件
情况三:只有普通的时间事件
情况二:有 IO 事件或者有需要紧急处理的时间事件
aeApiPoll 是如何捕获事件呢?
流程图
总结
首先,需要先实现一个主循环函数(对应 aeMain),负责一直运行框架。
其次,需要编写事件注册函数(对应 aeCreateFileEvent),用来注册监听的事件和事件对应的处理函数。只有对事件和处理函数进行了注册,才能在事件发生时调用相应的函数进行处理。
最后,需要编写事件监听、分发函数(对应 aeProcessEvents),负责调用操作系统底层函数来捕获网络连接、读、写事件,并分发给不同处理函数进一步处理。
Redis中的事件驱动框架
主循环体结构:aeEventLoop
事件类型
IO 事件
时间事件
aeFiredEvent - 并非专门的事件类型
源码
aeEventLoop结构 初始化
初始化时间
Redis server启动时initServer()函数中执行
aeCreateEventLoop 函数调用
函数执行步骤
第一步
1、aeCreateEventLoop()会创建一个 aeEventLoop 结构体类型的变量 eventLoop
2、给 eventLoop 的成员变量分配内存空间
3、给 eventLoop 的成员变量赋初始值
第二步
1、aeCreateEventLoop 函数会调用 aeApiCreate 函数
aeApiCreate 函数就会调用 epoll_create 创建 epoll 实例,同时会创建 epoll_event 结构的数组,数组大小等于参数 setsize
通过aeApiState持有epoll对象
第三步
把所有网络 IO 事件对应文件描述符的掩码,初始化为 AE_NONE
两个关键点
IO 事件数组大小就等于setsize,决定了和 Redis server 连接的客户端数量
通过 aeApiCreate 函数创建 epoll_event 结构数组
一个亮点
将fd作为aeFileEvent数组的下标
eventLoop中的aeFileEvent数组持有全部已连接IO时间的fd信息
IO 事件处理
IO 事件分类
可读事件
可写事件
屏障事件
四个组成部分
套接字
I/O多路复用程序
文件事件分派器
事件处理器
aeFileEvent 结构体
IO 事件创建:aeCreateFileEvent
其中fd参数
流程
1、获取该描述符关联的 IO 事件
2、调用 aeApiAddEvent 函数,添加要监听的事件
首先,获取aeApiState变量state
其次,对于要执行的操作类型的设置
添加操作:EPOLL_CTL_ADD
修改操作:EPOLL_CTL_MOD
最后,创建 epoll_event 类型变量 ee
调用 epoll_ctl 创建监听事件
读写事件创建并注册回调函数
最开始创建的监听事件
1、为服务端server.port端口添加连接事件监听
目的:接受客户端连接
2、注册acceptTcpHandler函数
客户端连接server时,触发acceptCommonHandler函数
createClient函数调用aeCreateFileEvent,对已连接套接字上(客户端socket fd),创建监听事件,类型为 AE_READABLE
注册回调函数是readQueryFromClient
读事件处理
流程
写事件处理
主循环aeMain每次调用 aeProcessEvents 函数前,都会调用 beforeSleep 函数
beforeSleep 函数调用的 handleClientsWithPendingWrites 函数
aeMain 函数还会循环调用 aeProcessEvents 函数,来检测已触发的事件(检测fired列表)
aeProcessEvents伪代码(在aeMain函数中循环调用)
aeApiPoll函数
源码
processFileEvent函数
源码
IO事件运行图
IO 事件和相应套接字、回调函数的对应关系
注意
无论客户端发送的请求是读或写操作,对于 server 来说,都是要读取客户端的请求并解析处理。所以,server 在客户端的已连接套接字上注册的是可读事件。
当实例需要向客户端写回数据时,实例会在事件驱动框架中注册可写事件,并使用 sendReplyToClient 作为回调函数,将缓冲区中数据写回客户端。
Redis多线程
多线程演变
Redis 6.0 之前单线程指的是 Redis 只有一个线程干活么?
单线程事件循环
Redis 3.0前多线程
Redis 4.0多线程
Redis 6.0多线程
一、多线程启动流程
默认关闭多线程
main 入口函数
1、主线程初始化
aeCreateEventLoop 处理
绑定监听服务端口
注册事件回调函数
调用 aeApiAddEvent
2、io 线程启动
源码 InitServerLast->initThreadedIO -> IOThreadMain
IOThreadMain流程图
二、主线程事件循环
aeProcessEvents源码
事件循环处理1:新连接到达
acceptTcpHandler源码
事件循环处理2:用户命令请求到达
readQueryFromClient 代码
推迟客户端的读写操作
四个条件
条件一:全局变量 server 的 io_threads_active 值为 1
条件二:全局变量 server 的 io_threads_do_read 值为 1
条件三:ProcessingEventsWhileBlocked 变量值为 0
条件四:客户端现有标识不能有 CLIENT_MASTER、CLIENT_SLAVE 和 CLIENT_PENDING_READ。
事件循环处理3:epoll_wait 前进行任务处理
beforeSleep
三、主线程 && io 线程处理读请求
主线程分配任务
handleClientsWithPendingReadsUsingThreads源码
主要逻辑分成四步
1、判断是否激活IO多线程、读多线程
2、遍历clients_pending_read列表,并逐个分配到IO线程及主线程
通知IO线程执行
IO子线程处理流程
3、主线程执行io_threads_list 数组 0 号列表元素
读请求处理:readQueryFromClient->processInputBuffer
processCommandAndResetClient(只有主线程能执行)
getCommand(只有主线程能执行)
addReplyBulk(只有主线程能执行)
主线程等待所有 IO 线程完成待读客户端的处理
4、主线程调用 processCommandAndResetClient 函数执行命
执行过程
写处理结果到发送缓存区
prepareClientToWrite
_addReplyToBuffer
四、主线程 && io 线程配合处理写请求
主线程分配任务
IOThreadMain
写请求处理-writeToClient
不开启多线程写操作的情况
总结
工作过程
总结来说
缺陷
主线程是在处理读、写任务队列的时候还要等待其它的 io 线程处理完才能进入下一步
对多核的利用率并不算高
多线程的无锁设计
redis快?
四个主要原因
C 语言实现
纯内存 I/O
I/O 多路复用
单线程模型
为何选择单线程
避免过多的上下文切换开销
避免同步机制的开销
简单可维护
redis调优
slowlog:慢查询日志
慢查询命令
redis-benchmark
RDB 文件分析
Redis性能问题排查
耗时的操作
如何检测Redis是否变慢?
过期key操作
大量数据插入
大 Key 问题
分析大key
拆分大key
将大 Key 进行分割,拆成几个小的 key-value,使用 multiGet 获取值
内存碎片
查看内存碎片
处理内存碎片
NVM
Redis配置
内存
过期时间的设置
业务层面
0 条评论
下一页