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

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[];
}
  • len:用于记录 buf 中已使用空间的长度;
  • free:buf 中空闲空间的长度;
  • buf[]:存储实际内容。

优点:

  • 常数时间内可以获得字符串的长度;
  • 避免了缓冲区溢出;
  • 减少字符串修改时带来的内存重新分配的次数;
    • 空间预分配。如果对 SDS 修改后,len 的长度小于 1M,那么程序将分配和 len 相同长度的未使用空间。
    • 惰性空间释放。当对 SDS 进行缩短操作时,程序并不会回收多余的内存空间,而是使用 free 字段将这些字节数量记录下来不释放,后面如果需要 append 操作,则直接使用 free 中未使用的空间,减少了内存的分配。
  • 二进制安全。字符串结束不是'\0',而是由SDS中的len来决定。

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

List

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

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

  • 压缩列表ziplist;
  • 双向链表linkedlist;

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

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

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

  • 试图往列表新添加一个字符串值,且这个字符串的长度超过 server.list_max_ziplist_value (默认值为 64 )。
  • ziplist 包含的节点超过 server.list_max_ziplist_entries (默认值为 512 )。

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

压缩列表ziplist:

  • 一个特殊的双向链表。没有维护双向指针:prev next;而是存储上一个 entry的长度和 当前entry的长度,通过长度推算下一个元素在什么地方。
  • 字段、值比较小,才会用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 等命令的关键。

这两种存储方式的优缺点

  • 双向链表linkedlist便于在表的两端进行push和pop操作,在插入节点上复杂度很低,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
  • ziplist存储在一段连续的内存上,所以存储效率很高。但是,它不利于修改操作,插入和删除操作需要频繁的申请和释放内存。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝。

版本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,条件如下:

  • 集合对象保存的所有元素都是整数值;
  • 集合对象保存的元素数量不超过512个。

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编码。

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
  • 哈希对象保存的键值对数量小于512个。

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

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

Zset

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

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

  • 有序集合保存的所有元素的长度小于64字节;
  • 有序集合保存的元素数量小于128个。

当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 字段记录了对象的类型,包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象。

过期数据删除

三种删除策略:

  • 定时删除。在这是键的过期时间的同时,创建一个定时器 Timer,让定时器在键过期时间来临时立即执行对过期键的删除;
  • 惰性删除。键过期后不管,每次读取该键时,判断该键是否过期,如果过期删除该键返回空;
  • 定期删除。每隔一段时间对数据库中的过期键进行一次检查。

总之

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

  • 纯内存操作;
  • 单线程;
  • 高效的数据结构;
  • 合理的数据结构编码。

你可能感兴趣的:(Redis基础--Redis单线程为什么快?)