Redis基础--Redis单线程为什么快?

2020-11-10  本文已影响0人  DevilRoshan

Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。

Redis的五种基本类型

数据类型 可以存储的值 操作
String 字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作
List 链表、列表 从两端压入或者弹出元素
读取单个或者多个元素
进行修剪,只保留一个范围内的元素
Set 无序集合 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素
Hash 包含键值对的无序散列表 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在
ZSet 有序集合 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名

String

Redis 是用 C 语言开发完成的,但在 Redis 字符串中,并没有使用 C 语言中的字符串,而是用一种称为 SDS(Simple Dynamic String)的结构体来保存字符串。

struct sdshdr {
    int len;
    int free;
    char buf[];
}

优点:

应用场景:缓存、计数器、分布式锁等。

List

Redis列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素导列表的头部(左边)或者尾部(右边)。

在版本3.2之前,Redis 列表list使用两种数据结构作为底层实现。

因为双向链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表, 并且在有需要的时候, 才从压缩列表实现转换到双向链表实现。

压缩列表转化成双向链表条件:

创建新列表时 redis 默认使用 redis_encoding_ziplist 编码, 当以下任意一个条件被满足时, 列表会被转换成 redis_encoding_linkedlist 编码:

注意:这两个条件是可以修改的,在 redis.conf 中。

压缩列表ziplist:

ziplist使用一块连续的内存,每一个节点(entry)都是连续存储的,其包含了如下内容:

长度/类型 阈的值
zlbytes uint32_t 整个ziplist所用的内存字节数。
zltail uint32_t 到达ziplist表尾节点的偏移量。
zllen uint16_t ziplist中节点的数量。
entryX ziplist所保存的节点。
zlend uint8_t 用于标记ziplist的末端。

每一个存储节点(entry)都是一个zlentry (zip list entry)。ziplist每一个存储节点、都是一个 zlentry,就是上文所说的entry;

typedef struct zlentry {    // 压缩列表节点
    unsigned int prevrawlensize, prevrawlen;    // prevrawlen是前一个节点的长度,prevrawlensize是指prevrawlen的大小,有1字节和5字节两种
    unsigned int lensize, len;  // len为当前节点长度 lensize为编码len所需的字节大小
    unsigned int headersize;    // 当前节点的header大小
    unsigned char encoding; // 节点的编码方式
    unsigned char *p;   // 指向节点的指针
} zlentry;

void zipEntry(unsigned char *p, zlentry *e) {   // 根据节点指针返回一个enrty
    ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);    // 获取prevlen的值和长度
    ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);  // 获取当前节点的编码方式、长度等
    e->headersize = e->prevrawlensize + e->lensize; // 头大小
    e->p = p;
}

ziplist的主要优点是节省内存,但它上面的查找操作只能按顺序查找(可以是从前往后、也可以从后往前)ziplist将数据按照一定规则编码在一块连续的内存区域,目的是节省内存,这种结构并不擅长做修改操作。一旦数据发生改动,就会引发内存realloc,可能导致内存拷贝。

双向链表linkedlist

当链表entry数据超过512、或单个value 长度超过64,底层就会转化成linkedlist编码;

linkedlist是标准的双向链表,Node节点包含prev和next指针,可以进行双向遍历;还保存了 head 和 tail 两个指针,因此,对链表的表头和表尾进行插入的复杂度都为 (1) —— 这是高效实现 LPUSH 、 RPOP、 RPOPLPUSH 等命令的关键。

这两种存储方式的优缺点

版本3.2之后,重新引入 quicklist,列表的底层都由quicklist实现。

quickList

可以认为quickList,是ziplist和linkedlist二者的结合;quickList将二者的优点结合起来。quickList是一个ziplist组成的双向链表,每个节点使用ziplist来保存数据。

typedef struct quicklistNode {
    struct quicklistNode *prev; //上一个node节点
    struct quicklistNode *next; //下一个node
    unsigned char *zl;            //保存的数据 压缩前ziplist 压缩后压缩的数据
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

typedef struct quicklistLZF {
    unsigned int sz; /* LZF size in bytes*/
    char compressed[];
} quicklistLZF;

typedef struct quicklist {
    quicklistNode *head; //头结点
    quicklistNode *tail; //尾节点
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned int len;           /* number of quicklistNodes */
    int fill : 16;              /* fill factor for individual nodes *///负数代表级别,正数代表个数
    unsigned int compress : 16; /* depth of end nodes not to compress;0=off *///压缩级别
} quicklist;
  • quickList就是一个标准的双向链表的配置,有head 有tail;
  • 每一个节点是一个quicklistNode,包含prev和next指针;
  • 每一个quicklistNode 包含 一个ziplist,*zp 压缩链表里存储键值;

所以quicklist是对ziplist进行一次封装,使用小块的ziplist来既保证了少使用内存,也保证了性能。

应用场景:链表、队列、微博关注人时间轴列表等。

Set

Set 就是一个集合,集合的概念就是一堆不重复值的组合。利用 Redis 提供的 Set 数据结构,可以存储一些集合性的数据。

Set的底层存储结构底层使用了intset和hashtable两种数据结构,intset可以理解为数组,hashtable就是普通的哈希表(key为set的值,value为null)。使用intset存储必须满足下面两个条件,否则使用hashtable,条件如下:

intset数据结构

intset内部其实是一个数组(int8_t coentents[]数组),而且存储数据的时候是有序的,因为在查找数据的时候是通过二分查找来实现的。

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

应用场景:去重、赞、踩、共同好友等。

Hash

Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。

Redis的哈希对象的底层存储可以使用ziplist(压缩列表)和hashtable。当hash对象可以同时满足一下两个条件时,哈希对象使用ziplist编码。

Redis的hash架构就是标准的hashtab的结构,通过挂链解决冲突问题。

应用场景:用户信息、Hash 表等。

Zset

Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset底层的存储结构包括ziplist或skiplist,在同时满足以下两个条件的时候使用ziplist,其他时候使用skiplist,两个条件如下:

当skiplist作为zset的底层存储结构的时候,使用skiplist按序保存元素及分值,使用dict来保存元素和分值的映射关系。

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

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

前进指针:用于从表头向表尾方向遍历;

后退指针:用于从表尾向表头方向回退一个节点,和前进指针不同的是,前进指针可以一次性跳跃多个节点,后退指针每次只能后退到上一个节点。

跨度:表示当前节点和下一个节点的距离,跨度越大,两个节点中间相隔的元素越多。

应用场景:访问量排行榜、点击量排行榜等。

编码转化

Redis 使用对象(redisObject)来表示数据库中的键值,当我们在 Redis 中创建一个键值对时,至少创建两个对象,一个对象是用做键值对的键对象,另一个是键值对的值对象。

typedef struct redisObject{
    //类型
   unsigned type:4;
   //编码
   unsigned encoding:4;
   //指向底层数据结构的指针
   void *ptr;
    //...
 }robj;

其中 type 字段记录了对象的类型,包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象。

过期数据删除

三种删除策略:

总之

Redis性能如此高的原因,有如下几点:

上一篇 下一篇

猜你喜欢

热点阅读