Redis的实现解读

一. 数据结构

我们知道redis有5种基本类型:string、list、hash、set、zset,我们来看一下他们是怎么实现的。

1. 简单动态字符串(Simple Dynamic String,SDS)

Redis中的可变字符串都是由SDS实现,比如键值对的键和值,list的键和list中的字符串等...。


SDS结构

可以看到它由三部分组成:

  • free(表示空闲字节数)
  • len(表示字符串长度)
  • buf(指向字符数组,存在占位符兼容C语言字符串,但它对用户、free、len不可见;并且buf中可能包含多余的空字节)

SDS的内存优化策略:
(1)空间预分配

  • SDS只在free空间不够时,才进行扩容
  • 扩容的SDS的长度小于1M时,分配的空间时len的2倍,如“redis”,会分配11个字节的buf(包含占位符);
  • SDS的长度大于1M时,每次多分配1M。

(2)惰性空间释放
SDS字符串变短时,并不马上缩短buf长度,而是通过free字段暂时保存着部分字节。

SDS的优点如下:

  • 获取长度的复杂度为O(1),适合于Redis高性能的场景
  • 通过free字段,我们可以杜绝缓冲区溢出
  • 减少字符串修改带来的内存重分配次数
  • 可以存储二进制信息
  • 兼容部分C字符串函数

2.链表

(1)在Redis中,链表应用于列表键、发布与订阅、慢查询、监视器。
(2)redis的list,是双向链表,且包含链表长度等信息。

3.字典

(1)在Redis中,字典使用时非常广泛的,Redis数据库的底层就是使用字典实现;字典也是hash、set的底层实现之一。(redis有0-15,16个数据库,默认连接0数据库,可以通过select 2 切换数据库)
(2)在Redis中,字典中哈希表的实现和JDK中HashMap的实现类似,数组用于保存键值对节点,当发生冲突时,采用链表法解决冲突。
(3)字典中包含两个哈希表ht0、ht1,一个多余的哈希表用于rehash。
(4)渐进式rehash,每次更新操作,都会对其所在的id位置进行复制到备份节点。(防止哈希表过大,复制影响性能)

4.跳跃表

跳跃表的查询效率可以和平衡树媲美,并且是实现更简单,所以不少程序使用跳跃表代替平衡树,它支持平均O(logN),最坏O(N)的复杂度。
(1)Redis采用跳跃表作为有序集合(zset)的底层实现之一(当zset中元素较多,或者成员是比较长的字符串的时候);
(2)跳跃表的实现:跳跃表由若干个跳跃表节点组成。


skiplist

5.整数集合

(1)整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值,且元素树木不多时,Redis就会采用整数集合作为集合键的底层实现。
(2)intset由一个int变量表示编码类型,一个int8数组表示存储的数据,实际存储的类型由编码决定,还有一个表示长度的int变量


intset

(3)intset只支持编码的升级,不支持降级。

6.压缩列表

(1)压缩列表是列表键和哈希键的底层实现之一;列表键或哈希键只包含少量元素时,会使用压缩列表;
(2)压缩列表的每个节点氛围三部分:前一节点长度、编码、content;
通过前一节点长度我们可以从任意位置遍历列表(可能是1字节,可能是5字节,比较trick的技巧),编码由编码前两位决定编码长度,content记录内容;
(3)连锁更新,删除或新增节点,导致list中的元素长度连续变化,造成连锁更新;不过连锁更新的性能虽然比较差,但是出现的几率是很小的。

7.对象

Redis没有采用底层的数据结构直接表示数据类型,而是基于这些数据结构创造了一个对象系统,这样做的好处有:

  • 采用对象封装,方便编写代码,方便使用;
  • 封装为对象,可以优化其在不同场景下的使用效率;
  • 实现了基于引用计数的内存回收机制;
  • 可以基于引用计数实现对象共享机制;
  • 对象包含访问时间,当内存超过maxmemory时,可以优先剔除未访问时间较长的键。

Redis中的对象都是由一个RedisObject的数据结构表示,与保存数据有关的是type、encoding、*ptr三个属性,分别表示类型、编码、实际数据的指针。


RedisObject

(1)类型
Redis数据库保存的是键值对,键是“字符串对象”,而值可能是“字符串对象”,“列表对象”,“哈希对象”,“集合对象”,“有序集合对象”。
type命令查看的实际是值的类型。
(2)编码
*ptr指向的数据所采用的数据结构由“编码”决定,每个Redis的数据对象都有多种编码方式。
编码为Redis提供了在性能和存储空间之前平衡的灵活性,在不同的场景下,可以使用不同的编码,以适应当前的场景。

1.字符串对象

字符串对象有int、raw、embstr三种编码。
一个比较特殊的是embstr和raw,他们都是SDS类型,但不同的是raw分两次创建redisObject和sdshdr结构,embstr一次分配内存创建,这样分配、释放内存更快,并且更容易利用缓存;另外,embstr是只读的。

2.列表对象

列表对象有ziplist、linkedlist两种编码。
满足2个条件使用ziplist,不满足则使用linkedlist:
(1)列表对象保存的所有字符串元素小于64字节;
(2)列表元素数目小于512个;

3.哈希对象

列表对象有ziplist、hashtable两种编码。
使用ziplist存储键值对时,键先入队,值再入队。
满足2个条件使用ziplist,不满足则使用hashtable:
(1)哈希表中保存的所有字符串元素小于64字节;
(2)哈希表中元素数目小于512个;

4.集合对象

集合对象的编码可以时intset或者hashtable。
满足2个条件使用intset,不满足则使用hashtable:
(1)集合中保存的元素都是整数值;
(2)集合中元素数目小于512个;

5.有序集合对象(zset根据元素的分值进行排序)

有序集合的编码可以是ziplist或者skiplist(该编码方式同时存在skiplist和hashtable两种数据结构)

满足2个条件使用ziplist,不满足则使用skiplist:
(1)有序集合中保存的所有元素成员小于64字节;
(2)有序集合中元素数目小于128个;

skiplist同时存在skiplist和hashtable两种编码,skiplist用于对有序集合进行范围操作,hashtable用于查找给定成员的分值。skiplist和hashtable共享相同元素的成员和分值,不浪费额外的内存。
同时使用这两种编码是为了兼顾随机查询和范围查询。

类型检查与命令多态

类型由值对象的type决定,多态选择由值对象的编码类型决定。


llen命令执行过程

内存回收

redisObject内部有一个参数refcount,记录该对象的引用数目。
引用计数为0时,对象所占内存会被回收。
object refcount B 可以得到对象的引用数目。

对象共享

redisObject可以通过refcount数字维护对象的共享。
redisObject会共享0-9999的字符串对象。

对象的空转时长

redisObject还有一个参数——lru,用来记录对象最后一次被访问的时间。
object idletime B可以得到对象的空转时长。

二. 单机数据库实现

1.数据库

1.Redis数据库概述

Redis数据库由redisServer数据结构内的redisDb数组保存。
Redis默认有16个数据库,可以通过select 0,... ,select 15来选择,默认连接的是数据库0。

2.Redis数据库的结构

  • Redis是一个键值对数据库,redisDb结构中实际是用一个字典dict存储所有的键值对,也就是我们平时使用的键值对。
  • redisDb结构中expires字典保存了数据库中所有键的过期时间。
  • 过期键删除策略:
    三种可能的策略:
    (1)定时删除:
    创建键的时候,创建定时器,到时间删除这个键,因为消耗CPU(需要用到redis的时间事件机制,复杂度O(n))
    (2)惰性删除:
    访问这个键的时候才看它的过期时间,过期了才删除,节省CPU,但是可能会占用大量内存,造成内存泄漏。
    (3)定期删除:
    每隔一段时间,执行一次删除过期键的操作,并通过限制删除操作的时长和频率减少对CPU的影响。
    Redis结合使用“惰性删除”和“定期删除”两种策略。
    其中定期删除,会在规定时间内,分多次遍历服务器中的各个数据库,随机检查一些键的过期时间,并删除其中的过期键。

持久化中,删除键的处理:
RDB不会保存过期的键;由RDB或AOF恢复数据的时候,也不会写入过期的键;一个键被删除后,会向AOF加DEL命令。
从服务器发现键过期也不会删除它,而是等待主服务器的DEL命令。

2.持久化

1.RDB持久化(Redis DataBase)

(1)RDB文件的创建与载入
Redis中有SAVE和BGSAVE两个命令可以生成RDB快照,其中SAVE使用当前进程会阻塞其他Redis命令,而BGSAVE会新起一个子进程。同时他们之间是互斥的。
Redis启动时,会检测RDB文件是否存在,存在的话就自动载入。
(2)间隔保存
Redis可以设置save参数,定制规则,定期的对Redis进行BGSAVE。(一个周期性操作)
(3)RDB存储的是实际数据压缩后的结果


RDB文件存储结构

2.AOF持久化(Append Only File)

(1)AOF存储的是客户端发来的命令;
(2)AOF时机
Redis就是一个事件循环,每个循环中,先处理文件事件,再处理时间事件,在结束循环之前,会调用flushAppendOnlyFile函数,考虑是否将aof_buf中的内容写入和保存到AOF文件里。
关于flushAppendOnlyFile的行为,有三种方式:

  • always:每次都进行同步
  • everysec:每秒进行同步(默认)
  • no:何时同步由操作系统决定


    Redis事件循环

    (3)AOF文件的载入
    RedisServer只能接收客户端的命令,所以,载入AOF文件需要建立一个伪客户端与Server通信。
    (4)AOF重写
    随着执行的命令越来越多,AOF日志会膨胀,BGREWRITE命令能够重写AOF日志,即新的日志与原日志执行结果相同,但所占空间更小(实际上后台创建一个子进程,直接读数据,变为命令)。

3.事件

Redis服务器是一个事件驱动程序,它包含两类事件:文件事件、时间事件。
文件事件:Redis服务器与客户端(以及其他Redis服务器)通信所产生的事件;
时间事件:Redis的一些操作需要在给定事件执行(比如serverCon),即对定时操作的抽象。

1. 文件事件

Redis文件事件处理器基于Reactor模式使用IO多路复用技术,单线程监听多个套接字;当发生网络事件时,根据事件类型,选择合适的文件事件处理器,处理该网络事件;
(1)“单线程”和“IO多路复用”保证了该通信模型的高性能;
(2)Reactor模式保证了单线程设计的简单性和扩展性;
(3)不同的事件由不同的处理器进行处理。


文件事件处理器

注:
Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。(以往,我认为Reactor模型必然是多线程的。)
Reactor模型的优势:
解耦、提升复用性、模块化、可移植性、事件驱动、细力度的并发控制等。

2. 时间事件

Redis中时间事件有两种:周期性时间事件,定时事件事件。
Redis目前只使用“周期性时间事件”,在Redis中就是serverCon函数。

实现:
Redis将所有的时间事件都放在一个无序链表上,每次遍历整个链表,执行所有到期的事件对应的处理器(实际上Redis只有serverCon一个事件,无序链表退化为指针,不影响性能)。

serverCon的实例:

  • 更新服务器各类统计信息
  • 清理数据库内过期键值对
  • 关闭、清理失效的连接
  • 尝试进行AOF或RDB持久化
  • 主从服务器之间的同步

3. 事件的调度与执行

当服务器未关闭时,循环执行如下步骤:

  • 限时等待文件事件到达
  • 处理到达的文件事件
  • 处理到达的时间事件(所以时间事件可能稍有延迟)


    文件事件和时间事件调度流程

4.客户端与服务器

三. 集群实现

1.复制

Redis可以使用SLAVEOF命令或者设置slaveof选项,让一个服务器去复制(replicate)另一个Redis服务器。

Redis 2.8以前的实现

Redis主从同步分为两个步骤:
(1)同步(SYNC)
从服务器向主服务器发送SYNC命令,主服务器通过BGSAVE生成一个RDB快照,发送给从服务器,从服务器从RDB快照中恢复数据。
(2)命令传播
同步完毕后, 主服务器会把新执行的写操作发送给从服务器,以保持二者的一致状态。
实际上,从服务器相当于主服务器的客户端。

缺陷:
主从服务器初次复制是没有问题的,但是如果主从服务断开连接,从服务器再次连上时,需要再次获得主服务器当前的RDB,这就可能导致,只丢失了一小部分数据,却耗费大量资源重新复制。

Redis 2.8及以后的实现

Redis2.8用PSYNC代替了SYNC,PSYNC会根据情况选择“全量同步”或者“部分同步”。
具体实现:在server端设置固定大小的缓冲区,并且每条语句在主服务器端都有对应的偏移量,从服务器端也记录当前的偏移量;重连时,若偏移量在缓冲区内,则进行“部分同步”,也就是把缓冲区内的更新语句发给从服务器;另外还要记录从服务器的id。(复制偏移量、复制积压缓冲区、从服务ID)

2.Sentinel

哨兵是Redis的高可用性解决方案,由一个或多个Sentinel组成的哨兵系统可以见识任意多个主服务器以及其下的从服务器;当主服务器下线时,会将其下的某个从服务器代替其为主服务器。(故障自动迁移)

  • Sentinel是一个运行在特殊模式下的Redis服务器,只是启动时的命令不同,并且在启动时指定了主服务器的地址端口。

  • Sentinel向主服务器发送INFO命令来获取从服务器信息,Sentinel每十秒向主服务器和从服务器发送INFO命令(命令连接);

  • Sentinel还会向主从服务器的指定频道发送信息,再从订阅这些主从服务器该频道上的信息,以此来获得其他Sentinel的信息(遇到本Sentinel的消息会丢弃);维护主服务器到Sentinel列表的映射(用于后续的主管下线和客观下线)。

  • 主管下线和客观下线。
    (1)主管下线:
    Sentinel会每秒向其他服务器(Sentinel、Master、Slave)发送Ping消息,当超过一定时间无响应,会判断该服务主管下线(各Sentinel时间期限配置可能不同)
    (2)客观下线
    Sentinel发现某服务下线,则通过向其他Sentinel发送查询命令,确认该服务是否下线,超过指定数目确认后,认为该服务客观下线。(概数目可配置)。
    (Sentinel的三个定时任务:INFO 、发送指定频道消息、Ping)

  • 领头Sentinel选择
    实际上是Raft算法的实现。

  • 故障自动转移
    (1)选取从服务器为主服务器,根据最近应答时间、复制偏移量、优先级、默认排序等规则选取
    (2)设置其他从服务器
    (3)设置重新上线的主服务器

3.集群

  • Redis启动后,添加其他节点进入集群;
  • Redis集群采用哈希槽分配数据到各个节点,总共16384个哈希槽;
  • Redis接收到一个命令,会查看这个key是否在自己的哈希槽内,在就返回,不在就返回一个Moved响应,让客户端到合适的节点取数据;
  • 集群中主节点下线后,从节点能够升级为主节点,代替主节点之前的作用(也是Raft协议实现)。

至此,我们看到了Redis使用的四种架构:Redis单机、Redis主从、Redis Sentinel、Redis集群。

四. 其他功能的实现

1. 发布订阅功能

可以在Redis发布订阅“频道”,也可以订阅“模式”(频道是一个字符串键,模式则是一个正则匹配表达式,用来匹配符合条件的频道)。下面来看下具体的实现:
(1)订阅频道
SUBSCRIBE命令可以订阅一个或多个频道,UNSUBSCRIBE可以退订一个或多个频道;具体的实现是,在pubsub_channels字典上查看该频道是否存在,若不存在则在插入key为频道名和value为空链表的元素,并在链表上加入该客户端信息;若存在则直接找到对应的链表加入客户端信息(退订相反操作)。
(2)订阅模式
PSUBSCRIBE订阅模式,PUNSUBSCRIBE退订模式。
Redis中存在pubsub_patterns的链表用于保存模式,链表中每个元素存储了模式以及该模式对应的客户端链表。
(3)发送消息
Publish 发送消息,会将对应的频道以及匹配到的模式,链表上的客户端得到,然后向他们发送该消息。(会遍历模式链表)
(4)查看订阅信息
通过PUBSUB命令可以查看频道与模式的订阅信息。

2. 事务功能

(1)Redis事务

    MULTI开启事务
    EXEC执行事务
    DISCARD放弃事务
    WATCH实现乐观锁,当被WATCH的值改变事务提交失败

(2)Redis事务的实现
如下图,当服务端的Client对象处于事务状态时,会特殊处理上述四个命令,遇到其他命令则先入队列,而不提交。


Redis事务的实现

(3)Redis事务总是具有ACI特性,当运行在AOF模式下,且appendfsync为always时,也具有D特性。

3. 慢查询日志

SLOWLOG GET 可以查看Redis的慢查询日志(链表实现)。
可以设置时间阈值、以及最多保存的日志条数。

4. Lua脚本

Redis2.6引入了对Lua脚本的支持,redis客户端可以执行Lua脚本,原子的执行任务。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容