学习【Redis原理篇】这一篇就够了

1. 数据结构

1-1. 动态字符串(SDS)

保存的Key是字符串,value往往是字符串或者字符串的集合。可见字符串是Redis中最常用的一种数据结构。

不过Redis没有直接使用C语言中的字符串,因为C语言字符串存在很多问题

在这里插入图片描述

  • C语言字符串底层是字符数组。获取字符串长度需要遍历字符数组个数,并减去结束标识:‘\0’。(时间复杂度O(n))
  • 非二进制安全。(如果字符串包含‘\0’字符,会被当作结束标识符)
  • 不可修改。(字面量的字符串保存在常量池中,无法修改。如果需要修改要申请更多的内存空间将原先的字符数组放进去)

基于以上的问题,Redis构建了一种新的字符串结构,称为简单动态字符串(Simple Dynamic String),简称SDS

在这里插入图片描述

Redis是C语言实现的,其中SDS是一个结构体,源码如下:

在这里插入图片描述

alloc:buf申请的内存,第一次申请的大小与len一样,之后都要比len大。

在这里插入图片描述
(name占4个字节,所以使用SDS_TYPE_8(255)即可)

在这里插入图片描述

相比于C语言字符串的优点:

  • 获取字符串长度的时间复杂度为O(1)
  • 支持动态扩容
  • 减少内存分配次数
  • 二进制安全

总结:

  • Redis 字符串的底层实现的是简单动态字符串(SDS)。它是C语言的一个结构体。它其中有4个属性:已保存的字节数已分配的字节数SDS的头类型字符数组
  • Redis为什么不使用原生C语言的字符串,而使用SDS呢?
  • C语言的字符串要获取字符串长度时,需要遍历整个字符数组,时间复杂度O(n)。SDS可以直接从结构体的len属性读取,时间复杂度O(1)
  • C语言的字符串非二进制安全,如果存储的字符是结束标识符,那么统计长度的时候会被结束。SDS是二进制安全的,直接读取len属性。
  • SDS支持动态扩容,如果字符数组不足时,会自动申请空间,而且会进行内存预分配。

1-2. Intset

IntSet是Redis中set集合的一种实现方式,基于整数数组来实现,并且具备长度可变有序(可以进行二分查找) 等特征。

结构如下:

在这里插入图片描述

其中的encoding包含三种模式,表示存储的整数大小不同:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Intset可以看做是特殊的整数数组,具备一些特点:

  • Redis会确保Intset中的元素唯一有序
  • 具备类型升级机制,可以节省内存空间
  • 底层采用二分查找方式来查询

总结:

  • IntSet是Redis中set集合的一种实现方式,基于C语言的整数数组来实现,并且具备长度可变有序唯一等特征。
  • 具备类型升级机制,可以节省内存空间
  • 不适合存储大量的数据,因为底层是数组,需要连续的内存空间。

1-3. Dict

我们知道Redis是一个键值型(Key-Value Pair)的数据库,我们可以根据键实现快速的增删改查。而键与值的映射关系正是通过Dict来实现的。

Dict由三部分组成,分别是:哈希表(DictHashTable)、哈希节点(DictEntry)、字典(Dict)

在这里插入图片描述

在这里插入图片描述

Dict的结构:

在这里插入图片描述

  • dictht 类似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]执行,其它操作在两个哈希表

1-4. ZipList

ZipList是一种类似于“双端链表”的结构,由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/弹出操作,并且该操作的时间复杂度为O(1)。

在这里插入图片描述
在这里插入图片描述

特性:

  1. 压缩列表的可以看做一种连续内存空间的"双向链表"
  2. 列表的节点之间不是通过指针连接,而是记录上一节点和本节点长度来寻址,内存占用较低
  3. 如果列表数据过多,导致链表过长,可能影响查询性能
  4. 增或删较大数据时有可能发生连续更新问题

总结:

  • ZipList:压缩列表,可以从双端访问,采用连续的内存空间,不需要指针。
  • 如果存储大量的数据,要申请一大片连续的内存空间,是非常困难的,所以通常存储少量的数据。

1-5. QuickList

在这里插入图片描述
QuickList的特点:

  • 是一个节点为ZipList的双端链表
  • 节点采用ZipList,解决了传统链表的内存占用问题
  • 控制了ZipList大小,解决连续内存空间申请效率问题
  • 中间节点可以压缩,进一步节省了内存

总结:

  • QuickList: LinkedList + ZipList,是一个双端链表,每个节点是一个ZipList,存储上限高。

1-6. SkipList

在这里插入图片描述

SkipList的特点:

  • 跳跃表是一个双向链表,每个节点都包含score和ele值
  • 节点按照score值排序,score值一样则按照ele字典排序
  • 每个节点都可以包含多层指针,层数是1到32之间的随机数
  • 不同层指针到下一个节点的跨度不同,层级越高,跨度越大
  • 增删改查效率与红黑树基本一致,实现却更简单

1-7. RedisObject

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1-7. 五种数据类型

String

String是Redis中最常见的数据存储类型:

  • 其基本编码方式是RAW,基于简单动态字符串(SDS)实现,存储上限为512mb
  • 如果存储的SDS长度小于等于44字节,则会采用EMBSTR编码,此时object head与SDS是一段连续空间。申请内存时只
    需要调用一次内存分配函数,效率更高。(小于等于44字节是为了 + object head字节大小刚好小于等于内存中一个页的大小,只需要进行一次内存分配。)
  • 如果存储的字符串是整数值,并且大小在LONG MAX范围内,则会采用INT编码:直接将数据保存在RedisObject的
    ptr指针位置(刚好8字节),不再需要SDS了。

在这里插入图片描述


总结:

  • Redis的String有3种编码方式:rawembstrint
  • raw:raw是基于SDS实现的,存储上限是512MB
  • embstr:如果SDS存储的字节大小小于等于44字节,则会采用embstr编码,此时头信息和SDS是一块连续的内存空间。SDS字节大小小于44,是为了加上头信息的大小刚好小于等于内存中一页的大小,只需要进行一次内存分配。
  • int:如果字符串存储的是整数值,并且是在8个字节以内,则会采用INT编码

List

Redis的List结构类似一个双端链表,可以从首、尾操作列表中的元素:

  • 3.2版本之前,Redis采用ZipListLinkedList来实现List,当元素数量小于512并且元素大小小于64字节时采用ZipList编码,超过则采用LinkedList编码。
  • 3.2版本之后,Redis统一采用QuickList来实现List

在这里插入图片描述

Set

Set是Redis中的集合,不一定确保元素有序,可以满足元素唯一查询效率要求极高

  • 为了查询效率和唯一性,set采用HT编码(Dict)。Dict中的key用来存储元素,value统一为null
  • 当存储的所有数据都是整数,并且元素数量不超过set-max-intset-entries时,Set会采用IntSet编码,以节省内存。

在这里插入图片描述

在这里插入图片描述

ZSet

ZSet底层数据结构必须满足键值存储键必须唯一可排序这几个需求。

  • ZSet实现方法一:HT(Dict)+ SkipList。HT保证键值存储、键必须唯一,SkipList保证可排序。缺点:Dict里面存储一份数据,SkipList中也需要存储一份数据。非常消耗内存。

kip

  • ZSet实现方法二:如果存储的数据量比较少时,采用 zipList。但zipList本身没有排序的功能,而且没有键值对概念,所以需要进行改进。
    • ZipList是连续内存,因此score和element是紧挨在一起的两个entry,element在前,score在后
    • score越小越接近队首,score越大越接近队尾,按照score值升序排列

在这里插入图片描述

Hash

在这里插入图片描述

  • Hash结构默认采用ZipList编码,用以节省内存。ZipList中相邻的两个entry分别保存field和value
  • 当数据量较大时,Hash结构会转为HT编码,也就是Dict,触发条件有两个:
    ①ZipList中的元素数量超过了hash-max-ziplist-entries(默认512)
    ②ZipList中的任意entry:大小超过了hash-max-ziplist-value(默认64字节)

ZipList编码
在这里插入图片描述
Ht编码

在这里插入图片描述

2. 网络模型

2-1. 用户空间和内核空间

为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分离的:

进程的寻址空间会划分为两部分:内核空间用户空间

用户空间只能执行受限的命令 (Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问

内核空间可以执行特权命令(Ring0),调用一切系统资源

在这里插入图片描述

Liux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:

写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备

读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区

例子:

  1. 下图的右边相当于我们的客户端,要发送请求到网卡,服务端的网卡接收到请求后,将响应数据再通过网卡发送出去。
  2. 首先,我们的应用程序(在用户空间)调用系统接口发送一个读取网卡数据的请求。
  3. 在调用系统接口时,首先判断内核空间的缓冲区buffer是否已经缓存了对应的数据,如果存在,将缓存区的数据copy到用户空间的buffer中。
  4. 如果不存在,内核空间的进程则将请求发送到网卡,等待网卡数据的返回。数据返回之后将网卡中的数据读取到内核空间的缓冲区,之后再copy到用户空间的缓冲区。

在这里插入图片描述

2-2. 阻塞IO(Blocking IO)

  • 比如用户进程要获取磁盘的数据,此时要发起系统调用,切换成内核态,用户进程进行阻塞等待。
  • 内核进程首先检查内核空间的内核缓冲区是否有数据,如果没有,则要将磁盘中的数据拷贝到内核缓冲区,然后再由内核缓冲区拷贝到用户缓冲区

在这里插入图片描述

在这里插入图片描述

2-3. 非阻塞IO(NonBlocking IO)

  • 可以看到,非阻塞IO模型中,用户进程在第一个阶段(1.等待数据)是非阻塞,第二个阶段(2.从内核拷贝数据到用户空间)是阻塞状态。虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致CPU空转,CPU使用率暴增。
  • 非阻塞IO就是用户进程进行系统调用的时候,如果内核空间的缓冲区没有数据,则直接返回给用户进程。如果用户进程一直请求,内核空间一直没有,此时频繁的进行内核态和用户态的切换,会非常的消耗性能。
  • 非阻塞IO 要与 IO多路复用一起使用才会提高性能

在这里插入图片描述

2-4. IO多路复用(IO Multiplexing)

IO多路复用: 利用单个用户线程来同时监听多个socket,并在某个socket可读、可写时得到通知。

在这里插入图片描述

监听socket的方式、通知的方式有多种实现,常见的有:

  • select:
  • poll:
  • epoll:

IO多路复用 - select

在这里插入图片描述

  • 使用fd_set的每一位记录要监听的socket。大小是固定的1024位。(在当前高并发的场景下,socket只能监听到1024还是不够)
  • 调用select的时候,将用户空间的fd_set拷贝到内核空间,内核空间的线程则会检查fd_set对应的socket是否有数据,如果有数据,则再fd_set拷贝到用户空间。(对于socket还没有数据的,置为0)
  • 用户空间收到fd_set后,逐个遍历fd_set,如果发现其中有就绪的,则去读取对应socket的数据。

IO多路复用 - poll

在这里插入图片描述

与select的比较:

  • select监听的fd数量不超过1024,而poll在内核采用的是链表,理论无上限。
  • poll同样也需要将fd数组拷贝到内核空间,结束时也需要从内核空间拷贝到用户空间。
  • poll同样返回的也是准备就绪的个数,所以在用户空间也需要遍历一遍fd数组。

IO多路复用 - epoll

在这里插入图片描述
在这里插入图片描述

select poll epoll 总结

  • select :使用fd_set来记录要监听的socket,大小是固定的1024。并且在开始和结束时,分别需要进行用户空间和内核空间的拷贝。
  • poll:利用链表结构解决select监听上限的问题,但同样在开始和结束时,分别需要进行用户空间和内核空间的拷贝。
  • epoll:利用红黑树保存要监听的socket,性能更好。每个socket只需要拷贝一次到红黑树,减少了重复拷贝socket到内核空间。内核返回的是就绪的socket,所以无需用户空间再去进行判断,查看是哪个socket准备就绪。

2-5. 信号驱动IO(Signal Driven IO)

2-6. 异步IO(Asynchronous IO)

2-7. Redis网络模型

Redis是单线程吗?

Redis的命令处理是单线程。

Redis的网络处理是多线程。

为什么Redis要使用单线程

  1. Redis本身就是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟,而不是执行速度。
  2. 多线程还要考虑线程安全问题,实现复杂度也会大大增多。

3. 通信协议

Redis中采用的是RESP协议。

4. 内存策略

4-1. 过期策略

在学习Redis缓存的时候我们说过,可以通过expire命令给Redis的key设置TTL(存活时间):

在这里插入图片描述

但有两个问题我们需要思考:

  1. Redis是如何知道一个key是否过期呢?
    • Redis利用2个Dict分别记录key - value以及 key - ttl。如果需要判断一个key是否过期,可以去对应的Dict查看。
  2. 是不是TTL到期就立即删除了呢?
    • 惰性删除:不是在TTL到期后就立刻删除,而是在访问一个key的时候,先检查该key的存活时间,如果已经过期才执行删除。
    • 周期删除:每个一段时间,随机选取key进行检查,如果发现某个key已经过期,就会将其删除。有2种定期清理模式。
      • slow:执行频率默认为10,每秒执行10次,每次不超过25ms
      • fast:执行频率不固定,每次耗时不超过1ms

4-2. 淘汰策略

内存淘汰:就是当Redis内存使用达到设置的阈值时,Redis主动挑选部分key删除以释放更多内存的流程。

在这里插入图片描述

  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值