96SEO 2026-02-19 20:21 12
Redis数据结构Redis网络模型Redis通信协议-RESP协议

我们都知道Redis中保存的Key是字符串value往往是字符串或者字符串的集合。
可见字符串是Redis中最常用的一种数据结构。
不过Redis没有直接使用C语言中的字符串因为C语言字符串存在很多问题
Redis构建了一种新的字符串结构称为简单动态字符串Simple
那么Redis将在底层创建两个SDS其中一个是包含“name”的SDS另一个是包含“虎哥”的SDS。
SDS之所以叫做动态字符串是因为它具备动态扩容的能力例如一个内容为“hi”的SDS
假如我们要给SDS追加一段字符串“,Amy”这里首先会申请新内存空间
如果新字符串大于1M则新空间为扩展后字符串长度1M1。
称为内存预分配。
IntSet是Redis中set集合的一种实现方式基于整数数组来实现并且具备长度可变、有序等特征。
现在数组中每个数字都在int16_t的范围内因此采用的编码方式是INTSET_ENC_INT16每部分占用的字节大小为
我们向该其中添加一个数字50000这个数字超出了int16_t的范围intset会自动升级编码方式到合适的大小。
每个整数占4字节并按照新的编码方式及元素个数扩容数组倒序依次将数组中的元素拷贝到扩容后的正确位置将待添加的元素放入数组末尾最后将inset的encoding属性改为INTSET_ENC_INT32将length属性改为4
Redis会确保Intset中的元素唯一、有序具备类型升级机制可以节省内存空间底层采用二分查找方式来查询
Pair的数据库我们可以根据键实现快速的增删改查。
而键与值的映射关系正是通过Dict来实现的。
Dict由三部分组成分别是哈希表DictHashTable、哈希节点DictEntry、字典Dict
当我们向Dict添加键值对时Redis首先根据key计算出hash值h然后利用
sizemask来计算元素应该存储到数组中的哪个索引位置。
我们存储k1v1假设k1的哈希值h
Dict由三部分组成分别是哈希表DictHashTable、哈希节点DictEntry、字典Dict
Dict中的HashTable就是数组结合单向链表的实现当集合中元素较多时必然导致哈希冲突增多链表过长则查询效率会大大降低。
Dict在每次新增键值对时都会检查负载因子LoadFactor
不管是扩容还是收缩必定会创建新的哈希表导致哈希表的size和sizemask变化而key的查询与sizemask有关。
因此必须对哈希表中的每一个key重新计算索引插入新的哈希表这个过程称为rehash。
过程是这样的
计算新hash表的realeSize值取决于当前要做的是扩容还是收缩
如果是扩容则新size为第一个大于等于dict.ht[0].used
1的2^n如果是收缩则新size为第一个大于等于dict.ht[0].used的2^n
按照新的realeSize申请内存空间创建dictht并赋值给dict.ht[1]
将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]
将dict.ht[1]赋值给dict.ht[0]给dict.ht[1]初始化为空哈希表释放原来的dict.ht[0]的内存
在rehash过程中新增操作则直接写入ht[1]查询、修改和删除则会在dict.ht[0]和dict.ht[1]依次查找并执行。
这样可以确保ht[0]的数据只减不增随着rehash最终为空
类似java的HashTable底层是数组加链表来解决哈希冲突Dict包含两个哈希表ht[0]平常用ht[1]用来rehash
当LoadFactor大于5或者LoadFactor大于1并且没有子进程任务时Dict扩容当LoadFactor小于0.1时Dict收缩扩容大小为第一个大于等于used
的2^nDict采用渐进式rehash每次访问Dict时执行一次rehashrehash时ht[0]只减不增新增操作只在ht[1]执行其它操作在两个哈希表
由一系列特殊编码的连续内存块组成。
可以在任意一端进行压入/弹出操作,
字节记录整个压缩列表占用的内存字节数zltailuint32_t4
字节记录压缩列表表尾节点距离压缩列表的起始地址有多少字节通过这个偏移量可以确定表尾节点的地址。
zllenuint16_t2
65534如果超过这个值此处会记录为65535但节点的真实数量需要遍历整个压缩列表才能计算得出。
entry列表节点不定压缩列表包含的各个节点节点的长度由节点保存的内容决定。
zlenduint8_t1
中的Entry并不像普通链表那样记录前后节点的指针因为记录两个指针要占用16个字节浪费内存。
而是采用了下面的结构
previous_entry_length前一节点的长度占1个或5个字节。
如果前一节点的长度小于254字节则采用1个字节来保存这个长度值如果前一节点的长度大于254字节则采用5个字节来保存这个长度值第一个字节为0xfe后四个字节才是真实长度数据
encoding编码属性记录content的数据类型字符串还是整数以及长度占用1个、2个或5个字节
ZipList中所有存储长度的数值均采用小端字节序即低位字节在前高位字节在后。
例如数值0x1234采用小端字节序后实际存储值为0x3412
ZipListEntry中的encoding编码分为字符串和整数两种
字符串如果encoding是以“00”、“01”或者“10”开头则证明content是字符串
bytes|10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt|5
ZipListEntry中的encoding编码分为字符串和整数两种
整数如果encoding是以“11”开始则证明content是整数且encoding固定只占用1个字节
bytes)1111xxxx1直接在xxxx位置保存数值范围从0001~1101减1后结果为实际值
ZipList的每个Entry都包含previous_entry_length来记录上一个节点的大小长度是1个或5个字节
如果前一节点的长度小于254字节则采用1个字节来保存这个长度值
如果前一节点的长度大于等于254字节则采用5个字节来保存这个长度值第一个字节为0xfe后四个字节才是真实长度数据
现在假设我们有N个连续的、长度为250~253字节之间的entry因此entry的previous_entry_length属性用1个字节即可表示如图所示
ZipList这种特殊情况下产生的连续多次空间扩展操作称之为连锁更新Cascade
压缩列表的可以看做一种连续内存空间的双向链表列表的节点之间不是通过指针连接而是记录上一节点和本节点长度来寻址内存占用较低如果列表数据过多导致链表过长可能影响查询性能增或删较大数据时有可能发生连续更新问题
问题1ZipList虽然节省内存但申请内存必须是连续空间如果内存占用较多申请内存效率很低。
怎么办
答为了缓解这个问题我们必须限制ZipList的长度和entry大小。
问题2但是我们要存储大量数据超出了ZipList最佳的上限该怎么办
问题3数据拆分后比较分散不方便管理和查找这多个ZipList如何建立联系
答Redis在3.2版本引入了新的数据结构QuickList它是一个双端链表只不过链表中的每个节点都是一个ZipList。
为了避免QuickList中的每个ZipList中entry过多Redis提供了一个配置项list-max-ziplist-size来限制。
-1每个ZipList的内存占用不能超过4kb-2每个ZipList的内存占用不能超过8kb-3每个ZipList的内存占用不能超过16kb-4每个ZipList的内存占用不能超过32kb-5每个ZipList的内存占用不能超过64kb
以下是QuickList的和QuickListNode的结构源码
是一个节点为ZipList的双端链表节点采用ZipList解决了传统链表的内存占用问题控制了ZipList大小解决连续内存空间申请效率问题中间节点可以压缩进一步节省了内存
跳跃表是一个双向链表每个节点都包含score和ele值节点按照score值排序score值一样则按照ele字典排序每个节点都可以包含多层指针层数是1到32之间的随机数不同层指针到下一个节点的跨度不同层级越高跨度越大增删改查效率与红黑树基本一致实现却更简单
Redis中的任意数据类型的键和值都会被封装为一个RedisObject也叫做Redis对象源码如下
从Redis的使用者的角度来看⼀个Redis节点包含多个database非cluster模式下默认是16个cluster模式下只能是1个而一个database维护了从key
space的映射关系。
这个映射关系的key是string类型⽽value可以是多种数据类型比如
set等。
我们可以看到key的类型固定是string而value可能的类型是多个。
⽽从Redis内部实现的⾓度来看database内的这个映射关系是用⼀个dict来维护的。
dict的key固定用⼀种数据结构来表达就够了这就是动态字符串sds。
而value则比较复杂为了在同⼀个dict内能够存储不同类型的value这就需要⼀个通⽤的数据结构这个通用的数据结构就是robj全名是redisObject。
Redis中会根据存储的数据类型不同选择不同的编码方式共包含11种不同类型
编号编码方式说明0OBJ_ENCODING_RAWraw编码动态字符串1OBJ_ENCODING_INTlong类型的整数的字符串2OBJ_ENCODING_HThash表字典dict3OBJ_ENCODING_ZIPMAP已废弃4OBJ_ENCODING_LINKEDLIST双端链表5OBJ_ENCODING_ZIPLIST压缩列表6OBJ_ENCODING_INTSET整数集合7OBJ_ENCODING_SKIPLIST跳表8OBJ_ENCODING_EMBSTRembstr的动态字符串9OBJ_ENCODING_QUICKLIST快速列表10OBJ_ENCODING_STREAMStream流
Redis中会根据存储的数据类型不同选择不同的编码方式。
每种数据类型的使用的编码方式如下
数据类型编码方式OBJ_STRINGint、embstr、rawOBJ_LISTLinkedList和ZipList(3.2以前)、QuickList3.2以后OBJ_SETintset、HTOBJ_ZSETZipList、HT、SkipListOBJ_HASHZipList、HT
其基本编码方式是RAW基于简单动态字符串SDS实现存储上限为512mb。
如果存储的SDS长度小于44字节则会采用EMBSTR编码此时object
String可以动态扩展内存但是如果⼀个String类型的value的值是数字那么Redis内部会把它转成long类型来存储从⽽减少内存的使用。
如果存储的字符串是整数值并且大小在LONG_MAX范围内则会采用INT编码直接将数据保存在RedisObject的ptr指针位置刚好8字节不再需要SDS了。
用来表示String的robj可能编码成3种内部表⽰OBJ_ENCODING_RAWOBJ_ENCODING_EMBSTROBJ_ENCODING_INT。
其中前两种编码使⽤的是sds来存储最后⼀种OBJ_ENCODING_INT编码直接把string存成了long型。
decr等操作的时候如果它内部是OBJ_ENCODING_INT编码那么可以直接行加减操作如果它内部是OBJ_ENCODING_RAW或OBJ_ENCODING_EMBSTR编码那么Redis会先试图把sds存储的字符串转成long型如果能转成功再进行加减操作。
对⼀个内部表示成long型的string执行append,
getrange这些命令针对的仍然是string的值即⼗进制表示的字符串而不是针对内部表⽰的long型进⾏操作。
比如字符串”32”如果按照字符数组来解释它包含两个字符它们的ASCII码分别是0x33和0x32。
当我们执行命令setbit
0的时候相当于把字符0x33变成了0x32这样字符串的值就变成了”22”。
⽽如果将字符串”32”按照内部的64位long型来解释那么它是0x0000000000000020在这个基础上执⾏setbit位操作结果就完全不对了。
因此在这些命令的实现中会把long型先转成字符串再进行相应的操作。
压缩列表可以从双端访问内存占用低存储上限低QuickListLinkedList
ZipList可以从双端访问内存占用较低包含多个ZipList存储上限高
Redis的List结构类似一个双端链表可以从首、尾操作列表中的元素
在3.2版本之前Redis采用ZipList和LinkedList来实现List当元素数量小于512并且元素大小小于64字节时采用ZipList编码超过则采用LinkedList编码。
在3.2版本之后Redis统一采用QuickList来实现List
可以看出Set对查询元素的效率要求非常高思考一下什么样的数据结构可以满足
HashTable也就是Redis中的Dict不过Dict是双列集合可以存键、值对
Set是Redis中的集合不一定确保元素有序可以满足元素唯一、查询效率要求极高。
为了查询效率和唯一性set采用HT编码Dict。
Dict中的key用来存储元素value统一为null。
当存储的所有数据都是整数并且元素数量不超过set-max-intset-entries时Set会采用IntSet编码以节省内存
ZSet也就是SortedSet其中每一个元素都需要指定一个score值和member值
可以根据score值排序后member必须唯一可以根据member查询分数
因此zset底层数据结构必须满足键值存储、键必须唯一、可排序这几个需求。
之前学习的哪种编码结构可以满足
SkipList可以排序并且可以同时存储score和ele值memberHTDict可以键值存储并且可以根据key找value
当元素数量不多时HT和SkipList的优势不明显而且更耗内存。
因此zset还会采用ZipList结构来节省内存不过需要同时满足两个条件
元素数量小于zset_max_ziplist_entries默认值128每个元素都小于zset_max_ziplist_value字节默认值64
ziplist本身没有排序功能而且没有键值对的概念因此需要有zset通过编码实现
ZipList是连续内存因此score和element是紧挨在一起的两个entry
element在前score在后score越小越接近队首score越大越接近队尾按照score值升序排列
zset的键是member值是scorehash的键和值都是任意值zset要根据score排序hash则无需排序
当Hash中数据项比较少的情况下Hash底层才⽤压缩列表ziplist进⾏存储数据随着数据的增加底层的ziplist就可能会转成dict具体配置如下
当满足上面两个条件其中之⼀的时候Redis就使⽤dict字典来实现hash。
Redis的hash之所以这样设计是因为当ziplist变得很⼤的时候它有如下几个缺点
每次插⼊或修改引发的realloc操作会有更⼤的概率造成内存拷贝从而降低性能。
⼀旦发生内存拷贝内存拷贝的成本也相应增加因为要拷贝更⼤的⼀块数据。
当ziplist数据项过多的时候在它上⾯查找指定的数据项就会性能变得很低因为ziplist上的查找需要进行遍历。
总之ziplist本来就设计为各个数据项挨在⼀起组成连续的内存空间这种结构并不擅长做修改操作。
⼀旦数据发⽣改动就会引发内存realloc可能导致内存拷贝。
因此Hash底层采用的编码与Zset也基本一致只需要把排序有关的SkipList去掉即可
当数据量较大时Hash结构会转为HT编码也就是Dict触发条件有两个
ZipList中的元素数量超过了hash-max-ziplist-entries默认512ZipList中的任意entry大小超过了hash-max-ziplist-value默认64字节
服务器大多都采用Linux系统这里我们以Linux为例来讲解:
都是Linux的发行版发行版可以看成对linux包了一层壳任何Linux发行版其系统内核都是Linux。
我们的应用都需要通过Linux内核与硬件交互
用户的应用比如redismysql等其实是没有办法去执行访问我们操作系统的硬件的所以我们可以通过发行版的这个壳子去访问内核再通过内核去访问计算机硬件
计算机硬件包括如cpu内存网卡等等内核通过寻址空间可以操作硬件的但是内核需要不同设备的驱动有了这些驱动之后内核就可以去对计算机硬件去进行
我们想要用户的应用来访问计算机就必须要通过对外暴露的一些接口才能访问到从而简介的实现对内核的操控但是内核本身上来说也是一个应用所以他本身也需要一些内存cpu等设备资源用户应用本身也在消耗这些资源如果不加任何限制用户去操作随意的去操作我们的资源就有可能导致一些冲突甚至有可能导致我们的系统出现无法运行的问题因此我们需要把用户和内核隔离开
进程的寻址空间划分成两部分内核空间、用户空间用户空间只能执行受限的命令
(Ring3)而且不能直接调用系统资源必须通过内核提供的接口来访问内核空间可以执行特权命令
什么是寻址空间呢我们的应用程序也好还是内核空间也好都是没有办法直接去物理内存的而是通过分配一些虚拟内存映射到物理内存中我们的内核和应用程序去访问虚拟内存的时候就需要一个虚拟地址这个地址是一个无符号的整数比如一个32位的操作系统他的带宽就是32他的虚拟地址就是2的32次方也就是说他寻址的范围就是0~2的32次方
这片寻址空间对应的就是2的32个字节就是4GB这个4GB会有3个GB分给用户空间会有1GB给内核系统
在linux中他们权限分成两个等级0和3用户空间只能执行受限的命令Ring3而且不能直接调用系统资源必须通过内核提供的接口来访问内核空间可以执行特权命令Ring0调用一切系统资源所以一般情况下用户的操作是运行在用户空间而内核运行的数据是在内核空间的而有的情况下一个应用程序需要去调用一些特权资源去调用一些内核空间的操作所以此时他俩需要在用户态和内核态之间进行切换。
Linux系统为了提高IO效率会在用户空间和内核空间都加入缓冲区
针对这个操作我们的用户在写读数据时会去向内核态申请想要读取内核的数据而内核数据要去等待驱动程序从硬件上读取数据当从磁盘上加载到数据之后内核会将数据写入到内核的缓冲区中然后再将数据拷贝到用户态的buffer中然后再返回给应用程序整体而言速度慢就是这个原因为了加速我们希望read也好还是wait
应用程序想要去读取数据他是无法直接去读取磁盘数据的他需要先到内核里边去等待内核操作硬件拿到数据这个过程就是1是需要等待的等到内核从磁盘上把数据加载出来之后再把这个数据写给用户的缓存区这个过程是2如果是阻塞IO那么整个过程中用户从发起读请求开始一直到读取到数据都是一个阻塞状态。
用户去读取数据时会去先发起recvform一个命令去尝试从内核上加载数据如果内核没有数据那么用户就会等待此时内核会去从硬件上读取数据内核读取数据之后会把数据拷贝到用户态并且返回ok整个过程都是阻塞等待的这就是阻塞IO
用户进程尝试读取数据比如网卡数据此时数据尚未到达内核需要等待数据此时用户进程也处于阻塞状态
数据到达并拷贝到内核缓冲区代表已就绪将内核数据拷贝到用户缓冲区拷贝过程中用户进程依然阻塞等待拷贝完成用户进程解除阻塞处理数据
顾名思义非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程。
用户进程尝试读取数据比如网卡数据此时数据尚未到达内核需要等待数据返回异常给用户进程用户进程拿到error后再次尝试读取循环往复直到数据就绪
将内核数据拷贝到用户缓冲区拷贝过程中用户进程依然阻塞等待拷贝完成用户进程解除阻塞处理数据可以看到非阻塞IO模型中用户进程在第一个阶段是非阻塞第二个阶段是阻塞状态。
虽然是非阻塞但性能并没有得到提高。
而且忙等机制会导致CPU空转CPU使用率暴增。
无论是阻塞IO还是非阻塞IO用户应用在一阶段都需要调用recvfrom来获取数据差别在于无数据时的处理方案
如果调用recvfrom时恰好没有数据阻塞IO会使CPU阻塞非阻塞IO使CPU空转都不能充分发挥CPU的作用。
如果调用recvfrom时恰好有数据则用户进程可以直接进入第二阶段读取并处理数据
比如服务端处理客户端Socket请求时在单线程情况下只能依次处理每一个socket如果正在处理的socket恰好未就绪(数据不可读或不可写)线程就会被阻塞所有其它客户端socket都必须等待性能自然会很差。
方案一增加更多服务员多线程方案二不排队谁想好了吃什么数据就绪了服务员就给谁点餐用户应用就去读取数据
所以接下来就需要详细的来解决多路复用模型是如何知道到底怎么知道内核数据是否就绪的问题了
开始的无符号整数用来关联Linux中的一个文件。
在Linux中一切皆文件例如常规文件、视频、硬件设备等当然也包括网络套接字Socket。
IO多路复用是利用单个线程来同时监听多个FD并在某个FD可读、可写时得到通知从而避免无效的等待充分利用CPU资源。
用户进程调用select指定要监听的FD集合核监听FD对应的多个socket任意一个或多个socket数据就绪则返回readable此过程中用户进程阻塞
用户进程找到就绪的socket依次调用recvfrom读取数据内核将数据拷贝到用户空间用户进程处理数据
当用户去读取数据的时候不再去直接调用recvfrom了而是调用select的函数select函数会将需要监听的数据交给内核由内核去检查这些数据是否就绪了如果说这个数据就绪了就会通知应用程序数据就绪然后来读取数据再从内核中把数据拷贝给用户态完成数据处理如果N多个FD一个都没处理完此时就进行等待。
用IO复用模式可以确保去读数据的时候数据是一定存在的他的效率比原来的阻塞IO和非阻塞IO性能都要高
IO多路复用是利用单个线程来同时监听多个FD并在某个FD可读、可写时得到通知从而避免无效的等待充分利用CPU资源。
不过监听FD的方式、通知的方式又有多种实现常见的有
其中select和pool相当于是当被监听的数据准备好之后他会把你监听的FD整个数据都发给你你需要到整个FD中去找哪些是处理好了的需要通过遍历的方式所以性能也并不是那么好
而epoll则相当于内核准备好了之后他会把准备好的数据直接发给你咱们就省去了遍历的动作。
简单说就是我们把需要处理的数据封装成FD然后在用户态时创建一个fd的集合这个集合的大小是要监听的那个FD的最大值1但是大小整体是有限制的
这个集合的长度大小是有限制的同时在这个集合中标明出来我们要控制哪些数据
比如要监听的数据是1,2,5三个数据此时会执行select函数然后将整个fd发给内核态内核态会去遍历用户态传递过来的数据如果发现这里边都数据都没有就绪就休眠直到有数据准备好时就会被唤醒唤醒之后再次遍历一遍看看谁准备好了然后再将处理掉没有准备好的数据最后再将这个FD集合写回到用户态中去此时用户态就知道了奥有人准备好了但是对于用户态而言并不知道谁处理好了所以用户态也需要去进行遍历然后找到对应准备好数据的节点再去发起读请求我们会发现这种模式下他虽然比阻塞IO和非阻塞IO好但是依然有些麻烦的事情
poll模式对select模式做了简单改进但性能提升不明显部分关键代码如下
创建pollfd数组向其中添加关注的fd信息数组大小自定义调用poll函数将pollfd数组拷贝到内核空间转链表存储无上限内核遍历fd判断是否就绪数据就绪或超时后拷贝pollfd数组到用户空间返回就绪fd数量n用户进程判断n是否大于0,大于0则遍历pollfd数组找到就绪的fd
select模式中的fd_set大小固定为1024而pollfd在内核中采用链表理论上无上限监听FD越多每次遍历消耗时间也越久性能反而会下降
epoll模式是对select和poll的改进它提供了三个函数
紧接着调用epoll_ctl操作将要监听的数据添加到红黑树上去并且给每个fd设置一个监听函数这个函数会在fd数据就绪时触发就是准备好了现在就把fd把数据添加到list_head中去
就去等待在用户态创建一个空的events数组当就绪之后我们的回调函数会把数据添加到list_head中去当调用这个函数的时候会去检查list_head当然这个过程需要参考配置的等待时间可以等一定时间也可以一直等
如果在此过程中检查到了list_head中有数据会将数据添加到链表中此时将数据放入到events数组中并且返回对应的操作的数量用户态的此时收到响应后从events中拿到对应准备好的数据的节点再去调用方法去拿数据。
能监听的FD最大不超过1024每次select都需要把所有要监听的FD都拷贝到内核空间每次都要遍历所有FD来判断就绪状态
poll利用链表解决了select中监听FD上限的问题但依然要遍历所有FD如果监听较多性能会下降
基于epoll实例中的红黑树保存要监听的FD理论上无上限而且增删改查效率都非常高每个FD只需要执行一次epoll_ctl添加到红黑树以后每次epol_wait无需传递任何参数无需重复拷贝FD到内核空间利用ep_poll_callback机制来监听FD状态无需遍历所有FD因此性能不会随监听的FD数量增多而下降
当FD有数据可读时我们调用epoll_wait或者select、poll可以得到通知。
但是事件通知的模式有两种
LevelTriggered简称LT也叫做水平触发。
只要某个FD中有数据可读每次调用epoll_wait都会得到通知。
EdgeTriggered简称ET也叫做边沿触发。
只有在某个FD有状态变化时调用epoll_wait才会被通知。
假设一个客户端socket对应的FD已经注册到了epoll实例中客户端socket发送了2kb的数据服务端调用epoll_wait得到通知说FD就绪服务端从FD读取了1kb数据回到步骤3再次调用epoll_wait形成循环
如果我们采用LT模式因为FD中仍有1kb数据则第⑤步依然会返回结果并且得到通知
如果我们采用ET模式因为第③步已经消费了FD可读事件第⑤步FD状态没有变化因此epoll_wait不会返回数据无法读取客户端响应超时。
服务器启动以后服务端会去调用epoll_create创建一个epoll实例epoll实例中包含两个数据
创建好了之后会去调用epoll_ctl函数此函数会会将需要监听的数据添加到rb_root中去并且对当前这些存在于红黑树的节点设置回调函数当这些被监听的数据一旦准备完成就会被调用而调用的结果就是将红黑树的fd添加到list_head中去(但是此时并没有完成)
3、当第二步完成后就会调用epoll_wait函数这个函数会去校验是否有数据准备完毕因为数据一旦准备就绪就会被回调函数添加到list_head中在等待了一段时间后(可以进行配置)如果等够了超时时间则返回没有数据如果有则进一步判断当前是什么事件如果是建立连接时间则调用accept()
接受客户端socket拿到建立连接的socket然后建立起来连接如果是其他事件则把数据进行写出
信号驱动IO是与内核建立SIGIO的信号关联并设置回调当内核有FD就绪时会发出SIGIO信号通知用户期间用户应用可以执行其它业务无需阻塞等待。
用户进程调用sigaction注册信号处理函数内核返回成功开始监听FD用户进程不阻塞等待可以执行其它业务当内核数据就绪后回调用户进程的SIGIO处理函数
收到SIGIO回调信号调用recvfrom读取内核将数据拷贝到用户空间用户进程处理数据
当有大量IO操作时信号较多SIGIO处理函数不能及时处理可能导致信号队列溢出而且内核空间与用户空间的频繁信号交互性能也较低。
异步IO的整个过程都是非阻塞的用户进程调用完异步API后就可以去做其它事情内核等待数据就绪并拷贝到用户空间后才会递交信号通知用户进程。
这种方式不仅仅是用户态在试图读取数据后不阻塞而且当内核的数据准备完成后也不会阻塞
他会由内核将所有数据处理完成后由内核将数据写入到用户态中然后才算完成所以性能极高不会有任何阻塞全部都由内核完成可以看到异步IO模型中用户进程在两个阶段都是非阻塞状态。
IO操作是同步还是异步关键看数据在内核空间与用户空间的拷贝过程(数据读写的IO操作)
如果仅仅聊Redis的核心业务部分命令处理答案是单线程如果是聊整个Redis那么答案就是多线程
在Redis版本迭代过程中在两个重要的时间节点上引入了多线程的支持
v4.0引入多线程异步处理一些耗时较旧的任务例如异步删除命令unlinkRedis
6.0之前确实都是单线程。
是利用epollLinux系统这样的IO多路复用技术在事件循环中不断处理客户端情况。
内存操作执行速度非常快它的性能瓶颈是网络延迟而不是执行速度因此多线程并不会带来巨大的性能提升。
多线程会导致过多的上下文切换带来不必要的开销引入多线程会面临线程安全问题必然要引入线程锁这样的安全手段实现复杂度增高而且性能也会大打折扣
Redis通过IO多路复用来提高网络性能并且支持各种不同的多路复用实现并且将这些实现进行封装提供了统一的高性能事件库API库AE:
当我们的客户端想要去连接我们服务器会去先到IO多路复用模型去进行排队会有一个连接应答处理器他会去接受读请求然后又把读请求注册到具体模型中去此时这些建立起来的连接如果是客户端请求处理器去进行执行命令时他会去把数据读取出来然后把数据放入到client中
clinet去解析当前的命令转化为redis认识的命令接下来就开始处理这些命令从redis中的command中找到这些命令然后就真正的去操作对应的数据了当数据操作完成后会去找到命令回复处理器再由他将数据写出。
Redis是一个CS架构的软件通信一般分两步不包括pipeline和PubSub
因此客户端发送命令的格式、服务端响应结果的格式必须有一个规范这个规范就是通信协议。
6.0版本中从RESP2升级到了RESP3协议增加了更多数据类型并且支持6.0的新特性–客户端缓存
但目前默认使用的依然是RESP2协议也是我们要学习的协议版本以下简称RESP。
在RESP中通过首字节的字符来区分不同数据类型常用的数据类型包括5种
后面跟上数字格式的字符串以CRLF结尾。
例如“:10\r\n”
Redis支持TCP通信因此我们可以使用Socket来模拟客户端与Redis服务端建立连接
OutputStreamWriter(s.getOutputStream(),
StandardCharsets.UTF_8));reader
InputStreamReader(s.getInputStream(),
handleResponse();System.out.println(obj
handleResponse();System.out.println(obj
handleResponse();System.out.println(obj
handleResponse();System.out.println(obj
{e.printStackTrace();}}}private
RuntimeException(reader.readLine());case
Long.parseLong(reader.readLine());case
Integer.parseInt(reader.readLine());if
再读数据,读len个字节。
我们假设没有特殊字符所以读一行简化return
RuntimeException(错误的数据格式);}}private
Integer.parseInt(reader.readLine());if
{list.add(handleResponse());}return
arg.getBytes(StandardCharsets.UTF_8).length);writer.println(arg);}writer.flush();}
Redis之所以性能强最主要的原因就是基于内存存储。
然而单节点的Redis其内存大小不宜过大会影响持久化或主从同步性能。
当内存使用达到上限时就无法存储更多数据了。
为了解决这个问题Redis提供了一些策略实现内存回收
在学习Redis缓存的时候我们说过可以通过expire命令给Redis的key设置TTL存活时间
可以发现当key的TTL到期以后再次访问name返回的是nil说明这个key已经不存在了对应的内存也得到释放。
从而起到内存回收的目的。
Redis本身是一个典型的key-value内存存储数据库因此所有的key、value都保存在之前学习过的Dict结构中。
不过在其database结构体中有两个Dict一个用来记录key-value另一个用来记录key-TTL。
利用两个Dict分别记录key-value对及key-ttl对
惰性删除顾明思议并不是在TTL到期后就立刻删除而是在访问一个key的时候检查该key的存活时间如果已经过期才执行删除。
周期删除顾明思议是通过一个定时任务周期性的抽样部分过期的key然后执行删除。
执行周期有两种
Redis服务初始化函数initServer()中设置定时任务按照server.hz的频率来执行过期key清理模式为SLOWRedis的每个事件循环前会调用beforeSleep()函数执行过期key清理模式为FAST
执行频率受server.hz影响默认为10即每秒执行10次每个执行周期100ms。
执行清理耗时不超过一次执行周期的25%.默认slow模式耗时不超过25ms逐个遍历db逐个遍历db中的bucket抽取20个key判断是否过期如果没达到时间上限25ms并且过期key比例大于10%再进行一次抽样否则结束FAST模式规则过期key比例小于10%不执行
执行频率受beforeSleep()调用频率影响但两次FAST模式间隔不低于2ms执行清理耗时不超过1ms逐个遍历db逐个遍历db中的bucket抽取20个key判断是否过期
如果没达到时间上限1ms并且过期key比例大于10%再进行一次抽样否则结束
FAST模式执行频率不固定但两次间隔不低于2ms每次耗时不超过1ms
内存淘汰就是当Redis内存使用达到设置的上限时主动挑选部分key删除以释放更多内存的流程。
Redis会在处理客户端命令的方法processCommand()中尝试做内存淘汰
不淘汰任何key但是内存满时不允许写入新数据默认就是这种策略。
对设置了TTL的key比较key的剩余TTL值TTL越小越先被淘汰
Used最少最近使用。
用当前时间减去最后一次访问时间这个值越大则淘汰优先级越高。
LFULeast
Used最少频率使用。
会统计每个key的访问频率值越小淘汰优先级越高。
LFU的访问次数之所以叫做逻辑访问次数是因为并不是每次key被访问都计数而是通过运算
1且最大不超过255访问次数会随时间衰减距离上一次访问时间每隔
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback