对于redis,我们经常在使用,常用的类型如 string ,list, set ,sortedset, hash等,还有一些不常见的如:位图类型bitmap,地理位置类型geo,数据流类型stream。 具体用法和相关api呢很多,这里我想说的是,redis的底层类型。
一:redis-db的结构:
整体结构:
redis共有16个库,保存在结构 redisServer 的一个成员 redisServer.db 数组中。redisClient中存在一个名叫db的指针指向当前使用的数据库。 redisDB结构如下:
typedef struct redisDb {int id; //id是数据库序号,为0-15(默认Redis有16个数据库)
long avg_ttl; //存储的数据库对象的平均存活时间(time to live),用于统计
dict *dict; //存储数据库所有的key-value,也就是下面要讲的数据字典
dict *expires; //存储key的过期时间
dict *blocking_keys; //blpop 存储阻塞key和操作的客户端对象
dict *ready_keys;//存储阻塞后push的key和操作的客户端对象
dict *watched_keys;//存储watch监控的的key和操作的客户端对象
} redisDb ;
typedef struct redisObject {unsigned type:4; //数据类型,占4位 。 查看命令:type ‘key’
unsigned encoding:4;//编码,占4位。 命令: object encoding 'key'
void *ptr; //指向底层实现数据结构的指针。比如:set hello world,ptr 指向包含字符串 world 的 SDS (什么是SDS,后面讲)。
int refcount; //记录对象被引用的次数,用于引用计数和垃圾回收
unsigned lru:LRU_BITS; //占24位。 高16位记录分钟级别的最后访问时间戳时间戳,低8位存储最近访问次数。 所以这个字段其实代表两种: lru淘汰策略: 使用高16位,,越久没有访问的数据越容易淘汰。 lfu策略: 使用低8位,最近访问次数越少越容易淘汰。
} robj ;
struct sdshdr {//记录buf数组中已使用字节的数量
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
将一个有序链表分层,每一层也是一个有序链表。 查找某个节点A时从最上层开始,当next节点大于A 或 next节点指向null,继续从当前位置的下一层寻找。举例: 查找元素9常规遍历: 遍历8次采用分层:显然,理想层数的跳跃表大大增加了查询效率。 理想层数为底层节点数的1/2,因为这里用了2分查找法,上层的节点数是下面一层的1/2。 那么,再插入和删除节点时,如何保证按1/2动态的改变层数和节点呢。使用抛硬币的方式,一次是正面,下一次必定是反面。正面插入,反面不插入, 按上图第三层的结果推理如下:假如第一层有个元素0:a、插入元素1: 1层肯定要插入(理解为抛硬币正面);2层首次接着1层抛为反面,不需要插入;b、插入元素2:1层肯定要插入,2层第二次抛硬币,是正面(2层上一次抛是反面),插入; 3层首次接着2层抛是反面,不插入。c、插入元素5:1层肯定要插入,2层第三次抛硬币,是反面(2层第二次抛是正面),不需要插入。d、插入元素6,1层要插入,2层第四次抛硬币,是正面(2层第三个是反面),需要插入; 3层第二次抛硬币,正面(上一次是反面),需要插入。 4层首次接第三层抛,则为反面,不需要插入。如上:以此类推,最终得到对于删除,找到元素直接删除,调整指针即可。
typedef struct dict {dictType *type; // 该字典对应的:特定操作函数
void *privdata; // 上述类型函数对应的:可选参数
dictht ht[2]; /* 两张哈希表,存储键值对数据,ht[0]为原生哈希表,ht[1]为rehash表。当ht[0]大小需要动态改变的时候,数据就往ht[1]迁移,清空ht[0]。 下一次又反过来 */
long rehashidx; /*rehash标识 当等于-1时表示没有在 rehash, 否则表示正在进行rehash,该值表示hash表 ht[0]的rehash进行到哪个索引值。
什么叫渐进式 rehash?也就是说扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进式完成的。如果保存在Redis中的键值对只有几个几十个,那么 rehash 操作可以瞬间完成,但是如果键值对有几百万,几千万甚至几亿,那么要一次性的进行 rehash,势必会造成Redis一段时间内不能进行别的操作。所以Redis采用渐进式 rehash,这样在进行渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行 增加操作,一定是在新的哈希表上进行的
int iterators; // 当前运行的迭代器数量
} dict ;
hash表的初始容量为4,每次1倍扩容,索引值=hash值%容量typedef struct dictht {dictEntry **table; // 哈希表节点数组-存放多个hash表节点
unsigned long size; // 哈希表数组的大小
unsigned long sizemask; // 用于映射位置的掩码,值永远等于(size-1)
unsigned long used; // 哈希表已用节点的数量,包含next单链表数据
} dictht ;
typedef struct dictEntry {void *key; // 键
union { // 值v的类型可以是以下4种类型
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 指向下一个哈希表节点,形成单向链表 解决hash冲突
} dictEntry
127 .0.0.1:6379> lpush list:001 1 2 5 4 3(integer) 5
127.0.0.1:6379> object encoding list:001
"quicklist"
typedef struct quicklist {quicklistNode *head; // 指向quicklist的头部
quicklistNode *tail; // 指向quicklist的尾部
unsigned long count; // 列表中所有数据项的个数总和
unsigned int len; // quicklist节点的个数,即ziplist的个数
int fill : 16; // ziplist大小限定,由list-max-ziplist-size给定
(Redis设定)
unsigned int compress : 16; // 节点压缩深度设置,由list-compress-depth给定(Redis
设定)
} quicklist ;
# maxmemory-policy no-enviction下面介绍几种主动删除策略:LRU: 推荐最长时间未使用的优先删除。 常用实现是用链表保存数据,当有新数据插入或数据被访问时,放入链表头部,当内存不足时,从尾部删除。配置方式:volatile-lru:从已设置过期时间的数据集( server.db[i].expires)中挑选最近最少使用的数据淘汰。 这种情况一般是把redis既当缓存,又做持久化存储的时候才用,不推荐allkeys-lru( 推荐 ):从数据集( server.db[i].dict)中挑选最近最少使用的数据淘汰。LFU:最近一段时间,使用次数最少的优先删除。配置方式:volatile-lfuallkeys-lfurandom:不推荐volatile-random: 从已设置过期时间的数据集( server.db[i].expires )中任意选择数据淘汰allkeys-random: 从数据集( server.db[i].dict )中任意选择数据淘汰ttl:不推荐volatile-ttl:从已设置过期时间的数据集(server.db[i].expires )中挑选将要过期的数据淘汰noenviction: 不推荐不删除,内存不足时候报错,体验不好