Redis的稍微详细那么一点的介绍

324 阅读41分钟

废话不多说, 直接正文

0.1. Redis解决Hash冲突的方法是链地址法, Redis使用动态字符串保存字符串, 此数据结构可以自动增长, 拥有长度等属性

0.2. Redis的链表特点是: 双端, 无环, 带有表头指针和表尾指针, 带有链表长度计数器, 多态(使用void*指针来保存)

0.3. Redis使用渐进式rehash, 目的是减轻服务器的负担, 每次对字典进行增删改查操作时, 会顺带把数据放到新的哈希表里面去, 所以每个字典有两个哈希表, 一个查询用, 一个rehash用

0.4. 跳跃表是有序集合的底层实现之一, 跳跃表有两个数据结构, 一个用来保存跳跃表信息, 一个用来记录跳跃表节点, 跳跃表的层高是1-32之间的随机数, 同一个跳跃表中, 可以包含相同的分值, 但是每个节点的成员对象必须是唯一的, 当分值相同时, 会比较成员对象的大小

0.5. 压缩列表是为了节约内存而开发的, 每个压缩列表的节点可以保存一个整数值或一个字节数组

  1. object encoding对应:

    

int:整数;
embstr:embstr编码的简单动态字符串;
raw:简单动态字符串;
hashtable:字典;
linkedlist:双端链表;
ziplist:压缩链表;
intset:整数集合;
skiplist:跳跃表和字典
  1. 字符串对象的编码可以是: int, raw, embstr.

    

int:整数值且可以用long表示;
raw:字符串且长度大于32字节, 或者可以用long double保存的浮点数, 没法用long保存的整数或无法用long double保存的整数;
embstr:字符串且长度小于等于32字节, 或者可以用long double保存的浮点数, 没法用long保存的整数或无法用long double保存的整数.
embstr更加节省内存开销, 但是它是只读的, 对embstr进行修改会把它们变成raw编码的.
  1. 字符串对象API

    

SET: 对值进行保存
GET: 获取保存的字符串, 如果是数字就转换成字符串再保存
APPEND: 追加到现有字符串的末尾
INCRBYFLOAT: 取出整数值并转换成浮点数然后相加, 然后保存相加的结果
INCRBY: 对整数数值进行加法计算, 然后保存整数结果
DECRBY: 取出整数, 做减法, 保存整数结果
STRLEN: 返回字符串长度
SETRANGE: 把对象转换成raw编码(特指整数和embstr类型的编码), 然后执行操作(将字符串特定索引上的值设置为给定的字符)
GETRANGE: 把对象转换成字符串(特指用于整数), 然后返回给定索引的位置的字符

    

  1. 使用ziplist条件有两个:

    

A.所保存的所有字符串元素大小都小于等于64.
B.所保存的元素数量小于等于512;任何一个不满足都会使用linkedlist

5.列表API

    

LPUSH: 把新的元素压入表头
RPUSH: 把新的元素压入表尾
LPOP: 返回表头元素并删除
RPOP: 返回表尾元素并删除
LINDEX: 定位指定下标的元素并返回
LLEN: 返回列表长度
LINSERT: 插入到指定位置
LREM: 遍历列表, 删除包含了给定元素的节点
LSET: 定位到指定位置, 然后更新此位置

    

  1. 哈希对象有两种底层实现: ziplist或者hashtable. 使用ziplist条件:

    

A. 键和值的长度都小于等于64字节时;
B. 键值对数量小于等于512个. 其中任意一个不满足会自动转换成hashtable

    

ziplist: 保存键值对的两个节点总会挨在一块, 每次添加的节点总是会被添加到表尾; 先添加键, 再添加值
hashtable: 使用字典键值对来保存, 每一个键是一个字符串对象, 里面保存了键; 每一个值又是一个字符串对象, 里面保存了值.
  1. Hash对象API

    

HSET: 添加键值对
HGET: 获取键对应的值
HEXISTS: 判断是否存在键
HDEL: 将键所对应的键值对删除
HLEN: 返回键值对数量
HGETALL: 遍历整个哈希对象
  1. 集合对象, 有两个编码方式: intset和hashtable.

    

intset只包含整数, 使用整数集合作为底层实现;
hashtable使用字典作为底层实现, 键用来保存, 值全为NULL; 编码转换: A. 集合所有元素都是整数; B. 元素个数不超过512个. 任何一个不满足都会转换成hashtable编码
  1. 集合对象API

    

SADD: 添加元素
SCARD: 返回大小
SISMEMBER: 返回是否在集合中
SMEMBERS: 遍历整个集合
SRANDMEMBER: 随机返回一个元素
SPOP: 随机返回一个元素并删除
SREM: 删除等于给定元素的元素
  1. 有序集合对象, 使用ziplist或skiplist来保存.

    

ziplist使用两个压缩节点来保存, 第一个保存元素成员, 第二个保存元素分值.
然后根据分值排序(升序), 分值小的在表头, 大的在表尾.
skiplist使用zset作为底层实现, 每个跳跃表节点都保存了一个元素, object属性保存了元素的成员, score保存了元素的分值
  1. 有序集合API

    

ZADD: 添加元素
ZCARD: 返回元素个数
ZCOUNT: 统计分值在给定范围内的元素的数量
ZRANGE: 返回给定索引范围内的所有元素
ZREVRANGE: 反向返回给定索引范围内的所有元素
ZRANK: 返回给定成员的排名
ZREVRANK: 反向返回给定成员的排名
ZREM: 删除包含了给定元素的节点
ZSCORE: 获取成员的分值
  1. Redis只会对整数类型的字符串对象实现内存共享机制
  2. TTL和PTTL会返回键的剩余生存时间
  3. 设置过期时间有四种方式:

    

EXPIRE <key> <ttl>: 将过期时间设置为ttl秒
PEXPIRE: <key> <ttl>: 将过期时间设置为ttl毫秒
EXPIREAT: <key> <ttl>: 将过期时间设置为ttl秒数时间戳
PEXPIREAT: <key> <ttl>: 将过期时间设置为ttl毫秒时间戳

前三种最终通过转换成PEXPIREAT来实现, 数据库有一个过期字典, 键为指向键值对的指针, 值为过期时间, 用来维护键和键的过期时间的关系.

  1. PERSIST 会移除键值对的过期时间; TTL以秒为单位返回键的剩余生存时间, PTTL以毫秒为单位返回键的剩余生存时间

  2. 三种删除过期键的方法:

    

定时删除: 在删除时间被设立起, 建立一个定时器, 时间到了删除; 对内存有好, 对CPU不友好
惰性删除: 在键被取出时才会进行检查, 删除; 对CPU友好, 对内存不友好
定期删除: 定期删除键, 但是删除效率受执行时长和频率影响, 如果时长长或频率高, 就会退化成定时删除, 过多的浪费CPU资源; 如果时长过短或者频率过低, 就会退化成惰性删除, 造成内存泄漏
  1. Redis使用的是惰性删除和定期删除;

    

对于惰性删除, 每次都会先执行键生命检查函数, 再执行相应的具体操作. 如果没过期, 就执行, 过期就执行过期操作;
对于定期删除, 会每次随机取出一定数量的键, 进行删除检查, 并会利用进度指针, 标记进度到哪里了.
  1. 数据库中的过期键不会对新的RDB文件造成影响. 如果服务器以主服务器运行, 会在载入RDB文件时, 过滤掉过期键; 如果是以从服务器运行, 那么会全部载入, 但是主从服务器同步时, 又会滤掉过期键.

  2. 数据库中过期键也不会对AOF产生影响, 在键被惰性或定期删除后, 会追加一条删除指令到AOF文件, 因此过期键并不会对AOF产生影响, AOF重写类似于RDB载入.

  3. 当服务器在复制模式下运行时, 从服务器的过期键删除动作由主服务器决定:

    

当主服务器删除过期键时, 会显示的发送删除指令给从服务器, 从而实现删除;
但是从服务器进行读时, 并不会惰性的删除, 就像键没过期那样; 从服务器只有在接收到主服务器的删除指令时, 才会删除键.
主服务器来删除键这一特点可以保证主从服务器数据的一致性.
  1. Redis数据保存在内存中, 所以需要把数据再次保存在硬盘中, 以免断电或退出造成数据丢失. 有两种保存方式: RDB和AOF. 服务器载入RDB文件时, 会一直阻塞直到操作完成.

  2. RDB使用SAVE或BGSAVE生成RDB文件以实现数据的保存.

SAVE会阻塞当前线程, 去保存, 这会导致服务器无法执行任何外界操作; BGSAVE会创建一个新的线程去保存, 所以不会阻塞当前线程.

RDB文件的载入是在服务器启动时完成的, 不需要手动载入. 需要注意的是, 由于AOF文件更新的更加频繁, 所以服务器会优先载入AOF文件. 最后, 在BGSAVE执行期间, 客户端发送的SAVE指令会被拒绝, 因为会发生父线程和子线程冲突竞争资源; 客户端发送的BGSAVE也会被拒绝, 两个子线程也会冲突;

BGREWRITEAOF和BGSAVE不能同时执行: 如果BGSAVE在执行, 那么BGREWRITEAOF会被延迟到BGSAVE之后执行; 如果BGWRITEAOF正在执行, 那么客户端发送的BGSAVE会被服务器拒绝, 这两个命令均是子线程执行, 但是考虑到磁盘读写性能, 才不允许并发执行.

  1. 用户可以通过save选项设置多个保存条件, 只要其中任意一个被满足, 就会执行BGSAVE.

    

设置格式: save <time> <ops>

解释: 在time时间内对数据库执行了ops次修改将会触发BGSAVE条件.

  1. 数据库的saveparam数组维持了一个数组, 这个数组元素是一个结构体, 结构体包含两个属性:

    

时间和修改次数;

    

服务器还维持一个dirty计数器, 用来记录距离上次成功保存服务器完成了多少次修改;

lastsave是一个UNIX时间戳, 记录了上一次成功保存的时间. 同时, serverCron每隔100ms就会执行一次, 此函数用于对正在运行的服务器进行维护, 其中一项工作就是检查save选项所设置的保存条件是否满足, 如果满足, 就执行BGSAVE指令.

  1. 关于RDB文件结构, 有点复杂, 详见书125页.

  2. RDB保存的是数据, AOF保存的是操作指令, 以此来实现对于数据的保存.

  3. 数据库包含一个服务器状态的aof_buf缓冲区, 此缓冲区用于追加命令, 每次更新命令都会转换成协议格式然后追加到缓冲区末尾; AOF的写入和同步, 有三个属性值决定对于aof_buf缓冲区的冲刷:

    

always:一直冲刷, 安全性最强, 但效率低;
everysec:每秒刷新一次, 效率和安全性的折中措施(默认选择);
no:从不主动冲刷, 冲刷行为取决于操作系统, 不安全.
  1. AOF重写, 遍历数据库的数据, 然后创建需要的指令并写入, 所以没有一条命令是多余的, 大大减少了AOF文件的体积. AOF重写用的是子线程, 为了避免写入时发生了新的变化, 父线程会把变化同时写到aof_buf和AOF重写缓冲区两个缓冲区(注意, 文件写入是先写入到缓冲区再到硬盘), 重写完成, 会把重写缓冲区里面的数据写入到磁盘中, 对新文件重命名, 原子性的覆盖现有的AOF文件, 完成文件的更替

  2. Redis使用Reactor多路复用, 所以单线程但高效; Redis多路复用类似单线程的NIO, 有套接字, Selector, 派发器(dispatch), 相应的Handler. 但是IO多路复用程序(Selector)内部维护了一个任务队列, 保证套接字有序被传递到派发器. 性能依次是: evport, epoll, kqueue, select.

  3. 当客户端connect服务器时, 会产生AE_READABLE事件, 此时套接字变成可读的(客户端对套接字执行write或close操作), 如果可读事件和可写事件同时发生, 会优先处理可读事件.

  4. Redis设置了很多文件事件处理器, 包括应答连接, 命令请求, 命令回复, 主从服务器复制等. 一次完整的事件请求过程:

    

1. 服务器的监听套接字(ServerSocket)处于监听状态下, 有一个应答连接处理器和它绑定;
2. 此时, 有一个客户端发起连接, 会触发服务器应答连接处理器执行连接, 产生一个和客户端关联的套接字(Socket)并把此套接字设置为AE_READABLE事件, 准备接受客户端命令;
3. 接收到命令, 执行得到结果, 套接字变成AE_WRITEABLE事件, 有一个回复应答处理器和它绑定, 准备向客户端写入应答;
4. 写入完成, 再次准备接受可读事件.
P.s:其实就和Java NIO流那一套一样的, 由事件触发, 服务端的ServerSocket监听端口, 一旦有连接就建立一个和客户端绑定的Socket然后利用此Socket进行处理.
  1. 时间事件, 有两类, 一是定时事件, 一是周期事件.

服务器为时间事件创建了全局唯一ID, 从小到大排序, 新事件的ID比旧事件的ID大;

    

when, 毫秒精度的UNIX时间戳, 记录了时间到达的时间;
timeProc, 事件处理器, 一个函数, 当事件到达时, 会调用它来处理事件.
事件是定时事件还是周期事件取决于timeProc的返回值: 如果是定时事件, 返回值为AE_NOMORE, 到达一次后就会被删除, 之后不再到达. 如果返回值非AE_NOMORE, 那么事件为周期性事件, 服务器会根据返回值对事件的when属性进行更新.
  1. 所有的时间事件都保存在一个无序链表里, 新事件总是添加到表头, 正因无序, 所以需要遍历整个链表才能确定所有的事件, 无序链表并不影响服务器性能, 因为服务器里的时间事件就一两个. 比如serverCron函数, 用来检测服务器状态.

  2. 对于事件调度, 会计算最近的时间事件到达时间, 然后阻塞并等待文件事件到达, 具体阻塞时间取决于到达时间和当前时间之差, 所以是优先执行文件事件, 待文件事件全部处理完成, 处理时间事件, 然后循环.

  3. 时间事件和文件事件都会有序地, 原子地执行, 同时文件事件会主动让出执行权, 避免饥饿事件的产生.

  4. 对于每个与服务器连接的客户端, 服务器都保存了"客户端状态"来记录客户端的状态, 包括:

    

1.套接字描述符
2.客户端的名字
3.客户端的标志值
4.客户端正在使用的数据库的指针, 以及该数据库的编号
5.客户端当前要执行的指令, 指令的参数, 参数的个数, 以及指向实现这个指令的函数的指针
6.客户端的输入缓冲区和输出缓冲区
7.客户端的复制状态信息, 以及进行复制所需要的数据结构
8.客户端执行BRPOP, BLPOP等列表阻塞命令是使用的数据结构
9.客户端的事务状态, 以及执行WATCH命令时用到的数据结构
10.客户端执行发布与订阅功能时用到的数据结构
11.客户端的身份验证标志
12.客户端的创建时间, 与服务器的最后一次通信时间, 以及输入缓冲区大小超出软性限制的时间
  1. 通过遍历记录客户端状态结构的链表实现查找客户端状态. 载入AOF文件和执行Lua脚本这两个伪客户端的fd属性值为-1, 其他的真的客户端都是自然数, 客户端还有名字属性, 用来区分, 好记!

  2. 客户端的flag属性记录了客户端的角色, 具体角色见书p165. 但是吧, PUBSUB和SCRIPT LOAD虽然不执行修改, 但是也会被强制写入AOF文件, 因为改变了服务器的状态.

  3. 输入缓冲区大小能动态调整, 但不能超过1G, 不然会关闭这个客户端. 客户端维持两个命令属性: 命令和命令个数. 然后会根据命令参数第一个去(在一个字典中)查找相应的执行函数, 然后执行.

  4. 客户端有两个输出缓冲区, 一个大小固定, 一个大小可变; 固定大小的用来处理简短的回复, 可变的用来处理较大的回复. 可变大小的实际上是一个字符串链表, 里面的多个字符串链接起来组成了长回复. 客户端的身份验证属性记录了客户端是否验证通过. 客户端还有两个时间属性, 一个是客户端创建时间, 一个是最后一次活跃时间.

  5. 对于普通客户端, 如果有新的连接建立, 那么就会把这个客户端状态添加到clients链表的末尾. 客户端连接关闭的原因有很多, 比如回复过大, 输入过大, 发送了不规范请求, 或者超时时间到了(主, 从服务器作为客户端执行某些命令时除外), 伪客户端, AOF的在载入是创建, 载入完成关闭, Lua的: 初始化时会创建执行Lua脚本的客户端, 然后一直存在, 直到服务器关闭.

  6. 服务端进行客户端命令处理: 客户端发送命令请求, 服务端接收命令请求, 解析命令, 进入到命令执行器环节.

  7. 根据客户端状态的argv[0]参数, 在命令表(一个字典)里找到此参数所指定的命令, 并将找到的命令保存到客户端状态的cmd属性里面; 字典的键是一个命令名字, 值是redisCommand结构, 记录了命令的实现信息, 包括:

    

name: 命令的名字
proc: 函数指针, 指向命令的实现函数
arity: 命令参数的个数, 如果值是-n, 那么表示参数数量必须大于等于n, 注意, 命令本身也是参数
sflags: 记录了这个命令的属性
flags: 以二进制标志命令属性, 方便解析和处理
calls: 总共执行了多少次这个命令
milliseconds: 执行这个命令耗费的总时长

然后执行预备操作, 包括:

    

1. 判断命令是否合法
2. 检查参数个数
3. 是否通过身份验证
4. 如果服务器打开了maxmemory, 会先检查可用内存大小, 有必要时会进行内存回收
5. 在BGSAVE出错是否继续进行写命令
6. 是否是命令订阅频道
7. 如果数据库正在进行命令载入, 那么判断命令是否带有i标志
8. 服务器进行执行Lua脚本时和客户端正在执行事务时的特殊命令核实
9. 如果服务器打开了监视器功能, 会把命令的详细信息发送给监视器

接下来调用命令的实现函数, 既调用客户端的cmd属性, 同时, 因为客户端状态保存了命令的信息, 所以实现函数执行时只要一个指向客户端状态的指针作为参数就行, 最后, 执行一些后续工作, 比如开启日志, 更新milliseconds属性, AOF同步, 从服务器同步.

  1. 服务器把回复写入到客户端的输出缓冲区, 并设置为应答模式, 然后发送给客户端, 客户端再打印回复.

  2. serverCron函数, 它的作用有:

    

1.更新服务器时间的缓存(因为有一些应用会频繁的获取时间, 所以有了时间缓存, 就是一个数值而已), 但是对于那些对时间精度要求高的, 还是得获取线时间(在线时间);
2.更新LRU时钟; 更新服务器每秒执行次数, 采用抽样的方法统计最近这一秒处理的命令请求的数量;
3.更新服务器内存峰值记录; 处理SIGTERM信号;
4.管理客户端资源, 比如, 如果连接超时, 会释放此客户端, 如果输入缓冲区过大, 会释放当前缓冲区并创建一个默认大小的输入缓冲区, 从而防止客户端的输入缓冲区耗费了过多的内存;
5.管理数据库资源;
6.执行被延迟的BGREWRITEAOF;
7.检查持久化操作的运行状态;
8.将AOF缓冲区中的内容写入到AOF文件;
9.关闭异步客户端;
10.增加cronloops计数器的值
  1. 初始化服务器, 包括设置一些默认参数; 载入配置选项, 比如端口, 比如数据库数量; 初始化服务器数据结构, 比如客户端链表(server.clients), 服务器数据库(server.db)等; 为服务器设置信号处理器, 创建共享对象, 打开服务器的监听端口, 为serverCorn创建时间事件; 准备AOF持久化功能, 初始化服务器后台的BIO模块; 还原数据库状态, 如果开启了AOF功能, 就用AOF文件来还原, 否则用RDB文件来还原; 最后一步, 执行事件循环, 开始正式工作了!

  2. SLAVEOF命令可以创建主从服务器关系, 比如: 从服务器ip:端口> SLAVEOF <主服务器ip:端口>, 两个服务器会保持数据库状态一致, 接下来介绍新旧版本复制功能的实现.

  3. 旧版本, Redis的复制功能分为同步和命令传播两个操作., 同步, 把从服务器状态同步至和主服务器状态一致, 命令传播用于在主服务器状态发生改变时, 导致主从不一致, 从而让数据再次回归一致的状态. 同步, 当执行SLAVEOF命令时, 从服务器首先会进行同步操作, 将从服务器状态同步至和主服务器一致, 具体步骤: 从服务器向主服务器发送SYNC命令, 主服务器接受并执行BGSAVE指令, 生成RDB文件, 并记录从现在开始的所有的写命令, 当BGSAVE执行完毕, 发送RDB文件给从服务器, 从服务器载入, 然后发送缓冲区的命令, 保持完全一致, 再然后, 执行命令传播, 保证一直一致. 缺陷是, 断线复制依旧采用以上发送SYNC命令的方法, 而SYNC命令会占用大量的系统资源, 比较低效.

  4. 新版本, 使用PSYNC命令来替代SYNC来执行复制时的同步操作. PSYNC具有完整重同步, 和部分重同步两种模式.

完整重同步用于执行初次复制的情况, 步骤类似于SYNC命令; 部分重同步, 主服务器会把从服务器断线期间的写命令记录下来, 待到重新连接时, 再发送给从服务器, 从而实现同步.

部分重同步实现细节:

    

一, 主服务器的复制偏移量和从服务器的复制偏移量;
主服务器的复制积压缓冲区; 服务器的运行ID.
理解, 主服务器复制偏移量: 每次向从服务器发送n个字节, 偏移量就+n, 从服务器接收到n字节数据, 也把自己的偏移量+n, 这样, 通过对比主从服务器的偏移量, 就能判断是否同步;
复制积压缓冲区是由主服务器维护的一个固定长度(当元素数大于队列长度时, 队首出队, 队尾入队), 先进先出的队列, 默认大小为1MB.
如果从服务器偏移量+1到主服务器偏移量(m-n)的数据在积压缓冲区里, 就发送CONTINUE+回复, 表示使用部分重同步, 否则就是完整重同步.
正确设置积压缓冲区的大小也是关键;
每个服务器都有自己的运行ID, 初次复制时, 主服务器会把自己的ID发送给从服务器, 从服务器会保存起来, 断线之后, 连接时, 从服务器会发送之前保存的ID, 如果ID一致, 执行部分重同步, 否则, 完整重同步.
  1. PSYNC的实现:

    

如果从服务器从没复制过, 或执行过"SLAVEOF no one", 那么将发送PSYNC ? -1以表明要实现完整重同步;
如果复制过, 会发送PSYNC <ID> <offset>, 解释: ID是上一次复制时的主服务器的ID, offset是当前从服务器的复制偏移量;
对于服务器的回复:
如果主服务器返回+FULLRESYNC <ID> <offset>, 那么表示即将开始完成重同步, ID是主服务器的ID, offset是主服务器的当前复制偏移量, 从服务器会把这个值作为自己的初始化偏移量;
倘若返回了+CONTINUE回复, 那么表示主服务器将发送从服务器缺少的那部分;
要是返回-ERR, 那么表示主服务版本低于2.8, 无法识别PSYNC
  1. 复制的实现

    

1.设置主服务器的IP和端口
2.建立套接字连接(和Java IO那一套一样)
3.发送Ping命令, 一是检查套接字读写状态是否正常, 二是检查主服务器是否正常, 三是判断从服务器的网络状态, 四是告诉从服务器主服务器能否正常处理请求, 五是, 如果读到了Pong表明正常
4.身份验证, 根据主从服务器是否设置了这一选项来判断是否验证身份, 所以有四种情况
5.发送从服务器的端口号
6.发送PSYNC实现同步, 在这之前, 只有从服务器是客户端角色, 然后主服务器担当既是客户端, 又是服务端的角色
7.主服务器执行命令广播, 实现实时同步
  1. 心跳机制, 在命令广播阶段, 从服务器会以每秒一次的频率, 向主服务器发送命令: REPLCONF ACK , 其中, offset是从服务器当前复制偏移量, 它的作用有:

    

一是检测主从服务器网络状态.
二是辅助实现min-slaves选项.
三是, 检测命令丢失.
这里提一下检测命令丢失, 如果因为网络不佳等原因, 造成了主服务器发送的数据没有被接受完全, 那么此时, 便可通过从服务器发过来的复制偏移量来确定, 然后重新补发.
  1. Sentinel(哨兵)机制, 用来监视主服务器和从服务器, 当主服务器下线时, 会把某个从服务器设置成新的主服务器, 来继续处理请求.

  2. 启动Sentinel命令: redis-sentinel <sentinel.conf路径>

    

1.初始化服务器
2.将普通的Redis服务器使用的代码替换成Sentinel专用代码
3.初始化Sentinel状态
4.根据给定的配置文件, 初始化Sentinel的监视主服务器列表
5.创建连向主服务器的网络连接

Sentinel本质是一个运行在特殊模式下的Redis服务器, 所以要先初始化一个服务器, 但是也有一点不同, 比如, Sentinel不使用数据库, 所以不会使用RDB或AOF文件来载入数据库内容.

还有一些, 比如不使用数据库键值对方面的命令(SET, DEL, FLUSHDB);

不使用事务命令(MULTI, WATCH); 不使用脚本命令(EVAL); 不使用RDB和AOF持久化命令(SAVE, BGSAVE, BGREWRITEAOF);

对于复制命令, Sentinel内部使用, 客户端不使用;

发布和订阅命令, PUBLISH只能在内部使用;

文件事件处理器(内部使用, 且处理器略有不同);

时间事件处理器(内部使用, 且还会在调用serverCron之后再调用sentinelTimer)

使用Sentinel专用代码, 既, 把普通Redis服务器使用的代码替换成Sentinel专用代码, 由于Sentinel模式下使用的命令表并不包含键值对命令, 所以不能用这些命令, 所以, sentinel客户端可以执行的命令只有:

    

PING
SENTINEL
INFO
SUBSCRIBE
UNSUBSCRIBE
PSUBSCRIBE
PUNSUBSCRIBE

这七个命令.

    

服务器会初始化一个sentinel结构, 保存了服务器中所有和sentinel功能有关的状态;
初始化字典属性master, 里面记录了被监视的主服务器的信息, 键是被监视的主服务器的名字, 值是被监视主服务器对应的sentinelRedisInstance结构;
这个实例结构可以是主服务器, 从服务器, 甚至另一个sentinel;
初始化连向主服务器的网络连接: 此时sentinel成为主服务器的客户端, 但是sentinel会创建两个连接, 一个是普通的命令连接, 一个是专门用于订阅主服务器的_sentinel_:hello频道;
sentinel会以每10秒一次发送INFO指令来获取主服务器信息, 以及附属的从服务器的信息.
sentinel向从服务器发送INFO然后得到从服务器的信息;
接下来还会向主从服务器发送信息, 这条信息包含sentinel本身的信息;
接收来自主从服务器的频道信息;
更新sentinels字典, 因为它监视的主服务器可能不仅它在监视, 还会有其他的sentinel在监视, 所以会更新此信息;
当sentinel发现了新的sentinel时, 不仅会保存它的信息, 还会创建连向它的命令连接, 但是并不会创建订阅连接;
  1. 检查主观下线状态, 若设置时间内没有收到+PONG, -LOADING, -MASTERDOWN三种回复中的一种, 或, 在此时间内不停返回这三个之外的回复, 那么sentinel就是把此服务器视为主观下线;

    之后, 此sentinel会询问监视此服务器的其他sentinel, 如果获得足够多(选举参数的值)的下线认可, 就会把它判定为客观下线, 准备故障转移;

    选出领头sentinel, 由它进行故障转移.

  2. 故障转移, 从下线主服务器的从服务器选取一个新的主服务器, 让剩余的从服务器复制新的主服务器, 把旧的主服务器设置为此主服务器的从服务器, 通过发送SLAVEOF no one可以把一个服务器变成主服务器

  3. CLUSTER MEET , 可以把ip和port指定的节点添加到当前客户端所连接的服务器所在的集群里, 集群模式的节点就是一个普通的服务器, 只是多了clisterNode, clusterLink, clusterState这三个结构.

  4. clusterNode保存了节点的详细信息, 其中的link属性指向一个clusterLink; 这个结构类似redisClient结构, 包含连接相关信息, 套接字描述符, 输入输出缓冲区等, 和redisClient的区别在于, redisClient用于连接客户端, clusterLink用于连接节点; clusterState记录了当前节点视角下的集群状态.

  5. CLUSTER MEET实现原理:

    

1.收到命令的节点A会与节点B进行握手, 为节点B创建一个clusterNode结构, 并保存至clusterState.nodes字典里面
2.节点B在收到消息后, 也会为A创建一个clusterNode结构, 用来保存节点A的信息
3.节点B返回一条PONG信息
4.节点A返回PING信息
5.节点B收到PING, 握手完成
  1. 集群通过分片的方式来保存数据, 集群的数据库被分为16384个槽(slot), 数据库中的每个键都属于这16384个槽之一, 每个节点可处理0-16384个槽;

    如果每个槽都有节点处理, 那么就说集群处于上线模式, 否则就是下线模式. 把槽指派给节点: CLUSTER ADDSLOTS [slot ...] 比如: CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000.

    clusterNode结构的slots属性(一个二进制数组)和numslot属性记录了节点负责处理哪些槽; 一个节点除了负责记录, 还会把自己要处理的槽告诉其他节点;

    clsterState还有一个slots指针数组, 可以指向需要处理的槽, 空代表不处理此槽, 使用两种记录的意义是, 提高发送槽信息和槽位置的效率.

  2. CLUSTER ADDSLOTS实现, 会先遍历所有的槽, 判断指派的槽是否被指派过了, 如果已被指派, 就返回错误, 否则, 再次遍历, 进行设置二进制值和指针(两个slots属性); 在集群中执行命令, 服务器会先检查命令想要处理的数据库属于哪个槽, 如果是自己的就处理, 如果不是, 返回MOVED错误并指引客户端去正确的节点处理.

  3. 计算键数据的槽: CRC16(key) & 16383 即可得到槽号, 然后检查clsterState.slots[i]的指针是否指向自己来判断是否应该由自己处理, 对于MOVED错误: MOVED <槽号> :;

    事实上, 一个集群模式下的客户端常常连着不止一个节点, 所以收到MOVED错误, 会跳转到另一个套接字来执行, 而如果没连接, 会先连接, 再跳转;

    节点数据库的实现, 和单机数据库一样, 但是只能使用0号数据库; 重新分片操作, 可以让已经被分派的节点分派给另一个节点, 此操作可以在线进行;

    重新分片实现原理: 重新分片的操作是由Redis的集群管理软件redis-trib负责执行, redis提供了所需的全部命令, 而redis-trib则通过对源节点和目标节点发送命令来进行操作. 具体步骤:

        

    1. 对目标节点发送: CLUSTER SETSLOT <slot> IMPORTING <source_id>, 让目标节点准备导入slot的键值对
    2. 对源节点发送: CLUSTER SETSLOT <slot> MIGRATING <target_id>, 来让源节点准备好把槽迁移至目标节点
    3. 向源节点发送: CLUSTER GETKEYSINSLOT <slot> <count>, 获取最多count个属于槽<slot>的键值对的键名
    4. 对于得到的每个键名, redis-trib都会向源节点发送一个MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>, 被选中的键会原子地从源节点迁移至目标节点
    5. 重复3, 4, 直到槽里面的左右的键值对都被迁移至目标节点为止
    6. redis-trib向集群中的任意一个节点发送CLUSTER SETSLOT <slot> NODE <target_id>命令 会告诉集群里面的所有节点此迁移信息.
    

如果客户端操作的节点正在被转移, 那么服务器会返回一个ASK错误, 然后引导客户端转向目标节点, 并再次发送之前想要执行的命令.

  1. 对于命令的实现, clusterState结构的import_slots_from记录了当前节点正在从其他节点导入的槽, 相对的, migrating_slots_to[i]保存了当前节点正在迁移至其他节点的槽, 对于此情况, 会告诉客户端;

    客户端会先发送ASKING(此命令会把客户端的ASK标识设为ASKING), 然后向目标节点重新发送命令;

    此时目标节点会检查客户端的ASKING标识, 如果是, 就临时为它执行此命令, 否则返回MOVED命令;

    MOVED错误在于槽不被此节点负责, 所以再次执行还会出现MOVED错误, 但是ASK错误一般就此一次, 因为下一次槽就被新的(原本旧的节点告诉客户端的节点)节点处理, 在这里就能找到槽, 所以ASK错误只会出现一次, 除非又转移了.

  2. 对于节点, 分为主从节点, 从节点用于在主节点挂了后重新成为新的主节点. 主节点负责处理槽, 这项工作由集群的其他主节点完成, 此节点会因为槽转移而拥有旧的主节点的槽;

    当旧的主节点再次上线, 会成为新的主节点的从节点. 对一个节点发送:

        

    CLUSTER REPLICATE <node_id> 
    

    会让接受命令的节点成为node_id节点的从节点, 并开始对主节点进行复制;

    节点通过发送PING来进行故障检测, 如果没及时返回, 会被记录为PFAIL(疑似下线), 同时, 各个节点还会互相交换信息;

    如果某个节点有半数主节点被认为PFAIL, 那么它会被标记成FAIL(下线);

    接下来就是故障转移(选取新的主节点, 然后复制转移成为新的主节点), 接下来是投票选取新的主节点.

  3. 节点是通过消息完成通信的, 一共有5种主要的消息:

        

    MEET(请求接受者加入当前集群);
    PING(判断是否在线);
    PONG(对PING的回复);
    FAIL(告诉其余所有节点某个节点已下线);
    PUBLISH(当节点接收到此命令, 也会执行此命令, 向其他节点发送此命令, 就像链式反应, 一个接一个);
    

    节点发送的消息类似HTTP消息, 具有消息头, 里面包含了发送者的信息(包括, 消息长度, 消息类型, 消息正文包含的节点信息数量, 既有几个消息, 发送者所处的配置纪元(均是主节点的配置纪元), 发送者的名字, 发送者目前的槽指派信息, 当前复制的主节点的名字, 发送者的端口号, 发送者的标示值, 发送者所处的集群状态, 消息的正文(内容)).

  4. MEET, PING, PONG, 会随机选取两个节点作为目标发送;

    FAIL信息传播迅速, 可以立刻告诉所有主节点关于某一主节点的下线信息;

    向某一节点发送PUBLISH会导致所有节点都向channel频道发送消息

  5. 如果某些客户端订阅了一个频道, 那么向此频道发送命令, 会让订阅此频道的客户端都接收到此命令;

    除了SUBSCRIBE命令外, 还有PSUBSCRIBE命令让某客户端订阅某个模式, 每当有客户端向某个频道发送消息时, 不仅频道的订阅者会接受到, 与之匹配的模式的订阅者也会接收到命令.

  6. 当客户端执行SUBSCRIBE命令订阅某个或某些频道时, 客户端与被订阅的频道之间就建立了一种订阅关系.

    Redis将所有频道的订阅关系都保存在服务器状态的: pubsub_channels字典里面. 这个字典的键是某个被订阅的频道, 值是一个链表, 里面记录了所有订阅此频道的客户端. 类似频道, 模式的订阅关系也由Redis数据库保存;

    而pubsub_patterns属性是一个链表, 链表的节点是一个订阅关系, 节点的pattern属性记录了被订阅的模式, client属性记录了订阅模式的客户端.

  7. 查看订阅信息的命令:

    

1. PUBSUB CHANNELS <pattern>: 返回所有与模式pattern相匹配的频道, 如果不指定pattern则会返回所有频道
2. PUBSUB NUMSUB <channel-1 channel-2 ... channel-n>: 返回频道的订阅者的数量
3. PUBSUB NUMPAT: 返回服务器当前被订阅的模式的数量
  1. 事务: 提供了一种将多个命令请求打包, 然后一次性, 按顺序地执行多个命令的机制, 并且在事务执行期间, 服务器会被阻塞, 不会停下来去执行别的任务.

    实现事务功能的命令有:

        

    MULTI;
    EXEC;
    WATCH;
    

    等命令.

    事务的实现通常会经历以下三个阶段:

        

    事务开始;
    命令入队;
    事务执行;
    
  2. MULTI命令的执行标志着事务的开始, 既把客户端切换到事务状态;

    命令执行:

        

    对于客户端发送的命令, 要是EXEC, DISCARD, WATCH, MULTI四个命令其中一个时, 服务器会立即执行这个命令;
    否则会把这个命令放入事务队列里面去, 并返回QUEUED回复;
    每个客户端都有自己的事务状态, 这个事务状态保存在客户端状态的mstate属性里面, 事务状态包含一个事务队列, 以及一个已入队的命令的计数器;
    事务队列是一个multiCmd类型的数组, 数组中的每个元素都保存了一个已入队的命令的相关信息, 包含指向命令实现的函数, 命令的参数, 以及参数的数量. 当客户端执行EXEC命令时, 服务端会执行事务队列的全部命令, 并返回所有命令的执行结果.
    
  3. WATCH命令是一个乐观锁, 它会监视任意数量的数据库的键, 在执行EXEC命令时, 会检查被监视的键是否至少有一个已经被修改, 如果是, 那么拒绝执行事务, 并返回空回复.

    每个数据库都有一个watched_keys字典, 字典的键是某个正在被WATCH命令监视的数据库键, 字典的值是一个链表, 链表元素的监视此键的客户端. 任何对键进行修改的命令都会触发不安全键函数, 这会让服务器根据watched_keys字典把所有监视此键的客户端的不安全标识打开, 当这些数据库触发EXEC时, 会返回错误, 因为键已被修改.

  4. 事件的ACID性质:

    

原子性: 对于操作, 要么全部执行, 要么一个都不执行, 所以Redis事务是原子性的
一致性: 如果数据库是一致的, 那么不管事务执行是否成功, 数据库都应该还是一致的
隔离性: 即使数据库有多个任务在执行, 各个事务也不会互相影响, 且事务执行结果相同
耐久性: RBD模式下的事务不具有耐久性(因为只有满足特定条件才会触发RDB保存), 一旦数据库停机, 数据就会丢失, 只有AOF且appendfsync设为always时才会拥有持久性
  1. Redis的SORT命令可以对列表键, 集合键, 或者有序集合键的值进行排序. 还有SORT BY, ASC, DESC, ALPHA, LIMIT, STORE, GET, BY命令.

  2. SORT命令的使用以及实现:

    

SORT <key>: 这个命令可以对一个包含数字值的键key进行排序, 也就是说, key的元素都是数字. SORT [number]实现: 创建一个和number等长的数组, 数组的元素是redisSortObject结构, 这个结构有两个属性(obj指针和double类型的u), 其中, u用来记录值, obj指向num[i], u就是num[i]的值, 然后根据u排序, 得到排序结果, 遍历, 向客户端返回新顺序的数组的obj指向的num的值
SORT <key> ALPHA: 这个命令根据包含字符串的键进行排序, 命令的实现: 创建一个redisSortObject数组, 遍历数组, 将各个数组项的obj指向字符串集合的各个元素, 根据obj所指向的集合元素, 在字典中的顺序, 进行排序(交换指针位置), 完成排序
SORT <key> ASC: 这是一个升序的选项, SORT <key> DESC: 这是一个降序的选项, 排序默认升序
SORT <key> BY: 根据指定的域的值作为关键词来进行排序, 实现: 创建一个redisSortObject数组; 遍历数组, obj指针指向集合的各个元素; 遍历数组, 根据obj指针所指向的元素, 以及BY选项所给定的模式, 查找相应的权重键; 把权重键转换成double类型的浮点数, 并保存在u属性里面; 以u属性作为权重, 对数组进行排序, 得到新的数组; 遍历数组, 依次把obj指向的元素返慢查询回给客户端
SORT <key> BY <?> ALPHA: BY选项默认权重键是数字, 如果是字符串, 那么还要配合ALPHA选项, 实现: 依旧遍历两遍数组, 找到权重键(字符串), 进行排序, 再次遍历, 返回obj指向的元素.
SORT <key> [可选] LIMIT <offset> <count>: offset表示要跳过的元素的数量, count表示跳过给定数量的元素后, 要返回的元素的数量, 实现: 创建数组, 排序, 把指针定位到offset上, 依次返回count个元素
SORT <key> [可选] GET <模式>: GET命令可以选择某些键的值而不是全部的键, 实现: 排序, 遍历, 根据GET指定的模式, 查找相应的键, 返回查找到的键. 对于多个GET, 会进行多次选择, 然后返回结果.
SORT <key> [可选] STORE <新的键名>: STORE命令可以保存排序结果, 保存在新的键里面. 实现: 排序, 检查新的键名是否存在, 存在就删除, 不存在就创建一个列表键, 遍历数组, 依次把元素压入列表里面, 然后遍历数组, 向客户端返回排序结果.
  1. 排序条件的执行顺序: 排序->限制长度->获取外部键(GET选项)->保存排序的结果集->向客户端返回结果; 以上顺序不能出现后面的比前面的先执行. 同时, 选项的摆放顺序不会影响排序时的执行顺序.

  2. Redis提供了SETBIT, GETBIT, BITCOUNT, BITOP四个命令用于处理二进制位数组. 其中SETBIT可以为位数组指定偏移量上的二进制位设置值, 偏移量从0开始计数, 从右往左数, 二进制位的值可以是0也可以是1, 用法:

    

SETBIT bit <index> [0/1];
GETBIT则用于获取位数组指定偏移量上的二进制位的值, 用法: GETBIT bit <index>;
BITCOUNT命令用于统计位数组里面, 值为1的二进制位的数量, 用法: BITCOUNT bit;
最后BITOP命令既可以对多个位数组进行按位与, 按位或, 按位异或运算, 也能对给定的位数组进行取反.
  1. Redis使用字符串对象(SDS)来表示位数组, 且也使用SDS结构的操作函数来处理位数组.

  2. 慢查询日志功能, 用于记录执行时间超过给定时长的命令. 服务器的两个配置:

    

slowlog-log-slower-than选项记录执行时间超过n毫秒的命令请求会被记录到日志上;
slowlog-max-len选项指定服务器最多保存多少条慢查询日志
  1. SLOWLOG GET指令用于显示慢查询日志.

  2. 客户端发送MONITOR命令可以让客户端成为监视器, 这样可以接收并打印被监视的服务端当前处理的命令请求的相关信息.