我对Redis的理解

本文主要对Redis的设计和实现原理做了一个介绍很总结,有些东西我也介绍的不是很详细准确,尽量在自己的理解范围内把一些知识点和关键性技术做一个描述。如有错误,还望见谅,欢迎指出。

1、使用和基础数据结构(外观)

redis的基本使用方式是建立在redis提供的数据结构上的。

字符串
REDIS_STRING (字符串)是 Redis 使用得最为广泛的数据类型,它除了是 SET 、GET 等命令 的操作对象之外,数据库中的所有键,以及执行命令时提供给 Redis 的参数,都是用这种类型 保存的。

字符串类型分别使用 REDIS_ENCODING_INT 和 REDIS_ENCODING_RAW 两种编码

只有能表示为 long 类型的值,才会以整数的形式保存,其他类型 的整数、小数和字符串,都是用 sdshdr 结构来保存

哈希表
REDIS_HASH (哈希表)是HSET 、HLEN 等命令的操作对象

它使用 REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_HT 两种编码方式

Redis 中每个hash可以存储232-1键值对(40多亿)

列表
REDIS_LIST(列表)是LPUSH 、LRANGE等命令的操作对象

它使用 REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_LINKEDLIST 这两种方式编码

一个列表最多可以包含232-1 个元素(4294967295, 每个列表超过40亿个元素)。

集合
REDIS_SET (集合) 是 SADD 、 SRANDMEMBER 等命令的操作对象

它使用 REDIS_ENCODING_INTSET 和 REDIS_ENCODING_HT 两种方式编码

Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)

有序集
REDIS_ZSET (有序集)是ZADD 、ZCOUNT 等命令的操作对象

它使用 REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_SKIPLIST 两种方式编码

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

有序集合的成员是唯一的,但分数(score)却可以重复。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)

下图说明了,外部数据结构和底层实际数据结构是通过realobject来连接的。一个外观类型里面必然存着一个realobject,通过它来访问底层数据结构。
这里写图片描述
2、底层数据结构

下面讨论redis底层数据结构

1 SDS动态字符串

sds字符串是字符串的实现

动态字符串是一个结构体,内部有一个buf数组,以及字符串长度,剩余长度等字段,优点是通过长度限制写入,避免缓冲区溢出,另外剩余长度不足时会自动扩容,扩展性较好,不需要频繁分配内存。

并且sds支持写入二进制数据,而不一定是字符。

2 dict字典

dict字典是哈希表的实现。

dict字典与Java中的哈希表实现简直如出一辙,首先都是数组+链表组成的结构,通过dictentry保存节点。

其中dict同时保存两个entry数组,当需要扩容时,把节点转移到第二个数组即可,平时只使用一个数组。

3 压缩链表ziplist

3.1 ziplist是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供push和pop操作。

3.2 实际上,ziplist充分体现了Redis对于存储效率的追求。一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。

3.3 而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。

3.4 另外,ziplist为了在细节上节省内存,对于值的存储采用了变长的编码方式,大概意思是说,对于大的整数,就多用一些字节来存储,而对于小的整数,就少用一些字节来存储。

实际上。redis的字典一开始的数据比较少时,会使用ziplist的方式来存储,也就是key1,value1,key2,value2这样的顺序存储,对于小数据量来说,这样存储既省空间,查询的效率也不低。

当数据量超过阈值时,哈希表自动膨胀为之前我们讨论的dict。

4 quicklist

quicklist是结合ziplist存储优势和链表灵活性与一身的双端链表。

quicklist的结构为什么这样设计呢?总结起来,大概又是一个空间和时间的折中:

4.1 双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。

首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。

4.2 ziplist由于是一整块连续内存,所以存储效率很高。

但是,它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。
这里写图片描述
5 zset
zset其实是两种结构的合并。也就是dict和skiplist结合而成的。dict负责保存数据对分数的映射,而skiplist用于根据分数进行数据的查询(相辅相成)

6 skiplist

sortset数据结构使用了ziplist+zset两种数据结构。

Redis里面使用skiplist是为了实现sorted set这种对外的数据结构。sorted set提供的操作非常丰富,可以满足非常多的应用场景。这也意味着,sorted set相对来说实现比较复杂。

sortedset是由skiplist,dict和ziplist组成的。

当数据较少时,sorted set是由一个ziplist来实现的。
当数据多的时候,sorted

set是由一个叫zset的数据结构来实现的,这个zset包含一个dict + 一个skiplist。dict用来查询数据到分数(score)的对应关系,而skiplist用来根据分数查询数据(可能是范围查找)。

在本系列前面关于ziplist的文章里,我们介绍过,ziplist就是由很多数据项组成的一大块连续内存。由于sorted set的每一项元素都由数据和score组成,因此,当使用zadd命令插入一个(数据, score)对的时候,底层在相应的ziplist上就插入两个数据项:数据在前,score在后。
这里写图片描述
skiplist的节点中存着节点值和分数。并且跳表是根据节点的分数进行排序的,所以可以根据节点分数进行范围查找。

7inset

inset是一个数字结合,他使用灵活的数据类型来保持数字。
这里写图片描述
新创建的intset只有一个header,总共8个字节。其中encoding = 2, length = 0。
添加13, 5两个元素之后,因为它们是比较小的整数,都能使用2个字节表示,所以encoding不变,值还是2。
当添加32768的时候,它不再能用2个字节来表示了(2个字节能表达的数据范围是-215~215-1,而32768等于215,超出范围了),因此encoding必须升级到INTSET_ENC_INT32(值为4),即用4个字节表示一个元素。

8总结

sds是一个灵活的字符串数组,并且支持直接存储二进制数据,同时提供长度和剩余空间的字段来保证伸缩性和防止溢出。

dict是一个字典结构,实现方式就是Java中的hashmap实现,同时持有两个节点数组,但只使用其中一个,扩容时换成另外一个。

ziplist是一个压缩链表,他放弃内存不连续的连接方式,而是直接分配连续内存进行存储,减少内存碎片。提高利用率,并且也支持存储二进制数据。

quicklist是ziplist和传统链表的中和形成的链表结果,每个链表节点都是一个ziplist。

skiplist一般有ziplist和zset两种实现方法,根据数据量来决定。zset本身是由skiplist和dict实现的。

inset是一个数字集合,他根据插入元素的数据类型来决定数组元素的长度。并自动进行扩容。

9 他们实现了哪些结构

字符串由sds实现

list由ziplist和quicklist实现

sortset由ziplist和zset实现

hash表由dict实现

集合由inset实现。
这里写图片描述
3、redis server结构和数据库redisDb

1 redis服务器中维护着一个数据库名为redisdb,实际上他是一个dict结构。

Redis的数据库使用字典作为底层实现,数据库的增、删、查、改都是构建在字典的操作之上的。

2 redis服务器将所有数据库都保存在服务器状态结构redisServer(redis.h/redisServer)的db数组(应该是一个链表)里:

同理也有一个redis client结构,通过指针可以选择redis client访问的server是哪一个。

3 redisdb的键空间

typedef struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;                 /* The keyspace for this DB */
    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires;              /* Timeout of keys with a timeout set */
    // 数据库号码
    int id;                     /* Database ID */
    // 数据库的键的平均 TTL ,统计信息
    long long avg_ttl;          /* Average TTL, just for stats */
    //..
} redisDb

这部分的代码说明了,redisdb除了维护一个dict组以外,还需要对应地维护一个expire的字典数组。

大的dict数组中有多个小的dict字典,他们共同负责存储redisdb的所有键值对。

同时,对应的expire字典则负责存储这些键的过期时间.
image

4 过期键的删除策略

2、过期键删除策略
通过前面的介绍,大家应该都知道数据库键的过期时间都保存在过期字典里,那假如一个键过期了,那么这个过期键是什么时候被删除的呢?现在来看看redis的过期键的删除策略:

a、定时删除:在设置键的过期时间的同时,创建一个定时器,在定时结束的时候,将该键删除;

b、惰性删除:放任键过期不管,在访问该键的时候,判断该键的过期时间是否已经到了,如果过期时间已经到了,就执行删除操作;

c、定期删除:每隔一段时间,对数据库中的键进行一次遍历,删除过期的键。

4、redis的事件模型

redis处理请求的方式基于reactor线程模型,即一个线程处理连接,并且注册事件到IO多路复用器,复用器触发事件以后根据不同的处理器去执行不同的操作。总结以下客户端到服务端的请求过程

总结

远程客户端连接到 redis 后,redis服务端会为远程客户端创建一个 redisClient 作为代理。

redis 会读取嵌套字中的数据,写入 querybuf 中。

解析 querybuf 中的命令,记录到 argcargv 中。

根据 argv[0] 查找对应的 recommand。

执行 recommend 对应的执行函数。

执行以后将结果存入 buf & bufpos & reply 中。

返回给调用方。返回数据的时候,会控制写入数据量的大小,如果过大会分成若干次。保证 redis 的相应时间。

Redis 作为单线程应用,一直贯彻的思想就是,每个步骤的执行都有一个上限(包括执行时间的上限或者文件尺寸的上限)一旦达到上限,就会记录下当前的执行进度,下次再执行。保证了 Redis 能够及时响应不发生阻塞。
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值