三.数据库基础-非关系型数据库Redis学习这一篇就够了(详细)

Redis介绍

Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。Redis也是 NoSQL技术阵营中的一员,其官方网址:https://redis.io/。

Redis学习笔记

第一章 基本数据结构

一、SDS字符串

1. SDS的结构

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

SDS遵循C字符串以空字符结尾的惯例,遵循空字符结尾这一惯例的好处是,SDS可以直接重用一部分C字符串函数库里面的函数。

2. SDS与C字符串的区别
a.C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符’\0’,但并不能满足Redis对字符串在安全性、效率以及功能方面的要求。
b.所以SDS添加了len与free,保存字符长度与未使用空间大小。
其带来的好处:
1.获取字符串长度常数复杂度
2.增长字符串时杜绝缓冲区溢出
当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题。
3.减少修改字符串时带来的内存重分配次数。
通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。
4.二进制安全
C字符串中除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设。且SDS使用len属性的值而不是空字符来判断字符串是否结束
在这里插入图片描述

二、链表

1.链表与链表节点结构

  • 链表节点
typedef struct listNode {
    // 前置节点
    struct listNode * prev;
    // 后置节点
    struct listNode * next;
    //节点的值
    void * value;
}listNode;
  • 链表
typedef struct list {
    //
    表头节点
    listNode * head;
    //
    表尾节点
    listNode * tail;
    //
    链表所包含的节点数量
    unsigned long len;
    //
    节点值复制函数
    void *(*dup)(void *ptr);
    //
    节点值释放函数
    void (*free)(void *ptr);
    //
    节点值对比函数
    int (*match)(void *ptr,void *key);
} list;

2.Redis的链表实现的特性
a.双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。
b.无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
c.带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。
d.带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。
e.多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

三、字典

字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。
Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。字典还是哈希键的底层实现之一。
1.哈希表结构
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

  • 哈希表结构
typedef struct dictht {
    //哈希表数组
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,用于计算索引值
    //总是等于size-1
    unsigned long sizemask;
    //该哈希表已有节点的数量
    unsigned long used;
} dictht;

table属性是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对

  • 哈希字典结构
typedef struct dictEntry {
    //键
    void *key;
    //值
    union{
        void *val;
        uint64_tu64;
        int64_ts64;
    } v;
    //指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

key属性保存着键值对中的键,而v属性则保存着键值对中的值,其中键值对的值可以是一个指针,或者是一个uint64_t整数,又或者是一个int64_t整数。

2.字典结构

typedef struct dict {
    //类型特定函数
    dictType *type;
    //私有数据
    void *privdata;
    //哈希表
    dictht ht[2];
    // rehash索引
    //当rehash不在进行时,值为-1
    in trehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;

type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的。
ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
在这里插入图片描述
3.哈希算法
当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。Redis使用MurmurHash2算法来计算键的哈希值。

Redis的哈希表使用链地址法(separate chaining)来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题。

随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成,Redis通过将h[0]数据复制到h[1]来实现。

当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:1)服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。2)服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。
#负载因子=哈希表已保存节点数量/哈希表大小
load_factor = ht[0].used / ht[0].size
当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。

扩展或收缩哈希表需要将ht[0]里面的所有键值对rehash到ht[1]里面,但是,这个rehash动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。
在渐进式rehash进行期间,字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行。新添加到字典的键值对一律会被保存到ht[1]里面。

四、跳跃表

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,在大部分情况下,跳跃表的效率可以和平衡树相媲美。
Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构。
1.跳跃表的数据结构
Redis的跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息。
在这里插入图片描述
zskiplistNode结构:

typedef struct zskiplistNode {
    //层
    struct zskiplistLevel {
        //前进指针
        struct zskiplistNode *forward;
        //跨度
        unsigned int span;
    } level[];
    //后退指针
    struct zskiplistNode *backward;
    //分值
    double score;
    //成员对象
    robj *obj;
} zskiplistNode;
  • 层:跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快
  • 前进指针:每个层都有一个指向表尾方向的前进指针,用于从表头向表尾方向访问节点。
  • 跨度:用于记录两个节点之间的距离。跨度实际上是用来计算排位(rank)的:在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位
  • 后退指针:用于从表尾向表头方向访问节点。
  • 分值和成员:节点的分值(score属性)是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。节点的成员对象(obj属性)是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值。在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面。

zskiplist结构:

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

五、整数集合

整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素
1.整数结构

typedef struct intset {
    //编码方式
    uint32_t encoding;
    //集合包含的元素数量
    uint32_t length;
    //保存元素的数组
    int8_t contents[];
} intset;

整数集合的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。
2.类型升级
每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级(upgrade),然后才能将新元素添加到整数集合里面。
整数集合的升级策略有两个好处,一个是提升整数集合的灵活性,另一个是尽可能地节约内存。
整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态

六、压缩列表

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。
1.压缩列表的组成
在这里插入图片描述
在这里插入图片描述
2.压缩列表节点
在这里插入图片描述
每个压缩列表节点可以保存一个字节数组或者一个整数值。

  • previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度,压缩列表的从表尾向表头遍历操作就是使用这一原理实现的。
  • encoding属性记录了节点的content属性所保存数据的类型以及长度。
  • 节点的content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。
    3.连锁更新
    previous_entry_length属性都记录了前一个节点的长度,如果前一节点的长度小于254字节,那么previous_entry_length属性需要用1字节长的空间来保存这个长度值。如果前一节点的长度大于等于254字节,那么previous_entry_length属性需要用5字节长的空间来保存这个长度值。
    在特殊情况下产生的连续多次空间扩展操作称之为“连锁更新”,每次空间重分配的最坏复杂度为O(N),所以连锁更新的最坏复杂度为O(N 2)。

第二章 五种数据对象

Redis用到的所有主要数据结构:简单动态字符串(SDS)、双端链表、字典、压缩列表、整数集合等等。Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统。
这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象。
Redis的对象系统还实现了基于引用计数技术的内存回收机制,Redis还通过引用计数技术实现了对象共享机制。
Redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时长较大的那些键可能会优先被服务器删除。

对象类型

Redis中的每个对象都由一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type属性、encoding属性和ptr属性

typedef struct redisObject {
    //类型
    unsigned type:4;
    //编码
    unsigned encoding:4;
    //指向底层实现数据结构的指针
    void *ptr;
    //引用计数
    int refcount;
    //记录了对象最后一次被命令程序访问的时间
    unsigned lru:22;
    
    // ...
} robj;
  • type为常量:在这里插入图片描述
    对于Redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种
  • encoding属性记录了对象所使用的编码。
    在这里插入图片描述
  • 对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。
    在这里插入图片描述
  • 当对象的引用计数值变为0时,对象所占用的内存会被释放。除了用于实现引用计数内存回收机制之外,对象的引用计数属性还带有对象共享的作用。
  • 空转时长较高的那部分键会优先被服务器释放,从而回收内存。

1.字符串对象

字符串对象的编码可以是int、raw或者embstr。

  • 如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么编码设置为int。
  • 如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS),编码设置为raw。
  • 如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于32字节,那么字符串对象将使用embstr编码。embstr编码是专门用于保存短字符串的一种优化编码方式。
  • 可以用long double类型表示的浮点数在Redis中也是作为字符串值来保存的。在有需要的时候,程序会将保存在字符串对象里面的字符串值转换回浮点数值,执行某些操作。
    在这里插入图片描述
  • int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。
  • Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值

2.列表对象

列表对象的编码可以是ziplist或者linkedlist。

  • ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素。
  • linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。(字符串对象是Redis五种类型的对象中唯一一种会被其他四种类型对象嵌套的对象)
  • 当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码。
    1.列表对象保存的所有字符串元素的长度都小于64字节
    2.列表对象保存的元素数量小于512个;不能满足这两个条件的列表对象需要使用linkedlist编码
    当使用ziplist编码所需的两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行,原本保存在压缩列表里的所有列表元素都会被转移并保存到双端链表里面

3.哈希对象

哈希对象的编码可以是ziplist或者hashtable。

  • ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾
  • hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存。
  • hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存:
    1.hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存
    2.hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存

4.集合对象

集合对象的编码可以是intset或者hashtable。

  • intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。
  • hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。
  • 当集合对象可以同时满足以下两个条件时,对象使用intset编码
    1.集合对象保存的所有元素都是整数值
    2.集合对象保存的元素数量不超过512个

5.有序集合对象

有序集合的编码可以是ziplist或者skiplist

  • ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。
  • skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。
  • 当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码
    1.有序集合保存的元素数量小于128个
    2.有序集合保存的所有元素成员的长度都小于64字节。

类型检查与命令多态

Redis中用于操作键的命令基本上可以分为两种类型。
一种命令可以对任何类型的键执行,比如说DEL命令、EXPIRE命令、RENAME命令、TYPE命令、OBJECT命令等。
另一种命令只能对特定类型的键执行,比如说:❑SET、GET、APPEND、STRLEN等命令只能对字符串键执行;❑HDEL、HSET、HGET、HLEN等命令只能对哈希键执行;❑RPUSH、LPOP、LINSERT、LLEN等命令只能对列表键执行;❑SADD、SPOP、SINTER、SCARD等命令只能对集合键执行;❑ZADD、ZCARD、ZRANK、ZSCORE等命令只能对有序集合键执行

Redis除了会根据值对象的类型来判断键是否能够执行指定命令之外,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令

第三章 数据库

1服务器中数据库

Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中。

  • db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库。
  • 服务器状态的dbnum属性来决定应该创建多少个数据库。默认情况下,Redis客户端的目标数据库为0号数据库。客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针。

2数据库键空间

1服务器中的每个数据库都由一个redis.h/redisDb结构表示,其中,redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space)。

  • 每个键都是一个字符串对象。每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的任意一种Redis对象

2当使用Redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作

  • 在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数。
  • 在读取一个键之后,服务器会更新键的LRU(最后一次使用)时间。
  • 如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键。
  • 如果有客户端使用WATCH命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务程序注意到这个键已经被修改过。
  • 服务器每次修改一个键之后,都会对脏(dirty)键计数器的值增1,这个计数器会触发服务器的持久化以及复制操作。
  • 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知。

3键的生存时间或过期时间

  • Redis有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除)。EXPIRE<key><ttl>命令、PEXPIRE<key><ttl>命令、EXPIREAT<key><timestamp>命令、PEXPIREAT<key><timestamp>命令。
  • redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典
  • Redis服务器实际使用的是惰性删除和定期删除两种策略删除过期键。过期键的惰性删除策略由db.c/expireIfNeeded函数实现,过期键的定期删除策略由redis.c/activeExpireCycle函数实现。
  • RDB持久化不会同步过期键。
  • AOF持久化会同步过期键,当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显式地记录该键已被删除。
  • 在复制(主从)模式下时,从服务器的过期键删除动作由主服务器控制。通过由主服务器来控制从服务器统一地删除过期键,可以保证主从服务器数据的一致性,也正是因为这个原因,当一个过期键仍然存在于主服务器的数据库时,这个过期键在从服务器里的复制品也会继续存在。

4持久化

  • RDB持久化可以将Redis在内存中的数据库状态保存到磁盘里。RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态。有两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE。SAVE命令会阻塞Redis服务器进程,BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件。
  • AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面。

其他大神总结

Redis学习总结
学习Redis5这一篇就够了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值