【redis-1】redis底层结构和过期策略

对于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(默认Redis16个数据库)

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 ;
二: RedisObject 结构
 
1、结构概览:
 
redis存储的value是一个对象,存储我们前面说的那些数据类型, 结构如下:
 
typedef struct redisObject {

unsigned type:4//数据类型,占4位 。 查看命令:type ‘key’

unsigned encoding:4;//编码,占4位。 命令: object encoding 'key'

void *ptr;  //指向底层实现数据结构的指针。比如:set hello worldptr 指向包含字符串 world SDS (什么是SDS,后面讲)

int refcount//记录对象被引用的次数,用于引用计数和垃圾回收

unsigned lru:LRU_BITS; //占24位。 高16位记录分钟级别的最后访问时间戳时间戳,低8位存储最近访问次数。 所以这个字段其实代表两种: lru淘汰策略: 使用高16位,,越久没有访问的数据越容易淘汰。   lfu策略: 使用低8位,最近访问次数越少越容易淘汰。 

} robj ;
2、redisObject的7种type:
 
1、字符串类型SDS: Redis 使用了 SDS(Simple Dynamic String-简单动态字符串), 用于存储字符串和整型数据
struct sdshdr {

//记录buf数组中已使用字节的数量

int len;

//记录 buf 数组中未使用字节的数量

int free;

//字节数组,用于保存字符串

char buf[];

}
优点:相对于C语言,由于记录了使用长度和剩余长度,获取字符串长度的时间复杂度是O1(C是On,从头遍历),并且在缓冲区要内存溢出前可自动分配内存。
 
2、跳跃表: sorted-set的底层实现
将一个有序链表分层,每一层也是一个有序链表。 查找某个节点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层首次接第三层抛,则为反面,不需要插入。
 
如上:以此类推,最终得到
 
 
对于删除,找到元素直接删除,调整指针即可。
 
 
3、 字典 dict:
dict又称散列表(包含hash表),除了存储键值对,也是hash对象的底层实现。 在redis中进行CURD其实就是对dict中的数据进行。实现如下:
 
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 ;
 
 
4、 Hash表  dictht
hash表的初始容量为4,每次1倍扩容,索引值=hash值%容量
 
typedef struct dictht {

dictEntry **table; // 哈希表节点数组-存放多个hash表节点

unsigned long size; // 哈希表数组的大小

unsigned long sizemask; // 用于映射位置的掩码,值永远等于(size-1)

unsigned long used; // 哈希表已用节点的数量,包含next单链表数据

} dictht ;
 
 
 
 
5、 Hash表节点   dictEntry:真正存储key-value的结构
typedef struct dictEntry {

void *key; //

union { // 值v的类型可以是以下4种类型

       void *val;

       uint64_t u64;

       int64_t s64;

       double d;

} v;

struct dictEntry *next; // 指向下一个哈希表节点,形成单向链表 解决hash冲突

} dictEntry
 
 
6、快速列表:quicklist-列表list的底层实现
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 ;
 
 
二:缓存过期和淘汰策略
 
官方统计:redis单机读在11w次/s,写在8w次/s。 但是内存满了会造成内存与磁盘交互虚拟内存,频繁io性能下降。
 
不设置过期的场景: 
1、redis使用固定key,不会增加。
2、redis当db使用,要保证数据完整性。
 
我们知道,所有的key-value都存在dict中,在上面介绍的redisDb中还有一个属性: expires   ,存放的是过期key。
redis删除采用: 主动删除+惰性删除
 
惰性删除:  key被访问时,先判断是否失效,失效则删除。
主动删除:  在redis.conf文件中配置主动删除(默认 no-enviction  不删除),内存不足时触发。
# maxmemory-policy no-enviction
 
下面介绍几种主动删除策略:
 
LRU: 推荐
最长时间未使用的优先删除。  常用实现是用链表保存数据,当有新数据插入或数据被访问时,放入链表头部,当内存不足时,从尾部删除。
 
配置方式:
volatile-lru:从已设置过期时间的数据集( server.db[i].expires)中挑选最近最少使用的数据淘汰。  这种情况一般是把redis既当缓存,又做持久化存储的时候才用,不推荐
allkeys-lru( 推荐 ):从数据集( server.db[i].dict)中挑选最近最少使用的数据淘汰。
 
LFU:
最近一段时间,使用次数最少的优先删除。
 
配置方式:
volatile-lfu
allkeys-lfu
 
 
random:不推荐
volatile-random: 从已设置过期时间的数据集( server.db[i].expires )中任意选择数据淘汰
allkeys-random: 从数据集( server.db[i].dict )中任意选择数据淘汰
 
ttl:不推荐
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires )中挑选将要过期的数据淘汰
 
noenviction: 不推荐
不删除,内存不足时候报错,体验不好
 
注意:对于redis当db使用的场景,如字典库。这种时候不需要删除数据,不要设置删除策略。
1 、不设置 maxmemory
2 、使用 noenviction 策略
 
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值