不积跬步无以至千里,一直使用并学习redis,尽管redis 学习的文章很多大佬都写过了,我还是依然要写这样的笔记类型的文章,其一在写博客的过程中,会学习大量该方面的资料,翻阅其他大佬的文章,力求吸收他们的精华,同时加入自己的理解,以此来扩展并加深自己对redis的学习以及理解。其二,通过总结吸收,在加入自己的理解,希望为读到自己文章的同学提供有用的帮助;接下来开始我们的口水话旅程;
还是老生常谈式的介绍一下redis的数据类型,包括:字符串(binary-safe strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets), hyperloglogs 、 地理空间(geospatial)、Streams;不知道有没有注意,字符串写的是"binary-safe strings"(二进制安全字符串),不知道你们知不知道,我最初确实是不知道其中原由0 !0,请看下面对String类型的分解。
1、value能够存放的数据类型:
value能够存放 long、float、string这三种类型的数据,且最多不能超过512M的长度,其中如果一个字符串内容可转为long,那么该字符串会被转化为long类型,对象ptr指向该long,并且对象类型也用int类型表示,这与底层的编码有关,我们一会儿分解;
2、存储原理:
2.1、dicEntry:
redis会将 k,v 均存在一个叫做dictEntry的数据结构中,里面指向了key和value的指针,dicEntry结构:
typedef struct dictEntry {
void *key; //key的指针
union {
void *val; //value的指针
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; //next指针,指向下一个键值对节点
} dictEntry;
对于是key,最多存放512M,且在Redis进行存储时没有直接使用C的字符数组,而是存储在自定义的SDS中。value既不是直接作为字符串存储,也不是直接存储在SDS中,而是存储在redisObject中。实际上五种常用的数据类型的任何一种,都是通过redisObject来存储的。如果我们设置一个 key = k1,value = abc 的数据到redis,那么它的存储方式是:
2.2、redisObject:
刚才分析了dicEntry的结构,接下来分析一下redisObject,redisObject是在数据结构之上包装的一个对象,其目的就在于使Redis 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令并为对象设置不同的实现,从而优化内存或查询速度。其在源码(src/server.h)的结构为:
typedef struct redisObject {
//对象类型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET
unsigned type:4;
//对象底层所使用的编码
unsigned encoding:4;
//对象最后一次被访问的时间,与内存回收有关
unsigned lru:LRU_BITS;
//引用计数。当refcount为0的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了
int refcount;
//指向对象内容的具体存储位置
void *ptr;
} robj;
type:对象类型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET;
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
encoding:对象底层所使用的编码;
lru:REDIS_LRU_BITS:对象最后一次被访问的时间,与内存回收有关;
refcount:引用计数。当refcount为0的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了;
ptr: 指向对象内容的具体存储位置。
查看类型可以利用命令: type key;
2.3、SDS:
讨论了redisObject,那么redis对于String 类型的存放实现的抽象数据结构SDS(simple dynamic string 简单动态字符串)就该登场了,同样,我们看一下在3.2以后的版本中SDS的结构:
typedef char *sds;
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
redis针对长度不同的字符串又分为sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,选取不同的数据类型uint8_t或者uint16_t等来表示长度、一共申请字节的大小等,分别代表2^5=32byte,2^8=256byte,2^16=65536byte=64KB,2^32byte=4GB。结构体中的__attribute__ ((__packed__))
设置是告诉编译器取消字节对齐,则结构体的大小就是按照结构体成员实际大小相加得到的。以sdshdr8为例:
struct__attribute__((__packed__))sdshdr8{
uint8_t len; //当前字符数组的长度
uint8_t alloc; //当前字符数组总共分配的内存大小
unsigned char flags; //当前字符数组的属性、用来标识到底是sdshdr8还是sdshdr16等
char buf[]; //字符串真正的值
};
对于redisObject的ptr指向则可以形象理解为下图的模型:
关于SDS,编码是其另一个核心,其编码分为了三种:int,embstr、raw;
a、int 存储8个字节的长整型(long,2^63-1);
b、embstr 存放长度小于或等于44字节;
c、raw 存放长度大于44字节;
至此,对于String类型的存储原理基本上有了一个大致的认识,接下来,我们思考一些问题。
3、想一想:
3.1、为什么Redis要用SDS实现字符串?
以一个表格再阐述C语言存储字符串与SDS之间的差异:
3.2、embstr和raw同样是字符串编码,除了在存储长度不一样以外,还有什么不同?
3.3、int和embstr与raw的转换规则是怎么样的?
3.4、当长度小于阈值时,会还原吗?