Redis高可用高性能缓存的应用系列1 - 数据类型以及底层结构和原理

2023-04-02 19:53:29 浏览数 (1)

概述

介绍redis缓存原理与设计执行流程,单线程的处理方式是高效的原因,以及redis数据类型以及底层结构和原理进行说明,这对我们使用Redis有很大帮助。

底层运行实现模型

客户端的请求先进行linux运行内核,而redis和内核之间用了epoll非阻塞I/O多路复用的方式来处理,请求是I/O操作会有序的存入在epoll的待处理队列中,Redis的是内存操作,内存运行的速度要远远高于I/O操作的,Redis是单线程的处理方式是高效的,Redis会顺序的一笔一笔的处理在epoll的待处理队列中的请求。

所谓Redis单线程是用于计算的worker线程,redis还会有其他的线程,比如持久化,异步删除等等。

Redis基本结构

Redis核心结构只要有redisServer和redisObject组成,在Redis初始化的时候,先初始化RedisServer结构,通过dict映射dictEntry,把具体的类型和值存入redisObject进行统一管理,如果有不理解的地方,可以看redis设计与实现一书。

redisServer

1.下面是redisServer的结构体初始化,在文件server.h中。

代码语言:txt复制
struct redisServer {
    /* General */
    pid_t pid;                  /* Main process pid. */
    pthread_t main_thread_id;         /* Main thread id */
    char *configfile;    /* Absolute config file path, or NULL */
    
    //...
    redisDb *db;
    dict *commands;             /* Command table */
    dict *orig_commands;        /* Command table before command renaming. */
    aeEventLoop *el;
    // ...
}

redisDb *db :保存了数据库的信息,初始化默认16个数据库,通过参数可以修改, dict是redisDb中最核心的结构体,dict中保存了所有的键值对,称为键空间。

对dict的理解很重要,Redis的对象相当于大的dict结构体对象,所有的类型结构实现的都是以dict做前提的。

代码语言:txt复制
typedef struct redisDb {
    dict *dict;                
    dict *expires;              
    dict *blocking_keys;       
    dict *blocking_keys_unblock_on_nokey;  
    dict *ready_keys;           
    dict *watched_keys; 
    // ...
} redisDb;

下面在看dict结构,它做了2个字典做渐进式Hash,数据组保存的是具体的dictEntry。

代码语言:txt复制
struct dict {
    dictType *type;
    dictEntry **ht_table[2];
    unsigned long ht_used[2];
    long rehashidx; 
    int16_t pauserehash; 
    signed char ht_size_exp[2];
    void *metadata[];           
};

struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;    
    void *metadata[];
};

dictEntry *next ,当Hash的值一样时就形成一个链表,指向下一个dictEntry,再dictEntry结构体中,key保存实际的key值,val指向的是redisObject结构。

redisObject

下面详细介绍一下redisObject类型和结构体中主要的功能:

  • type: 对外的数据类型,string、hash、list、set 、zset,通过type命令看到的类型。
  • encoding:内部实际保存的数据类型,具体选择时通过配置文件对应参数实现的,例如list 按照存入的值分为ziplist和quicklist。
  • lru:管理缓存淘汰机制
  • refcount:引用计数,主要用于内存回收使用
  • ptr:存储真实数据的物理指针地址
代码语言:txt复制
struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; 
    int refcount;
    void *ptr;
};

Redis数据结构

String

Redis的底层实现没有实现C语言的字符串,而是自己简单封装了一层动态字符串Sds,Sds内部又可以转为int、embstr、raw。

代码语言:txt复制
127.0.0.1:6379> set number 100
OK
127.0.0.1:6379> object encoding number
"int"

127.0.0.1:6379> set name stark张宇
OK
127.0.0.1:6379> object encoding name
"embstr"

127.0.0.1:6379> set logstr “stark张宇关于Redisraw类型编码的演示,他需要超过44字节”
OK
127.0.0.1:6379> object encoding logstr
"raw"

Sds内部的装换取决于存储值的字节大小,值为int的直接存储成int,小于等于44字节的存储成embstr类型,大于44字节的存储成raw类型。

embstr与raw存储上的区别:embstr存储在一块连续上的内存,读取一次就可以,raw存储的是不连续的内存,需要读取2次内存。

Redis为什么这么设计Sds结构体存储字符串?

1.效率:变量中经常会用到字符串长度len,如果使用C语言的字符长度时,需要对整个字符串进行遍历,它的时间复杂度O(n),在Sds中的len,时间复杂度O(1),在高并发场景下,频繁遍历字符串,会造成性能瓶颈。

2.防止数据溢出:C语言的字符串因为没有 记录自身长度,另外Sds再保存二进制是安全的,方便value值修改时修改存储空间。

3.内存空间的预分配:当修改字符串内存空间时,不仅会修改字符串存储空间,还会额外预留空间,下次修改的时候就会先检查未使用的内存空间。

4.惰性空间释放:当当修改字符串内存空间变小时,不会立即回收内存空间,防止再次修改。

分配内存空间规则:当值小于1M时,就会分配相同的空间,当大于1M时就会分配1M的空间。

Hash

Redis的Hash的底层是一个dict,当数据量比较小或者数据值比较小时会采用ziplist,数据大的时候采用hashtable的结构来存储数据。

代码语言:txt复制
127.0.0.1:6379> hgetall user1
1) "name"
2) "stark"
3) "age"
4) "33"
5) "sex"
6) "1"
127.0.0.1:6379> object encoding user1
"ziplist"

127.0.0.1:6379> hset user1 mark "stark张宇关于HashTable类型编码的演示,值很大他就变成了HashTable"
(integer) 1
127.0.0.1:6379> object encoding user1
"hashtable"
ziplist

ziplist结构的组成部分详解:

  • zlbytes: 32位无符号整型,表示整个ziplist所占的空间大小,包含了zlbytes所占的4个字节。这个字段可以在重置整个ziplist大小时不需要遍历整个list来确定大小,空间换时间。
  • zltail:32位无符号整型,表示整个list中最后一项所在的偏移量,方便再尾部做pop操作。
  • zllen:16位,表示ziplist中所存储的entry数量。
  • entry:不定长,可能有多个。
  • zlend:8位,ziplist的末尾表示,其固定值是255。

entry由3部分组成,前一个entry的大小,当前编码类型和长度,真实的字符串和数字。

ziplist优缺点:

优点:因为是连续内存空间,所以利用率高,访问效率高。

缺点:更新效率低,当插入和删除一个元素时,会频繁的进行内存的扩展和缩小,数据的搬移效率低。

list

Redis的list的有序数据结构,底层分为ziplist和quicklist。

ziplist在Hash类型里已经说了,在这里不做多余的叙述了。

quicklist结构的优缺点:

优点:因为是双向链表,更新效率比较高,在插入和删除操作时非常方便,复杂度O(n),前后元素的复杂度是O(1)。

缺点:增加了内存的开销。

Set

Redis里面的Set是无序的,自动去重的数据类型,它的底层是一个dict,当数据用整型并且数据元素小于配置文件中set-max-intest-entries,否则用dict。

代码语言:txt复制
127.0.0.1:6379> sadd gather 100 200 300
(integer) 3
127.0.0.1:6379> object encoding gather
"intset"
127.0.0.1:6379> sadd gather stark
(integer) 1
127.0.0.1:6379> object encoding gather
"hashtable"

Zset

Redis的Zset是有序的,自动去重的数据类型,底层是由字典Dict和跳表Skiplist实现的,数据较少时使用Ziplist结构来存储。

Ziplist可以在配置文件中通过zset-max-ziplist-entrieszset-max-ziplist-value来配置。

常见问题:Redis的Zset为什么不使用红黑树和二叉树,而要选择跳表呢?

1)红黑树和二叉树的范围查找不是很好,有序集合很大的场景是进行范围查找的,范围查找在跳表上是非常方便的,因为它是一个链表,红黑树和二叉树的范围查找相对来说就复杂的多。2)跳表的实现要比红黑树简单很多了。

0 人点赞