Redis(一):Redis为什么高性能?

Redis的高性能主要得益于其单线程模型、内存操作、I/O多路复用机制以及高效的数据结构。文章详细介绍了Redis的基础数据结构,如SDS、链表、字典、跳跃表、整数集合和压缩列表,揭示了它们如何优化内存分配和查找效率。例如,SDS通过空间预分配和惰性空间释放减少内存分配次数,而跳跃表则通过多级索引实现快速查找。这些设计使得Redis在处理大量数据时保持了出色的性能。
摘要由CSDN通过智能技术生成

Redis为什么这么快?

  1. Redis是单线程的,避免了多线程的上下文切换和并发控制开销;
  2. Redis大部分操作时基于内存,读写数据不需要磁盘I/O,所以速度非常快;
  3. Redis采用了I/O多路复用机制,提高了网络I/O并发性;
  4. Redis提供高效的数据结构,如跳跃表、哈希表等;

基础数据结构

SDS

Redis的简单动态字符串SDS是可变的,遵循C字符串以1字节空字符结尾,最大长度为512M。

SDS为什么使用1字节空字符结尾呢?
使用1字节空字符结尾可重用C字符串的部分函数。

结构定义

SDS底层使用一个字节数组保存字符串内容,通过len属性可O(1)的复杂度获取字符串长度。

struct sdshdr{
	//字符串长度,即buf[]已使用字节数
	int len;
	//buf[]剩余字节数
	int free;
	//字节数组,用于保存字符串内容
	char buf[];
};

在这里插入图片描述

内存分配策略

SDS采用空间预分配和惰性空间释放来优化SDS的内存分配次数(n次 → 最多n次)。

  1. 空间预分配

空间预分配用于优化字符串的增长操作。当修改SDS需要扩展内存空间时,不仅会分配所需的空间,还会根据len属性分配额外的未使用空间。

  • 如果修改SDS后,len < 1MB,将分配和len同样大小的未使用空间;
  • 如果修改SDS后,len > 1MB,将分配1MB的未使用空间;
  1. 惰性空间释放

惰性空间释放用于优化SDS缩短操作。当缩短SDS时,程序不立即回收未使用的空间,使用free记录未使用空间长度,等待将来使用。(也可调用函数手动释放空间)

SDS和C字符串的区别

  1. 获取字符串长度
  • C字符串需要遍历整个字符串计数统计长度,时间复杂度为O(n);
  • SDS只需要获取sdshdr.len即可,时间复杂度为O(1);
  1. 缓冲区溢出
  • C字符串不记录自身长度,在进行修改时如果没有分配足够的内存可能造成缓冲区溢出;
  • SDS在修改时会先根据sdshdr.free属性校验内存是否足够,如果不够会先进行扩容,再执行修改操作;
  1. 二进制安全
  • C字符串除末尾之外不能包含空字符,否则最先被读入的空字符会被误认为是字符串结尾;(所以C字符串只能保存文本数据,不能用于保存二进制数据)
  • SDS通过sdshdr.len判断字符串是否结束,可以用于保存二进制数据。
  1. 内存分配次数
  • C字符串每次修改操作都需要进行内存重分配;
  • SDS需要最多n次内存重分配;

链表

特点

  1. 双向链表:获取某个节点的前驱节点和后继节点复杂度O(1);
  2. 无环:头节点前驱指针和尾节点后继指针指向NULL;
  3. 插入和删除快,时间复杂度O(1);查找慢,时间复杂度O(n);

结构定义

节点定义:

typedef struct listNode{
	//前驱节点指针
	struct listNode *prev;
	//后继节点指针
	struct listNode *next;
	//节点值
	void *value;
}listNode;

在这里插入图片描述

链表定义:

typedef struct list {
    //头节点
    listNode *head;
    //尾节点
    listNode *tail;
    //复制节点值函数
    void *(*dup)(void *ptr);
    //释放节点值函数
    void (*free)(void *ptr);
    //比对节点值函数
    int (*match)(void *ptr, void *key);
    //节点数量
    unsigned long len;
} list;

在这里插入图片描述

字典

特点

字典用于保存键值对,包含的每个键都是唯一的。

结构定义

字典ht[2]是一个包含两个哈希表的数组,一般情况下只使用ht[0],只有在rehash时会使用ht[1]。dict.rehashidx记录目前rehash进度。

typedef struct dict {
    //特定类型操作函数
    dictType *type;
    //私有数据(传给特定类型操作函数的参数)
    void *privdata;
    //哈希表
    dictht ht[2];
    //rehash索引,没有在进行rehash时rehashidx=-1
    long rehashidx;   
} dict;

在这里插入图片描述

哈希表底层基于一个dictEntry数组实现,每一项保存一个键值对,哈希到同一个数组项的节点通过next连接。

typedef struct dictht {
    //哈希表数组
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,用于哈希函数计算索引值,sizemask=size-1
    unsigned long sizemask;
    //已有节点数量
    unsigned long used;
} dictht;

typedef struct dictEntry {
    //键
    void *key;
    //值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    //下一个节点
    struct dictEntry *next;
} dictEntry;

在这里插入图片描述

哈希过程

  1. 根据哈希函数计算键值对键的哈希值;
  2. 根据哈希值对哈希表掩码dictht.sizemask取模计算索引值;
  3. 根据索引值将键值对放入哈希表数组的对应索引位置上;

如何解决哈希冲突?
使用链地址法解决哈希冲突。每个哈希表节点dictEntry都保存一个next指针,得分配到同一个数组项的节点通过next指针连接成一个单向链表。

rehash过程

  1. 为ht[1]分配空间:如果是扩容操作,ht[1]大小等于第一个≥ht[0].used*2的2n;如果是收缩操作,ht[1]大小等于第一个≥ht[0].used的2n;
  2. 将ht[0]上的所有键值对rehash(重新计算键的哈希值和索引值)后放入ht[1];——渐进式rehash:rehash期间每次对字典执行增/删/改/查操作都会将ht[0]在rehashidx索引上的所有键值对rehash到ht[1],rehashidx++;
  3. 当ht[0]上的所有键值对都迁移到ht[1]后,释放ht[0],将ht[1]设置为ht[0],新建一个ht[1]为下次rehash做准备;

1. 什么时候会触发rehash?
根据哈希表的负载因子(dictht.used/dictht.size),在哈希表过大或过小时会触发rehash:

  1. 当服务器没有在执行BGSAVE/BGWRITEAOF命令时,哈希表的负载因子≥1;
  2. 当服务器正在执行BGSAVE/BGWRITEAOF命令时,哈希表的负载因子≥5;

2. 渐进式rehash的好处
将rehash操作分摊到字典的每个增/删/改/查操作上,避免集中rehash带来庞大的计算量而导致服务器停顿。

3. rehash过程中的查找/插入操作

  1. 查找:在rehash过程中会同时使用ht[0]和ht[1],如果要查找某个key,会先在ht[0]中查找,如果没找到,继续在ht[1]中查找。
  2. 插入:在rehash过程中新增的键值对会被保存到ht[1],保证了ht[0]的键值对数量只减不增。

跳跃表

特点

跳跃表基于分值从小到大排序,查找的过程近似二分查找。

结构定义

跳跃表基于有序链表实现,通过在链表的基础上增加多级索引提升查找的效率。跳跃表每一层都是一个链表,最底层链表包含所有元素,链表的每个节点包含2个指针,一个forward指针指向同一链表中该节点的下一个节点,一个backward指向同一链表中该节点的前一个节点。

typedef struct zskiplist {
   
    //头节点、尾节点
    struct zskiplistNode *header, *tail;
    //跳跃表长度(包含的节点数量)
    unsigned long length;
    //跳跃表内层数最大的节点的层数
    int level;
} zskiplist;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值