首先问一下有关redis可能知其然不知其所以然的问题

1、为什么redis默认是16个数据库?

2、为什么redis有这么多数据结构,它的数据结构都存储在哪儿呢?

3、为什么redis还可以存储二进制字符串?

接下来我们带着疑问,去github上面把源码拉下来,看看其中到底有何神秘之处,是否真如我们所想,一看便知。

1、数据结构分析

源码拉取

Redis的底层数据结构分析_redis

我们直接拉去最新的代码一看便知

# 使用git工具克隆项目到本地
git clone https://github.com/redis/redis.git

Redis的底层数据结构分析_数组_02

1.1、找到redisDb的数据结构

Redis的底层数据结构分析_数组_03

1.2、redisDb结构

typedef struct redisDb {
dict *dict; /* 存储所有的key-value */
dict *expires; /* 存储key的过期时间 */
dict *blocking_keys; /* blpop存储阻塞key和客户端对象*/
dict *ready_keys; /* 阻塞后push,响应阻塞的那些客户端和key */
dict *watched_keys; /* 存储watch监控的key和客户端对象 WATCHED keys for MULTI/EXEC CAS */
int id; /* 数据库的ID为0-15,默认redis有16个数据库 */
long long avg_ttl; /* 存储对象的额平均ttl(time in live)时间用于统计 */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
clusterSlotToKeyMapping *slots_to_keys; /* Array of slots to keys. Only used in cluster mode (db 0). */
} redisDb;

重要字段

字段

类型

描述

id

int整型

数据库的ID为0-15,默认redis有16个数据库

dict

dict字典类型

存储所有的key-value

expires

dict字典类型

存储key的过期时间

1.3、RedisObject对象

我们在server.h中还发现了一个RedisObject, 它就是存储Value的值的,包括string, list, set, sortedset, hash等

typedef struct redisObject {
unsigned type:4; /*类型 5种对象类型*/
unsigned encoding:4; /*编码*/
unsigned lru:LRU_BITS; /* LRU_BITS为24bit记录最后一次被命令程序访问的时间 */
int refcount; /*引用计数*/
void *ptr; /*指向底层实现数据结构的指针*/
} robj;

1.3.1、type字段

type表示对象的类型,占4位,当我们执行type命令时,便是通过RedisObject的type字段获得对象的类型。

命令查看type类型

# 设置整数 类型为string
127.0.0.1:6379> set test01 1
OK
127.0.0.1:6379> type test01
string
# 设置短字符串 长度小于44个字节 类型为string
127.0.0.1:6379> set test02 aaa
OK
127.0.0.1:6379> type test02
string
# 设置列表 类型为list
127.0.0.1:6379> lpush test03 aaa bbb ccc
(integer) 3
127.0.0.1:6379> type test03
list
# 设置集合 类型为set
127.0.0.1:6379> sadd test04 aaa bbb ccc
(integer) 3
127.0.0.1:6379> type test04
set
# 设置散列表 类型为hash
127.0.0.1:6379> hset test05 name zhangsan age 20
(integer) 2
127.0.0.1:6379> type test05
hash
# 设置有序集合 类型为zset
127.0.0.1:6379> zadd test06 100 zhangsan 80 lisi 90 wangwu
(integer) 3
127.0.0.1:6379> type test06
zset

1.3.2、encoding字段

也是4位,表示对象的内部编码,每个对象有不同的实现编码。

Redis可以通过不同的使用场景来设置对象不同的编码,大大的提高了redis的灵活性。

通过object encoding可以查看对象采用什么样的编码

# key为test01表示整数,编码是int
127.0.0.1:6379> object encoding test01
"int"
# key为test02表示字符串,编码是embstr字符串
127.0.0.1:6379> object encoding test02
"embstr"
# 设置长字符串(超过44个字节) 类型为string
127.0.0.1:6379> set test02 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
OK
127.0.0.1:6379> object encoding test02
"raw"
# key为test03表示字符串,编码是列表 存储数据的是quicklist(快速列表)
127.0.0.1:6379> object encoding test03
"quicklist"
# key为test04表示set集合,编码是哈希表 存储数据的是hashtable
127.0.0.1:6379> object encoding test04
"hashtable"
# key为test05表示hash散列表,编码是ziplist 也就是压缩列表
127.0.0.1:6379> object encoding test05
"ziplist"
# key为test05表示hashtable散列表,编码是hashtable 也就散列表 存储大字段
127.0.0.1:6379> mget test05 name
1) (nil)
2) "zhangsan"
127.0.0.1:6379> hset test05 name zhangsan1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
(integer) 0
127.0.0.1:6379> object encoding test05
"hashtable"
# sadd数字和上面存储的字符串结构不一样
127.0.0.1:6379> sadd test10 a b c
(integer) 3
127.0.0.1:6379> sadd test11 1 2 3
(integer) 3
127.0.0.1:6379> object encoding test10
"hashtable"
127.0.0.1:6379> object encoding test11
"intset"
# 跳跃表例子
127.0.0.1:6379> zadd test12 100 zhangsan11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 45 lisi 90 wangwu
(integer) 3
127.0.0.1:6379> object encoding test12
"skiplist"

从这里可以看出,redis的编码很多,也对应这不同的数据结构,为什么这么多呢,当然是效率咯。后面会详细说明这些编码。

1.3.3、lru字段

lru 记录的是对象最后一次被命令程序访问的时间,高16位存储一个分钟数级别的时间戳,低8位存储访问计数。

1.3.4、refcount字段

refcount 记录的是该对象被引用的次数,类型为整型, 主要在于对象的引用计数和内存回收。

当对象的refcount>1时,称为共享对象.

Redis 为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象

1.3.5、ptr字段

ptr 指针指向具体的数据,比如:set name zhangsan,ptr指向包含字符串world 的指针

1.4、redis的字符串类型结构

redis的字符串跟一般的字符串不一样,它叫做SDS(Simple Dynamic String), 用于存储字符串和整型数据。结构如下:

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

字符串的buf数组长度=free+len+1

使用SDS有什么好处呢,为什么要专门设计一个呢?

  • SDS在字符串基础上加入了len和free字段,获取字段长度时间复杂度从O(N)变为O(1).
  • SDS记录了长度,在可能造成缓冲区溢出时,会自动重新分配内存,杜绝溢出。
  • 可以存储二进制数据,以字符串长度len来作为结束标识符

1.5、Redis中的跳跃表

1.5.1、跳跃表简介

跳跃表时有序集合的底层实现,效率高,实现简单。

就是将有序链表中的部分节点分层,每一层都是一个有序节点。只是越上层的节点数量越少,直接快速到达目标节点,从而提升效率。

Redis的底层数据结构分析_字符串_04

可以看到,分层之后我们可以更快速的查找到目标元素。

也是在server.h中可以看到跳跃表的结构

typedef struct zskiplistNode {
sds ele; /*存储字符串类型数据*/
double score; /*分数*/
struct zskiplistNode *backward; /*后退指针,指向当前节点最底层的前一个节点*/
struct zskiplistLevel { /*层,是一个数组,随机生成1-64的值*/
struct zskiplistNode *forward; /*指向本层下一个节点*/
unsigned long span; /*本层下个节点到本节点的元素个数*/
} level[];
} zskiplistNode;

1.5.2、跳跃表的优势

  • 快速查找到目标节点
  • 可以在O(1)复杂度获取头节点,尾节点,长度和高度。

1.6、压缩列表

压缩列表(ziplist)是由一系列特殊编码的连续内存块组成的顺序型数据结构, 就是为了节省内存。

sorted-set和hash元素个数少且是小整数或短字符串, 就是使用压缩列表存储的。

list用快速链表(quicklist)数据结构存储,而快速链表是双向列表与压缩列表的组合。

1.7、快速列表

1.7.1、快速列表简介

快速列表(quicklist)是Redis底层重要的数据结构。是列表的底层实现,也是一个双向列表,既可以充当队列,也可以充当栈的使用。

typedef struct quicklist {
quicklistNode *head; /*头节点指针*/
quicklistNode *tail; /*尾结点指针*/
unsigned long count; /* 列表中所有数据项的个数总和 */
unsigned long len; /* quicklist节点的个数,即ziplist的个数 */
signed int fill : QL_FILL_BITS; /* ziplist大小限定,由list-max-ziplist-size给定 */
unsigned int compress : QL_COMP_BITS; /* 节点压缩深度设置,由list-compress-depth给定 */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;

快速列表节点定义

typedef struct quicklistNode {
struct quicklistNode *prev; /*上一个节点的指针*/
struct quicklistNode *next; /*下一个节点的指针*/
unsigned char *entry; /*数据指针,如果没有被压缩,就指向ziplist结构,反之指quicklistLZF结构*/
size_t sz; /* 表示指向ziplist结构的总长度(内存占用长度) */
unsigned int count : 16; /* 表示ziplist中的数据项个数 */
unsigned int encoding : 2; /* RAW==1 or LZF==2 编码方式,1--ziplist,2--quicklistLZF*/
unsigned int container : 2; /* PLAIN==1 or PACKED==2 预留字段,存放数据的方式,1--NONE,2--ziplist*/
unsigned int recompress : 1; /* 解压标记,当查看一个被压缩的数据时,需要暂时解压,标记此参数为1,之后再重新进行压缩 */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

quicklist每个节点的实际数据存储结构为ziplist,这种结构的优势在于节省存储空间。为了进一步降低ziplist的存储空间,还可以对ziplist进行压缩。

Redis采用的压缩算法是LZF。

其基本思想是:数据与前面重复的记录重复位置及长度,不重复的记录原始数据。

压缩过后的数据可以分成多个片段,每个片段有两个部分:解释字段和数据字段。quicklistLZF的结构

typedef struct quicklistLZF {
size_t sz; /* LZF压缩后占用的字节数*/
char compressed[]; /*数组,指向数据部分*/
} quicklistLZF;

1.7.2、应用场景

列表(list)的底层实现、发布与订阅、慢查询、监视器等功能

1.8、字典dict

1.8.1、字典简介

什么是字典?

字典dict又称之为散列表(hash),是用来存储键值对的一种数据结构。

Redis整个数据库都是采用字典来存储的。所以也叫做KV结构数据库。

Hash表的实现是使用数组+链表。

什么是数组?

用来存储数据的容器,就是在计算机内开辟一块连续的空间用于存储数据。因为空间是连续的,就可以使用头节点内存地址+偏移量就可以定位到每一个元素的内存地址。时间复杂度O(1)

什么是Hash函数

因为数组的长度是有限的,我们通过hash函数将redis的key进行hash函数转换为固定长度的散列值,然后再对数组容量进行取模。

数组下标=hash(key) % 数组容量(hash值除以数组容量得到的余数)

什么是hash冲突?

我数组长度只有16,你来了30个key, 并且其中存在两个key通过hash函数取模之后计算的数组下标是一样的。这就是hash冲突。

hash冲突如何解决?

既然存储的值会冲突,冲突的值可以存储为一个链表,如果出现hash冲突了,就在链表的末尾添加一个节点。

什么是链表?

链表就是再计算机内开辟一块不连续的空间用于存储数据,但是每个节点出了存储数据本身,还可以存储下一个节点的内存地址的一个指向。这就是单链表。如果存储了前后指针,那就是双向链表。如果双向链表首尾相连,那就是循环链表。

搞清楚了这些下后,来看一下redis中字典的结构。

1.8.2、Redis的字典结构

Redis的底层数据结构分析_字符串_05

字典数据结构

typedef struct dict {
dictEntry **table; /*hash表数组*/
dictType *type; /*字典类型*/
unsigned long size; /*哈希表数组的大小*/
unsigned long sizemask; /*用于映射位置的掩码,值永远等于(size-1)*/
unsigned long used; /*哈希表已有节点的数量,包含next单链表数据*/
void *privdata;
} dict;

hash表的数组初始容量为4,随着k-v存储量的增加需要对hash表数组进行扩容,新扩容量为当前量的一倍,即4,8,16,32。

索引值=Hash值&掩码值(Hash值与Hash表容量取余)

hash表的节点对象

typedef struct dictEntry {
void *key; /*键*/
void *val; /*值*/
struct dictEntry *next; /*下一个节点的指针*/
} dictEntry;

字典扩容

字典达到存储上限,需要rehash(扩容)

  1. 初次申请默认容量为4个dictEntry,非初次申请为当前hash表容量的一倍。
  2. rehashidx=0表示要进行rehash操作
  3. 新增加的数据在新的hash表h[1]
  4. 修改、删除、查询在老hash表h[0]、新hash表h[1]中(rehash)
  5. 将老的hash表h[0]的数据重新计算索引值后, 全部迁移到新的hash表h[1]中,这个过程称为rehash

1.9、流对象

stream主要由:消息、生产者、消费者和消费组构成。

Redis Stream的底层主要使用了listpack(紧凑列表)和Rax树(基数树)。

listpack表示一个字符串列表的序列化,listpack可用于存储字符串或整数。用于存储stream的消息内容。

Rax 是一个有序字典树 (基数树 Radix Tree),按照 key 的字典序排列,支持快速地定位、插入和删除操作。

Rax 被用在 Redis Stream 结构里面用于存储消息队列,在 Stream 里面消息 ID 的前缀是时间戳 + 序号,这样的消息可以理解为时间序列消息。

使用 Rax 结构进行存储就可以快速地根据消息 ID 定位到具体的消息,然后继续遍历指定消息之后的所有消息。

2、小结

Redis里面采用了大量的数据结构优化来提升性能,所以才能更加强大,更好的了解redis的底层数据结构,有利于我们直到redis为什么性能这么高的一个重要原因之一。