当前位置: 首页 > 科技观察

搞定Redis的数据存储原理,不要只知道Set和Get

时间:2023-03-17 21:29:43 科技观察

上一篇通过源码编译搭建了可调节的环境后,想必想进一步了解我的整体架构。当你熟悉了我的整体架构和各个模块后,遇到问题就可以直奔源头,直奔龙门,一笑破天。我的核心模块如图1-10所示。Clientclient,官方提供了一个用C语言开发的客户端,可以发送命令,性能分析和测试等。网络层事件驱动模型,基于I/O多路复用,封装了一个短小的,高性能的ae库,全称是一个简单的事件驱动编程库。在ae库中,我通过aeApiState结构适配了epoll、select、kqueue、evport这四种I/O多路复用的实现,使得上层调用者无法感知不同操作系统的I/O多路复用。多路复用的差异。Redis中的事件可以分为两类:一类是网络连接、读写事件;另一种是时间事件,是在特定时间触发的事件,比如定期进行rehash操作。命令解析执行层负责执行客户端的各种命令,如SET、DEL、GET等。内存分配和回收,为数据分配内存,提供不同的数据结构来存储数据。持久层提供了RDB内存快照文件和AOF两种持久化策略来实现数据的可靠性。高可用模块提供副本、哨兵和集群来实现高可用。监控统计,提供一些监控工具和性能分析工具,如内存使用监控、基准测试、内存碎片、bigkey统计、慢指令查询等。掌握整体架构和模块后,进入src源码目录并使用下面的命令执行redis-server可执行程序来启动Redis。./redis-server../redis.conf我会把每一个启动的服务抽象成一个redisServer,源码设置在server.h的redisServer结构体中。该结构包含数据库实例存储键值对、redis.conf文件路径、命令列表、加载的Module、网络监控、客户端列表、RDBAOF加载信息、配置信息、RDB持久化、主从复制、clientSide缓存、数据结构压缩、pub/sub、Cluster、Sentinel等一系列Redis实例运行所需的信息。结构体字段很多,就不一一列举了。一些核心领域如下。tructredisServer{pid_tpid;/*主进程pid。*/pthread_tmain_thread_id;/*主线程ID*/char*configfile;/*redis.conf文件的绝对路径*/redisDb*db;/*存储键值对数据的redisDb实例*/intdbnum;/*数据库数量*/dict*commands;/*当前实例可以处理的命令表,key是命令名,value是执行命令的入口*/aeEventLoop*el;/*事件循环处理*/intsentinel_mode;/*true表示作为哨兵实例启动*//*网络相关*/intport;/*TCP监听端口*/list*clients;/*连接到当前实例的客户端列表*/list*clients_to_close;/*要关闭的客户端列表*/client*current_client;/*客户端当前正在执行命令*/};数据存储原理其中,redisDb*db指针很重要,它指向一个长度为dbnum(默认16)的redisDb数组,它是整个存储的核心,就是我用来存储键值对的。redisDbredisDb结构定义如下。typedefstructredisDb{dict*dict;字典*过期;字典*blocking_keys;字典*ready_keys;字典*watched_keys;内部编号;长长avg_ttl;无符号长expires_cursor;列表*defrag_later;expires是两个最重要的属性。底层数据结构是字典,用于存储键值对数据和键过期时间。expires,底层数据结构是一个dict字典,里面存放的是每个key的过期时间。?MySQL:“Whyseparatestorage?”问得好,之所以分开存储是因为没有为每个key设置过期时间,不是key-value对的固有属性,虽然需要两次查找之后分离,可以节省内存开销。blocking_keys和ready_keys的底层数据结构是dict字典,主要用于实现BLPOP等阻塞命令。当client使用BLPOP命令阻塞等待list元素取出时,我会把key写到blocking_keys中,value就是被阻塞的client。当收到下一个PUSH命令执行时,我会先检查blocking_keys中是否有等待阻塞的key。如果存在,则将密钥放入ready_keys。Redis在接下来的事件处理过程中,会遍历ready_keys数据,从blocking_keys中取出阻塞的客户端响应。watched_keys用于执行watch命令,存放watch命令的key。idRedis数据库的唯一ID,一个Redis服务支持多个数据库,默认16个。avg_ttl用于统计平均过期时间。expires_cursor统计过期事件循环被执行的次数。defrag_later保存一个键列表,这些键被一个一个地进行碎片整理。slots_to_keys只在Cluster模式下使用,使用Cluster模式时,只能有一个数据库db0。slots_to_keys是一个数组,用于记录cluster模式下key和hashslots的映射关系。dictRedis使用dict结构保存所有的键值对(key-value)数据,是一个哈希表,所以key查询的时间复杂度是O(1)。所谓哈希表,我们可以把它类比成Java中的HashMap,它其实就是一个数组,数组的每一个元素称为一个哈希桶。dict结构的源代码定义在dict.h中。structdict{dictType*类型;dictEntry**ht_table[2];无符号长ht_used[2];长重新哈希;int16_t暂停哈希;signedcharht_size_exp[2];};ht_table[2],longrehashidx是三个非常重要的结构。类型存储函数,例如散列函数、键和值的副本;ht_table[2],长度为2的数组,默认使用ht_table[0]存储键值对数据。我会使用ht_table[1]进行渐进式reahsh操作。rehashidx是一个整数值,用来标记是否进行了rehash操作,-1表示没有进行rehash。如果正在执行rehash,则其值表示当前执行rehash操作的ht_table[1]中dictEntry数组的索引。pauserehash表示rehash的状态,大于0表示暂停rehash,小于0表示出错。ht_used[2],一个长度为2的数组,表示每个哈希桶中存储了多少个键值对实体(dictEntry)。值越大,哈希冲突的概率越高。ht_size_exp[2],每个哈希表的大小,即哈希桶的个数。关注ht_table数组。数组的每个位置称为哈希桶,存储所有键值对。每个哈希桶的类型是dictEntry。?MySQL:“Redis支持这么多数据类型,hash桶怎么存?”他的秘诀就在dictEntry。每个dict有两个ht_tables用于存储键值对数据和实现progressiverehash。dictEntry的结构如下。typedefstructdictEntry{void*key;union{//指向实际值的指针void*val;uint64_tu64;int64_ts64;双d;}v;//哈希表冲突生成的链表structdictEntry*next;void*metadata[];}dictEntry;*key指向键值对的键的指针,指向一个sds对象,键都是字符串类型。v是键值对的值,是一个联合(union)。当其值为uint64_t、int64_t或doubledigital类型时,不需要额外存储,这意味着有助于减少内存碎片。(为了节省内存,我心碎了。)当值是非数值类型时,用一个val指针存储。*next指向另一个dictEntry结构。多个dictEntry可以通过next指针连接成一个链表。从这里可以看出,ht_table是使用链地址的方式来处理键冲突的:当多个不同的键有相同的哈希值时,哈希表使用一个链表来连接这些键。哈希桶本身并不保存值,而是一个指向特定值的指针,从而实现了哈希桶可以存储不同数据类型的需求。redisObjectdictEntry的*val指针所指向的值其实是一个redisObject结构体,这是一个非常重要的结构体。我的key只能是string类型,value可以是String、Lists、Set、SortedSet、hashtable等数据类型,键值对的值被封装成redisObject对象,redisObject在server.h中定义。typedefstructredisObject{无符号类型:4;无符号编码:4;无符号lru:LRU_BITS;int重新计票;void*ptr;}robj;type:记录对象的类型,string,set,hash,Lis,SortedSet等,根据type来决定使用哪种数据类型,使用何种API操作。encoding:编码方式,表示ptr指向的数据类型的具体数据结构,即这个对象使用什么数据结构作为底层实现来保存数据。同一个对象使用不同的编码,内存占用有明显差异,内部编码对于内存优化非常重要。lru:LRU_BITS:LRU策略下最后一次访问对象的时间。如果是LFU策略,低8位表示访问频率,高16位表示访问时间。refcount:表示引用计数。由于C语言没有内存回收功能,Redis在自己的对象系统中加入了这个属性。当一个对象的引用计数为0时,表示该对象还没有被任何对象引用过。然后它可以被垃圾收集。ptr指针:指向对象底层实现数据结构,指向值的指针。如图1-11所示,是redisDb、dict、dictEntry、redisObejct的关系图:注意,一开始我只用哈希表ht_table[0]读写数据,ht_table[1]指向NULL。当这个哈希表的容量不足时,就会触发扩容操作,此时会创建一个更大的哈希表ht_table[1]。然后我将ht_table[0]的数据以progressiverehash的方式迁移到ht_table[1]。全部迁移完成后,我会修改指针,让ht_table[0]指向扩展后的哈希表,回收原来的哈希表,ht_table[1]再次指向NULL。