底层数据结构
主要有七大底层结构:

介绍通常所熟知底层数据结构外的结构:
1.简单动态字符串(SDS)
与日常认知的c的字符串不同,简单动态字符串不仅可以保存二进制数据,还可以保存文本数据。SDS的所有API都会进行处理二进制的方式来处理其存放的数据。
(所有SDS的最大区别特点是:可以存储图片,音频,视频以及压缩文件的二进制数据)
【注意】
1)redis中SDS的API都是安全的,拼接字符串不会导致缓冲区溢出。因为在拼接时候,会先检查SDS的空间是否满足需求,如果不够则会自动扩容,避免了缓冲区溢出问题。
2)相比于C的字符串获取长度,SDS的获取长度len方法是O(1)的,因为其本身就存储了长度,而C中字符串获取长度则是O(n)。
SDS存储结构变化:
Redis6之前:

free表示剩余可用空间,而len表示buf中已经占用的空间。
Redis6之后:
会依据字符串长度不同,定义5种SDS的结构体sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,长度分别对应,2的n次幂。redis会依据不同字符串长度进行选择合适的结构,从而提升内存的利用率,减少空间浪费。
【区别】其中sdshdr5的存储格式是不同于8,16等存储格式:
Sdshdr5存储格式是:

其中一个字节进行存储对应的数据类型和长度,这一个字节高位3位进行存储type,剩余5位进行存储当前字符串长度。
Sdshdr8-64存储格式是:
分别给1-2个字节给字符串长度len(已使用长度),buf区总存储长度alloc,以及一个flags标识(其中存储对应数据类型type),剩余都是buf区存储位置。
2.压缩列表
压缩列表本质其实是一个数组而已,但是不同的是,压缩链表表头有三个字段:zlbytes(列表长度)、zltail(列表偏移量)和zllen(列表中entry个数),尾还有一个zlend(标识列表结束)。

针对压缩列表底层进行解析:
由于可以借助表头三个字段以及表尾结束字段进行辅助确定第一个元素和最后一个元素所以第一个元素和最后一个元素查询效率是O(1),而其他元素则需要遍历,查询时间复杂度是O(N)。
【注意】因为查询效率不算高,所以压缩列表不适合保存过多元素。
3.跳表
跳表是在链表基础上,给加上了多级索引,通过索引位置的跳转,实现数据的快速定位。
具体细节:
1)跳表是基于链表实现的,而且其访问是随机访问,平均时间花费O(log n)而最坏是全部访问完,则是O(n)。
2)但是由于是链表,则其查询效率会低,但是针对数据的修改、增加和删除效率较高,所以适合动态数据。
3)跳表适合用于内存存储,因为链表结构在内存中访问效率高,但是空间的利用率低。
Value值存储结构体

type字段:表示当前对象使用的数据类型,Redis主要支持5种数据类型:string, hash、 list,set,zset。可以使用type { key}命令查看对象所属类型,type命令返回的是值对象类型,键都是string类型。
encoding字段:表示Redis内部编码类型,encoding在 Redis内部使用,代表当前对象内部采用哪种数据结构实现。理解Redis内部编码方式对于优化内存非常重要,同一个对象采用不同的编码实现内存占用存在明显差异。
lru字段(辅助淘汰策略执行):记录对象最后次被访问的时间,当配置了maxmemory和maxmemory-policy=volatile-lru或者allkeys-lru时,用于辅助LRU算法删除键数据。可以使用object idletime {key}命令在不更新lru字段情况下查看当前键的空闲时间。
refcount字段:记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全回收当前对象空间。使用object refcount(key}获取当前对象引用。当对象为整数且范围在[0-9999]时,Redis可以使用共享对象的方式来节省内存。
【重点】redis使用什么内存回收算法?
由于redisobject内部存有refcount计数字段,用于辅助判断淘汰,所以redis中使用的是引用计数法形式进行内存回收。
*ptr字段:相当于一个引用,指向真实存储值的内存位置。
【问题】redisobject对象有什么用?
相当于定制一个统一结构体,这个结构体既可以存储string类型又可以存储别的类型,并且依据encoding字段进行区分到底当前对象是哪种结构体。这有个好处就是不需要定义不同存储结构进行存储不同数据类型,统一采用一个存储结构即可实现。
Redis中线程和IO模型

【注意】TCP通信建立都会建立socket。
文件事件分派器
文件事件分派器接收 I/O 多路复用程序传来的socket, 并根据socket产生的事件类型, 调用相应的事件处理器。
【结论】所以说就算实现多路复用,其传输到文件事件分派器时候都是单线程,一个个socket被按序执行,这就是为什么bigkey导致阻塞可能会有很大影响。
Redis6的多线程
【前情】在redis6之前其实也不能算单纯的单线程,因为除了获取(读socket),解析,执行,内容返回(socket写)等都是由一个主线程执行外,在redis4之后加入的后台线程进行处理慢操作、清理脏数据、大key删除等也算是多线程了。
【问题】为什么redis6之前不使用多线程?
因为使用redis时候,几乎不存在CPU瓶颈时期,redis主要受限于内存和网络,不会太占用CPU。同时使用单线程后,可维护性也比较高,多线程模型虽然在某些方面表些优异,但是也容易引来相关线程执行顺序不确定性问题,带来一些并发读写问题,死锁问题导致性能损耗,增加系统复杂度与出错概率。
同时在单线程情况下,就不需要考虑hash的惰性rehash、lpush等线程不安全情况,也就不必要进行加锁。
【问题】那为什么redis6之后会引入多线程呢?
Redis将所有的数据放到了内存中,内存响应时长大约100纳秒,对于小数据包来说,Redis服务器可以处理8w-10w的qps,这就是单线程的极限,对于大部分人来说,单线程Redis足够用了。
但是随着业务复杂,大公司需要更大qps,常见解决方案就是分布式架构中对数据分区和采用多个服务器进行存储,但是这样做会导致由于redis服务器过多,从而使得维护成本过高,而且数据分区也无法解决热点读/写问题以及数据倾斜(bigkey)等复杂场景。
Redis6之后就算是支持多线程,但是多线程也是默认不开启状态,如果需要打开多线程则需要修改对应redis.conf配置:
Io-threads-do-redis yes
Redis淘汰策略
由于redis中配置了内存限制(maxmemory),一旦超过限制的内存,则会触发淘汰策略,而可选择的策略有以下几种:
1)noeviction(默认):不继续接受写请求,读请求可以正常运行。
2)volatile-ttl:针对被设置过期时间的key,设置ttl剩余寿命时间,ttl越小越优先被淘汰。
3)volatile-lru:最少被使用的已经被设置过期时间的key进行淘汰,没有设置过期时间的key不会收到影响。
4)volatile-random:随机淘汰已经设置过期时间的键。
5)allkeys-lru:采用lru算法,淘汰最长不被使用的key。
6)allkeys-random:随机淘汰key。
LRU算法
Redis使用的是近似LRU算法(不是LRU),它跟LRU算法不太一样,它给每个key增加一个额外的小字段,字段长度为24个bit,用于记录最近一次被访问的时间戳。
当Redis执行写操作时候,发现内存超出maxmemory时候,就会执行一次LRU算法,这个算法使用随机采样法,采出固定个数个key,依据这个key种所存储的最近被访问时间戳数据,淘汰掉最旧的key。
【问题】为什么使用随机采样?
原生LRU算法就是全部值中进行比对,然后淘汰,使用随机采样是因为遍历全部数据说消耗的成本太高,随机采样固定抽出个数进行比对后淘汰所消耗时间,效率都更少。
LFU算法
LFU是在Redis4版本之后,新加入的一种淘汰策略,核心思想是基于key被访问次数多少决定,是否被淘汰,其会占用原先分配给LRU算法的记录字段,24个bit中8个bit进行记录当前key被访问次数。
这很好的表示一个key的热度,同时弥补了LRU算法的不足,当一个key很久都没有被访问,只是在进行LRU检查时候被访问一次,那这个key基于LRU算法来说就不会被淘汰,而LFU正好弥补了这个判断失误。
【问题】为什么Redis要缓存系统时间戳?
因为从系统中获取一次时间戳是一次系统调用,系统调用相对来说比较消耗时间与性能,作为单主线程的Redis来说,对其性能消耗比较大,所以说Redis会进行缓存,有一个定时任务进行执行每一毫秒更新一次时间缓存,key获取时间戳都是从缓存中拿。
Redis过期策略【重点】
Redis 所有的数据结构都可以设置过期时间,时间一到,就会自动删除。但是因为同一时间太多的key 过期,以至于忙不过来。同时因为Redis 是单线程的,删除的时间也会占用线程的处理时间,如果删除的太过于繁忙,会不会导致线上读写指令出现卡顿。
(1)定期删除
Redis会将设置了过期时间的key统一放入到一个字典中,后续会定期随机进行扫描字典中固定个数的key,查看是否存在过期的key,并进行删除到期的key。如果扫描出的过期Key占比高于本次扫描key的总数,则再次进行扫描。
【问题】如果大面积的key同时过期怎么办?
这就会导致redis忙于扫描删除,导致阻塞卡顿。
解决办法是在设置过期时间时候,给过期时间加上一个随机值(1-5s),让其不会全部再同一时刻过期。
(2)惰性删除
只有再访问这个key时候,再进行判断是否过期,过期则删除,且不会返回任何东西。
【重点】lazyfree
这是在redis4之后加入的机制,它可以有效缓解主线程在执行删除bigkey,删除大量Key等问题时候,导致redis阻塞情况。
其本质就是将删除Key和针对数据库的操作放在了后台线程执行,尽可能避免服务器阻塞。
(3)整体扫描删除
由于定期删除和惰性删除结合,可以解决大部分场景,但是针对高并发量,如秒杀场景、双11等,则会导致由于大量key的过期仍未被删除情况下,又新加入许多key情况,会导致删除过期key力不从心。
这时候就需要一个兜底整体缓存扫描了,固定时间对整体缓存进行扫描,以达到清除大量过期key的效果。
【扩展】从库过期删除策略
由于从库是不会进行定期扫描的,所以说从库的处理是被动的。主库在key到期情况下,会在AOF文件中加入del该key的指令,这样从库进行复制时候,会拿到AOF文件并执行其中指令,就可以达到删除过期key问题。
但是仍然存在问题,就是在同步过程中,从库键是没有被删除的,所以说会导致主从数据不一致情况。

被折叠的 条评论
为什么被折叠?



