• 持久化
      • 快照RDB

    全量备份,二进制,存储紧凑
    使用操作系统的COW机制实现快照持久化。
    使用fork创建子进程,
    子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存 数据结构进行不间断的修改。 这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复 制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的, 还是进程产生时那一瞬间的数据。

    • AOF日志

    内存数据修改的指令记录文本,重启时会重放,需要定期AOF重写来瘦身。
    Redis 会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文本存储到 AOF 日志中,也就是先存到磁盘,然后再执行指令。
    bgrewriteaof 指令用于对 AOF 日志进行瘦身的原理:开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。 序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。

    fsync
    写AOF文件时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘的。
    每秒执行一次fsync操作。周期可配置
    其他策略:永不fsync,每条指令都fsync

    Redis主节点不进行持久化,从节点来做持久化。

    • 混合持久化

    前一部分用rdb恢复,后小部分用aof持久化
    用rdb会丢失大量数据,为什么?

    aof比较慢。
    redis 4.0新增混合持久化。
    Redis深度历险 - 图1
    重启以后先加载rdb内容,然后重放增量AOF日志。

    • Redis管道 Pipeline

    客户端通过对管道中的指令列表改变读写顺序就可以大幅节省 IO 时间。管道中指令越多,效果越好。

    本质
    Redis深度历险 - 图2
    write不需要等待对方收到消息才返回,只要发送缓冲没有满,就会立即返回,否则,需要阻塞等待,这才是写操作IO耗时。
    read也同理,只有缓冲是空的,才是读IO操作的真正耗时。
    而对于管道来说,连续的 write 操作根本就没有耗时,之后第一个 read 操作会等待一个 网络的来回开销,然后所有的响应消息就都已经回送到内核的读缓冲了,后续的 read 操作 直接就可以从缓冲拿到结果,瞬间就返回了。

    • 事务

    multi 指示事务的开始, exec 指示事务的执行,discard 指示事务的丢弃。
    不具有原子性,当某个中间指令失败了,后面的指令还会继续执行。
    discard 指令,用于丢弃事务缓存队列中的所有指令。
    事务常常结合pipeline一起使用,可以将多次IO操作压缩为单次IO操作。
    watch,乐观锁,要在multi之前就开始watch,当发现关键变量自watch后被修改了,exec会返回null表示执行失败。

    • PubSub

    subcribe 订阅
    publish 发布
    模式订阅,如*匹配任何
    消息结构:
    {‘pattern’: None, ‘type’: ‘subscribe’, ‘channel’: ‘codehole’, ‘data’: 1L}
    data 是消息的内容,一个字符串。
    channel 表示当前订阅的主题名称。
    type 表示消息的类型,如果是一个普通的消息,那么类型就是 message,如果是控制消息,比如订阅指令的反馈,它的类型就是 subscribe,如果是模式订阅的反馈,它的类型就 是 psubscribe,还有取消订阅指令的反馈 unsubscribe 和 punsubscribe。
    pattern 表示当前消息是使用哪种模式订阅到的,如果是通过 subscribe 指令订阅的, 那么这个字段就是空。

    缺点:
    消息不是持久化的,重启会消失。另外如果消费者不在,消息就直接扔了。
    几乎找不到合适的应用场景。
    redis 5.0出了Stream数据结构,是持久化的,PubSub可以消失了。

    • 小对象压缩

    32bit和64bit:
    如果内存不超过4G,使用32bit可以节约大量内存。
    如果不足可以通过增加实例来解决。

    ziplist
    每个元素紧挨着
    Redis深度历险 - 图3
    如果它存储的是 hash 结构,那么 key 和 value 会作为两个 entry 相邻存在一起。
    如果它存储的是 zset,那么 value 和 score 会作为两个 entry 相邻存在一起。
    object encoding可以用来查看某个key是以什么数据结构存储的
    intset用来存整数且元素个数较少的set集合。动态升级,从16位到32位到64位。
    Redis深度历险 - 图4
    如果 set 里存储的是字符串,那么 sadd 立即升级为 hashtable 结构。
    存储界限
    当集合对象的元素不断增加,或者某个 value 值过大,这种小对象存储也会被升级为标准结构。

    内存回收机制
    操作系统回收内存是以页为单位,如果这个页上只要有一个 key 还在使用,那么它就不能被回收。
    flushdb会删除所有key,所以内存马上就回收了。

    内存分配算法
    用jemalloc管理内存,也可以切换到tcmalloc。

    • 主从同步
      • CAP
      • C 一致性
      • A 可用性
      • P 分区容忍性

    网络分区发生时,一致性和可用性两难全。

    跳过了集群和拓展部分

    • 字符串源码分析

    Simple Dynamic String
    struct SDS {
    T capacity; // 数组容量
    T len; // 数组长度
    byte flags; // 特殊标识位,不理睬它
    byte[] content; // 数组内容
    }

    字符串可以修改,append操作类似于ArrayList的add,如果没有足够空间,就重新分配。
    Redis深度历险 - 图5

    用泛型T的原因是有的字符串很短,可以用short或者byte表示。是为了内存优化
    字符串长度不得超过512MB
    创建时len和capacity一样长,绝大多数场景不会用append操作。

    embstr vs raw
    字符串有两种存储方式,特别短时用emb形式,长度超过44时,用raw形式存储。

    区别?分界线为什么是44?
    Redis对象都有一个对象头结构体,16字节
    struct RedisObject {
    int4 type; // 4bits 表示类型
    int4 encoding; // 4bits 同一个类型的type也有不同的存储形式
    int24 lru; // 24bits 记录LRU信息
    int32 refcount; // 4bytes 引用计数,0时对象销毁
    void *ptr; // 8bytes,64-bit system 对象内容具体存储位置
    } robj;

    SDS结构体
    Redis深度历险 - 图6
    显然sds对象头的大小是16+3

    embstr是一次性分配内存,RedisObject 对象头和 SDS 对 象连续存在一起
    raw需要两次

    内存分配器 jemalloc/tcmalloc 等分配内存大小的单位都是 2、4、8、16、32、64。
    大字符串,总体超出64字节。
    当分配64字节,有19用来存头,还有一个位置要存0用来标识字符串结束, 所以有44个位置可以存字符串。

    扩容策略
    字符串小于1M之前,加倍扩容。
    大于1M之后,每次增加1M。

    什么场景会用到字符串的append方法?

    • 字典源码

    • 压缩列表ziplist

    struct ziplist {
    int32 zlbytes; // 整个压缩列表占用字节数
    int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
    int16 zllength; // 元素个数
    T[] entries; // 元素内容列表,挨个挨个紧凑存储
    int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
    }
    Redis深度历险 - 图7
    struct entry {
    int prevlen; // 前一个 entry 的字节长度
    int encoding; // 元素类型编码
    optional byte[] content; // 元素内容
    }
    encoding通过前缀位来区分内容。
    content是可选的。
    ziplist是紧凑存储,没有冗余空间,插入新元素需要realloc扩展内存。不适合存储大型字符串,也不适合存太多元素。
    级联更新
    前面提到每个 entry 都会有一个 prevlen 字段存储前一个 entry 的长度。如果内容小于254 字节,prevlen 用 1 字节存储,否则就是 5 字节。这意味着如果某个 entry 经过了修改 操作从 253 字节变成了 254 字节,那么它的下一个 entry 的 prevlen 字段就要更新,从 1 个字节扩展到 5 个字节;如果这个 entry 的长度本来也是 253 字节,那么后面 entry 的 prevlen 字段还得继续更新。 如果 ziplist 里面每个 entry 恰好都存储了 253 字节的内容,那么第一个 entry 内容的 修改就会导致后续所有 entry 的级联更新,这就是一个比较耗费计算资源的操作。

    ziplist

    • zset和hash容器元素较少时
    • 连续内存空间,元素紧挨着,没有冗余空间
    • 为了支持双向遍历,有一个ztail_offset

    IntSet 小整数集合
    struct intset {
    int32 encoding; // 决定整数位宽是 16 位、32 位还是 64 位
    int32 length; // 元素个数
    int contents; // 整数数组,可以是 16 位、32 位和 64 位
    }

    • 快速列表 quicklist

    Redis深度历险 - 图8

    默认单个 ziplist 长度为 8k 字节,超出了这个字节数,就会新起一个ziplist。
    默认压缩深度是0,也就是不压缩。
    压缩深度为1表示首尾不压缩,这样可以支持快速的push/pop操作。

    • 跳跃列表

    结构
    Redis深度历险 - 图9
    Redis深度历险 - 图10
    struct zsl {
    zslnode header; // 跳跃列表头指针
    int maxLevel; // 跳跃列表当前的最高层
    map<string, zslnode
    > ht; // hash结构的所有键值对
    }

    struct zslnode {
    string value;
    double score;
    zslnode[] forwards; // 多层连接指针
    zslnode
    backward; // 回溯指针
    }

    晋升概率ZSKIPLIST_P = 25%
    插入过程:首先在搜索合适插入点的过程中将「搜索路径」摸出来了,然后就可以开始创建新节点了,创建的时候需要给这个节点随机分配一个层数,再将搜索路径上的节点和这个新节点通过前向后向指针串起来。如果分配的新节点的高度高于当前跳跃列表的最大高度,就需要更新一下跳跃列表的最大高度。
    修改过程:一个简单的策略就是先删除这个元素,再插入这个元素,需要经过两次路径搜索。
    假设这个新的 score 值不会带来排序位置上的改变,那么就不需要调整位置,直接修改元素的 score 值就可以了。

    如果 score 值都一样呢?
    zset 的排序元素不只看 score 值,如果 score 值相同还需要再比较 value 值。

    struct zslforward {
    zslnode* item;
    long span; // 跨度
    }
    rank计算方法,将搜索路径上的所有节点的span加起来。

    • 紧凑列表 listpack

    struct listpack {
    int32 total_bytes; // 占用的总字节数
    int16 size; // 元素个数
    T[] entries; // 紧凑排列的元素列表
    int8 end; // 同 zlend 一样,恒为 0xFF
    }

    ziplist的改进,更节省,更精简。设计目的是取代ziplist。由于ziplist使用太广泛,所以替换复杂度很高。目前只在Stream中用。
    Redis深度历险 - 图11
    struct lpentry {
    int encoding;
    optional byte[] content;
    int length; // 当前元素的长度
    }

    不需要存zltail_offset,因为可以直接算出来,用total_bytes和最后一个元素长度

    • 基数树

    跳过