Redis底层总结

简介

Redis是一个开源的使用C语言编写的一个kv存储系统,是一个速度非常快的非关系远程内存数据库。它支持包括String、List、Set、Zset、hash五种数据结构。一个基于内存的非关系型的键值对数据库,因它基于内存的特性所以它的速度比传统的关系型数据库快,除此之外它还具有许多特性:

  • 支持事务.
  • 支持AOF和RDB持久化
  • 支持多种数据数据结构
  • 流水线、发布\订阅功能
  • 主从复制
  • 内存回收

此外,redis同时也内置了LUA脚本、复制等功能,提供两种持久化选项,一种是每隔一段时间将数据导入到磁盘(快照模式),另一种是追加命令到日志中(AOF模式)。如果只是作为高效的内存数据库使用也可以关闭持久化功能。通过哨兵(sentinel)和自动分区(Cuuster)的方式可以提高redis服务器的高可用性。

redis一般应用场景:

  1. 缓存会话(单点登录)
  2. 分布式锁,比如:使用setnx
  3. 各种排行榜或计数器
  4. 商品列表或用户基础数据列表等
  5. 使用list作为消息对列
  6. 秒杀,库存扣减等

数据库结构

在这里插入图片描述
每一个redis服务器内部的数据结构都是一个redisDb[],该数组的大小可以在redis.conf中配置(“database 16”,默认为16),而我们所有的缓存操作(set/hset/get等)都是在redisDb[]中的一个redisDb(库)上进行操作,这个redisDb默认是redisDb[0]。

注意:

  • 可以通过"select 1"来选择接下来的操作在redisDb[1]上进行操作
  • 在实际使用中,我们只在redisDb[0]上操作,因为
  1. redis没有获取当前是在哪一个redisDb上操作的函数,所以很容易才select多次之后,我们就不知道在哪一个库上了,而且既然是只在redisDb[0]上进行操作,那么"database"就可以设置为1了,
  2. 该参数设置为1后,不仅可以将原有的其他redisDb所占的内存给了redisDb[0],在的"定期删除"策略中,我们也只扫描一个redisDb就可以了。

redis数据类型

redis数据结构的五种基本类型:
Redis是一个键值对数据库,而键都是字符串(Redis内部实现的简单动态字符串)类型,值有5种基本类型,分别是:

  • STRING(字符串)
  • LIST(列表)
  • SET(集合)
  • ZSET(有序集合)
  • HASH(哈希)

五种数据类型的应用场景:

  1. String,redis对于KV的操作效率很高,可以直接用作计数器。例如,统计在线人数等等,另外string类型是二进制存储安全的,所以也可以使用它来存储图片,甚至是视频等。
  2. hash,存放键值对,一般可以用来存某个对象的基本属性信息,例如,用户信息,商品信息等,另外,由于hash的大小在小于配置的大小的时候使用的是ziplist结构,比较节约内存,所以针对大量的数据存储可以考虑使用hash来分段存储来达到压缩数据量,节约内存的目的,例如,对于大批量的商品对应的图片地址名称。比如:商品编码固定是10位,可以选取前7位做为hash的key,后三位作为field,图片地址作为value。这样每个hash表都不超过999个,只要把redis.conf中的hash-max-ziplist-entries改为1024,即可。
  3. list,列表类型,可以用于实现消息队列,也可以使用它提供的range命令,做分页查询功能。
  4. set,集合,整数的有序列表可以直接使用set。可以用作某些去重功能,例如用户名不能重复等,另外,还可以对集合进行交集,并集操作,来查找某些元素的共同点
  5. zset,有序集合,可以使用范围查找,排行榜功能或者topN功能。

数据库底层设计

redis服务器将所有数据库都保存在redis.h/redisServer结构中的db数组中,db数组中的每一项都是redis.h/redisDb结构,而redisDb就是一个数据库的底层表现形式。

struct redisServer{
    //····
    //数组形式的,保存着redis服务器中的所有数据库,一般为16个,可以通过dbnum来指定
    redisDb *db;
    int dbnum;
    //···
}

redis服务端会保存每个客户端的状态属性,其中也包括客户端使用的目标数据库,一般默认使用0号数据库,可以通过select命令进行切换。

struct redisClient{
    //......
    //记录客户端当前正在使用的数据库
    redisDb *db;
    //......
}redisClient;

redis是一个键值对数据数据库服务器(key-value pair),在redisDb结构的dict字典中保存了一个数据库下所有的键值对,我们称之为键空间(key space)

typedef struct redisDb{
    dict *dict;//数据库键空间
    dict *expires;//过期管理字典
    dict *wathcd_keys;//实现watch,用于事物
    struct evictionPoolEntry *eviction_pool;//内存不足是,用于LRU算法回收
    //......
}redisDb;

需要注意两点,
1、键空间的键都是字符串对象
2、键空间的值可以是字符串对象、列表对象、哈希表对象、集合对象和有序对象中的任意一种。

每一个对象都对应着一个redisObject

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    // 引用计数,用于内存回收与对象共享
    int refcount;
    // 指向实际值的指针
    void *ptr;
} robj;
 
// 通过调用createObject方法可以创建其对象
robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = OBJ_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;
 
    /* Set the LRU to the current lruclock (minutes resolution), or
     * alternatively the LFU counter. */
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }
    return o;
}

下面解释下各个属性:

type
该属性表示对象的类型,占4个bit,源码如下:

#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4

在Redis中通过使用TYPE命令可以获得其类型:

127.0.0.1:6379> TYPE key
string

encoding
该属性表示该类型的对象具体的实现,这样做的目的是为了使在不同场景下灵活使用不同的数据结构

#define REDIS_ENCODING_RAW 0     /* Raw representation */
#define REDIS_ENCODING_INT 1     /* Encoded as integer */
#define REDIS_ENCODING_HT 2      /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6  /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8  /* Embedded sds string encoding */

在Redis中通过使用OBJECT ENCODING命令可以获得其编码:

127.0.0.1:6379> OBJECT ENCODING key
“embstr"

lru
该属性记录该对象最近一次被访问的时间,如果服务器打开了maxmemory选项,并且服务器用于内存回收的算法为volatile-lru或allkeys-lru,当占用内存超过maxmemory设置的上限时,最早被访问的会先被释放。
在Redis中通过使用OBJECT IDLETIME命令可以获得其lru时间:

127.0.0.1:6379> OBJECT IDLETIME key
(integer) 10155

refcount
该属性有两个作用:
用于引用计数实现内存回收,该属性值表示对象被多少个程序引用,当对象被创建时,该属性值为1,当该值为0时对象占用的内存会被回收。
用于对象共享,比如Redis在初始化服务器时就会创建0到9999的字符串对象用于对象共享,每当使用set命令创建一个新字符串对象,如果要创建的字符串已经存在则将指向值的指针指向该字符串对象,并将该对象的refcount加1。

127.0.0.1:6379> OBJECT REFCOUNT key
(integer) 2

Redis字符串(String)

Redis 没有直接使用 C 语言的字符串,而是构建了自己的抽象类型简单动态字符串,简称SDS(simple dynamic string),下面的结构体sdshdr就表示一个SDS,它具有以下几个优点:

  • 常数时间获取字符串长度
  • 自动扩容
  • 预分配空间以减少内存重新分配次数
  • 二进制安全
  • 重用部分C语言函数库
    在这里插入图片描述

Redis的字符串一共有三种实现方式,分别适用于不同场景。

  1. STRING(字符串) INT(整型)
  2. STRING(字符串) EMBSTR(简单动态字符串)
  3. STRING(字符串) RAW(简单动态字符串)

INT
当字符串保存的是一个可以用long类型来表示的整数时,那么robj对象里的属性ptr的类型void *就会被替换为long,而encoding的值会设置为int表示该字符串的实现方式是整型。

127.0.0.1:6379> SET key 88
OK
127.0.0.1:6379> OBJECT ENCODING key
“int”

EMBSTR
在目前最新版本中,当字符串保存的是一个小于等于44个字节的字符串时,那么robj对象里的属性ptr就会指向一个SDS对象,而encoding的值会设置为embstr表示该字符串的实现方式是SDS(简单动态字符串)。embstr是一种用来保存短字符串的编码方式,embstr编码通过调用一次内存分配函数来创建一块连续的内存空间,即redisObject对象和它的ptr指针指向的SDS对象是连续的。不过embstr编码的字符串对象是只读性的,一旦对其指向APPEND命令追加字符串会导致其变为raw编码实现。

127.0.0.1:6379> SET key value
OK
127.0.0.1:6379> OBJECT ENCODING key
“embstr”

RAW
在目前最新版本中,当字符串对象保存的是一个超过44个字节的字符串时,那么robj对象里的属性ptr就会指向一个SDS对象,而encoding的值会设置为raw表示该字符串的实现方式是SDS(简单动态字符串)。raw编码的字符串对象是可读可写的,对其指向APPEND命令追加字符串会不会导致其实现改变,如果追加的字符串的长度超过其free属性值,会在追加前重新进行内存空间分配。

127.0.0.1:6379> SET key value
OK
127.0.0.1:6379> OBJECT ENCODING key
“raw”

Redis列表(List)

redis对键表的结构支持使得它在键值存储的世界中独树一帜,一个列表结构可以有序地存储多个字符串,拥有例如:lpush lpop rpush rpop等等操作命令。
在3.2版本之前,列表是使用ziplist和linkedlist实现的,在这些老版本中,当列表对象同时满足以下两个条件时,列表对象使用ziplist编码:

  • 列表对象保存的所有字符串元素的长度都小于64字节
  • 列表对象保存的元素数量小于512个
  • 当有任一条件 不满足时将会进行一次转码,使用linkedlist。

而在3.2版本之后,重新引入了一个quicklist的数据结构,列表的底层都是由quicklist实现的,它结合了ziplist和linkedlist的优点

ziplist的结构(压缩表)
由表头和N个entry节点和压缩列表尾部标识符zlend组成的一个连续的内存块。然后通过一系列的编码规则,提高内存的利用率,主要用于存储整数和比较短的字符串。可以看出在插入和删除元素的时候,都需要对内存进行一次扩展或缩减,还要进行部分数据的移动操作,这样会造成更新效率低下的情况。

压缩列表是一块连续的内存区域,这块内存区域布编码示意图大致如下:
在这里插入图片描述
常态的压缩列表内存编码如上图所示,整个内存块区域内分为五个部分,下面分别介绍着五个部分:

  • zlbytes:存储一个无符号整数,固定四个字节长度,用于存储压缩列表所占用的字节,当重新分配内存的时候使用,不需要遍历整个列表来计算内存大小。
  • zltail:存储一个无符号整数,固定四个字节长度,代表指向列表尾部的偏移量,偏移量是指压缩列表的起始位置到指定列表节点的起始位置的距离。
  • zllen:压缩列表包含的节点个数,固定两个字节长度,源码中指出当节点个数大于2^16-2个数的时候,该值将无效,此时需要遍历列表来计算列表节点的个数。
  • entryX:列表节点区域,长度不定,由列表节点紧挨着组成。
  • zlend:一字节长度固定值为255,用于表示列表结束。

列表元素编码
上面介绍了压缩列表的总体内存布局,对于初entryX区域以外的四个区域的长度都是固定的,下面再看看entryX区域的编码情况。
每个列表节点由三部分组成:
在这里插入图片描述
每个压缩列表节点区域头部包含两部分,一部分叫做previous length,另一部分叫encoding,最后是主体内容,叫做content,下面分别介绍他们:

  • previous length
    用于存储上一个节点的长度,因此压缩列表可以从尾部向头部遍历,即当前节点位置减去上一个节点的长度即得到上一个节点的起始位置。previous length的长度可能是1个字节或者是5个字节,如果上一个节点的长度小于254,则该节点只需要一个字节就可以表示前一个节点的长度了,如果前一个节点的长度大于等于254,则previous length的第一个字节为254,后面用四个字节表示当前节点前一个节点的长度。这么做很有效地减少了内存的浪费。
  • encoding
    节点的encoding保存的是节点的content的内容类型以及长度,encoding类型一共有两种,一种字节数组一种是整数,encoding区域长度为1字节、2字节或者5字节长。Redis作者巧妙的利用了前两个字节来表示content存储的内容类型和encoding区域的长度,我们先看看字节数组类型的encoding内容:
    在这里插入图片描述
    再看看整数编码类型的encoding内容:
    在这里插入图片描述
    content
    content区域用于保存节点的内容,节点内容类型和长度由encoding决定,上面可以看出目前content的内容类型有整数类型和字节数组类型,且某些条件下content的长度可能为0。

这篇文章对ziplist的结构讲的还是比较详细的:
https://blog.csdn.net/yellowriver007/article/details/79021049

linkedlist的结构
意思为一个双向链表,和普通的链表定义相同,每个entry包含向前向后的指针,当插入或删除元素的时候,只需要对此元素前后指针操作即可。所以插入和删除效率很高。但查询的效率却是O(n)[n为元素的个数]。

上面说的“ziplist组成的双向链表”是什么意思?实际上,它整体宏观上就是一个链表结构,只不过每个节点都是以压缩列表ziplist的结构保存着数据,而每个ziplist又可以包含多个entry。也可以说一个quicklist节点保存的是一片数据,而不是一个数据。总结:

  • 整体上quicklist就是一个双向链表结构,和普通的链表操作一样,插入删除效率很高,但查询的效率却是O(n)。不过,这样的链表访问两端的元素的时间复杂度却是O(1)。所以,对list的操作多数都是poll和push。
  • 每个quicklist节点就是一个ziplist,具备压缩列表的特性。

在redis.conf配置文件中,有两个参数可以优化列表:

  1. list-max-ziplist-size 表示每个quicklistNode的字节大小。默认为-2 表示8KB
  2. list-compress-depth 表示quicklistNode节点是否要压缩。默认是0 表示不压缩
    在这里插入图片描述

哈希(hash)

redis的散列可以存储多个键 值 对之间的映射,散列存储的值既可以是字符串又可以是数字值,并且用户同样可以对散列存储的数字值执行自增操作或者自减操作。散列可以看作是一个文档或关系数据库里的一行。hash底层的数据结构实现有两种:

  • 一种是ziplist,上面已经提到过。当存储的数据超过配置的阀值时就是转用hashtable的结构。这种转换比较消耗性能,所以应该尽量避免这种转换操作。同时满足以下两个条件时才会使用这种结构:
    • 当键的个数小于hash-max-ziplist-entries(默认512)
    • 当所有值都小于hash-max-ziplist-value(默认64)
  • 另一种就是hashtable。这种结构的时间复杂度为O(1),但是会消耗比较多的内存空间。

集合(Set)

redis的集合和列表都可以存储多个字符串,它们之间的不同在于,列表可以存储多个相同的字符串,而集合则通过使用散列表(hashtable)来保证自已存储的每个字符串都是各不相同的(这些散列表只有键,但没有与键相关联的值),redis中的集合是无序的。还可能存在另一种集合,那就是intset,它是用于存储整数的有序集合,里面存放同一类型的整数。共有三种整数:int16_t、int32_t、int64_t。查找的时间复杂度为O(logN),但是插入的时候,有可能会涉及到升级(比如:原来是int16_t的集合,当插入int32_t的整数的时候就会为每个元素升级为int32_t)这时候会对内存重新分配,所以此时的时间复杂度就是O(N)级别的了。注意:intset只支持升级不支持降级操作。

intset在redis.conf中也有一个配置参数set-max-intset-entries默认值为512。表示如果entry的个数小于此值,则可以编码成REDIS_ENCODING_INTSET类型存储,节约内存。否则采用dict的形式存储。

有序集合(zset)

有序集合和散列一样,都用于存储键值对:有序集合的键被称为成员(member),每个成员都是各不相同的。有序集合的值则被称为分值(score),分值必须为浮点数。有序集合是redis里面唯一一个既可以根据成员访问元素(这一点和散列一样),又可以根据分值以及分值的排列顺序访问元素的结构。它的存储方式也有两种:

  • 是ziplist结构。
    与上面的hash中的ziplist类似,member和score顺序存放并按score的顺序排列
  • 另一种是skiplist与dict的结合。
    skiplist是一种跳跃表结构,用于有序集合中快速查找,大多数情况下它的效率与平衡树差不多,但比平衡树实现简单。redis的作者对普通的跳跃表进行了修改,包括添加span\tail\backward指针、score的值可重复这些设计,从而实现排序功能和反向遍历的功能。

一般跳跃表的实现,主要包含以下几个部分:

  • 表头(head):指向头节点
  • 表尾(tail):指向尾节点
  • 节点(node):实际保存的元素节点,每个节点可以有多层,层数是在创建此节点的时- 候随机生成的一个数值,而且每一层都是一个指向后面某个节点的指针。
  • 层(level):目前表内节点的最大层数
  • 长度(length):节点的数量。
  • 复杂度:平均O(logN) 最坏O(N)

跳跃表的遍历总是从高层开始,然后随着元素值范围的缩小,慢慢降低到低层。
在这里插入图片描述
跳跃表的结构:

/*
 * 跳跃表
 */
typedef struct zskiplist {
 // 表头节点和表尾节点
 struct zskiplistNode *header, *tail;
 // 表中节点的数量
 unsigned long length;
 // 表中层数最大的节点的层数
 int level;
} zskiplist;

header和tail指针分别指向跳跃表的表头结点和表尾结点,通过这两个指针,定位表头结点和表尾结点的复杂度为O(1)。表尾结点是表中最后一个结点。而表头结点实际上是一个伪结点,该结点的成员对象为NULL,分值为0,它的层数固定为32(层的最大值)。

length属性记录结点的数最,程序可以在O(1)的时间复杂度内返回跳跃表的长度。

level属性记录跳跃表的层数,也就是表中层高最大的那个结点的层数,注意,表头结点的层高并不计算在内。

/*
 * 跳跃表节点
 */
typedef struct zskiplistNode {
 // 成员对象
 robj *obj;
 // 分值
 double score;
 // 后退指针
 struct zskiplistNode *backward;
 // 层
 struct zskiplistLevel {
 // 前进指针
 struct zskiplistNode *forward;
 // 跨度
 unsigned int span;
 } level[];
} zskiplistNode;

obj是该结点的成员对象指针(member),score是该对象的分值,是一个浮点数,跳跃表中的所有结点,都是根据score从小到大来排序的。

同一个跳跃表中,各个结点保存的成员对象必须是唯一的,但是多个结点保存的分值却可以是相同的:分值相同的结点将按照成员对象的字典顺序从小到大进行排序。

level数组是一个柔性数组成员,它可以包含多个元素,每个元素都包含一个层指针(level[i].forward),指向该结点在本层的后继结点。该指针用于从表头向表尾方向访问结点。可以通过这些层指针来加快访问结点的速度。

每次创建一个新跳跃表结点的时候,程序都根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是该结点包含的层数。

/*
 * 有序集合
 */
typedef struct zset {
 // 字典,键为成员,值为分值
 // 用于支持 O(1) 复杂度的按成员取分值操作
 dict *dict;
 // 跳跃表,按分值排序成员
 // 用于支持平均复杂度为 O(log N) 的按分值定位成员操作
 // 以及范围操作
 zskiplist *zsl;
} zset

添加member和score时会把member和score存入dict里,用来支持 O(1) 复杂度的按成员取分值操作 。
实现逻辑:
1. 获取集合的长度O(1) zskiplist->length
2. 获取score值O(1) redis会把member和score存入dict里
3. zrank怎么取排名的? 时间复杂度:T_wrost = O(N), T_avg = O(log N)

在这里插入图片描述
redis会从header节点遍历整个跳跃表,从level层开始,沿着前进指针遍历 然后逐层递减遍历,期间会读取zskiplistNode->span 用变量rank记录累积跨越的节点数量,如果找到最终返回rank,没找到返回0。
4. zadd新增节点 时间复杂度:T_wrost = O(N), T_avg = O(log N)

1) redis从header节点遍历整个跳跃表,从level层开始,沿着前进指针遍历,在各个层查找节点的插入位置,沿途会记录下跨越的节点数rank[i],和将要和新节点相连接的节点update[i]

2)通过幂次定律获取一个随机值作为新节点的层数,最大32,然后创建新节点

3)将前面记录的指针指向新节点,并做相应的设置,设置各个层的span,forward

4)设置新节点的后退指针,跳跃表的节点计数增一

跳跃表的实现原理可以参考:https://blog.csdn.net/Acceptedxukai/article/details/17333673

有序列表是使用skiplist和dict结合实现的,skiplist用来保障有序性和访问查找性能,dict就用来存储元素信息,并且dict的访问时间复杂度为O(1)。

redis如何实现高可用【主从复制、哨兵机制】:https://blog.csdn.net/itcats_cn/article/details/82428716
redis实战–redis主从复制实现读写分离(原理):https://blog.csdn.net/xuxian6823091/article/details/81195231
基于Redis的分布式锁到底安全吗?https://blog.csdn.net/paincupid/article/details/75094550
如何优雅地用Redis实现分布式锁? https://baijiahao.baidu.com/s?id=1623086259657780069&wfr=spider&for=pc
Redis分布式锁的实现原理看这篇就够了~ https://www.douban.com/note/697473588/
Redis分布式锁原理解析~ https://blog.csdn.net/andy86869/article/details/81668355

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值