Redis 原理
学习资料
黑马官方资料:G:\开发\Redis-笔记资料
Redis 数据结构
底层数据结构
SDS 动态字符串
我们都知道Redis中保存的Key是字符串,value往往是字符串或者字符串的集合。可见字符串是Redis中最常用的一种数据结构。 不过Redis没有直接使用C语言中的字符串,因为C语言字符串存在很多问题:
- 获取字符串长度的需要通过运算
- 非二进制安全
- 不可修改 Redis构建了一种新的字符串结构,称为简单动态字符串(Simple Dynamic String),简称SDS。
源码案例
例如,我们执行命令:
那么Redis将在底层创建两个SDS,其中一个是包含“name”的SDS,另一个是包含“虎哥”的SDS。 Redis是C语言实现的,其中SDS是一个结构体,源码如下:
例如,一个包含字符串“name”的sds结构如下: 
动态扩容
SDS之所以叫做动态字符串,是因为它具备动态扩容的能力,例如一个内容为“hi”的SDS:
假如我们要给SDS追加一段字符串“,Amy”,这里首先会申请新内存空间:
- 如果新字符串小于1M,则新空间为扩展后字符串长度的两倍+1;
- 如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1。称为内存预分配。
注意:alloc 不包含最后一个 \0,所以是 12。实际上申请的总内存是 13
优点总结

IntSet
IntSet是Redis中set集合的一种实现方式,基于整数数组来实现,并且具备长度可变、有序等特征。
结构
结构如下: 
其中的encoding包含三种模式,表示存储的整数大小不同: 
为了方便查找,Redis会将intset中所有的整数按照升序依次保存在contents数组中,结构如图: 
现在,数组中每个数字都在int16_t的范围内,因此采用的编码方式是INTSET_ENC_INT16,每部分占用的字节大小为:
- encoding:4字节
- length:4字节
- contents:2字节 * 3 = 6字节

添加数字
我们向该其中添加一个数字:50000,这个数字超出了int16_t的范围,intset会自动升级编码方式到合适的大小。 以当前案例来说流程如下:
- 升级编码为INTSET_ENC_INT32, 每个整数占4字节,并按照新的编码方式及元素个数扩容数组
- 倒序依次将数组中的元素拷贝到扩容后的正确位置
- 将待添加的元素放入数组末尾
- 最后,将inset的encoding属性改为INTSET_ENC_INT32,将length属性改为4
源码


总结
Intset可以看做是特殊的整数数组,具备一些特点:
- Redis会确保Intset中的元素唯一、有序
- 具备类型升级机制,可以节省内存空间
- 底层采用二分查找方式来查询
Dict
我们知道Redis是一个键值型(Key-Value Pair)的数据库,我们可以根据键实现快速的增删改查。而键与值的映射关系正是通过Dict来实现的。
结构
Dict由三部分组成,分别是:哈希表(DictHashTable)、哈希节点(DictEntry)、字典(Dict)

当我们向Dict添加键值对时,Redis首先根据key计算出hash值(h),然后利用 h & sizemask来计算元素应该存储到数组中的哪个索引位置。我们存储k1=v1,假设k1的哈希值h =1,则1&3 =1,因此k1=v1要存储到数组角标1位置。 
Dict 的 rehash

1)
2)
3) 
总结
Dict的结构:
- 类似java的HashTable,底层是数组加链表来解决哈希冲突
- Dict包含两个哈希表,ht[0]平常用,ht[1]用来rehash
Dict的伸缩:
- 当LoadFactor大于5或者LoadFactor大于1并且没有子进程任务时,Dict扩容
- 当LoadFactor小于0.1时,Dict收缩
- 扩容大小为第一个大于等于used + 1的2^n
- 收缩大小为第一个大于等于used 的2^n
- Dict采用渐进式rehash,每次访问Dict时执行一次rehash
- rehash时ht[0]只减不增,新增操作只在ht[1]执行,其它操作在两个哈希表
ZipList
ZipList 是一种特殊的“双端链表” (但不是由指针指向前后节点),由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/弹出操作, 并且该操作的时间复杂度为 O(1) 

ZipListEntry

Encoding 编码
字符串:

整数:

连锁更新问题
ZipList的每个Entry都包含previous_entry_length来记录上一个节点的大小,长度是1个或5个字节:
- 如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
- 如果前一节点的长度大于等于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据 现在,假设我们有N个连续的、长度为250~253字节之间的entry,因此entry的previous_entry_length属性用1个字节即可表示,如图所示:

ZipList这种特殊情况下产生的连续多次空间扩展操作称之为连锁更新(Cascade Update)。新增、删除都可能导致连锁更新的发生。
总结
ZipList特性:
- 压缩列表的可以看做一种连续内存空间的"双向链表"
- 列表的节点之间不是通过指针连接,而是记录上一节点和本节点长度来寻址,内存占用较低
- 如果列表数据过多,导致链表过长,可能影响查询性能
- 增或删较大数据时有可能发生连续更新问题
QuickList

控制 ZipList entry 数量

ZipList 压缩

源码
以下是QuickList的和QuickListNode的结构源码: 
我们接下来用一段流程图来描述当前的这个结构: 
总结
QuickList的特点:
- 是一个节点为ZipList的双端链表
- 节点采用ZipList,解决了传统链表的内存占用问题
- 控制了ZipList大小,解决连续内存空间申请效率问题
- 中间节点可以压缩,进一步节省了内存
SkipList

源码


总结
SkipList的特点:
- 跳跃表是一个双向链表,每个节点都包含score和ele值
- 节点按照score值排序,score值一样则按照ele字典排序
- 每个节点都可以包含多层指针,层数是1到32之间的随机数
- 不同层指针到下一个节点的跨度不同,层级越高,跨度越大
- 增删改查效率与红黑树基本一致,实现却更简单
RedisObject
在 Redis 中,每个键值对的键都是一个由 SDS 存储的字符串,而它的值则会被封装成一个 RedisObject,也叫做Redis对象,源码如下: 
什么是 RedisObject ?
- 从Redis的使用者的角度来看: ⼀个Redis节点包含多个database(非cluster模式下默认是16个,cluster模式下只能是1个),而一个database维护了从key space到object space的映射关系。这个映射关系的key是string类型,⽽value可以是多种数据类型,比如: string, list, hash、set、sorted set等。我们可以看到,key的类型固定是string,而value可能的类型是多个。
- ⽽从Redis内部实现的⾓度来看: database内的这个映射关系是用⼀个dict来维护的。dict的key固定用⼀种数据结构来表达就够了,这就是动态字符串sds。而value则比较复杂,为了在同⼀个dict内能够存储不同类型的value,这就需要⼀个通⽤的数据结构,这个通用的数据结构就是robj,全名是redisObject。
编码方式

五种数据结构

数据类型
String


使用建议
使用 String 数据类型时,尽量存储数字或者是长度小于44的字符串,避免使用 RAW 编码
List
Redis的List类型可以从首、尾操作列表中的元素:
哪一个数据结构能满足上述特征?
- LinkedList :普通链表,可以从双端访问,内存占用较高,内存碎片较多
- ZipList :压缩列表,可以从双端访问,内存占用低,存储上限低
- QuickList:LinkedList + ZipList,可以从双端访问,内存占用较低,包含多个ZipList,存储上限高
源码


Set
Set是Redis中的单列集合,满足下列特点:
- 不保证有序性
- 保证元素唯一
- 求交集、并集、差集
可以看出,Set对查询元素的效率要求非常高,思考一下,什么样的数据结构可以满足? - HashTable,也就是Redis中的Dict,不过Dict是双列集合(可以存键、值对)
源码

结构图
encoding有两种编码方式:
- intset:ptr 指向上面那张表格
- HT:(dict)如下面的表格所示
ps:表格有误,dictht 的 used 应该为4
ZSet
编码

结构图

编码转换

zsetAdd函数:(考虑两种编码) 
ZipList 本身不支持 ZSet 要求的功能,怎么实现?

Hash
编码

编码转换

Redis 网络模型
用户空间和内核空间




Linux 五种不同的 IO 模型

阻塞 IO

- 性能不怎么好
非阻塞 IO

- 性能和阻塞 IO 一样差,CPU 使用率还会暴增
- 特定场合使用时反而会是个好选择
IO 多路复用
解决方案

FD

- 类比餐厅:一次性监听多个顾客,而不是排队点餐
三种实现方式

IO多路复用-select

select模式存在的问题:
- 需要将整个fd_set从用户空间拷贝到内核空间,select结束还要再次拷贝回用户空间
- select无法得知具体是哪个fd就绪,需要遍历整个fd_set
- fd_set监听的fd数量不能超过1024
IO多路复用-poll

- 除了改进 select 模式的问题3,其他没改进,性能也没什么提升。
- 链表随着数量的增加,因为链表遍历,性能会变差
- Linux 基本没怎么使用
IO多路复用-epoll
优势(相比较select解决的问题):
- fd只需要拷贝一次(执行一次ctl),就会一直添加到红黑树上,以后的循环过程中不需要再拷贝
- 每次只拷贝就绪的fd,减少拷贝的次数(每个fd只拷贝一次),减少拷贝数量(只拷贝就绪的fd)
- 红黑树理论上讲支持的数量无限,红黑树增删改查的性能基本不会因为数量改变而改变,性能好而且稳定
总结
select模式存在的三个问题:
- 能监听的FD最大不超过1024
- 每次select都需要把所有要监听的FD都拷贝到内核空间
- 每次都要遍历所有FD来判断就绪状态 poll模式的问题:
- poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降 epoll模式中如何解决这些问题的?
- 基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高
- 每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
- 利用ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降
事件通知机制
LT 模式缺点:
- 重复通知影响性能
- 惊群问题
web服务流程

信号驱动 IO

异步 IO

同步还是异步

Redis 网络模型
Redis到底是单线程还是多线程?
- 如果仅仅聊Redis的核心业务部分(命令处理),答案是单线程
- 如果是聊整个Redis,那么答案就是多线程
在Redis版本迭代过程中,在两个重要的时间节点上引入了多线程的支持:
- Redis v4.0:引入多线程异步处理一些耗时较旧的任务,例如异步删除命令unlink
- Redis v6.0:在核心网络模型中引入 多线程,进一步提高对于多核CPU的利用率
因此,对于Redis的核心网络模型,在Redis 6.0之前确实都是单线程。是利用epoll(Linux系统)这样的IO多路复用技术在事件循环中不断处理客户端情况。
为什么Redis要选择单线程?
- 抛开持久化不谈,Redis是纯 内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升。
- 多线程会导致过多的上下文切换,带来不必要的开销(单核情况下)
- 引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣
为什么单线程速度快?
- Redis是纯 内存操作,执行速度非常快(最主要)
- 其他:IO多路复用
不同的多路复用(用哪个取决于你的操作系统)

Redis 单线程网络模型的流程
Tips
源码解析见黑马视频&PPT 实在太干货了🤣 牢记第二幅图!!!可对面试官装逼


传统单线程模型下,真正影响性能的不是命令处理,也不是IO多路复用,而是IO:与数据库连接,与MySQL连接……
Redis 通信协议
RESP 协议

- RESP2和RESP3兼容性问题比较大
数据类型

Redis 内存策略

过期策略

DB 结构
Redis 是如何知道一个key是否过期呢?
利用两个Dict分别记录key-value对及key-ttl对


是不是TTL到期就立即删除了呢?
- 惰性删除
- 周期删除
惰性删除

周期删除


淘汰策略
内存淘汰

淘汰策略

- LRU:最近没用(用的时间远)
- LFU:用的频率低
结构

流程

注意:alloc 不包含最后一个 


可以看出,Set对查询元素的效率要求非常高,思考一下,什么样的数据结构可以满足?
Lilyana