基础

  • Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
  • Redis使用引用计数实现内存回收
  • Redis 有多个数据库,默认使用数据库0
  • Redis使用epoll实现io复用,并且单线程,除了一些备份数据的操作fork多进程,其他均是单线程

内部结构

SDS

struct sdshdr {
    // 记录 buf 数组中已使用字节的数量
    // 等于 SDS 所保存字符串的长度
    int len;
    // 记录 buf 数组中未使用字节的数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
};
  • 一个可以存储二进制的字符串结构而已,类似string
  • 二进制安全

使用场景

  • 所有的key,以及string的Val类型,以及其他类型的字符串存储

链表

typedef struct listNode {
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 节点的值
    void *value;

} listNode;
  • 朴实无华的双向链表

使用场景

  • list数据结构

字典

typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;
typedef struct dictEntry {
    void *key;// 键
    union {
        void *val;// 值
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;

} dictEntry;
  • unordered_map,哈希表
  • 使用拉链法解决hash冲突

信息

  • ht[1]只用于rehash(hash扩容)使用
  • size为2的倍数,方便按照倍数扩容
  • hash算法使用MurmurHash3
  • 计算过程为先算出hash,然后和size &运算(保证不会超出范围),冲突时插入时候优先插入链表头部

rehash

服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ; 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5

  1. 渐近式,不会出现突发性能问题(还是用空间换时间)
  2. 按照倍数扩大
过程
  1. ht[1] 分配空间, 让字典同时持有 ht[0]ht[1] 两个哈希表。
  2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
  3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
  4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
  • 标识开始hash,在每次操作时候进行,按照顺序
  • 均为单线程操作
操作
  1. 查找先在原来的查,找不到再查新的,删除,改变归同时操作两个
  2. 增加只在新的增加

使用场景

  • 键值对的map映射

跳表

跳表

  • 就是一个二维链表,从有序链表构建
  • 最上层开始查,下一个比他小就向右,否则向下
  • 插入数据后random,50%的概率向上插入,递归向上遍历

优缺点

  • 实现简单,比平衡树简单多了
  • 比B树占用内存更少
  • 缓存局部性

确定层数

  • level=log2(n) - 1,每一层是下一层的一半
typedef struct zskiplistNode {
    // 后退指针
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成员对象
    robj *obj;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
}

使用场景

  • 需要排序的zset

为什么不用B+树

  1. 实现简单:跳表相对于B+树来说,实现起来更为简单,代码量也更少。这使得跳表更容易维护和优化。
  2. 更适合内存访问模式:Redis通常将数据存储在内存中,而跳表更适合于随机访问模式,能够充分利用CPU高速缓存,提高查询效率。
  3. 查询性能表现优秀:跳表的查找、插入、删除等操作都可以在O(log n)的时间复杂度内完成
  4. 范围查询不常用

整型数组

  • 小型整数list用于节省内存的做法,底层就是数组,略

压缩列表

  • listpack,小型只包含整数以及短串的list,节省内存做法.底层就是紧密排列的char数组.略

总结

  1. string 使用SDS实现
  2. list 压缩列表(小型)或者链表实现
  3. hash 使用压缩列表(小型,直接追加在尾部,查找直接遍历),或者字典实现
  4. set 整型数组或者字典
  5. zset 压缩列表(直接按照score排序)或者跳表(实际上又用字典再次保存了元素的val->score)实现

单机数据库

过期设置

2024111117145349pasteboard.paste

  • 使用另外一个dict储存按键的过期时间,添加过期时间时候插入链表
  • 拿出时候和目前的time比较

常见删除策略

  1. 惰性删除(找key时候看一下,过期返回nil并且删除),对内存不友好
  2. 定时删除(定时遍历key查),对CPU不友好

redis策略

  • 惰性删除和定期删除的结合
  1. 每个key都执行惰性删除
  2. 服务器周期性执行删除(每次有最大时间限制,随机抽取)

回收策略

  • 当redis内存占用太多了,超过限制了就会触发回收

策略

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(默认),不会准确的删除所有键中最近最少使用的键,而是随机抽取3个键,删除这三个键中最近最少使用的键,抽取值可以设置,使用LFU策略
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

备份

  • 实际上redis的备份应该是RDB备份,备份期间产生的请求直接使用AOF追加在文件结尾

RDB备份

  • 一种压缩的二进制文件,储存当前redis所有key和val等信息,实现细节较为复杂,略
  • 可以使用同步或者异步备份,默认不开启
  • SAVE同步执行,BESAVE异步fork执行,因为是fork,读享写复,redis在此期间操作不影响RDB文件
  • 优点: 体积小,效率高 缺点:key写入后如果改变无法写入(除非重写)
  • 缺点: 最后一次备份可能数据丢失,在两次备份间隔中如果出现宕机,那么中间的数据就会丢失(核心问题是RDB写了之后不能修改导致写的时候发生的请求仅仅用RDB无法记录)

AOF备份

  • 一种保存压缩命令的文件,类似状态机,通过重写复原数据库状态
  • 默认开始,可以通过重写减少体积
  • 优点: 可以实时更新 缺点:文件体积太大

命令编码

  • SET key value
  • *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n,SDS串

命令请求过程

  1. 客户端编码命令并发送
  2. 服务端解析命令
  3. 预备操作(大部分是一些检查)
  4. 调用命令所在的函数指针(调用处理函数)
  5. 执行后续操作并发送结果

多机数据库实现

一致性hash算法(一致性哈希)

  • 一致性hash都不是缓存机器自身的功能,而是集群前置的代理或客户端实现的。而redis官方的集群是集群本身通过slots实现了数据分片。

实现原理

请求
  • 先对机器进行hash后对2^ 32-1取模(这个hash函数可以选择其他的),放到圆环上面
  • 数据请求打入时候,顺时针找到下一个node的位置
  • 如果出现数据倾斜的时候可以使用虚拟节点
虚拟节点
  • 为了使得机器分布均匀使得负载均衡,添加些虚拟节点,节点的ip指向已经存在的机器而非新机器

优点

  • 扩容和宕机处理方便,只需要改变一小部分,扩展性强

缺点

  1. 一致性哈希的某个节点宕机或者掉线后,当该机器上原本缓存的数据被请求时,会从数据源重新获取数据,并将数据添加到失效机器后面的机器,这个过程被称为 "缓存抖动" ,而使用哈希槽的节点宕机,会导致一定范围内的槽不可用,只能通过主从复制加哨兵模式保证高可用。
  2. 当某台机器宕机时,极易引起雪崩,如上述介绍中删除节点。
    • 删除节点就会把当前节点所有数据加到它的下一个节点上。这样会导致下一个节点使用率暴增,可能会导致挂掉,如果下一个节点挂掉,下下个节点将会承受更大的压力,最终导致集群雪崩。
  3. 哈希槽,一致性哈希算法更复杂

redis运行模式

单机模式

上文

  • 优点:简单 缺点:有性能瓶颈

主从复制

  • 将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
  • 读取可以从slave读,但是写入转发到master写
  • 优点: 读压力分担给从服务器 缺点:写瓶颈和储存瓶颈都在master服务器上面,宕机之后需要人工干预

哨兵模式

  • 哨兵是一种特殊的redis服务器,也有自己的master,通过raft算法选举
  • 负责副redis服务器监控,类似心跳包,检查master存活状态,
  • 发现master宕机之后马上选举新的master,并且通知slave,实现自动故障恢复
  • 优点:宕机自动恢复可靠性高 缺点:在线扩容复杂,还是有主从的缺点

集群模式

  • 把key经过hash之后放到16384个slots(一个二进制数组char bits[2048],2048*8=16384)
  • 把slots指派给不同的redis master处理,当key到来时候,先计算hash,在发送MOVE重定向到正确的服务器(类似于负载均衡)
  • 通过发送自己的二进制bits,告诉别人自己负责的部分
  • 优点:动态扩容简单,重新分片即可
MOVE和ASK
  • MOVE是槽的时候机器计算key不止自己负责的,就会永久重定向到负责的机器
  • ASK则是,临时重定向,代表临时错误,一半不做处理(比如整个集群正在迁移)
崩溃恢复

总结

  • 目前通常使用集群模式管理key,然后每个分片通常配置主从模式和哨兵模式,保证高可用性和可靠性
  • 首先使用slot的机制(这个也可以换成一致性hash)分配到某些集群,每个集群中使用主从机制工作,同时配备哨兵模式容灾

同步

旧版

  1. 使用sync命令开始
  2. 生成RDB文件,并在内存开辟缓冲区,实时写入当前的持续操作(类似AOF)
  3. master把结果发生给slave
  • 缺点:断线重链复制东西太多了

新版

  1. 使用psync命令开始
  2. 给oper标号,master和slave分别保存目前oper标号
  3. master保存一段操作的oper到内存(大约1M内存)
  4. 断线之后slave发送自己目前的oper标号
  5. master对比,如果内存还保存有,就直接发送内存里对应的内容,否则和旧版一样全发送

独立功能

订阅频道

  • 订阅使用字典存储key,val使用链表结构,订阅者会插入链表头
  • key更改会先去找有没有订阅者

事务

  1. watch监控变量是否改变,multi开始事务,exec开始执行事务
  2. 执行前检查watch变量是否改变,改变则不执行事务,执行中途失败不会回滚
  • 使用 set 等操作修改key 时候会遍历这个key 对应的watch, 将对应的 REDIS_DIRTY_CAS 打开, 此时提交事务检查这个标识就会发现从而触发

others

慢日志查询

  • 记录查询事件超过某个特定事件的操作

监控器

  • 客户端可以变为监视器,开始监视服务端的所有操作

参考

  • redis设计与实现,https://1e-gallery.redisbook.com/
  • https://www.cnblogs.com/zhonglongbo/p/13128955.html
  • https://zhuanlan.zhihu.com/p/179266232 推荐书籍
  • redis设计与实现