《Redis设计与实现》笔记


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示例.png
带有未使用空间的SDS.png

SDS与C字符串的区别

C字符串使用长度为N+1的字符数组来表示长度为N的字符串,并且最后一个元素总是空字符'\0',这种简单字符串表示方式不能满足Redis对字符串在安全性、效率以及功能方面的要求。

  1. 通过len常数复杂度获取字符串长度,strlen命令
  2. 杜绝缓冲区溢出(buffer overflow)
  3. 减少修改字符串时带来的内存重分配次数,空间预分配,惰性空间释放
  4. 二进制安全
  5. 兼容部分C字符串函数
C字符串和SDS区别.png

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;
空的哈希表.png

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;
}
普通状态下的字典.png

4.4哈希算法

4.5解决hash冲突

链地址法(separate chaining)

4.6 rehash

扩展和收缩哈希表

4.7 渐进式rehash

如果哈希表key数量很多,上百万、千万,不是一次性将ht[0]的所有key全部rehash到ht[1],而是分多次。分而治之。

字典总结.png

5. 跳跃表

skipList是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,达到快速访问节点的目的。

跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

跳跃表.png
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;

总结:


跳跃表总结.png

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;

总结:


intSet总结.png

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),每个节点可以保存一个字节数组或者一个整数值。

压缩表.png

总结:


压缩表总结.png

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

总结:


对象总结.png

Part 2 单机数据库的实现

9. 数据库

默认16个数据库。

切换数据库——select命令

typedef struct redisDb{
  //数据库键空间(字典),保存所有的键值对
  dict *dict;
  //过期地点,保存键的过期时间,和键空间指向同一个键对象
  dict *expires;
  ...
} redisDb
#设置过期时间
expire
#移除
persist
#返回剩余时间
ttl

过期键的判断:

  1. key是否存在于过期字典,如果存在,取得过期时间
  2. 检测当前Unix时间戳是否大于key的过期时间,如果是,已过期

过期键的删除策略

  1. 定时删除,对内存友好,尽可能快地删除,对cpu不友好,key多的情况
  2. 定期删除,折中,难点是确定执行的时长和频率
  3. 惰性删除,对内存不友好,对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

总结:


redis 单机数据库总结.png

10. RDB持久化

生成RDB文件:

  • save命令:会阻塞redis服务器进程,知道rdb文件创建完毕,期间服务器不能处理任何命令请求
  • bgsave命令:派生出一个子进程,创建rdb文件,服务器进程(父进程)继续处理命令请求
  1. 如果服务器开启了aof持久化功能,服务器会优先使用aof文件来还原数据
  2. 只有aof持久化功能处于关闭状态,服务器才会使用rdb文件还原数据库状态

服务器在载入rdb文件期间,会一直处于阻塞状态,知道载入完成。

#服务器在900s内,对数据库进行了至少1次修改
save 900 1
save 300 10
save 60 10000

服务器每隔100ms,执行函数serverCron,对服务器进行维护,其中一项工作就是检查save命令的条件是否满足,如果满足,执行bgsave命令

总结:


RDB总结.png

11. AOF(Append Only File)

保存服务器所执行的写命令来记录数据库状态。

三步:

  1. 命令追加(append):服务器执行一个写命令后,会以协议格式将命令追加到服务器状态的aof_buf缓冲区的末尾
  2. 文件写入
  3. 文件同步(sync)

AOF重写(rewrite):

为了解决aof文件体积膨胀的问题,redis提供了aof文件重写的功能。redis创建一个新的aof文件替代原文件,新aof文件不会包含任何浪费空间的冗余命令,体积通常要小得多。

总结:


AOF总结.png

12. 事件

redis服务器是一个事件驱动程序,服务器需要处理一下两类事件:

  • 文件事件(file event):服务器通过socket与客户端连接,文件事件是服务器对socket操作的抽象。
  • 时间事件(time event):服务器的一些操作(serverCron函数)需要在给定的时间点执行,时间事件是这类定时操作的抽象。

总结:


事件总结.png

13. 客户端

客户端总结.png

14. 服务端

服务器总结.png

Part 3 多机数据库的实现

15. 复制

slaveof命令

15.1 旧版本(2.8以前)复制功能的实现

Redis的复制功能分为同步(sync)命令传播(command propagate)两个操作:

  • 同步操作用于将从服务器的数据库状态更新至主服务的数据库状态
  • 命令传播用于在主服务器的数据库状态被修改时,让主从服务器的数据库重新回到一致的状态

同步:

  1. 从服务器向主服务器发送sync命令
  2. 收到sync命令的主服务器执行bgsave命令,生成RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令
  3. 当服务器的bgsave命令执行完毕,主服务器会将bgsave命令生成的RDB文件发送给从服务器,从服务器接收并且载入文件
  4. 主服务器将记录在缓冲区的所有写命令发送给从服务器,从服务器执行这些命令。

命令传播:

主服务器会将自己执行的写命令,发送给从服务器执行,当从服务器执行了命令后,主从达到一致状态。

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相同,主服务器可以继续尝试执行部分重同步
  • 如果不同,说明之前不是这个主服务器,主服务器将执行完整重同步操作。

总结:


复制总结.png

16. Sentinel(哨兵)

sentinel总结.png

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 重新分片

可以将任意数量的槽改为另外一个节点,键值对会从源节点移动到目标节点。

集群总结:


集群总结.png

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;
发布订阅总结.png

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
  1. 事务开始
  2. 命令入队:服务器命令队列
  3. 事务执行

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
事务总结.png

20. Lua脚本

lua总结.png

21. 排序

sort命令可以对list,set或者zset键的值进行排序。


sort命令总结.png

22. 二进制位数组

二进制数组总结.png

23. 慢查询日志

slowlog总结.png

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链表结尾,每次处理命令请求前,都将命令发送到各个监视器。


monitor总结.png

你可能感兴趣的:(《Redis设计与实现》笔记)