Redis底层数据结构
前言
Redis中共有五种常用的数据类型,分别是string、list、set、hash以及zset。但是其底层数据结构是啥样的以及是如何实现的呢?今天我们就深入了解一下Redis中的底层数据结构。
redisObject
当我们使用Redis中各种类型来存储数据时,无论是哪种类型,Redis中都不是直接存储的,而是存储在一个叫做redisObject的数据结构中。不同的版本redisObject结构可能不同,但都具有如下的结构:
typedef struct redisObject {
// 类型 string list set hash zset等 4bit
unsigned type:4;
// 编码方式 4bit
unsigned encoding:4;
// LRU 时间 24bit
unsigned lru:LRU_BITS;
// 引用计数 4byte
int refcount;
// 指向对象的指针 8byte
void *ptr;
} robj;
type 、encoding 和 ptr 是redisObject最重要的三个属性。
type
type字段表示对象的类型,占4个bit,它记录了对象所保存的值的类型。我们可以通过客户端使用type命令来查看对象的类型,如下所示:

type的常见取值如下:
/*
* 对象类型
*/
#define REDIS_STRING 0 // 字符串
#define REDIS_LIST 1 // 列表
#define REDIS_SET 2 // 集合
#define REDIS_ZSET 3 // 有序集
#define REDIS_HASH 4 // 哈希表
encoding
encoding表示对象的内部编码,占4个bit,其实也就是该对象底层到底使用的哪种数据结构来存储。
由于Redis对内存进行了极致的利用,为了节省空间,Redis底层会根据对象大小使用不同的数据结构来存储。通过encoding属性,Redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了Redis的灵活性和效率。例如,常见的string类型,其底层实现就有int、embstr和raw三种编码方式。使用 object encoding key 命令可以查看对象的编码方式,如下所示:

5种对象类型对应的编码方式将在后面细说,这里先留下一个印象。而在redisObject中,encoding的取值有以下这些:
/*
* 对象编码
*/
#define REDIS_ENCODING_RAW 0 // 编码为字符串
#define REDIS_ENCODING_INT 1 // 编码为整数
#define REDIS_ENCODING_HT 2 // 编码为哈希表
#define REDIS_ENCODING_ZIPMAP 3 // 编码为 zipmap(2.6 后不再使用)
#define REDIS_ENCODING_LINKEDLIST 4 // 编码为双端链表
#define REDIS_ENCODING_ZIPLIST 5 // 编码为压缩列表
#define REDIS_ENCODING_INTSET 6 // 编码为整数集合
#define REDIS_ENCODING_SKIPLIST 7 // 编码为跳跃表
ptr
ptr指针指向具体的数据,如 set test hello 命令,ptr指向的就是存储hello的字符串。
lru
lru记录的是对象最后一次被命令程序访问的时间,占用的bit因版本不同而不同,2.6版本22 bit、4.0版本为24 bit。
通过对比lru时间与系统当前时间,可以计算某个对象的在Redis中存在了多长时间,使用object idletime 即可看到(注:单位是秒),如下所示:

object idletime命令的一个特殊之处在于它不改变对象的lru值。另外,说到lru,是不是感觉特别熟悉。没错,Redis的数据淘汰策略中,就有allkeys-lru和volatile-lru两种,当redis.conf中设置了maxmemory选项时,若是Redis内存占用超过maxmemory指定的值时,Redis会优先选择生存时间较长的对象进行释放,从而达到回收内存的目的。
refcount
refcount记录的是该对象被其他引用的次数,类型为整型。refcount的作用主要在于记录对象的引用计数及内存回收。当创建对象时,refcount初始化为1;当有新程序使用该对象时,refcount++;当对象不再被一个程序使用时,refcount--;当refcount变为0时,即代表对象可以被回收,其占用的内存将会被释放。
Redis中被多次使用的对象,即refcount>1的被称为共享对象。Redis为了节省内存,当有一些重复对象出现时,新的程序不会创建新的对象,而是直接使用重复对象,然后使refcount++。需要注意的是,目前共享对象仅仅支持整数值的字符串对象。
之所以只支持整数型共享对象,是出于对内存和CPU的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为O(1);对于普通字符串,判断复杂度为O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2)。
就目前的实现来说,Redis服务器在初始化时,会创建10000个字符串对象,值分别是**09999**之间的整数值;当`Redis`需要使用值为09999的字符串对象时,可以直接使用这些共享对象。10000这个数字可以通过调整redis.conf文件中的OBJ_SHARED_INTEGERS参数来修改。
共享对象的引用次数可以通过 object refcount 命令查看,如下图所示:

命令执行的结果页佐证了只有0~9999之间的整数会作为共享对象。
总结
综上所述,redisObject的结构与对象类型、编码、内存回收、共享对象都有关系,一个redisObject的大小约为16字节:4 bit + 4 bit + 24 bit + 4 byte + 8 byte= 16 byte。


浙公网安备 33010602011771号