Redis是一种高性能的开源内存数据存储系统,它支持多种不同类型的键值对。这些键值对的实现是基于一系列底层的数据结构。Redis底层数据结构包括字符串、双端链表、字典、压缩列表、整数集合等,但是Redis为了加快读写速度,并没有直接使用这些数据结构,而是在此基础上又包装了一层称之为RedisObject,RedisObject 有五种对象:字符串(String)、列表(List)、集合(Set)、有序集合(Sorted Set)和哈希(Hash)。
RedisObject是 Redis 的对象系统的核心,它是所有数据类型的最外层的一层结构定义。任意数据类型的键和值都会被封装为一个RedisObject。
RedisObject底层设计
typedef struct redisObject {
// 类型
unsigned type:4个bit;
// 编码
unsigned encoding:4个bit;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* LRU_BITS为24bit*/
//引用计数
int refcount;4个字节
// 指向实际值的指针
void *ptr;8个字节
} robj;
下面分别解释一下各个字段的含义:
- type 记录了对象的类型占4个bit位,目前Redis支持的对象类型如下:
类型常量 | 常量对应的对象类型 |
OBJ_STRING | 字符串(String) |
OBJ_LIST | 列表(List) |
OBJ_SET | 集合(Set) |
OBJ_ZSET | 有序集合(Sorted Set) |
OBJ_HASH | 哈希(Hash) |
- ptr 指向对象的底层实现数据结构,即为具体对象值,占8个字节,这个后面会具体分析。
- encoding表示 ptr 指向的具体数据结构的编码方式,占4个bit位,Redis支持的编码如下:
编码常量 | 编码所对应的底展数据结构 |
OBJ_ENCODING_INT | long 类型的整数 |
OBJ_ENCODING_EMBSTR | embstr编码的简单动态字符串 |
OBJ_ENCODING_RAW | 简单动态字符申 |
OBJ_ENCODING_HT | 字典 |
OBJ_ENCODING_LINKEDLIST | 双端链表,Redis3.2之前使用 |
OBJ_ENCODING_ZIPLIST | 压缩列表 |
OBJ_ENCODING_QUICKLIST | 双端链表与压缩列表结合,Redis3.2之后使用 |
OBJ_ENCODING_INTSET | 整数集合 |
OBJ_ENCODING_SKIPLIST | 跳跃表 |
OBJ_ENCODING_ZIPMAP | 压缩Map,Redis2.6之前使用 |
OBJ_ENCODING_STREAM | Stream流 |
上面提到的type 用于标识 String、Hash、List、Set、Sorted Set五种数据类型、encoding 用于标识底层数据结构。通过这两个字段的组合,同一种数据类型也有多种实现方式,一个完整的映射关系如下表:
类型type | 编码encoding | 描述 |
OBJ_STRING | OBJ_ENCODING_INT | 整数实现字符串对象 |
OBJ_STRING | OBJ_ENCODING_EMBSTR | embstr编码实现字符串对象 |
OBJ_STRING | OBJ_ENCODING_RAW | sds实现字符串对象 |
OBJ_LIST | OBJ_ENCODING_LINKEDLIST | 双端链表实现列表对象 |
OBJ_LIST | OBJ_ENCODING_ZIPLIST | 压缩链表实现列表对象 |
OBJ_LIST | OBJ_ENCODING_QUICKLIST | 双端链表+压缩链表相结合 |
OBJ_LIST | OBJ_ENCODING_LISTPACK | 紧凑链表取代了压缩链表 |
OBJ_SET | OBJ_ENCODING_INTSET | 整数集合实现集合对象 |
OBJ_SET | OBJ_ENCODING_HT | 字典实现集合对象 |
OBJ_ZSET | OBJ_ENCODING_ZIPLIST | 压缩链表实现有序集合对象 |
OBJ_ZSET | OBJ_ENCODING_SKIPLIST | 跳跃表实现有序集合对象 |
OBJ_HASH | OBJ_ENCODING_ZIPLIST | 压缩表实现Hash对象 |
OBJ_HASH | OBJ_ENCODING_HT | 字典实现Hash对象 |
- lru:表示该对象最后一次被访问的时间,其占用24个bit位。便于判断空闲时间太久的key。
- refcount 表示引用计数,占四个字节,由于 C 语言并不具备内存回收功能,Redis 在自己的对象系统中添加了这个属性,当一个对象的引用计数为0时,则表示该对象已经不被任何对象引用,则可以进行垃圾回收了。
refcount:记录了对象被引用的次数。在Redis中,每个数据对象都有一个引用计数器。
- 当这个数据对象被新的键值对引用时,其引用计数就会增加1。
- 当这个数据对象被删除或者没有任何键值对引用时,其引用计数就会减少1。
- 当引用计数减少到0时,Redis就会自动释放这个数据对象所占用的内存空间。
QA扩展一下:Java中由于引用计数法解决不了循环引用的问题,所以 Java 中使用了可达性分析算法,那么 Redis 有没有考虑循环引用的问题呢?
由于C语言跟贴近操作系统,直接跟操作系统交互,命令执行响应比较快,所以Redis选择C语言进行编写可以提高性能,但是C 语言不具备自动回收内存功能,于是乎Redis自己构建了一个内存回收机制。
创建一个新对象,redisObject 对象中的refcount属性就会加1,对象被一个新程序使用,调用incrRefCount函数进行加 1,如果有对象不再被应用程序使用了,那么它就会调用decrRefCount函数进行减 1,当对象的引用计数值为 0 的时候,那么这个对象所占用的内存就会被释放。
从这里可以看出来,这其实就是Java虚拟机中引用计数的内存回收机制,在Java中这种回收机制不被使用,因为它不能解决循环引用的问题。
循环引用举例:A引用B,B引用C,C引用A。
Redis通过在配置文件中修改相关的配置,来达到解决循环引用的问题,在Redis的配置文件里,Windows的配置文件是redis.windows.conf,Linux系统的配置文件是redis.conf。
在配置文件中有一个配置:maxmemory-policy,当内存使用达到最大值时,redis使用的清楚策略,默认配置是noeviction
- volatile-lru 删除已有的过期时间的key
- allkeys-lru 删除所有的key
- volatile-random 已有过期时间的key 随机删除
- allkeys-random 随机删除key
- volatile-ttl 删除即将过期的key
- noeviction 不删除任何key,只是返回一个写错误,这个是默认选项 对于整数值的字符串对象(例如:1,2,3这种的)可实现内存共享。