关于Redis中的字符串对象

一、SDS

redis中定义Object types有5种

/* Object types */
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4

Objects encoding有9种 

#define REDIS_ENCODING_RAW 0     /* Raw representation */
#define REDIS_ENCODING_INT 1 /* Encoded as integer */
#define REDIS_ENCODING_HT 2 /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8 /* Embedded sds string encoding */

关于SDS的结构,在sds.h文件中定义

struct sdshdr {
int len;// 记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
int free;//记录buf数组中未使用字节的数量
char buf[]; // 字节数组,用于保存字符串
};

这个与C中定义有一些区别

1、对于运算或者连接(strcat)等的时候,不需要每次都扩展字符串(空间换时间),这样对内存可以大量的内存分配;还可以避免缓冲区的溢出,

2、strlen计算长度的时候时间复杂度是O(n),而这里直接记录,时间复杂度为1

3、SDS遵循C字符串以空字符结尾(\0)的惯例,遵循这一惯例SDS可以直接重用一部分C字符串函数库里面的函数

二、对象类型

/* A redis object, that is a type able to hold a string / list / set */

/* The actual Redis Object */
#define REDIS_LRU_BITS 24
#define REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1) /* Max value of obj->lru */
#define REDIS_LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
typedef struct redisObject {
unsigned type:4;//(REDIS_STRING,REDIS_LIST,REDIS_HASH,REDIS_SET,REDIS_ZSET)
unsigned encoding:4; // 1
unsigned lru:REDIS_LRU_BITS; //内存紧张的时候使用
int refcount;
void *ptr;//数据指针,例如:10086
} robj;

redis 中定义了 struct redisObject,它是一个简单优秀的数据结构,因为在 redisObject 中数据属性和数据分开来了,其中,数据属性包括数据类型,存储编码方式,淘汰时钟,引用计数

淘汰时钟,redis 对数据集占用内存的大小有「实时」的计算,当超出限额时,会淘汰超时的数据。

引用计数,一个 redis 对象可能被多个指针引用。当需要增加或者减少引用的时候,必须调用相应的函数,程序员必须遵守这一准则

// 增加 redis 对象引用
void incrRefCount(robj *o) {
o->refcount++;
} // 减少 redis 对象引用。特别的,引用为零的时候会销毁对象
void decrRefCount(robj *o) {
if (o->refcount <= 0) redisPanic("decrRefCount against refcount <= 0"); // 如果取消的是最后一个引用,则释放资源
if (o->refcount == 1) {
// 不同数据类型,销毁操作不同
switch(o->type) {
case REDIS_STRING: freeStringObject(o); break;
case REDIS_LIST: freeListObject(o); break;
case REDIS_SET: freeSetObject(o); break;
case REDIS_ZSET: freeZsetObject(o); break;
case REDIS_HASH: freeHashObject(o); break;
default: redisPanic("Unknown object type"); break;
}
zfree(o);
} else {
o->refcount--;
}
}

字符串对象底层数据结构当然可以全部是SDS,但是在某些场景下SDS可能不是最优解

三、字符串对象编码

可以是 int 、 raw 或者 embstr

A、整形

如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成long),并将字符串对象的编码设置为int

相对于SDS优势在于 1、节省内存 2、对于整数值的字符串对象可能会被执行INCR操作,SDS需要先将字符串转成整形,在执行加减操作,再将结果转成字符串保存如果底层保存一个整形变量就不需要做类型转换了

B、字符串值

如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于 39 字节, 那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值, 并将对象的编码设置为 raw

如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于 39 字节, 那么字符串对象将使用 embstr 编码的方式来保存这个字符串值

embstr与sdshdr区别

embstr 编码是专门用于保存短字符串的一种优化编码方式, 这种编码和 raw 编码一样, 都使用 redisObject 结构和 sdshdr 结构来表示字符串对象

raw 编码会调用两次内存分配函数来分别创建 redisObject 结构和 sdshdr 结构, 而 embstr 编码则通过调用一次内存分配函数来分配一块连续的空间, 空间中依次包含 redisObject 和 sdshdr 两个结构、

embstr 有以下好处

  • embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次

  • 释放 embstr 编码的字符串对象只需要调用一次内存释放函数, 而释放 raw 编码的字符串对象需要调用两次内存释放函数

  • 因为 embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面, 所以这种编码的字符串对象比起 raw 编码的字符串对象能够更好地利用缓存带来的优势

四、编码的转换

int 编码的字符串对象和 embstr 编码的字符串对象在条件满足的情况下, 会被转换为 raw 编码的字符串对象。

对于 int 编码的字符串对象来说, 如果我们向对象执行了一些命令, 使得这个对象保存的不再是整数值, 而是一个字符串值, 那么字符串对象的编码将从 int 变为 raw 。

在下面的示例中, 我们通过 APPEND 命令, 向一个保存整数值的字符串对象追加了一个字符串值, 因为追加操作只能对字符串值执行, 所以程序会先将之前保存的整数值 10086 转换为字符串值 "10086" , 然后再执行追加操作, 操作的执行结果就是一个 raw 编码的、保存了字符串值的字符串对象:

redis> SET number 10086
OK redis> OBJECT ENCODING number
"int" redis> APPEND number " is a good number!"
(integer) 23 redis> GET number
"10086 is a good number!" redis> OBJECT ENCODING number
"raw"

另外, 因为 Redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序 (只有 int 编码的字符串对象和 raw 编码的字符串对象有这些程序), 所以 embstr 编码的字符串对象实际上是只读的: 当我们对 embstr 编码的字符串对象执行任何修改命令时, 程序会先将对象的编码从 embstr 转换成 raw , 然后再执行修改命令; 因为这个原因, embstr 编码的字符串对象在执行修改命令之后, 总会变成一个 raw 编码的字符串对象。

以下代码展示了一个 embstr 编码的字符串对象在执行 APPEND 命令之后, 对象的编码从 embstr 变为 raw 的例子:

redis> SET msg "hello world"
OK redis> OBJECT ENCODING msg
"embstr" redis> APPEND msg " again!"
(integer) 18 redis> OBJECT ENCODING msg
"raw"

五、字符串命令的实现

因为字符串键的值为字符串对象, 所以用于字符串键的所有命令都是针对字符串对象来构建的, 表 8-7 列举了其中一部分字符串命令, 以及这些命令在不同编码的字符串对象下的实现方法

命令         int 编码的实现方法 embstr 编码的实现方法 raw 编码的实现方法
SET 使用 int 编码保存值。 使用 embstr 编码保存值。 使用 raw 编码保存值。
GET 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 然后向客户端返回这个字符串值。 直接向客户端返回字符串值。 直接向客户端返回字符串值。
APPEND 将对象转换成 raw 编码, 然后按raw 编码的方式执行此操作。 将对象转换成 raw 编码, 然后按raw 编码的方式执行此操作。 调用 sdscatlen 函数, 将给定字符串追加到现有字符串的末尾。
INCRBYFLOAT 取出整数值并将其转换成 longdouble 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 取出字符串值并尝试将其转换成long double 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数, 那么向客户端返回一个错误。 取出字符串值并尝试将其转换成 longdouble 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数, 那么向客户端返回一个错误。
INCRBY 对整数值进行加法计算, 得出的计算结果会作为整数被保存起来。 embstr 编码不能执行此命令, 向客户端返回一个错误。 raw 编码不能执行此命令, 向客户端返回一个错误。
DECRBY 对整数值进行减法计算, 得出的计算结果会作为整数被保存起来。 embstr 编码不能执行此命令, 向客户端返回一个错误。 raw 编码不能执行此命令, 向客户端返回一个错误。
STRLEN 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 计算并返回这个字符串值的长度。 调用 sdslen 函数, 返回字符串的长度。 调用 sdslen 函数, 返回字符串的长度。
SETRANGE 将对象转换成 raw 编码, 然后按raw 编码的方式执行此命令。 将对象转换成 raw 编码, 然后按raw 编码的方式执行此命令。 将字符串特定索引上的值设置为给定的字符。
GETRANGE 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 然后取出并返回字符串指定索引上的字符。 直接取出并返回字符串指定索引上的字符。 直接取出并返回字符串指定索引上的字符。

六、embstr为什么是39 

这要从内存分配说起了,embstr是一块连续的内存区域,由redisObject和sdshdr组成

使用info查看,redis的内存分配方式为:mem_allocator:jemalloc-3.6.0(redis 2.4以后),这个比glibc的malloc要好不少,节省内存。在这里可以简单理解,jemalloc会分配8,16,32,64等字节的内存

再看一下结构体

typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};

redisObject 占(4+4+24)/4+4+4 = 16 字节,sdshdr为8+1 +一个不固定的值(这里的1是指\0),那么64-25 = 39 ,这样39就来了

上一篇:Linux iptables:规则原理和基础


下一篇:VBS基础篇 - 对象(6) - Folder对象