Redis底层核心数据结构
Redis是key-value数据结构,k-v 在java中会想到map,redis中是叫dict。
redis还是数据库,它是使用数组和链表来存储海量的数据的。
hash(key) --> 自然数 % array.size --> 这样就得到了数组的下标了,然后把key-value保存在数组的这个位置
如果产生了hash冲突,是使用链表来解决的,使用的头插法
DB
Redis中默认有16个数据库,底层是redisDb对象,代码如下
typedef struct redisDb {
dict *dict; // 上面提到的 key-value 就是存储在dict中的
dict *expires; // 存储各个key的过期时间
dict *blocking_keys; // 存储的是阻塞队列相关的内容
dict *ready_keys; // 维护key和client客户端连接之间的对应关系
dict *watched_keys; // 关于事务处理是存放在watched_keys
int id; // 这个就是数据库的id 0~15
long long avg_ttl;
unsigned long expires_cursor;
list *defrag_later;
} redisDb;
重点是要讲解dict
,我们可以理解为这就是一个hashtable
typedef struct dict {
dictType *type; // 指定hash算法,并且产生hash冲突后去进行equals比较 是否进行覆盖或者头插法插入元素
void *privdata;
dictht ht[2]; // 这就是一个hashtable结构,ht[0]是老数组 ht[1]是新数组,指向下面的dictht对象
long rehashidx;
unsigned long iterators;
} dict;
dictht
的代码如下所示。这就是一个hashtable的数据结构,每个dict
字典都有两个dictht
,目的就是实现一个渐进式的rehash,其实就是数组的扩容,把老数组的内容拷贝到扩容后新数组中去。Redis的扩容是newSize=oldSize*2,扩容完成后并不是一次性把所有的key-value移动到新数组中去,而是一次移动一部分数据,然后去处理用户请求,过一会了又移动一部分,扩容是在master线程中执行的,扩容触发的条件是size : used = 1。当把ht[0]中的数据都移动到ht[1]之后,会把ht[0]指向ht[1],ht[1]=null
在移动数据过程中如果客户端进行了更新操作,Redis会操作两个dictht,先去老数组ht[0]中找,如果没有找到就直接去新数组ht[1]中操作,如果老数组找到了就是在老数组中去操作,同时会把这个hash桶中的数据全都移动到新数组中去。
typedef struct dictht {
dictEntry **table;
unsigned long size; // hash桶个数、数组的长度
unsigned long sizemask; // size-1 计算key在数组中的下标时 hash(key)%2^n == hash(key) & (2^n-1),位运行要快,sizemask存在的意义
unsigned long used; // 已经存在了多少个元素,不是使用hash桶的个数
} dictht;
我们现在知道了dictht
的作用与结构,那么也就知道Key-value其实存储在这其中的,具体就是存储在dictEntry
指针中的,如下所示
dictEntry
代码如下所示,其实就是存储了key、value、next三个元素
typedef struct dictEntry {
void *key; // 这个指针就是指向的一个SDS的对象
union {
void *val; // void *表示一个空指针,可以指向任意的数据类型,实际上指向下面的redisObject对象
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 产生hash冲突后,链表就是靠next实现的
} dictEntry;
val这个指针指向的对象类型可能是String、hash、list、set、zset…,当然Redis中也不是简单的直接用这些类型对象,而是用了redisObject
对象来封装
typedef struct redisObject {
// type当key不存在时根据客户端执行的命令确定value是什么类型,比如set命令->string。
// 当key存在时会约束客户端命令的操作,比如name是一个string类型,但是我却执行lpush name [value...]那么这里直接就抛错了
unsigned type:4;
unsigned encoding:4; // 某个key对应的value,这个value在redis内存中到底是什么类型的编码
unsigned lru:LRU_BITS; // 设置了内存淘汰策略时才会用到
int refcount; // 类似于java的垃圾标记算法——引用计数法,用这个来管理内存回收
void *ptr; // 指向value内存中真实存储的位置
} robj;
RedisDB主体数据结构如下图所示
Key
Redis中所有的key都是String类型的,底层是使用的SDS类型,没有使用c语言的字符数组去实现字符串。因为c语言是以\0
作为字符串结束标识的,redis需要支持各种语言,当数据以stream流的形式传输到Redis-server后可能某个字符串中就包含这个\0
字符。
SDS simple dynamic string
关键特点是:
-
二进制安全的数据结构
它有一个属性指定了当前字符串的长度,然后根据这个长度去读取字符串,而不是根据
\0
作为结束标识 -
提供了内存预分配机制,避免频繁的内存分配
如果我们使用append等命令修改一个字符串时,会去判断当前剩余空间是否足够,如果不足够这则按照
(length + addlen) * 2
去重新分配内存 创建spring对象,当达到了1024*1024后 也就是1M后就会按照每次增加1M去扩容 -
兼容c语言的函数库
会自动的在字符串的结尾添加
\0
去兼容c语言的函数库
SDS
free:6
len:10
ch