Redis 是一个广泛使用的内存数据库,以其高性能和丰富的数据结构而闻名。不同于磁盘数据库,磁盘数据库将数据读取到文件中维护,而内存数据库将数据存储在内存中,意味着其想要维护数据,必须在代码中维护一个保存数据的结构,而redis由c语言编写,对应的其一定是通过结构体来保存数据的结构。
redisDb结构体
在redis源码中,每个redisDb结构体代表着一个数据库,结构体大致如下
typedef struct redisDb {
dict *dict;
dict *expires;
dict *blocking_keys;
dict *ready_keys;
dict *watched_keys;
int id;
long long avg_ttl;
unsigned long expires_cursor;
} redisDb;
让我们来一个一个解释其意义
dict:
数据字典,其中保存了所有存入的数据,在redis中没有表的概念,数据作为键值对存储,直接存入数据库中,以hash表的结构存储,我在讲解redis中五个基本数据类型中详细讲解了hash表的结构,想了解可以看我的这篇文章Redis五种数据类型,底层存储数据结构,以及相关命令。
expire:
这也是一个数据字典,不同的是,其存储的是key和其对应的过期时间(时间戳)。将过期时间单独存储,有利于redis便利key查找过期key(有的key不设过期时间,单独存储可以防止便利这些没有过期时间的key)
blocking_keys:
这仍然是一个数据字典,其中保存的是正在阻塞等待中的key,比如说一个客户端对一个list数据使用了blpop命令,但list没有数据,此时客户端就会进入阻塞状态,等待list插入数据,当多个客户端都对一个空的list使用blpop命令,那么则需要一个结构来维护他们的先后关系,blocking_keys的作用就是维护阻塞key和等待其数据的客户端的先后关系。
ready_keys:
这也是一个数据字典,当blocking_keys中维护的key有新数据插入时(每次插入数据都会检查是否包含在blocking_keys中),会讲对应的key放入ready_keys中,等待当前事件循环中的插入数据操作完成后,在便利ready_key获取key,并且根据这个key在block_keys和dict中获取客户端和值并且发送(高并发情况下,一次会有大量的插入操作,先执行完当前事件循环的插入操作,并且放入ready_keys中,插入操作完成后再统一返回给客户端)。当使用publish发布订阅消息时,订阅消息也会放入ready_keys中,与blpop不同的是,所有订阅这个消息的客户端都会收到这个消息,没有先后顺序一分,所以不需要维护客户端的先后关系,也就不需要进入block_keys。
watched_keys:
当我们使用redis开启一个事务时,我们需要先确定这个事务依赖于哪些key,然后通过watch key1 key2 ... 的命令来监控这些key,那么当前这个事务就会作为这些key的value被保存在watched_keys中,当对key进行修改操作时,会去查看watch_keys中是否有这个key,如果有,则将key对应的全部事务进行取消。并且便利watched_keys将其他key中保存的当前事务删除。
id:
唯一id,没什么好说的。
avg_ttl:
保存expire中保存的过期时间的平均值,每当平均时间改变时都要重新计算一次。
expires_cursor:
当前便利位置的游标,redis内存淘汰过程中需要便利检查key是否过期,不过大量的key一次性遍历势必会造成程序卡顿,为了防止这种情况,在周期性模式下,一次性会检查一部分key,然后保存当前位置作为游标,下次检查时会接着游标的位置继续遍历。
结构体示意图如下
redisObject
我们在redisDB结构体的中dict数据字典中保存的都是key和数据,而数据的结构则是redisObject,同redisDB一样,redisObject也是一个结构体,代码如下
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
};
type:
对象类型,也就是redis五大基本数据类型以及两个高级数据类型(bitmap和hyperloglog底层是string类型,而geo底层是zset类型),其中包括REDIS_STRING,REDIS_LIST,REDIS_SET,REDIS_ZSET,REDIS_HASH,REDIS_MODULE,REDIS_STREAM
encoding:
编码方式,简单来说redis基本数据类型的保存也要精心设计来提高其内部属性的操作性能,因为一个基本数据类型内部也保存了大量的元素或键值对,而编码方式就是某个类型用了什么样的数据结构来保存其内部的键值对或元素,其中包括SDS,整数列表,压缩列表,双向链表,快速列表,hash表,跳表。想了解更多可以看我的另一个文章Redis五种数据类型,底层存储数据结构,以及相关命令
lru:
LFU策略
在 LFU 内存淘汰策略下,lru 字段表示键的访问频率,但并不是记录具体的访问次数。Redis 使用一种近似算法来计算访问频率,以节省内存并仍然能够体现出键的访问频率。具体操作如下:
访问频率计算:
- Redis 使用 LFU 算法时,lru 字段存储的是一个代表访问频率大小的整数(并不是访问次数)。
- 每次访问键时,Redis 会根据一个公式来决定是否增加该计数器的值。这个公式是 1 / (lru现在的值 * lfu_log_factor + 1),其中 lfu_log_factor是用于控制lru增涨速度的系数,默认为 10。
- 根据这个公式计算出的概率值,再生成一个 0 到 1 之间的随机数,如果随机数小于这个概率值,则增加 lru 的值。
通过这种方式,随着lru的增大公式计算的值就会越来愈小,生成随机数小于这个概率值的概率就会越来越小,增加就会越来愈慢,以达到反应当前key的访问频率,并且节省内存的目的。
减少频率:
- 每过一段时间,Redis 会减小 lru 字段的值,默认是每分钟减小一次。这确保了即使频繁访问过的键,如果长时间不再被访问,其频率计数也会逐渐减少。
LRU 策略
在 LRU 内存淘汰策略下,lru 字段记录的是键的最近访问时间的时间戳。Redis 使用一个 24 位的时间戳来表示键的最近访问时间。具体操作如下:
访问时间记录:
- 每次访问键时,Redis 会更新该键的 lru 字段,记录当前的时间戳。
淘汰策略:
- 当内存达到上限,需要淘汰键时,Redis 会选择那些 lru 字段值最小(即最久未被访问)的键进行淘汰。
refcount:
引用计数,用于内存管理。当引用计数为 0 时,表示没有任何地方引用该对象,内存可以被回收。
ptr:
指向实际数据值的指针。实际数据的类型和结构取决于 type
和 encoding
字段的值。比如说如果是string类型,那么就可以指向一个SDS结构体的对象,如果是hash类型,有可能会指向一个hash表等等。