redis部分原理数据结构详解

Redis 介绍

键值存储 | 内存数据库 | 支持多种数据结构(字符串、列表、集合、有序集合、哈希表) | 高性能

发布/订阅模式 | 事务支持 | 分布式缓存 | 实时分析和计数

Redis 应用

1. Redis 锁

2. Redis缓存

a. 限制短信通知次数

使用Redis记录用户短信通知次数,有效期 24 小时,以控制和限制通知次数

b. 请求幂等性

通过Redis记录重复的请求数据,确保请求的幂等性,同时可用于削峰处理

c. 记录任务信息 + 优先级消息队列

使用Redis记录任务信息,并结合优先级消息队列实现任务调度和处理

d. 延时队列

通过Redis实现延时队列,用于处理需要延迟执行的任务

c. 高频热点资源的记录

缓存高频热点资源,如合同导出结果,以减少性能损耗和提高响应速度

d. 埋点

使用Redis来记录用户行为,如用户请求接口和用户 24 小时内短信次数统计,以方便后台统计数据

Redis 原理

IO 模型 | 持久化 | 

一. Redis IO模型

1. IO模型: 

BIO Blocking I/O 阻塞| NIO Non-Blocking 非阻塞| AIO Asynchronous I/O  异步

二.持久化

Redis支持数据持久化,将内存中的数据保存到硬盘上以便在重启时恢复数据.

RDB持久化
1. RDB持久化的触发
  • RDB是一种快照持久化方式,它将Redis的数据以二进制形式保存到磁盘上。

  • 使用 SAVE 命令进行 RDB 持久化,它会阻塞 Redis 服务器进程,直到 RDB 文件创建完成。Redis服务器进程在执行 SAVE 命令期间被阻塞,不能处理其他命令请求。

  • 使用 BGSAVE 命令触发 BGSAVE 持久化,它会创建一个子进程负责生成 RDB 文件,父进程继续处理命令请求。BGSAVE 操作是在后台进行的,允许 Redis 服务器在持久化操作期间继续处理其他命令请求,提高了并发性。

  • SAVE 命令和BGSAVE 命令都是针对全数据库的持久化.

2. RDB自动间隔性保存

因为BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。

save选项示例: 

save选项允许设置多个保存条件,只要满足其中任意一个条件,服务器就会执行BGSAVE命令。

save 900 1
save 300 10
save 60 10000

那么只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行:
❑服务器在900秒之内,对数据库进行了至少1次修改。
❑服务器在300秒之内,对数据库进行了至少10次修改。
❑服务器在60秒之内,对数据库进行了至少10000次修改。
2.1 设置保存条件

当Redis服务器启动时,用户可以通过指定配置文件或者传入启动参数的方式设置save选项.

在企业级项目中,通常会在Redis服务器的配置文件(如redis.conf)中配置这些选项.

public class RedisServer {
   
    // 其他属性...
    private SaveParameter[] saveparams;


}

public class SaveParameter {

    // 秒数
    public long seconds;

    // 修改数
    public int changes;
}
2.2 dirty计数器和lastsave属性 
除了saveparams数组之外,服务器状态还维持着一个dirty计数器,以及一个lastsave属性:

❑dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,
服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)

❑lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间

public class RedisServer {

    // 修改计数器
    private long dirty;

    // 上一次执行保存的时间
    private Date lastsave;
   
    // 其他属性...
    private SaveParameter[] saveparams;

}
  • 服务器维护一个dirty计数器,记录距离上一次成功执行SAVE或BGSAVE命令之后,数据库状态(所有数据库)发生了多少次修改。
  • lastsave属性记录了服务器上一次成功执行SAVE或BGSAVE命令的时间戳。
2.3 保存条件检查

Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令。



public class RedisServer {
    // ...
    // 其他成员变量和方法...

    public void serverCron() {
        // ...
        // 遍历所有保存条件
        List<SaveParam> saveParams = server.getSaveParams();
        long currentTime = System.currentTimeMillis() / 1000; // 获取当前时间的秒数
        for (SaveParam saveParam : saveParams) {
            // 计算距离上次执行保存操作有多少秒
            long saveInterval = currentTime - server.getLastSaveTime();
            // 如果数据库状态的修改次数超过条件所设置的次数
            // 并且距离上次保存的时间超过条件所设置的时间
            // 那么执行保存操作
            if (server.getDirty() >= saveParam.getChanges() &&
                saveInterval > saveParam.getSeconds()) {
                bgSave();
            }
        }
        // ...
    }
    // ...
}

public class SaveParam {
    private int seconds;
    private int changes;


}

程序会遍历并检查saveparams数组中的所有保存条件,只要有任意一个条件被满足,那么服务器就会执行BGSAVE命令。

BGSAVE在执行之后完成,其中dirty计数器已经被重置为0,而lastsave属性也被更新为BGSAVE完成的时间.

2.4 RDB文件结构

📢: 上图中, 全大写单词标示常量,用全小写单词标示变量和数据

a. RDB是二进制文件

b. RDB文件的最开头是REDIS部分,这个部分的长度为5字节,保存着“REDIS”五个字符。通过这五个字符,程序可以在载入文件时,快速检查所载入的文件是否RDB文件。

c. db_version长度为4字节,它的值是一个字符串表示的整数,这个整数记录了RDB文件的版本号,比如"0006"就代表RDB文件的版本为第六版。本章只介绍第六版RDB文件的结构。

d. databases部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据:

❑如果服务器的数据库状态为空(所有数据库都是空的),那么这个部分也为空,长度为0字              节。

❑如果服务器的数据库状态为非空(有至少一个数据库非空), 那么这个部分也为非空,根据               数据库所保存键值对的数量、类型和内容不同,这个部分的长度也会有所不同。

e. EOF常量的长度为1字节,这个常量标志着RDB文件正文内容的结束,当读入程序遇到这个值的时候,它知道所有数据库的所有键值对都已经载入完毕了

f. check_sum是一个8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对REDIS、db_version、databases、EOF四个部分的内容进行计算得出的。服务器在载入RDB文件时,会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现。

DATABASE部分:

一个RDB文件的databases部分可以保存任意多个非空数据库。例如,如果服务器的0号数据库和3号数据库非空,那么服务器将创建一个如下图所示的RDB文件,图中的database 0代表0号数据库中的所有键值对数据,而database 3则代表3号数据库中的所有键值对数据。

每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、key_value_pairs三个部分:

AOF持久化

Redis数据结构

简单动态字符串 | 链表 | 字典 | 跳跃表 | 对象

一. 简单动态字符串: SDS Simple Dynamic Strings

1. 基本组成部分
public class SDS {

    private int len;      // 保存字符串的长度
    private int free;     // 保存未使用字节的数量
    private char[] buf;   // 字节数组,用于保存字符串
}

C字符串(字符数组)结尾的惯例:SDS遵循了C字符串(以Null字符 \0 结尾的字符串)的惯例。这意味着SDS的存储方式与C字符串类似,字符串的结尾会有一个Null字符,表示字符串的结束。但需要注意的是,这个Null字符不计入SDS的len属性(字符串的长度)中。

2. "未使用空间在SDS中的作用"
  • C字符串的长度计算复杂度是O(N):C字符串是以空字符 '\0' 结尾的,要获取其长度,程序必须遍历整个字符串直到遇到空字符为止。这种操作的时间复杂度是O(N),其中N是字符串的长度。

  • SDS的长度计算复杂度是O(1):SDS内部记录了字符串的长度(len属性),因此无论字符串有多长,获取长度的操作复杂度都是O(1)。这是SDS的一个显著优势。

  • 性能优势:Redis中采用SDS作为字符串的内部表示方式,这降低了获取字符串长度的复杂度,从O(N) 降低到了O(1)。这确保了Redis在处理字符串键时的高性能,即使字符串非常长也不会影响系统性能。

3. 杜绝缓冲区溢出
  • C字符串的缓冲区管理:C字符串操作(例如 strcat)假定用户已经为目标字符串(dest)分配了足够的内存,以容纳要追加的内容。如果这个假定不成立,就会发生缓冲区溢出,导致数据覆盖相邻内存。

  • SDS的自动空间管理:与C字符串不同,SDS的API会自动管理字符串的内存分配和扩展。在执行修改操作之前,SDS的API会检查是否有足够的空间来容纳修改后的内容,如果不足,API会自动扩展SDS的内存,然后再执行修改操作。这杜绝了缓冲区溢出的可能性。

4. 内存重分配次数

  • C字符串的长度与底层数组长度关联:C字符串的底层实现总是一个N+1个字符长的数组,其中N是字符串的长度。因此,每次增长或缩短C字符串都需要进行内存重分配,这可能会导致缓冲区溢出或内存泄漏。

  • SDS的解耦长度和底层数组长度:SDS通过未使用的空间来解除字符串长度和底层数组长度之间的关联。SDS的buf数组可以包含未使用的字节,这些字节的数量由SDS的free属性记录。SDS的长度和底层数组的长度不一定相等。

  • 空间预分配策略:SDS通过空间预分配策略优化了字符串的增长操作。当SDS需要进行空间扩展时,程序会为SDS分配额外的未使用空间,其数量取决于SDS的长度变化。如果长度变化较小,程序分配的未使用空间也较小;如果长度变化较大,程序会分配1MB的未使用空间。这减少了连续执行字符串增长操作所需的内存重分配次数.

SDS(Simple Dynamic Strings)中空间预分配策略的原则

📢其根据字符串长度的变化来确定额外分配的未使用空间数量

长度小于1MB的情况:如果对SDS进行修改后,SDS的长度(len属性的值)将小于1MB,那么程序将分配的未使用空间与len属性的值相同。例如,如果修改后的长度为13字节,程序会分配额外的13字节未使用空间。此时,SDS的buf数组的实际长度将变为原长度加上额外分配的未使用空间再加上1字节用于保存空字符,即 len + 13 + 1 字节。

长度大于等于1MB的情况:如果对SDS进行修改后,SDS的长度将大于等于1MB,程序会分配1MB的未使用空间。例如,如果修改后的长度为30MB,程序会分配1MB的未使用空间。此时,SDS的buf数组的实际长度将变为原长度加上1MB的未使用空间再加上1字节用于保存空字符,即 len + 1MB + 1 字节。

5. 惰性空间释放
  • 惰性空间释放的原理:当SDS的API需要缩短SDS保存的字符串时,程序不会立即使用内存重分配来回收多出来的字节,而是使用free属性记录这些多余字节的数量,并将其保留在SDS内部。这些字节并不立即释放,而是等待将来的使用。

  • 优化未来增长操作:通过惰性空间释放策略,SDS避免了在缩短字符串时不必要的内存重分配操作。而当将来需要执行增长操作时,SDS可以利用这些未使用空间,从而减少内存开销。

  • 手动释放未使用空间:SDS还提供了相应的API,允许开发者在有需要时手动释放SDS的未使用空间,以确保不会浪费内存资源。这意味着程序可以在适当的时候释放那些不再需要的空间。

6. 二进制安全

SDS的二进制安全性使其更加灵活,能够处理各种类型的数据,包括文本和二进制数据。这使得Redis可以适用于广泛的使用场景,不仅仅局限于文本数据的存储和处理.

SDS不管存储什么数据, 都会以空字符结尾, 以此来兼容C语言的字符串相关的库.

  • C字符串的限制:C字符串必须符合某种编码(如ASCII),并且字符串中除了末尾的空字符外,不能包含其他空字符。这些限制使得C字符串只能用于保存文本数据,无法保存二进制数据。如果特殊数据格式使用空字符分割单词,C字符串函数会误认为空字符为字符串结尾。

  • SDS的二进制安全:为了适应不同的使用场景,SDS的API都是二进制安全的。SDS API以处理二进制数据的方式来操作SDS的buf数组,不对数据进行限制、过滤或假设。SDS的buf属性被称为字节数组,用于存储一系列二进制数据。

  • 灵活性:SDS的二进制安全特性使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。特殊数据格式,如使用空字符分割的单词,也可以轻松地保存在SDS中,因为SDS使用len属性而不是空字符来确定字符串是否结束。

二. 链表

1. 基本组成部分
a. 链表节点基本组成部分
public class ListNode<T> {
    // 前置节点
    public ListNode<T> prev;
    // 后置节点
    public ListNode<T> next;
    // 节点的值
    public T value;

    public ListNode(ListNode<T> prev, ListNode<T> next, T value) {
        this.prev = prev;
        this.next = next;
        this.value = value;
    }
}
b.链表组成部分
public class ListNode<T> {
    // 表头节点
    public ListNode<T> head;
    // 表尾节点
    public ListNode<T> tail;
    // 链表所包含的节点数量
    public long len;
    // 节点值复制函数
    public DupFunction<T> dup;
    // 节点值释放函数
    public FreeFunction<T> free;
    // 节点值对比函数
    public MatchFunction<T> match;

    public ListNode(ListNode<T> head, ListNode<T> tail, long len, DupFunction<T> dup, FreeFunction<T> free, MatchFunction<T> match) {
        this.head = head;
        this.tail = tail;
        this.len = len;
        this.dup = dup;
        this.free = free;
        this.match = match;
    }
}

// 定义函数式接口用于节点值复制、释放和对比函数
@FunctionalInterface
public interface DupFunction<T> {
    T duplicate(T ptr);
}

@FunctionalInterface
public interface FreeFunction<T> {
    void free(T ptr);
}

@FunctionalInterface
public interface MatchFunction<T> {
    int match(T ptr, T key);
}
  • 双端链表:Redis链表是双向链表,每个节点都有指向前一个节点(prev)和后一个节点(next)的指针,因此获取某个节点的前置节点和后置节点的复杂度都是O(1)。

  • 无环链表:Redis链表没有环,表头节点的prev指针和表尾节点的next指针都指向NULL,确保对链表的访问以NULL为终点。

  • 带表头和表尾指针:链表结构中包含head指针和tail指针,这使得程序能够以O(1)的复杂度获取链表的表头节点和表尾节点。

  • 带长度计数器:Redis链表使用len属性来追踪链表中节点的数量,这使得程序能够以O(1)的复杂度获取链表的节点数量。

  • 多态:链表节点使用void*指针来保存节点值,而且可以为节点值设置类型特定的函数,包括dup(复制)、free(释放)、match(对比)。这使得Redis链表可以用于保存各种不同类型的值,实现多态的数据结构。

三. 字典 ​​​​

字典是一种数据结构,它可以存储和组织键值对(key-value pair),其中每个键(key)都唯一地对应一个值(value)。这允许通过键来快速查找、插入、更新或删除对应的值。字典通常用于存储和管理各种类型的数据,使其易于检索和操作。

1. 基本组成部分 
a. 哈希表    
public class DictHt {
    // 哈希表数组
    private DictEntry[][] table;
    // 哈希表大小
    private long size;
    // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
    private long sizemask;
    // 该哈希表已有节点的数量
    private long used;
}
  • table 属性是一个数组,其中每个元素都是一个指向 dictEntry 结构的指针。每个 dictEntry 结构保存着一个键值对。

  • size 属性记录了哈希表的大小,即 table 数组的大小。它表示哈希表可以容纳多少个键值对。

  • used 属性记录了哈希表目前已有节点(键值对)的数量。它表示当前哈希表中存储的键值对数量。

  • sizemask 属性的值总是等于 size - 1。这个属性和哈希值一起决定了一个键应该被放到 table 数组的哪个索引位置上。哈希值与 sizemask 进行按位与操作,以确定键在数组中的位置

b. 哈希表节点

哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对.

public class DictEntry {
    // 键
    public Object key;
    
    // 值,使用 Object 类型来兼容不同数据类型
    public Object value;

    // 指向下一个哈希表节点,形成链表
    public DictEntry next;
}

  • key 属性保存键值对中的键。
  • v 属性用一个联合体 (union) 存储键值对中的值,这个值可以是指针,也可以是 uint64_t 整数,或者是 int64_t 整数。
  • next 属性是一个指向下一个哈希表节点的指针,它用于解决键冲突,将多个哈希值相同的键值对连接在一起,形成链表结构。

这个结构体允许哈希表存储不同数据类型的值,同时解决了哈希冲突的问题,确保具有相同哈希值的键值对能够正确地存储和检索。

c. 字典
public class Dict {
    // 类型特定函数
    public DictType type;

    // 私有数据
    public Object privdata;

    // 哈希表数组
    public DictHt[] ht;

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    public int rehashidx;
}

public class DictType {
    // 计算哈希值的函数
    public interface HashFunction {
        int hashFunction(Object key);
    }

    // 复制键的函数
    public interface KeyDup {
        Object keyDup(Object privdata, Object key);
    }

    // 复制值的函数
    public interface ValDup {
        Object valDup(Object privdata, Object obj);
    }

    // 对比键的函数
    public interface KeyCompare {
        int keyCompare(Object privdata, Object key1, Object key2);
    }

    // 销毁键的函数
    public interface KeyDestructor {
        void keyDestructor(Object privdata, Object key);
    }

    // 销毁值的函数
    public interface ValDestructor {
        void valDestructor(Object privdata, Object obj);
    }

    // 计算哈希值的函数
    public HashFunction hashFunction;

    // 复制键的函数
    public KeyDup keyDup;

    // 复制值的函数
    public ValDup valDup;

    // 对比键的函数
    public KeyCompare keyCompare;

    // 销毁键的函数
    public KeyDestructor keyDestructor;

    // 销毁值的函数
    public ValDestructor valDestructor;
}

  • type 属性是一个指向 dictType 结构的指针,用于定义针对特定类型键值对的操作函数。这允许 Redis 创建多态字典,以便应对不同类型的数据。

  • privdata 属性保存了传递给类型特定函数的可选参数,这些函数在处理键值对时可能需要使用。

  • ht 属性是一个包含两个 dictht 哈希表的数组,通常只使用 ht[0] 哈希表。ht[1] 哈希表主要用于在进行哈希表的 rehash 操作时暂时存储数据。

  • rehashidx 属性用于跟踪哈希表的 rehash 进度,如果没有进行 rehash,其值为 -1。

2. 哈希算法

当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。

3. 键冲突
  • 当多个键被分配到哈希表数组的相同索引位置时,发生了键冲突。

  • Redis使用链地址法(separate chaining)来解决键冲突。每个哈希表节点都有一个 next 指针,多个节点可以用 next 指针构成一个单向链表。

  • 处理冲突的方法是将新的节点添加到链表的表头位置,通过 next 指针将多个节点连接成一个链表。这样,最新添加的节点总是位于链表的开头。

  • 这种方式使得在解决冲突时的操作复杂度为 O(1),因为无论链表有多长,都可以直接将新节点插入到表头位置,而不需要遍历整个链表。

四. 跳跃表

跳跃表(Skip List)是一种数据结构,用于存储一组有序的元素,支持快速的插入、删除和查找操作。跳跃表的特点在于它通过多层次的链表结构来实现快速查找,类似于多层索引,这使得在跳跃表中进行查找操作的时间复杂度保持在 O(log n) 级别。

跳跃表的核心思想是通过添加多级索引来加速查找。具体而言,跳跃表由多个层级组成,每个层级都是一个有序的链表,其中最底层包含所有的元素。上层的链表是下层链表的子集,用于跳过一些元素,从而快速定位到目标元素。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值