Part 1 数据结构和对象
2. 简单动态字符串
Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组),而是自己构建了一种名为SDS的抽象类型
SDS(simple dynamic string)
,redis
使用SDS
作为默认字符串表示
SDS
还被用作缓冲区(buffer):AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现。
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于sds所保存字符串的长度,不计算空字符'\0'
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
SDS与C字符串的区别
C字符串使用长度为N+1的字符数组来表示长度为N的字符串,并且最后一个元素总是空字符'\0',这种简单字符串表示方式不能满足Redis对字符串在安全性、效率以及功能方面的要求。
- 通过len常数复杂度获取字符串长度,strlen命令
- 杜绝缓冲区溢出(buffer overflow)
- 减少修改字符串时带来的内存重分配次数,空间预分配,惰性空间释放
- 二进制安全
- 兼容部分C字符串函数
3. 链表
list,发布订阅,慢查询,监视器等功能使用到了链表,redis服务器使用链表保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区(output buffer)。
typedef struct listNode{
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode;
typedef struct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//节点数量
unsigned long len;
//节点值复制函数
//节点值释放函数
//节点值对比函数
}list;
list特性:
- 双向
- 无环
- 带head和tail指针
- 带长度计数器
- 多态
4. 字典
又称符号表(symbol table)、map
字典使用哈希表作为底层实现,一个哈希表可以有多个哈希表节点,每个节点保存了字典中的一个键值对。
4.1哈希表
typedef struct dictht{
//哈希表数组
dicEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总数等于size-1
unsigned long sizemask;
//哈希表已有节点的数量
unsigned long used;
}dictht;
4.2哈希表节点
typedef struct dictEntry{
//键
void *key;
//值,可以是指针,或者是uint64_t整数,或者是int64_t整数
union{
void *val;
uint64_tu64;
int64_ts64;
} v;
//指向下个哈希表节点,形成链表,可以将多个哈希值相同的键值对连接在一起,解决键冲突(collision)问题
struct dicEntry *next;
} dictEntry;
4.3字典
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引
//当rehash不存在时,值为-1
int trehashidx;
}
4.4哈希算法
4.5解决hash冲突
链地址法(separate chaining)
4.6 rehash
扩展和收缩哈希表
4.7 渐进式rehash
如果哈希表key数量很多,上百万、千万,不是一次性将ht[0]的所有key全部rehash到ht[1],而是分多次。分而治之。
5. 跳跃表
skipList是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,达到快速访问节点的目的。
跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
typedef struct zskiplistNode{
//层
struct zskiplistLevel{
//前进指针
struct szkiplistNode *forward;
//跨度
unsigned int span;
} level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
} zskiplistNode;
typedef struct zskiplist{
//表头结点和表尾结点
struct zskiplistNode *header,*tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点层数
int level;
} zskiplist;
总结:
6. 整数集合(intSet)
intSet是集合键的底层实现之一,当一个集合只包含整数值元素,并且元素数量不多,redis就会使用整数集合作为集合键的底层实现。
10.202.114.105:8080> sadd test_numbers 1 3 4 5 6
(integer) 5
10.202.114.105:8080> OBJECT ENCODING test_numbers
"intset"
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
总结:
7. 压缩列表(zipList)
压缩列表是列表键和哈希键的底层实现之一。
当一个列表键质保函少量列表项,并且每项要么是小整数值,要么是长度比较短的字符串,那么redis采用zipList作为底层实现。
10.202.114.105:8080> rpush 1st 1 3 2 10068 "hello" "world"
(integer) 6
10.202.114.105:8080> OBJECT ENCODING 1st
"ziplist"
当哈希键只包含少量键值对,每项的键和值要么是小整数值,要么是长度比较短的字符串,那么redis采用zipList作为底层实现。
10.202.114.105:8080> hmset testprofile "name" "jack" "age" 28 "job" "programmer"
OK
10.202.114.105:8080> OBJECT ENCODING testprofile
"ziplist"
压缩列表是redis为了节约内存而开发,由一些列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩表可以包含任意个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
总结:
8. 对象
8.1对象类型和编码
redid中的每个对象由一个RedisObject结构表示。键总是一个字符串对象
typedef struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr
//...
} robj;
类型常量 | 对象名称 | TYPE命令输出 |
---|---|---|
REDIS_STRING | 字符串对象 | string |
REDIS_LIST | 列表 | list |
REDIS_HASH | 哈希 | hash |
REDIS_SET | 集合 | set |
REDIS_ZSET | 有序结合 | zset |
10.202.114.105:8080> set foo "123"
OK
10.202.114.105:8080> get foo
"123"
10.202.114.105:8080> type foo
string
#使用object encoding命令可以查看一个数据库键的值对象的编码
10.202.114.105:8080> object encoding foo
"int"
8.2 字符串对象
编码可以是int、raw或者embstr
8.3 列表对象
编码可以是ziplist或者linkedlist
8.4 哈希对象
编码可以是zipList或者hashtable
8.5 集合对象
编码可以是intset或者hashtable
8.6 有序集合
编码可以是ziplist或者skiplist
8.8 内存回收
redis在自己的对象系统中构建了一个引用计数计数实现的内存回收机制,程序可以跟踪对象的引用计数信息,在适当的时候释放对象并进行回收。
typedef struct redisObject{
//引用计数
int refcount;
} robj;
8.9 对象共享
对象的引用计数属性还有对象共享的作用。redis在初始化服务器时,创建一万个字符串对象,包含了从0到9999所有整数值,当需要用这些字符串对象时,服务器会使用这些共享对象,而不是新建对象。
redis只对包含整数值的字符串对象进行共享.
8.10 对象的空转时长
typedef struct redisObject{
//对象最后一次被命令程序访问的时间
unsigned lru:22;
} robj;
可用于内存回收
#当前时间减去对象的lru时间,得到165
10.202.114.105:8080> object idletime foo
(integer) 165
总结:
Part 2 单机数据库的实现
9. 数据库
默认16个数据库。
切换数据库——select命令
typedef struct redisDb{
//数据库键空间(字典),保存所有的键值对
dict *dict;
//过期地点,保存键的过期时间,和键空间指向同一个键对象
dict *expires;
...
} redisDb
#设置过期时间
expire
#移除
persist
#返回剩余时间
ttl
过期键的判断:
- key是否存在于过期字典,如果存在,取得过期时间
- 检测当前Unix时间戳是否大于key的过期时间,如果是,已过期
过期键的删除策略
- 定时删除,对内存友好,尽可能快地删除,对cpu不友好,key多的情况
- 定期删除,折中,难点是确定执行的时长和频率
- 惰性删除,对内存不友好,对cpu友好
生成RDB文件
生成RDB文件时,程序会对key进行检查,已过期的key不会被保存到新创建的RDB文件中,因此,是否包含过期键不会对新生成的RDB文件造成影响。
载入RDB文件
redis启动时,如果开启了RDB功能,服务器将载入rdb文件。
- 如果服务器以主服务器模式运行,载入rdb文件时,程序会对文件中保存的key进行检查,未过期的键才载入数据库
- 如果服务器以从服务器模式运行,载入RDB文件时,所有key都会被载入数据库中。不过,主从服务器在进行数据同步的时候,从服务器的数据库会被清空。所以,一般也没影响。
AOF文件写入
如果数据某个key已经过期,但是没有被删除,aof文件不会因为过期而产生任何影响。当过期的key被删除,aof文件会追加一条del命令。
AOF重写
程序会对文件中保存的key进行检查,未过期的键才载入数据库。
复制
在复制模式下,从服务器的过期key删除,由主服务器控制(保证主从数据库数据一致性):
- 主服务器删除一个key后,会向所有从服务器发送一个del命令
- 从服务器在执行客户端的读命令时,及时碰到过期key,也不会删除。
- 从服务器只有接到主服务器的del命令后,才删除过期key
总结:
10. RDB持久化
生成RDB文件:
- save命令:会阻塞redis服务器进程,知道rdb文件创建完毕,期间服务器不能处理任何命令请求
- bgsave命令:派生出一个子进程,创建rdb文件,服务器进程(父进程)继续处理命令请求
- 如果服务器开启了aof持久化功能,服务器会优先使用aof文件来还原数据
- 只有aof持久化功能处于关闭状态,服务器才会使用rdb文件还原数据库状态
服务器在载入rdb文件期间,会一直处于阻塞状态,知道载入完成。
#服务器在900s内,对数据库进行了至少1次修改
save 900 1
save 300 10
save 60 10000
服务器每隔100ms,执行函数serverCron,对服务器进行维护,其中一项工作就是检查save命令的条件是否满足,如果满足,执行bgsave命令
总结:
11. AOF(Append Only File)
保存服务器所执行的写命令来记录数据库状态。
三步:
- 命令追加(append):服务器执行一个写命令后,会以协议格式将命令追加到服务器状态的aof_buf缓冲区的末尾
- 文件写入
- 文件同步(sync)
AOF重写(rewrite):
为了解决aof文件体积膨胀的问题,redis提供了aof文件重写的功能。redis创建一个新的aof文件替代原文件,新aof文件不会包含任何浪费空间的冗余命令,体积通常要小得多。
总结:
12. 事件
redis服务器是一个事件驱动程序,服务器需要处理一下两类事件:
- 文件事件(file event):服务器通过socket与客户端连接,文件事件是服务器对socket操作的抽象。
- 时间事件(time event):服务器的一些操作(serverCron函数)需要在给定的时间点执行,时间事件是这类定时操作的抽象。
总结:
13. 客户端
14. 服务端
Part 3 多机数据库的实现
15. 复制
slaveof命令
15.1 旧版本(2.8以前)复制功能的实现
Redis的复制功能分为同步(sync)和命令传播(command propagate)两个操作:
- 同步操作用于将从服务器的数据库状态更新至主服务的数据库状态
- 命令传播用于在主服务器的数据库状态被修改时,让主从服务器的数据库重新回到一致的状态
同步:
- 从服务器向主服务器发送sync命令
- 收到sync命令的主服务器执行bgsave命令,生成RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令
- 当服务器的bgsave命令执行完毕,主服务器会将bgsave命令生成的RDB文件发送给从服务器,从服务器接收并且载入文件
- 主服务器将记录在缓冲区的所有写命令发送给从服务器,从服务器执行这些命令。
命令传播:
主服务器会将自己执行的写命令,发送给从服务器执行,当从服务器执行了命令后,主从达到一致状态。
15.2 旧版复制功能的缺陷
断线重复制情况时的低效
15.3 新版复制功能的实现
为了解决断线重复制情况时的低效问题,2.8开始,redis使用psync命令代替sync命令在执行复制时的同步。
psync命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式:
- 完整重同步用于初次复制情况,和sync命令步骤基本一样。
- 部分重同步用于断线后重复制情况:从服务器断线后重连,主服务器可以将从服务器断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些命令。
15.4 部分重同步的实现
- 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量
- 主服务器的复制积压缓冲区(replication backlog)(默认为1MB)
- 服务器的运行ID (run ID)
- 如果offset偏移量后的数据任然存在于复制积压缓冲区里面,那么主服务器将对从服务器执行部分重同步操作
- 如果offset偏移量后的数据已经不存在于复制积压缓冲区里面,那么主服务器将对从服务器执行完整重同步操作。
15.4.3 服务器的运行ID
- 每个redis服务器,都有自己的运行ID
- 运行ID在服务器启动时生成,40个随机16进制字符组成
从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,从服务器保存ID。
当从服务器断线并重新连上一个主服务器,从服务器发送保存的ID给主服务器:
- 如果从服务器保存的ID与当前主服务器的ID相同,主服务器可以继续尝试执行部分重同步
- 如果不同,说明之前不是这个主服务器,主服务器将执行完整重同步操作。
总结:
16. Sentinel(哨兵)
17. 集群
redis集群是redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能
17.1 节点
17.2 slot
Redis集群通过分片的方式保存数据库的键值对:整个数据库被分为16384个槽(slot),数据库中的每个键都属于其中一个槽,集群的每个节点可以处理0个或最多16384个槽。
当所有槽都有节点在处理,集群处于上线状态(ok);相反,如果有任何一个槽没有得到处理,集群处于下线状态(fail)。
17.3 在集群中执行命令
- 如果key所在的槽正好指派给了当前节点,那么节点直接执行命令
- 如果没有指派给当前节点,那么节点会向客户端返回一个moved错误,指引客户端转向(redirect)至正确的节点,并再次发送之前想要执行的命令。
计算属于哪个槽
CRC16(key)&16383
cluster keyslot "mykey"
(integer)2022
17.4 重新分片
可以将任意数量的槽改为另外一个节点,键值对会从源节点移动到目标节点。
集群总结:
Part 4 独立功能的实现
18. 发布订阅
subscribe 订阅
unsubscribe 取消订阅
psubscribe 模式订阅
punsubscribe 取消模式订阅
struct redisServer{
//保存所有频道与订阅的关系,字典,key是被订阅的频道,value是一个链表,记录所有订阅了频道的客户端
dict *pubsub_channels;
//保存所有 模式订阅 关系
list *pubsub_patterns;
};
typedef struct pubsubPattern{
//订阅模式的客户端
redisClient *client;
//被订阅的模式, 如" news.* "
robj *pattern;
}pubsubPattern;
19. 事务
Redis通过multi、exec、watch等命令来实现事务功能。将多个命令打包,然后一次性、按顺序地执行多个命令,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,会将所有命令执行完毕,才去处理其他客户端的请求。
10.202.114.105:8080> multi
OK
10.202.114.105:8080> set foo 123
QUEUED
10.202.114.105:8080> set foo2 1234
QUEUED
10.202.114.105:8080> exec
1) OK
2) OK
- 事务开始
- 命令入队:服务器命令队列
- 事务执行
watch命令
watch命令是一个乐观锁(optimistic locking),可以在exec命令执行之前,监视任意数量的key,在exec命令执行时,检查这些key是否至少一个被修改了,如果是,服务器拒绝执行事务,返回客户端空回复(代表执行失败)。
10.202.114.105:8080> set foo 1234
OK
10.202.114.105:8080> watch foo
OK
10.202.114.105:8080> multi
OK
10.202.114.105:8080> set foo2 1234 #此时客户端二修改foo
QUEUED
10.202.114.105:8080> exec
(nil)
#客户端二
10.202.114.105:8080> set foo 12345
OK
20. Lua脚本
21. 排序
sort命令可以对list,set或者zset键的值进行排序。
22. 二进制位数组
23. 慢查询日志
24. 监视器
输入monitor命令,客户端可以将自己变为一个监视器,实时打印服务器命令。
10.202.114.105:8080> monitor
OK
1562229204.153750 [0 10.202.114.98:63259] "PING"
1562229204.327345 [0 10.202.114.96:29831] "PING"
1562229204.332611 [0 10.203.60.144:18747] "PING"
1562229204.399101 [0 10.202.114.96:29831] "PUBLISH" "__sentinel__:hello" "10.202.114.96,8001,dc7ad32b058d53b799fcd7b0b6be16d04b04b2a1,0,EOS_TDOP_CORE_REDIS_C04,10.202.114.105,8080,0"
1562229204.596748 [0 10.203.60.144:18747] "PUBLISH" "__sentinel__:hello" "10.203.60.144,8001,c62b89b7c4b6089a1677f99d54e1c80ce4d9e729,0,EOS_TDOP_CORE_REDIS_C04,10.202.114.105,8080,0"
1562229204.962132 [0 10.203.60.144:18747] "INFO"
输入monitor命令后,服务器将客户端加到monitors链表结尾,每次处理命令请求前,都将命令发送到各个监视器。