代码片段: SET msg "hello world"
红色: 专有名词
蓝色: 特有特点
绿色: 用途 源码注释
黄色 书名 重点语句
紫色 属性 与其它知识的联动
本文共计 44238字
在Redis官网里有一个官方的交互式教程 https://try.redis.io/
可以模拟Redis环境,还是建议大家先安装Redis环境在看本文
文章目录
Redis数据库里面每个减值对(key-value)都是由对象(object)组成
- 键 总是一个字符串对象(string object)
- 值 可以是字符串对象 队列对象(list) 哈希对象(hash) 集合对象(set) 有序集合对象(sorted set) 这五种对象中的一种
在Redis官网有一个线上交互演示 点击进入
输入以下命令在数据库中创建一个键为字符串对象 值也为字符串对象的键值对
SET msg "hello world"
回车
key为msg
value为hello world
的键值对就被存储在Redis中了
现在来获取它
GET msg
回车
成功拿到值"hello world"
小总结: 对应存储字符串类型的键值对 Redis的命令是
SET key value
GET key
在Redis数据库创建一个 键为字符串 ,值为列表对象的键值对:
RPUSH numbers 1 2 3 4
根据常用的Redis命令可以在这里愉快的玩耍
前言
Redis的优点
Redis是内存型的数据库
Redis的工作模式为单线程,不需要线程间的同步操作。Redis采用单线程主要因为其瓶颈在内存和带宽上,而不是CPU
Redis中key-value的value不仅可以是字符串,也可以是复杂的数据类型,如链表、集合、散列表等。
Redis支持数据持久化,可以采用RDB、AOF、RDB&AOF三种方案。计算机重启后可以在磁盘中进行数据恢复。
Redis支持主从结构,可以利用从实例进行数据备份
Redis高性能的原因
Redis是以出色性能著称,Redis高性能的主要原因是:
- Redis是基于内存的数据库,内存的读写速度快
- Redis单线程服务(运行Redis Server肯定不止一个线程,但只有一个线程处理网络请求),避免了不必要的上下文切换
java8新特性中的stream流,有并行流和串行流,在低数据量的情况下,并行由于上下文的切换速度反而没有串行快
- Redis使用多路I/O复用模型,高效处理大量并发连接
- Redis中数据结构是专门设计的,增删该查简单
Redis到底是单线程还是多线程
redis4.0前是 单线程的
redis4.0时,引入了多线程, 额外的线程只是用于后台处理,例如 删除对象
核心流程(接收命令,解析命令,执行命令)依然是单线程
redis6.0 多线程用于I/O阶段,接收命令 和 写回命令阶段
执行命令 是单线程
第一部分:数据结构与对象
是Redis键值对的数据类型
而数据类型底层实现是数据结构
Redis的各种类型数据结构也设计良好:
简单稳定不容易溢出 的字符串结构sds
开始排序查找 的跳跃表skiplist
节约内存 的压缩列表ziplist
基于Hash表实现 的字典dict
基于链表list和压缩列表ziplist实现的快速列表qucklist
String(字符串)对象,
List(列表)对象,
Hash(哈希)对象,
Set(集合)对象,
Zset(有序集合)对象
图为:《Redis设计与实现》这本书将的是Redis3.0
图为:Redis键值对对数据库的全景图(Redis对象和数据结构的关系),
一简单动态字符串SDS
Redis构建了简单动态字符串SDS(simple dynamic string )的抽象类型,并将SDS用作Redis的默认字符串表示
- C语言传统字符串作为字符串字面量(string literal)用于无须修改的字符串值地方,
如 打印日志:
- Redis需要可以修改的字符串值时,就会使用简单动态字符串SDS
如客户端执行命令:
SET msg "hello world"
执行这个命令后,
键值对的 键 是一个保存着字符串"msg"的SDS
键值对的 值 是一个保存着字符串"hello world"的SDS
SDS除了用来保存数据库字符串外,还用与缓存区(buffer)即AOF缓冲区
1.1SDS的定义
通过这里进入Redis源码的下载https://github.com/huangz1990/redis-3.0-annotated
下载好后找到以下目录
找到这个函数:
这个函数表示了一个SDS值的结构
在举个例子
SET msg "Redis"
在Redis中的存储就是
图为:SDS结构示例
- len属性值为5,表示这个SDS保存的字符串长度为5
- free属性值为0,表示这个SDS没有剩余空间
- buf属性是一个char类型数组 ,最后一个字节保存了空字符 ‘\0’
图为:带有未使用空间的SDS
SDS遵循C字符串以空字符结尾的惯例
这个空字节不计算在SDS的len属性上,
并且为空字符分配空间
添加空字符到字符串尾等操作是SDS函数自动完成的
遵循空字符结尾让SDS可以直接重用C字符串函数库里的函数
1.2SDS与C字符串的区别
首先C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组最后一个元素为空字符 ‘\0’
而SDS的结构如上.
C字符串和SDS之间的区别:
1.2.1 SDS获取字符串长度的复杂度为O(1)
.
SDS通过其属性 len 记录了字符串的长度
而C语言字符串的获取字符串长度需要遍历整个字符串,O(N)
1.2.2 SDS杜绝了缓存区溢出问题
.
缓存区溢出问题:
在内存中紧邻的两个字符串,将第一个字符串变成更长长度时没有为其分配足够的空间,导致修改后期内容溢出到第二个字符串的空间上
.
SDS API需要对SDS修改时,会检查SDS空间是否满足所需要求
不满足时,SDS API会自动将SDS的空间扩展合理的大小后才会修改SDS
SDS不需要手动修改大小,有不会出现缓存区溢出问题
1.2.3 减少修改字符串带来的内存重分配的次数
由于
C字符串的长度 = 底层数组长度+ 1
的关联性
所以 每次修改C字符串,需要对此字符串数组内存重新分配
- 进行拼接操作(append),不 扩展 底层数组的空间大小
会产生 缓冲区溢出 (如1.2.2 SDS杜绝了缓存区溢出问题)- 进行截断操作(trim),不 释放 不再使用的空间
会产生 内存泄漏 问题而内存的重新分配是需要时间的,
为了避免修改频繁的发送对性能造成的影响
SDS通过空间预分配 和 惰性空间释放 两个优化策略
- 1 空间预分配
当SDS字符串 增加 时会为SDS分配额外的空白空间
即当SDS的长度(即len属性的值)小于1MB,程序会自动分配将free的值等于len的值
举个例子:
SDS的len修改后变成13,那么free的值也会变成13 ,buf 的长度变成13+13+1=27字节当SDS的长度(即len属性的值)大于1MB,系统会自动分配lfree的值等于1MB
举个例子:
SDS的len修改后变成30MB,那么程序会分配free的值为1MB,buf的长度变成 30MB+1MB+1byte通过空间预分配策略,Redis减少字符串增长操作时内存重新分配次数
即将连续增长N次字符串所需的内存重分配次数降低为最多N次
- 2惰性空间释放
当SDS字符串 缩短 时,free属性将需要释放的字节数量记录下来,
避免了缩短字符串时所需的内存重分配,为将来有可能的增长操作优化
SDS有对应的API帮我们真正释放内存
1.2.4二进制安全
C语言字符串中不能包含空字符,故而只能保存文本数据无法保存图片等二进制数据
SDS通过其属性buf值判断字符串是否结束,而不是以空字符判断
使得SDS可以保存二进制数据
1.2.5SDS兼容部分C字符串函数
由于SDS和C字符串一样遵循以空字符结尾的惯例
所以SDS可以重用一些C的函数
1.3SDS的API
二链表
链表与数组的区别是:
链表节点在内存的存储位置不同于数组元素
链表是不连贯的 而 数组是连贯的
链表增删快 查询慢
数组增删慢 查询快
链表提供了
- 高效的节点重排功能
- 顺序性的节点访问方式
- 增删节点调增链表长度
2.1链表和链表节点的实现
通过这里进入Redis源码的下载https://github.com/huangz1990/redis-3.0-annotated
下载好后找到以下目录
图为:每个链表节点的结构
链表通过 prev 和 next 指针组成双端链表
图为:双端链表示意图
图为: 每个链表由list结构图
2.2链表总结
链表优点:
- 链表在Redis中被广泛应用 列表键 发布与订阅 慢查询 监视器
- 每个链表节点由一个 listNode结构表示,且每个节点都有 前后指针,而且这两个指针都可以指向 NULL,所以链表是无环链表
- list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1);
- list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1);
- Redis链表可以保存不同类型的值
链表缺点:
- 链表节点内存不连贯,也就意味着无法利用CPU缓存,能很好利用 CPU 缓存的数据结构就是数组,因为数组的内存是连续的,这样就可以充分利用 CPU 缓存来加速访问。
- 保存一个链表节点需要链表节点结构头的分配,内存开销较大。
所以
Redis3.0在List对象数据量少,会采用 压缩列表作为底层数据结构的实现,其优势为节省内存空间,且是内存紧凑型数据结构
不过,压缩列表存在性能问题(具体什么问题,下面会说),
所以 Redis 在 3.2 版本设计了新的数据结构 quicklist,
并将 List 对象的底层数据结构改由 quicklist 实现。
然后在 Redis 5.0 设计了新的数据结构 listpack,
沿用了压缩列表紧凑型的内存布局,
最终在最新的 Redis 版本,
将 Hash 对象和 Zset 对象的底层数据结构实现之一的压缩列表,
替换成由 listpack 实现。
六压缩列表
是为节约内存开发的顺序型结构,
优势:
占用一块连续的内存空间,可以利用CPU缓存
缺点:
不能保存过多元素,会降低查询效率
Redis 针对压缩列表在设计上的不足,
在后来的版本中,
新增设计了两种数据结构:
quicklist(Redis 3.2 引入) 和 listpack(Redis 5.0 引入)。
这两种数据结构的设计目标,
就是尽可能地保持压缩列表节省内存的优势,
同时解决压缩列表的「连锁更新」的问题。
三hash字典
是一种保存键值对(key-value pair)的抽象数据结构
在字典中 一个键(key)和一个值(value)进行关联,这些关联的键和值称为键值对
字典中每个 键是独一无二 的
3.1字典的实现
Redis字典使用哈希表 作为底层实现
一个哈希表里面可以有多个 哈希表节点
每个哈希表节点就保存了字典中的一个键值对
3.1.1哈希表
下图为一个空的哈希表
3.1.2哈希表节点
每个dictEntry结构都存储着一个键值对
下图为存储索引值相同的两个键
3.1.3字典
下图为:没有进行rehash的字典
3.2哈希算法
当要将一个新的键值对添加到字典里面时,
- 根据键计算出哈希值和索引值
hash = dict ->type -> hashFunction(key);
使用字典设置的哈希函数,计算key的哈希值
- 根据索引值将 包含新键值对的哈希表节点放到哈希表数组的指定索引上
index = hash & dict ->ht[x].sizemask;
使用哈希表的sizemask属性和哈希值,计算出索引值
ht[x]可以是ht[0]也可以是ht[1]举个例子
将 为 k0和 v0 的键值对,添加到字典里
程序先执行
hash = dict ->type -> hashFunction(k0);
计算出键k0的哈希值,假设 hash=8
在执行
index = hash & dict ->ht[0].sizemask = 8 & 3 =0;
表示键k0的索引值0,
添加k0-v0键值对后的字典
3.3解决键冲突
当多个键被分配到哈希表数组的同一索引上, 称为 键发生了冲突(collision)
Redis的哈希表使用链地址法(separate chaining)来解决键冲突
解决哈希冲突的常见2中方法 ,链地址法 和 开放寻址法
3.3.1链地址法
每个哈希表节点都有一个next指针,
多个哈希表节点可以用next指针构成一个单向链表,
被分配到同一个索引上的多个节点可以用这个单向链表连接起来
举个例子
哈希表用已有键k1, 如图
现在将k2插入,但是k2计算出的索引值和k1相同
使用 链地址法,
用next指针将键k2 和键k1 所在的哈希表节点连接
由于哈希表节点组成的链表没有指向链表表尾的指针,
为了性能,系统总是将新节点添加到链表的 表头位置
Java中的hashMap也是链地址法解决hash冲突的3.3.2开放寻址法
从发生碰撞的单元起,从哈希表中寻找一个空闲单元存储碰撞元素
开放寻址法需要的表长度要大于等于所存放的元素数量,适用于装载因子>>小的表
缺点:
不能真正删除
点击进入开放寻址法详解
3.4rehash哈希表扩容与收缩
扩展和收缩哈希表的工作通过执行rehash重新散列操作完成
Redis对字典的哈希表执行rehash的步骤如下
-
对字典的ht[1]哈希表分配空间,
其大小取决于要执行的操作
与
ht[0]里的键值对数量(即used属性的值)
让博主想到了Java中的CopyOnWriteArrayList集合就是这样实现线程安全的,点击进入 -
将ht[0]保存的键值对rehash(重新计算哈希值和索引值 )到ht[1]上面;
-
ht[0]数据都迁移到ht[1]后,
释放ht[0],
将ht[1]设置为ht[0],
并创建一个新的ht[1]以备下次rehash
让博主想到了Java中的hashMap1.7的扩容头插法
3.5渐进式rehash哈希表扩容与收缩
如果ht[0]保存的键值对数量过大,一次性将这些键值对全部rehash会影响性能
渐进式rehash的步骤
- List item为ht[1]分配空间
- 在字典中维护 索引计数器变量,初始值为0
- rehash重新计算哈希值和索引值并数据迁移后,索引计数器值+1
- 当所有数据都迁移后, 索引计数器值设为-1
3.6rehash触发条件
这个和 负载因子有关
触发rehash的条件主要有两个
- 当负载因子大于等于1,且Redis没有执行
bgasve
命令或者bgrewiteaof
命令(即没有执行RDB快照或者没有执行AOF重写),就会进行rehash操作 - 当负载因子大于等于5,不管有没有执行RDB快照或AOF重写,都强制rehash操作
3.7字典总结
Redis字典用哈希表作为底层实现
每个字典带有两个哈希表, 一个crud使用,一个rehash时使用
哈希表使用 链地址法解决键冲突,新加入的冲突键在链表的头部
对哈希表扩展或收缩时,程序将现有哈希表包含的所有键值对rehash对新哈希表里,此过程不是一次性完成的,而是渐进式的完成
四跳跃表
跳跃表是一种有序数据结构
每个节点维持多个指向其它节点的指针
从而达到快速访问的目的.
Redis使用跳跃表作为:
- 有序集合键Zset的底层实现之一,另一个是 哈希表
- 集群节点中用作内部数据结构,
跳跃表的实现有两个结构定义
zskiplistNode结构表示跳跃表的节点
zskiplist结构保存跳跃表节点的相关信息(节点数等)
五整数集合
当集合只包含整数值元素,集合元素数量不多时,Redis会使用整数集合作为集合键的底层实现
整数集合底层为数组,这个数组有序 无重复,且会根据新添加元素的类型改变数组类型
升级操作使数组更灵活,且节约内存
支持升级,不支持降级
整数集合每个元素都是contents数组的数组项,各个项在数组按值从小到大排序,数组中不包含重复项
5.1升级
将一个新元素添加到整数集合里面,新元素类型比整数集合现有所有元素类型长时,整数集合需要进行升级
- 根据新元素类型,扩展整数集合底层数组的大小
- 将数组中所有元素转换成新元素相同类型
- 将新元素按照顺序添加到底层数组里
升级的好处:
节省内存 提示灵活性
七对象
Redis基于数据结构创建了对象系统:
字符串对象string
列表对象list
哈希对象hash
集合对象set
有序集合对象zset
每个键是一个对象
每个值是一个对象
Redis共享值为0到9999的字符串对象
7.1内存回收
Redis构建了一个引用计数来实现内存回收机制
- 创建新对象时,引用计数值初始化为 1
- 被一个新程序使用时,引用计数值增 1
- 不在被程序使用时,引用计数值减 1
- 引用计数值为0 时,内存被释放
让博主想到了JVM的引用计数法点击进入JVM回收
7.2对象共享
对象共享: 键A创建了一个包含正整数值100的字符串对象作为值对象
键B也要创建一个同样保存了整数值100的字符串,
那么Redis会让键B和键A共享同一个值对象
对象共享节约内存
让多个键共享同一个值对象需要执行:
- 将键指针指向现有的的值对象
- 被共享的值对象的引用计数增1
注意:Redis不共享包含字符串的对象:
由于程序先检查给定的共享对象和键想创建的对象是否完全相同,
只有在完全相同时才会共享
而共享对象保存的值越复杂,
验证共享对象和目标对象是否完全相同复杂度越高
- 共享对象保存正整值的字符串对象 ,验证复杂度为 O(1)
- 共享对象保存字符串值的字符串对象,验证复杂度为O(N)
- 共享对象保存多个值对象,比如列表对象或哈希对象,验证复杂度为O(N2)
所以Redis只对包含整数值的字符串对象共享
quicklist
在Redis3.0之前,List对象的底层数据结构是双向链表或者压缩列表
在Redis3.2时,List对象的底层改由quickList数据结构实现
其实quickList 就是 双向链表 + 压缩列表 的组合
因为quickList就是一个链表,而链表每个元素又是一个压缩列表
回顾一下压缩列表
压缩列表通过紧凑型的内存布局节省了内存开销,但由于其结构设计,如果保存元素数量增加,或元素变大了,压缩列表有 连锁更新的风险,导致性能下降
而
quickList的解决方法:
通过控制每个链表节点中压缩列表的 大小或元素个数,来规避连锁更新的问题,
因为压缩列表元素越少 连锁更新 带来的影响越小
从而提供了更好的访问性能
quicklist 结构设计
图为:quickList的结构体
图为:quickListNode的结构体
quickListNode结构体包含了
前一个节点 和 下一个节点,这样每个quickListNode形成双向链表
但 链表节点元素是保存了 一个 压缩列表
所以quickListNode有个指向压缩列表的指针
图为:quickList数据结构
quickList添加元素
向quickList添加元素时,不是新建一个链表节点
而是
检查插入位置的 压缩列表是否能容纳该元素
如果能 直接保存到quickListNode结构里的压缩列表中
如果不能 才新建一个新的quickListNode结构
quickList控制quickListNode结构里的压缩列表的大小或元素个数, 规避 潜在的 连锁更新的风险,
但没有完全解决这个问题
listpack
由于quickListNode还是用了压缩列表保存元素,而 压缩列表连锁更新是由于压缩列表的结构设计
于是
Redis在5.0新设计了一个数据结构 listpack,来替代压缩列表
它最大特点是 listpack 中每个节点不再包含前一个节点的长度了,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。
listpack结构设计
同压缩列表一样用一块连续的内存空间来紧凑地保存数据,为了节省内存开销,listpack节点采用不同的编码方式保存不同大小的数据
图为:listpack的数据结构
主要包含三个方面内容:
encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
data,实际存放的数据;
len,encoding+data的总长度;
listpack没有压缩列表中记录前一个节点长度的字段
只记录当前节点的长度,当向listpack加入新元素时
不会影响其他节点的长度字段变化
从而避免了压缩列表的连锁更新问题
第二部分:单价数据库的实现
八数据库
- Redis服务器所有数据库保存在 redisServer.db数组汇总,默认16个,可通过redisServer.dbum属性设置
- 命令
select index
是客户端修改目标数据库指针,指向redisServer.db数组不同元素切换不同数据库 - 键总是字符串对象 ,值可以是 字符串对象、哈希表对象、集合对象、列表对象、有序集合对象
- 数据库有 dict字典(负责保存键值对)和 expires字典(负责保存键的过期时间) 构成
- expires字典 键指向数据库键,值是毫秒单位的时间戳
- 通过使用惰性删除和定期删除来删除键
- 执行
save
和bgsave
命令产生的新RDB文件不会包含过期键 - 执行
bgrewriteaof
命令产生的重写AOF文件不会包含过期键 - 过期键被删除后,服务器会 追加
DEL
命令到现有AOF文件尾,显式删除过期键 - 主服务器删除过期键后,向从服务器 发送 DEL命令,显式删除过期键
- 从服务器发现过期键 不会 自作主张删除,而是等待主服务器DEL命令,这样统一,中心化的过期键删除策略保证主从服务器数据的一致性.
8.1服务器中的数据库
每个RedisDB结构代表一个数据库
RedisServer中的dbum属性表示初始化服务器时创建多少个数据库;
dbum属性 默认是 16
图为:初始化服务器时,创建的数据库
默认客户端使用0号数据库,切换数据库的命令:
8.2数据库键空间
Redis是一个键值对(key-value pair)数据库服务器,服务器中每个数据库都由一个redis.h/RedisDB结构表示,
其中
redisDb中的 dict属性保存所有键值对,将dict字典称为键空间(key space)
例如在官方交互数据库中执行以下命令:
SET msg "hello world"
RPUSH alphabet "a" "b" "c"
HSET book name "Redis in Action"
HSET book author "Josiah L. Carlson"
HSET book publisher "Manning"
数据库的键空间如图:
8.2.1添加新键
继续执行以下命令:
SET date "2021.11.2"
那么在键空间中添加date键后如图:
8.2.2删除键
删除数据库中的键,就是在键空间中删除键所对应的键值对对象
继续执行以下命令:
DEL book
DEL date
删除键值对book及键值对date的键空间如图:
8.2.3更新键
对数据库键更新,本质上是 对键空间中 键对应的值对象更新
继续执行以下命令:
SET message "blah blah"
键message原来的值对象被更新成新值
键空间如图:
8.2.4读写键空间时的维护操作
Redis对数据库进行读写时,服务器不单单对键空间执行指定的读写操作
还会执行一些 额外的维护操作
- 读取一个键后,服务器根据键是否存在来
更新服务器键空间命中(hit)次数 或 不命中次数(miss)次数,
以在INFO stats
命令的keyspace_hits属性和keyspace_misses属性查看. - 读取一个键后,服务器会更新键的LRU时间,这个值用于计算键的闲置时间,使用
IBJECT idletime <key>
命令可以查看键key的闲置时间 - 读取时发现该键过期,服务器会先删除该键(下文 8.3.2过期键删除策略<–点击跳转)
- 服务器每修改一个键,对 脏键计数器(dirty)的值增1,这个计数器会触发服务器的持久化以及复制操作
8.3设置键的生存时间或过期时间
通过RXPIRE
命令或PEXPIRE
命令,客户端为某个键设置过期时间
服务会自动删除生存时间为0的键:
执行以下命令:
SET key value
EXPIRE key 5
8.3.1设置过期时间
Redis有4个命令设置键的生存时间
- expire命令用于将键key的生存时间设置为ttl秒
- pexpire命令用于将键key的生存时间设置为ttl毫秒
- expireat命令用于将键key的过期时间设置为timestamp时间戳秒
- pexpireat命令用于将键key的过期时间设置为timestamp时间戳毫秒
底层执行的都是expireat
redisDb结构中expires字典保存了数据库所有键的过期时间,即过期字典:
- 过期字典键指向 数据库某个键对象
- 过期字典值是 long类型整数,保存了键所指向的数据库键的过期时间
图为:带有过期字典的数据库例子
8.3.2过期键删除策略
- 定时删除:在设置过期时间时创建定时器,在键的过期时间来临时对键删除操作
- 惰性删除:放任键过期不管,每次从键空间获取键时,检查是否过期,过期删除键,没过期返回键
- 定期删除:每隔一段时间,数据库检查一次,删除部分过期键
定时删除和定期删除为主动删除策略
惰性删除为被动删除策略
定时删除
对内存友好,定时删除保证过期键尽可能被删除,同事释放过期键的内存
对CPU不友好,在CPU紧张时会对服务器响应时间和吞吐量影响
惰性删除
对CPU友好:只有在取出键时才会过期检查
对内存不友好,只要过期键不被查询,就不会被删除
定期删除
定期删除每隔一段时间执行一次删除过期键操作,限制删除操作执行的时长与频率降低对CPU 的消耗
定期删除策略难点在 删除操作执行的时长与频率
Redis采用的 是 惰性删除,定期删除两种策略
8.3.3AOF、RDB对过期键的处理
生成RDB文件
执行save
命令,或者执行bgsave
命令
创建一个新的RDB文件
不会保存已过期的键
载入RDB文件
过期的键对载入RDB文件的
主服务器会被忽略
从服务器会继续保存,然而主从服务器数据同步时,从服务器数据被清空,所以过期键对载入RDB文件的从服务器也不会有影响
AOF文件写入
当服务器以AOF持久化运行时,某个键过期
且未被惰性删除或者定期删除,AOF文件不会有任何动作
被惰性删除或者定期删除后,AOF文件追加(append)一条DEL命令,显式记录
如果客户端使用GET message
命令
访问过期键message
服务器会执行以下动作
- 从数据库删除键message
- 追加一条DEL message命令到AOF文件
- 向执行GET 命令的客户端返回空回复
AOF重写
过期的键不会被写入
8.3.4复制模式下对过期键的处理
- 主服务器删除过期键后,显式向从服务器发送DEL命令
- 从服务器执行客户端发送的读命令,碰到过期键也不会删除,而是忽视过期
- 从服务器只有收到主服务器发的DEL命令才会删除过期键
保证数据一致性
九RDB持久化
9.1RDB文件的生成
Redis是内存数据库,一旦服务器退出,Redis数据库状态不见
所以
Redis提供RDB持久化,将Redis内存中的数据库保存在磁盘中
RDB持久化功能生成RDB文件是经过压缩的二进制文件,
RDB文件用于保存和还原Redis服务器中所有的键值对数据
SAVE
BGSAVE
都可以生成RDB文件
-
SAVE
命令会阻塞服务器,直到RDB文件创建完成
在服务器进程阻塞时,服务器无法处理任何请求 -
BGSAVE
命令会派生子进程,子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求
创建RDB文件工作由 rdb.c/rdbSave函数完成,SAVE命令和BGSAVE命令会以不同的方式调用此函数
图为:
SAVE
命令 和BGSAVE
命令调用rdb.c/rdbSave函数时的伪代码
9.2RDB文件的载入
载入RDB文件是在服务器启动时 自动执行 的
Redis服务器启动时打印的日志记录,第二条日志 就是服务器在成功载入RDB文件之后打印的:
图为:Redis服务器启动时打印的日志记录
服务器在载入RDB文件期间,会一直阻塞,直到工作完成
9.3自动间隔性保存
Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE
命令
通过save选项设置多个保存条件,只要任意一个条件满足,服务器会执行BGSAVE
命令
例子:想服务器提供以下配置:
save 900 1
save 300 10
save 60 1000
那么满足以下三个条件中的任意一个,BGSAVE
命令就会被执行:
- 服务器在900秒之内,对数据库进行了至少1次修改
- 服务器在300秒之内,对数据库进行了至少10次修改
- 服务器在60秒之内,对数据库进行了至少10000次修改
举个例子,以下是Redis服务器在60秒之内,对数据库进行了至少10000次修改后,服务器自动执行BGSAVE
命令时打印的日志
十AOF持久化
AOF文件通过保存所有**修改数据库的写命令**请求来记录服务器的数据库状态,
AOF文件所有命令都以Redis命令请求协议的格式保存
命令请求 先保存到<font color=red>AOF缓冲区</font>,之后定期写入到AOF文件
服务器只有载入并重新执行保存在AOF文件中的命令,就可以还原数据库本来的状态
AOF重写可以产生一个新AOF文件,这个新文件和原来AOF文件所保存的数据库状态一样,但体积更小
执行`BGREWRITEAOF`命令时,Redis服务器会维护一个<font color=red>AOF重写缓冲区</font>,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令..当子进程完成创建新AOF文件工作之后,服务器会重写缓冲区中所有内容最加到新AOF文件的末尾,使得新旧两个AOF文件保存的数据库状态一致,最后服务器用新的AOF文件替换旧的AOF文件,来完成AOF文件重写操作
10.1什么是AOF持久化
AOF持久化是保存Redis服务器 执行的写命令记录 来数据库的状态
图为
RDB持久化是将键值对保存到RDB文件
AOF持久化是将服务器执行SET、SADD、RPUSH命令保存到AOF文件
10.2AOF文件载入
由于AOF文件包含了重建数据库状态所需的写命令,所以服务器只要读入并执行一遍AOF文件中的 写命令就可以还原服务器关闭之前的数据库状态
Redis读取AOF文件并还原数据库状态的详细步骤如下:
注意:因为Redis命令只能在客户端上下文中执行,
而载入AOF文件时使用的命令来源于AOF文件而不是网络连接,
所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令
下文内容
11.4AOF文件的伪客户端
图为:Redis读取AOF文件并还原数据库状态的详细步骤
10.3AOF与RDB区别
10.3AOF重写
问题出现:
随着服务器运行时间的流逝,AOF文件会越来越大,导致AOF文件还原所需时间越长.
解决方案
为了解决AOF文件体积膨胀问题,Redis提供AOF文件重写(rewrite)功能.
创建一个新AOF文件替代现有AOF文件,新AOF不会包含任何浪费空间的冗余命令,所以新比旧体积小,
实现原理
AOF文件重写(rewrite)的原理是: 重新从数据库中读取最新的键的值,用一条命令代替保存在原AOF文件中的命令
新问题的出现:
AOF重写时,开启子线程来进行大量写入操作,同时服务器进程继续执行处理命令的请求,执行新命令后如何保证AOF文件和数据库保存的状态一致呢?
解决AOF与现数据库一致
Redis设置AOF重写缓冲区,
Redis执行一个命令同时将这个写命令发送到AOF缓冲区和AOF重写缓冲区,
AOF缓冲区
的内容定期写入和同步AOF文件,AOF重写缓冲区
是从创建子进程开始,服务器执行的写命令被记录到AOF重写缓冲区中当子进程完成AOF的重写后, 子进程发送标记到父进程,
继续执行AOF重写缓冲区中的内容
对新的AOF文件进行改名,原子覆盖现有的AOF文件,完成新旧替换
下文
13.2.6将AOF缓冲区内容写入AOF文件
介绍了 服务器如何通过 serverCron函数 将AOF缓冲区内容写入AOF文件里
十一Redis事件
对应Redis设计与实现12事件
博主看不懂,鸽了鸽了
十二客户端
Redis是一对多的服务器,
即
一个服务器可以与多个客户端建立网络连接
每个客户端可以向服务器发送命令请求,
而服务器接收并处理客户端发送的命令请求,并向客户端返回命令回复
Redis服务器使用单线程单进程处理命令请求
服务器为这些客户端建立了相应的redis.h/redisClient结构
12.1普通客户端的创建
客户端使用connect函数连接服务器时,
服务器将新的客户端状态添加到服务器状态结构 clients链表结尾
例子: 如果当前有c1和c2两个普通客户端在连接服务器当一个新普通客户端c3连接到服务器后,服务器会将c3对应的客户端状态添加到 clients链表的末尾
如图:
12.2普通客户端的关闭
一个普通客户端可以因为多个原因被关闭:
- 客户端进程退出或被杀死,客户端与服务器之间的网络连接将被关闭
- 客户端向服务器发送了不符合协议格式的命令请求
- 发送给客户端的命令回复大小超过了输出缓冲区的限制大小,那么这个客户端被服务器关闭
下文
13.2.7关闭客户端
对缓存区超出大小限制被关闭有介绍
12.3使用Lua脚本的客户端
服务器会在初始化时 创建负责执行Lua脚本中包含的Redis命令的伪客户端
并将这个伪客户端 关联到服务器状态结构的 lua_client属性中
12.4AOF文件的伪客户端
服务器在载入AOF文件时,
会创建用于执行AOF文件中包含了Redis命令的 伪客户端
在载入完成后,关闭这个伪客户端
上文内容
10.2AOF文件载入
12.5客户端重点回顾
- 输入缓冲区记录了客户端发送的命令请求,超过1GB,服务器会断开连接
- 客户端通过网络连接连上服务器时,服务器会为这个客户端创建相应的客户端状态.
如果
网络连接关闭,
发送了不合协议格式的命令请求
空转时间超时
输出缓冲区大小超过了服务器设置的条件(默认1GB),
这都会造成客户端被关闭 - 处理Lua脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,直到服务器关闭
- 载入AOF文件时使用的伪客户端在载入工作开始时动态创建,载入工作完毕后关闭
十三服务器
Redis服务器负责与多个客户端建立网络连接,
处理客户端发送的命令请求,
在数据库中保存客户端执行命令产生的数据
13.1命令请求的执行过程
一个命令请求从发送到获得回复的过程中,
客户端和服务器需要完成一系列操作
举个例子,如果使用客户端执行以下命令
那么从客户端发送SET KEY VALUE
命令到获得回复OK期间,客户端和服务器共需要执行以下操作:
- 客户端发送
SET KEY VALUE
命令 - 服务器接收并 处理命令,产生命令回复OK
- 服务器将命令回复OK发送给客户端
- 客户端接收服务器返回命令回复OK,并将这个回复打印给用户观看
下面来详细展开看看Redis是如何完成的
13.1.1客户端发送命令
客户端将SET KEY VALUE
命令转换成 协议格式,
通过连接到服务器的套接字,
将协议格式的命令请求发送给服务器
图为:客户端接收并发送命令请求的过程
举例,用户在客户端键入命令:
SET KEY BALUE
客户端将这个命令转换成协议:
·3\r\n$3\r\nSET\r\n$3\r\nKET\r\n$5\r\nVALUE\r\n
最后将这段协议内容发送给服务器
13.1.2服务端读取命令请求并处理
- 读取套接字中协议格式的命令请求,保存到客户端状态的输入缓冲区里
- 对输入缓冲区中命令请求分析,提取命令请求包含的命令参数,
- 调用命令执行器,执行客户端指定命令
图为:服务端将命令请求保存到客户端状态的输入缓冲区之后,客户端的状态
对输入缓冲区中的协议进行分析:
·3\r\n$3\r\nSET\r\n$3\r\nKET\r\n$5\r\nVALUE\r\n
并将得出的分析结果保存到客户端状态的argv属性和argc属性里面,
图为:客户端状态的argv属性和argc属性
13.2serverCron函数
服务器的serverCron函数默认每隔100毫秒执行一次,
这个函数负责
管理服务器的资源
保持服务器良好运转
13.2.1更新服务器时间缓存
Redis服务器有不少功能需要获取系统当前时间,
而每次获取系统当前时间都需要执行一次系统调用,
为了减少系统调用的执行次数
服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存:
图为:redisServer中的unixtime属性和mstime属性
因为serverCron函数默认以每100毫秒一次频率更新unixtime属性和mstime属性,
所以这两个属性记录的时间精确度不高
- 服务器在打印日志,
更新服务器的LRU时钟
决定是否执行持久化任务
计算服务器上线时间
这类对时间精度要求不高的功能上 - 为键设置过期时间
添加慢查询日志
这类需要高精确度时间的功能上,服务器还会再次执行系统调用获取当前准确时间
13.2.2更新LRU时钟
服务器状态中的:lruclock属性保存了服务器的LRU时钟,这个属性和上面介绍的unixtime属性和mstime属性一样,都是服务器时间缓存的一种:
图为:lruclock属性
每个Redis对象都会有一个lru属性,这个属性保存 对象最后一次被命令访问的时间
图为:redisObject的lru属性
当服务器计算一个数据库键 的空转时间(也即是 数据库对应的值对象的空转时间)时,
程序会用服务器的lruclock属性记录的时间
减去
对象的lru属性记录的时间,
得出计算结果就是这个对象的空转时间:
serverCron函数默认以每10秒一次的频率更新lruclock属性的值,
由于
这个时钟不是实时的, 根据这个属性计算出来的LRU时间实际上是一个模糊的估算值:
输入命令INFO server
查看:
图为: 输入
INFO server
命令
13.2.3更新服务器每秒执行命令次数
serverCron函数中的trackOperationsPerSecond函数会每100毫秒一次执行, 以 抽样计算估算服务器最近一秒处理的命令请求数量
图为: trackOperationsPerSecond函数
对于估算的最近一秒处理的命令数量 可以通过命令INFO status
来查看:
图为:
INFO status
命令后,最近一秒内服务器处理了大概6个命令
13.2.4管理客户端资源
serverCron函数每次执行都会调用clientsCron函数,
clientsCron函数 会对客户端进行以下检查
- 如果客户端与服务器连接超时,释放客户端
- 客户端执行上次命令后,输入缓冲区大小超过一定长度,
释放客户端当前输入缓冲区, 并创建默认大小的输入缓冲区,
防止客户端输入缓冲区耗费过多内存
13.2.5管理数据库资源
serverCron函数每次执行都会调用databaseCron函数
这个函数会对 服务器中一部分数据库检查,删除过期键
或
对字典进行 收缩操作
上文
3.4rehash哈希表扩容与收缩
就是对 字典的收缩
13.2.6将AOF缓冲区内容写入AOF文件
如果服务器开启AOF持久化,将AOF缓冲区里还有待写入的数据,
serverCron函数会将
AOF缓冲区中内容写入到AOF文件里面
上文
10.3AOF重写
就是对 AOF缓冲区内容写入AOF文件 的详解
13.2.7关闭客户端
serverCron函数会检查输出缓冲区
服务器会关闭 输出缓冲区大小超出限制的客户端
上文
12.2普通客户端的关闭
13.3初始化服务器
一个 redisc服务器从启动到能接受客户端命令请求
需要一系列初始化 和 设置过程
如
初始化服务器状态
接受用户指定的服务器配置
创建数据结构
网络连接
…
13.3.1初始化服务器
由 redis.c/initServerConfig函数 初始化一个struct RedisServer类型的实例变量server作为初始化服务器的第一步
initServerConfig函数的主要工作:
- 设置服务器运行ID
- 设置服务器默认运行频率
- 设置服务器默认配置文件路径
- 设置服务器运行架构
- 设置服务器默认端口号
- 设置服务器默认RDB持久化条件和AOF持久化条件
- 初始化服务器LRU时钟
- 创建命令表
13.3.2载入配置选项
在启动服务器时,用户可以通过给定的配置参数或指定的配置文件来修改服务器默认的配置
举个例子:
输入redis-server --port 10086
命令
则通过给定的配置参数修改了服务器运行端口号
输入redis-server redis.conf
命令
并且redis.conf文件包含以下内容:
那么就通过指定配置文件修改了服务器的数据库数量, 以及RDB持久化模块压缩功能
- 服务器在调用initServerConfig函数初始化server变量后,
- 载入用户给定的配置参数和配置文件
- 根据用户设定的配置,对server变量值进行修改
13.3.3初始化服务器数据结构
initServerConfig函数初始化一般属性
initServer函数 初始化数据结构,和 其它重要的设置操作初始化数据结构
- server.clients链表,该链表记录了所有与服务器相连的客户端的状态结构
- server.db数组,数组包含了服务器所有数据库
- server.lua,用于执行Lua脚本
其它重要的设置操作
如果AOF持久化功能打开,则打开AOF文件
如果AOF文件不存在, 则创建并打开新AOF文件,为AOF写入做准备
initServer函数执行完毕后,服务器用ASCII字符在日志中打印出Redis的图标以及Redis版本号信息:
注意 是
服务器先 载入用户指定的配置选项,
然后 才 对数据结构初始化
目的是: 防止用户指定的配置选择指定了数据结构,导致数据结构重新初始化
13.3.4还原数据库状态
在完成对服务器状态server变量的初始化之后,
服务器需要载入RDB文件或AOF文件
并根据文件内容还原数据库状态
当服务器完成数据库状态还原工作后,服务器将日志打印出载入文件并还原数据库状态耗费时长:
13.3.5执行事件循环
在初始化最后一步,服务器将打印以下日志:
并开始执行服务器的事件循环(loop)
至此,服务器初始化工作圆满完成,服务器可以接受客户端的连接请求,并处理请求
第三部分:多机数据库实现
十四主从复制
问题出现:
redis如何保证 高可用
解决方案:
主从复制:将master中的数据及时,有效的复制到slave中
.
为了避免单点Redis服务器故障,准备多台服务器,将数据复制多个副本中
并保持数据同步
实现Redis的 高可用
- 提供数据方:master 主服务器 写数据,并同步到slave
- 接收数据方:slave 从服务器 读数据
命令实现:
.
SLAVEOF
命令让一个服务器去 复制 另一个服务器
举个例子
有两个Redis服务器,地址分别是127.0.0.1:6379和127.0.0.1:123456
输入以下命令:
那么
进行 复制 的主从服务器双方数据库将保存相同的数据
概念上 简称 一致
比如,在主服务器上执行以下命令:
那么我们即可以在主服务器上获取msg键的值
又可以在从服务器上获取msg键的值
同样的,如果在主服务器上 删除键msg:
那么, 不仅主服务器上的msg键被删除
从服务器上的msg键也被删除:
主从复制的作用:
- 读写分离: master写,slave读,提高服务器读写分离能力
- 故障恢复: master出现问题,slave提供服务,实现快速故障恢复
- 数据冗余: 实现数据热备份,是持久化之外的一种数据冗余方式
- 高可用
- 负载均衡, 基于主从结构,配合读写分离,由slave分担master负载
并根据需求改变slave的数量,提高 Redis服务器的并发量与数据吞吐量
Redis官网关于复制的页面点击进入–>
14.1旧版复制功能实现
Redis复制操作分为 同步(sync) 和 命令传播 两个操作
14.1.1同步
执行SLAVEOF
命令,同步是第一步
即, 将从服务器的数据库状态更新至主服务器当前的数据库状态
同步需要slave想master发送SYNC
命令
步骤如下:
- slave想master发送
SYNC
命令 - master执行
BGSAVE
生成RDB文件, - master创建 复制缓冲区记录从现在开始执行的所有写命令
- master生成RDB文件,并传输给slave
- slave接收并载入RDB文件,这个过程为全面复制
- slave告知RDB文件恢复完成
- master将记录在缓冲区里面的所有写命令发送给slave,
- slave执行这些写命令,将自己的数据库状态更新mater数据库当前所处状态,这个过程为部分复制
图为:主从服务器执行
SYNC
命令的大体通信过程
图为:主从服务器执行
SYNC
命令的详细通信过程
举个例子:
14.1.2命令传播
当同步执行完毕后
master执行客户端发送的写命令后,与slave的命令就又不一样 了
为了让主从服务器再次回到一致状态
主从服务器需要对slave执行 命令传播操作:
master将自己执行的写命令(赵成主从数据不 一致 的那条命令)
发送给slave
14.2旧版复制功能的缺陷
对于初次复制,旧版复制功能能够很好完成任务,
断线后复制,旧版复制功能 效率低下
图为:slave断线后,重新复制主服务器
为了让slave弥补一小部分断线丢失数据,
但需要重新执行SYNC
命令,
这是低效的
在执行
SYNC
命令时的低效原因主要有:
第2步 master执行BGSAVE
生成RDB文件,消耗master的 CPU内存磁盘IO资源
第4步,master将RDB文件传输给slave,发送会消耗主从服务器网络资源
第5不步,slave载入RDB,会阻塞无法处理命令请求
14.3新版复制功能的实现
Redis从2.8版本开始,使用PSYNC
命令代替SYNC
命令执行复制的同步操作
PSYNC
命令分为 完整重同步和 部分重同步
- 完整重同步用于处理初次复制情况: 执行步骤和
SYNC
命令的执行步骤一样
都是master创建并发送RDB文件,slave执行RDB和缓冲区写命令 来完成同步 - 部分重同步 处理断线后复制: 即 断线后smaster只发送 连接断开时的写命令
图为:
PSYNC
命令断线后复制图1
图为:
PSYNC
命令断线后执行部分重同步的过程
14.4部分重同步的实现
部分重同步由以下三个部分构成:
- master的复制偏移量和slave的复制偏移量
- master的复制积压缓冲区
- 服务器运行的ID(run ID)
14.4.1复制偏移量
执行复制的双方–mster和slave都会维护一个复制偏移量
概念
偏移量: 一个数字,记录复制缓冲区中指令字节位置
分类
master复制偏移量: 记录发送给所有slave的指令字节对应的位置(多个)
slave复制偏移量:记录slave接收master发送过来的指令字节对应的位置(一个)
作用
同步信息,对比master和slave 偏移量的差异,当slave断线后,恢复数据使用
数据来源
master端:发送一次记录一次
slave端:接收一次记录一次
14.4.2复制缓冲区
又名 复制积压缓冲区,
是一个先进先出(FIFO)的队列,用于存储服务器执行的命令
默认存储空间大小为1M
当入队元素数量大于队列长度, 最先入队的元素被弹出,新元素放入队列
数据来源
master接收主客户端指令时,除了将指令执行,还会将指令存储到复制缓冲区中
图为:master向复制缓冲区和 所有slave传播写命令数据
master会为FIFO队列每个字节记录相应的复制偏移量
图为:复制积压缓冲区的构造
当slaved断线后,
- 重新连接后想服务器发送
PSYNC
命令,报告自己的复制偏移量 - master检查偏移量,如果数据存在于复制缓冲区中,以部分重同步模式同步数据
14.4.3服务器运行ID
除了复制偏移量个复制缓冲区之外,实现 部分重同步 还需要服务器运行ID(run ID):
概念
服务器运行ID 是每一台服务器每次运行的身份识别码,一个服务器多次运行可以生成多个运行id
组成
运行id由40位字符组成,是一个随机的16进制字符
例如:fdc9ff13b9bbaab28db42b3d50f852bb5e3fcdce
实现方式
每台服务器启动自动生成运行id,master首次连接slave,将自己的运行id发送给slave
slave保存此id
作用:
识别身份
当slave断线重新连接上一个mster时,
slave将向当前连接的master发送之前保存的运行ID:如果slave保存的运行ID和当前连接的master运行ID相同,
master是断线前的master,
master执行 部分重同步如果…不同
master不是 断线前的master
master对slave执行 完整重同步
14.5心跳检测(续约)
进入命令传播阶段,master与slave续约进行信息交换
使用心跳机制 维护,实现双方连接保持 在线
14.6重点回顾
-
Redis2.8以前的复制功能不能高效的处理断线后重复制情况
Redis2.8新增了 部分重同步 功能解决这个问题 -
部分重同步 通过 复制偏移量 复制缓冲区 服务器运行ID 3部分实现
-
master向slave传播命令更新slave的状态,保持主从服务器 一致
slave向master发送命令进行 心跳检测
十五哨兵模式
15.1哨兵概念
哨兵模式(Sentinel)是Redis的 高可用 的解决方案,
由一个或多个哨兵实例组成的哨兵系统
可以监控任意多个主服务器,以及这些主服务器属下的所有从服务器.
在被监视的主服务器进入下线状态时,
自动将通过投票的方式,
将下线的主服务器属下的某个从服务器升级成新的主服务器,
由新的主服务器代替已经下线的主服务器
15.2哨兵作用
- 监控:监控master和slave是否正常运行
- 通知:当服务器出现问题,想其它(哨兵,客户端)发送通知
- 自动故障转移:断开master和slave连接,选取一个slave作为master,将其它slave连接新额master,告知客户端新的服务器地址
15.3哨兵工作原理
哨兵在进行主从切换过程经历三个阶段
- 监控
- 通知
- 故障转移
监控
用于同步各个节点的状态信息
- 获取各个sentinel的状态(是否在线)
- 获取master的状态(master属性)
- 获取slave的状态(slave属性)
通知
sentinel在通知阶段要不断的去获取master/slave的信息,在各个sentinel之间进行共享:
故障转移
当master宕机后sentinel的具体操作
当sentinel认定master下线后,需要决定由那个sentinel来更换master
在选举时每台sentinel都有一票,谁先过来投给谁,得票多的sentinel成为处理事故的人
.
选举胜出的sentinel去slave中选一个新master出来
选取master的原则是
不在线的不选
响应慢的不选
与原master断开时间长的不选
.
选出新master后,sentinel给其它slave发送指令
向新master发送slaveof no one
向其它slave发送新master的ip端口
.
总结:故障转移阶段
1.发现问题,主观下线和客观下线
2竞选负责人
优选新master
新master上任,其它slave切换master,原master作为slave故障恢复后连接
十六集群
集群使用网络将若干计算机联通起立,提供统一的管理,对外呈现 单机 的服务效果
- 分撒单台服务器的访问压力, 实现负载均衡
- 分散单台服务器的存储压力,实现可扩展性
- 降低单台服务器宕机带来的业务灾难
- 通过算法计算出key应该保存的位置
- 将所有的存储空间计划切割成16384份,每台主机保存一部分(每份代表一个存储空间,不是一个key保存空间)
- 将key按照计算出的结果放到对应的存储空间
可扩展性
对于可扩展性的实现,例如我们增加一个集群节点
将其它每个节点都给新节点相同的槽
查找数据
查找数据时,集群的操作如下:
- 各个数据库互相通信,保存各个库里槽的编号数据
- 一次命中,直接返回
- 一次未命中,告知具体位置
- 节点通过握手来将其它节点添加到自己所处的集群中
- 集群中16384个槽可以指派给集群中的各个节点,每个节点都会记录哪些槽指派给了自己,而哪些槽被指派给了其它节点
- 节点在接到命令请求时,先检查这个命令请求要处理的键所在的槽是否由自己负责,
如果不是:节点将向客户端返回一个MOVEN错误,错误携带的信息可以指引客户端转向到正在负责相关槽的节点- 如果节点A正在迁移槽i到节点B,当节点A没能在自己的数据库中找到命令指定的数据库键时,节点A向客户端返回一个ASK错误,指引客户端到节点B 继续查找指定的数据库键
- MOVED错误表示槽的负责权已经从一个节点转移到了另一个节点,而ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施
- 集群里从节点用于复制主节点,并在主节点下线时,代替主节点继续处理命令请求
- 集群中的节点通过发送和接收消息来进行通信,常见的信息包括MEET,PING,PONG,PUBLISH,FALL五种
第四部分:独立功能的实现
十七发布与订阅
Redis的发布与订阅功能由PUBLISH
,SUBSCRIBE
,PSUBSCRIBE
等命令组成
通过执行SUBSCRIBE
命令,客户端可以订阅一个或多个频道,从而为这些频道的订阅者(subscriber):每当有其他客户端向被订阅的频道发送消息(message)时,频道的所有订阅者都会收到这条消息.
举个例子:
假设A,B,C三个客户端都执行了命令:
SUBSCRIBE
‘‘news.it’’
哪么这三个客户端就是 '‘news.it’'频道的订阅者,
图为:news.it频道和它的三个订阅者
这时某个客户端执行命令
PUBLISH
‘‘news.it’’ ‘‘hello’’
向’‘news.it’‘频道发送消息’‘hello’’,那么’‘news.it’'的三个订阅者都将收到这条消息,
图为:news.it频道发送消息,订阅者们都收到消息
除了订阅频道之外,客户端还可以通过执行PSUBSCRIBE
命令订阅一个或多个模式,
从而成为这些模式的订阅者:
每当有其他客户端向某个频道发送消息时,
消息不仅会被发送给这个频道的所有订阅者,
还会被发送给所有与这个频道相匹配的模式的订阅者.
图为:频道和模式的订阅状态
举个例子:
假设上图所示:
- 客户端A正在订阅频道’‘news.it’’
- 客户端B正在订阅频道’‘news.et’’
- 客户端C和客户端D正在订阅’‘news.[ie]t’’
如果这时某个客户端执行命令
PUBLISH "news.it" "hello"
向"news.it"频道发送消息"hello",
那么不仅正在订阅"news.it"频道的客户端A会收到消息,
客户端C和客户端D同样收到消息,
因为这两个客户端正在订阅匹配"news.it"频道的"news.[ie]t"模式
图为:将消息发送给频道的订阅者和匹配模式的订阅者
未完待续
十八事务
Redis通过MULTI
,EXEC
,WATCH
等命令来实现事务(transaction)功能
事务提供
将多个命令请求打包,
然后一次性,按顺序执行多个命令的机制
事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求
multi
开启一个事务,然后逐条将命令入队,以exec
表示提交并开始执行事务
如果将exec
替换为discard
表示放弃该事务
watch
命令通过乐观锁机制,可以监听多个key,只有当监听的key未修改时,事务才会执行
18.1事务的实现
事务从开始到结束通常会经历以下三个阶段
- 事务开始
- 命令入队
- 事务执行
1.事务开始
MULTI
命令标志着事务的开始:
MULTI
命令可以执行该命令的客户端从非事务状态切换至事务状态,
是通过客户端状态的flags属性打开REDIS_MULTI标识完成的
所以:
MULTI
命令的可以用以下伪代码来表示:
2.命令入队
图为:服务器判断命令是该入队还是该执行的过程3.事务队列
每个Redis客户端都有自己的事务状态,这个事务状态保存在redisClient客户端状态的mstate属性上:
进入multiState事务状态,
事务状态包含一个事务队列,以及一个已入队命令的计数器(事务队列的长度):
进入 multiCmd事务队列
事务队列 是FIFO队列即先进先出队列有了redisClient客户端状态,
multiState事务状态,
multiCmd事务队列后可以得到事务状态:
输入以下命令
服务器为客户端创建的 事务状态:
SET "name" "Practical Common Lisp"
在事务队列0索引位置GET "name"
在事务队列1索引位置SET "author" "Peter Seibel"
在事务队列2索引位置GET "author"
在事务队列3索引位置
图为:事务状态4执行事务
处于 事务状态的客户端向服务器发送EXEC
命令时,这个命令将立刻被服务器执行
服务器会遍历这个客户端的事务队列,
执行队列中保存的所有命令,
将结果返回给客户端
.
对于上图来说,服务器会依次执行事务队列中的命令
最后返回结果:
EXEC
命令的实现原理可以用以下伪代码描述:
18.2WATCH命令的实现
WATCH
命令是一个乐观锁,它在
EXEC
命令执行前监视任意数量的数据库键,并在
EXEC
命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果被监视的键被修改了,服务器拒绝执行事务,
并向客户端返回代表事务执行失败的空回复,
.
下面来一个事务执行失败的例子:
在可视化工具里创建两个连接到本地的客户端分别为:
客户端A–>本机
客户端B–>本机2在 本机 连接中敲出如下:
在 本机2 连接中敲出如下:
最后在 本机 连接中敲出如下:
下图展示了上面命令是如何导致 事务执行失败的:
在时间T4,本机2(客户端B)修改了"name"键的值,
在时间T5,本机(客户端A)执行EXEC
命令时,服务器发现WATCH
监视的键"name"被修改,
所以,服务器拒绝执行,本机(客户端A)的事务并返回 空回复
18.2.1WATCH命令监视数据库键
每个Redis数据库都保存了一个watched_keys字典,
此字典
键 被WATCH
命令监视的数据库键,
值 是一个链表,链表中记录了所有监视相应数据库键的客户端
通过watched_keys字典,
服务器知道哪些数据库正在被监视,
哪些客户端正在监视这些数据库键
如下图:
- 客户端c1和c2正在监视键"name"
- 客户端c3正在监视键"age"
- 客户端c2和c4正在监视键"address"
图为:一个watched_keys字典
执行WATCH
命令, 客户端通过watched_keys字典中与被监视的键进行关联
eg: 客户端为c10086,此客户端执行WATCH
命令后:
图为:执行
WATCH
命令后的font color =red>watched_keys字典
18.2.2监视机制的触发
所有对数据库进行修改的命令,
比如SET,LPUSH,SADD,ZREM,DEL,FLUSHDB等,
执行后对watched_keys字典检查,
即是否有客户端正在监视刚刚被命令修改过的数据库键,
如果有会打开REDIS_DIRTY_CAS标识表示该客户端的事务安全性已经被破坏
eg
- 如果键"name"被修改,那么c1,c2,c10086三个客户端的REDIS_DIRTY_CAS标识被打开
- 如果键"age"被修改,那么c3和c10086两个客户端的REDIS_DIRTY_CAS标识被打开
- 如果键"address"被修改,那么c2和c4两个客户端的REDIS_DIRTY_CAS标识被打开
图为:eg的示例图
18.2.3判断事务是否安全
当服务器接收一个客户端发来的EXEC
命令时,
服务器会根据这个客户端是否打开REDIS_DIRTY_CAS标识决定
是否执行事务:
- 标识打开,说明客户端监视的键被修改,事务不安全 ,服务器服务器拒绝执行客户端提交的事务
- 标识没有打开,服务器监视的键没有修改,事务安全,服务器执行客户端提交这个事务
图为:服务器判断是否执行事务的过程
18.2.4一个完整的WATCH事务执行过程
当客户端为c10086,而数据库watched_keys字典当前状态如下:
图为:执行
WATCH
命令之前
图为:执行
WATCH
之后
客户端c10086继续向服务器发送MULTI
命令,将SET
命令加入事务队列
就在此刻,另一个客户端c999向服务器发送SET
命令,将"name"键值设置为"join"
c999执行的SET
命令导致正在监视"name"键的所有客户端的REDIS_DIRTY_CAS标识被打开,当然包含客户端c10086
当c10086发送EXEC
命令时,由于标识被打开,所以服务器拒绝执行它的EXEC
命令
18.3事务的ACID性质
在Redis中,事务总是具有原子性,一致性,隔离性,
当Redis运行某些特定的持久化模式下,具有持久性,
Redis事务和传统关系型数据库事务的最大区别在于,Redis不支持事务回滚机制(rollback),
即使事务队列中的某个命令执行期间出错,整个事务也会继续执行下去,直到事务队列中所有的命令都执行完毕
Redis作者在事务功能文档中:
不支持事务回滚,
因为回滚这种复杂的功能和Redis追求简单高效的设计主旨不相符,
且
Redis事务执行错误通常都是编译错误,
编程错误只会发生在开发环境,很少会出现实际的生成环境,
所以他认为没必要为Redis开发事务回滚功能
18.3.1原子性
事务原子性:事务是最小的执行单位,
事务中的操作要不全部执行,
要不全不执行
对应Redis就是:
事务队列命令要么全部都执行,要不一个都不执行
所以Redis具有原子性
图为:执行成功的事务,Redis的原子性体现
相反,下图为执行失败的事务,这个事务因为命令入队出错被服务器拒绝执行事务队列中所有的命令
图为:执行失败的事务
18.3.2一致性
事务具有一致性:
数据库执行事务之前是一致的
事务执行之后
无论事务是否执行成功
数据库也仍是一致的
1.入队错误
如果事务在入队命令过程中,命令不存在或者命令格式不正确
Redis会拒绝执行此事务
图为:入队出现错误命令 get,服务器拒绝此事务
- 执行错误是不能在入队时被服务器发现的错误,只会在命令执行时触发
- 即使事务执行时发生错误,服务器不会中断事务执行,继续执行事务余下的其它命令,已执行的命令不会受到影响
对数据库键执行错误操作是事务执行期间常见的错误
因为事务执行过程中,出错的命令会被服务器识别出来,并进行相应的错误处理
所以出错的命令不会对数据库做修改不会对事务一致性产生影响
- 服务器无持久化内存模式下,重启为空白,所以数据总是一致
- 服务器运行在RDB/AOF模式下,在执行事务时服务器停机后根据现有RDB/AOF文件恢复数据库到一个一致的状态.找不到RDB/AOF文件,数据库是空白的,空白数据库也是一致的
所以事务执行途中发生停机都不会影响数据库一致
18.3.3隔离性
事务隔离性指的是,
多个事务之间不会互相影响,并且并发状态下执行的事务和串行执行的事务产生的结果完全相同
由于Redis单线程执行事务(事务队列中的命令),在执行事务期间不会对事务进行中断,所以Redis事务具有隔离性
18.3.4持久性
持久性:事务完成就永久保存在磁盘 上
Redis事务就用队列包裹的一组Redis命令.
Redis没有为事务提供额外的持久化功能
所以
Redis事务的持久性由Redis使用的持久化模式决定:
无论Redis在什么模式下运行
事务最后加SAVE
命令总可以保证事务的持久性:
然而这样效率底下,并不具有实用性
18.3.5事务总结
- Redis事务提供了一种将多个命令打包,然后一次性,有序执行的机制
- 多个命令执行过程中不会被中断,当事务队列所有命令被执行完毕后,事务才结束
- 带有
WATCH
命令的事务将客户端和被监视的键在数据库的watched_keys字典中进行关联
当键被修改时,程序会将所有监视被修改键的客户端的REDIS_DIRTY_CAS标识打开 - 只有客户端的REDIS_DIRTY_CAS标识未被打开,服务器才会执行客户端提交的事务,否则服务器拒绝执行客户端提交的事务
- Redis事务总是具有ACID中的原子性,一致性隔离性,当服务器运行在AOF持久化模式下,且appendfsync选项的值为always时,事务也具有耐久性
19LUA
咕咕咕
20排序
Redis的SORT
命令可以对列表键,集合键或者有序集合键的值进行排序
再来一个例子
用SORT
和 BY选项
对有序集合 test-result中的 三个 键值对排序
20.1SORT命令的实现
SORT
命令最简单执行形式为:
SORT <key>
表示对一个包含数字值的键key进行排序
服务器执行SORT number
命令的详细步骤如下:
- 创建一个和number列表长度相同的数组,该数组的每一项都是
redisSortObject结构
图为:redisSortObect源码
图为:
SORT
命令为了排序number列表而创建的数组
- 遍历数组,将各个数组项的obj指针分别指向number列表的各个项
- 遍历数组,将各个obj指针所指向的列表项转换成一个double类型的浮点数,并将这个浮点数保存在相应数组项的 u.score属性里
- 根据数组项 u.score的值,对数组进行数字值排序,排序后数组项按 u.score属性的值从小到大排序
5. 遍历数组,将各个数组项的obj指针指向的列表项作为结果返回给客户端
其它SORT <key>
命令的执行步骤也和这里给出的 SORT number命令执行步骤类似
总结:
SORT
命令为每个被排序的键都创建一个与键长度相同的数组
数组每个项都是一个 redisSortObject结构
根据SORT
命令使用的选项不同,程序使用redisSortObject结构的方式也不同
20.2ALPHA选项的实现
通过ALPHA选项,SORT
命令对字符串值进行排序
服务器执行 SORT fruits ALPHA
命令的详细步骤如下:
- 创建一个redisSortObject结构数组,数组长度等于fruits集合大小
- 遍历集合,将各个数组项的obj指针分别指向fruits集合的各个元素
- 根据obj指针所指向的集合元素,对数组进行字符串排序,排序后的数组项按集合元素的字符串值从小到大排列
- 遍历数组,依次将数组项的obj指针所指向的元素返回给客户端
其它SORT <key> ALPHA
命令的执行步骤也和这里给出的类似
20.3ASC选项和DESC选项的实现
默认SORT
命令执行升序排序.即结果为从小到大排序
所以这个两个命令是等价的
SORT <key>
SORT <key> ASC
而
SORT <key> DESC
结果值从大到下排序
升序与降序都由相同的快速排序算法执行
不过是产生的结构排序不同
20.4BY选项的实现
默认情况下,SORT命令使用被排序键包含的元素作为排序的权重
元素本身决定了元素在排序后所处的位置
如下例子:
通过 BY 选项,SORT
命令可以指定某些字符串键,或者某个哈希键所包含额某些域 来作为元素的权重对一个键排序
如下:对集合键fruits进行排序
服务器执行 SORT fruits BY *-price
命令的详细步骤如下:
-
创建一个redisSortObject结构数组,数组长度等于 fruits集合大小
-
遍历数组,将各个数组项的obj指针分别指向fruits集合的各个元素
-
遍历数组,根据各个数组项的obj指针所指向的元素,以及BY选项给定的模式 查找相应的权重键: “apple"元素,查找程序返回权重键"apple-price”
-
将各个权重键的值转换成一个double类型,保存到u.score里
-
以数组项u.score属性的值为权重,对数组进行排序,得到一个u.score属性的值从小到大排序的数组
20.5排序总结
SORT
命令通过将被排序键包含的元素载入到数组里面,对数组排序完成对键的排序工作- 默认
SORT
命令被排序键包含的都是数字值,以数字方式排序 SORT
命令由快速排序算法实现SORT
命令的DESC选项决定升序还是降序