数据结构

简单动态字符串(SDS)

struct sdshdr{
    int len;//SDS所保存字符串长度,等于buf数组已使用字节的数量
    int free;//buf数组未使用字节的数量
    char[] buf;//字节数组,用于保存字符串
}

image.png

SDS与c语言字符串区别
1.SDS保存字符数组长度,计算字符串长度时间复杂度为O(1),c字符串为O(n)
2.SDS在字符串拼接操作中会先检查内存空间是否满足,不满足则自动拓展空间,避免了c字符串拼接时存在空间溢出的问题 
3.SDS存在空间预分配机制,即SDS在对字符串进行拓展修改时,redis不仅会对修改增加必要内存,同时预先分配同等长度的未使用空间
4.SDS API在对字符串进行缩短修改时不会立即回收空余内存,而是记录在free字段方便字符增长使用 
为什么Redis选择这样的数据结构?

双端链表

(数组 + 双向链表)

image.png

字典

底层实现为hash table,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。

image.png

字典结构
typedef struct dict {
    dictType *type;     // 特定类型函数
    void *privdata;     // 私有数据,保存着dictType结构中函数的参数。
    dictht ht[2];       // 两张哈希表。
    long rehashidx;     // rehash标记,-1表示没在进行rehash
} dict;
哈希表结构
typedef struct dictht { //哈希表
    dictEntry **table;      //哈希表数组地址
    unsigned long size;     //哈希表大小,初始化大小为4
    unsigned long sizemask; //哈希表大小掩码,用于计算索引值(总是等于size-1)
    unsigned long used;     //记录哈希表已有的节点(键值对)数量。
} dictht;
哈希表节点
typedef struct dictEntry {
    void *key;  // 键
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;    //值
    struct dictEntry *next;     //指向下一个hash节点,形成链表
} dictEntry;
rehash操作
扩容或者收缩hash表
扩容
1.服务器目前没有执行BGSAVE或者BGREWRITEAOF命令,且哈希表负载因子>=1
2.服务器目前正在执行BGSAVE或者BGREWRITEAOF命令正在执行,且哈希表负载因子>=5
收缩
哈希表哈希表负载因子<0.1
渐进式rehash

跳跃表

有序集合键的底层实现之一,有序集合包含的元素数量比较多,或者有序集合中元素是比较长的字符串时,redis会使用跳跃表作为有序集合的实现。支持平均O(log(N)),最坏O(N)复杂度查找。

image.png

typedef struct zskiplist {
    // 表头节点与表为节点
    struct zskiplistNode *header, *tail;  
    // 表中节点数量
    unsigned long length;  
    // 层数最大的节点的层数
    int level;  
} zskiplist;  
1.header:指向跳跃表的表头结点,表头结点实际上是一个伪结点,该结点的成员对象为NULL,分值为0,它的层数固定为32(层的最大值)
2.tail:指向跳跃表的表尾结点,表尾结点是表中最后一个结点。通过这两个指针,定位表头结点和表尾结点的复杂度为O(1)
4.length:记录结点的数量,程序可以在O(1)的时间复杂度内返回跳跃表的长度。
5.level:记录跳跃表的层数,也就是表中层高最大的那个结点的层数(表头结点的层高并不计算在内)
typedef struct zskiplistNode {  
    // 层
    struct zskiplistLevel { 
        struct zskiplistNode *forward;  //前进指针
        unsigned int span;  // 跨度
    } level[];  

    robj *obj;  //成员对象
    double score;  //分值
    struct zskiplistNode *backward; //  后退指针
    
} zskiplistNode;  
1. 层:每次创建一个新的跳跃表节点时,程序会根据幂次定律(越大的数出现概率越小)随机生成一个介于1~32之间的值作为节点level[]的大小
2. 跨度:记录两个节点之间的距离,用于计算排位:在查找某个节点时,将沿途访问过的所有节点的跨度累计起来,就是目标节点在跳跃表中的排位值
3. obj:该结点的成员对象指针
4. score:该对象的分值,浮点数,跳跃表中结点根据score从小到大来排序的。同一个跳跃表中,各个结点保存的成员对象必须是唯一的,但是多个结点保存的分值却可以是相同的,分值相同的结点将按照成员对象的字典顺序从小到大进行排序
5. 后退指针:用于从表尾向表头方向访问结点。通过结点的前继指针,组成了一个普通的链表。因为每个结点只有一个前继指针,所以只能依次访问结点,而不能跳过结点

整数集合

当一个集合只包含整数元素时,并且集合的元素数量不多时,redis会使用整数集合作为集合键的底层实现。
整数集合的底层实现为数组,以有序,无重复方式保存集合元素
typedef struct intset {
    // 编码方式
    uint32_t encoding;
    // 元素数量
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];

} intset;

image.png

压缩列表

列表键和哈希键的底层实现之一


对象

对象构成

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 指向底层实现数据结构的指针
    void *ptr;
    // ...
} robj;

类型

REDIS_STRING    字符串对象
REDIS_LIST        列表对象
REDIS_HASH        哈希对象
REDIS_SET        集合对象
REDIS_ZSET        有序集合对象

字符串

列表

集合

有序集合

有序集合的使用场景

热点资讯排行榜,根据点击量,时间等进行排序

哈希

持久化

RDB

RDB文件是一个经过压缩的二进制文件 
RDB 文件生成

1.SAVE命令 :阻塞服务器进程直至RDB文件生成完毕,阻塞期间服务器不接受任何命令请求
2.BGSAVE:派生子进程创建RDB,父进程继续接受命令请求。可通过配置文件设置BGSAVE执行的频率

    save 900 1  //服务器在900秒内数据库执行至少1次修改
    save 300 10 //服务器在300秒内数据库执行至少10次修改
    save 60 10000  //服务器在60秒内数据库执行至少10000次修改
RDB文件载入
服务器在启动时只要检测到RDB文件存在,就会自动载入RDB文件(AOF持久化关闭前提下),服务器在RDB载入过程中处于阻塞状态

AOF

通过保存服务器执行的写命令保存数据库状态,AOF文件的更新频率通常比RDB文件高
命令追加
服务器会在写请求完成后将写命令追加到aof缓冲区的末尾
文件写入与同步

服务器配置appendfsync的值决定aof缓冲区何时写入AOF文件和同步aof文件,默认为everysec

always:总是将缓冲区命令写入aof文件,并进行aof文件的同步
everysec:每隔一秒将缓冲区命令写入aof文件,并进行aof文件的同步 
no: 由操作系统决定合适将缓冲区中写命令写入aof文件,但不进行aof文件的同步
AOF文件重写(BGREWRITEAOF)

针对aof文件体积膨胀问题 aof重写通过读取当前数据库状态反向生成,并非通过现有aof文件,aof重写程序由子进程执行 父进程在子进程执行重写期间将新的写请求写入 AOF重写缓冲区。当子进程完成重写后,父进程会将aof重写缓冲区写请求写入新的aof文件,并代替旧aof文件,此过程父进程会阻塞

键过期时间

redis中数据库结构主要由dict字典与expires字典构成,前者负责保存键值对,后者则负责保存键的过期时间

image.png

EXPIRE key ttl -- 设置键过期时间为ttl秒
PEXPIRE key ttl -- 设置键过期时间为ttl毫秒
EXPIREAT key timesmap -- 设置键过期时间为timesmap秒时间戳
PEXPIREAT key timesmap -- 设置键过期时间为timesmap毫秒时间戳

键过期判定 -- TTL或者PTTL命令,执行命令返回值>=0,说明键未过期

数据淘汰策略

  • volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
  • volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
  • allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
  • allkeys-random:从数据集中任意选择数据淘汰
  • no-enviction(驱逐):禁止驱逐数据

事务

事务常用指令

MULTI、EXEC、DISCARD、WATCH

事务在执行过程中发生错误,redis不会中断事务的执行,它会继续执行剩下的命令,不会rollback

主从复制模型

同步阶段

1. 从服务器向主服务器发送 sync命令 
2. 主服务器接受到 sync命令,开启BGSAVE操作生成RDB文件,并通过缓冲区记录此期间新的写请求命令
3.主服务器发送RDB文件至从服务器,从服务器载入RDB文件
4.主服务器发送缓冲区写命令至从服务器,从服务器执行写命令使数据库更新至主服务器当前状态

命令传播阶段

主服务器会在同步阶段结束以后将新的写命令发送至从服务器 
PSYCN命令

即部分重同步,解决在命令传播阶段断线后重新执行全量同步耗时长效率低问题,命令传播阶段断线后从服务器通过向主服务发送PSYNC命令请求部分重同步,携带自身的复制偏移量,主服务通过对比自身复制偏移量与从服器的复制偏移量,假如从服务器复制偏移量之后的数据仍然存在与复制积压缓冲区中则执行部分重同步否则执行全量同步

部分重同步实现

主服务器和从服务器的复制偏移量
主服务器的复制积压缓冲区
服务器的运行ID

哨兵模型

哨兵监控主服务器

哨兵默认每10秒一次向主服务器发送info命令,分析回复获取当前主服务器的情况 哨兵服务器与主从服务器之间的连接分两类:命令连接和订阅连接,其中订阅连接用于在多个哨兵系统广播某个服务器的信息,多个哨兵服务器之间只创建命令连接 

检测主服务器下线机制

1.主观下线:当某个哨兵通过向主服务器发送ping命令,在设定时间内无法收到有效恢复时判定主服务器为主观下线 
2.客观下线检测:当某个哨兵判定主服务为主观下线状态时,会通过命令询问其他哨兵服务器该主服务器是否为主观下线状态,超出设定数量哨兵系统判定为主观下线时可判定为客观下线 

哨兵领头选举

执行故障转移操作

故障转移

1. 哨兵系统挑选从服务器之一成为新的主服务器 
2. 对所有从服务器发送复制命令,从服务器开始执行复制新的主服务器操作 
3. 哨兵会继续监控已下线的旧主服务器,并在其重新上线时成为新的从服务器

集群

哈希槽

slots数组保存当前节点负责的哈希槽位置
1.集群数据结构: 集群中的每一个节点 使用 clusterNode 数据结构保存 节点的信息 其中包括 配置纪元信息 用于故障转移
clusterLink数据结构用于保存 节点的输出 输入 缓冲区,关联的节点信息
clusterState 保存集群相关的信息

hash一致性

集群相关命令

CLUSTER MEET ---添加节点到集群
CLUSTER ADDSLOTS --- 将输入的槽指派给特定节点
crc16(key) & 16383 ----计算key属于哪个槽
clusterNode 属性 slot 属性记录了节点负责的槽
clusterState属性 slot 属性记录了集群16384 个槽的槽指派信息
MOVED 错误 --- 节点接受set命令,计算key属于哪个槽, 若key属于当前节点负责,则返回客户端MOVED 错误,集群模式下该错误会被隐藏,自动跳转至响应节点

重新分片

重新分片操作 由集群管理 软件 redis-trib 负责执行
  • CLUSTER SETSLOTS <slot> IMPORTING <source_id> --- redis-trib 发送该指令至目标节点,让目标节点准备接受从源节点导入属于槽的键值对
  • CLUSTER SETSLOTS <slot> MIGRATING <target_id> ---- 让源节点准备好将键值对迁移至 目标节点slot

集群高可用实现

gossip协议

  • meet:某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信;
  • ping:每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据;
  • pong: 返回ping和meet,包含自己的状态和其他信息,也可以用于信息广播和更新;
  • fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了

Jedis与Redisson 对比

  • Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。官方推荐使用Redisson
  • Jedis使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。Redisson使用非阻塞的I/O和基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作
  • Jedis仅支持基本的数据类型如(String、Hash、List、Set、Sorted Set),而  Redisson还提供了一系列的分布式Java常用对象如(Semaphore, Lock, AtomicLong, CountDownLatch),在分布式开发中Redisson可提供更便捷的方法

缓存雪崩

发生场景 : 当Redis服务器重启或者大量缓存在同一时期失效时,此时大量的流量会全部冲击到数据库上面,数据库有可能会因为承受不住而宕机
解决方案 : 
    1.均匀分布 : 我们应该在设置失效时间时应该尽量均匀的分布,比如失效时间是当前时间加上一个时间段的随机值
    2.熔断机制 : 类似于SpringCloud的熔断器,我们可以设定阈值或监控服务,如果达到熔断阈值(QPS,服务无法响应,服务超时)时,则直接返回,不再调用目标服务,并且还需要一个检测机制,如果目标服务已经可以正常使用,则重置阈值,恢复使用
    3.隔离机制 : 类似于Docker一样,当一个服务器上某一个tomcat出了问题后不会影响到其它的tomcat,这里我们可以使用线程池来达到隔离的目的,当线程池执行拒绝策略后则直接返回,不再向线程池中增加任务
    4.限流机制 : 其实限流就是熔断机制的一个版本,设置阈值(QPS),达到阈值之后直接返回
    5.双缓存机制 : 将数据存储到缓存中时存储俩份,一份的有效期是正常的,一份的有效期长一点.不建议用这个方案,因为比较消耗内存资源,毕竟Redis是直接存储到内存中的

缓冲穿透

发生场景 : 此时要查询的数据不存在,缓存无法命中所以需要查询完数据库,但是数据是不存在的,此时数据库肯定会返回空,也就无法将该数据写入到缓存中,那么每次对该数据的查询都会去查询一次数据库
解决方案 : 
    1.布隆过滤 : 我们可以预先将数据库里面所有的key全部存到一个大的map里面,然后在过滤器中过滤掉那些不存在的key.但是需要考虑数据库的key是会更新的,此时需要考虑数据库 --> map的更新频率问题
    2.缓存空值 : 哪怕这条数据不存在但是我们任然将其存储到缓存中去,设置一个较短的过期时间即可,并且可以做日志记录,寻找问题原因

粥于于
9 声望1 粉丝

代码搬运工


« 上一篇
Java集合总结
下一篇 »
TCP/IP