redis的结构图如下:
下面,分别对各个数据结构进行介绍
RedisServer
众所周知,redis的配置文件是redis.conf,redis在启动时会加载配置文件。那么配置文件的配置项都加载在哪里呢?
阅读redis源码,会看到Redis将配置文件的配置数据加载到数据结构redisServer,并将所有的数据库保存在db数组中。根据配置文件中的database,决定要创建多少个数据库dbnum。
struct redisServer {
//其余配置...
// 数据库
redisDb *db;
//数据库的数量
int dbnum;
//其余配置...
};
默认情况下,redis会创建16个数据库。
RedisClient
RedisClient的数据结构如下,可以看到,每个Redis客户端都有自己的目标数据库。db是一个指向了redisDb的指针,记录了客户端当前的目标数据库。当执行读、写等命令时,redis客户端就是通过操作目标数据库来执行这些命令的。
当执行select来切换数据库时,redisClent通过修改db指针,让它指向不同的数据库。
struct redisClient {
//其余配置...
// 客户端正在使用的数据库
redisDb *db;
//其余配置...
};
redisDb
redisDb的数据结构如下。dict(字典)保存了这个数据库所有的键值对,称为键空间。本质上,redis是个k-v数据库服务器。
dict和我们见到的数据库是直接对应的,dict可以理解为是个hashtable,它的key是字符串对象,值就是数据库的值,可以是字符串对象、列表对象、哈希表对象、集合或者有序集合,任意一种redis对象。
typedef struct redisDb {
// 数据库键空间,保存着数据库中的所有键值对
dict *dict;
// 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
dict *expires;
// 正处于阻塞状态的键
dict *blocking_keys;
// 可以解除阻塞的键
dict *ready_keys;
// 正在被 WATCH 命令监视的键
dict *watched_keys;
struct evictionPoolEntry *eviction_pool;
// 数据库号码
int id;
// 数据库的键的平均 TTL ,统计信息
long long avg_ttl;
} redisDb;
dict
字典的数据结构如下所示。
type属性是一个指向dictType结构的指针,每个dictType保存了一簇用于操作特定类型键值对的函数。Redis为用途不同的字典设置了不同的类型特定函数
privdata属性保存了需要传给那些类型特定函数的可选参数
ht属性是包含两个dictht的数组,一般情况下,字典只使用ht[0]哈希表,在rehash过程中,才会使用ht[1]
rehashidx属性记录了rehash的状态,如果为-1,就表示当前没有在进行rehash。如果不是-1,就表示正在rehash的哈希表ht[0]的索引
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx;
// 目前正在运行的安全迭代器的数量
int iterators;
} dict;
Redis添加数据的过程
例如在命令行执行lpush命令,会发生什么呢?
127.0.0.1:6379> lpush mylist 11 22 33 44 55 (integer) 5
不完全准确的添加数据过程:
那么,必然会存在哈希冲突
键冲突当有两个或者两个以上的键被分配到了哈希表的同一个索引上,就称这些键发生了键冲突。
redis使用链地址法解决键冲突
rehash哈希表的负载因子=哈希表已保存的节点数量/哈希表大小
当哈希表保存的键值对过多或者过少时,为了让负载因子维持在合理的范围内,会通过执行rehash操作,来对哈希表进行扩容或者收缩。
扩展的rehash操作:
渐进式rehash 服务器正在执行bgsave或者bgrewriteaof操作的时候,需要进行rehash的判断条件-负载因子更大,是因为在执行bgsave或者bgrewriteaof操作的时候,redis需要创建当前服务器进程的子进程,而大多数操作系统使用了写时复制技术来优化子进程使用效率。所以服务器提高了执行扩展操作所需要的负载因子,从而尽可能避免在子进程存在期间进行哈希表扩展或者收缩操作。
当负载因子<0.1 会自动进行收缩操作
上面说的rehah操作并不是一次性完成的,当哈希表保存的键值对数量过多时,如果一次性rehash的话,可能会导致服务器在这段时间内停止服务。
所以,redis选择分多次、渐进式的将ht][0]的键值对rehash到ht[1]:在rehash期间,每次对字典进行添加、删除、查找、更新操作时,除了执行指定操作外,还会将ht[0]在rehashidx索引上的键值对rehash到ht[1],当ht[0]的所有键值对都rehash到ht[1],将rehashidx置为-1,表示rehash操作已完成
添加数据的流程:
dictht
dictht是字典dict所使用的哈希表,哈希表的table是个数组,每个数组元素都是个指向dictEntry结构的指针,每个dictEntry结构存储了一个键值对。
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已用节点的数量
unsigned long used;
} dictht;
dictEntry
dictEntry是哈希表的节点,每个dictEntry结构都保存着一个键值对,
key保存着键值对中的键,是以sds数据结构存储
v属性保存键值对中的值,它可以是个指针,也可以说unit64_t整数,或者是int64_t整数
next属性是哈希表节点的指针,这个指针将多个哈希值相同的键值链接,来解决键冲突问题
typedef struct dictEntry {
//键
void *key;
//值,同一时间只会用下面几种的一种数据结构
union{
//redisObject
void *val;
//unit64_t整数
unit64_t u64;
//int64_t整数
int64_t s64;
double d;
}v;
//指向下个节点的指针
struct dictEntry *next;
} dictEntry;
redisObject
redis中的每个对象都由一个redisObject结构表示
type属性记录了对象的类型,由操作的命令类型决定。如 set:string ;hset:hash
encoding属性记录了值对象底层实现的数据结构
refcount:引用计数,通过这个属性就可以跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收
lru:记录对象最后一次被程序访问的时间,用于计算键的空转时长。当回收内存算法是volatile-lru或者allkeys-lru时,这些键会优先被服务器释放
typedef struct redisObject {
//类型:string/list/hash/set/zset
unsigned type:4;
//编码 int/embstr/raw/ziplist/quicklist/hashtable/intset/skiplist
unsigned encoding:4;
//内存淘汰算法相关
unsigned lru:LRU_BITS;
//引用计数法
int refcount;
//指向底层数据结构的指针
void *ptr;
} redisObject;
不同类型对应的底层数据结构
string用object encoding命令就可以看到不同类型的实际底层数据结构
每种数据类型根据不同的情况来使用不同的数据结构,从而极大的提升了redis的灵活性与效率,优化某一场景下的效率
四个字符串对象:
127.0.0.1:6379> set intstr 77 OK 127.0.0.1:6379> set doublestr 77.7 OK 127.0.0.1:6379> set shortstr hello OK 127.0.0.1:6379> set longstr aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa OK
分别用type命令查看他们的数据类型:
127.0.0.1:6379> type intstr string 127.0.0.1:6379> type doublestr string 127.0.0.1:6379> type shortstr string 127.0.0.1:6379> type longstr string
再查看他们的底层数据结构:
127.0.0.1:6379> object encoding intstr "int" 127.0.0.1:6379> object encoding doublestr "embstr" 127.0.0.1:6379> object encoding shortstr "embstr" 127.0.0.1:6379> object encoding longstr "raw"
redis保存字符串对象的判断逻辑:
listraw与embstr
embstr编码是保存短字符串的一种优化编码方式,本质上与raw一样,都使用redisObject结构与sdshdr来表示字符串对象
raw编码会调用两次内存分配:创建redisObject结构与sdshdr结构
embstr:调用一次内存分配函数分配一块连续的内存空间,依次包含redisObject与sdshdr
通过这种优化,embstrk只需要调用依次内存分配函数,释放的时候也只需要调用一次。并且因为embstr占用的内存是连续的,所以可以更好的利用缓存带来的优势。
3.2版本以前,redis使用ziplist与linkedlist作为列表对象的编码。但是linkedlist的附加空间太高,因为每个节点都需有prev与next指针(8byte*2),而且每个节点的内存都需要单独分配空间,影响内存管理效率,3.2版本以后,redis使用quicklist代替了ziplist与linkedlist。



